A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

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

Parser and codegen fixes

+280 -99
+16 -3
mlf-cli/src/check.rs
··· 1 - use crate::config::{find_project_root, ConfigError, MlfConfig}; 1 + use crate::config::{find_project_root, get_mlf_cache_dir, ConfigError, MlfConfig}; 2 + use crate::workspace_ext::workspace_with_std_and_cache; 2 3 use miette::Diagnostic; 3 4 use mlf_diagnostics::{ParseDiagnostic, ValidationDiagnostic}; 4 5 use std::path::PathBuf; ··· 106 107 } 107 108 } 108 109 109 - let mut workspace = mlf_lang::Workspace::with_std().map_err(|e| { 110 + // Try to load cached lexicons from .mlf directory 111 + let current_dir = std::env::current_dir() 112 + .map_err(|e| CheckError::ReadFile { 113 + path: ".".to_string(), 114 + source: e, 115 + })?; 116 + 117 + let mlf_cache_dir = find_project_root(&current_dir) 118 + .ok() 119 + .map(|root| get_mlf_cache_dir(&root)); 120 + 121 + let mut workspace = workspace_with_std_and_cache(mlf_cache_dir.as_deref()).map_err(|e| { 122 + eprintln!("Error loading workspace: {}", e); 110 123 CheckError::ValidationErrors { 111 - help: Some(format!("Failed to load standard library: {:?}", e)), 124 + help: Some(format!("Failed to load workspace: {}", e)), 112 125 } 113 126 })?; 114 127
+14 -8
mlf-cli/src/fetch.rs
··· 269 269 270 270 println!(" Processing: {}", record_nsid); 271 271 272 - // Save JSON file 272 + // Save JSON file with directory structure 273 + // e.g., "place.stream.key" -> "place/stream/key.json" 273 274 let json_str = serde_json::to_string_pretty(&record.value)?; 274 - let json_path = mlf_dir 275 - .join("lexicons/json") 276 - .join(format!("{}.json", record_nsid)); 275 + let mut json_path = mlf_dir.join("lexicons/json"); 276 + for segment in record_nsid.split('.') { 277 + json_path.push(segment); 278 + } 279 + json_path.set_extension("json"); 277 280 278 281 // Create parent directories 279 282 if let Some(parent) = json_path.parent() { ··· 287 290 let mlf_content = crate::generate::mlf::generate_mlf_from_json(&record.value) 288 291 .map_err(|e| FetchError::ConversionError(format!("{:?}", e)))?; 289 292 290 - // Save MLF file 291 - let mlf_path = mlf_dir 292 - .join("lexicons/mlf") 293 - .join(format!("{}.mlf", record_nsid)); 293 + // Save MLF file with directory structure 294 + // e.g., "place.stream.key" -> "place/stream/key.mlf" 295 + let mut mlf_path = mlf_dir.join("lexicons/mlf"); 296 + for segment in record_nsid.split('.') { 297 + mlf_path.push(segment); 298 + } 299 + mlf_path.set_extension("mlf"); 294 300 295 301 // Create parent directories 296 302 if let Some(parent) = mlf_path.parent() {
+55 -14
mlf-cli/src/generate/mlf.rs
··· 214 214 output.push('\n'); 215 215 } 216 216 "object" => { 217 - let mlf = generate_def_type(name, def)?; 217 + let mlf = generate_def_type(name, def, last_segment)?; 218 218 output.push_str(&mlf); 219 219 output.push('\n'); 220 220 } ··· 302 302 let required_marker = if is_required { "!" } else { "" }; 303 303 304 304 let field_type = generate_type(field_def)?; 305 + let escaped_field_name = escape_name(field_name); 305 306 output.push_str(&format!( 306 307 " {}{}: {},\n", 307 - field_name, required_marker, field_type 308 + escaped_field_name, required_marker, field_type 308 309 )); 309 310 } 310 311 311 - output.push_str("};\n"); 312 + output.push_str("}\n"); 312 313 Ok(output) 313 314 } 314 315 ··· 352 353 let is_required = required.contains(&param_name.as_str()); 353 354 let required_marker = if is_required { "!" } else { "" }; 354 355 let param_type = generate_type(param_def).unwrap_or_else(|_| "unknown".to_string()); 356 + let escaped_param_name = escape_name(param_name); 355 357 356 358 // Add doc comment inline if present 357 359 let mut result = String::new(); ··· 360 362 result.push_str(&format!("\n /// {}\n ", desc)); 361 363 } 362 364 } 363 - result.push_str(&format!("{}{}: {}", param_name, required_marker, param_type)); 365 + result.push_str(&format!("{}{}: {}", escaped_param_name, required_marker, param_type)); 364 366 result 365 367 }) 366 368 .collect(); ··· 440 442 let required_marker = if is_required { "!" } else { "" }; 441 443 let param_type = 442 444 generate_type(param_def).unwrap_or_else(|_| "unknown".to_string()); 445 + let escaped_param_name = escape_name(param_name); 443 446 444 447 // Add doc comment inline if present 445 448 let mut result = String::new(); ··· 450 453 } 451 454 result.push_str(&format!( 452 455 "{}{}: {}", 453 - param_name, required_marker, param_type 456 + escaped_param_name, required_marker, param_type 454 457 )); 455 458 result 456 459 }) ··· 530 533 let is_required = required.contains(&param_name.as_str()); 531 534 let required_marker = if is_required { "!" } else { "" }; 532 535 let param_type = generate_type(param_def).unwrap_or_else(|_| "unknown".to_string()); 536 + let escaped_param_name = escape_name(param_name); 533 537 534 - format!("{}{}: {}", param_name, required_marker, param_type) 538 + format!("{}{}: {}", escaped_param_name, required_marker, param_type) 535 539 }) 536 540 .collect(); 537 541 ··· 566 570 } 567 571 } 568 572 569 - output.push_str(&format!("token {};\n", name)); 573 + let escaped_name = escape_name(name); 574 + output.push_str(&format!("token {};\n", escaped_name)); 570 575 Ok(output) 571 576 } 572 577 573 - fn generate_def_type(name: &str, def: &Value) -> Result<String, MlfGenerateError> { 578 + fn generate_def_type(name: &str, def: &Value, last_segment: &str) -> Result<String, MlfGenerateError> { 574 579 let mut output = String::new(); 575 580 576 - output.push_str(&format!("def type {} = ", name)); 581 + // Use last segment of NSID for "main" definitions 582 + let def_name = if name == "main" { 583 + escape_name(last_segment) 584 + } else { 585 + escape_name(name) 586 + }; 587 + 588 + output.push_str(&format!("def type {} = ", def_name)); 577 589 let type_str = generate_type_with_indent(def, 0)?; 578 590 output.push_str(&type_str); 579 591 output.push_str(";\n"); ··· 620 632 let is_required = required.contains(&field_name.as_str()); 621 633 let required_marker = if is_required { "!" } else { "" }; 622 634 let field_type = generate_type_with_indent(field_def, indent_level + 1)?; 635 + let escaped_field_name = escape_name(field_name); 623 636 output.push_str(&format!( 624 637 "{}{}{}: {},\n", 625 - field_indent, field_name, required_marker, field_type 638 + field_indent, escaped_field_name, required_marker, field_type 626 639 )); 627 640 } 628 641 ··· 698 711 message: "Missing 'items' in array type".to_string(), 699 712 } 700 713 })?; 701 - let item_type = generate_type(items)?; 714 + 715 + // Check if items have constraints 716 + let items_obj = items.as_object(); 717 + let has_item_constraints = items_obj.map_or(false, |obj| { 718 + obj.contains_key("minLength") || 719 + obj.contains_key("maxLength") || 720 + obj.contains_key("minGraphemes") || 721 + obj.contains_key("maxGraphemes") || 722 + obj.contains_key("minimum") || 723 + obj.contains_key("maximum") || 724 + obj.contains_key("enum") || 725 + obj.contains_key("knownValues") || 726 + obj.contains_key("default") 727 + }); 728 + 729 + let item_type = if has_item_constraints { 730 + // If item has constraints, we need to wrap in parentheses to apply constraints before [] 731 + // For now, just generate the base type without item constraints 732 + // TODO: Consider generating a type alias for complex constrained items 733 + items.get("type") 734 + .and_then(|t| t.as_str()) 735 + .unwrap_or("unknown") 736 + .to_string() 737 + } else { 738 + generate_type(items)? 739 + }; 740 + 702 741 let mut result = format!("{}[]", item_type); 703 742 result = apply_constraints(result, type_def); 704 743 Ok(result) ··· 735 774 let is_required = required.contains(&field_name.as_str()); 736 775 let required_marker = if is_required { "!" } else { "" }; 737 776 let field_type = generate_type(field_def)?; 777 + let escaped_field_name = escape_name(field_name); 738 778 output.push_str(&format!( 739 779 " {}{}: {},\n", 740 - field_name, required_marker, field_type 780 + escaped_field_name, required_marker, field_type 741 781 )); 742 782 } 743 783 ··· 767 807 } 768 808 Some("ref") => { 769 809 if let Some(ref_str) = type_def.get("ref").and_then(|v| v.as_str()) { 770 - // Convert ref format from namespace#name to namespace.name 771 - // Also strip leading # for local refs 810 + // Convert refs: strip leading # and convert remaining # to . 811 + // "#audio" -> "audio" (local ref, just the name) 812 + // "com.example#foo" -> "com.example.foo" (external ref) 772 813 let clean_ref = ref_str.trim_start_matches('#').replace('#', "."); 773 814 Ok(clean_ref) 774 815 } else {
+21 -10
mlf-cli/src/workspace_ext.rs
··· 18 18 .map_err(|e| format!("Failed to read {}: {}", file_path.display(), e))?; 19 19 20 20 // Convert file path to namespace 21 - // e.g., ".mlf/lexicons/mlf/stream.place.mlf" -> "stream.place" 21 + // e.g., ".mlf/lexicons/mlf/place/stream/key.mlf" -> "place.stream" 22 22 let namespace = extract_namespace_from_path(&file_path, dir)?; 23 23 24 24 // Parse the lexicon ··· 63 63 } 64 64 65 65 /// Extract namespace from file path relative to base directory 66 - /// e.g., base=".mlf/lexicons/mlf", path=".mlf/lexicons/mlf/stream.place.mlf" -> "stream.place" 66 + /// The namespace includes the full path WITH the filename (minus .mlf extension) 67 + /// e.g., base=".mlf/lexicons/mlf", path=".mlf/lexicons/mlf/place/stream/key.mlf" -> "place.stream.key" 68 + /// This allows "place.stream.key" to resolve to a definition named "key" in namespace "place.stream.key" 67 69 fn extract_namespace_from_path(path: &Path, base: &Path) -> Result<String, String> { 68 70 let relative = path 69 71 .strip_prefix(base) ··· 80 82 .unwrap_or(path_str); 81 83 82 84 // Replace path separators with dots 83 - // e.g., "stream/place/foo.mlf" -> "stream.place.foo" 85 + // e.g., "place/stream/key" -> "place.stream.key" 84 86 let namespace = without_ext.replace(std::path::MAIN_SEPARATOR, "."); 85 87 86 88 Ok(namespace) ··· 122 124 use super::*; 123 125 124 126 #[test] 125 - fn test_extract_namespace() { 127 + fn test_extract_namespace_nested() { 126 128 let base = Path::new(".mlf/lexicons/mlf"); 127 - let path = Path::new(".mlf/lexicons/mlf/stream.place.mlf"); 129 + let path = Path::new(".mlf/lexicons/mlf/place/stream/key.mlf"); 128 130 let namespace = extract_namespace_from_path(path, base).unwrap(); 129 - assert_eq!(namespace, "stream.place"); 131 + // Full path "place/stream/key" becomes "place.stream.key" 132 + assert_eq!(namespace, "place.stream.key"); 130 133 } 131 134 132 135 #[test] 133 - fn test_extract_namespace_nested() { 136 + fn test_extract_namespace_deep() { 134 137 let base = Path::new(".mlf/lexicons/mlf"); 135 138 let path = Path::new(".mlf/lexicons/mlf/com/atproto/admin/defs.mlf"); 136 139 let namespace = extract_namespace_from_path(path, base).unwrap(); 137 - // On Unix: "com/atproto/admin/defs" -> "com.atproto.admin.defs" 138 - // Note: This depends on the directory structure 139 - assert!(namespace.contains("com")); 140 + // Full path "com/atproto/admin/defs" becomes "com.atproto.admin.defs" 141 + assert_eq!(namespace, "com.atproto.admin.defs"); 142 + } 143 + 144 + #[test] 145 + fn test_extract_namespace_root() { 146 + let base = Path::new(".mlf/lexicons/mlf"); 147 + let path = Path::new(".mlf/lexicons/mlf/simple.mlf"); 148 + let namespace = extract_namespace_from_path(path, base).unwrap(); 149 + // File "simple" becomes namespace "simple" 150 + assert_eq!(namespace, "simple"); 140 151 } 141 152 }
+25 -13
mlf-codegen/src/lib.rs
··· 159 159 count_type_references(&param.ty, &mut usage_counts); 160 160 } 161 161 match &query.returns { 162 + ReturnType::None { .. } => {} 162 163 ReturnType::Type(ty) => count_type_references(ty, &mut usage_counts), 163 164 ReturnType::TypeWithErrors { success, .. } => { 164 165 count_type_references(success, &mut usage_counts) ··· 170 171 count_type_references(&param.ty, &mut usage_counts); 171 172 } 172 173 match &procedure.returns { 174 + ReturnType::None { .. } => {} 173 175 ReturnType::Type(ty) => count_type_references(ty, &mut usage_counts), 174 176 ReturnType::TypeWithErrors { success, .. } => { 175 177 count_type_references(success, &mut usage_counts) ··· 180 182 for param in &subscription.params { 181 183 count_type_references(&param.ty, &mut usage_counts); 182 184 } 183 - count_type_references(&subscription.messages, &mut usage_counts); 185 + if let Some(messages) = &subscription.messages { 186 + count_type_references(messages, &mut usage_counts); 187 + } 184 188 } 185 189 Item::InlineType(inline_type) => { 186 190 count_type_references(&inline_type.ty, &mut usage_counts); ··· 292 296 }; 293 297 294 298 let output = match &query.returns { 299 + ReturnType::None { .. } => None, 295 300 ReturnType::Type(ty) => { 296 301 let mut output_obj = Map::new(); 297 302 output_obj.insert("encoding".to_string(), json!("application/json")); 298 303 output_obj.insert("schema".to_string(), generate_type_json(ty, usage_counts, workspace, current_namespace)); 299 - Value::Object(output_obj) 304 + Some(Value::Object(output_obj)) 300 305 } 301 306 ReturnType::TypeWithErrors { success, errors, .. } => { 302 307 let mut error_defs = Map::new(); ··· 313 318 output_obj.insert("encoding".to_string(), json!("application/json")); 314 319 output_obj.insert("schema".to_string(), generate_type_json(success, usage_counts, workspace, current_namespace)); 315 320 output_obj.insert("errors".to_string(), json!(error_defs)); 316 - Value::Object(output_obj) 321 + Some(Value::Object(output_obj)) 317 322 } 318 323 }; 319 324 ··· 321 326 query_obj.insert("type".to_string(), json!("query")); 322 327 query_obj.insert("description".to_string(), json!(extract_docs(&query.docs))); 323 328 query_obj.insert("parameters".to_string(), params); 324 - query_obj.insert("output".to_string(), output); 329 + if let Some(output_val) = output { 330 + query_obj.insert("output".to_string(), output_val); 331 + } 325 332 Value::Object(query_obj) 326 333 } 327 334 ··· 358 365 }; 359 366 360 367 let output = match &procedure.returns { 368 + ReturnType::None { .. } => None, 361 369 ReturnType::Type(ty) => { 362 370 let mut output_obj = Map::new(); 363 371 output_obj.insert("encoding".to_string(), json!("application/json")); 364 372 output_obj.insert("schema".to_string(), generate_type_json(ty, usage_counts, workspace, current_namespace)); 365 - Value::Object(output_obj) 373 + Some(Value::Object(output_obj)) 366 374 } 367 375 ReturnType::TypeWithErrors { success, errors, .. } => { 368 376 let mut error_defs = Map::new(); ··· 379 387 output_obj.insert("encoding".to_string(), json!("application/json")); 380 388 output_obj.insert("schema".to_string(), generate_type_json(success, usage_counts, workspace, current_namespace)); 381 389 output_obj.insert("errors".to_string(), json!(error_defs)); 382 - Value::Object(output_obj) 390 + Some(Value::Object(output_obj)) 383 391 } 384 392 }; 385 393 ··· 389 397 if let Some(input_val) = input { 390 398 result.insert("input".to_string(), input_val); 391 399 } 392 - result.insert("output".to_string(), output); 400 + if let Some(output_val) = output { 401 + result.insert("output".to_string(), output_val); 402 + } 393 403 Value::Object(result) 394 404 } 395 405 ··· 426 436 Value::Null 427 437 }; 428 438 429 - let message = json!({ 430 - "schema": generate_type_json(&subscription.messages, usage_counts, workspace, current_namespace) 431 - }); 432 - 433 439 let mut result = json!({ 434 440 "type": "subscription", 435 - "description": extract_docs(&subscription.docs), 436 - "message": message 441 + "description": extract_docs(&subscription.docs) 437 442 }); 443 + 444 + if let Some(messages) = &subscription.messages { 445 + let message = json!({ 446 + "schema": generate_type_json(messages, usage_counts, workspace, current_namespace) 447 + }); 448 + result["message"] = message; 449 + } 438 450 439 451 if !parameters.is_null() { 440 452 result["parameters"] = parameters;
+4 -1
mlf-lang/src/ast.rs
··· 164 164 pub annotations: Vec<Annotation>, 165 165 pub name: Ident, 166 166 pub params: Vec<Field>, 167 - pub messages: Type, // Union of message types 167 + pub messages: Option<Type>, // Union of message types (optional) 168 168 pub span: Span, 169 169 } 170 170 171 171 /// Return type for queries and procedures 172 172 #[derive(Debug, Clone, PartialEq)] 173 173 pub enum ReturnType { 174 + /// No return type specified 175 + None { span: Span }, 174 176 /// Simple return type 175 177 Type(Type), 176 178 /// Return type with error handling ··· 184 186 impl ReturnType { 185 187 pub fn span(&self) -> Span { 186 188 match self { 189 + ReturnType::None { span } => *span, 187 190 ReturnType::Type(ty) => ty.span(), 188 191 ReturnType::TypeWithErrors { span, .. } => *span, 189 192 }
+135 -48
mlf-lang/src/parser.rs
··· 386 386 387 387 let params = self.parse_params()?; 388 388 389 - self.expect(LexToken::RightParen)?; 390 - self.expect(LexToken::Colon)?; 389 + let right_paren_span = self.expect(LexToken::RightParen)?; 391 390 392 - let output = self.parse_base_type()?; 391 + // Check if there's a return type (colon present) 392 + let returns = if matches!(self.current().token, LexToken::Colon) { 393 + self.advance(); // consume colon 394 + 395 + let output = self.parse_base_type()?; 393 396 394 - let returns = if matches!(self.current().token, LexToken::Pipe) { 395 - self.advance(); 396 - if matches!(self.current().token, LexToken::Error) { 397 + if matches!(self.current().token, LexToken::Pipe) { 397 398 self.advance(); 398 - let errors = self.parse_errors()?; 399 - let error_span = errors.last().map(|e| e.span).unwrap_or(output.span()); 400 - ReturnType::TypeWithErrors { 401 - success: output, 402 - errors, 403 - span: error_span, 404 - } 405 - } else { 406 - let mut types = alloc::vec![output]; 407 - types.push(self.parse_base_type()?); 408 - 409 - while matches!(self.current().token, LexToken::Pipe) { 399 + if matches!(self.current().token, LexToken::Error) { 410 400 self.advance(); 401 + let errors = self.parse_errors()?; 402 + let error_span = errors.last().map(|e| e.span).unwrap_or(output.span()); 403 + ReturnType::TypeWithErrors { 404 + success: output, 405 + errors, 406 + span: error_span, 407 + } 408 + } else { 409 + let mut types = alloc::vec![output]; 411 410 types.push(self.parse_base_type()?); 411 + 412 + while matches!(self.current().token, LexToken::Pipe) { 413 + self.advance(); 414 + types.push(self.parse_base_type()?); 415 + } 416 + 417 + let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 418 + // Return type unions are open by default (no ! support in return types yet) 419 + ReturnType::Type(Type::Union { types, closed: false, span }) 412 420 } 413 - 414 - let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 415 - // Return type unions are open by default (no ! support in return types yet) 416 - ReturnType::Type(Type::Union { types, closed: false, span }) 421 + } else { 422 + ReturnType::Type(output) 417 423 } 418 424 } else { 419 - ReturnType::Type(output) 425 + // No return type specified 426 + ReturnType::None { span: right_paren_span } 420 427 }; 421 428 422 429 let end = self.expect(LexToken::Semicolon)?; ··· 438 445 439 446 let params = self.parse_params()?; 440 447 441 - self.expect(LexToken::RightParen)?; 442 - self.expect(LexToken::Colon)?; 448 + let right_paren_span = self.expect(LexToken::RightParen)?; 449 + 450 + // Check if there's a return type (colon present) 451 + let returns = if matches!(self.current().token, LexToken::Colon) { 452 + self.advance(); // consume colon 443 453 444 - let output = self.parse_base_type()?; 454 + let output = self.parse_base_type()?; 445 455 446 - let returns = if matches!(self.current().token, LexToken::Pipe) { 447 - self.advance(); 448 - if matches!(self.current().token, LexToken::Error) { 456 + if matches!(self.current().token, LexToken::Pipe) { 449 457 self.advance(); 450 - let errors = self.parse_errors()?; 451 - let error_span = errors.last().map(|e| e.span).unwrap_or(output.span()); 452 - ReturnType::TypeWithErrors { 453 - success: output, 454 - errors, 455 - span: error_span, 456 - } 457 - } else { 458 - let mut types = alloc::vec![output]; 459 - types.push(self.parse_base_type()?); 460 - 461 - while matches!(self.current().token, LexToken::Pipe) { 458 + if matches!(self.current().token, LexToken::Error) { 462 459 self.advance(); 460 + let errors = self.parse_errors()?; 461 + let error_span = errors.last().map(|e| e.span).unwrap_or(output.span()); 462 + ReturnType::TypeWithErrors { 463 + success: output, 464 + errors, 465 + span: error_span, 466 + } 467 + } else { 468 + let mut types = alloc::vec![output]; 463 469 types.push(self.parse_base_type()?); 464 - } 465 470 466 - let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 467 - // Return type unions are open by default (no ! support in return types yet) 468 - ReturnType::Type(Type::Union { types, closed: false, span }) 471 + while matches!(self.current().token, LexToken::Pipe) { 472 + self.advance(); 473 + types.push(self.parse_base_type()?); 474 + } 475 + 476 + let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 477 + // Return type unions are open by default (no ! support in return types yet) 478 + ReturnType::Type(Type::Union { types, closed: false, span }) 479 + } 480 + } else { 481 + ReturnType::Type(output) 469 482 } 470 483 } else { 471 - ReturnType::Type(output) 484 + // No return type specified 485 + ReturnType::None { span: right_paren_span } 472 486 }; 473 487 474 488 let end = self.expect(LexToken::Semicolon)?; ··· 491 505 let params = self.parse_params()?; 492 506 493 507 self.expect(LexToken::RightParen)?; 494 - self.expect(LexToken::Colon)?; 495 508 496 - let messages = self.parse_type()?; 509 + // Check if there's a messages type (colon present) 510 + let messages = if matches!(self.current().token, LexToken::Colon) { 511 + self.advance(); // consume colon 512 + Some(self.parse_type()?) 513 + } else { 514 + None 515 + }; 497 516 498 517 let end = self.expect(LexToken::Semicolon)?; 499 518 ··· 1413 1432 } 1414 1433 } 1415 1434 _ => panic!("Expected inline type"), 1435 + } 1436 + } 1437 + 1438 + #[test] 1439 + fn test_parse_query_without_return_type() { 1440 + let input = "query foo();"; 1441 + let result = parse_lexicon(input); 1442 + assert!(result.is_ok()); 1443 + let lexicon = result.unwrap(); 1444 + assert_eq!(lexicon.items.len(), 1); 1445 + match &lexicon.items[0] { 1446 + Item::Query(q) => { 1447 + assert_eq!(q.name.name, "foo"); 1448 + assert_eq!(q.params.len(), 0); 1449 + assert!(matches!(q.returns, ReturnType::None { .. })); 1450 + } 1451 + _ => panic!("Expected query"), 1452 + } 1453 + } 1454 + 1455 + #[test] 1456 + fn test_parse_query_with_params_no_return() { 1457 + let input = "query bar(id: string, count: integer);"; 1458 + let result = parse_lexicon(input); 1459 + assert!(result.is_ok()); 1460 + let lexicon = result.unwrap(); 1461 + assert_eq!(lexicon.items.len(), 1); 1462 + match &lexicon.items[0] { 1463 + Item::Query(q) => { 1464 + assert_eq!(q.name.name, "bar"); 1465 + assert_eq!(q.params.len(), 2); 1466 + assert!(matches!(q.returns, ReturnType::None { .. })); 1467 + } 1468 + _ => panic!("Expected query"), 1469 + } 1470 + } 1471 + 1472 + #[test] 1473 + fn test_parse_procedure_without_return_type() { 1474 + let input = "procedure baz();"; 1475 + let result = parse_lexicon(input); 1476 + assert!(result.is_ok()); 1477 + let lexicon = result.unwrap(); 1478 + assert_eq!(lexicon.items.len(), 1); 1479 + match &lexicon.items[0] { 1480 + Item::Procedure(p) => { 1481 + assert_eq!(p.name.name, "baz"); 1482 + assert_eq!(p.params.len(), 0); 1483 + assert!(matches!(p.returns, ReturnType::None { .. })); 1484 + } 1485 + _ => panic!("Expected procedure"), 1486 + } 1487 + } 1488 + 1489 + #[test] 1490 + fn test_parse_subscription_without_messages() { 1491 + let input = "subscription updates();"; 1492 + let result = parse_lexicon(input); 1493 + assert!(result.is_ok()); 1494 + let lexicon = result.unwrap(); 1495 + assert_eq!(lexicon.items.len(), 1); 1496 + match &lexicon.items[0] { 1497 + Item::Subscription(s) => { 1498 + assert_eq!(s.name.name, "updates"); 1499 + assert_eq!(s.params.len(), 0); 1500 + assert!(s.messages.is_none()); 1501 + } 1502 + _ => panic!("Expected subscription"), 1416 1503 } 1417 1504 } 1418 1505 }
+10 -2
mlf-lang/src/workspace.rs
··· 963 963 } 964 964 965 965 match &query.returns { 966 + ReturnType::None { .. } => { 967 + // No return type to resolve 968 + } 966 969 ReturnType::Type(ty) => { 967 970 if let Err(mut ty_errors) = self.resolve_type(namespace, ty) { 968 971 errors.append(&mut ty_errors); ··· 992 995 } 993 996 994 997 match &procedure.returns { 998 + ReturnType::None { .. } => { 999 + // No return type to resolve 1000 + } 995 1001 ReturnType::Type(ty) => { 996 1002 if let Err(mut ty_errors) = self.resolve_type(namespace, ty) { 997 1003 errors.append(&mut ty_errors); ··· 1020 1026 } 1021 1027 } 1022 1028 1023 - if let Err(mut msg_errors) = self.resolve_type(namespace, &subscription.messages) { 1024 - errors.append(&mut msg_errors); 1029 + if let Some(messages) = &subscription.messages { 1030 + if let Err(mut msg_errors) = self.resolve_type(namespace, messages) { 1031 + errors.append(&mut msg_errors); 1032 + } 1025 1033 } 1026 1034 1027 1035 if errors.is_empty() {