A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

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

Website updates

+2024 -602
+9 -9
SPEC.md
··· 72 72 73 73 #### Semicolons 74 74 75 - - **Records** do NOT have semicolons after the closing brace `}` 76 - - All other definitions require semicolons: 77 - - `use` statements end with `;` 78 - - `token` definitions end with `;` 79 - - `inline type` definitions end with `;` 80 - - `def type` definitions end with `;` 81 - - `query` definitions end with `;` 82 - - `procedure` definitions end with `;` 83 - - `subscription` definitions end with `;` 75 + All definitions require semicolons: 76 + - `record` definitions end with `};` 77 + - `use` statements end with `;` 78 + - `token` definitions end with `;` 79 + - `inline type` definitions end with `;` 80 + - `def type` definitions end with `;` 81 + - `query` definitions end with `;` 82 + - `procedure` definitions end with `;` 83 + - `subscription` definitions end with `;` 84 84 85 85 #### Commas 86 86
+1
website/content/docs/_index.md
··· 3 3 description = "Complete guide to MLF" 4 4 sort_by = "weight" 5 5 template = "section.html" 6 + redirect_to = "/docs/getting-started/" 6 7 +++ 7 8 8 9 MLF (Matt's Lexicon Format) is a human-friendly DSL for writing ATProto Lexicons. This documentation will help you learn the language, use the CLI tools, and integrate MLF into your projects.
+131
website/content/docs/language-guide/01-your-first-lexicon.md
··· 1 + +++ 2 + title = "Your First Lexicon" 3 + weight = 1 4 + +++ 5 + 6 + Welcome to MLF! Let's create your first lexicon by defining a simple record. 7 + 8 + ## A Basic Record 9 + 10 + Here's a complete MLF file that defines a user profile: 11 + 12 + ```mlf 13 + /// A user profile 14 + record profile { 15 + /// The user's display name 16 + name: string, 17 + /// The user's email address 18 + email: string, 19 + /// When the account was created 20 + createdAt: Datetime, 21 + } 22 + ``` 23 + 24 + This defines a `profile` record with three fields: `name`, `email`, and `createdAt`. 25 + 26 + ## File Naming and Namespaces 27 + 28 + The file path determines the lexicon namespace. If you save this as: 29 + 30 + ``` 31 + com/example/forum/profile.mlf 32 + ``` 33 + 34 + Then the namespace will be `com.example.forum.profile`, and the full identifier for this record is `com.example.forum.profile`. 35 + 36 + The namespace comes from the file path, not from any declaration in the file. 37 + 38 + ## What Gets Generated 39 + 40 + When you compile this MLF file, it generates a JSON lexicon: 41 + 42 + ```json 43 + { 44 + "lexicon": 1, 45 + "id": "com.example.forum.profile", 46 + "defs": { 47 + "main": { 48 + "type": "record", 49 + "description": "A user profile", 50 + "key": "tid", 51 + "record": { 52 + "type": "object", 53 + "required": ["name", "email", "createdAt"], 54 + "properties": { 55 + "name": { 56 + "type": "string", 57 + "description": "The user's display name" 58 + }, 59 + "email": { 60 + "type": "string", 61 + "description": "The user's email address" 62 + }, 63 + "createdAt": { 64 + "type": "string", 65 + "format": "datetime", 66 + "description": "When the account was created" 67 + } 68 + } 69 + } 70 + } 71 + } 72 + } 73 + ``` 74 + 75 + The MLF syntax is much cleaner and easier to read! 76 + 77 + ## Comments 78 + 79 + MLF supports three types of comments: 80 + 81 + **Documentation comments** (`///`) appear in the generated lexicon: 82 + ```mlf 83 + /// This comment appears in generated docs 84 + record example { 85 + /// This field comment also appears 86 + field: string, 87 + } 88 + ``` 89 + 90 + **Regular comments** (`//`) are for internal notes only: 91 + ```mlf 92 + // This is a note to yourself, won't appear in output 93 + record example { 94 + field: string, // Inline comments work too 95 + } 96 + ``` 97 + 98 + **Hash comments** (`#`) at the start of a file are ignored (useful for shebangs): 99 + ```mlf 100 + #!/usr/bin/env mlf 101 + # This line is ignored 102 + 103 + record example { 104 + field: string, 105 + } 106 + ``` 107 + 108 + ## Complete Example 109 + 110 + Here's a minimal, complete lexicon for a forum post: 111 + 112 + **File: `com/example/forum/post.mlf`** 113 + ```mlf 114 + /// A forum post 115 + record post { 116 + /// Post title 117 + title: string, 118 + /// Post content 119 + body: string, 120 + /// Post author's DID 121 + author: Did, 122 + /// When the post was published 123 + publishedAt: Datetime, 124 + } 125 + ``` 126 + 127 + This creates the lexicon `com.example.forum.post` with a single record definition. 128 + 129 + ## What's Next? 130 + 131 + Now that you understand the basics, let's learn about fields in more detail.
+198
website/content/docs/language-guide/02-fields.md
··· 1 + +++ 2 + title = "Fields" 3 + weight = 2 4 + +++ 5 + 6 + Fields are the building blocks of records. Let's explore the different types of fields you can define. 7 + 8 + ## Required vs Optional Fields 9 + 10 + By default, all fields are required. Use `?` to make a field optional: 11 + 12 + ```mlf 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 18 + } 19 + ``` 20 + 21 + ## Primitive Types 22 + 23 + MLF supports several primitive types: 24 + 25 + **Strings:** 26 + ```mlf 27 + record example { 28 + name: string, 29 + } 30 + ``` 31 + 32 + **Integers** (64-bit signed): 33 + ```mlf 34 + record example { 35 + count: integer, 36 + age: integer, 37 + } 38 + ``` 39 + 40 + **Numbers** (double-precision floats): 41 + ```mlf 42 + record example { 43 + price: number, 44 + rating: number, 45 + } 46 + ``` 47 + 48 + **Booleans:** 49 + ```mlf 50 + record example { 51 + isActive: boolean, 52 + verified: boolean, 53 + } 54 + ``` 55 + 56 + **Binary data:** 57 + ```mlf 58 + record example { 59 + data: bytes, // Raw byte array 60 + image: blob, // Binary with metadata (MIME type, size) 61 + } 62 + ``` 63 + 64 + **Unknown** (for forward compatibility): 65 + ```mlf 66 + record example { 67 + metadata: unknown, // Can be any value 68 + } 69 + ``` 70 + 71 + **Null:** 72 + ```mlf 73 + record example { 74 + nothing: null, // Always null (rarely used) 75 + } 76 + ``` 77 + 78 + ## Special Format Types 79 + 80 + MLF provides built-in types for common formats: 81 + 82 + ```mlf 83 + record post { 84 + author: Did, // Decentralized Identifier (did:*) 85 + uri: AtUri, // AT Protocol URI (at://...) 86 + timestamp: Datetime, // ISO 8601 datetime 87 + website: Uri, // Generic URI 88 + contentHash: Cid, // Content Identifier 89 + handle: Handle, // Handle (domain name) 90 + language: Language, // BCP 47 language code 91 + } 92 + ``` 93 + 94 + These are actually inline type aliases defined in the prelude, but you can use them as if they were primitive types. 95 + 96 + ## Objects 97 + 98 + Define inline object types with curly braces: 99 + 100 + ```mlf 101 + record post { 102 + author: { 103 + did: Did, 104 + handle: Handle, 105 + name: string, 106 + }, 107 + } 108 + ``` 109 + 110 + Objects can be nested: 111 + 112 + ```mlf 113 + record profile { 114 + location: { 115 + city: string, 116 + coordinates: { 117 + lat: number, 118 + lng: number, 119 + }, 120 + }, 121 + } 122 + ``` 123 + 124 + ## Arrays 125 + 126 + Add `[]` after any type to make it an array: 127 + 128 + ```mlf 129 + record post { 130 + tags: string[], // Array of strings 131 + images: Uri[], // Array of URIs 132 + counts: integer[], // Array of integers 133 + } 134 + ``` 135 + 136 + Arrays of objects: 137 + 138 + ```mlf 139 + record post { 140 + authors: { 141 + did: Did, 142 + role: string, 143 + }[], 144 + } 145 + ``` 146 + 147 + Nested arrays: 148 + 149 + ```mlf 150 + record matrix { 151 + grid: integer[][], // Array of arrays 152 + } 153 + ``` 154 + 155 + ## Complete Example 156 + 157 + Here's a complete record showing all field types: 158 + 159 + **File: `com/example/forum/post.mlf`** 160 + ```mlf 161 + /// A forum post 162 + record post { 163 + /// Post text content 164 + text: string, 165 + 166 + /// Post author 167 + author: Did, 168 + 169 + /// When the post was created 170 + createdAt: Datetime, 171 + 172 + /// Optional reply count 173 + replyCount?: integer, 174 + 175 + /// Whether the post is pinned 176 + isPinned: boolean, 177 + 178 + /// Optional geographic location 179 + location?: { 180 + name: string, 181 + lat: number, 182 + lng: number, 183 + }, 184 + 185 + /// Tags on this post 186 + tags: string[], 187 + 188 + /// Optional embedded images 189 + images?: Uri[], 190 + 191 + /// Arbitrary metadata 192 + metadata: unknown, 193 + } 194 + ``` 195 + 196 + ## What's Next? 197 + 198 + Now that you understand fields, let's learn how to add validation rules with constraints.
+256
website/content/docs/language-guide/03-constraints.md
··· 1 + +++ 2 + title = "Constraints" 3 + weight = 3 4 + +++ 5 + 6 + Constraints add validation rules to your fields, ensuring data meets specific requirements. 7 + 8 + ## String Constraints 9 + 10 + Strings support several validation options: 11 + 12 + **Length constraints** (in bytes): 13 + ```mlf 14 + title: string constrained { 15 + minLength: 1, 16 + maxLength: 200, 17 + } 18 + ``` 19 + 20 + **Grapheme constraints** (user-perceived characters): 21 + ```mlf 22 + text: string constrained { 23 + minGraphemes: 1, 24 + maxGraphemes: 500, 25 + } 26 + ``` 27 + 28 + Use `maxGraphemes` for user-visible content (handles emojis correctly), and `maxLength` for technical limits. 29 + 30 + **Enum** (closed set - only these values allowed): 31 + ```mlf 32 + status: string constrained { 33 + enum: ["draft", "published", "archived"], 34 + } 35 + ``` 36 + 37 + **Known values** (open set - these values are documented, but others are allowed): 38 + ```mlf 39 + postType: string constrained { 40 + knownValues: ["text", "image", "video"], 41 + } 42 + ``` 43 + 44 + **Default value:** 45 + ```mlf 46 + visibility: string constrained { 47 + enum: ["public", "private"], 48 + default: "public", 49 + } 50 + ``` 51 + 52 + **Format validation:** 53 + ```mlf 54 + email: string constrained { 55 + format: "email", 56 + } 57 + ``` 58 + 59 + **All string constraints together:** 60 + ```mlf 61 + username: string constrained { 62 + minLength: 3, 63 + maxLength: 20, 64 + minGraphemes: 3, 65 + maxGraphemes: 20, 66 + } 67 + ``` 68 + 69 + ## Integer Constraints 70 + 71 + Integers can have numeric ranges and specific values: 72 + 73 + **Range constraints:** 74 + ```mlf 75 + age: integer constrained { 76 + minimum: 0, 77 + maximum: 150, 78 + } 79 + ``` 80 + 81 + **Enum values:** 82 + ```mlf 83 + priority: integer constrained { 84 + enum: [1, 2, 3, 4, 5], 85 + } 86 + ``` 87 + 88 + **Default value:** 89 + ```mlf 90 + count: integer constrained { 91 + minimum: 0, 92 + default: 0, 93 + } 94 + ``` 95 + 96 + ## Number Constraints 97 + 98 + Numbers (floats) work the same as integers: 99 + 100 + ```mlf 101 + rating: number constrained { 102 + minimum: 0.0, 103 + maximum: 5.0, 104 + } 105 + 106 + price: number constrained { 107 + minimum: 0.0, 108 + default: 0.0, 109 + } 110 + ``` 111 + 112 + ## Boolean Constraints 113 + 114 + Booleans only support default values: 115 + 116 + ```mlf 117 + isActive: boolean constrained { 118 + default: true, 119 + } 120 + ``` 121 + 122 + ## Array Constraints 123 + 124 + Arrays can be constrained by length: 125 + 126 + ```mlf 127 + tags: string[] constrained { 128 + minLength: 1, 129 + maxLength: 10, 130 + } 131 + 132 + images: Uri[] constrained { 133 + maxLength: 4, 134 + } 135 + ``` 136 + 137 + ## Blob Constraints 138 + 139 + Blobs support MIME type and size constraints: 140 + 141 + ```mlf 142 + avatar: blob constrained { 143 + accept: ["image/png", "image/jpeg", "image/webp"], 144 + maxSize: 1000000, // 1MB in bytes 145 + } 146 + 147 + video: blob constrained { 148 + accept: ["video/mp4"], 149 + maxSize: 50000000, // 50MB 150 + } 151 + ``` 152 + 153 + ## Constraint Refinement 154 + 155 + You can apply constraints multiple times, but they must always become **more restrictive**: 156 + 157 + ```mlf 158 + record post { 159 + // First constraint: max 500 characters 160 + text: string constrained { 161 + maxGraphemes: 500, 162 + }, 163 + } 164 + 165 + record shortPost { 166 + // Further constrain to max 100 characters - valid! 167 + text: string constrained { 168 + maxGraphemes: 500, 169 + } constrained { 170 + maxGraphemes: 100, 171 + }, 172 + } 173 + ``` 174 + 175 + **Refinement rules:** 176 + - `minimum` can only increase 177 + - `maximum` can only decrease 178 + - `minLength`/`minGraphemes` can only increase 179 + - `maxLength`/`maxGraphemes` can only decrease 180 + - `enum` can only restrict to a subset 181 + - `format` cannot change once set 182 + 183 + **Invalid refinement:** 184 + ```mlf 185 + // ERROR: Can't increase maximum 186 + text: string constrained { 187 + maxLength: 100, 188 + } constrained { 189 + maxLength: 200, // Invalid! Going from 100 to 200 190 + } 191 + ``` 192 + 193 + ## Complete Example 194 + 195 + Here's a complete record demonstrating various constraints: 196 + 197 + **File: `com/example/forum/thread.mlf`** 198 + ```mlf 199 + /// A forum thread 200 + record thread { 201 + /// Thread title (1-200 characters) 202 + title: string constrained { 203 + minGraphemes: 1, 204 + maxGraphemes: 200, 205 + }, 206 + 207 + /// Thread content 208 + body: string constrained { 209 + maxGraphemes: 5000, 210 + }, 211 + 212 + /// View count (must be non-negative) 213 + views: integer constrained { 214 + minimum: 0, 215 + }, 216 + 217 + /// Reply count 218 + replies: integer constrained { 219 + minimum: 0, 220 + default: 0, 221 + }, 222 + 223 + /// Thread status 224 + status: string constrained { 225 + enum: ["open", "closed", "pinned"], 226 + default: "open", 227 + }, 228 + 229 + /// Thread images (1-10 images) 230 + images: Uri[] constrained { 231 + minLength: 1, 232 + maxLength: 10, 233 + }, 234 + 235 + /// Optional thread thumbnail 236 + thumbnail?: blob constrained { 237 + accept: ["image/png", "image/jpeg"], 238 + maxSize: 500000, 239 + }, 240 + 241 + /// Average rating (0-5 stars) 242 + rating?: number constrained { 243 + minimum: 0.0, 244 + maximum: 5.0, 245 + }, 246 + 247 + /// Whether thread is featured 248 + featured: boolean constrained { 249 + default: false, 250 + }, 251 + } 252 + ``` 253 + 254 + ## What's Next? 255 + 256 + Now that you can validate your data, let's learn how to create reusable type definitions.
+197
website/content/docs/language-guide/04-custom-types.md
··· 1 + +++ 2 + title = "Custom Types" 3 + weight = 4 4 + +++ 5 + 6 + As you build lexicons, you'll find yourself repeating the same constraints and object shapes. Custom types help you avoid duplication. 7 + 8 + ## Inline Types 9 + 10 + Inline types are like macros or type aliases - they expand at the point of use and never appear in the generated lexicon. 11 + 12 + **Without inline types:** 13 + ```mlf 14 + record user { 15 + name: string constrained { 16 + minGraphemes: 1, 17 + maxGraphemes: 100, 18 + }, 19 + displayName: string constrained { 20 + minGraphemes: 1, 21 + maxGraphemes: 100, 22 + }, 23 + } 24 + ``` 25 + 26 + **With inline types:** 27 + ```mlf 28 + inline type ShortText = string constrained { 29 + minGraphemes: 1, 30 + maxGraphemes: 100, 31 + }; 32 + 33 + record user { 34 + name: ShortText, 35 + displayName: ShortText, 36 + } 37 + ``` 38 + 39 + When compiled, `ShortText` is replaced with the full constraint definition. It's purely for convenience. 40 + 41 + **More examples:** 42 + ```mlf 43 + inline type PositiveInt = integer constrained { 44 + minimum: 0, 45 + }; 46 + 47 + inline type EmailAddress = string constrained { 48 + format: "email", 49 + maxLength: 254, 50 + }; 51 + 52 + inline type UserId = Did; // Simple alias 53 + 54 + record account { 55 + id: UserId, 56 + email: EmailAddress, 57 + loginCount: PositiveInt, 58 + } 59 + ``` 60 + 61 + Inline types can define objects too: 62 + 63 + ```mlf 64 + inline type Coordinates = { 65 + lat: number, 66 + lng: number, 67 + }; 68 + 69 + record location { 70 + coords: Coordinates, 71 + } 72 + ``` 73 + 74 + ## Def Types 75 + 76 + When you want a type to be **shared and referenced by name** in the generated lexicon, use `def type`: 77 + 78 + ```mlf 79 + def type author = { 80 + did: Did, 81 + handle: Handle, 82 + displayName?: string, 83 + }; 84 + 85 + record post { 86 + author: author, 87 + } 88 + 89 + record comment { 90 + author: author, 91 + } 92 + ``` 93 + 94 + In the generated lexicon, `author` appears as a named definition that both `post` and `comment` reference. 95 + 96 + **Key difference:** 97 + - `inline type` - expands inline, doesn't appear in output 98 + - `def type` - becomes a named definition, referenced by name 99 + 100 + ## When to Use Each 101 + 102 + **Use inline types for:** 103 + - Type aliases (`inline type UserId = Did;`) 104 + - Reusable constraint patterns 105 + - Types that should be transparent in the output 106 + - Simple wrappers 107 + 108 + **Use def types for:** 109 + - Complex objects used multiple times 110 + - Types that form part of your API contract 111 + - Types you want to reference from other files 112 + - Types that should have their own documentation 113 + 114 + ## Example Comparison 115 + 116 + **Inline type (expands everywhere):** 117 + ```mlf 118 + inline type ShortString = string constrained { 119 + maxGraphemes: 100, 120 + }; 121 + 122 + record post { 123 + title: ShortString, // Expands to: string constrained { maxGraphemes: 100 } 124 + } 125 + ``` 126 + 127 + **Def type (referenced by name):** 128 + ```mlf 129 + def type postRef = { 130 + uri: AtUri, 131 + cid: Cid, 132 + }; 133 + 134 + record reply { 135 + replyTo: postRef, // References: #postRef in lexicon 136 + } 137 + ``` 138 + 139 + ## Complete Example 140 + 141 + Here's a complete file showing both types: 142 + 143 + **File: `com/example/forum/thread.mlf`** 144 + ```mlf 145 + // Inline types for common patterns 146 + inline type ShortText = string constrained { 147 + minGraphemes: 1, 148 + maxGraphemes: 200, 149 + }; 150 + 151 + inline type LongText = string constrained { 152 + maxGraphemes: 50000, 153 + }; 154 + 155 + // Def type for shared object 156 + def type author = { 157 + did: Did, 158 + handle: Handle, 159 + displayName?: ShortText, 160 + }; 161 + 162 + /// A forum thread 163 + record thread { 164 + /// Thread title 165 + title: ShortText, 166 + 167 + /// Thread body 168 + body: LongText, 169 + 170 + /// Thread author 171 + author: author, 172 + 173 + /// When created 174 + createdAt: Datetime, 175 + } 176 + 177 + /// A reply to a thread 178 + record reply { 179 + /// Reply text 180 + text: LongText, 181 + 182 + /// Reply author (reuses author type) 183 + author: author, 184 + 185 + /// When created 186 + createdAt: Datetime, 187 + } 188 + ``` 189 + 190 + In the generated lexicon: 191 + - `ShortText` and `LongText` don't appear - they're expanded inline 192 + - `author` appears as a named definition in the `defs` block 193 + - Both `thread` and `reply` reference `#author` 194 + 195 + ## What's Next? 196 + 197 + Now that you can create reusable types, let's learn about unions for accepting multiple types.
+168
website/content/docs/language-guide/05-unions.md
··· 1 + +++ 2 + title = "Unions" 3 + weight = 5 4 + +++ 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). 7 + 8 + ## Closed Unions 9 + 10 + Use the pipe operator `|` to create a union of types: 11 + 12 + ```mlf 13 + def type textPost = { 14 + text: string, 15 + }; 16 + 17 + def type imagePost = { 18 + image: Uri, 19 + caption?: string, 20 + }; 21 + 22 + def type videoPost = { 23 + video: Uri, 24 + duration: integer, 25 + }; 26 + 27 + record post { 28 + content: textPost | imagePost | videoPost, 29 + } 30 + ``` 31 + 32 + The `content` field must be one of these three types. No other types are accepted. 33 + 34 + ## Open Unions 35 + 36 + Add `| _` to allow unknown types for forward compatibility: 37 + 38 + ```mlf 39 + record post { 40 + content: textPost | imagePost | _, 41 + } 42 + ``` 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. 45 + 46 + ## Why Use Open Unions? 47 + 48 + Open unions help with forward compatibility: 49 + 50 + ```mlf 51 + // Version 1: Only text and images 52 + record post { 53 + content: textPost | imagePost | _, 54 + } 55 + 56 + // Version 2: Add video support 57 + // Old clients still work because of the `_` 58 + def type videoPost = { 59 + video: Uri, 60 + }; 61 + 62 + record post { 63 + content: textPost | imagePost | videoPost | _, 64 + } 65 + ``` 66 + 67 + Old clients that don't know about `videoPost` can still handle the lexicon because of the open union. 68 + 69 + ## Unions with Inline Objects 70 + 71 + You don't need to define types separately: 72 + 73 + ```mlf 74 + record embed { 75 + content: { 76 + text: string, 77 + } | { 78 + image: Uri, 79 + } | { 80 + link: Uri, 81 + title: string, 82 + }, 83 + } 84 + ``` 85 + 86 + Though defining types separately is often cleaner. 87 + 88 + ## Unions in Arrays 89 + 90 + Unions work in arrays: 91 + 92 + ```mlf 93 + def type mention = { 94 + did: Did, 95 + start: integer, 96 + end: integer, 97 + }; 98 + 99 + def type link = { 100 + uri: Uri, 101 + start: integer, 102 + end: integer, 103 + }; 104 + 105 + def type tag = { 106 + name: string, 107 + start: integer, 108 + end: integer, 109 + }; 110 + 111 + record post { 112 + text: string, 113 + facets: (mention | link | tag)[], 114 + } 115 + ``` 116 + 117 + The `facets` array can contain any mix of mentions, links, and tags. 118 + 119 + ## Complete Example 120 + 121 + Here's a complete forum post system with unions: 122 + 123 + **File: `com/example/forum/post.mlf`** 124 + ```mlf 125 + /// Text content 126 + def type textContent = { 127 + text: string constrained { 128 + maxGraphemes: 2000, 129 + }, 130 + }; 131 + 132 + /// Image content 133 + def type imageContent = { 134 + url: Uri, 135 + width: integer, 136 + height: integer, 137 + alt?: string, 138 + }; 139 + 140 + /// File attachment 141 + def type fileContent = { 142 + url: Uri, 143 + filename: string, 144 + size: integer, 145 + mimeType: string, 146 + }; 147 + 148 + /// A forum post with different content types 149 + record post { 150 + /// Post author 151 + author: Did, 152 + 153 + /// Post content (text, image, or file) 154 + content: textContent | imageContent | fileContent | _, 155 + 156 + /// When the post was created 157 + createdAt: Datetime, 158 + 159 + /// Optional reply reference 160 + replyTo?: AtUri, 161 + } 162 + ``` 163 + 164 + The `| _` at the end means future content types can be added without breaking old clients. 165 + 166 + ## What's Next? 167 + 168 + Now that you understand unions, let's learn about tokens for named constants.
+245
website/content/docs/language-guide/06-tokens.md
··· 1 + +++ 2 + title = "Tokens" 3 + weight = 6 4 + +++ 5 + 6 + Tokens are named constants that can be used in enums, known values, defaults, and unions. They make your lexicons more maintainable and self-documenting. 7 + 8 + ## Defining Tokens 9 + 10 + Tokens are simple named values with documentation: 11 + 12 + ```mlf 13 + /// Open state 14 + token open; 15 + 16 + /// Closed state 17 + token closed; 18 + 19 + record issue { 20 + state: string constrained { 21 + enum: [open, closed], 22 + }, 23 + } 24 + ``` 25 + 26 + Notice we reference tokens without quotes: `[open, closed]`, not `["open", "closed"]`. 27 + 28 + ## Tokens in Constraints 29 + 30 + Tokens work in enum and knownValues constraints: 31 + 32 + ```mlf 33 + /// Public visibility 34 + token public; 35 + 36 + /// Private visibility 37 + token private; 38 + 39 + /// Unlisted visibility 40 + token unlisted; 41 + 42 + record post { 43 + visibility: string constrained { 44 + enum: [public, private, unlisted], 45 + default: public, 46 + }, 47 + } 48 + ``` 49 + 50 + ## Tokens in Unions 51 + 52 + Tokens can be used directly in unions: 53 + 54 + ```mlf 55 + /// Success status 56 + token success; 57 + 58 + /// Error status 59 + token error; 60 + 61 + /// Pending status 62 + token pending; 63 + 64 + record result { 65 + status: success | error | pending, 66 + } 67 + ``` 68 + 69 + This creates a union where the field must be one of these three token values. 70 + 71 + ## How Tokens Become Strings 72 + 73 + **Important:** In the generated lexicon, tokens are converted to **fully qualified NSID string literals**. 74 + 75 + If you define this in `com/example/forum/post.mlf`: 76 + 77 + ```mlf 78 + token draft; 79 + token published; 80 + 81 + record post { 82 + status: string constrained { 83 + enum: [draft, published], 84 + }, 85 + } 86 + ``` 87 + 88 + The generated lexicon will have: 89 + 90 + ```json 91 + { 92 + "status": { 93 + "type": "string", 94 + "enum": ["com.example.forum.post#draft", "com.example.forum.post#published"] 95 + } 96 + } 97 + ``` 98 + 99 + The tokens become fully qualified: `com.example.forum.post#draft` and `com.example.forum.post#published`. 100 + 101 + ## Why Use Tokens? 102 + 103 + **Without tokens:** 104 + ```mlf 105 + record post { 106 + state: string constrained { 107 + knownValues: ["draft", "published", "archived"], 108 + }, 109 + } 110 + ``` 111 + 112 + - No documentation for individual values 113 + - Easy to typo 114 + - Hard to reuse across files 115 + 116 + **With tokens:** 117 + ```mlf 118 + /// Draft state - not yet published 119 + token draft; 120 + 121 + /// Published state - visible to all 122 + token published; 123 + 124 + /// Archived state - no longer active 125 + token archived; 126 + 127 + record post { 128 + state: string constrained { 129 + knownValues: [draft, published, archived], 130 + }, 131 + } 132 + ``` 133 + 134 + - Each value is documented 135 + - No typos (references are checked) 136 + - Can be reused across definitions 137 + - More maintainable 138 + 139 + ## Reusing Tokens 140 + 141 + Define tokens once and use them everywhere: 142 + 143 + ```mlf 144 + token active; 145 + token inactive; 146 + token suspended; 147 + 148 + record user { 149 + status: string constrained { 150 + enum: [active, inactive, suspended], 151 + }, 152 + } 153 + 154 + record account { 155 + status: string constrained { 156 + enum: [active, inactive], 157 + }, 158 + } 159 + ``` 160 + 161 + ## Tokens vs String Literals 162 + 163 + You can mix tokens and string literals: 164 + 165 + ```mlf 166 + token published; 167 + token archived; 168 + 169 + record post { 170 + status: string constrained { 171 + knownValues: [published, archived, "draft"], // Mix of token and literal 172 + }, 173 + } 174 + ``` 175 + 176 + But using tokens consistently is cleaner. 177 + 178 + ## Complete Example 179 + 180 + Here's a complete forum thread system using tokens: 181 + 182 + **File: `com/example/forum/thread.mlf`** 183 + ```mlf 184 + /// Thread is open for replies 185 + token open; 186 + 187 + /// Thread is closed 188 + token closed; 189 + 190 + /// Thread is pinned 191 + token pinned; 192 + 193 + /// Thread is locked 194 + token locked; 195 + 196 + /// High priority 197 + token high; 198 + 199 + /// Medium priority 200 + token medium; 201 + 202 + /// Low priority 203 + token low; 204 + 205 + /// A thread in a forum 206 + record thread { 207 + /// Thread title 208 + title: string constrained { 209 + minGraphemes: 1, 210 + maxGraphemes: 200, 211 + }, 212 + 213 + /// Thread content 214 + body?: string constrained { 215 + maxGraphemes: 5000, 216 + }, 217 + 218 + /// Thread status 219 + status: string constrained { 220 + enum: [open, closed, pinned, locked], 221 + default: open, 222 + }, 223 + 224 + /// Thread priority 225 + priority: string constrained { 226 + enum: [high, medium, low], 227 + default: medium, 228 + }, 229 + 230 + /// Thread author 231 + author: Did, 232 + 233 + /// When thread was created 234 + createdAt: Datetime, 235 + } 236 + ``` 237 + 238 + In the generated lexicon: 239 + - `open` becomes `com.example.forum.thread#open` 240 + - `closed` becomes `com.example.forum.thread#closed` 241 + - And so on... 242 + 243 + ## What's Next? 244 + 245 + Now that you understand data types, let's learn about XRPC operations: queries, procedures, and subscriptions.
+345
website/content/docs/language-guide/07-xrpc.md
··· 1 + +++ 2 + title = "XRPC" 3 + weight = 7 4 + +++ 5 + 6 + So far we've defined data structures with records. Now let's define operations using XRPC (Cross-organizational RPC). 7 + 8 + ## XRPC Structure 9 + 10 + All XRPC definitions follow this pattern: 11 + 12 + ``` 13 + <keyword> <name>(<input>):<output> 14 + ``` 15 + 16 + Where: 17 + - **keyword** - `query`, `procedure`, or `subscription` 18 + - **name** - The operation name 19 + - **input** - Parameters in parentheses 20 + - **output** - Return type after `:` 21 + 22 + ## Queries 23 + 24 + Queries are **read-only operations** that use HTTP GET. They retrieve data without modifying state. 25 + 26 + **Basic query:** 27 + ```mlf 28 + /// Get a user profile 29 + query getProfile( 30 + actor: Did 31 + ):{ 32 + did: Did, 33 + handle: Handle, 34 + displayName?: string, 35 + }; 36 + ``` 37 + 38 + **Query with optional parameters:** 39 + ```mlf 40 + /// Search for posts 41 + query searchPosts( 42 + q: string, 43 + limit?: integer constrained { 44 + minimum: 1, 45 + maximum: 100, 46 + default: 25, 47 + }, 48 + cursor?: string 49 + ):{ 50 + posts: post[], 51 + cursor?: string, 52 + }; 53 + ``` 54 + 55 + **Query returning a record:** 56 + ```mlf 57 + record profile { 58 + did: Did, 59 + handle: Handle, 60 + displayName?: string, 61 + } 62 + 63 + query getProfile( 64 + actor: Did 65 + ):profile; 66 + ``` 67 + 68 + ## Procedures 69 + 70 + Procedures are **write operations** that use HTTP POST. They create, update, or delete data. 71 + 72 + **Basic procedure:** 73 + ```mlf 74 + /// Create a new post 75 + procedure createPost( 76 + text: string constrained { 77 + minGraphemes: 1, 78 + maxGraphemes: 500, 79 + } 80 + ):{ 81 + uri: AtUri, 82 + cid: Cid, 83 + }; 84 + ``` 85 + 86 + **Procedure with multiple parameters:** 87 + ```mlf 88 + /// Update a user profile 89 + procedure updateProfile( 90 + displayName?: string, 91 + bio?: string, 92 + avatar?: blob 93 + ):{ 94 + success: boolean, 95 + }; 96 + ``` 97 + 98 + **Delete procedure:** 99 + ```mlf 100 + /// Delete a post 101 + procedure deletePost( 102 + uri: AtUri 103 + ):{ 104 + success: boolean, 105 + }; 106 + ``` 107 + 108 + ## Subscriptions 109 + 110 + Subscriptions are **real-time event streams** over WebSocket. They push updates to clients as events occur. 111 + 112 + **Basic subscription:** 113 + ```mlf 114 + /// Subscribe to new posts 115 + subscription subscribePosts():post; 116 + ``` 117 + 118 + **Subscription with parameters:** 119 + ```mlf 120 + /// Subscribe to posts from specific users 121 + subscription subscribePosts( 122 + authors?: Did[] 123 + ):post; 124 + ``` 125 + 126 + **Subscription with multiple message types:** 127 + ```mlf 128 + def type postCreated = { 129 + post: post, 130 + }; 131 + 132 + def type postDeleted = { 133 + uri: AtUri, 134 + }; 135 + 136 + def type postUpdated = { 137 + post: post, 138 + }; 139 + 140 + /// Subscribe to post events 141 + subscription subscribePostEvents():postCreated | postDeleted | postUpdated; 142 + ``` 143 + 144 + **Resumable subscription with cursor:** 145 + ```mlf 146 + /// Subscribe to repository events 147 + subscription subscribeRepos( 148 + cursor?: integer 149 + ):commit | identity | tombstone; 150 + ``` 151 + 152 + The `cursor` parameter lets clients resume from where they left off. 153 + 154 + ## Differences Between Operations 155 + 156 + | Feature | Query | Procedure | Subscription | 157 + |---------|-------|-----------|--------------| 158 + | HTTP Method | GET | POST | WebSocket | 159 + | Purpose | Read data | Write data | Real-time events | 160 + | Idempotent | Yes | Usually no | N/A | 161 + | Errors | `| error` | `| error` | Error frames | 162 + 163 + ## Error Handling 164 + 165 + Queries and procedures can specify errors using `| error`: 166 + 167 + ```mlf 168 + query getPost( 169 + uri: AtUri 170 + ):post | error { 171 + /// Post not found 172 + NotFound, 173 + /// No permission to view 174 + Forbidden, 175 + }; 176 + ``` 177 + 178 + **Procedure with errors:** 179 + ```mlf 180 + procedure createPost( 181 + text: string 182 + ):{ 183 + uri: AtUri, 184 + cid: Cid, 185 + } | error { 186 + /// Text exceeds maximum length 187 + TextTooLong, 188 + /// User is rate limited 189 + RateLimited, 190 + /// User not authenticated 191 + Unauthorized, 192 + }; 193 + ``` 194 + 195 + Each error should have a doc comment explaining when it occurs. 196 + 197 + **Note:** Subscriptions don't use `| error` - errors are sent as special message frames over the WebSocket connection. 198 + 199 + ## Parameters 200 + 201 + Parameters can have constraints just like record fields: 202 + 203 + ```mlf 204 + query searchPosts( 205 + /// Search query (1-200 characters) 206 + q: string constrained { 207 + minLength: 1, 208 + maxLength: 200, 209 + }, 210 + 211 + /// Results per page 212 + limit?: integer constrained { 213 + minimum: 1, 214 + maximum: 100, 215 + default: 25, 216 + } 217 + ):{ 218 + posts: post[], 219 + } 220 + ``` 221 + 222 + ## Return Types 223 + 224 + Operations can return: 225 + 226 + **Inline objects:** 227 + ```mlf 228 + query getStats():{ 229 + posts: integer, 230 + followers: integer, 231 + }; 232 + ``` 233 + 234 + **Named records:** 235 + ```mlf 236 + query getProfile(did: Did):profile; 237 + ``` 238 + 239 + **Unions:** 240 + ```mlf 241 + query getPost(uri: AtUri):post | deleted; 242 + ``` 243 + 244 + ## Complete Example 245 + 246 + Here's a complete API for a forum: 247 + 248 + **File: `com/example/forum/post.mlf`** 249 + ```mlf 250 + /// A forum post 251 + record post { 252 + /// Post title 253 + title: string constrained { 254 + minGraphemes: 1, 255 + maxGraphemes: 200, 256 + }, 257 + 258 + /// Post content 259 + body: string constrained { 260 + maxGraphemes: 50000, 261 + }, 262 + 263 + /// Post author 264 + author: Did, 265 + 266 + /// When published 267 + publishedAt: Datetime, 268 + } 269 + 270 + /// Get a single post 271 + query getPost( 272 + /// Post URI 273 + uri: AtUri 274 + ):post | error { 275 + /// Post not found 276 + NotFound, 277 + /// Post is private 278 + Forbidden, 279 + } 280 + 281 + /// List posts by author 282 + query listPosts( 283 + /// Author DID 284 + author: Did, 285 + 286 + /// Results per page 287 + limit?: integer constrained { 288 + minimum: 1, 289 + maximum: 100, 290 + default: 25, 291 + }, 292 + 293 + /// Pagination cursor 294 + cursor?: string 295 + ):{ 296 + posts: post[], 297 + cursor?: string, 298 + }; 299 + 300 + /// Create a new post 301 + procedure createPost( 302 + /// Post title 303 + title: string constrained { 304 + minGraphemes: 1, 305 + maxGraphemes: 200, 306 + }, 307 + 308 + /// Post body 309 + body: string constrained { 310 + maxGraphemes: 50000, 311 + } 312 + ):{ 313 + uri: AtUri, 314 + cid: Cid, 315 + post: post, 316 + } | error { 317 + /// User not authenticated 318 + Unauthorized, 319 + /// Title or body invalid 320 + InvalidInput, 321 + }; 322 + 323 + /// Delete a post 324 + procedure deletePost( 325 + /// Post URI to delete 326 + uri: AtUri 327 + ):{ 328 + success: boolean, 329 + } | error { 330 + /// Post not found 331 + NotFound, 332 + /// User doesn't own this post 333 + Forbidden, 334 + }; 335 + 336 + /// Subscribe to new posts 337 + subscription subscribePosts( 338 + /// Optional author filter 339 + author?: Did 340 + ):post; 341 + ``` 342 + 343 + ## What's Next? 344 + 345 + Now that you can define complete APIs, let's learn how to import definitions from other files.
+226
website/content/docs/language-guide/08-imports.md
··· 1 + +++ 2 + title = "Imports" 3 + weight = 8 4 + +++ 5 + 6 + As your schemas grow, you'll want to split them across multiple files and reuse definitions. The `use` statement lets you import definitions from other files. 7 + 8 + ## Basic Import 9 + 10 + Import a definition from another file: 11 + 12 + ```mlf 13 + use com.example.forum.profile; 14 + 15 + record post { 16 + author: profile, 17 + } 18 + ``` 19 + 20 + This imports the `profile` record from `com/example/forum/profile.mlf`. 21 + 22 + ## How Imports Work 23 + 24 + The namespace matches the file path: 25 + 26 + | File Path | Namespace | Import Statement | 27 + |-----------|-----------|------------------| 28 + | `com/example/forum/user.mlf` | `com.example.forum.user` | `use com.example.forum.user;` | 29 + | `com/example/forum/post.mlf` | `com.example.forum.post` | `use com.example.forum.post;` | 30 + 31 + ## What Can Be Imported 32 + 33 + You can import **data-shaped definitions**: 34 + 35 + - ✅ **Records** - `record user { ... }` 36 + - ✅ **Def types** - `def type author = { ... }` 37 + - ✅ **Tokens** - `token public;` 38 + 39 + You **cannot** import XRPC operations: 40 + 41 + - ❌ **Queries** - `query getUser(...)` 42 + - ❌ **Procedures** - `procedure createUser(...)` 43 + - ❌ **Subscriptions** - `subscription subscribeUsers(...)` 44 + 45 + **Note:** You also cannot import inline types - they're file-local only. 46 + 47 + ## Using Imported Definitions 48 + 49 + Once imported, reference the definition by its name: 50 + 51 + ```mlf 52 + use com.example.forum.author; 53 + use com.example.forum.postRef; 54 + 55 + record comment { 56 + text: string, 57 + author: author, 58 + replyTo: postRef, 59 + } 60 + ``` 61 + 62 + ## Multiple Imports 63 + 64 + Import multiple definitions with separate `use` statements: 65 + 66 + ```mlf 67 + use com.example.forum.author; 68 + use com.example.forum.timestamp; 69 + use com.example.forum.location; 70 + 71 + record post { 72 + author: author, 73 + createdAt: timestamp, 74 + location?: location, 75 + } 76 + ``` 77 + 78 + ## Organizing Files 79 + 80 + Common organization patterns: 81 + 82 + **By feature:** 83 + ``` 84 + com/ 85 + example/ 86 + forum/ 87 + user.mlf 88 + post.mlf 89 + comment.mlf 90 + ``` 91 + 92 + **Shared types:** 93 + ``` 94 + com/ 95 + example/ 96 + forum/ 97 + author.mlf 98 + postRef.mlf 99 + post.mlf 100 + comment.mlf 101 + ``` 102 + 103 + ## Avoiding Circular Dependencies 104 + 105 + Don't create circular imports: 106 + 107 + ```mlf 108 + // user.mlf 109 + use com.example.forum.post; 110 + 111 + record user { 112 + recentPost?: post, // References post 113 + } 114 + ``` 115 + 116 + ```mlf 117 + // post.mlf 118 + use com.example.forum.user; 119 + 120 + record post { 121 + author: user, // References user - CIRCULAR! 122 + } 123 + ``` 124 + 125 + **Solution:** Use a shared reference type: 126 + 127 + ```mlf 128 + // userRef.mlf 129 + def type userRef = { 130 + did: Did, 131 + handle: Handle, 132 + }; 133 + ``` 134 + 135 + ```mlf 136 + // post.mlf 137 + use com.example.forum.userRef; 138 + 139 + record post { 140 + author: userRef, // No circular dependency 141 + } 142 + ``` 143 + 144 + ## Complete Example 145 + 146 + Here's a well-organized multi-file lexicon: 147 + 148 + **File: `com/example/forum/author.mlf`** 149 + ```mlf 150 + /// Basic author information 151 + def type author = { 152 + did: Did, 153 + handle: Handle, 154 + displayName?: string, 155 + }; 156 + ``` 157 + 158 + **File: `com/example/forum/postRef.mlf`** 159 + ```mlf 160 + /// Reference to a post 161 + def type postRef = { 162 + uri: AtUri, 163 + cid: Cid, 164 + }; 165 + ``` 166 + 167 + **File: `com/example/forum/post.mlf`** 168 + ```mlf 169 + use com.example.forum.author; 170 + use com.example.forum.postRef; 171 + 172 + /// A forum post 173 + record post { 174 + /// Post text 175 + text: string constrained { 176 + minGraphemes: 1, 177 + maxGraphemes: 500, 178 + }, 179 + 180 + /// Post author 181 + author: author, 182 + 183 + /// Optional reply reference 184 + replyTo?: postRef, 185 + 186 + /// When created 187 + createdAt: Datetime, 188 + } 189 + 190 + /// Get a post by URI 191 + query getPost( 192 + uri: AtUri 193 + ):post | error { 194 + NotFound, 195 + }; 196 + ``` 197 + 198 + **File: `com/example/forum/comment.mlf`** 199 + ```mlf 200 + use com.example.forum.author; 201 + use com.example.forum.postRef; 202 + 203 + /// A comment on a post 204 + record comment { 205 + /// Comment text 206 + text: string constrained { 207 + minGraphemes: 1, 208 + maxGraphemes: 500, 209 + }, 210 + 211 + /// Comment author 212 + author: author, 213 + 214 + /// Post being commented on 215 + post: postRef, 216 + 217 + /// When created 218 + createdAt: Datetime, 219 + } 220 + ``` 221 + 222 + Both `post.mlf` and `comment.mlf` import and reuse the same `author` and `postRef` types. 223 + 224 + ## What's Next? 225 + 226 + Finally, let's learn about the prelude - built-in types available in every file.
+196
website/content/docs/language-guide/09-prelude.md
··· 1 + +++ 2 + title = "Prelude" 3 + weight = 9 4 + +++ 5 + 6 + The prelude is a set of definitions automatically available in every MLF file. You don't need to import them - they're always there. 7 + 8 + ## String Format Types 9 + 10 + The prelude provides inline type aliases for common string formats: 11 + 12 + ```mlf 13 + // These are defined in the prelude - you can use them anywhere 14 + record example { 15 + id: Did, // Decentralized Identifier (did:*) 16 + uri: AtUri, // AT Protocol URI (at://...) 17 + timestamp: Datetime, // ISO 8601 datetime 18 + website: Uri, // Generic URI 19 + hash: Cid, // Content Identifier 20 + username: Handle, // Handle (domain name) 21 + lang: Language, // BCP 47 language code 22 + } 23 + ``` 24 + 25 + ## All Prelude Types 26 + 27 + Here are all the format types in the prelude: 28 + 29 + | Type | Description | Example | 30 + |------|-------------|---------| 31 + | `Did` | Decentralized Identifier | `did:plc:abc123...` | 32 + | `AtUri` | AT Protocol URI | `at://did:plc:abc/app.bsky.feed.post/123` | 33 + | `AtIdentifier` | DID or Handle | `did:plc:abc` or `alice.com` | 34 + | `Handle` | Domain name handle | `alice.com` | 35 + | `Datetime` | ISO 8601 datetime | `2024-01-15T10:30:00Z` | 36 + | `Uri` | Generic URI | `https://example.com` | 37 + | `Cid` | Content Identifier (IPFS) | `bafyrei...` | 38 + | `Nsid` | Namespaced Identifier | `com.example.post` | 39 + | `Tid` | Timestamp Identifier | `3l2p5g7...` | 40 + | `RecordKey` | Record key in a repo | `3l2p5g7...` | 41 + | `Language` | BCP 47 language tag | `en`, `en-US`, `ja` | 42 + 43 + ## How They Work 44 + 45 + These types are implemented as inline types with format constraints: 46 + 47 + ```mlf 48 + // Simplified version of what's in the prelude 49 + inline type Did = string constrained { 50 + format: "did", 51 + }; 52 + 53 + inline type Datetime = string constrained { 54 + format: "datetime", 55 + }; 56 + 57 + inline type Uri = string constrained { 58 + format: "uri", 59 + }; 60 + ``` 61 + 62 + When you use `Datetime` in your record, it expands to `string` with `format: "datetime"`. 63 + 64 + ## Future: ATProto Types 65 + 66 + In the future, the prelude will also include all `com.atproto.*` definitions: 67 + 68 + ```mlf 69 + // Eventually, these will be in the prelude 70 + record myPost { 71 + // Reference standard ATProto types without importing 72 + repo: com.atproto.sync.repo, 73 + commit: com.atproto.sync.commit, 74 + } 75 + ``` 76 + 77 + This will make it easier to reference standard ATProto types without manual imports. 78 + 79 + ## Using Prelude Types 80 + 81 + You can use prelude types anywhere: 82 + 83 + **In records:** 84 + ```mlf 85 + record post { 86 + uri: AtUri, 87 + author: Did, 88 + createdAt: Datetime, 89 + } 90 + ``` 91 + 92 + **In queries:** 93 + ```mlf 94 + query getPost( 95 + uri: AtUri 96 + ):post; 97 + ``` 98 + 99 + **In constraints:** 100 + ```mlf 101 + record event { 102 + participants: Did[] constrained { 103 + maxLength: 100, 104 + }, 105 + } 106 + ``` 107 + 108 + **In custom types:** 109 + ```mlf 110 + def type reference = { 111 + uri: AtUri, 112 + cid: Cid, 113 + }; 114 + ``` 115 + 116 + ## Complete Example 117 + 118 + Here's a complete lexicon using various prelude types: 119 + 120 + **File: `com/example/forum/profile.mlf`** 121 + ```mlf 122 + /// A user profile 123 + record profile { 124 + /// User's DID 125 + did: Did, 126 + 127 + /// User's handle 128 + handle: Handle, 129 + 130 + /// Display name 131 + displayName?: string constrained { 132 + maxGraphemes: 64, 133 + }, 134 + 135 + /// Profile description 136 + description?: string constrained { 137 + maxGraphemes: 256, 138 + }, 139 + 140 + /// Avatar image URI 141 + avatar?: Uri, 142 + 143 + /// Website link 144 + website?: Uri, 145 + 146 + /// Account creation date 147 + createdAt: Datetime, 148 + 149 + /// Preferred language 150 + language?: Language, 151 + } 152 + 153 + /// Get a profile by DID or handle 154 + query getProfile( 155 + /// User identifier (DID or handle) 156 + actor: AtIdentifier 157 + ):profile | error { 158 + /// Profile not found 159 + NotFound, 160 + }; 161 + 162 + /// Update your profile 163 + procedure updateProfile( 164 + /// New display name 165 + displayName?: string, 166 + 167 + /// New description 168 + description?: string, 169 + 170 + /// New avatar URI 171 + avatar?: Uri, 172 + 173 + /// New website URI 174 + website?: Uri 175 + ):{ 176 + uri: AtUri, 177 + cid: Cid, 178 + profile: profile, 179 + } | error { 180 + /// User not authenticated 181 + Unauthorized, 182 + }; 183 + ``` 184 + 185 + All the format types (`Did`, `Handle`, `Uri`, `Datetime`, `Language`, `AtIdentifier`, `AtUri`, `Cid`) are from the prelude - no imports needed. 186 + 187 + ## Summary 188 + 189 + The prelude provides: 190 + - ✅ String format types for common patterns 191 + - ✅ No imports needed - always available 192 + - ✅ Eventually will include all `com.atproto.*` types 193 + 194 + You've now learned all the core features of MLF! You can define records, add constraints, create custom types, use unions and tokens, define XRPC operations, import from other files, and use prelude types. 195 + 196 + Check out the [Playground](/playground/) to experiment with MLF, or read the [CLI documentation](/docs/cli/) to learn how to compile your lexicons.
+11
website/content/docs/language-guide/_index.md
··· 1 + +++ 2 + title = "Language Guide" 3 + description = "A comprehensive guide to the MLF language" 4 + weight = 2 5 + sort_by = "weight" 6 + template = "section.html" 7 + +++ 8 + 9 + This guide will teach you MLF from the ground up, starting with simple examples and gradually introducing more advanced features. 10 + 11 + Each section builds on the previous ones, so we recommend reading them in order if you're new to MLF.
-593
website/content/docs/syntax.md
··· 1 - +++ 2 - title = "Language Syntax" 3 - description = "Complete reference for MLF syntax and features" 4 - weight = 2 5 - +++ 6 - 7 - ## File Structure 8 - 9 - ### File Extension 10 - - `.mlf` - MLF source files 11 - 12 - ### Shebang (Optional) 13 - ```mlf 14 - #!/usr/bin/env mlf 15 - ``` 16 - 17 - ### File Naming Convention 18 - The file path determines the lexicon NSID. Files should follow the lexicon NSID structure: 19 - - `com.example.forum.thread.mlf` → Lexicon NSID: `com.example.forum.thread` 20 - - `com.example.user.profile.mlf` → Lexicon NSID: `com.example.user.profile` 21 - 22 - The lexicon NSID is derived solely from the filename, not from any internal declarations. 23 - 24 - ## Basic Structure 25 - 26 - Every MLF file can contain: 27 - 28 - - Use statements (imports) 29 - - Type definitions (record, inline type, def type, token, query, procedure, subscription) 30 - 31 - ## Syntax Rules 32 - 33 - ### Semicolons 34 - 35 - - **Records** do NOT have semicolons after the closing brace `}` 36 - - All other definitions require semicolons: 37 - - `use` statements end with `;` 38 - - `token` definitions end with `;` 39 - - `inline type` definitions end with `;` 40 - - `def type` definitions end with `;` 41 - - `query` definitions end with `;` 42 - - `procedure` definitions end with `;` 43 - - `subscription` definitions end with `;` 44 - 45 - ### Commas 46 - 47 - Commas are **required** between items, with **trailing commas allowed**: 48 - 49 - **Record fields:** 50 - ```mlf 51 - record example { 52 - field1: string, 53 - field2: integer, // trailing comma allowed 54 - } 55 - ``` 56 - 57 - **Constraints:** 58 - ```mlf 59 - title: string constrained { 60 - maxLength: 200, 61 - minLength: 1, // trailing comma allowed 62 - } 63 - ``` 64 - 65 - **Error definitions:** 66 - ```mlf 67 - query getThread(): thread | error { 68 - NotFound, 69 - BadRequest, // trailing comma allowed 70 - } 71 - ``` 72 - 73 - ## Primitive Types 74 - 75 - - `null` - Null value 76 - - `boolean` - True/false 77 - - `integer` - 64-bit integer 78 - - `number` - Double-precision float 79 - - `string` - UTF-8 string 80 - - `bytes` - Byte array 81 - - `blob` - Binary large object with metadata 82 - - `unknown` - Any value (for forward compatibility) 83 - 84 - ## Special String Formats 85 - 86 - These are defined in the prelude and available everywhere: 87 - 88 - - `Did` - Decentralized Identifier (did:*) 89 - - `AtUri` - AT-URI (at://...) 90 - - `AtIdentifier` - Either a DID or Handle 91 - - `Handle` - Handle identifier (domain name) 92 - - `Datetime` - ISO 8601 datetime 93 - - `Uri` - Generic URI 94 - - `Cid` - Content Identifier 95 - - `Nsid` - Namespaced Identifier 96 - - `Tid` - Timestamp Identifier 97 - - `RecordKey` - Record key 98 - - `Language` - BCP 47 language code 99 - 100 - ## Records 101 - 102 - Records define structured data types stored in repositories: 103 - 104 - ```mlf 105 - /// A forum thread 106 - record thread { 107 - /// Thread title 108 - title: string constrained { 109 - maxLength: 200 110 - minLength: 1 111 - } 112 - /// Thread body 113 - body?: string // Optional field 114 - /// Thread creation timestamp 115 - createdAt: Datetime 116 - } 117 - ``` 118 - 119 - ## Type Definitions 120 - 121 - MLF supports two kinds of type definitions: 122 - 123 - ### Inline Types 124 - 125 - Expanded at the point of use, never appear in generated lexicon defs: 126 - 127 - ```mlf 128 - inline type AtIdentifier = string constrained { 129 - format "at-identifier" 130 - }; 131 - ``` 132 - 133 - ### Def Types 134 - 135 - Become named definitions in the lexicon's defs block: 136 - 137 - ```mlf 138 - def type replyRef = { 139 - root: AtUri 140 - parent: AtUri 141 - }; 142 - 143 - record thread { 144 - reply?: replyRef 145 - } 146 - ``` 147 - 148 - Use `inline type` for type aliases that should be expanded inline (like primitive type wrappers). Use `def type` for types that should be referenced by name in the generated lexicon. 149 - 150 - ## Tokens 151 - 152 - Tokens are named constants used in enums and unions: 153 - 154 - ```mlf 155 - /// Open state 156 - token open; 157 - 158 - /// Closed state 159 - token closed; 160 - 161 - record issue { 162 - state: string constrained { 163 - knownValues: [open, closed] 164 - default: "open" 165 - } 166 - } 167 - ``` 168 - 169 - Tokens must have doc comments describing their purpose. 170 - 171 - ## Constrained Types 172 - 173 - Add validation constraints to types: 174 - 175 - ```mlf 176 - title: string constrained { 177 - maxLength: 200 178 - minLength: 1 179 - } 180 - 181 - age: integer constrained { 182 - minimum: 0 183 - maximum: 150 184 - } 185 - 186 - status: string constrained { 187 - enum: ["draft", "published", "archived"] 188 - } 189 - ``` 190 - 191 - ### String Constraints 192 - 193 - - `maxLength` / `minLength` - Length in bytes 194 - - `maxGraphemes` / `minGraphemes` - Length in grapheme clusters 195 - - `format` - Format validation (datetime, uri, did, handle, etc.) 196 - - `enum` - Allowed values (closed set) - accepts string literals or token references 197 - - `knownValues` - Known values (extensible set) - accepts string literals or token references 198 - - `default` - Default value 199 - 200 - **enum, knownValues, and default** can use either literals or references: 201 - ```mlf 202 - // String literals 203 - status: string constrained { 204 - knownValues: ["open", "closed", "pending"] 205 - default: "open" 206 - } 207 - 208 - // References to named items (tokens, aliases, records, etc.) 209 - token open; 210 - token closed; 211 - 212 - status: string constrained { 213 - knownValues: [open, closed] // References tokens defined above 214 - default: open // References the token 215 - } 216 - ``` 217 - 218 - ### Integer Constraints 219 - 220 - - `minimum` / `maximum` - Min/max values 221 - - `enum` - Allowed values 222 - - `default` - Default value 223 - 224 - ### Array Constraints 225 - 226 - ```mlf 227 - tags: string[] constrained { 228 - minLength: 1 229 - maxLength: 10 230 - } 231 - ``` 232 - 233 - ### Blob Constraints 234 - 235 - ```mlf 236 - avatar: blob constrained { 237 - accept: ["image/png", "image/jpeg"] 238 - maxSize: 1000000 // bytes 239 - } 240 - ``` 241 - 242 - ### Boolean Constraints 243 - 244 - ```mlf 245 - field: boolean constrained { 246 - default: false 247 - } 248 - ``` 249 - 250 - ### Constraint Refinement 251 - 252 - Constraints can only make types **more restrictive**, never less restrictive: 253 - 254 - ```mlf 255 - def type shortString = string constrained { 256 - maxLength: 100 257 - }; 258 - 259 - record post { 260 - // Valid: 50 is more restrictive than 100 261 - title: shortString constrained { 262 - maxLength: 50 263 - } 264 - } 265 - ``` 266 - 267 - **Refinement rules:** 268 - - Numeric bounds: `minimum` can only increase, `maximum` can only decrease 269 - - Length bounds: `minLength`/`minGraphemes` can only increase, `maxLength`/`maxGraphemes` can only decrease 270 - - Enums: Can only restrict to a subset 271 - - Format: Cannot change once specified 272 - 273 - ## Arrays 274 - 275 - ```mlf 276 - tags: string[] 277 - 278 - items: string[] constrained { 279 - minLength: 1, 280 - maxLength: 10, 281 - } 282 - ``` 283 - 284 - ## Unions 285 - 286 - Use the pipe operator `|`: 287 - 288 - ```mlf 289 - // Closed union (only these types) 290 - content: text | image | video 291 - 292 - // Union of tokens 293 - state: open | closed | pending 294 - ``` 295 - 296 - Open unions (allowing unknown types) use `_`: 297 - 298 - ```mlf 299 - // Open union (can include unknown types) 300 - content: text | image | _ 301 - ``` 302 - 303 - ## Objects 304 - 305 - Inline object types: 306 - 307 - ```mlf 308 - metadata: { 309 - version: integer 310 - timestamp: Datetime 311 - } 312 - ``` 313 - 314 - ## Queries 315 - 316 - Queries are read-only HTTP endpoints (GET): 317 - 318 - ```mlf 319 - /// Get a user profile 320 - query getProfile( 321 - /// The actor's DID or handle 322 - actor: AtIdentifier 323 - ): profile; 324 - ``` 325 - 326 - With errors: 327 - 328 - ```mlf 329 - query getThread( 330 - uri: AtUri 331 - ): thread | error { 332 - /// Thread not found 333 - NotFound 334 - /// Invalid request 335 - BadRequest 336 - }; 337 - ``` 338 - 339 - ## Procedures 340 - 341 - Procedures are write operations (POST): 342 - 343 - ```mlf 344 - /// Create a new thread 345 - procedure createThread( 346 - title: string 347 - body: string 348 - ): { 349 - uri: AtUri 350 - cid: Cid 351 - } | error { 352 - /// Title too long 353 - TitleTooLong 354 - }; 355 - ``` 356 - 357 - ## Subscriptions 358 - 359 - Subscriptions are WebSocket-based event streams: 360 - 361 - ```mlf 362 - /// Subscribe to repository events 363 - subscription subscribeRepos( 364 - /// Optional cursor for resuming 365 - cursor?: integer 366 - ): commit | identity | handle; 367 - ``` 368 - 369 - Message types must be defined as def types or records: 370 - 371 - ```mlf 372 - /// Commit message 373 - def type commit = { 374 - seq: integer 375 - repo: Did 376 - commit: Cid 377 - time: Datetime 378 - }; 379 - 380 - /// Identity message 381 - def type identity = { 382 - did: Did 383 - handle: Handle 384 - }; 385 - ``` 386 - 387 - ## Comments 388 - 389 - ### Documentation Comments 390 - 391 - Use `///` for documentation (appears in generated docs/code): 392 - 393 - ```mlf 394 - /// A forum thread 395 - record thread { 396 - /// Thread title 397 - title: string 398 - } 399 - ``` 400 - 401 - ### Regular Comments 402 - 403 - Use `//` for comments that won't appear in output: 404 - 405 - ```mlf 406 - // This is a regular comment 407 - record example { 408 - field: string // inline comment 409 - } 410 - ``` 411 - 412 - ## Annotations 413 - 414 - Annotations use `@` and provide metadata for external tooling: 415 - 416 - ### Simple Annotation 417 - ```mlf 418 - @deprecated 419 - record oldRecord { 420 - field: string 421 - } 422 - ``` 423 - 424 - ### Positional Arguments 425 - ```mlf 426 - @since(1, 2, 0) 427 - @doc("https://example.com/docs") 428 - record example { 429 - field: string 430 - } 431 - ``` 432 - 433 - ### Named Arguments 434 - ```mlf 435 - @validate(min: 0, max: 100, strict: true) 436 - @table(name: "threads", indexes: "did,createdAt") 437 - record thread { 438 - @indexed 439 - did: Did 440 - 441 - @sensitive(pii: true) 442 - title: string 443 - } 444 - ``` 445 - 446 - Annotations can be placed on records, inline types, def types, tokens, queries, procedures, subscriptions, and fields. 447 - 448 - ## Imports 449 - 450 - Import definitions from other lexicons: 451 - 452 - ```mlf 453 - // Single import 454 - use com.example.user.profile; 455 - 456 - // Multiple imports 457 - use com.example.forum.{thread, reply}; 458 - 459 - // Alias import 460 - use com.example.user as User; 461 - 462 - // Wildcard import 463 - use com.example.forum.*; 464 - 465 - // Import with alias 466 - use com.example.forum.{thread as ForumThread}; 467 - ``` 468 - 469 - After importing, use the short name: 470 - 471 - ```mlf 472 - use com.example.user.profile; 473 - 474 - record thread { 475 - author: profile // Instead of com.example.user.profile 476 - } 477 - ``` 478 - 479 - ## References 480 - 481 - Reference local or external definitions: 482 - 483 - ```mlf 484 - // Local reference (same file) 485 - record thread { 486 - author: author // References 'def type author' in same file 487 - } 488 - 489 - // Cross-file reference 490 - record thread { 491 - profile: com.example.user.profile // References com/example/user/profile.mlf 492 - } 493 - ``` 494 - 495 - All references use dotted notation. 496 - 497 - ## Optional Fields 498 - 499 - Use `?` to mark fields as optional: 500 - 501 - ```mlf 502 - record thread { 503 - title: string // Required 504 - body?: string // Optional 505 - tags?: string[] // Optional array 506 - } 507 - ``` 508 - 509 - ## Raw Identifiers 510 - 511 - Use backticks to escape reserved keywords when you need to use them as identifiers: 512 - 513 - ```mlf 514 - def type `record` = { 515 - `record`: com.atproto.repo.strongRef 516 - `error`: string 517 - }; 518 - ``` 519 - 520 - This is useful when working with existing schemas that use MLF keywords as field or type names. 521 - 522 - ## Format Strings 523 - 524 - Available format strings for constrained strings: 525 - 526 - - `datetime` - ISO 8601 datetime 527 - - `uri` - URI (RFC 3986) 528 - - `at-uri` - AT-URI (ATProto) 529 - - `did` - Decentralized Identifier 530 - - `handle` - ATProto handle 531 - - `nsid` - Namespaced Identifier 532 - - `cid` - Content Identifier 533 - - `at-identifier` - DID or handle 534 - - `language` - BCP 47 language tag 535 - - `tid` - Timestamp ID 536 - - `record-key` - Record key 537 - 538 - ## Complete Example 539 - 540 - ```mlf 541 - #!/usr/bin/env mlf 542 - 543 - use com.example.user.profile; 544 - 545 - /// Open state 546 - token open; 547 - 548 - /// Closed state 549 - token closed; 550 - 551 - /// A forum thread 552 - record thread { 553 - /// Thread title 554 - title: string constrained { 555 - minGraphemes: 1 556 - maxGraphemes: 200 557 - } 558 - /// Thread body (markdown) 559 - body?: string constrained { 560 - maxGraphemes: 10000 561 - } 562 - /// Thread state 563 - state: string constrained { 564 - knownValues: [open, closed] 565 - default: "open" 566 - } 567 - /// Author profile 568 - author: profile 569 - /// Creation timestamp 570 - createdAt: Datetime 571 - } 572 - 573 - /// Get a thread by URI 574 - query getThread( 575 - /// Thread AT-URI 576 - uri: AtUri 577 - ): thread | error { 578 - /// Thread not found 579 - NotFound 580 - }; 581 - 582 - /// Create a new thread 583 - procedure createThread( 584 - title: string 585 - body?: string 586 - ): { 587 - uri: AtUri 588 - cid: Cid 589 - } | error { 590 - /// Title too long 591 - TitleTooLong 592 - }; 593 - ```
+26
website/sass/style.scss
··· 823 823 font-weight: 500; 824 824 } 825 825 826 + .doc-nav .nav-section { 827 + margin-top: 1rem; 828 + } 829 + 830 + .doc-nav .nav-section-title { 831 + display: block; 832 + padding: 0.5rem 0.75rem; 833 + font-weight: 600; 834 + color: var(--text); 835 + margin-bottom: 0.25rem; 836 + } 837 + 838 + .doc-nav .nav-section ul { 839 + margin-top: 0.25rem; 840 + padding-left: 1rem; 841 + } 842 + 843 + .doc-nav .nav-section ul li { 844 + margin-bottom: 0.25rem; 845 + } 846 + 847 + .doc-nav .nav-section ul a { 848 + font-size: 0.875rem; 849 + padding: 0.375rem 0.75rem; 850 + } 851 + 826 852 .doc-main { 827 853 min-width: 0; 828 854 }
+15
website/templates/page.html
··· 19 19 </a> 20 20 </li> 21 21 {% endfor %} 22 + {% for subsection in docs_section.subsections %} 23 + {% set sub = get_section(path=subsection) %} 24 + <li class="nav-section"> 25 + <span class="nav-section-title">{{ sub.title }}</span> 26 + <ul> 27 + {% for p in sub.pages %} 28 + <li> 29 + <a href="{{ p.permalink }}" {% if p.permalink == page.permalink %}class="active"{% endif %}> 30 + {{ p.title }} 31 + </a> 32 + </li> 33 + {% endfor %} 34 + </ul> 35 + </li> 36 + {% endfor %} 22 37 </ul> 23 38 </nav> 24 39 </aside>