A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

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

mlf generation support

+831
+820
mlf-cli/src/generate/mlf.rs
··· 1 + use miette::Diagnostic; 2 + use serde_json::Value; 3 + use std::path::{Path, PathBuf}; 4 + use thiserror::Error; 5 + 6 + #[derive(Error, Debug, Diagnostic)] 7 + pub enum MlfGenerateError { 8 + #[error("Failed to read file: {path}")] 9 + #[diagnostic(code(mlf::generate::read_file))] 10 + ReadFile { 11 + path: String, 12 + #[source] 13 + source: std::io::Error, 14 + }, 15 + 16 + #[error("Failed to parse JSON: {path}")] 17 + #[diagnostic(code(mlf::generate::parse_json))] 18 + ParseJson { 19 + path: String, 20 + #[source] 21 + source: serde_json::Error, 22 + }, 23 + 24 + #[error("Failed to write output: {path}")] 25 + #[diagnostic(code(mlf::generate::write_output))] 26 + WriteOutput { 27 + path: String, 28 + #[source] 29 + source: std::io::Error, 30 + }, 31 + 32 + #[error("Invalid lexicon format: {message}")] 33 + #[diagnostic(code(mlf::generate::invalid_lexicon))] 34 + InvalidLexicon { message: String }, 35 + 36 + #[error("Failed to expand glob pattern")] 37 + #[diagnostic(code(mlf::generate::glob_error))] 38 + GlobError { 39 + #[source] 40 + source: glob::GlobError, 41 + }, 42 + 43 + #[error("Invalid glob pattern: {pattern}")] 44 + #[diagnostic(code(mlf::generate::invalid_glob))] 45 + InvalidGlob { 46 + pattern: String, 47 + #[source] 48 + source: glob::PatternError, 49 + }, 50 + } 51 + 52 + pub fn run(input_patterns: Vec<String>, output_dir: PathBuf) -> Result<(), MlfGenerateError> { 53 + let mut file_paths = Vec::new(); 54 + 55 + for pattern in input_patterns { 56 + if pattern.contains('*') || pattern.contains('?') { 57 + for entry in glob::glob(&pattern).map_err(|source| MlfGenerateError::InvalidGlob { 58 + pattern: pattern.clone(), 59 + source, 60 + })? { 61 + let path = entry.map_err(|source| MlfGenerateError::GlobError { source })?; 62 + file_paths.push(path); 63 + } 64 + } else { 65 + file_paths.push(PathBuf::from(pattern)); 66 + } 67 + } 68 + 69 + std::fs::create_dir_all(&output_dir).map_err(|source| MlfGenerateError::WriteOutput { 70 + path: output_dir.display().to_string(), 71 + source, 72 + })?; 73 + 74 + let mut errors = Vec::new(); 75 + let mut success_count = 0; 76 + 77 + for file_path in file_paths { 78 + let source = match std::fs::read_to_string(&file_path) { 79 + Ok(s) => s, 80 + Err(source) => { 81 + errors.push(( 82 + file_path.display().to_string(), 83 + format!("Failed to read file: {}", source), 84 + )); 85 + continue; 86 + } 87 + }; 88 + 89 + let json: Value = match serde_json::from_str(&source) { 90 + Ok(j) => j, 91 + Err(source) => { 92 + errors.push(( 93 + file_path.display().to_string(), 94 + format!("Failed to parse JSON: {}", source), 95 + )); 96 + continue; 97 + } 98 + }; 99 + 100 + let mlf_content = match generate_mlf_from_json(&json) { 101 + Ok(content) => content, 102 + Err(e) => { 103 + errors.push((file_path.display().to_string(), format!("{:?}", e))); 104 + continue; 105 + } 106 + }; 107 + 108 + // Extract namespace from JSON "id" field 109 + let namespace = json 110 + .get("id") 111 + .and_then(|v| v.as_str()) 112 + .ok_or_else(|| MlfGenerateError::InvalidLexicon { 113 + message: "Missing 'id' field in lexicon".to_string(), 114 + })?; 115 + 116 + // Create output path from namespace 117 + let mut output_path = output_dir.clone(); 118 + for segment in namespace.split('.') { 119 + output_path.push(segment); 120 + } 121 + if let Err(source) = std::fs::create_dir_all(&output_path.parent().unwrap()) { 122 + errors.push(( 123 + file_path.display().to_string(), 124 + format!("Failed to create directory: {}", source), 125 + )); 126 + continue; 127 + } 128 + output_path.set_extension("mlf"); 129 + 130 + if let Err(source) = std::fs::write(&output_path, mlf_content) { 131 + errors.push(( 132 + output_path.display().to_string(), 133 + format!("Failed to write file: {}", source), 134 + )); 135 + continue; 136 + } 137 + 138 + println!("Generated: {}", output_path.display()); 139 + success_count += 1; 140 + } 141 + 142 + if !errors.is_empty() { 143 + eprintln!( 144 + "\n{} file(s) generated successfully, {} error(s) encountered:\n", 145 + success_count, 146 + errors.len() 147 + ); 148 + for (path, error) in &errors { 149 + eprintln!(" {} - {}", path, error); 150 + } 151 + eprintln!(); 152 + return Err(MlfGenerateError::InvalidLexicon { 153 + message: format!("{} errors total", errors.len()), 154 + }); 155 + } 156 + 157 + println!("\nSuccessfully generated {} file(s)", success_count); 158 + Ok(()) 159 + } 160 + 161 + fn generate_mlf_from_json(json: &Value) -> Result<String, MlfGenerateError> { 162 + let mut output = String::new(); 163 + 164 + let defs = json.get("defs").and_then(|v| v.as_object()).ok_or_else(|| { 165 + MlfGenerateError::InvalidLexicon { 166 + message: "Missing or invalid 'defs' field".to_string(), 167 + } 168 + })?; 169 + 170 + // Process all definitions 171 + for (name, def) in defs { 172 + let def_type = def.get("type").and_then(|v| v.as_str()).ok_or_else(|| { 173 + MlfGenerateError::InvalidLexicon { 174 + message: format!("Missing 'type' field for definition '{}'", name), 175 + } 176 + })?; 177 + 178 + match def_type { 179 + "record" => { 180 + let mlf = generate_record(name, def)?; 181 + output.push_str(&mlf); 182 + output.push('\n'); 183 + } 184 + "query" => { 185 + let mlf = generate_query(name, def)?; 186 + output.push_str(&mlf); 187 + output.push('\n'); 188 + } 189 + "procedure" => { 190 + let mlf = generate_procedure(name, def)?; 191 + output.push_str(&mlf); 192 + output.push('\n'); 193 + } 194 + "subscription" => { 195 + let mlf = generate_subscription(name, def)?; 196 + output.push_str(&mlf); 197 + output.push('\n'); 198 + } 199 + "token" => { 200 + let mlf = generate_token(name, def)?; 201 + output.push_str(&mlf); 202 + output.push('\n'); 203 + } 204 + "object" => { 205 + let mlf = generate_def_type(name, def)?; 206 + output.push_str(&mlf); 207 + output.push('\n'); 208 + } 209 + _ => { 210 + // Unknown type, skip 211 + } 212 + } 213 + } 214 + 215 + Ok(output) 216 + } 217 + 218 + fn generate_record(name: &str, def: &Value) -> Result<String, MlfGenerateError> { 219 + let mut output = String::new(); 220 + 221 + // Add doc comment if present 222 + if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { 223 + if !desc.is_empty() { 224 + for line in desc.lines() { 225 + output.push_str(&format!("/// {}\n", line)); 226 + } 227 + } 228 + } 229 + 230 + // Use "main" name if present, otherwise use the definition name 231 + let record_name = if name == "main" { 232 + // Try to extract the last segment from the namespace ID 233 + // This is a heuristic - we could make it better 234 + "main" 235 + } else { 236 + name 237 + }; 238 + 239 + output.push_str(&format!("record {} {{\n", record_name)); 240 + 241 + // Get the record object 242 + let record_obj = def.get("record").and_then(|v| v.as_object()).ok_or_else(|| { 243 + MlfGenerateError::InvalidLexicon { 244 + message: format!("Missing 'record' field in record definition '{}'", name), 245 + } 246 + })?; 247 + 248 + let properties = record_obj 249 + .get("properties") 250 + .and_then(|v| v.as_object()) 251 + .ok_or_else(|| MlfGenerateError::InvalidLexicon { 252 + message: format!("Missing 'properties' in record '{}'", name), 253 + })?; 254 + 255 + let required = record_obj 256 + .get("required") 257 + .and_then(|v| v.as_array()) 258 + .map(|arr| { 259 + arr.iter() 260 + .filter_map(|v| v.as_str()) 261 + .collect::<Vec<_>>() 262 + }) 263 + .unwrap_or_default(); 264 + 265 + for (field_name, field_def) in properties { 266 + // Add field doc comment 267 + if let Some(desc) = field_def.get("description").and_then(|v| v.as_str()) { 268 + if !desc.is_empty() { 269 + for line in desc.lines() { 270 + output.push_str(&format!(" /// {}\n", line)); 271 + } 272 + } 273 + } 274 + 275 + let is_required = required.contains(&field_name.as_str()); 276 + let required_marker = if is_required { "!" } else { "" }; 277 + 278 + let field_type = generate_type(field_def)?; 279 + output.push_str(&format!( 280 + " {}{}: {},\n", 281 + field_name, required_marker, field_type 282 + )); 283 + } 284 + 285 + output.push_str("};\n"); 286 + Ok(output) 287 + } 288 + 289 + fn generate_query(name: &str, def: &Value) -> Result<String, MlfGenerateError> { 290 + let mut output = String::new(); 291 + 292 + // Add doc comment 293 + if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { 294 + if !desc.is_empty() { 295 + for line in desc.lines() { 296 + output.push_str(&format!("/// {}\n", line)); 297 + } 298 + } 299 + } 300 + 301 + let query_name = if name == "main" { "query" } else { name }; 302 + output.push_str(&format!("query {}", query_name)); 303 + 304 + // Parameters 305 + output.push('('); 306 + if let Some(params) = def.get("parameters").and_then(|v| v.as_object()) { 307 + let properties = params.get("properties").and_then(|v| v.as_object()); 308 + let required = params 309 + .get("required") 310 + .and_then(|v| v.as_array()) 311 + .map(|arr| { 312 + arr.iter() 313 + .filter_map(|v| v.as_str()) 314 + .collect::<Vec<_>>() 315 + }) 316 + .unwrap_or_default(); 317 + 318 + if let Some(props) = properties { 319 + let param_strs: Vec<String> = props 320 + .iter() 321 + .map(|(param_name, param_def)| { 322 + let is_required = required.contains(&param_name.as_str()); 323 + let required_marker = if is_required { "!" } else { "" }; 324 + let param_type = generate_type(param_def).unwrap_or_else(|_| "unknown".to_string()); 325 + 326 + // Add doc comment inline if present 327 + let mut result = String::new(); 328 + if let Some(desc) = param_def.get("description").and_then(|v| v.as_str()) { 329 + if !desc.is_empty() { 330 + result.push_str(&format!("\n /// {}\n ", desc)); 331 + } 332 + } 333 + result.push_str(&format!("{}{}: {}", param_name, required_marker, param_type)); 334 + result 335 + }) 336 + .collect(); 337 + 338 + if !param_strs.is_empty() { 339 + output.push_str(&param_strs.join(",")); 340 + } 341 + } 342 + } 343 + output.push(')'); 344 + 345 + // Output type 346 + if let Some(output_obj) = def.get("output").and_then(|v| v.as_object()) { 347 + if let Some(schema) = output_obj.get("schema") { 348 + let return_type = generate_type(schema)?; 349 + output.push_str(&format!(": {}", return_type)); 350 + 351 + // Check for errors 352 + if let Some(errors) = output_obj.get("errors").and_then(|v| v.as_object()) { 353 + output.push_str(" | error {\n"); 354 + for (error_name, error_def) in errors { 355 + if let Some(desc) = error_def.get("description").and_then(|v| v.as_str()) { 356 + if !desc.is_empty() { 357 + output.push_str(&format!(" /// {}\n", desc)); 358 + } 359 + } 360 + output.push_str(&format!(" {},\n", error_name)); 361 + } 362 + output.push('}'); 363 + } 364 + } 365 + } 366 + 367 + output.push_str(";\n"); 368 + Ok(output) 369 + } 370 + 371 + fn generate_procedure(name: &str, def: &Value) -> Result<String, MlfGenerateError> { 372 + let mut output = String::new(); 373 + 374 + // Add doc comment 375 + if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { 376 + if !desc.is_empty() { 377 + for line in desc.lines() { 378 + output.push_str(&format!("/// {}\n", line)); 379 + } 380 + } 381 + } 382 + 383 + let procedure_name = if name == "main" { "procedure" } else { name }; 384 + output.push_str(&format!("procedure {}", procedure_name)); 385 + 386 + // Input parameters 387 + output.push('('); 388 + if let Some(input) = def.get("input").and_then(|v| v.as_object()) { 389 + if let Some(schema) = input.get("schema").and_then(|v| v.as_object()) { 390 + let properties = schema.get("properties").and_then(|v| v.as_object()); 391 + let required = schema 392 + .get("required") 393 + .and_then(|v| v.as_array()) 394 + .map(|arr| { 395 + arr.iter() 396 + .filter_map(|v| v.as_str()) 397 + .collect::<Vec<_>>() 398 + }) 399 + .unwrap_or_default(); 400 + 401 + if let Some(props) = properties { 402 + let param_strs: Vec<String> = props 403 + .iter() 404 + .map(|(param_name, param_def)| { 405 + let is_required = required.contains(&param_name.as_str()); 406 + let required_marker = if is_required { "!" } else { "" }; 407 + let param_type = 408 + generate_type(param_def).unwrap_or_else(|_| "unknown".to_string()); 409 + 410 + // Add doc comment inline if present 411 + let mut result = String::new(); 412 + if let Some(desc) = param_def.get("description").and_then(|v| v.as_str()) { 413 + if !desc.is_empty() { 414 + result.push_str(&format!("\n /// {}\n ", desc)); 415 + } 416 + } 417 + result.push_str(&format!( 418 + "{}{}: {}", 419 + param_name, required_marker, param_type 420 + )); 421 + result 422 + }) 423 + .collect(); 424 + 425 + if !param_strs.is_empty() { 426 + output.push_str(&param_strs.join(",")); 427 + } 428 + } 429 + } 430 + } 431 + output.push(')'); 432 + 433 + // Output type 434 + if let Some(output_obj) = def.get("output").and_then(|v| v.as_object()) { 435 + if let Some(schema) = output_obj.get("schema") { 436 + let return_type = generate_type(schema)?; 437 + output.push_str(&format!(": {}", return_type)); 438 + 439 + // Check for errors 440 + if let Some(errors) = output_obj.get("errors").and_then(|v| v.as_object()) { 441 + output.push_str(" | error {\n"); 442 + for (error_name, error_def) in errors { 443 + if let Some(desc) = error_def.get("description").and_then(|v| v.as_str()) { 444 + if !desc.is_empty() { 445 + output.push_str(&format!(" /// {}\n", desc)); 446 + } 447 + } 448 + output.push_str(&format!(" {},\n", error_name)); 449 + } 450 + output.push('}'); 451 + } 452 + } 453 + } 454 + 455 + output.push_str(";\n"); 456 + Ok(output) 457 + } 458 + 459 + fn generate_subscription(name: &str, def: &Value) -> Result<String, MlfGenerateError> { 460 + let mut output = String::new(); 461 + 462 + // Add doc comment 463 + if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { 464 + if !desc.is_empty() { 465 + for line in desc.lines() { 466 + output.push_str(&format!("/// {}\n", line)); 467 + } 468 + } 469 + } 470 + 471 + let subscription_name = if name == "main" { 472 + "subscription" 473 + } else { 474 + name 475 + }; 476 + output.push_str(&format!("subscription {}", subscription_name)); 477 + 478 + // Parameters 479 + output.push('('); 480 + if let Some(params) = def.get("parameters").and_then(|v| v.as_object()) { 481 + let properties = params.get("properties").and_then(|v| v.as_object()); 482 + let required = params 483 + .get("required") 484 + .and_then(|v| v.as_array()) 485 + .map(|arr| { 486 + arr.iter() 487 + .filter_map(|v| v.as_str()) 488 + .collect::<Vec<_>>() 489 + }) 490 + .unwrap_or_default(); 491 + 492 + if let Some(props) = properties { 493 + let param_strs: Vec<String> = props 494 + .iter() 495 + .map(|(param_name, param_def)| { 496 + let is_required = required.contains(&param_name.as_str()); 497 + let required_marker = if is_required { "!" } else { "" }; 498 + let param_type = generate_type(param_def).unwrap_or_else(|_| "unknown".to_string()); 499 + 500 + format!("{}{}: {}", param_name, required_marker, param_type) 501 + }) 502 + .collect(); 503 + 504 + if !param_strs.is_empty() { 505 + output.push_str(&param_strs.join(", ")); 506 + } 507 + } 508 + } 509 + output.push(')'); 510 + 511 + // Message types 512 + if let Some(message) = def.get("message").and_then(|v| v.as_object()) { 513 + if let Some(schema) = message.get("schema") { 514 + let message_type = generate_type(schema)?; 515 + output.push_str(&format!(": {}", message_type)); 516 + } 517 + } 518 + 519 + output.push_str(";\n"); 520 + Ok(output) 521 + } 522 + 523 + fn generate_token(name: &str, def: &Value) -> Result<String, MlfGenerateError> { 524 + let mut output = String::new(); 525 + 526 + // Add doc comment 527 + if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { 528 + if !desc.is_empty() { 529 + for line in desc.lines() { 530 + output.push_str(&format!("/// {}\n", line)); 531 + } 532 + } 533 + } 534 + 535 + output.push_str(&format!("token {};\n", name)); 536 + Ok(output) 537 + } 538 + 539 + fn generate_def_type(name: &str, def: &Value) -> Result<String, MlfGenerateError> { 540 + let mut output = String::new(); 541 + 542 + output.push_str(&format!("def type {} = ", name)); 543 + let type_str = generate_type_with_indent(def, 0)?; 544 + output.push_str(&type_str); 545 + output.push_str(";\n"); 546 + 547 + Ok(output) 548 + } 549 + 550 + fn generate_type_with_indent(type_def: &Value, indent_level: usize) -> Result<String, MlfGenerateError> { 551 + let type_name = type_def.get("type").and_then(|v| v.as_str()); 552 + 553 + match type_name { 554 + Some("object") => { 555 + let indent = " ".repeat(indent_level); 556 + let field_indent = " ".repeat(indent_level + 1); 557 + 558 + let mut output = String::from("{\n"); 559 + let properties = type_def 560 + .get("properties") 561 + .and_then(|v| v.as_object()) 562 + .ok_or_else(|| MlfGenerateError::InvalidLexicon { 563 + message: "Missing 'properties' in object type".to_string(), 564 + })?; 565 + 566 + let required = type_def 567 + .get("required") 568 + .and_then(|v| v.as_array()) 569 + .map(|arr| { 570 + arr.iter() 571 + .filter_map(|v| v.as_str()) 572 + .collect::<Vec<_>>() 573 + }) 574 + .unwrap_or_default(); 575 + 576 + for (field_name, field_def) in properties { 577 + // Add field doc comment 578 + if let Some(desc) = field_def.get("description").and_then(|v| v.as_str()) { 579 + if !desc.is_empty() { 580 + for line in desc.lines() { 581 + output.push_str(&format!("{}/// {}\n", field_indent, line)); 582 + } 583 + } 584 + } 585 + 586 + let is_required = required.contains(&field_name.as_str()); 587 + let required_marker = if is_required { "!" } else { "" }; 588 + let field_type = generate_type_with_indent(field_def, indent_level + 1)?; 589 + output.push_str(&format!( 590 + "{}{}{}: {},\n", 591 + field_indent, field_name, required_marker, field_type 592 + )); 593 + } 594 + 595 + output.push_str(&format!("{}}}", indent)); 596 + Ok(output) 597 + } 598 + _ => generate_type(type_def), 599 + } 600 + } 601 + 602 + fn generate_type(type_def: &Value) -> Result<String, MlfGenerateError> { 603 + let type_name = type_def.get("type").and_then(|v| v.as_str()); 604 + 605 + match type_name { 606 + Some("null") => Ok("null".to_string()), 607 + Some("boolean") => Ok("boolean".to_string()), 608 + Some("integer") => { 609 + let mut result = "integer".to_string(); 610 + result = apply_constraints(result, type_def); 611 + Ok(result) 612 + } 613 + Some("string") => { 614 + // Check if this is a format string that maps to a prelude type 615 + if let Some(format) = type_def.get("format").and_then(|v| v.as_str()) { 616 + let prelude_type = match format { 617 + "did" => "Did", 618 + "at-uri" => "AtUri", 619 + "at-identifier" => "AtIdentifier", 620 + "handle" => "Handle", 621 + "datetime" => "Datetime", 622 + "uri" => "Uri", 623 + "cid" => "Cid", 624 + "nsid" => "Nsid", 625 + "tid" => "Tid", 626 + "record-key" => "RecordKey", 627 + "language" => "Language", 628 + _ => { 629 + // Unknown format, fall through to normal string with constraints 630 + let mut result = "string".to_string(); 631 + result = apply_constraints(result, type_def); 632 + return Ok(result); 633 + } 634 + }; 635 + // If it's a known prelude type with only the format constraint, use the prelude type directly 636 + // Check if there are other constraints besides format 637 + let has_other_constraints = type_def.get("minLength").is_some() 638 + || type_def.get("maxLength").is_some() 639 + || type_def.get("minGraphemes").is_some() 640 + || type_def.get("maxGraphemes").is_some() 641 + || type_def.get("enum").is_some() 642 + || type_def.get("knownValues").is_some() 643 + || type_def.get("default").is_some(); 644 + 645 + if !has_other_constraints { 646 + return Ok(prelude_type.to_string()); 647 + } 648 + } 649 + 650 + let mut result = "string".to_string(); 651 + result = apply_constraints(result, type_def); 652 + Ok(result) 653 + } 654 + Some("bytes") => Ok("bytes".to_string()), 655 + Some("blob") => { 656 + let mut result = "blob".to_string(); 657 + result = apply_constraints(result, type_def); 658 + Ok(result) 659 + } 660 + Some("unknown") => Ok("unknown".to_string()), 661 + Some("array") => { 662 + let items = type_def.get("items").ok_or_else(|| { 663 + MlfGenerateError::InvalidLexicon { 664 + message: "Missing 'items' in array type".to_string(), 665 + } 666 + })?; 667 + let item_type = generate_type(items)?; 668 + let mut result = format!("{}[]", item_type); 669 + result = apply_constraints(result, type_def); 670 + Ok(result) 671 + } 672 + Some("object") => { 673 + let mut output = String::from("{\n"); 674 + let properties = type_def 675 + .get("properties") 676 + .and_then(|v| v.as_object()) 677 + .ok_or_else(|| MlfGenerateError::InvalidLexicon { 678 + message: "Missing 'properties' in object type".to_string(), 679 + })?; 680 + 681 + let required = type_def 682 + .get("required") 683 + .and_then(|v| v.as_array()) 684 + .map(|arr| { 685 + arr.iter() 686 + .filter_map(|v| v.as_str()) 687 + .collect::<Vec<_>>() 688 + }) 689 + .unwrap_or_default(); 690 + 691 + for (field_name, field_def) in properties { 692 + // Add field doc comment 693 + if let Some(desc) = field_def.get("description").and_then(|v| v.as_str()) { 694 + if !desc.is_empty() { 695 + for line in desc.lines() { 696 + output.push_str(&format!(" /// {}\n", line)); 697 + } 698 + } 699 + } 700 + 701 + let is_required = required.contains(&field_name.as_str()); 702 + let required_marker = if is_required { "!" } else { "" }; 703 + let field_type = generate_type(field_def)?; 704 + output.push_str(&format!( 705 + " {}{}: {},\n", 706 + field_name, required_marker, field_type 707 + )); 708 + } 709 + 710 + output.push_str(" }"); 711 + Ok(output) 712 + } 713 + Some("union") => { 714 + let refs = type_def.get("refs").and_then(|v| v.as_array()).ok_or_else(|| { 715 + MlfGenerateError::InvalidLexicon { 716 + message: "Missing 'refs' in union type".to_string(), 717 + } 718 + })?; 719 + 720 + let type_strs: Vec<String> = refs 721 + .iter() 722 + .map(|r| generate_type(r).unwrap_or_else(|_| "unknown".to_string())) 723 + .collect(); 724 + 725 + let mut result = type_strs.join(" | "); 726 + 727 + // Check if closed 728 + if type_def.get("closed").and_then(|v| v.as_bool()).unwrap_or(false) { 729 + result.push_str(" | !"); 730 + } 731 + 732 + Ok(result) 733 + } 734 + Some("ref") => { 735 + if let Some(ref_str) = type_def.get("ref").and_then(|v| v.as_str()) { 736 + // Convert ref format from namespace#name to namespace.name 737 + // Also strip leading # for local refs 738 + let clean_ref = ref_str.trim_start_matches('#').replace('#', "."); 739 + Ok(clean_ref) 740 + } else { 741 + Err(MlfGenerateError::InvalidLexicon { 742 + message: "Missing 'ref' in ref type".to_string(), 743 + }) 744 + } 745 + } 746 + _ => Ok("unknown".to_string()), 747 + } 748 + } 749 + 750 + fn apply_constraints(mut type_str: String, type_def: &Value) -> String { 751 + let mut constraints = Vec::new(); 752 + 753 + if let Some(min_length) = type_def.get("minLength").and_then(|v| v.as_i64()) { 754 + constraints.push(format!("minLength: {}", min_length)); 755 + } 756 + if let Some(max_length) = type_def.get("maxLength").and_then(|v| v.as_i64()) { 757 + constraints.push(format!("maxLength: {}", max_length)); 758 + } 759 + if let Some(min_graphemes) = type_def.get("minGraphemes").and_then(|v| v.as_i64()) { 760 + constraints.push(format!("minGraphemes: {}", min_graphemes)); 761 + } 762 + if let Some(max_graphemes) = type_def.get("maxGraphemes").and_then(|v| v.as_i64()) { 763 + constraints.push(format!("maxGraphemes: {}", max_graphemes)); 764 + } 765 + if let Some(minimum) = type_def.get("minimum").and_then(|v| v.as_i64()) { 766 + constraints.push(format!("minimum: {}", minimum)); 767 + } 768 + if let Some(maximum) = type_def.get("maximum").and_then(|v| v.as_i64()) { 769 + constraints.push(format!("maximum: {}", maximum)); 770 + } 771 + if let Some(format) = type_def.get("format").and_then(|v| v.as_str()) { 772 + constraints.push(format!("format: \"{}\"", format)); 773 + } 774 + if let Some(enum_vals) = type_def.get("enum").and_then(|v| v.as_array()) { 775 + let vals: Vec<String> = enum_vals 776 + .iter() 777 + .filter_map(|v| v.as_str()) 778 + .map(|s| format!("\"{}\"", s)) 779 + .collect(); 780 + constraints.push(format!("enum: [{}]", vals.join(", "))); 781 + } 782 + if let Some(known_vals) = type_def.get("knownValues").and_then(|v| v.as_array()) { 783 + let vals: Vec<String> = known_vals 784 + .iter() 785 + .filter_map(|v| v.as_str()) 786 + .map(|s| format!("\"{}\"", s)) 787 + .collect(); 788 + constraints.push(format!("knownValues: [{}]", vals.join(", "))); 789 + } 790 + if let Some(accept) = type_def.get("accept").and_then(|v| v.as_array()) { 791 + let mimes: Vec<String> = accept 792 + .iter() 793 + .filter_map(|v| v.as_str()) 794 + .map(|s| format!("\"{}\"", s)) 795 + .collect(); 796 + constraints.push(format!("accept: [{}]", mimes.join(", "))); 797 + } 798 + if let Some(max_size) = type_def.get("maxSize").and_then(|v| v.as_i64()) { 799 + constraints.push(format!("maxSize: {}", max_size)); 800 + } 801 + if let Some(default) = type_def.get("default") { 802 + let default_str = match default { 803 + Value::String(s) => format!("\"{}\"", s), 804 + Value::Number(n) => n.to_string(), 805 + Value::Bool(b) => b.to_string(), 806 + _ => "null".to_string(), 807 + }; 808 + constraints.push(format!("default: {}", default_str)); 809 + } 810 + 811 + if !constraints.is_empty() { 812 + type_str.push_str(" constrained {\n"); 813 + for constraint in &constraints { 814 + type_str.push_str(&format!(" {},\n", constraint)); 815 + } 816 + type_str.push_str(" }"); 817 + } 818 + 819 + type_str 820 + }
+1
mlf-cli/src/generate/mod.rs
··· 1 1 pub mod code; 2 2 pub mod lexicon; 3 + pub mod mlf;
+10
mlf-cli/src/main.rs
··· 71 71 #[arg(long, help = "Use flat file structure (e.g., app.bsky.post.ts)")] 72 72 flat: bool, 73 73 }, 74 + Mlf { 75 + #[arg(short, long, help = "Input JSON lexicon files (glob patterns supported)")] 76 + input: Vec<String>, 77 + 78 + #[arg(short, long, help = "Output directory")] 79 + output: PathBuf, 80 + }, 74 81 } 75 82 76 83 fn main() { ··· 89 96 } 90 97 GenerateCommands::Code { generator, input, output, flat } => { 91 98 generate::code::run(generator, input, output, flat).into_diagnostic() 99 + } 100 + GenerateCommands::Mlf { input, output } => { 101 + generate::mlf::run(input, output).into_diagnostic() 92 102 } 93 103 }, 94 104 };