A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

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

Switch to default optional for records/objects and default open for unions

+424 -406
+70 -68
SPEC.md
··· 170 170 171 171 ```mlf 172 172 record post { 173 - text: string constrained { 173 + text!: string constrained { 174 174 maxLength: 300 175 175 maxGraphemes: 300 176 176 } 177 - createdAt: Datetime 178 - reply?: replyRef // Optional field 177 + createdAt!: Datetime 178 + reply: replyRef // Optional field (default) 179 179 } 180 180 ``` 181 181 ··· 195 195 196 196 ```mlf 197 197 def type ReplyRef = { 198 - root: AtUri 199 - parent: AtUri 198 + root!: AtUri 199 + parent!: AtUri 200 200 }; 201 201 ``` 202 202 ··· 214 214 token closed; 215 215 216 216 record issue { 217 - state: string constrained { 217 + state!: string constrained { 218 218 knownValues: [ 219 219 open // References token defined above 220 220 closed ··· 234 234 /// Get a user profile 235 235 query getProfile( 236 236 /// The actor's DID or handle 237 - actor: AtIdentifier 238 - /// Optional viewer context 239 - viewer?: Did 237 + actor!: AtIdentifier 238 + /// Optional viewer context (default) 239 + viewer: Did 240 240 ): profileView | error { 241 241 /// Profile not found 242 242 ProfileNotFound ··· 252 252 ```mlf 253 253 /// Create a new post 254 254 procedure createPost( 255 - text: string 256 - createdAt: Datetime 255 + text!: string 256 + createdAt!: Datetime 257 257 ): { 258 - uri: AtUri 259 - cid: Cid 258 + uri!: AtUri 259 + cid!: Cid 260 260 } | error { 261 261 /// Text exceeds maximum length 262 262 TextTooLong ··· 270 270 ```mlf 271 271 /// Subscribe to repository events 272 272 subscription subscribeRepos( 273 - /// Optional cursor for resuming from a specific point 274 - cursor?: integer 273 + /// Optional cursor for resuming from a specific point (default) 274 + cursor: integer 275 275 ): commit | identity | handle | migrate | tombstone | info; 276 276 ``` 277 277 ··· 280 280 ```mlf 281 281 /// Commit message emitted by subscribeRepos 282 282 def type commit = { 283 - seq: integer 284 - rebase: boolean 285 - tooBig: boolean 286 - repo: Did 287 - commit: Cid 288 - rev: string 289 - since: string 290 - blocks: bytes 291 - ops: repoOp[] 292 - blobs: Cid[] 293 - time: Datetime 283 + seq!: integer 284 + rebase!: boolean 285 + tooBig!: boolean 286 + repo!: Did 287 + commit!: Cid 288 + rev!: string 289 + since!: string 290 + blocks!: bytes 291 + ops!: repoOp[] 292 + blobs!: Cid[] 293 + time!: Datetime 294 294 }; 295 295 296 296 /// Info message 297 297 def type info = { 298 - name: string 299 - message?: string 298 + name!: string 299 + message: string // Optional (default) 300 300 }; 301 301 ``` 302 302 ··· 315 315 /// Subscribe to chat messages for a stream 316 316 subscription subscribeChat( 317 317 /// The DID of the streamer 318 - streamer: Did 319 - /// Optional cursor to resume from 320 - cursor?: string 318 + streamer!: Did 319 + /// Optional cursor to resume from (default) 320 + cursor: string 321 321 ): message | delete | join | leave; 322 322 323 323 /// Chat message payload 324 324 def type message = { 325 - id: string 326 - text: string 327 - author: Did 328 - createdAt: Datetime 325 + id!: string 326 + text!: string 327 + author!: Did 328 + createdAt!: Datetime 329 329 }; 330 330 331 331 /// Delete event payload 332 332 def type delete = { 333 - id: string 333 + id!: string 334 334 }; 335 335 336 336 /// Join event payload 337 337 def type join = { 338 - user: Did 338 + user!: Did 339 339 }; 340 340 341 341 /// Leave event payload 342 342 def type leave = { 343 - user: Did 343 + user!: Did 344 344 }; 345 345 ``` 346 346 ··· 355 355 356 356 ## Type Modifiers 357 357 358 - ### Optional Fields 358 + ### Optional and Required Fields 359 + 360 + Fields are **optional by default**. Use `!:` to mark a field as required: 359 361 360 362 ```mlf 361 363 record example { 362 - required: string 363 - optional?: string 364 + optional: string // Optional (default) 365 + required!: string // Required (marked with !) 364 366 } 365 367 ``` 366 368 ··· 378 380 379 381 ### Unions 380 382 381 - Use the pipe operator `|`: 383 + Use the pipe operator `|`. Unions are **open by default** (allowing unknown types): 382 384 383 385 ```mlf 384 386 record example { 385 - // Closed union (only these types) 387 + // Open union (default, can include unknown types) 386 388 content: text | image | video 387 389 388 - // Union of tokens 390 + // Union of tokens (also open by default) 389 391 state: open | closed | pending 390 392 } 391 393 ``` 392 394 393 - Open unions (allowing unknown types) use `_`: 395 + Closed unions (only allowing listed types) use `| !`: 394 396 395 397 ```mlf 396 398 record example { 397 - // Open union (can include unknown types) 398 - content: text | image | _ 399 + // Closed union (marked with !, only these types allowed) 400 + content: text | image | video | ! 399 401 } 400 402 ``` 401 403 ··· 590 592 record profile { 591 593 /// User's DID 592 594 @indexed 593 - did: Did 595 + did!: Did 594 596 595 - /// Display name 597 + /// Display name (optional) 596 598 @sensitive(pii: true) 597 - displayName?: string 599 + displayName: string 598 600 } 599 601 ``` 600 602 ··· 743 745 /// An issue in a repository 744 746 record issue { 745 747 /// The repository this issue belongs to 746 - repo: AtUri 748 + repo!: AtUri 747 749 /// Issue title 748 - title: string constrained { 750 + title!: string constrained { 749 751 minGraphemes: 1 750 752 maxGraphemes: 200 751 753 } 752 754 /// Issue body (markdown) 753 - body?: string constrained { 755 + body: string constrained { 754 756 maxGraphemes: 10000 755 757 } 756 758 /// Issue state 757 - state: string constrained { 759 + state!: string constrained { 758 760 knownValues: [ 759 761 open 760 762 closed ··· 762 764 default: "open" 763 765 } 764 766 /// Creation timestamp 765 - createdAt: Datetime 767 + createdAt!: Datetime 766 768 } 767 769 768 770 /// A comment on an issue 769 771 record comment { 770 772 /// The issue this comment belongs to 771 - issue: AtUri 773 + issue!: AtUri 772 774 /// Comment body (markdown) 773 - body: string constrained { 775 + body!: string constrained { 774 776 minGraphemes: 1 775 777 maxGraphemes: 10000 776 778 } 777 779 /// Creation timestamp 778 - createdAt: Datetime 780 + createdAt!: Datetime 779 781 /// Optional reply target 780 - replyTo?: AtUri 782 + replyTo: AtUri 781 783 } 782 784 783 785 /// Get an issue by URI 784 786 query getIssue( 785 787 /// Issue AT-URI 786 - uri: AtUri 788 + uri!: AtUri 787 789 ): issue | error { 788 790 /// Issue not found 789 791 NotFound ··· 791 793 792 794 /// Create a new issue 793 795 procedure createIssue( 794 - repo: AtUri 795 - title: string 796 - body?: string 796 + repo!: AtUri 797 + title!: string 798 + body: string // Optional (default) 797 799 ): { 798 - uri: AtUri 799 - cid: Cid 800 + uri!: AtUri 801 + cid!: Cid 800 802 } | error { 801 803 /// Repository not found 802 804 RepoNotFound ··· 814 816 **MLF:** 815 817 ```mlf 816 818 record post { 817 - text: string constrained { 819 + text!: string constrained { 818 820 maxLength: 300 819 821 } 820 - createdAt: Datetime 822 + createdAt!: Datetime 821 823 } 822 824 ``` 823 825 ··· 854 856 **MLF:** 855 857 ```mlf 856 858 subscription subscribeRepos( 857 - cursor?: integer 859 + cursor: integer // Optional (default) 858 860 ): commit | identity; 859 861 ``` 860 862
+5 -2
mlf-lang/src/lexer.rs
··· 50 50 Dot, 51 51 Pipe, 52 52 Question, 53 + Exclamation, 53 54 At, 54 55 Equals, 55 56 Semicolon, ··· 100 101 Token::Dot => write!(f, "."), 101 102 Token::Pipe => write!(f, "|"), 102 103 Token::Question => write!(f, "?"), 104 + Token::Exclamation => write!(f, "!"), 103 105 Token::At => write!(f, "@"), 104 106 Token::Equals => write!(f, "="), 105 107 Token::Semicolon => write!(f, ";"), ··· 260 262 map(char('.'), |_| Token::Dot), 261 263 map(char('|'), |_| Token::Pipe), 262 264 map(char('?'), |_| Token::Question), 265 + map(char('!'), |_| Token::Exclamation), 263 266 map(char('@'), |_| Token::At), 264 267 map(char('='), |_| Token::Equals), 265 268 map(char(';'), |_| Token::Semicolon), ··· 341 344 342 345 #[test] 343 346 fn test_keywords() { 344 - let input = "record alias query"; 347 + let input = "record inline query"; 345 348 let tokens = tokenize(input).unwrap(); 346 349 assert_eq!(tokens[0].token, Token::Record); 347 - assert_eq!(tokens[1].token, Token::Alias); 350 + assert_eq!(tokens[1].token, Token::Inline); 348 351 assert_eq!(tokens[2].token, Token::Query); 349 352 } 350 353
+57 -53
mlf-lang/src/parser.rs
··· 301 301 let annotations = self.parse_annotations()?; 302 302 let name = self.parse_field_name()?; 303 303 304 - let optional = if matches!(self.current().token, LexToken::Question) { 304 + // Check for ! to mark as required (default is optional) 305 + let optional = if matches!(self.current().token, LexToken::Exclamation) { 305 306 self.advance(); 306 - true 307 + false // ! means required 307 308 } else { 308 - false 309 + true // default is optional 309 310 }; 310 311 311 312 self.expect(LexToken::Colon)?; ··· 411 412 } 412 413 413 414 let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 414 - // Return type unions are closed by default (no _ support in return types yet) 415 - ReturnType::Type(Type::Union { types, closed: true, span }) 415 + // Return type unions are open by default (no ! support in return types yet) 416 + ReturnType::Type(Type::Union { types, closed: false, span }) 416 417 } 417 418 } else { 418 419 ReturnType::Type(output) ··· 463 464 } 464 465 465 466 let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 466 - // Return type unions are closed by default (no _ support in return types yet) 467 - ReturnType::Type(Type::Union { types, closed: true, span }) 467 + // Return type unions are open by default (no ! support in return types yet) 468 + ReturnType::Type(Type::Union { types, closed: false, span }) 468 469 } 469 470 } else { 470 471 ReturnType::Type(output) ··· 546 547 let annotations = self.parse_annotations()?; 547 548 let name = self.parse_ident()?; 548 549 549 - let optional = if matches!(self.current().token, LexToken::Question) { 550 + // Check for ! to mark as required (default is optional) 551 + let optional = if matches!(self.current().token, LexToken::Exclamation) { 550 552 self.advance(); 551 - true 553 + false // ! means required 552 554 } else { 553 - false 555 + true // default is optional 554 556 }; 555 557 556 558 self.expect(LexToken::Colon)?; ··· 625 627 if matches!(self.current().token, LexToken::Pipe) { 626 628 let mut types = alloc::vec![base]; 627 629 628 - let mut has_underscore = false; 630 + let mut has_exclamation = false; 629 631 while matches!(self.current().token, LexToken::Pipe) { 630 632 self.advance(); 631 633 if matches!(self.current().token, LexToken::Error) { 632 634 break; 633 635 } 634 - // Check if this is an underscore (open union marker) 635 - if matches!(self.current().token, LexToken::Underscore) { 636 - has_underscore = true; 636 + // Check if this is an exclamation mark (closed union marker) 637 + if matches!(self.current().token, LexToken::Exclamation) { 638 + has_exclamation = true; 637 639 self.advance(); 638 640 } else { 639 641 types.push(self.parse_base_type()?); ··· 641 643 } 642 644 643 645 let span = Span::new(types[0].span().start, types.last().unwrap().span().end); 644 - // Unions are closed by default, open if _ is present 645 - let closed = !has_underscore; 646 + // Unions are open by default, closed if ! is present 647 + let closed = has_exclamation; 646 648 return Ok(Type::Union { types, closed, span }); 647 649 } 648 650 ··· 1157 1159 let input = r#"record user { 1158 1160 name: string, 1159 1161 age: integer, 1160 - };"#; 1162 + }"#; 1161 1163 let result = parse_lexicon(input); 1162 1164 assert!(result.is_ok()); 1163 1165 let lexicon = result.unwrap(); ··· 1172 1174 } 1173 1175 1174 1176 #[test] 1175 - fn test_parse_alias() { 1176 - let input = "alias userId = string;"; 1177 + fn test_parse_inline_type() { 1178 + let input = "inline type userId = string;"; 1177 1179 let result = parse_lexicon(input); 1178 1180 assert!(result.is_ok()); 1179 1181 let lexicon = result.unwrap(); 1180 1182 assert_eq!(lexicon.items.len(), 1); 1181 1183 match &lexicon.items[0] { 1182 - Item::Alias(a) => { 1184 + Item::InlineType(a) => { 1183 1185 assert_eq!(a.name.name, "userId"); 1184 1186 } 1185 - _ => panic!("Expected alias"), 1187 + _ => panic!("Expected inline type"), 1186 1188 } 1187 1189 } 1188 1190 ··· 1247 1249 1248 1250 #[test] 1249 1251 fn test_parse_subscription() { 1250 - let input = "subscription subscribeRepos(cursor?: integer,): commit | identity;"; 1252 + let input = "subscription subscribeRepos(cursor: integer,): commit | identity;"; 1251 1253 let result = parse_lexicon(input); 1252 1254 assert!(result.is_ok()); 1253 1255 let lexicon = result.unwrap(); ··· 1263 1265 } 1264 1266 1265 1267 #[test] 1266 - fn test_parse_namespace() { 1267 - let input = "namespace actor;"; 1268 - let result = parse_lexicon(input); 1269 - assert!(result.is_ok()); 1270 - let lexicon = result.unwrap(); 1271 - assert_eq!(lexicon.items.len(), 1); 1272 - match &lexicon.items[0] { 1273 - Item::Namespace(n) => { 1274 - assert_eq!(n.name.name, "actor"); 1275 - } 1276 - _ => panic!("Expected namespace"), 1277 - } 1278 - } 1279 - 1280 - #[test] 1281 1268 fn test_parse_constrained_type() { 1282 - let input = r#"alias shortString = string constrained { 1269 + let input = r#"inline type shortString = string constrained { 1283 1270 maxLength: 100, 1284 1271 };"#; 1285 1272 let result = parse_lexicon(input); ··· 1287 1274 let lexicon = result.unwrap(); 1288 1275 assert_eq!(lexicon.items.len(), 1); 1289 1276 match &lexicon.items[0] { 1290 - Item::Alias(a) => { 1277 + Item::InlineType(a) => { 1291 1278 match &a.ty { 1292 1279 Type::Constrained { constraints, .. } => { 1293 1280 assert_eq!(constraints.len(), 1); ··· 1295 1282 _ => panic!("Expected constrained type"), 1296 1283 } 1297 1284 } 1298 - _ => panic!("Expected alias"), 1285 + _ => panic!("Expected inline type"), 1299 1286 } 1300 1287 } 1301 1288 1302 1289 #[test] 1303 1290 fn test_parse_union_type() { 1304 - let input = "alias result = success | failure;"; 1291 + let input = "inline type result = success | failure;"; 1305 1292 let result = parse_lexicon(input); 1306 1293 assert!(result.is_ok()); 1307 1294 let lexicon = result.unwrap(); 1308 1295 assert_eq!(lexicon.items.len(), 1); 1309 1296 match &lexicon.items[0] { 1310 - Item::Alias(a) => { 1297 + Item::InlineType(a) => { 1311 1298 match &a.ty { 1312 1299 Type::Union { types, .. } => { 1313 1300 assert_eq!(types.len(), 2); ··· 1315 1302 _ => panic!("Expected union type"), 1316 1303 } 1317 1304 } 1318 - _ => panic!("Expected alias"), 1305 + _ => panic!("Expected inline type"), 1319 1306 } 1320 1307 } 1321 1308 1322 1309 #[test] 1323 1310 fn test_parse_array_type() { 1324 - let input = "alias userList = [user];"; 1311 + let input = "inline type userList = [user];"; 1325 1312 let result = parse_lexicon(input); 1326 1313 assert!(result.is_ok()); 1327 1314 let lexicon = result.unwrap(); 1328 1315 assert_eq!(lexicon.items.len(), 1); 1329 1316 match &lexicon.items[0] { 1330 - Item::Alias(a) => { 1317 + Item::InlineType(a) => { 1331 1318 match &a.ty { 1332 1319 Type::Array { .. } => {} 1333 1320 _ => panic!("Expected array type"), 1334 1321 } 1335 1322 } 1336 - _ => panic!("Expected alias"), 1323 + _ => panic!("Expected inline type"), 1337 1324 } 1338 1325 } 1339 1326 1340 1327 #[test] 1341 1328 fn test_parse_annotation() { 1342 - let input = "@deprecated\nrecord old {};"; 1329 + let input = "@deprecated\nrecord old {}"; 1343 1330 let result = parse_lexicon(input); 1344 1331 assert!(result.is_ok()); 1345 1332 let lexicon = result.unwrap(); ··· 1355 1342 1356 1343 #[test] 1357 1344 fn test_parse_annotation_with_args() { 1358 - let input = "@validate(min: 0, max: 100)\nrecord data {};"; 1345 + let input = "@validate(min: 0, max: 100)\nrecord data {}"; 1359 1346 let result = parse_lexicon(input); 1360 1347 assert!(result.is_ok()); 1361 1348 let lexicon = result.unwrap(); ··· 1372 1359 #[test] 1373 1360 fn test_parse_optional_field() { 1374 1361 let input = r#"record user { 1375 - name?: string, 1376 - };"#; 1362 + name: string, 1363 + }"#; 1377 1364 let result = parse_lexicon(input); 1378 1365 assert!(result.is_ok()); 1379 1366 let lexicon = result.unwrap(); ··· 1387 1374 } 1388 1375 1389 1376 #[test] 1377 + fn test_parse_required_field() { 1378 + let input = r#"record user { 1379 + name!: string, 1380 + }"#; 1381 + let result = parse_lexicon(input); 1382 + assert!(result.is_ok()); 1383 + let lexicon = result.unwrap(); 1384 + match &lexicon.items[0] { 1385 + Item::Record(r) => { 1386 + assert_eq!(r.fields.len(), 1); 1387 + assert!(!r.fields[0].optional); 1388 + } 1389 + _ => panic!("Expected record"), 1390 + } 1391 + } 1392 + 1393 + #[test] 1390 1394 fn test_parse_enum_constraint() { 1391 - let input = r#"alias status = string constrained { 1395 + let input = r#"inline type status = string constrained { 1392 1396 enum: ["active", "inactive"], 1393 1397 };"#; 1394 1398 let result = parse_lexicon(input); 1395 1399 assert!(result.is_ok()); 1396 1400 let lexicon = result.unwrap(); 1397 1401 match &lexicon.items[0] { 1398 - Item::Alias(a) => { 1402 + Item::InlineType(a) => { 1399 1403 match &a.ty { 1400 1404 Type::Constrained { constraints, .. } => { 1401 1405 match &constraints[0] { ··· 1408 1412 _ => panic!("Expected constrained type"), 1409 1413 } 1410 1414 } 1411 - _ => panic!("Expected alias"), 1415 + _ => panic!("Expected inline type"), 1412 1416 } 1413 1417 } 1414 1418 }
+45 -45
mlf-lang/src/workspace.rs
··· 1151 1151 #[test] 1152 1152 fn test_workspace_basic() { 1153 1153 let mut ws = Workspace::new(); 1154 - let input = "record foo { bar: string, };"; 1154 + let input = "record foo { bar: string, }"; 1155 1155 let lexicon = parse_lexicon(input).unwrap(); 1156 1156 assert!(ws.add_module("test".into(), lexicon).is_ok()); 1157 1157 } ··· 1159 1159 #[test] 1160 1160 fn test_duplicate_definition() { 1161 1161 let mut ws = Workspace::new(); 1162 - let input = "record foo {}; alias foo = string;"; 1162 + let input = "record foo {} inline type foo = string;"; 1163 1163 let lexicon = parse_lexicon(input).unwrap(); 1164 1164 let result = ws.add_module("test".into(), lexicon); 1165 1165 assert!(result.is_err()); ··· 1168 1168 #[test] 1169 1169 fn test_undefined_reference() { 1170 1170 let mut ws = Workspace::new(); 1171 - let input = "record foo { bar: unknown_type, };"; 1171 + let input = "record foo { bar: unknown_type, }"; 1172 1172 let lexicon = parse_lexicon(input).unwrap(); 1173 1173 ws.add_module("test".into(), lexicon).unwrap(); 1174 1174 let result = ws.resolve(); ··· 1178 1178 #[test] 1179 1179 fn test_self_reference() { 1180 1180 let mut ws = Workspace::new(); 1181 - let input = "record foo { bar: foo, };"; 1181 + let input = "record foo { bar: foo, }"; 1182 1182 let lexicon = parse_lexicon(input).unwrap(); 1183 1183 ws.add_module("test".into(), lexicon).unwrap(); 1184 1184 assert!(ws.resolve().is_ok()); ··· 1188 1188 fn test_cross_module_reference() { 1189 1189 let mut ws = Workspace::new(); 1190 1190 1191 - let a = parse_lexicon("record foo {};").unwrap(); 1191 + let a = parse_lexicon("record foo {}").unwrap(); 1192 1192 ws.add_module("a".into(), a).unwrap(); 1193 1193 1194 - let b = parse_lexicon("record bar { baz: a.foo, };").unwrap(); 1194 + let b = parse_lexicon("record bar { baz: a.foo, }").unwrap(); 1195 1195 ws.add_module("b".into(), b).unwrap(); 1196 1196 1197 1197 assert!(ws.resolve().is_ok()); ··· 1209 1209 fn test_use_import_all() { 1210 1210 let mut ws = Workspace::new(); 1211 1211 1212 - let a = parse_lexicon("record foo {}; alias bar = string;").unwrap(); 1212 + let a = parse_lexicon("record foo {} inline type bar = string;").unwrap(); 1213 1213 ws.add_module("a".into(), a).unwrap(); 1214 1214 1215 - let b = parse_lexicon("use a; record baz { x: foo, y: bar, };").unwrap(); 1215 + let b = parse_lexicon("use a; record baz { x: foo, y: bar, }").unwrap(); 1216 1216 ws.add_module("b".into(), b).unwrap(); 1217 1217 1218 1218 assert!(ws.resolve().is_ok()); ··· 1222 1222 fn test_use_import_with_alias() { 1223 1223 let mut ws = Workspace::new(); 1224 1224 1225 - let a = parse_lexicon("record post {};").unwrap(); 1225 + let a = parse_lexicon("record post {}").unwrap(); 1226 1226 ws.add_module("app.bsky.feed".into(), a).unwrap(); 1227 1227 1228 - let b = parse_lexicon("use app.bsky.feed.post as FeedPost; record like { subject: FeedPost, };").unwrap(); 1228 + let b = parse_lexicon("use app.bsky.feed.post as FeedPost; record like { subject: FeedPost, }").unwrap(); 1229 1229 ws.add_module("app.bsky.feed.like".into(), b).unwrap(); 1230 1230 1231 1231 let result = ws.resolve(); ··· 1239 1239 fn test_use_undefined_module() { 1240 1240 let mut ws = Workspace::new(); 1241 1241 1242 - let a = parse_lexicon("use nonexistent; record foo {};").unwrap(); 1242 + let a = parse_lexicon("use nonexistent; record foo {}").unwrap(); 1243 1243 ws.add_module("test".into(), a).unwrap(); 1244 1244 1245 1245 let result = ws.resolve(); ··· 1250 1250 fn test_use_undefined_type() { 1251 1251 let mut ws = Workspace::new(); 1252 1252 1253 - let a = parse_lexicon("record foo {};").unwrap(); 1253 + let a = parse_lexicon("record foo {}").unwrap(); 1254 1254 ws.add_module("a".into(), a).unwrap(); 1255 1255 1256 - let b = parse_lexicon("use a.bar; record baz {};").unwrap(); 1256 + let b = parse_lexicon("use a.bar; record baz {}").unwrap(); 1257 1257 ws.add_module("b".into(), b).unwrap(); 1258 1258 1259 1259 let result = ws.resolve(); ··· 1264 1264 fn test_typecheck_string_constraint_on_string() { 1265 1265 let mut ws = Workspace::new(); 1266 1266 1267 - let input = r#"alias shortString = string constrained { 1267 + let input = r#"inline type shortString = string constrained { 1268 1268 maxLength: 100, 1269 1269 };"#; 1270 1270 let lexicon = parse_lexicon(input).unwrap(); ··· 1277 1277 fn test_typecheck_string_constraint_on_integer() { 1278 1278 let mut ws = Workspace::new(); 1279 1279 1280 - let input = r#"alias constrainedInt = integer constrained { 1280 + let input = r#"inline type constrainedInt = integer constrained { 1281 1281 maxLength: 100, 1282 1282 };"#; 1283 1283 let lexicon = parse_lexicon(input).unwrap(); ··· 1291 1291 fn test_typecheck_numeric_constraint_on_integer() { 1292 1292 let mut ws = Workspace::new(); 1293 1293 1294 - let input = r#"alias positiveInt = integer constrained { 1294 + let input = r#"inline type positiveInt = integer constrained { 1295 1295 minimum: 0, 1296 1296 };"#; 1297 1297 let lexicon = parse_lexicon(input).unwrap(); ··· 1304 1304 fn test_typecheck_numeric_constraint_on_string() { 1305 1305 let mut ws = Workspace::new(); 1306 1306 1307 - let input = r#"alias constrainedString = string constrained { 1307 + let input = r#"inline type constrainedString = string constrained { 1308 1308 minimum: 0, 1309 1309 };"#; 1310 1310 let lexicon = parse_lexicon(input).unwrap(); ··· 1319 1319 let mut ws = Workspace::new(); 1320 1320 1321 1321 let input = r#" 1322 - alias shortString = string constrained { 1322 + inline type shortString = string constrained { 1323 1323 maxLength: 100, 1324 1324 }; 1325 1325 1326 - alias tinyString = shortString constrained { 1326 + inline type tinyString = shortString constrained { 1327 1327 maxLength: 50, 1328 1328 }; 1329 1329 "#; ··· 1338 1338 let mut ws = Workspace::new(); 1339 1339 1340 1340 let input = r#" 1341 - alias shortString = string constrained { 1341 + inline type shortString = string constrained { 1342 1342 maxLength: 100, 1343 1343 }; 1344 1344 1345 - alias tinyString = shortString constrained { 1345 + inline type tinyString = shortString constrained { 1346 1346 maxLength: 50, 1347 1347 }; 1348 1348 "#; ··· 1357 1357 let mut ws = Workspace::new(); 1358 1358 1359 1359 let input = r#" 1360 - alias shortString = string constrained { 1360 + inline type shortString = string constrained { 1361 1361 maxLength: 50, 1362 1362 }; 1363 1363 1364 - alias longString = shortString constrained { 1364 + inline type longString = shortString constrained { 1365 1365 maxLength: 100, 1366 1366 }; 1367 1367 "#; ··· 1379 1379 let mut ws = Workspace::new(); 1380 1380 1381 1381 let input = r#" 1382 - alias minString = string constrained { 1382 + inline type minString = string constrained { 1383 1383 minLength: 10, 1384 1384 }; 1385 1385 1386 - alias longerMinString = minString constrained { 1386 + inline type longerMinString = minString constrained { 1387 1387 minLength: 20, 1388 1388 }; 1389 1389 "#; ··· 1398 1398 let mut ws = Workspace::new(); 1399 1399 1400 1400 let input = r#" 1401 - alias minString = string constrained { 1401 + inline type minString = string constrained { 1402 1402 minLength: 20, 1403 1403 }; 1404 1404 1405 - alias shorterMinString = minString constrained { 1405 + inline type shorterMinString = minString constrained { 1406 1406 minLength: 10, 1407 1407 }; 1408 1408 "#; ··· 1418 1418 let mut ws = Workspace::new(); 1419 1419 1420 1420 let input = r#" 1421 - alias largeInt = integer constrained { 1421 + inline type largeInt = integer constrained { 1422 1422 maximum: 1000, 1423 1423 }; 1424 1424 1425 - alias smallInt = largeInt constrained { 1425 + inline type smallInt = largeInt constrained { 1426 1426 maximum: 100, 1427 1427 }; 1428 1428 "#; ··· 1437 1437 let mut ws = Workspace::new(); 1438 1438 1439 1439 let input = r#" 1440 - alias smallInt = integer constrained { 1440 + inline type smallInt = integer constrained { 1441 1441 maximum: 100, 1442 1442 }; 1443 1443 1444 - alias largeInt = smallInt constrained { 1444 + inline type largeInt = smallInt constrained { 1445 1445 maximum: 1000, 1446 1446 }; 1447 1447 "#; ··· 1457 1457 let mut ws = Workspace::new(); 1458 1458 1459 1459 let input = r#" 1460 - alias positiveInt = integer constrained { 1460 + inline type positiveInt = integer constrained { 1461 1461 minimum: 0, 1462 1462 }; 1463 1463 1464 - alias strictPositiveInt = positiveInt constrained { 1464 + inline type strictPositiveInt = positiveInt constrained { 1465 1465 minimum: 1, 1466 1466 }; 1467 1467 "#; ··· 1476 1476 let mut ws = Workspace::new(); 1477 1477 1478 1478 let input = r#" 1479 - alias positiveInt = integer constrained { 1479 + inline type positiveInt = integer constrained { 1480 1480 minimum: 10, 1481 1481 }; 1482 1482 1483 - alias lowerInt = positiveInt constrained { 1483 + inline type lowerInt = positiveInt constrained { 1484 1484 minimum: 5, 1485 1485 }; 1486 1486 "#; ··· 1496 1496 let mut ws = Workspace::new(); 1497 1497 1498 1498 let input = r#" 1499 - alias colorEnum = string constrained { 1499 + inline type colorEnum = string constrained { 1500 1500 enum: ["red", "green", "blue"], 1501 1501 }; 1502 1502 1503 - alias primaryColors = colorEnum constrained { 1503 + inline type primaryColors = colorEnum constrained { 1504 1504 enum: ["red", "blue"], 1505 1505 }; 1506 1506 "#; ··· 1515 1515 let mut ws = Workspace::new(); 1516 1516 1517 1517 let input = r#" 1518 - alias primaryColors = string constrained { 1518 + inline type primaryColors = string constrained { 1519 1519 enum: ["red", "blue"], 1520 1520 }; 1521 1521 1522 - alias moreColors = primaryColors constrained { 1522 + inline type moreColors = primaryColors constrained { 1523 1523 enum: ["red", "blue", "green"], 1524 1524 }; 1525 1525 "#; ··· 1535 1535 let mut ws = Workspace::new(); 1536 1536 1537 1537 let input = r#" 1538 - alias baseString = string constrained { 1538 + inline type baseString = string constrained { 1539 1539 maxLength: 1000, 1540 1540 minLength: 10, 1541 1541 }; 1542 1542 1543 - alias mediumString = baseString constrained { 1543 + inline type mediumString = baseString constrained { 1544 1544 maxLength: 500, 1545 1545 minLength: 20, 1546 1546 }; 1547 1547 1548 - alias shortString = mediumString constrained { 1548 + inline type shortString = mediumString constrained { 1549 1549 maxLength: 100, 1550 1550 minLength: 30, 1551 1551 }; ··· 1561 1561 let mut ws = Workspace::new(); 1562 1562 1563 1563 let input = r#" 1564 - alias baseString = string constrained { 1564 + inline type baseString = string constrained { 1565 1565 maxLength: 1000, 1566 1566 }; 1567 1567 1568 - alias mediumString = baseString constrained { 1568 + inline type mediumString = baseString constrained { 1569 1569 maxLength: 500, 1570 1570 }; 1571 1571 1572 - alias breakChain = mediumString constrained { 1572 + inline type breakChain = mediumString constrained { 1573 1573 maxLength: 600, 1574 1574 }; 1575 1575 "#;
+20 -20
std/com/atproto/admin/defs.mlf
··· 1 1 // com.atproto.admin.defs 2 2 3 3 def type statusAttr = { 4 - applied: boolean, 5 - ref?: string, 4 + applied!: boolean, 5 + ref: string, 6 6 }; 7 7 8 8 def type accountView = { 9 - did: Did, 10 - handle: Handle, 11 - email?: string, 12 - relatedRecords?: [unknown], 13 - indexedAt: Datetime, 14 - invitedBy?: com.atproto.server.inviteCode, 15 - invites?: [com.atproto.server.inviteCode], 16 - invitesDisabled?: boolean, 17 - emailConfirmedAt?: Datetime, 18 - inviteNote?: string, 19 - deactivatedAt?: Datetime, 20 - threatSignatures?: [threatSignature], 9 + did!: Did, 10 + handle!: Handle, 11 + email: string, 12 + relatedRecords: [unknown], 13 + indexedAt!: Datetime, 14 + invitedBy: com.atproto.server.inviteCode, 15 + invites: [com.atproto.server.inviteCode], 16 + invitesDisabled: boolean, 17 + emailConfirmedAt: Datetime, 18 + inviteNote: string, 19 + deactivatedAt: Datetime, 20 + threatSignatures: [threatSignature], 21 21 }; 22 22 23 23 def type repoRef = { 24 - did: Did, 24 + did!: Did, 25 25 }; 26 26 27 27 def type repoBlobRef = { 28 - did: Did, 29 - cid: Cid, 30 - recordUri?: AtUri, 28 + did!: Did, 29 + cid!: Cid, 30 + recordUri: AtUri, 31 31 }; 32 32 33 33 def type threatSignature = { 34 - property: string, 35 - value: string, 34 + property!: string, 35 + value!: string, 36 36 };
+3 -3
std/com/atproto/identity/defs.mlf
··· 1 1 // com.atproto.identity.defs 2 2 3 3 def type identityInfo = { 4 - did: Did, 5 - handle: Handle, 6 - didDoc: unknown, 4 + did!: Did, 5 + handle!: Handle, 6 + didDoc!: unknown, 7 7 };
+20 -20
std/com/atproto/label/defs.mlf
··· 1 1 // com.atproto.label.defs 2 2 3 3 def type label = { 4 - ver?: integer, 5 - src: Did, 6 - uri: AtUri, 7 - cid?: Cid, 8 - val: string constrained { maxLength: 128 }, 9 - neg?: boolean, 10 - cts: Datetime, 11 - exp?: Datetime, 12 - sig?: bytes, 4 + ver: integer, 5 + src!: Did, 6 + uri!: AtUri, 7 + cid: Cid, 8 + val!: string constrained { maxLength: 128 }, 9 + neg: boolean, 10 + cts!: Datetime, 11 + exp: Datetime, 12 + sig: bytes, 13 13 }; 14 14 15 15 def type selfLabels = { 16 - values: [selfLabel] constrained { maxLength: 10 }, 16 + values!: [selfLabel] constrained { maxLength: 10 }, 17 17 }; 18 18 19 19 def type selfLabel = { 20 - val: string constrained { maxLength: 128 }, 20 + val!: string constrained { maxLength: 128 }, 21 21 }; 22 22 23 23 def type labelValueDefinition = { 24 - identifier: string constrained { maxLength: 100, maxGraphemes: 100 }, 25 - severity: string constrained { knownValues: ["inform", "alert", "none"] }, 26 - blurs: string constrained { knownValues: ["content", "media", "none"] }, 27 - defaultSetting?: string constrained { knownValues: ["ignore", "warn", "hide"], default: "warn" }, 28 - adultOnly?: boolean, 29 - locales: [labelValueDefinitionStrings], 24 + identifier!: string constrained { maxLength: 100, maxGraphemes: 100 }, 25 + severity!: string constrained { knownValues: ["inform", "alert", "none"] }, 26 + blurs!: string constrained { knownValues: ["content", "media", "none"] }, 27 + defaultSetting: string constrained { knownValues: ["ignore", "warn", "hide"], default: "warn" }, 28 + adultOnly: boolean, 29 + locales!: [labelValueDefinitionStrings], 30 30 }; 31 31 32 32 def type labelValueDefinitionStrings = { 33 - lang: Language, 34 - name: string constrained { maxGraphemes: 64, maxLength: 640 }, 35 - description: string constrained { maxGraphemes: 10000, maxLength: 100000 }, 33 + lang!: Language, 34 + name!: string constrained { maxGraphemes: 64, maxLength: 640 }, 35 + description!: string constrained { maxGraphemes: 10000, maxLength: 100000 }, 36 36 }; 37 37 38 38 def type labelValue = string constrained {
+1 -1
std/com/atproto/lexicon/schema.mlf
··· 1 1 // com.atproto.lexicon.schema 2 2 3 3 record schema { 4 - lexicon: integer, 4 + lexicon!: integer, 5 5 }
+2 -2
std/com/atproto/repo/defs.mlf
··· 1 1 // com.atproto.repo.defs 2 2 3 3 def type commitMeta = { 4 - cid: Cid, 5 - rev: Tid, 4 + cid!: Cid, 5 + rev!: Tid, 6 6 };
+2 -2
std/com/atproto/repo/strongRef.mlf
··· 2 2 // A URI with a content-hash fingerprint. 3 3 4 4 def type strongRef = { 5 - uri: AtUri, 6 - cid: Cid, 5 + uri!: AtUri, 6 + cid!: Cid, 7 7 };
+9 -9
std/com/atproto/server/defs.mlf
··· 1 1 // com.atproto.server.defs 2 2 3 3 def type inviteCode = { 4 - code: string, 5 - available: integer, 6 - disabled: boolean, 7 - forAccount: string, 8 - createdBy: string, 9 - createdAt: Datetime, 10 - uses: [inviteCodeUse], 4 + code!: string, 5 + available!: integer, 6 + disabled!: boolean, 7 + forAccount!: string, 8 + createdBy!: string, 9 + createdAt!: Datetime, 10 + uses!: [inviteCodeUse], 11 11 }; 12 12 13 13 def type inviteCodeUse = { 14 - usedBy: Did, 15 - usedAt: Datetime, 14 + usedBy!: Did, 15 + usedAt!: Datetime, 16 16 };
+3 -3
tree-sitter-mlf/grammar.js
··· 61 61 field: $ => seq( 62 62 optional($.doc_comment), 63 63 field('name', $.identifier), 64 - optional('?'), 64 + optional('!'), 65 65 ':', 66 66 field('type', $.type), 67 67 ',' ··· 136 136 137 137 parameter: $ => seq( 138 138 field('name', $.identifier), 139 - optional('?'), 139 + optional('!'), 140 140 ':', 141 141 field('type', $.type) 142 142 ), ··· 198 198 199 199 union_type: $ => prec.left(seq( 200 200 $.non_union_type, 201 - repeat1(seq('|', $.non_union_type)) 201 + repeat1(seq('|', choice($.non_union_type, '!'))) 202 202 )), 203 203 204 204 object_type: $ => seq(
+13 -4
tree-sitter-mlf/src/grammar.json
··· 171 171 "members": [ 172 172 { 173 173 "type": "STRING", 174 - "value": "?" 174 + "value": "!" 175 175 }, 176 176 { 177 177 "type": "BLANK" ··· 488 488 "members": [ 489 489 { 490 490 "type": "STRING", 491 - "value": "?" 491 + "value": "!" 492 492 }, 493 493 { 494 494 "type": "BLANK" ··· 724 724 "value": "|" 725 725 }, 726 726 { 727 - "type": "SYMBOL", 728 - "name": "non_union_type" 727 + "type": "CHOICE", 728 + "members": [ 729 + { 730 + "type": "SYMBOL", 731 + "name": "non_union_type" 732 + }, 733 + { 734 + "type": "STRING", 735 + "value": "!" 736 + } 737 + ] 729 738 } 730 739 ] 731 740 }
+4 -4
tree-sitter-mlf/src/node-types.json
··· 663 663 } 664 664 }, 665 665 { 666 + "type": "!", 667 + "named": false 668 + }, 669 + { 666 670 "type": "(", 667 671 "named": false 668 672 }, ··· 688 692 }, 689 693 { 690 694 "type": "=", 691 - "named": false 692 - }, 693 - { 694 - "type": "?", 695 695 "named": false 696 696 }, 697 697 {
+2 -2
tree-sitter-mlf/test.mlf
··· 1 1 /// A simple post record 2 2 record post { 3 3 /// Post text 4 - text: string constrained { 4 + text!: string constrained { 5 5 maxLength: 300, 6 6 minLength: 1, 7 7 }, 8 8 /// Creation timestamp 9 - createdAt: Datetime, 9 + createdAt!: Datetime, 10 10 }
+3 -3
website/content/_index.md
··· 8 8 /// A forum thread 9 9 record thread { 10 10 /// Thread title 11 - title: string constrained { 11 + title!: string constrained { 12 12 maxLength: 200, 13 13 minLength: 1, 14 14 }, 15 15 /// Thread body content 16 - body: string constrained { 16 + body!: string constrained { 17 17 maxLength: 10000, 18 18 }, 19 19 /// Thread creation timestamp 20 - createdAt: Datetime, 20 + createdAt!: Datetime, 21 21 }; 22 22 ```''' 23 23
+3 -3
website/content/docs/getting-started.md
··· 34 34 /// A forum thread 35 35 record thread { 36 36 /// Thread title 37 - title: string constrained { 37 + title!: string constrained { 38 38 maxLength: 200, 39 39 minLength: 1, 40 40 }, 41 41 /// Thread body 42 - body: string constrained { 42 + body!: string constrained { 43 43 maxLength: 10000, 44 44 }, 45 45 /// Thread creation timestamp 46 - createdAt: Datetime, 46 + createdAt!: Datetime, 47 47 }; 48 48 ``` 49 49
+8 -8
website/content/docs/language-guide/01-your-first-lexicon.md
··· 13 13 /// A user profile 14 14 record profile { 15 15 /// The user's display name 16 - name: string, 16 + name!: string, 17 17 /// The user's email address 18 - email: string, 18 + email!: string, 19 19 /// When the account was created 20 - createdAt: Datetime, 20 + createdAt!: Datetime, 21 21 } 22 22 ``` 23 23 24 - This defines a `profile` record with three fields: `name`, `email`, and `createdAt`. 24 + This defines a `profile` record with three required fields: `name`, `email`, and `createdAt`. 25 25 26 26 ## File Naming and Namespaces 27 27 ··· 114 114 /// A forum post 115 115 record post { 116 116 /// Post title 117 - title: string, 117 + title!: string, 118 118 /// Post content 119 - body: string, 119 + body!: string, 120 120 /// Post author's DID 121 - author: Did, 121 + author!: Did, 122 122 /// When the post was published 123 - publishedAt: Datetime, 123 + publishedAt!: Datetime, 124 124 } 125 125 ``` 126 126
+16 -16
website/content/docs/language-guide/02-fields.md
··· 7 7 8 8 ## Required vs Optional Fields 9 9 10 - By default, all fields are required. Use `?` to make a field optional: 10 + By default, all fields are optional. Use `!` to make a field required: 11 11 12 12 ```mlf 13 13 record user { 14 - name: string, // Required - must be provided 15 - bio?: string, // Optional - can be omitted 16 - email: string, // Required 17 - website?: string, // Optional 14 + name!: string, // Required - must be provided 15 + bio: string, // Optional - can be omitted (default) 16 + email!: string, // Required 17 + website: string, // Optional (default) 18 18 } 19 19 ``` 20 20 ··· 155 155 /// A forum post 156 156 record post { 157 157 /// Post text content 158 - text: string, 158 + text!: string, 159 159 160 160 /// Post author 161 - author: Did, 161 + author!: Did, 162 162 163 163 /// When the post was created 164 - createdAt: Datetime, 164 + createdAt!: Datetime, 165 165 166 166 /// Optional reply count 167 - replyCount?: integer, 167 + replyCount: integer, 168 168 169 169 /// Whether the post is pinned 170 - isPinned: boolean, 170 + isPinned!: boolean, 171 171 172 172 /// Optional geographic location 173 - location?: { 174 - name: string, 175 - lat: integer, 176 - lng: integer, 173 + location: { 174 + name!: string, 175 + lat!: integer, 176 + lng!: integer, 177 177 }, 178 178 179 179 /// Tags on this post 180 - tags: string[], 180 + tags!: string[], 181 181 182 182 /// Optional embedded images 183 - images?: Uri[], 183 + images: Uri[], 184 184 185 185 /// Arbitrary metadata 186 186 metadata: unknown,
+9 -9
website/content/docs/language-guide/03-constraints.md
··· 185 185 /// A forum thread 186 186 record thread { 187 187 /// Thread title (1-200 characters) 188 - title: string constrained { 188 + title!: string constrained { 189 189 minGraphemes: 1, 190 190 maxGraphemes: 200, 191 191 }, 192 192 193 193 /// Thread content 194 - body: string constrained { 194 + body!: string constrained { 195 195 maxGraphemes: 5000, 196 196 }, 197 197 198 198 /// View count (must be non-negative) 199 - views: integer constrained { 199 + views!: integer constrained { 200 200 minimum: 0, 201 201 }, 202 202 203 203 /// Reply count 204 - replies: integer constrained { 204 + replies!: integer constrained { 205 205 minimum: 0, 206 206 default: 0, 207 207 }, 208 208 209 209 /// Thread status 210 - status: string constrained { 210 + status!: string constrained { 211 211 enum: ["open", "closed", "pinned"], 212 212 default: "open", 213 213 }, 214 214 215 215 /// Thread images (1-10 images) 216 - images: Uri[] constrained { 216 + images!: Uri[] constrained { 217 217 minLength: 1, 218 218 maxLength: 10, 219 219 }, 220 220 221 221 /// Optional thread thumbnail 222 - thumbnail?: blob constrained { 222 + thumbnail: blob constrained { 223 223 accept: ["image/png", "image/jpeg"], 224 224 maxSize: 500000, 225 225 }, 226 226 227 227 /// Average rating (0-5 scale) 228 - rating?: integer constrained { 228 + rating: integer constrained { 229 229 minimum: 0, 230 230 maximum: 5, 231 231 }, 232 232 233 233 /// Whether thread is featured 234 - featured: boolean constrained { 234 + featured!: boolean constrained { 235 235 default: false, 236 236 }, 237 237 }
+24 -24
website/content/docs/language-guide/04-custom-types.md
··· 52 52 inline type UserId = Did; // Simple alias 53 53 54 54 record account { 55 - id: UserId, 56 - email: EmailAddress, 57 - loginCount: PositiveInt, 55 + id!: UserId, 56 + email!: EmailAddress, 57 + loginCount!: PositiveInt, 58 58 } 59 59 ``` 60 60 ··· 62 62 63 63 ```mlf 64 64 inline type Coordinates = { 65 - lat: integer, 66 - lng: integer, 65 + lat!: integer, 66 + lng!: integer, 67 67 }; 68 68 69 69 record location { 70 - coords: Coordinates, 70 + coords!: Coordinates, 71 71 } 72 72 ``` 73 73 ··· 77 77 78 78 ```mlf 79 79 def type author = { 80 - did: Did, 81 - handle: Handle, 82 - displayName?: string, 80 + did!: Did, 81 + handle!: Handle, 82 + displayName: string, 83 83 }; 84 84 85 85 record post { 86 - author: author, 86 + author!: author, 87 87 } 88 88 89 89 record comment { 90 - author: author, 90 + author!: author, 91 91 } 92 92 ``` 93 93 ··· 127 127 **Def type (referenced by name):** 128 128 ```mlf 129 129 def type postRef = { 130 - uri: AtUri, 131 - cid: Cid, 130 + uri!: AtUri, 131 + cid!: Cid, 132 132 }; 133 133 134 134 record reply { 135 - replyTo: postRef, // References: #postRef in lexicon 135 + replyTo!: postRef, // References: #postRef in lexicon 136 136 } 137 137 ``` 138 138 ··· 154 154 155 155 // Def type for shared object 156 156 def type author = { 157 - did: Did, 158 - handle: Handle, 159 - displayName?: ShortText, 157 + did!: Did, 158 + handle!: Handle, 159 + displayName: ShortText, 160 160 }; 161 161 162 162 /// A forum thread 163 163 record thread { 164 164 /// Thread title 165 - title: ShortText, 165 + title!: ShortText, 166 166 167 167 /// Thread body 168 - body: LongText, 168 + body!: LongText, 169 169 170 170 /// Thread author 171 - author: author, 171 + author!: author, 172 172 173 173 /// When created 174 - createdAt: Datetime, 174 + createdAt!: Datetime, 175 175 } 176 176 177 177 /// A reply to a thread 178 178 record reply { 179 179 /// Reply text 180 - text: LongText, 180 + text!: LongText, 181 181 182 182 /// Reply author (reuses author type) 183 - author: author, 183 + author!: author, 184 184 185 185 /// When created 186 - createdAt: Datetime, 186 + createdAt!: Datetime, 187 187 } 188 188 ``` 189 189
+48 -48
website/content/docs/language-guide/05-unions.md
··· 3 3 weight = 5 4 4 +++ 5 5 6 - Unions allow a field to accept multiple types. MLF supports both closed unions (fixed set of types) and open unions (allowing unknown types). 6 + Unions allow a field to accept multiple types. MLF supports both open unions (allowing unknown types) and closed unions (fixed set of types). 7 7 8 - ## Closed Unions 8 + ## Open Unions 9 9 10 - Use the pipe operator `|` to create a union of types: 10 + By default, unions are open. Use the pipe operator `|` to create a union of types: 11 11 12 12 ```mlf 13 13 def type textPost = { 14 - text: string, 14 + text!: string, 15 15 }; 16 16 17 17 def type imagePost = { 18 - image: Uri, 19 - caption?: string, 18 + image!: Uri, 19 + caption: string, 20 20 }; 21 21 22 22 def type videoPost = { 23 - video: Uri, 24 - duration: integer, 23 + video!: Uri, 24 + duration!: integer, 25 25 }; 26 26 27 27 record post { ··· 29 29 } 30 30 ``` 31 31 32 - The `content` field must be one of these three types. No other types are accepted. 32 + The `content` field accepts these three types, plus any unknown types. This allows for forward compatibility. 33 33 34 - ## Open Unions 34 + ## Closed Unions 35 35 36 - Add `| _` to allow unknown types for forward compatibility: 36 + Add `| !` to create a closed union that only accepts the listed types: 37 37 38 38 ```mlf 39 39 record post { 40 - content: textPost | imagePost | _, 40 + content: textPost | imagePost | !, 41 41 } 42 42 ``` 43 43 44 - Now the system knows about `textPost` and `imagePost`, but will also accept unknown types it hasn't seen before. This is useful when you expect the union to grow in the future. 44 + Now the system will only accept `textPost` and `imagePost`. No other types are allowed. Use closed unions when you want strict type checking. 45 45 46 46 ## Why Use Open Unions? 47 47 48 48 Open unions help with forward compatibility: 49 49 50 50 ```mlf 51 - // Version 1: Only text and images 51 + // Version 1: Only text and images (open by default) 52 52 record post { 53 - content: textPost | imagePost | _, 53 + content: textPost | imagePost, 54 54 } 55 55 56 56 // Version 2: Add video support 57 - // Old clients still work because of the `_` 57 + // Old clients still work because unions are open by default 58 58 def type videoPost = { 59 - video: Uri, 59 + video!: Uri, 60 60 }; 61 61 62 62 record post { 63 - content: textPost | imagePost | videoPost | _, 63 + content: textPost | imagePost | videoPost, 64 64 } 65 65 ``` 66 66 67 - Old clients that don't know about `videoPost` can still handle the lexicon because of the open union. 67 + Old clients that don't know about `videoPost` can still handle the lexicon because of the open union (the default behavior). 68 68 69 69 ## Unions with Inline Objects 70 70 ··· 73 73 ```mlf 74 74 record embed { 75 75 content: { 76 - text: string, 76 + text!: string, 77 77 } | { 78 - image: Uri, 78 + image!: Uri, 79 79 } | { 80 - link: Uri, 81 - title: string, 80 + link!: Uri, 81 + title!: string, 82 82 }, 83 83 } 84 84 ``` ··· 91 91 92 92 ```mlf 93 93 def type mention = { 94 - did: Did, 95 - start: integer, 96 - end: integer, 94 + did!: Did, 95 + start!: integer, 96 + end!: integer, 97 97 }; 98 98 99 99 def type link = { 100 - uri: Uri, 101 - start: integer, 102 - end: integer, 100 + uri!: Uri, 101 + start!: integer, 102 + end!: integer, 103 103 }; 104 104 105 105 def type tag = { 106 - name: string, 107 - start: integer, 108 - end: integer, 106 + name!: string, 107 + start!: integer, 108 + end!: integer, 109 109 }; 110 110 111 111 record post { 112 - text: string, 112 + text!: string, 113 113 facets: (mention | link | tag)[], 114 114 } 115 115 ``` ··· 124 124 ```mlf 125 125 /// Text content 126 126 def type textContent = { 127 - text: string constrained { 127 + text!: string constrained { 128 128 maxGraphemes: 2000, 129 129 }, 130 130 }; 131 131 132 132 /// Image content 133 133 def type imageContent = { 134 - url: Uri, 135 - width: integer, 136 - height: integer, 137 - alt?: string, 134 + url!: Uri, 135 + width!: integer, 136 + height!: integer, 137 + alt: string, 138 138 }; 139 139 140 140 /// File attachment 141 141 def type fileContent = { 142 - url: Uri, 143 - filename: string, 144 - size: integer, 145 - mimeType: string, 142 + url!: Uri, 143 + filename!: string, 144 + size!: integer, 145 + mimeType!: string, 146 146 }; 147 147 148 148 /// A forum post with different content types 149 149 record post { 150 150 /// Post author 151 - author: Did, 151 + author!: Did, 152 152 153 - /// Post content (text, image, or file) 154 - content: textContent | imageContent | fileContent | _, 153 + /// Post content (text, image, or file) - open union by default 154 + content: textContent | imageContent | fileContent, 155 155 156 156 /// When the post was created 157 - createdAt: Datetime, 157 + createdAt!: Datetime, 158 158 159 159 /// Optional reply reference 160 - replyTo?: AtUri, 160 + replyTo: AtUri, 161 161 } 162 162 ``` 163 163 164 - The `| _` at the end means future content types can be added without breaking old clients. 164 + Unions are open by default, which means future content types can be added without breaking old clients. 165 165 166 166 ## What's Next? 167 167
+55 -55
website/content/docs/language-guide/07-xrpc.md
··· 27 27 ```mlf 28 28 /// Get a user profile 29 29 query getProfile( 30 - actor: Did 30 + actor!: Did 31 31 ):{ 32 - did: Did, 33 - handle: Handle, 34 - displayName?: string, 32 + did!: Did, 33 + handle!: Handle, 34 + displayName: string, 35 35 }; 36 36 ``` 37 37 ··· 39 39 ```mlf 40 40 /// Search for posts 41 41 query searchPosts( 42 - q: string, 43 - limit?: integer constrained { 42 + q!: string, 43 + limit: integer constrained { 44 44 minimum: 1, 45 45 maximum: 100, 46 46 default: 25, 47 47 }, 48 - cursor?: string 48 + cursor: string 49 49 ):{ 50 - posts: post[], 51 - cursor?: string, 50 + posts!: post[], 51 + cursor: string, 52 52 }; 53 53 ``` 54 54 55 55 **Query returning a record:** 56 56 ```mlf 57 57 record profile { 58 - did: Did, 59 - handle: Handle, 60 - displayName?: string, 58 + did!: Did, 59 + handle!: Handle, 60 + displayName: string, 61 61 } 62 62 63 63 query getProfile( 64 - actor: Did 64 + actor!: Did 65 65 ):profile; 66 66 ``` 67 67 ··· 73 73 ```mlf 74 74 /// Create a new post 75 75 procedure createPost( 76 - text: string constrained { 76 + text!: string constrained { 77 77 minGraphemes: 1, 78 78 maxGraphemes: 500, 79 79 } 80 80 ):{ 81 - uri: AtUri, 82 - cid: Cid, 81 + uri!: AtUri, 82 + cid!: Cid, 83 83 }; 84 84 ``` 85 85 ··· 87 87 ```mlf 88 88 /// Update a user profile 89 89 procedure updateProfile( 90 - displayName?: string, 91 - bio?: string, 92 - avatar?: blob 90 + displayName: string, 91 + bio: string, 92 + avatar: blob 93 93 ):{ 94 - success: boolean, 94 + success!: boolean, 95 95 }; 96 96 ``` 97 97 ··· 99 99 ```mlf 100 100 /// Delete a post 101 101 procedure deletePost( 102 - uri: AtUri 102 + uri!: AtUri 103 103 ):{ 104 - success: boolean, 104 + success!: boolean, 105 105 }; 106 106 ``` 107 107 ··· 119 119 ```mlf 120 120 /// Subscribe to posts from specific users 121 121 subscription subscribePosts( 122 - authors?: Did[] 122 + authors: Did[] 123 123 ):post; 124 124 ``` 125 125 126 126 **Subscription with multiple message types:** 127 127 ```mlf 128 128 def type postCreated = { 129 - post: post, 129 + post!: post, 130 130 }; 131 131 132 132 def type postDeleted = { 133 - uri: AtUri, 133 + uri!: AtUri, 134 134 }; 135 135 136 136 def type postUpdated = { 137 - post: post, 137 + post!: post, 138 138 }; 139 139 140 140 /// Subscribe to post events ··· 145 145 ```mlf 146 146 /// Subscribe to repository events 147 147 subscription subscribeRepos( 148 - cursor?: integer 148 + cursor: integer 149 149 ):commit | identity | tombstone; 150 150 ``` 151 151 ··· 166 166 167 167 ```mlf 168 168 query getPost( 169 - uri: AtUri 169 + uri!: AtUri 170 170 ):post | error { 171 171 /// Post not found 172 172 NotFound, ··· 178 178 **Procedure with errors:** 179 179 ```mlf 180 180 procedure createPost( 181 - text: string 181 + text!: string 182 182 ):{ 183 - uri: AtUri, 184 - cid: Cid, 183 + uri!: AtUri, 184 + cid!: Cid, 185 185 } | error { 186 186 /// Text exceeds maximum length 187 187 TextTooLong, ··· 203 203 ```mlf 204 204 query searchPosts( 205 205 /// Search query (1-200 characters) 206 - q: string constrained { 206 + q!: string constrained { 207 207 minLength: 1, 208 208 maxLength: 200, 209 209 }, 210 210 211 211 /// Results per page 212 - limit?: integer constrained { 212 + limit: integer constrained { 213 213 minimum: 1, 214 214 maximum: 100, 215 215 default: 25, 216 216 } 217 217 ):{ 218 - posts: post[], 218 + posts!: post[], 219 219 } 220 220 ``` 221 221 ··· 226 226 **Inline objects:** 227 227 ```mlf 228 228 query getStats():{ 229 - posts: integer, 230 - followers: integer, 229 + posts!: integer, 230 + followers!: integer, 231 231 }; 232 232 ``` 233 233 234 234 **Named records:** 235 235 ```mlf 236 - query getProfile(did: Did):profile; 236 + query getProfile(did!: Did):profile; 237 237 ``` 238 238 239 239 **Unions:** ··· 250 250 /// A forum post 251 251 record post { 252 252 /// Post title 253 - title: string constrained { 253 + title!: string constrained { 254 254 minGraphemes: 1, 255 255 maxGraphemes: 200, 256 256 }, 257 257 258 258 /// Post content 259 - body: string constrained { 259 + body!: string constrained { 260 260 maxGraphemes: 50000, 261 261 }, 262 262 263 263 /// Post author 264 - author: Did, 264 + author!: Did, 265 265 266 266 /// When published 267 - publishedAt: Datetime, 267 + publishedAt!: Datetime, 268 268 } 269 269 270 270 /// Get a single post 271 271 query getPost( 272 272 /// Post URI 273 - uri: AtUri 273 + uri!: AtUri 274 274 ):post | error { 275 275 /// Post not found 276 276 NotFound, ··· 281 281 /// List posts by author 282 282 query listPosts( 283 283 /// Author DID 284 - author: Did, 284 + author!: Did, 285 285 286 286 /// Results per page 287 - limit?: integer constrained { 287 + limit: integer constrained { 288 288 minimum: 1, 289 289 maximum: 100, 290 290 default: 25, 291 291 }, 292 292 293 293 /// Pagination cursor 294 - cursor?: string 294 + cursor: string 295 295 ):{ 296 - posts: post[], 297 - cursor?: string, 296 + posts!: post[], 297 + cursor: string, 298 298 }; 299 299 300 300 /// Create a new post 301 301 procedure createPost( 302 302 /// Post title 303 - title: string constrained { 303 + title!: string constrained { 304 304 minGraphemes: 1, 305 305 maxGraphemes: 200, 306 306 }, 307 307 308 308 /// Post body 309 - body: string constrained { 309 + body!: string constrained { 310 310 maxGraphemes: 50000, 311 311 } 312 312 ):{ 313 - uri: AtUri, 314 - cid: Cid, 315 - post: post, 313 + uri!: AtUri, 314 + cid!: Cid, 315 + post!: post, 316 316 } | error { 317 317 /// User not authenticated 318 318 Unauthorized, ··· 323 323 /// Delete a post 324 324 procedure deletePost( 325 325 /// Post URI to delete 326 - uri: AtUri 326 + uri!: AtUri 327 327 ):{ 328 - success: boolean, 328 + success!: boolean, 329 329 } | error { 330 330 /// Post not found 331 331 NotFound, ··· 336 336 /// Subscribe to new posts 337 337 subscription subscribePosts( 338 338 /// Optional author filter 339 - author?: Did 339 + author: Did 340 340 ):post; 341 341 ``` 342 342
+2 -2
website/syntaxes/mlf.sublime-syntax
··· 67 67 scope: punctuation.section.mlf 68 68 - match: '[,;:]' 69 69 scope: punctuation.separator.mlf 70 - - match: '\?' 71 - scope: keyword.operator.optional.mlf 70 + - match: '!' 71 + scope: keyword.operator.required.mlf 72 72 - match: '\|' 73 73 scope: keyword.operator.union.mlf 74 74 - match: '='