use miette::Diagnostic; use serde_json::Value; use std::path::PathBuf; use thiserror::Error; #[derive(Error, Debug, Diagnostic)] pub enum MlfGenerateError { #[error("Failed to read file: {path}")] #[diagnostic(code(mlf::generate::read_file))] #[allow(dead_code)] ReadFile { path: String, #[source] source: std::io::Error, }, #[error("Failed to parse JSON: {path}")] #[diagnostic(code(mlf::generate::parse_json))] #[allow(dead_code)] ParseJson { path: String, #[source] source: serde_json::Error, }, #[error("Failed to write output: {path}")] #[diagnostic(code(mlf::generate::write_output))] WriteOutput { path: String, #[source] source: std::io::Error, }, #[error("Invalid lexicon format: {message}")] #[diagnostic(code(mlf::generate::invalid_lexicon))] InvalidLexicon { message: String }, #[error("Failed to expand glob pattern")] #[diagnostic(code(mlf::generate::glob_error))] GlobError { #[source] source: glob::GlobError, }, #[error("Invalid glob pattern: {pattern}")] #[diagnostic(code(mlf::generate::invalid_glob))] InvalidGlob { pattern: String, #[source] source: glob::PatternError, }, } pub fn run(input_patterns: Vec, output_dir: Option) -> Result<(), MlfGenerateError> { let current_dir = std::env::current_dir().map_err(|source| MlfGenerateError::WriteOutput { path: "current directory".to_string(), source, })?; // Load mlf.toml if available let project_root = crate::config::find_project_root(¤t_dir).ok(); let config = project_root .as_ref() .and_then(|root| { let config_path = root.join("mlf.toml"); crate::config::MlfConfig::load(&config_path).ok() }); // Determine output directory let output_dir = if let Some(explicit) = output_dir { explicit } else if let Some(cfg) = &config { // Find first mlf output in mlf.toml cfg.output .iter() .find(|o| o.r#type == "mlf") .map(|o| PathBuf::from(&o.directory)) .ok_or_else(|| MlfGenerateError::InvalidLexicon { message: "No mlf output configured in mlf.toml. Either add an output configuration or provide --output flag.".to_string(), })? } else { return Err(MlfGenerateError::InvalidLexicon { message: "No mlf.toml found and no --output flag provided. Either create a mlf.toml or provide --output flag.".to_string(), }); }; let mut file_paths = Vec::new(); for pattern in input_patterns { if pattern.contains('*') || pattern.contains('?') { for entry in glob::glob(&pattern).map_err(|source| MlfGenerateError::InvalidGlob { pattern: pattern.clone(), source, })? { let path = entry.map_err(|source| MlfGenerateError::GlobError { source })?; file_paths.push(path); } } else { file_paths.push(PathBuf::from(pattern)); } } std::fs::create_dir_all(&output_dir).map_err(|source| MlfGenerateError::WriteOutput { path: output_dir.display().to_string(), source, })?; let mut errors = Vec::new(); let mut success_count = 0; for file_path in file_paths { let source = match std::fs::read_to_string(&file_path) { Ok(s) => s, Err(source) => { errors.push(( file_path.display().to_string(), format!("Failed to read file: {}", source), )); continue; } }; let json: Value = match serde_json::from_str(&source) { Ok(j) => j, Err(source) => { errors.push(( file_path.display().to_string(), format!("Failed to parse JSON: {}", source), )); continue; } }; let mlf_content = match generate_mlf_from_json(&json) { Ok(content) => content, Err(e) => { errors.push((file_path.display().to_string(), format!("{:?}", e))); continue; } }; // Extract namespace from JSON "id" field let namespace = json .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| MlfGenerateError::InvalidLexicon { message: "Missing 'id' field in lexicon".to_string(), })?; // Create output path from namespace let mut output_path = output_dir.clone(); for segment in namespace.split('.') { output_path.push(segment); } if let Err(source) = std::fs::create_dir_all(&output_path.parent().unwrap()) { errors.push(( file_path.display().to_string(), format!("Failed to create directory: {}", source), )); continue; } output_path.set_extension("mlf"); if let Err(source) = std::fs::write(&output_path, mlf_content) { errors.push(( output_path.display().to_string(), format!("Failed to write file: {}", source), )); continue; } println!("Generated: {}", output_path.display()); success_count += 1; } if !errors.is_empty() { eprintln!( "\n{} file(s) generated successfully, {} error(s) encountered:\n", success_count, errors.len() ); for (path, error) in &errors { eprintln!(" {} - {}", path, error); } eprintln!(); return Err(MlfGenerateError::InvalidLexicon { message: format!("{} errors total", errors.len()), }); } println!("\nSuccessfully generated {} file(s)", success_count); Ok(()) } pub fn generate_mlf_from_json(json: &Value) -> Result { let mut output = String::new(); // Extract NSID to get the last segment for "main" definitions let nsid = json .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| MlfGenerateError::InvalidLexicon { message: "Missing 'id' field in lexicon".to_string(), })?; let last_segment = nsid.split('.').last().unwrap_or("main"); let defs = json.get("defs").and_then(|v| v.as_object()).ok_or_else(|| { MlfGenerateError::InvalidLexicon { message: "Missing or invalid 'defs' field".to_string(), } })?; // Create a context to pass the current namespace to type generation let ctx = ConversionContext { current_namespace: nsid.to_string(), }; // Process all definitions for (name, def) in defs { let def_type = def.get("type").and_then(|v| v.as_str()).ok_or_else(|| { MlfGenerateError::InvalidLexicon { message: format!("Missing 'type' field for definition '{}'", name), } })?; match def_type { "record" => { let mlf = generate_record(name, def, last_segment, &ctx)?; output.push_str(&mlf); output.push('\n'); } "query" => { let mlf = generate_query(name, def, last_segment, &ctx)?; output.push_str(&mlf); output.push('\n'); } "procedure" => { let mlf = generate_procedure(name, def, last_segment, &ctx)?; output.push_str(&mlf); output.push('\n'); } "subscription" => { let mlf = generate_subscription(name, def, last_segment, &ctx)?; output.push_str(&mlf); output.push('\n'); } "token" => { let mlf = generate_token(name, def)?; output.push_str(&mlf); output.push('\n'); } _ => { // All other types (object, string, array, union, etc.) are treated as def type let mlf = generate_def_type(name, def, last_segment, &ctx)?; output.push_str(&mlf); output.push('\n'); } } } Ok(output) } struct ConversionContext { current_namespace: String, } /// Reserved words in MLF that need to be escaped const RESERVED_WORDS: &[&str] = &[ "main", "record", "query", "procedure", "subscription", "token", "def", "type", "use", "pub", "alias", "namespace", "constrained", "error", "unit", "null", "boolean", "integer", "string", "bytes", "blob", "unknown", "array", "object", "union", "ref", ]; /// Escape a name if it's a reserved word fn escape_name(name: &str) -> String { if RESERVED_WORDS.contains(&name) { format!("`{}`", name) } else { name.to_string() } } fn generate_record(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result { let mut output = String::new(); // Add doc comment if present if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { for line in desc.lines() { output.push_str(&format!("/// {}\n", line)); } } } // Add @main annotation for "main" definitions if name == "main" { output.push_str("@main\n"); } // Use last segment of NSID for "main" definitions let record_name = if name == "main" { escape_name(last_segment) } else { escape_name(name) }; output.push_str(&format!("record {} {{\n", record_name)); // Get the record object let record_obj = def.get("record").and_then(|v| v.as_object()).ok_or_else(|| { MlfGenerateError::InvalidLexicon { message: format!("Missing 'record' field in record definition '{}'", name), } })?; let properties = record_obj .get("properties") .and_then(|v| v.as_object()) .ok_or_else(|| MlfGenerateError::InvalidLexicon { message: format!("Missing 'properties' in record '{}'", name), })?; let required = record_obj .get("required") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str()) .collect::>() }) .unwrap_or_default(); for (field_name, field_def) in properties { // Add field doc comment if let Some(desc) = field_def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { for line in desc.lines() { output.push_str(&format!(" /// {}\n", line)); } } } let is_required = required.contains(&field_name.as_str()); let required_marker = if is_required { "!" } else { "" }; let field_type = generate_type(field_def, ctx)?; let escaped_field_name = escape_name(field_name); output.push_str(&format!( " {}{}: {},\n", escaped_field_name, required_marker, field_type )); } output.push_str("}\n"); Ok(output) } fn generate_query(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result { let mut output = String::new(); // Add doc comment if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { for line in desc.lines() { output.push_str(&format!("/// {}\n", line)); } } } // Add @main annotation for "main" definitions if name == "main" { output.push_str("@main\n"); } let query_name = if name == "main" { escape_name(last_segment) } else { escape_name(name) }; output.push_str(&format!("query {}", query_name)); // Parameters output.push('('); if let Some(params) = def.get("parameters").and_then(|v| v.as_object()) { let properties = params.get("properties").and_then(|v| v.as_object()); let required = params .get("required") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str()) .collect::>() }) .unwrap_or_default(); if let Some(props) = properties { let param_strs: Vec = props .iter() .map(|(param_name, param_def)| { let is_required = required.contains(¶m_name.as_str()); let required_marker = if is_required { "!" } else { "" }; let param_type = generate_type(param_def, ctx).unwrap_or_else(|_| "unknown".to_string()); let escaped_param_name = escape_name(param_name); // Add doc comment inline if present let mut result = String::new(); if let Some(desc) = param_def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { result.push_str(&format!("\n /// {}\n ", desc)); } } result.push_str(&format!("{}{}: {}", escaped_param_name, required_marker, param_type)); result }) .collect(); if !param_strs.is_empty() { output.push_str(¶m_strs.join(",\n ")); } } } output.push(')'); // Output type if let Some(output_obj) = def.get("output").and_then(|v| v.as_object()) { if let Some(schema) = output_obj.get("schema") { let return_type = generate_type(schema, ctx)?; output.push_str(&format!(": {}", return_type)); // Check for errors if let Some(errors) = output_obj.get("errors").and_then(|v| v.as_object()) { output.push_str(" | error {\n"); for (error_name, error_def) in errors { if let Some(desc) = error_def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { output.push_str(&format!(" /// {}\n", desc)); } } output.push_str(&format!(" {},\n", error_name)); } output.push('}'); } } } output.push_str(";\n"); Ok(output) } fn generate_procedure(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result { let mut output = String::new(); // Add doc comment if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { for line in desc.lines() { output.push_str(&format!("/// {}\n", line)); } } } // Add @main annotation for "main" definitions if name == "main" { output.push_str("@main\n"); } let procedure_name = if name == "main" { escape_name(last_segment) } else { escape_name(name) }; output.push_str(&format!("procedure {}", procedure_name)); // Input parameters output.push('('); if let Some(input) = def.get("input").and_then(|v| v.as_object()) { if let Some(schema) = input.get("schema").and_then(|v| v.as_object()) { let properties = schema.get("properties").and_then(|v| v.as_object()); let required = schema .get("required") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str()) .collect::>() }) .unwrap_or_default(); if let Some(props) = properties { let param_strs: Vec = props .iter() .map(|(param_name, param_def)| { let is_required = required.contains(¶m_name.as_str()); let required_marker = if is_required { "!" } else { "" }; let param_type = generate_type(param_def, ctx).unwrap_or_else(|_| "unknown".to_string()); let escaped_param_name = escape_name(param_name); // Add doc comment inline if present let mut result = String::new(); if let Some(desc) = param_def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { result.push_str(&format!("\n /// {}\n ", desc)); } } result.push_str(&format!( "{}{}: {}", escaped_param_name, required_marker, param_type )); result }) .collect(); if !param_strs.is_empty() { output.push_str(¶m_strs.join(",\n ")); } } } } output.push(')'); // Output type if let Some(output_obj) = def.get("output").and_then(|v| v.as_object()) { if let Some(schema) = output_obj.get("schema") { let return_type = generate_type(schema, ctx)?; output.push_str(&format!(": {}", return_type)); // Check for errors if let Some(errors) = output_obj.get("errors").and_then(|v| v.as_object()) { output.push_str(" | error {\n"); for (error_name, error_def) in errors { if let Some(desc) = error_def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { output.push_str(&format!(" /// {}\n", desc)); } } output.push_str(&format!(" {},\n", error_name)); } output.push('}'); } } } output.push_str(";\n"); Ok(output) } fn generate_subscription(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result { let mut output = String::new(); // Add doc comment if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { for line in desc.lines() { output.push_str(&format!("/// {}\n", line)); } } } // Add @main annotation for "main" definitions if name == "main" { output.push_str("@main\n"); } let subscription_name = if name == "main" { escape_name(last_segment) } else { escape_name(name) }; output.push_str(&format!("subscription {}", subscription_name)); // Parameters output.push('('); if let Some(params) = def.get("parameters").and_then(|v| v.as_object()) { let properties = params.get("properties").and_then(|v| v.as_object()); let required = params .get("required") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str()) .collect::>() }) .unwrap_or_default(); if let Some(props) = properties { let param_strs: Vec = props .iter() .map(|(param_name, param_def)| { let is_required = required.contains(¶m_name.as_str()); let required_marker = if is_required { "!" } else { "" }; let param_type = generate_type(param_def, ctx).unwrap_or_else(|_| "unknown".to_string()); let escaped_param_name = escape_name(param_name); format!("{}{}: {}", escaped_param_name, required_marker, param_type) }) .collect(); if !param_strs.is_empty() { output.push_str(¶m_strs.join(", ")); } } } output.push(')'); // Message types if let Some(message) = def.get("message").and_then(|v| v.as_object()) { if let Some(schema) = message.get("schema") { let message_type = generate_type(schema, ctx)?; output.push_str(&format!(": {}", message_type)); } } output.push_str(";\n"); Ok(output) } fn generate_token(name: &str, def: &Value) -> Result { let mut output = String::new(); // Add doc comment if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { for line in desc.lines() { output.push_str(&format!("/// {}\n", line)); } } } let escaped_name = escape_name(name); output.push_str(&format!("token {};\n", escaped_name)); Ok(output) } fn generate_def_type(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result { let mut output = String::new(); // Add doc comment if present if let Some(desc) = def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { for line in desc.lines() { output.push_str(&format!("/// {}\n", line)); } } } // Add @main annotation for "main" definitions if name == "main" { output.push_str("@main\n"); } // Use last segment of NSID for "main" definitions // Keywords are now allowed by the parser, so just escape with backticks let def_name = if name == "main" { escape_name(last_segment) } else { escape_name(name) }; output.push_str(&format!("def type {} = ", def_name)); let type_str = generate_type_with_indent(def, 0, ctx)?; output.push_str(&type_str); output.push_str(";\n"); Ok(output) } fn generate_type_with_indent(type_def: &Value, indent_level: usize, ctx: &ConversionContext) -> Result { let type_name = type_def.get("type").and_then(|v| v.as_str()); match type_name { Some("object") => { let indent = " ".repeat(indent_level); let field_indent = " ".repeat(indent_level + 1); let mut output = String::from("{\n"); let properties = type_def .get("properties") .and_then(|v| v.as_object()) .ok_or_else(|| MlfGenerateError::InvalidLexicon { message: "Missing 'properties' in object type".to_string(), })?; let required = type_def .get("required") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str()) .collect::>() }) .unwrap_or_default(); for (field_name, field_def) in properties { // Add field doc comment if let Some(desc) = field_def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { for line in desc.lines() { output.push_str(&format!("{}/// {}\n", field_indent, line)); } } } let is_required = required.contains(&field_name.as_str()); let required_marker = if is_required { "!" } else { "" }; let field_type = generate_type_with_indent(field_def, indent_level + 1, ctx)?; let escaped_field_name = escape_name(field_name); output.push_str(&format!( "{}{}{}: {},\n", field_indent, escaped_field_name, required_marker, field_type )); } output.push_str(&format!("{}}}", indent)); Ok(output) } _ => generate_type(type_def, ctx), } } fn generate_type(type_def: &Value, ctx: &ConversionContext) -> Result { let type_name = type_def.get("type").and_then(|v| v.as_str()); match type_name { Some("null") => Ok("null".to_string()), Some("boolean") => Ok("boolean".to_string()), Some("integer") => { let mut result = "integer".to_string(); result = apply_constraints(result, type_def); Ok(result) } Some("string") => { // Check if this is a format string that maps to a prelude type if let Some(format) = type_def.get("format").and_then(|v| v.as_str()) { let prelude_type = match format { "did" => "Did", "at-uri" => "AtUri", "at-identifier" => "AtIdentifier", "handle" => "Handle", "datetime" => "Datetime", "uri" => "Uri", "cid" => "Cid", "nsid" => "Nsid", "tid" => "Tid", "record-key" => "RecordKey", "language" => "Language", _ => { // Unknown format, fall through to normal string with constraints let mut result = "string".to_string(); result = apply_constraints(result, type_def); return Ok(result); } }; // If it's a known prelude type with only the format constraint, use the prelude type directly // Check if there are other constraints besides format let has_other_constraints = type_def.get("minLength").is_some() || type_def.get("maxLength").is_some() || type_def.get("minGraphemes").is_some() || type_def.get("maxGraphemes").is_some() || type_def.get("enum").is_some() || type_def.get("knownValues").is_some() || type_def.get("default").is_some(); if !has_other_constraints { return Ok(prelude_type.to_string()); } } let mut result = "string".to_string(); result = apply_constraints(result, type_def); Ok(result) } Some("bytes") => Ok("bytes".to_string()), Some("blob") => { let mut result = "blob".to_string(); result = apply_constraints(result, type_def); Ok(result) } Some("unknown") => Ok("unknown".to_string()), Some("array") => { let items = type_def.get("items").ok_or_else(|| { MlfGenerateError::InvalidLexicon { message: "Missing 'items' in array type".to_string(), } })?; // Check if items have constraints let items_obj = items.as_object(); let has_item_constraints = items_obj.map_or(false, |obj| { obj.contains_key("minLength") || obj.contains_key("maxLength") || obj.contains_key("minGraphemes") || obj.contains_key("maxGraphemes") || obj.contains_key("minimum") || obj.contains_key("maximum") || obj.contains_key("enum") || obj.contains_key("knownValues") || obj.contains_key("default") }); let item_type = if has_item_constraints { // If item has constraints, we need to wrap in parentheses to apply constraints before [] // For now, just generate the base type without item constraints // TODO: Consider generating a type alias for complex constrained items items.get("type") .and_then(|t| t.as_str()) .unwrap_or("unknown") .to_string() } else { generate_type(items, ctx)? }; let mut result = format!("{}[]", item_type); result = apply_constraints(result, type_def); Ok(result) } Some("object") => { let mut output = String::from("{\n"); let properties = type_def .get("properties") .and_then(|v| v.as_object()) .ok_or_else(|| MlfGenerateError::InvalidLexicon { message: "Missing 'properties' in object type".to_string(), })?; let required = type_def .get("required") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str()) .collect::>() }) .unwrap_or_default(); for (field_name, field_def) in properties { // Add field doc comment if let Some(desc) = field_def.get("description").and_then(|v| v.as_str()) { if !desc.is_empty() { for line in desc.lines() { output.push_str(&format!(" /// {}\n", line)); } } } let is_required = required.contains(&field_name.as_str()); let required_marker = if is_required { "!" } else { "" }; let field_type = generate_type(field_def, ctx)?; let escaped_field_name = escape_name(field_name); output.push_str(&format!( " {}{}: {},\n", escaped_field_name, required_marker, field_type )); } output.push_str(" }"); Ok(output) } Some("union") => { let refs = type_def.get("refs").and_then(|v| v.as_array()).ok_or_else(|| { MlfGenerateError::InvalidLexicon { message: "Missing 'refs' in union type".to_string(), } })?; let type_strs: Vec = refs .iter() .map(|r| generate_type(r, ctx).unwrap_or_else(|_| "unknown".to_string())) .collect(); let mut result = type_strs.join(" | "); // Check if closed if type_def.get("closed").and_then(|v| v.as_bool()).unwrap_or(false) { result.push_str(" | !"); } Ok(result) } Some("ref") => { if let Some(ref_str) = type_def.get("ref").and_then(|v| v.as_str()) { // Handle references: // "#defName" -> "defName" (local reference, same file) // "namespace.id#defName" -> Check if same namespace, if so use "defName", else use full path if let Some(stripped) = ref_str.strip_prefix('#') { // Local reference: #defName -> defName Ok(stripped.to_string()) } else if let Some((namespace, def_name)) = ref_str.split_once('#') { // Check if this references the current namespace if namespace == ctx.current_namespace { // Same namespace - use just the def name Ok(def_name.to_string()) } else { // Different namespace - use full NSID format Ok(format!("{}.{}", namespace, def_name)) } } else { // No # at all - shouldn't happen in valid lexicons, but handle gracefully Ok(ref_str.to_string()) } } else { Err(MlfGenerateError::InvalidLexicon { message: "Missing 'ref' in ref type".to_string(), }) } } _ => Ok("unknown".to_string()), } } fn apply_constraints(mut type_str: String, type_def: &Value) -> String { let mut constraints = Vec::new(); if let Some(min_length) = type_def.get("minLength").and_then(|v| v.as_i64()) { constraints.push(format!("minLength: {}", min_length)); } if let Some(max_length) = type_def.get("maxLength").and_then(|v| v.as_i64()) { constraints.push(format!("maxLength: {}", max_length)); } if let Some(min_graphemes) = type_def.get("minGraphemes").and_then(|v| v.as_i64()) { constraints.push(format!("minGraphemes: {}", min_graphemes)); } if let Some(max_graphemes) = type_def.get("maxGraphemes").and_then(|v| v.as_i64()) { constraints.push(format!("maxGraphemes: {}", max_graphemes)); } if let Some(minimum) = type_def.get("minimum").and_then(|v| v.as_i64()) { constraints.push(format!("minimum: {}", minimum)); } if let Some(maximum) = type_def.get("maximum").and_then(|v| v.as_i64()) { constraints.push(format!("maximum: {}", maximum)); } if let Some(format) = type_def.get("format").and_then(|v| v.as_str()) { constraints.push(format!("format: \"{}\"", format)); } if let Some(enum_vals) = type_def.get("enum").and_then(|v| v.as_array()) { let vals: Vec = enum_vals .iter() .filter_map(|v| v.as_str()) .map(|s| format!("\"{}\"", s)) .collect(); constraints.push(format!("enum: [{}]", vals.join(", "))); } if let Some(known_vals) = type_def.get("knownValues").and_then(|v| v.as_array()) { let vals: Vec = known_vals .iter() .filter_map(|v| v.as_str()) .map(|s| format!("\"{}\"", s)) .collect(); constraints.push(format!("knownValues: [{}]", vals.join(", "))); } if let Some(accept) = type_def.get("accept").and_then(|v| v.as_array()) { let mimes: Vec = accept .iter() .filter_map(|v| v.as_str()) .map(|s| format!("\"{}\"", s)) .collect(); constraints.push(format!("accept: [{}]", mimes.join(", "))); } if let Some(max_size) = type_def.get("maxSize").and_then(|v| v.as_i64()) { constraints.push(format!("maxSize: {}", max_size)); } if let Some(default) = type_def.get("default") { let default_str = match default { Value::String(s) => format!("\"{}\"", s), Value::Number(n) => n.to_string(), Value::Bool(b) => b.to_string(), _ => "null".to_string(), }; constraints.push(format!("default: {}", default_str)); } if !constraints.is_empty() { type_str.push_str(" constrained {\n"); for constraint in &constraints { type_str.push_str(&format!(" {},\n", constraint)); } type_str.push_str(" }"); } type_str }