A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at gen-lexicon-newline 842 lines 34 kB view raw
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(&param.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(&param.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(&param.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(&param.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(&param.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(&param.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(&param.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(&param.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(&param.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);