A human-friendly DSL for ATProto Lexicons

Annotations for encoding and record key selection

+151 -6
+61 -6
mlf-codegen/src/lib.rs
··· 72 72 annotations.iter().any(|ann| ann.name.name == "main") 73 73 } 74 74 75 + fn 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 + 89 + fn 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 + 75 115 pub fn generate_lexicon(namespace: &str, lexicon: &Lexicon, workspace: &Workspace) -> Value { 76 116 let usage_counts = analyze_type_usage(lexicon); 77 117 ··· 319 359 "properties": properties 320 360 }); 321 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 + 322 365 json!({ 323 366 "type": "record", 324 367 "description": extract_docs(&record.docs), 325 - "key": "tid", 368 + "key": key, 326 369 "record": record_obj 327 370 }) 328 371 } ··· 358 401 Value::Object(params_obj) 359 402 }; 360 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 + 361 408 let output = match &query.returns { 362 409 ReturnType::None { .. } => None, 363 410 ReturnType::Type(ty) => { 364 411 let mut output_obj = Map::new(); 365 - output_obj.insert("encoding".to_string(), json!("application/json")); 412 + output_obj.insert("encoding".to_string(), json!(output_encoding)); 366 413 output_obj.insert("schema".to_string(), generate_type_json(ty, usage_counts, workspace, current_namespace)); 367 414 Some(Value::Object(output_obj)) 368 415 } ··· 378 425 } 379 426 380 427 let mut output_obj = Map::new(); 381 - output_obj.insert("encoding".to_string(), json!("application/json")); 428 + output_obj.insert("encoding".to_string(), json!(output_encoding)); 382 429 output_obj.insert("schema".to_string(), generate_type_json(success, usage_counts, workspace, current_namespace)); 383 430 output_obj.insert("errors".to_string(), json!(error_defs)); 384 431 Some(Value::Object(output_obj)) ··· 413 460 params_properties.insert(param.name.name.clone(), param_json); 414 461 } 415 462 463 + // Check for @encoding annotation with "input" parameter, default to "application/json" 464 + let input_encoding = get_encoding_annotation(&procedure.annotations, "input") 465 + .unwrap_or_else(|| "application/json".to_string()); 466 + 416 467 let input = if !params_properties.is_empty() { 417 468 let mut schema_obj = Map::new(); 418 469 schema_obj.insert("type".to_string(), json!("object")); ··· 420 471 schema_obj.insert("properties".to_string(), json!(params_properties)); 421 472 422 473 let mut input_obj = Map::new(); 423 - input_obj.insert("encoding".to_string(), json!("application/json")); 474 + input_obj.insert("encoding".to_string(), json!(input_encoding)); 424 475 input_obj.insert("schema".to_string(), Value::Object(schema_obj)); 425 476 Some(Value::Object(input_obj)) 426 477 } else { 427 478 None 428 479 }; 429 480 481 + // Check for @encoding annotation with "output" parameter, default to "application/json" 482 + let output_encoding = get_encoding_annotation(&procedure.annotations, "output") 483 + .unwrap_or_else(|| "application/json".to_string()); 484 + 430 485 let output = match &procedure.returns { 431 486 ReturnType::None { .. } => None, 432 487 ReturnType::Type(ty) => { 433 488 let mut output_obj = Map::new(); 434 - output_obj.insert("encoding".to_string(), json!("application/json")); 489 + output_obj.insert("encoding".to_string(), json!(output_encoding)); 435 490 output_obj.insert("schema".to_string(), generate_type_json(ty, usage_counts, workspace, current_namespace)); 436 491 Some(Value::Object(output_obj)) 437 492 } ··· 447 502 } 448 503 449 504 let mut output_obj = Map::new(); 450 - output_obj.insert("encoding".to_string(), json!("application/json")); 505 + output_obj.insert("encoding".to_string(), json!(output_encoding)); 451 506 output_obj.insert("schema".to_string(), generate_type_json(success, usage_counts, workspace, current_namespace)); 452 507 output_obj.insert("errors".to_string(), json!(error_defs)); 453 508 Some(Value::Object(output_obj))
+2
website/content/docs/language-guide/01-your-first-lexicon.md
··· 74 74 75 75 The MLF syntax is much cleaner and easier to read! 76 76 77 + **Note:** The `"key": "tid"` field specifies that records use timestamp-based identifiers by default. You can customize this with the `@key` annotation. See [Annotations](/docs/language-guide/annotations/#key-for-records) for details. 78 + 77 79 ## Comments 78 80 79 81 MLF supports three types of comments:
+24
website/content/docs/language-guide/07-xrpc.md
··· 241 241 query getPost(uri: AtUri):post | deleted; 242 242 ``` 243 243 244 + ## Custom Encoding 245 + 246 + By default, all XRPC input and output uses `application/json` encoding. You can customize this using the `@encoding` annotation: 247 + 248 + **Query with custom output encoding:** 249 + ```mlf 250 + @encoding("application/cbor") 251 + query getBinaryData():bytes; 252 + ``` 253 + 254 + **Procedure with custom input encoding:** 255 + ```mlf 256 + @encoding(input: "text/plain") 257 + procedure parseText(content!: string):result; 258 + ``` 259 + 260 + **Procedure with different input/output encodings:** 261 + ```mlf 262 + @encoding(input: "application/xml", output: "application/json") 263 + procedure convertXmlToJson(xml!: string):object; 264 + ``` 265 + 266 + See [Annotations](/docs/language-guide/annotations/#encoding-for-queries-and-procedures) for more details on `@encoding`. 267 + 244 268 ## Complete Example 245 269 246 270 Here's a complete API for a forum:
+64
website/content/docs/language-guide/11-annotations.md
··· 126 126 - `@typescript:*` - TypeScript code generator annotations 127 127 - `@go:*` - Go code generator annotations 128 128 129 + ## MLF Built-in Annotations 130 + 131 + MLF's lexicon generator recognizes specific annotations that affect the generated ATProto JSON lexicon: 132 + 133 + ### `@key` for Records 134 + 135 + Controls the record key type. Defaults to `"tid"` if not specified. 136 + 137 + ```mlf 138 + // Use literal "self" as the record key 139 + @key("literal:self") 140 + record profile { 141 + name!: string, 142 + } 143 + 144 + // Use timestamp-based identifier (default) 145 + record post { 146 + text!: string, 147 + } 148 + ``` 149 + 150 + **Common key values:** 151 + - `"tid"` - Timestamp-based identifier (default) 152 + - `"literal:self"` - The record key is literally "self" 153 + - Custom values as needed for your schema 154 + 155 + ### `@encoding` for Queries and Procedures 156 + 157 + Controls MIME type encoding for XRPC input and output. Defaults to `"application/json"` if not specified. 158 + 159 + **Positional syntax** (applies to output for queries, both input/output for procedures): 160 + 161 + ```mlf 162 + @encoding("application/cbor") 163 + query getData(): string; 164 + 165 + @encoding("application/json") 166 + procedure upload(data!: string): string; 167 + ``` 168 + 169 + **Named syntax** (explicit control): 170 + 171 + ```mlf 172 + // Output only 173 + @encoding(output: "text/plain") 174 + query getText(): string; 175 + 176 + // Input only 177 + @encoding(input: "application/xml") 178 + procedure parse(data!: string): result; 179 + 180 + // Both input and output 181 + @encoding(input: "application/cbor", output: "application/json") 182 + procedure convert(data!: bytes): object; 183 + ``` 184 + 185 + **Common encoding values:** 186 + - `"application/json"` - JSON (default) 187 + - `"application/cbor"` - CBOR binary format 188 + - `"text/plain"` - Plain text 189 + - `"application/xml"` - XML 190 + - `"*/*"` - Any MIME type 191 + - Custom MIME types as needed 192 + 129 193 ## Annotation Processing 130 194 131 195 Annotations are preserved in the MLF AST and can be accessed by: