diff --git a/SPEC.md b/SPEC.md index a1271d2..f40c175 100644 --- a/SPEC.md +++ b/SPEC.md @@ -170,12 +170,12 @@ Records are the primary data structure, stored in repositories: ```mlf record post { - text: string constrained { + text!: string constrained { maxLength: 300 maxGraphemes: 300 } - createdAt: Datetime - reply?: replyRef // Optional field + createdAt!: Datetime + reply: replyRef // Optional field (default) } ``` @@ -195,8 +195,8 @@ inline type AtIdentifier = string constrained { ```mlf def type ReplyRef = { - root: AtUri - parent: AtUri + root!: AtUri + parent!: AtUri }; ``` @@ -214,7 +214,7 @@ token open; token closed; record issue { - state: string constrained { + state!: string constrained { knownValues: [ open // References token defined above closed @@ -234,9 +234,9 @@ Queries are read-only HTTP endpoints (GET): /// Get a user profile query getProfile( /// The actor's DID or handle - actor: AtIdentifier - /// Optional viewer context - viewer?: Did + actor!: AtIdentifier + /// Optional viewer context (default) + viewer: Did ): profileView | error { /// Profile not found ProfileNotFound @@ -252,11 +252,11 @@ Procedures are write operations (POST): ```mlf /// Create a new post procedure createPost( - text: string - createdAt: Datetime + text!: string + createdAt!: Datetime ): { - uri: AtUri - cid: Cid + uri!: AtUri + cid!: Cid } | error { /// Text exceeds maximum length TextTooLong @@ -270,8 +270,8 @@ Subscriptions are WebSocket-based event streams that emit messages over time. Th ```mlf /// Subscribe to repository events subscription subscribeRepos( - /// Optional cursor for resuming from a specific point - cursor?: integer + /// Optional cursor for resuming from a specific point (default) + cursor: integer ): commit | identity | handle | migrate | tombstone | info; ``` @@ -280,23 +280,23 @@ subscription subscribeRepos( ```mlf /// Commit message emitted by subscribeRepos def type commit = { - seq: integer - rebase: boolean - tooBig: boolean - repo: Did - commit: Cid - rev: string - since: string - blocks: bytes - ops: repoOp[] - blobs: Cid[] - time: Datetime + seq!: integer + rebase!: boolean + tooBig!: boolean + repo!: Did + commit!: Cid + rev!: string + since!: string + blocks!: bytes + ops!: repoOp[] + blobs!: Cid[] + time!: Datetime }; /// Info message def type info = { - name: string - message?: string + name!: string + message: string // Optional (default) }; ``` @@ -315,32 +315,32 @@ def type info = { /// Subscribe to chat messages for a stream subscription subscribeChat( /// The DID of the streamer - streamer: Did - /// Optional cursor to resume from - cursor?: string + streamer!: Did + /// Optional cursor to resume from (default) + cursor: string ): message | delete | join | leave; /// Chat message payload def type message = { - id: string - text: string - author: Did - createdAt: Datetime + id!: string + text!: string + author!: Did + createdAt!: Datetime }; /// Delete event payload def type delete = { - id: string + id!: string }; /// Join event payload def type join = { - user: Did + user!: Did }; /// Leave event payload def type leave = { - user: Did + user!: Did }; ``` @@ -355,12 +355,14 @@ Queries and procedures can return: ## Type Modifiers -### Optional Fields +### Optional and Required Fields + +Fields are **optional by default**. Use `!:` to mark a field as required: ```mlf record example { - required: string - optional?: string + optional: string // Optional (default) + required!: string // Required (marked with !) } ``` @@ -378,24 +380,24 @@ record example { ### Unions -Use the pipe operator `|`: +Use the pipe operator `|`. Unions are **open by default** (allowing unknown types): ```mlf record example { - // Closed union (only these types) + // Open union (default, can include unknown types) content: text | image | video - // Union of tokens + // Union of tokens (also open by default) state: open | closed | pending } ``` -Open unions (allowing unknown types) use `_`: +Closed unions (only allowing listed types) use `| !`: ```mlf record example { - // Open union (can include unknown types) - content: text | image | _ + // Closed union (marked with !, only these types allowed) + content: text | image | video | ! } ``` @@ -590,11 +592,11 @@ Annotations can be placed on: record profile { /// User's DID @indexed - did: Did + did!: Did - /// Display name + /// Display name (optional) @sensitive(pii: true) - displayName?: string + displayName: string } ``` @@ -743,18 +745,18 @@ token closed; /// An issue in a repository record issue { /// The repository this issue belongs to - repo: AtUri + repo!: AtUri /// Issue title - title: string constrained { + title!: string constrained { minGraphemes: 1 maxGraphemes: 200 } /// Issue body (markdown) - body?: string constrained { + body: string constrained { maxGraphemes: 10000 } /// Issue state - state: string constrained { + state!: string constrained { knownValues: [ open closed @@ -762,28 +764,28 @@ record issue { default: "open" } /// Creation timestamp - createdAt: Datetime + createdAt!: Datetime } /// A comment on an issue record comment { /// The issue this comment belongs to - issue: AtUri + issue!: AtUri /// Comment body (markdown) - body: string constrained { + body!: string constrained { minGraphemes: 1 maxGraphemes: 10000 } /// Creation timestamp - createdAt: Datetime + createdAt!: Datetime /// Optional reply target - replyTo?: AtUri + replyTo: AtUri } /// Get an issue by URI query getIssue( /// Issue AT-URI - uri: AtUri + uri!: AtUri ): issue | error { /// Issue not found NotFound @@ -791,12 +793,12 @@ query getIssue( /// Create a new issue procedure createIssue( - repo: AtUri - title: string - body?: string + repo!: AtUri + title!: string + body: string // Optional (default) ): { - uri: AtUri - cid: Cid + uri!: AtUri + cid!: Cid } | error { /// Repository not found RepoNotFound @@ -814,10 +816,10 @@ MLF compiles to standard ATProto JSON Lexicons: **MLF:** ```mlf record post { - text: string constrained { + text!: string constrained { maxLength: 300 } - createdAt: Datetime + createdAt!: Datetime } ``` @@ -854,7 +856,7 @@ record post { **MLF:** ```mlf subscription subscribeRepos( - cursor?: integer + cursor: integer // Optional (default) ): commit | identity; ``` diff --git a/mlf-lang/src/lexer.rs b/mlf-lang/src/lexer.rs index 7a5b2c9..a6cfb80 100644 --- a/mlf-lang/src/lexer.rs +++ b/mlf-lang/src/lexer.rs @@ -50,6 +50,7 @@ pub enum Token { Dot, Pipe, Question, + Exclamation, At, Equals, Semicolon, @@ -100,6 +101,7 @@ impl core::fmt::Display for Token { Token::Dot => write!(f, "."), Token::Pipe => write!(f, "|"), Token::Question => write!(f, "?"), + Token::Exclamation => write!(f, "!"), Token::At => write!(f, "@"), Token::Equals => write!(f, "="), Token::Semicolon => write!(f, ";"), @@ -260,6 +262,7 @@ fn symbol(input: &str) -> IResult<&str, Token> { map(char('.'), |_| Token::Dot), map(char('|'), |_| Token::Pipe), map(char('?'), |_| Token::Question), + map(char('!'), |_| Token::Exclamation), map(char('@'), |_| Token::At), map(char('='), |_| Token::Equals), map(char(';'), |_| Token::Semicolon), @@ -341,10 +344,10 @@ mod tests { #[test] fn test_keywords() { - let input = "record alias query"; + let input = "record inline query"; let tokens = tokenize(input).unwrap(); assert_eq!(tokens[0].token, Token::Record); - assert_eq!(tokens[1].token, Token::Alias); + assert_eq!(tokens[1].token, Token::Inline); assert_eq!(tokens[2].token, Token::Query); } diff --git a/mlf-lang/src/parser.rs b/mlf-lang/src/parser.rs index 3978911..c825c6e 100644 --- a/mlf-lang/src/parser.rs +++ b/mlf-lang/src/parser.rs @@ -301,11 +301,12 @@ impl Parser { let annotations = self.parse_annotations()?; let name = self.parse_field_name()?; - let optional = if matches!(self.current().token, LexToken::Question) { + // Check for ! to mark as required (default is optional) + let optional = if matches!(self.current().token, LexToken::Exclamation) { self.advance(); - true + false // ! means required } else { - false + true // default is optional }; self.expect(LexToken::Colon)?; @@ -411,8 +412,8 @@ impl Parser { } let span = Span::new(types[0].span().start, types.last().unwrap().span().end); - // Return type unions are closed by default (no _ support in return types yet) - ReturnType::Type(Type::Union { types, closed: true, span }) + // Return type unions are open by default (no ! support in return types yet) + ReturnType::Type(Type::Union { types, closed: false, span }) } } else { ReturnType::Type(output) @@ -463,8 +464,8 @@ impl Parser { } let span = Span::new(types[0].span().start, types.last().unwrap().span().end); - // Return type unions are closed by default (no _ support in return types yet) - ReturnType::Type(Type::Union { types, closed: true, span }) + // Return type unions are open by default (no ! support in return types yet) + ReturnType::Type(Type::Union { types, closed: false, span }) } } else { ReturnType::Type(output) @@ -546,11 +547,12 @@ impl Parser { let annotations = self.parse_annotations()?; let name = self.parse_ident()?; - let optional = if matches!(self.current().token, LexToken::Question) { + // Check for ! to mark as required (default is optional) + let optional = if matches!(self.current().token, LexToken::Exclamation) { self.advance(); - true + false // ! means required } else { - false + true // default is optional }; self.expect(LexToken::Colon)?; @@ -625,15 +627,15 @@ impl Parser { if matches!(self.current().token, LexToken::Pipe) { let mut types = alloc::vec![base]; - let mut has_underscore = false; + let mut has_exclamation = false; while matches!(self.current().token, LexToken::Pipe) { self.advance(); if matches!(self.current().token, LexToken::Error) { break; } - // Check if this is an underscore (open union marker) - if matches!(self.current().token, LexToken::Underscore) { - has_underscore = true; + // Check if this is an exclamation mark (closed union marker) + if matches!(self.current().token, LexToken::Exclamation) { + has_exclamation = true; self.advance(); } else { types.push(self.parse_base_type()?); @@ -641,8 +643,8 @@ impl Parser { } let span = Span::new(types[0].span().start, types.last().unwrap().span().end); - // Unions are closed by default, open if _ is present - let closed = !has_underscore; + // Unions are open by default, closed if ! is present + let closed = has_exclamation; return Ok(Type::Union { types, closed, span }); } @@ -1157,7 +1159,7 @@ mod tests { let input = r#"record user { name: string, age: integer, - };"#; + }"#; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); @@ -1172,17 +1174,17 @@ mod tests { } #[test] - fn test_parse_alias() { - let input = "alias userId = string;"; + fn test_parse_inline_type() { + let input = "inline type userId = string;"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { - Item::Alias(a) => { + Item::InlineType(a) => { assert_eq!(a.name.name, "userId"); } - _ => panic!("Expected alias"), + _ => panic!("Expected inline type"), } } @@ -1247,7 +1249,7 @@ mod tests { #[test] fn test_parse_subscription() { - let input = "subscription subscribeRepos(cursor?: integer,): commit | identity;"; + let input = "subscription subscribeRepos(cursor: integer,): commit | identity;"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); @@ -1262,24 +1264,9 @@ mod tests { } } - #[test] - fn test_parse_namespace() { - let input = "namespace actor;"; - let result = parse_lexicon(input); - assert!(result.is_ok()); - let lexicon = result.unwrap(); - assert_eq!(lexicon.items.len(), 1); - match &lexicon.items[0] { - Item::Namespace(n) => { - assert_eq!(n.name.name, "actor"); - } - _ => panic!("Expected namespace"), - } - } - #[test] fn test_parse_constrained_type() { - let input = r#"alias shortString = string constrained { + let input = r#"inline type shortString = string constrained { maxLength: 100, };"#; let result = parse_lexicon(input); @@ -1287,7 +1274,7 @@ mod tests { let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { - Item::Alias(a) => { + Item::InlineType(a) => { match &a.ty { Type::Constrained { constraints, .. } => { assert_eq!(constraints.len(), 1); @@ -1295,19 +1282,19 @@ mod tests { _ => panic!("Expected constrained type"), } } - _ => panic!("Expected alias"), + _ => panic!("Expected inline type"), } } #[test] fn test_parse_union_type() { - let input = "alias result = success | failure;"; + let input = "inline type result = success | failure;"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { - Item::Alias(a) => { + Item::InlineType(a) => { match &a.ty { Type::Union { types, .. } => { assert_eq!(types.len(), 2); @@ -1315,31 +1302,31 @@ mod tests { _ => panic!("Expected union type"), } } - _ => panic!("Expected alias"), + _ => panic!("Expected inline type"), } } #[test] fn test_parse_array_type() { - let input = "alias userList = [user];"; + let input = "inline type userList = [user];"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); assert_eq!(lexicon.items.len(), 1); match &lexicon.items[0] { - Item::Alias(a) => { + Item::InlineType(a) => { match &a.ty { Type::Array { .. } => {} _ => panic!("Expected array type"), } } - _ => panic!("Expected alias"), + _ => panic!("Expected inline type"), } } #[test] fn test_parse_annotation() { - let input = "@deprecated\nrecord old {};"; + let input = "@deprecated\nrecord old {}"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); @@ -1355,7 +1342,7 @@ mod tests { #[test] fn test_parse_annotation_with_args() { - let input = "@validate(min: 0, max: 100)\nrecord data {};"; + let input = "@validate(min: 0, max: 100)\nrecord data {}"; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); @@ -1372,8 +1359,8 @@ mod tests { #[test] fn test_parse_optional_field() { let input = r#"record user { - name?: string, - };"#; + name: string, + }"#; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); @@ -1386,16 +1373,33 @@ mod tests { } } + #[test] + fn test_parse_required_field() { + let input = r#"record user { + name!: string, + }"#; + let result = parse_lexicon(input); + assert!(result.is_ok()); + let lexicon = result.unwrap(); + match &lexicon.items[0] { + Item::Record(r) => { + assert_eq!(r.fields.len(), 1); + assert!(!r.fields[0].optional); + } + _ => panic!("Expected record"), + } + } + #[test] fn test_parse_enum_constraint() { - let input = r#"alias status = string constrained { + let input = r#"inline type status = string constrained { enum: ["active", "inactive"], };"#; let result = parse_lexicon(input); assert!(result.is_ok()); let lexicon = result.unwrap(); match &lexicon.items[0] { - Item::Alias(a) => { + Item::InlineType(a) => { match &a.ty { Type::Constrained { constraints, .. } => { match &constraints[0] { @@ -1408,7 +1412,7 @@ mod tests { _ => panic!("Expected constrained type"), } } - _ => panic!("Expected alias"), + _ => panic!("Expected inline type"), } } } diff --git a/mlf-lang/src/workspace.rs b/mlf-lang/src/workspace.rs index a042318..d23fe87 100644 --- a/mlf-lang/src/workspace.rs +++ b/mlf-lang/src/workspace.rs @@ -1151,7 +1151,7 @@ mod tests { #[test] fn test_workspace_basic() { let mut ws = Workspace::new(); - let input = "record foo { bar: string, };"; + let input = "record foo { bar: string, }"; let lexicon = parse_lexicon(input).unwrap(); assert!(ws.add_module("test".into(), lexicon).is_ok()); } @@ -1159,7 +1159,7 @@ mod tests { #[test] fn test_duplicate_definition() { let mut ws = Workspace::new(); - let input = "record foo {}; alias foo = string;"; + let input = "record foo {} inline type foo = string;"; let lexicon = parse_lexicon(input).unwrap(); let result = ws.add_module("test".into(), lexicon); assert!(result.is_err()); @@ -1168,7 +1168,7 @@ mod tests { #[test] fn test_undefined_reference() { let mut ws = Workspace::new(); - let input = "record foo { bar: unknown_type, };"; + let input = "record foo { bar: unknown_type, }"; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); @@ -1178,7 +1178,7 @@ mod tests { #[test] fn test_self_reference() { let mut ws = Workspace::new(); - let input = "record foo { bar: foo, };"; + let input = "record foo { bar: foo, }"; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); @@ -1188,10 +1188,10 @@ mod tests { fn test_cross_module_reference() { let mut ws = Workspace::new(); - let a = parse_lexicon("record foo {};").unwrap(); + let a = parse_lexicon("record foo {}").unwrap(); ws.add_module("a".into(), a).unwrap(); - let b = parse_lexicon("record bar { baz: a.foo, };").unwrap(); + let b = parse_lexicon("record bar { baz: a.foo, }").unwrap(); ws.add_module("b".into(), b).unwrap(); assert!(ws.resolve().is_ok()); @@ -1209,10 +1209,10 @@ mod tests { fn test_use_import_all() { let mut ws = Workspace::new(); - let a = parse_lexicon("record foo {}; alias bar = string;").unwrap(); + let a = parse_lexicon("record foo {} inline type bar = string;").unwrap(); ws.add_module("a".into(), a).unwrap(); - let b = parse_lexicon("use a; record baz { x: foo, y: bar, };").unwrap(); + let b = parse_lexicon("use a; record baz { x: foo, y: bar, }").unwrap(); ws.add_module("b".into(), b).unwrap(); assert!(ws.resolve().is_ok()); @@ -1222,10 +1222,10 @@ mod tests { fn test_use_import_with_alias() { let mut ws = Workspace::new(); - let a = parse_lexicon("record post {};").unwrap(); + let a = parse_lexicon("record post {}").unwrap(); ws.add_module("app.bsky.feed".into(), a).unwrap(); - let b = parse_lexicon("use app.bsky.feed.post as FeedPost; record like { subject: FeedPost, };").unwrap(); + let b = parse_lexicon("use app.bsky.feed.post as FeedPost; record like { subject: FeedPost, }").unwrap(); ws.add_module("app.bsky.feed.like".into(), b).unwrap(); let result = ws.resolve(); @@ -1239,7 +1239,7 @@ mod tests { fn test_use_undefined_module() { let mut ws = Workspace::new(); - let a = parse_lexicon("use nonexistent; record foo {};").unwrap(); + let a = parse_lexicon("use nonexistent; record foo {}").unwrap(); ws.add_module("test".into(), a).unwrap(); let result = ws.resolve(); @@ -1250,10 +1250,10 @@ mod tests { fn test_use_undefined_type() { let mut ws = Workspace::new(); - let a = parse_lexicon("record foo {};").unwrap(); + let a = parse_lexicon("record foo {}").unwrap(); ws.add_module("a".into(), a).unwrap(); - let b = parse_lexicon("use a.bar; record baz {};").unwrap(); + let b = parse_lexicon("use a.bar; record baz {}").unwrap(); ws.add_module("b".into(), b).unwrap(); let result = ws.resolve(); @@ -1264,7 +1264,7 @@ mod tests { fn test_typecheck_string_constraint_on_string() { let mut ws = Workspace::new(); - let input = r#"alias shortString = string constrained { + let input = r#"inline type shortString = string constrained { maxLength: 100, };"#; let lexicon = parse_lexicon(input).unwrap(); @@ -1277,7 +1277,7 @@ mod tests { fn test_typecheck_string_constraint_on_integer() { let mut ws = Workspace::new(); - let input = r#"alias constrainedInt = integer constrained { + let input = r#"inline type constrainedInt = integer constrained { maxLength: 100, };"#; let lexicon = parse_lexicon(input).unwrap(); @@ -1291,7 +1291,7 @@ mod tests { fn test_typecheck_numeric_constraint_on_integer() { let mut ws = Workspace::new(); - let input = r#"alias positiveInt = integer constrained { + let input = r#"inline type positiveInt = integer constrained { minimum: 0, };"#; let lexicon = parse_lexicon(input).unwrap(); @@ -1304,7 +1304,7 @@ mod tests { fn test_typecheck_numeric_constraint_on_string() { let mut ws = Workspace::new(); - let input = r#"alias constrainedString = string constrained { + let input = r#"inline type constrainedString = string constrained { minimum: 0, };"#; let lexicon = parse_lexicon(input).unwrap(); @@ -1319,11 +1319,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias shortString = string constrained { + inline type shortString = string constrained { maxLength: 100, }; - alias tinyString = shortString constrained { + inline type tinyString = shortString constrained { maxLength: 50, }; "#; @@ -1338,11 +1338,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias shortString = string constrained { + inline type shortString = string constrained { maxLength: 100, }; - alias tinyString = shortString constrained { + inline type tinyString = shortString constrained { maxLength: 50, }; "#; @@ -1357,11 +1357,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias shortString = string constrained { + inline type shortString = string constrained { maxLength: 50, }; - alias longString = shortString constrained { + inline type longString = shortString constrained { maxLength: 100, }; "#; @@ -1379,11 +1379,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias minString = string constrained { + inline type minString = string constrained { minLength: 10, }; - alias longerMinString = minString constrained { + inline type longerMinString = minString constrained { minLength: 20, }; "#; @@ -1398,11 +1398,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias minString = string constrained { + inline type minString = string constrained { minLength: 20, }; - alias shorterMinString = minString constrained { + inline type shorterMinString = minString constrained { minLength: 10, }; "#; @@ -1418,11 +1418,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias largeInt = integer constrained { + inline type largeInt = integer constrained { maximum: 1000, }; - alias smallInt = largeInt constrained { + inline type smallInt = largeInt constrained { maximum: 100, }; "#; @@ -1437,11 +1437,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias smallInt = integer constrained { + inline type smallInt = integer constrained { maximum: 100, }; - alias largeInt = smallInt constrained { + inline type largeInt = smallInt constrained { maximum: 1000, }; "#; @@ -1457,11 +1457,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias positiveInt = integer constrained { + inline type positiveInt = integer constrained { minimum: 0, }; - alias strictPositiveInt = positiveInt constrained { + inline type strictPositiveInt = positiveInt constrained { minimum: 1, }; "#; @@ -1476,11 +1476,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias positiveInt = integer constrained { + inline type positiveInt = integer constrained { minimum: 10, }; - alias lowerInt = positiveInt constrained { + inline type lowerInt = positiveInt constrained { minimum: 5, }; "#; @@ -1496,11 +1496,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias colorEnum = string constrained { + inline type colorEnum = string constrained { enum: ["red", "green", "blue"], }; - alias primaryColors = colorEnum constrained { + inline type primaryColors = colorEnum constrained { enum: ["red", "blue"], }; "#; @@ -1515,11 +1515,11 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias primaryColors = string constrained { + inline type primaryColors = string constrained { enum: ["red", "blue"], }; - alias moreColors = primaryColors constrained { + inline type moreColors = primaryColors constrained { enum: ["red", "blue", "green"], }; "#; @@ -1535,17 +1535,17 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias baseString = string constrained { + inline type baseString = string constrained { maxLength: 1000, minLength: 10, }; - alias mediumString = baseString constrained { + inline type mediumString = baseString constrained { maxLength: 500, minLength: 20, }; - alias shortString = mediumString constrained { + inline type shortString = mediumString constrained { maxLength: 100, minLength: 30, }; @@ -1561,15 +1561,15 @@ mod tests { let mut ws = Workspace::new(); let input = r#" - alias baseString = string constrained { + inline type baseString = string constrained { maxLength: 1000, }; - alias mediumString = baseString constrained { + inline type mediumString = baseString constrained { maxLength: 500, }; - alias breakChain = mediumString constrained { + inline type breakChain = mediumString constrained { maxLength: 600, }; "#; diff --git a/std/com/atproto/admin/defs.mlf b/std/com/atproto/admin/defs.mlf index 0b925af..6a948b7 100644 --- a/std/com/atproto/admin/defs.mlf +++ b/std/com/atproto/admin/defs.mlf @@ -1,36 +1,36 @@ // com.atproto.admin.defs def type statusAttr = { - applied: boolean, - ref?: string, + applied!: boolean, + ref: string, }; def type accountView = { - did: Did, - handle: Handle, - email?: string, - relatedRecords?: [unknown], - indexedAt: Datetime, - invitedBy?: com.atproto.server.inviteCode, - invites?: [com.atproto.server.inviteCode], - invitesDisabled?: boolean, - emailConfirmedAt?: Datetime, - inviteNote?: string, - deactivatedAt?: Datetime, - threatSignatures?: [threatSignature], + did!: Did, + handle!: Handle, + email: string, + relatedRecords: [unknown], + indexedAt!: Datetime, + invitedBy: com.atproto.server.inviteCode, + invites: [com.atproto.server.inviteCode], + invitesDisabled: boolean, + emailConfirmedAt: Datetime, + inviteNote: string, + deactivatedAt: Datetime, + threatSignatures: [threatSignature], }; def type repoRef = { - did: Did, + did!: Did, }; def type repoBlobRef = { - did: Did, - cid: Cid, - recordUri?: AtUri, + did!: Did, + cid!: Cid, + recordUri: AtUri, }; def type threatSignature = { - property: string, - value: string, + property!: string, + value!: string, }; diff --git a/std/com/atproto/identity/defs.mlf b/std/com/atproto/identity/defs.mlf index 92ea703..d644052 100644 --- a/std/com/atproto/identity/defs.mlf +++ b/std/com/atproto/identity/defs.mlf @@ -1,7 +1,7 @@ // com.atproto.identity.defs def type identityInfo = { - did: Did, - handle: Handle, - didDoc: unknown, + did!: Did, + handle!: Handle, + didDoc!: unknown, }; diff --git a/std/com/atproto/label/defs.mlf b/std/com/atproto/label/defs.mlf index 79570a9..03e3cbb 100644 --- a/std/com/atproto/label/defs.mlf +++ b/std/com/atproto/label/defs.mlf @@ -1,38 +1,38 @@ // com.atproto.label.defs def type label = { - ver?: integer, - src: Did, - uri: AtUri, - cid?: Cid, - val: string constrained { maxLength: 128 }, - neg?: boolean, - cts: Datetime, - exp?: Datetime, - sig?: bytes, + ver: integer, + src!: Did, + uri!: AtUri, + cid: Cid, + val!: string constrained { maxLength: 128 }, + neg: boolean, + cts!: Datetime, + exp: Datetime, + sig: bytes, }; def type selfLabels = { - values: [selfLabel] constrained { maxLength: 10 }, + values!: [selfLabel] constrained { maxLength: 10 }, }; def type selfLabel = { - val: string constrained { maxLength: 128 }, + val!: string constrained { maxLength: 128 }, }; def type labelValueDefinition = { - identifier: string constrained { maxLength: 100, maxGraphemes: 100 }, - severity: string constrained { knownValues: ["inform", "alert", "none"] }, - blurs: string constrained { knownValues: ["content", "media", "none"] }, - defaultSetting?: string constrained { knownValues: ["ignore", "warn", "hide"], default: "warn" }, - adultOnly?: boolean, - locales: [labelValueDefinitionStrings], + identifier!: string constrained { maxLength: 100, maxGraphemes: 100 }, + severity!: string constrained { knownValues: ["inform", "alert", "none"] }, + blurs!: string constrained { knownValues: ["content", "media", "none"] }, + defaultSetting: string constrained { knownValues: ["ignore", "warn", "hide"], default: "warn" }, + adultOnly: boolean, + locales!: [labelValueDefinitionStrings], }; def type labelValueDefinitionStrings = { - lang: Language, - name: string constrained { maxGraphemes: 64, maxLength: 640 }, - description: string constrained { maxGraphemes: 10000, maxLength: 100000 }, + lang!: Language, + name!: string constrained { maxGraphemes: 64, maxLength: 640 }, + description!: string constrained { maxGraphemes: 10000, maxLength: 100000 }, }; def type labelValue = string constrained { diff --git a/std/com/atproto/lexicon/schema.mlf b/std/com/atproto/lexicon/schema.mlf index 4af0d65..6288af7 100644 --- a/std/com/atproto/lexicon/schema.mlf +++ b/std/com/atproto/lexicon/schema.mlf @@ -1,5 +1,5 @@ // com.atproto.lexicon.schema record schema { - lexicon: integer, + lexicon!: integer, } diff --git a/std/com/atproto/repo/defs.mlf b/std/com/atproto/repo/defs.mlf index 4e99585..bdfd49d 100644 --- a/std/com/atproto/repo/defs.mlf +++ b/std/com/atproto/repo/defs.mlf @@ -1,6 +1,6 @@ // com.atproto.repo.defs def type commitMeta = { - cid: Cid, - rev: Tid, + cid!: Cid, + rev!: Tid, }; diff --git a/std/com/atproto/repo/strongRef.mlf b/std/com/atproto/repo/strongRef.mlf index 617954a..b4d9af6 100644 --- a/std/com/atproto/repo/strongRef.mlf +++ b/std/com/atproto/repo/strongRef.mlf @@ -2,6 +2,6 @@ // A URI with a content-hash fingerprint. def type strongRef = { - uri: AtUri, - cid: Cid, + uri!: AtUri, + cid!: Cid, }; diff --git a/std/com/atproto/server/defs.mlf b/std/com/atproto/server/defs.mlf index 15ea856..8802328 100644 --- a/std/com/atproto/server/defs.mlf +++ b/std/com/atproto/server/defs.mlf @@ -1,16 +1,16 @@ // com.atproto.server.defs def type inviteCode = { - code: string, - available: integer, - disabled: boolean, - forAccount: string, - createdBy: string, - createdAt: Datetime, - uses: [inviteCodeUse], + code!: string, + available!: integer, + disabled!: boolean, + forAccount!: string, + createdBy!: string, + createdAt!: Datetime, + uses!: [inviteCodeUse], }; def type inviteCodeUse = { - usedBy: Did, - usedAt: Datetime, + usedBy!: Did, + usedAt!: Datetime, }; diff --git a/tree-sitter-mlf/grammar.js b/tree-sitter-mlf/grammar.js index fdcde39..0edcca1 100644 --- a/tree-sitter-mlf/grammar.js +++ b/tree-sitter-mlf/grammar.js @@ -61,7 +61,7 @@ module.exports = grammar({ field: $ => seq( optional($.doc_comment), field('name', $.identifier), - optional('?'), + optional('!'), ':', field('type', $.type), ',' @@ -136,7 +136,7 @@ module.exports = grammar({ parameter: $ => seq( field('name', $.identifier), - optional('?'), + optional('!'), ':', field('type', $.type) ), @@ -198,7 +198,7 @@ module.exports = grammar({ union_type: $ => prec.left(seq( $.non_union_type, - repeat1(seq('|', $.non_union_type)) + repeat1(seq('|', choice($.non_union_type, '!'))) )), object_type: $ => seq( diff --git a/tree-sitter-mlf/src/grammar.json b/tree-sitter-mlf/src/grammar.json index d035b9b..5b62441 100644 --- a/tree-sitter-mlf/src/grammar.json +++ b/tree-sitter-mlf/src/grammar.json @@ -171,7 +171,7 @@ "members": [ { "type": "STRING", - "value": "?" + "value": "!" }, { "type": "BLANK" @@ -488,7 +488,7 @@ "members": [ { "type": "STRING", - "value": "?" + "value": "!" }, { "type": "BLANK" @@ -724,8 +724,17 @@ "value": "|" }, { - "type": "SYMBOL", - "name": "non_union_type" + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "non_union_type" + }, + { + "type": "STRING", + "value": "!" + } + ] } ] } diff --git a/tree-sitter-mlf/src/node-types.json b/tree-sitter-mlf/src/node-types.json index 81685fa..1ec5ce2 100644 --- a/tree-sitter-mlf/src/node-types.json +++ b/tree-sitter-mlf/src/node-types.json @@ -662,6 +662,10 @@ } } }, + { + "type": "!", + "named": false + }, { "type": "(", "named": false @@ -690,10 +694,6 @@ "type": "=", "named": false }, - { - "type": "?", - "named": false - }, { "type": "[", "named": false diff --git a/tree-sitter-mlf/test.mlf b/tree-sitter-mlf/test.mlf index 5e5c566..7f31959 100644 --- a/tree-sitter-mlf/test.mlf +++ b/tree-sitter-mlf/test.mlf @@ -1,10 +1,10 @@ /// A simple post record record post { /// Post text - text: string constrained { + text!: string constrained { maxLength: 300, minLength: 1, }, /// Creation timestamp - createdAt: Datetime, + createdAt!: Datetime, } diff --git a/website/content/_index.md b/website/content/_index.md index 4a2cff2..f1fbef5 100644 --- a/website/content/_index.md +++ b/website/content/_index.md @@ -8,16 +8,16 @@ mlf_example = '''```mlf /// A forum thread record thread { /// Thread title - title: string constrained { + title!: string constrained { maxLength: 200, minLength: 1, }, /// Thread body content - body: string constrained { + body!: string constrained { maxLength: 10000, }, /// Thread creation timestamp - createdAt: Datetime, + createdAt!: Datetime, }; ```''' diff --git a/website/content/docs/getting-started.md b/website/content/docs/getting-started.md index 3485759..025be44 100644 --- a/website/content/docs/getting-started.md +++ b/website/content/docs/getting-started.md @@ -34,16 +34,16 @@ Create a file `thread.mlf`: /// A forum thread record thread { /// Thread title - title: string constrained { + title!: string constrained { maxLength: 200, minLength: 1, }, /// Thread body - body: string constrained { + body!: string constrained { maxLength: 10000, }, /// Thread creation timestamp - createdAt: Datetime, + createdAt!: Datetime, }; ``` diff --git a/website/content/docs/language-guide/01-your-first-lexicon.md b/website/content/docs/language-guide/01-your-first-lexicon.md index 7cb774a..c757601 100644 --- a/website/content/docs/language-guide/01-your-first-lexicon.md +++ b/website/content/docs/language-guide/01-your-first-lexicon.md @@ -13,15 +13,15 @@ Here's a complete MLF file that defines a user profile: /// A user profile record profile { /// The user's display name - name: string, + name!: string, /// The user's email address - email: string, + email!: string, /// When the account was created - createdAt: Datetime, + createdAt!: Datetime, } ``` -This defines a `profile` record with three fields: `name`, `email`, and `createdAt`. +This defines a `profile` record with three required fields: `name`, `email`, and `createdAt`. ## File Naming and Namespaces @@ -114,13 +114,13 @@ Here's a minimal, complete lexicon for a forum post: /// A forum post record post { /// Post title - title: string, + title!: string, /// Post content - body: string, + body!: string, /// Post author's DID - author: Did, + author!: Did, /// When the post was published - publishedAt: Datetime, + publishedAt!: Datetime, } ``` diff --git a/website/content/docs/language-guide/02-fields.md b/website/content/docs/language-guide/02-fields.md index b11548d..abd648c 100644 --- a/website/content/docs/language-guide/02-fields.md +++ b/website/content/docs/language-guide/02-fields.md @@ -7,14 +7,14 @@ Fields are the building blocks of records. Let's explore the different types of ## Required vs Optional Fields -By default, all fields are required. Use `?` to make a field optional: +By default, all fields are optional. Use `!` to make a field required: ```mlf record user { - name: string, // Required - must be provided - bio?: string, // Optional - can be omitted - email: string, // Required - website?: string, // Optional + name!: string, // Required - must be provided + bio: string, // Optional - can be omitted (default) + email!: string, // Required + website: string, // Optional (default) } ``` @@ -155,32 +155,32 @@ Here's a complete record showing all field types: /// A forum post record post { /// Post text content - text: string, + text!: string, /// Post author - author: Did, + author!: Did, /// When the post was created - createdAt: Datetime, + createdAt!: Datetime, /// Optional reply count - replyCount?: integer, + replyCount: integer, /// Whether the post is pinned - isPinned: boolean, + isPinned!: boolean, /// Optional geographic location - location?: { - name: string, - lat: integer, - lng: integer, + location: { + name!: string, + lat!: integer, + lng!: integer, }, /// Tags on this post - tags: string[], + tags!: string[], /// Optional embedded images - images?: Uri[], + images: Uri[], /// Arbitrary metadata metadata: unknown, diff --git a/website/content/docs/language-guide/03-constraints.md b/website/content/docs/language-guide/03-constraints.md index 34f76cb..569dc47 100644 --- a/website/content/docs/language-guide/03-constraints.md +++ b/website/content/docs/language-guide/03-constraints.md @@ -185,53 +185,53 @@ Here's a complete record demonstrating various constraints: /// A forum thread record thread { /// Thread title (1-200 characters) - title: string constrained { + title!: string constrained { minGraphemes: 1, maxGraphemes: 200, }, /// Thread content - body: string constrained { + body!: string constrained { maxGraphemes: 5000, }, /// View count (must be non-negative) - views: integer constrained { + views!: integer constrained { minimum: 0, }, /// Reply count - replies: integer constrained { + replies!: integer constrained { minimum: 0, default: 0, }, /// Thread status - status: string constrained { + status!: string constrained { enum: ["open", "closed", "pinned"], default: "open", }, /// Thread images (1-10 images) - images: Uri[] constrained { + images!: Uri[] constrained { minLength: 1, maxLength: 10, }, /// Optional thread thumbnail - thumbnail?: blob constrained { + thumbnail: blob constrained { accept: ["image/png", "image/jpeg"], maxSize: 500000, }, /// Average rating (0-5 scale) - rating?: integer constrained { + rating: integer constrained { minimum: 0, maximum: 5, }, /// Whether thread is featured - featured: boolean constrained { + featured!: boolean constrained { default: false, }, } diff --git a/website/content/docs/language-guide/04-custom-types.md b/website/content/docs/language-guide/04-custom-types.md index 36d81f9..242643a 100644 --- a/website/content/docs/language-guide/04-custom-types.md +++ b/website/content/docs/language-guide/04-custom-types.md @@ -52,9 +52,9 @@ inline type EmailAddress = string constrained { inline type UserId = Did; // Simple alias record account { - id: UserId, - email: EmailAddress, - loginCount: PositiveInt, + id!: UserId, + email!: EmailAddress, + loginCount!: PositiveInt, } ``` @@ -62,12 +62,12 @@ Inline types can define objects too: ```mlf inline type Coordinates = { - lat: integer, - lng: integer, + lat!: integer, + lng!: integer, }; record location { - coords: Coordinates, + coords!: Coordinates, } ``` @@ -77,17 +77,17 @@ When you want a type to be **shared and referenced by name** in the generated le ```mlf def type author = { - did: Did, - handle: Handle, - displayName?: string, + did!: Did, + handle!: Handle, + displayName: string, }; record post { - author: author, + author!: author, } record comment { - author: author, + author!: author, } ``` @@ -127,12 +127,12 @@ record post { **Def type (referenced by name):** ```mlf def type postRef = { - uri: AtUri, - cid: Cid, + uri!: AtUri, + cid!: Cid, }; record reply { - replyTo: postRef, // References: #postRef in lexicon + replyTo!: postRef, // References: #postRef in lexicon } ``` @@ -154,36 +154,36 @@ inline type LongText = string constrained { // Def type for shared object def type author = { - did: Did, - handle: Handle, - displayName?: ShortText, + did!: Did, + handle!: Handle, + displayName: ShortText, }; /// A forum thread record thread { /// Thread title - title: ShortText, + title!: ShortText, /// Thread body - body: LongText, + body!: LongText, /// Thread author - author: author, + author!: author, /// When created - createdAt: Datetime, + createdAt!: Datetime, } /// A reply to a thread record reply { /// Reply text - text: LongText, + text!: LongText, /// Reply author (reuses author type) - author: author, + author!: author, /// When created - createdAt: Datetime, + createdAt!: Datetime, } ``` diff --git a/website/content/docs/language-guide/05-unions.md b/website/content/docs/language-guide/05-unions.md index 2fbd6e1..db30c74 100644 --- a/website/content/docs/language-guide/05-unions.md +++ b/website/content/docs/language-guide/05-unions.md @@ -3,25 +3,25 @@ title = "Unions" weight = 5 +++ -Unions allow a field to accept multiple types. MLF supports both closed unions (fixed set of types) and open unions (allowing unknown types). +Unions allow a field to accept multiple types. MLF supports both open unions (allowing unknown types) and closed unions (fixed set of types). -## Closed Unions +## Open Unions -Use the pipe operator `|` to create a union of types: +By default, unions are open. Use the pipe operator `|` to create a union of types: ```mlf def type textPost = { - text: string, + text!: string, }; def type imagePost = { - image: Uri, - caption?: string, + image!: Uri, + caption: string, }; def type videoPost = { - video: Uri, - duration: integer, + video!: Uri, + duration!: integer, }; record post { @@ -29,42 +29,42 @@ record post { } ``` -The `content` field must be one of these three types. No other types are accepted. +The `content` field accepts these three types, plus any unknown types. This allows for forward compatibility. -## Open Unions +## Closed Unions -Add `| _` to allow unknown types for forward compatibility: +Add `| !` to create a closed union that only accepts the listed types: ```mlf record post { - content: textPost | imagePost | _, + content: textPost | imagePost | !, } ``` -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. +Now the system will only accept `textPost` and `imagePost`. No other types are allowed. Use closed unions when you want strict type checking. ## Why Use Open Unions? Open unions help with forward compatibility: ```mlf -// Version 1: Only text and images +// Version 1: Only text and images (open by default) record post { - content: textPost | imagePost | _, + content: textPost | imagePost, } // Version 2: Add video support -// Old clients still work because of the `_` +// Old clients still work because unions are open by default def type videoPost = { - video: Uri, + video!: Uri, }; record post { - content: textPost | imagePost | videoPost | _, + content: textPost | imagePost | videoPost, } ``` -Old clients that don't know about `videoPost` can still handle the lexicon because of the open union. +Old clients that don't know about `videoPost` can still handle the lexicon because of the open union (the default behavior). ## Unions with Inline Objects @@ -73,12 +73,12 @@ You don't need to define types separately: ```mlf record embed { content: { - text: string, + text!: string, } | { - image: Uri, + image!: Uri, } | { - link: Uri, - title: string, + link!: Uri, + title!: string, }, } ``` @@ -91,25 +91,25 @@ Unions work in arrays: ```mlf def type mention = { - did: Did, - start: integer, - end: integer, + did!: Did, + start!: integer, + end!: integer, }; def type link = { - uri: Uri, - start: integer, - end: integer, + uri!: Uri, + start!: integer, + end!: integer, }; def type tag = { - name: string, - start: integer, - end: integer, + name!: string, + start!: integer, + end!: integer, }; record post { - text: string, + text!: string, facets: (mention | link | tag)[], } ``` @@ -124,44 +124,44 @@ Here's a complete forum post system with unions: ```mlf /// Text content def type textContent = { - text: string constrained { + text!: string constrained { maxGraphemes: 2000, }, }; /// Image content def type imageContent = { - url: Uri, - width: integer, - height: integer, - alt?: string, + url!: Uri, + width!: integer, + height!: integer, + alt: string, }; /// File attachment def type fileContent = { - url: Uri, - filename: string, - size: integer, - mimeType: string, + url!: Uri, + filename!: string, + size!: integer, + mimeType!: string, }; /// A forum post with different content types record post { /// Post author - author: Did, + author!: Did, - /// Post content (text, image, or file) - content: textContent | imageContent | fileContent | _, + /// Post content (text, image, or file) - open union by default + content: textContent | imageContent | fileContent, /// When the post was created - createdAt: Datetime, + createdAt!: Datetime, /// Optional reply reference - replyTo?: AtUri, + replyTo: AtUri, } ``` -The `| _` at the end means future content types can be added without breaking old clients. +Unions are open by default, which means future content types can be added without breaking old clients. ## What's Next? diff --git a/website/content/docs/language-guide/07-xrpc.md b/website/content/docs/language-guide/07-xrpc.md index 2835b80..cb2149b 100644 --- a/website/content/docs/language-guide/07-xrpc.md +++ b/website/content/docs/language-guide/07-xrpc.md @@ -27,11 +27,11 @@ Queries are **read-only operations** that use HTTP GET. They retrieve data witho ```mlf /// Get a user profile query getProfile( - actor: Did + actor!: Did ):{ - did: Did, - handle: Handle, - displayName?: string, + did!: Did, + handle!: Handle, + displayName: string, }; ``` @@ -39,29 +39,29 @@ query getProfile( ```mlf /// Search for posts query searchPosts( - q: string, - limit?: integer constrained { + q!: string, + limit: integer constrained { minimum: 1, maximum: 100, default: 25, }, - cursor?: string + cursor: string ):{ - posts: post[], - cursor?: string, + posts!: post[], + cursor: string, }; ``` **Query returning a record:** ```mlf record profile { - did: Did, - handle: Handle, - displayName?: string, + did!: Did, + handle!: Handle, + displayName: string, } query getProfile( - actor: Did + actor!: Did ):profile; ``` @@ -73,13 +73,13 @@ Procedures are **write operations** that use HTTP POST. They create, update, or ```mlf /// Create a new post procedure createPost( - text: string constrained { + text!: string constrained { minGraphemes: 1, maxGraphemes: 500, } ):{ - uri: AtUri, - cid: Cid, + uri!: AtUri, + cid!: Cid, }; ``` @@ -87,11 +87,11 @@ procedure createPost( ```mlf /// Update a user profile procedure updateProfile( - displayName?: string, - bio?: string, - avatar?: blob + displayName: string, + bio: string, + avatar: blob ):{ - success: boolean, + success!: boolean, }; ``` @@ -99,9 +99,9 @@ procedure updateProfile( ```mlf /// Delete a post procedure deletePost( - uri: AtUri + uri!: AtUri ):{ - success: boolean, + success!: boolean, }; ``` @@ -119,22 +119,22 @@ subscription subscribePosts():post; ```mlf /// Subscribe to posts from specific users subscription subscribePosts( - authors?: Did[] + authors: Did[] ):post; ``` **Subscription with multiple message types:** ```mlf def type postCreated = { - post: post, + post!: post, }; def type postDeleted = { - uri: AtUri, + uri!: AtUri, }; def type postUpdated = { - post: post, + post!: post, }; /// Subscribe to post events @@ -145,7 +145,7 @@ subscription subscribePostEvents():postCreated | postDeleted | postUpdated; ```mlf /// Subscribe to repository events subscription subscribeRepos( - cursor?: integer + cursor: integer ):commit | identity | tombstone; ``` @@ -166,7 +166,7 @@ Queries and procedures can specify errors using `| error`: ```mlf query getPost( - uri: AtUri + uri!: AtUri ):post | error { /// Post not found NotFound, @@ -178,10 +178,10 @@ query getPost( **Procedure with errors:** ```mlf procedure createPost( - text: string + text!: string ):{ - uri: AtUri, - cid: Cid, + uri!: AtUri, + cid!: Cid, } | error { /// Text exceeds maximum length TextTooLong, @@ -203,19 +203,19 @@ Parameters can have constraints just like record fields: ```mlf query searchPosts( /// Search query (1-200 characters) - q: string constrained { + q!: string constrained { minLength: 1, maxLength: 200, }, /// Results per page - limit?: integer constrained { + limit: integer constrained { minimum: 1, maximum: 100, default: 25, } ):{ - posts: post[], + posts!: post[], } ``` @@ -226,14 +226,14 @@ Operations can return: **Inline objects:** ```mlf query getStats():{ - posts: integer, - followers: integer, + posts!: integer, + followers!: integer, }; ``` **Named records:** ```mlf -query getProfile(did: Did):profile; +query getProfile(did!: Did):profile; ``` **Unions:** @@ -250,27 +250,27 @@ Here's a complete API for a forum: /// A forum post record post { /// Post title - title: string constrained { + title!: string constrained { minGraphemes: 1, maxGraphemes: 200, }, /// Post content - body: string constrained { + body!: string constrained { maxGraphemes: 50000, }, /// Post author - author: Did, + author!: Did, /// When published - publishedAt: Datetime, + publishedAt!: Datetime, } /// Get a single post query getPost( /// Post URI - uri: AtUri + uri!: AtUri ):post | error { /// Post not found NotFound, @@ -281,38 +281,38 @@ query getPost( /// List posts by author query listPosts( /// Author DID - author: Did, + author!: Did, /// Results per page - limit?: integer constrained { + limit: integer constrained { minimum: 1, maximum: 100, default: 25, }, /// Pagination cursor - cursor?: string + cursor: string ):{ - posts: post[], - cursor?: string, + posts!: post[], + cursor: string, }; /// Create a new post procedure createPost( /// Post title - title: string constrained { + title!: string constrained { minGraphemes: 1, maxGraphemes: 200, }, /// Post body - body: string constrained { + body!: string constrained { maxGraphemes: 50000, } ):{ - uri: AtUri, - cid: Cid, - post: post, + uri!: AtUri, + cid!: Cid, + post!: post, } | error { /// User not authenticated Unauthorized, @@ -323,9 +323,9 @@ procedure createPost( /// Delete a post procedure deletePost( /// Post URI to delete - uri: AtUri + uri!: AtUri ):{ - success: boolean, + success!: boolean, } | error { /// Post not found NotFound, @@ -336,7 +336,7 @@ procedure deletePost( /// Subscribe to new posts subscription subscribePosts( /// Optional author filter - author?: Did + author: Did ):post; ``` diff --git a/website/syntaxes/mlf.sublime-syntax b/website/syntaxes/mlf.sublime-syntax index bb727a3..cc07180 100644 --- a/website/syntaxes/mlf.sublime-syntax +++ b/website/syntaxes/mlf.sublime-syntax @@ -67,8 +67,8 @@ contexts: scope: punctuation.section.mlf - match: '[,;:]' scope: punctuation.separator.mlf - - match: '\?' - scope: keyword.operator.optional.mlf + - match: '!' + scope: keyword.operator.required.mlf - match: '\|' scope: keyword.operator.union.mlf - match: '='