forked from
stavola.xyz/mlf
A human-friendly DSL for ATProto Lexicons
1use mlf_lang::ast::*;
2use mlf_lang::Workspace;
3use serde_json::{json, Map, Value};
4use std::collections::HashMap;
5
6// Re-export inventory for macros
7#[doc(hidden)]
8pub use inventory;
9
10// Plugin system for code generators
11pub mod plugin {
12 use super::*;
13
14 /// Context passed to code generators
15 pub struct GeneratorContext<'a> {
16 pub namespace: &'a str,
17 pub lexicon: &'a Lexicon,
18 pub workspace: &'a Workspace,
19 }
20
21 /// Trait for code generators
22 pub trait CodeGenerator: Send + Sync {
23 /// Unique identifier for this generator (e.g., "typescript", "python", "rust")
24 fn name(&self) -> &'static str;
25
26 /// Human-readable description
27 fn description(&self) -> &'static str;
28
29 /// File extension for generated files (e.g., ".ts", ".py", ".rs")
30 fn file_extension(&self) -> &'static str;
31
32 /// Generate code from a lexicon
33 fn generate(&self, ctx: &GeneratorContext) -> Result<String, String>;
34 }
35
36 // Registry of code generators using inventory
37 inventory::collect!(&'static dyn CodeGenerator);
38
39 /// Get all registered generators as a Vec
40 pub fn generators() -> Vec<&'static dyn CodeGenerator> {
41 let mut result = Vec::new();
42 for generator in inventory::iter::<&'static dyn CodeGenerator> {
43 result.push(*generator);
44 }
45 result
46 }
47
48 /// Find a generator by name
49 pub fn find_generator(name: &str) -> Option<&'static dyn CodeGenerator> {
50 for generator in inventory::iter::<&'static dyn CodeGenerator> {
51 if generator.name() == name {
52 return Some(*generator);
53 }
54 }
55 None
56 }
57
58 /// Macro to easily register a generator
59 #[macro_export]
60 macro_rules! register_generator {
61 ($generator:expr) => {
62 $crate::inventory::submit! {
63 &$generator as &'static dyn $crate::plugin::CodeGenerator
64 }
65 };
66 }
67}
68
69pub use plugin::{CodeGenerator, GeneratorContext};
70
71fn has_main_annotation(annotations: &[Annotation]) -> bool {
72 annotations.iter().any(|ann| ann.name.name == "main")
73}
74
75fn get_annotation_string_value(annotations: &[Annotation], name: &str) -> Option<String> {
76 annotations.iter()
77 .find(|ann| ann.name.name == name)
78 .and_then(|ann| {
79 // Get first positional argument if it exists
80 ann.args.first().and_then(|arg| {
81 match arg {
82 AnnotationArg::Positional(AnnotationValue::String(s)) => Some(s.clone()),
83 _ => None,
84 }
85 })
86 })
87}
88
89fn get_encoding_annotation(annotations: &[Annotation], param_name: &str) -> Option<String> {
90 annotations.iter()
91 .find(|ann| ann.name.name == "encoding")
92 .and_then(|ann| {
93 // First check for named argument matching param_name
94 for arg in &ann.args {
95 if let AnnotationArg::Named { name, value } = arg {
96 if name.name == param_name {
97 if let AnnotationValue::String(s) = value {
98 return Some(s.clone());
99 }
100 }
101 }
102 }
103
104 // Fall back to positional argument (applies to both input and output)
105 for arg in &ann.args {
106 if let AnnotationArg::Positional(AnnotationValue::String(s)) = arg {
107 return Some(s.clone());
108 }
109 }
110
111 None
112 })
113}
114
115pub fn generate_lexicon(namespace: &str, lexicon: &Lexicon, workspace: &Workspace) -> Value {
116 let usage_counts = analyze_type_usage(lexicon);
117
118 // Extract the last segment of the namespace to determine main
119 let namespace_parts: Vec<&str> = namespace.split('.').collect();
120 let expected_main_name = namespace_parts.last().copied().unwrap_or("");
121 let is_defs_namespace = expected_main_name == "defs";
122
123 // Count main-eligible items (records, queries, procedures, subscriptions, def types) without @main
124 let main_eligible_items: Vec<&Item> = lexicon.items.iter()
125 .filter(|item| {
126 matches!(item, Item::Record(_) | Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) | Item::DefType(_))
127 })
128 .collect();
129
130 let main_eligible_count = main_eligible_items.len();
131
132 // Check if any item has @main annotation
133 let has_explicit_main = main_eligible_items.iter().any(|item| {
134 match item {
135 Item::Record(r) => has_main_annotation(&r.annotations),
136 Item::Query(q) => has_main_annotation(&q.annotations),
137 Item::Procedure(p) => has_main_annotation(&p.annotations),
138 Item::Subscription(s) => has_main_annotation(&s.annotations),
139 Item::DefType(d) => has_main_annotation(&d.annotations),
140 _ => false,
141 }
142 });
143
144 let mut defs = Map::new();
145
146 for item in &lexicon.items {
147 match item {
148 Item::Record(record) => {
149 let record_json = generate_record_json(record, &usage_counts, workspace, namespace);
150
151 // Check if this should be main
152 let is_main = if has_explicit_main {
153 // If @main is used explicitly, only that item is main
154 has_main_annotation(&record.annotations)
155 } else {
156 // Otherwise use heuristics: single item or name matches namespace
157 main_eligible_count == 1 || (!is_defs_namespace && record.name.name == expected_main_name)
158 };
159
160 if is_main {
161 defs.insert("main".to_string(), record_json);
162 } else {
163 defs.insert(record.name.name.clone(), record_json);
164 }
165 }
166 Item::Query(query) => {
167 let query_json = generate_query_json(query, &usage_counts, workspace, namespace);
168
169 let is_main = if has_explicit_main {
170 has_main_annotation(&query.annotations)
171 } else {
172 main_eligible_count == 1 || (!is_defs_namespace && query.name.name == expected_main_name)
173 };
174
175 if is_main {
176 defs.insert("main".to_string(), query_json);
177 } else {
178 defs.insert(query.name.name.clone(), query_json);
179 }
180 }
181 Item::Procedure(procedure) => {
182 let procedure_json = generate_procedure_json(procedure, &usage_counts, workspace, namespace);
183
184 let is_main = if has_explicit_main {
185 has_main_annotation(&procedure.annotations)
186 } else {
187 main_eligible_count == 1 || (!is_defs_namespace && procedure.name.name == expected_main_name)
188 };
189
190 if is_main {
191 defs.insert("main".to_string(), procedure_json);
192 } else {
193 defs.insert(procedure.name.name.clone(), procedure_json);
194 }
195 }
196 Item::Subscription(subscription) => {
197 let subscription_json = generate_subscription_json(subscription, &usage_counts, workspace, namespace);
198
199 let is_main = if has_explicit_main {
200 has_main_annotation(&subscription.annotations)
201 } else {
202 main_eligible_count == 1 || (!is_defs_namespace && subscription.name.name == expected_main_name)
203 };
204
205 if is_main {
206 defs.insert("main".to_string(), subscription_json);
207 } else {
208 defs.insert(subscription.name.name.clone(), subscription_json);
209 }
210 }
211 Item::DefType(def_type) => {
212 let def_type_json = generate_def_type_json(def_type, &usage_counts, workspace, namespace);
213
214 // Check if this should be main
215 let is_main = if has_explicit_main {
216 has_main_annotation(&def_type.annotations)
217 } else {
218 main_eligible_count == 1 || (!is_defs_namespace && def_type.name.name == expected_main_name)
219 };
220
221 if is_main {
222 defs.insert("main".to_string(), def_type_json);
223 } else {
224 defs.insert(def_type.name.name.clone(), def_type_json);
225 }
226 }
227 Item::InlineType(_) => {
228 // Inline types are never added to defs - they expand at point of use
229 // TODO: inline expansion will be handled by workspace/cross-file resolution
230 }
231 Item::Token(token) => {
232 let token_json = json!({
233 "type": "token",
234 "description": extract_docs(&token.docs)
235 });
236 defs.insert(token.name.name.clone(), token_json);
237 }
238 _ => {}
239 }
240 }
241
242 let mut root = Map::new();
243 root.insert("$type".to_string(), json!("com.atproto.lexicon.schema"));
244 root.insert("lexicon".to_string(), json!(1));
245 root.insert("id".to_string(), json!(namespace));
246 root.insert("defs".to_string(), json!(defs));
247 Value::Object(root)
248}
249
250fn analyze_type_usage(lexicon: &Lexicon) -> HashMap<String, usize> {
251 let mut usage_counts = HashMap::new();
252
253 for item in &lexicon.items {
254 match item {
255 Item::Record(record) => {
256 for field in &record.fields {
257 count_type_references(&field.ty, &mut usage_counts);
258 }
259 }
260 Item::Query(query) => {
261 for param in &query.params {
262 count_type_references(¶m.ty, &mut usage_counts);
263 }
264 match &query.returns {
265 ReturnType::None { .. } => {}
266 ReturnType::Type(ty) => count_type_references(ty, &mut usage_counts),
267 ReturnType::TypeWithErrors { success, .. } => {
268 count_type_references(success, &mut usage_counts)
269 }
270 }
271 }
272 Item::Procedure(procedure) => {
273 for param in &procedure.params {
274 count_type_references(¶m.ty, &mut usage_counts);
275 }
276 match &procedure.returns {
277 ReturnType::None { .. } => {}
278 ReturnType::Type(ty) => count_type_references(ty, &mut usage_counts),
279 ReturnType::TypeWithErrors { success, .. } => {
280 count_type_references(success, &mut usage_counts)
281 }
282 }
283 }
284 Item::Subscription(subscription) => {
285 for param in &subscription.params {
286 count_type_references(¶m.ty, &mut usage_counts);
287 }
288 if let Some(messages) = &subscription.messages {
289 count_type_references(messages, &mut usage_counts);
290 }
291 }
292 Item::InlineType(inline_type) => {
293 count_type_references(&inline_type.ty, &mut usage_counts);
294 }
295 Item::DefType(def_type) => {
296 count_type_references(&def_type.ty, &mut usage_counts);
297 }
298 _ => {}
299 }
300 }
301
302 usage_counts
303}
304
305fn count_type_references(ty: &Type, counts: &mut HashMap<String, usize>) {
306 match ty {
307 Type::Reference { path, .. } => {
308 if path.segments.len() == 1 {
309 let name = &path.segments[0].name;
310 *counts.entry(name.clone()).or_insert(0) += 1;
311 }
312 }
313 Type::Array { inner, .. } => count_type_references(inner, counts),
314 Type::Union { types, .. } => {
315 for t in types {
316 count_type_references(t, counts);
317 }
318 }
319 Type::Object { fields, .. } => {
320 for field in fields {
321 count_type_references(&field.ty, counts);
322 }
323 }
324 Type::Parenthesized { inner, .. } => count_type_references(inner, counts),
325 Type::Constrained { base, .. } => count_type_references(base, counts),
326 _ => {}
327 }
328}
329
330fn extract_docs(docs: &[DocComment]) -> String {
331 docs.iter()
332 .map(|d| d.text.trim())
333 .collect::<Vec<_>>()
334 .join("\n")
335}
336
337fn generate_record_json(record: &Record, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
338 let mut required = Vec::new();
339 let mut properties = Map::new();
340
341 for field in &record.fields {
342 if !field.optional {
343 required.push(field.name.name.clone());
344 }
345
346 let mut field_json = generate_type_json(&field.ty, usage_counts, workspace, current_namespace);
347 // Add description if the field has doc comments
348 if !field.docs.is_empty() {
349 if let Some(obj) = field_json.as_object_mut() {
350 obj.insert("description".to_string(), json!(extract_docs(&field.docs)));
351 }
352 }
353 properties.insert(field.name.name.clone(), field_json);
354 }
355
356 let record_obj = json!({
357 "type": "object",
358 "required": required,
359 "properties": properties
360 });
361
362 // Check for @key annotation, default to "tid"
363 let key = get_annotation_string_value(&record.annotations, "key").unwrap_or_else(|| "tid".to_string());
364
365 json!({
366 "type": "record",
367 "description": extract_docs(&record.docs),
368 "key": key,
369 "record": record_obj
370 })
371}
372
373fn generate_query_json(query: &Query, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
374 let mut params_properties = Map::new();
375 let mut params_required = Vec::new();
376
377 for param in &query.params {
378 if !param.optional {
379 params_required.push(param.name.name.clone());
380 }
381 let mut param_json = generate_type_json(¶m.ty, usage_counts, workspace, current_namespace);
382 // Add description if the parameter has doc comments
383 if !param.docs.is_empty() {
384 if let Some(obj) = param_json.as_object_mut() {
385 obj.insert("description".to_string(), json!(extract_docs(¶m.docs)));
386 }
387 }
388 params_properties.insert(param.name.name.clone(), param_json);
389 }
390
391 let params = if !params_properties.is_empty() {
392 let mut params_obj = Map::new();
393 params_obj.insert("type".to_string(), json!("params"));
394 params_obj.insert("required".to_string(), json!(params_required));
395 params_obj.insert("properties".to_string(), json!(params_properties));
396 Value::Object(params_obj)
397 } else {
398 let mut params_obj = Map::new();
399 params_obj.insert("type".to_string(), json!("params"));
400 params_obj.insert("properties".to_string(), json!({}));
401 Value::Object(params_obj)
402 };
403
404 // Check for @encoding annotation (output only for queries), default to "application/json"
405 let output_encoding = get_encoding_annotation(&query.annotations, "output")
406 .unwrap_or_else(|| "application/json".to_string());
407
408 let (output, errors) = match &query.returns {
409 ReturnType::None { .. } => (None, None),
410 ReturnType::Type(ty) => {
411 let mut output_obj = Map::new();
412 output_obj.insert("encoding".to_string(), json!(output_encoding));
413 output_obj.insert("schema".to_string(), generate_type_json(ty, usage_counts, workspace, current_namespace));
414 (Some(Value::Object(output_obj)), None)
415 }
416 ReturnType::TypeWithErrors { success, errors, .. } => {
417 let mut error_array = Vec::new();
418 for error in errors {
419 let error_docs = extract_docs(&error.docs);
420 let error_obj = if error_docs.is_empty() {
421 json!({ "name": error.name.name.clone() })
422 } else {
423 json!({
424 "name": error.name.name.clone(),
425 "description": error_docs
426 })
427 };
428 error_array.push(error_obj);
429 }
430
431 let mut output_obj = Map::new();
432 output_obj.insert("encoding".to_string(), json!(output_encoding));
433 output_obj.insert("schema".to_string(), generate_type_json(success, usage_counts, workspace, current_namespace));
434 (Some(Value::Object(output_obj)), Some(Value::Array(error_array)))
435 }
436 };
437
438 let mut query_obj = Map::new();
439 query_obj.insert("type".to_string(), json!("query"));
440 query_obj.insert("description".to_string(), json!(extract_docs(&query.docs)));
441 query_obj.insert("parameters".to_string(), params);
442 if let Some(output_val) = output {
443 query_obj.insert("output".to_string(), output_val);
444 }
445 if let Some(errors_val) = errors {
446 query_obj.insert("errors".to_string(), errors_val);
447 }
448 Value::Object(query_obj)
449}
450
451fn generate_procedure_json(procedure: &Procedure, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
452 let mut params_properties = Map::new();
453 let mut params_required = Vec::new();
454
455 for param in &procedure.params {
456 if !param.optional {
457 params_required.push(param.name.name.clone());
458 }
459 let mut param_json = generate_type_json(¶m.ty, usage_counts, workspace, current_namespace);
460 // Add description if the parameter has doc comments
461 if !param.docs.is_empty() {
462 if let Some(obj) = param_json.as_object_mut() {
463 obj.insert("description".to_string(), json!(extract_docs(¶m.docs)));
464 }
465 }
466 params_properties.insert(param.name.name.clone(), param_json);
467 }
468
469 // Check for @encoding annotation with "input" parameter, default to "application/json"
470 let input_encoding = get_encoding_annotation(&procedure.annotations, "input")
471 .unwrap_or_else(|| "application/json".to_string());
472
473 let input = if !params_properties.is_empty() {
474 let mut schema_obj = Map::new();
475 schema_obj.insert("type".to_string(), json!("object"));
476 schema_obj.insert("required".to_string(), json!(params_required));
477 schema_obj.insert("properties".to_string(), json!(params_properties));
478
479 let mut input_obj = Map::new();
480 input_obj.insert("encoding".to_string(), json!(input_encoding));
481 input_obj.insert("schema".to_string(), Value::Object(schema_obj));
482 Some(Value::Object(input_obj))
483 } else {
484 None
485 };
486
487 // Check for @encoding annotation with "output" parameter, default to "application/json"
488 let output_encoding = get_encoding_annotation(&procedure.annotations, "output")
489 .unwrap_or_else(|| "application/json".to_string());
490
491 let (output, errors) = match &procedure.returns {
492 ReturnType::None { .. } => (None, None),
493 ReturnType::Type(ty) => {
494 let mut output_obj = Map::new();
495 output_obj.insert("encoding".to_string(), json!(output_encoding));
496 output_obj.insert("schema".to_string(), generate_type_json(ty, usage_counts, workspace, current_namespace));
497 (Some(Value::Object(output_obj)), None)
498 }
499 ReturnType::TypeWithErrors { success, errors, .. } => {
500 let mut error_array = Vec::new();
501 for error in errors {
502 let error_docs = extract_docs(&error.docs);
503 let error_obj = if error_docs.is_empty() {
504 json!({ "name": error.name.name.clone() })
505 } else {
506 json!({
507 "name": error.name.name.clone(),
508 "description": error_docs
509 })
510 };
511 error_array.push(error_obj);
512 }
513
514 let mut output_obj = Map::new();
515 output_obj.insert("encoding".to_string(), json!(output_encoding));
516 output_obj.insert("schema".to_string(), generate_type_json(success, usage_counts, workspace, current_namespace));
517 (Some(Value::Object(output_obj)), Some(Value::Array(error_array)))
518 }
519 };
520
521 let mut result = Map::new();
522 result.insert("type".to_string(), json!("procedure"));
523 result.insert("description".to_string(), json!(extract_docs(&procedure.docs)));
524 if let Some(input_val) = input {
525 result.insert("input".to_string(), input_val);
526 }
527 if let Some(output_val) = output {
528 result.insert("output".to_string(), output_val);
529 }
530 if let Some(errors_val) = errors {
531 result.insert("errors".to_string(), errors_val);
532 }
533 Value::Object(result)
534}
535
536fn generate_subscription_json(
537 subscription: &Subscription,
538 usage_counts: &HashMap<String, usize>,
539 workspace: &Workspace,
540 current_namespace: &str,
541) -> Value {
542 let mut params_properties = Map::new();
543 let mut params_required = Vec::new();
544
545 for param in &subscription.params {
546 if !param.optional {
547 params_required.push(param.name.name.clone());
548 }
549 let mut param_json = generate_type_json(¶m.ty, usage_counts, workspace, current_namespace);
550 // Add description if the parameter has doc comments
551 if !param.docs.is_empty() {
552 if let Some(obj) = param_json.as_object_mut() {
553 obj.insert("description".to_string(), json!(extract_docs(¶m.docs)));
554 }
555 }
556 params_properties.insert(param.name.name.clone(), param_json);
557 }
558
559 let parameters = if !params_properties.is_empty() {
560 json!({
561 "type": "params",
562 "required": params_required,
563 "properties": params_properties
564 })
565 } else {
566 Value::Null
567 };
568
569 let mut result = json!({
570 "type": "subscription",
571 "description": extract_docs(&subscription.docs)
572 });
573
574 if let Some(messages) = &subscription.messages {
575 let message = json!({
576 "schema": generate_type_json(messages, usage_counts, workspace, current_namespace)
577 });
578 result["message"] = message;
579 }
580
581 if !parameters.is_null() {
582 result["parameters"] = parameters;
583 }
584
585 result
586}
587
588fn generate_def_type_json(def_type: &DefType, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
589 generate_type_json(&def_type.ty, usage_counts, workspace, current_namespace)
590}
591
592fn generate_type_json(ty: &Type, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value {
593 match ty {
594 Type::Primitive { kind, .. } => generate_primitive_json(*kind),
595 Type::Reference { path, .. } => {
596 // Try to resolve this reference in the workspace
597 if let Some(resolved_ty) = workspace.resolve_type_reference(path) {
598 // Check if this is an inline type by looking in the workspace
599 if workspace.is_inline_type(path) {
600 // Inline type: expand it recursively
601 return generate_type_json(&resolved_ty, usage_counts, workspace, current_namespace);
602 }
603 }
604
605 // Not an inline type (or couldn't resolve) - generate a ref
606 // First, try to get the fully resolved namespace for this type
607 if let Some(full_namespace) = workspace.resolve_reference_namespace(path, current_namespace) {
608 // We have the full namespace where this type is defined
609 if full_namespace == current_namespace {
610 // It's in the current namespace - use local reference
611 let type_name = path.segments.last().unwrap().name.as_str();
612 json!({
613 "type": "ref",
614 "ref": format!("#{}", type_name)
615 })
616 } else {
617 // It's in a different namespace - use full reference
618 let type_name = path.segments.last().unwrap().name.as_str();
619 json!({
620 "type": "ref",
621 "ref": format!("{}#{}", full_namespace, type_name)
622 })
623 }
624 } else if path.segments.len() == 1 {
625 // Couldn't resolve namespace - check if it's an imported type
626 let name = &path.segments[0].name;
627 let imports = workspace.get_imports(current_namespace);
628
629 // Look for this name in imports
630 if let Some((_local_name, original_path)) = imports.iter().find(|(local, _)| local == name) {
631 // Build the full namespace#type reference from the import path
632 // original_path is like ["com", "atproto", "label", "defs", "label"]
633 // We want "com.atproto.label.defs#label"
634 if original_path.len() > 1 {
635 let namespace = original_path[..original_path.len() - 1].join(".");
636 let type_name = original_path.last().unwrap();
637 json!({
638 "type": "ref",
639 "ref": format!("{}#{}", namespace, type_name)
640 })
641 } else {
642 // Fallback: single-segment import (shouldn't happen but handle it)
643 json!({
644 "type": "ref",
645 "ref": format!("#{}", name)
646 })
647 }
648 } else {
649 // Not an import - assume local reference
650 json!({
651 "type": "ref",
652 "ref": format!("#{}", name)
653 })
654 }
655 } else {
656 // Multi-segment path ref - use as-is
657 let namespace = path.segments[..path.segments.len()-1]
658 .iter()
659 .map(|s| s.name.as_str())
660 .collect::<Vec<_>>()
661 .join(".");
662 let def_name = &path.segments.last().unwrap().name;
663
664 json!({
665 "type": "ref",
666 "ref": format!("{}#{}", namespace, def_name)
667 })
668 }
669 }
670 Type::Array { inner, .. } => {
671 json!({
672 "type": "array",
673 "items": generate_type_json(inner, usage_counts, workspace, current_namespace)
674 })
675 }
676 Type::Union { types, closed, .. } => {
677 let refs: Vec<Value> = types
678 .iter()
679 .map(|t| generate_type_json(t, usage_counts, workspace, current_namespace))
680 .collect();
681 let mut union_obj = Map::new();
682 union_obj.insert("type".to_string(), json!("union"));
683 union_obj.insert("refs".to_string(), json!(refs));
684 // Only emit "closed" field if true (closed unions)
685 // Open unions omit the field (defaults to false per ATProto spec)
686 if *closed {
687 union_obj.insert("closed".to_string(), json!(true));
688 }
689 Value::Object(union_obj)
690 }
691 Type::Object { fields, .. } => {
692 let mut required = Vec::new();
693 let mut properties = Map::new();
694
695 for field in fields {
696 if !field.optional {
697 required.push(field.name.name.clone());
698 }
699 let mut field_json = generate_type_json(&field.ty, usage_counts, workspace, current_namespace);
700 // Add description if the field has doc comments
701 if !field.docs.is_empty() {
702 if let Some(obj) = field_json.as_object_mut() {
703 obj.insert("description".to_string(), json!(extract_docs(&field.docs)));
704 }
705 }
706 properties.insert(field.name.name.clone(), field_json);
707 }
708
709 let mut obj = Map::new();
710 obj.insert("type".to_string(), json!("object"));
711 obj.insert("required".to_string(), json!(required));
712 obj.insert("properties".to_string(), json!(properties));
713 Value::Object(obj)
714 }
715 Type::Parenthesized { inner, .. } => {
716 // Parentheses are just for grouping - unwrap and process inner type
717 generate_type_json(inner, usage_counts, workspace, current_namespace)
718 }
719 Type::Constrained { base, constraints, .. } => {
720 let mut base_json = generate_type_json(base, usage_counts, workspace, current_namespace);
721
722 if let Some(obj) = base_json.as_object_mut() {
723 for constraint in constraints {
724 apply_constraint_to_json(obj, constraint);
725 }
726 }
727
728 base_json
729 }
730 Type::Unknown { .. } => {
731 json!({ "type": "unknown" })
732 }
733 }
734}
735
736fn generate_primitive_json(kind: PrimitiveType) -> Value {
737 match kind {
738 PrimitiveType::Null => json!({ "type": "null" }),
739 PrimitiveType::Boolean => json!({ "type": "boolean" }),
740 PrimitiveType::Integer => json!({ "type": "integer" }),
741 PrimitiveType::String => json!({ "type": "string" }),
742 PrimitiveType::Bytes => json!({ "type": "bytes" }),
743 PrimitiveType::Blob => json!({ "type": "blob" }),
744 }
745}
746
747fn apply_constraint_to_json(obj: &mut Map<String, Value>, constraint: &Constraint) {
748 match constraint {
749 Constraint::MinLength { value, .. } => {
750 obj.insert("minLength".to_string(), json!(value));
751 }
752 Constraint::MaxLength { value, .. } => {
753 obj.insert("maxLength".to_string(), json!(value));
754 }
755 Constraint::MinGraphemes { value, .. } => {
756 obj.insert("minGraphemes".to_string(), json!(value));
757 }
758 Constraint::MaxGraphemes { value, .. } => {
759 obj.insert("maxGraphemes".to_string(), json!(value));
760 }
761 Constraint::Minimum { value, .. } => {
762 obj.insert("minimum".to_string(), json!(value));
763 }
764 Constraint::Maximum { value, .. } => {
765 obj.insert("maximum".to_string(), json!(value));
766 }
767 Constraint::Format { value, .. } => {
768 obj.insert("format".to_string(), json!(value));
769 }
770 Constraint::Enum { values, .. } => {
771 let enum_vals: Vec<String> = values
772 .iter()
773 .map(|v| match v {
774 mlf_lang::ast::ValueRef::Literal(s) => s.clone(),
775 mlf_lang::ast::ValueRef::Reference(path) => path.to_string(),
776 })
777 .collect();
778 obj.insert("enum".to_string(), json!(enum_vals));
779 }
780 Constraint::KnownValues { values, .. } => {
781 let known_vals: Vec<String> = values
782 .iter()
783 .map(|v| match v {
784 mlf_lang::ast::ValueRef::Literal(s) => s.clone(),
785 mlf_lang::ast::ValueRef::Reference(path) => path.to_string(),
786 })
787 .collect();
788 obj.insert("knownValues".to_string(), json!(known_vals));
789 }
790 Constraint::Accept { mimes, .. } => {
791 obj.insert("accept".to_string(), json!(mimes));
792 }
793 Constraint::MaxSize { value, .. } => {
794 obj.insert("maxSize".to_string(), json!(value));
795 }
796 Constraint::Default { value, .. } => {
797 let default_val = match value {
798 ConstraintValue::String(s) => json!(s),
799 ConstraintValue::Integer(i) => json!(i),
800 ConstraintValue::Boolean(b) => json!(b),
801 ConstraintValue::Reference(path) => json!(path.to_string()),
802 };
803 obj.insert("default".to_string(), default_val);
804 }
805 Constraint::Const { value, .. } => {
806 let const_val = match value {
807 ConstraintValue::String(s) => json!(s),
808 ConstraintValue::Integer(i) => json!(i),
809 ConstraintValue::Boolean(b) => json!(b),
810 ConstraintValue::Reference(path) => json!(path.to_string()),
811 };
812 obj.insert("const".to_string(), const_val);
813 }
814 }
815}
816
817// Example built-in generator: JSON Lexicon
818pub struct JsonLexiconGenerator;
819
820impl CodeGenerator for JsonLexiconGenerator {
821 fn name(&self) -> &'static str {
822 "json"
823 }
824
825 fn description(&self) -> &'static str {
826 "Generate AT Protocol JSON lexicon format"
827 }
828
829 fn file_extension(&self) -> &'static str {
830 ".json"
831 }
832
833 fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> {
834 let json = generate_lexicon(ctx.namespace, ctx.lexicon, ctx.workspace);
835 serde_json::to_string_pretty(&json)
836 .map_err(|e| format!("Failed to serialize JSON: {}", e))
837 }
838}
839
840// Register the JSON generator as a static instance
841static JSON_GENERATOR: JsonLexiconGenerator = JsonLexiconGenerator;
842register_generator!(JSON_GENERATOR);