A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

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

Fix docs and namespaces

+235 -58
+36 -12
README.md
··· 29 29 ### Install 30 30 31 31 ```bash 32 - cargo install --path mlf-cli 32 + # Install with all code generators (default: TypeScript, Go, Rust) 33 + cargo install --path mlf-cli --all-features 34 + 35 + # Install with only specific generators 36 + cargo install --path mlf-cli --no-default-features --features typescript,go 37 + 38 + # Install with JSON generation only 39 + cargo install --path mlf-cli --no-default-features 33 40 ``` 34 41 35 - ### Generate JSON lexicons from MLF 42 + ### Generate code from MLF 36 43 37 44 ```bash 45 + # Generate TypeScript types 46 + mlf generate code -g typescript -i examples/**/*.mlf -o output/ 47 + 48 + # Generate Go structs 49 + mlf generate code -g go -i examples/**/*.mlf -o output/ 50 + 51 + # Generate Rust structs with serde 52 + mlf generate code -g rust -i examples/**/*.mlf -o output/ 53 + 54 + # Generate JSON lexicons (always available) 55 + mlf generate code -g json -i examples/**/*.mlf -o output/ 56 + # Or use the legacy command: 38 57 mlf generate lexicon -i examples/**/*.mlf -o output/ 39 58 ``` 40 59 ··· 44 63 mlf check examples/app.bsky.feed.post.mlf 45 64 ``` 46 65 47 - ### Validate generated lexicons 66 + ### Validate JSON records 48 67 49 68 ```bash 50 - mlf validate output/app.bsky.feed.post.json 69 + mlf validate examples/app.bsky.feed.post.mlf record.json 51 70 ``` 52 71 53 72 ## Project layout 54 73 55 74 ``` 56 75 mlf/ 57 - ├── mlf-cli/ # Command-line app 58 - ├── mlf-lang/ # Parser and lexer (no_std compatible) 59 - ├── mlf-codegen/ # JSON lexicon code generation 60 - ├── mlf-validation/ # Lexicon validation 61 - ├── mlf-diagnostics/ # Fancy error reporting 62 - ├── mlf-wasm/ # WASM bindings for browser use 63 - ├── tree-sitter-mlf/ # Tree-sitter grammar for syntax highlighting 64 - └── website/ # Docs and playground 76 + ├── mlf-cli/ # Command-line app 77 + ├── mlf-lang/ # Parser and lexer (no_std compatible) 78 + ├── mlf-codegen/ # Core code generation with plugin system 79 + ├── codegen-plugins/ # Language-specific code generators 80 + │ ├── mlf-codegen-typescript/ # TypeScript generator 81 + │ ├── mlf-codegen-go/ # Go generator 82 + │ └── mlf-codegen-rust/ # Rust generator 83 + ├── mlf-validation/ # Lexicon validation 84 + ├── mlf-diagnostics/ # Fancy error reporting 85 + ├── mlf-wasm/ # WASM bindings for browser use 86 + ├── tree-sitter-mlf/ # Tree-sitter grammar for syntax highlighting 87 + └── website/ # Docs and playground 88 + └── mlf-playground-wasm/ # Playground WASM with all generators 65 89 ``` 66 90 67 91 ## Documentation
-12
codegen-plugins/mlf-codegen-go/src/lib.rs
··· 7 7 impl GoGenerator { 8 8 pub const NAME: &'static str = "go"; 9 9 10 - fn to_snake_case(&self, s: &str) -> String { 11 - // Simple camelCase to snake_case conversion 12 - let mut result = String::new(); 13 - for (i, ch) in s.chars().enumerate() { 14 - if ch.is_uppercase() && i > 0 { 15 - result.push('_'); 16 - } 17 - result.push(ch.to_lowercase().next().unwrap()); 18 - } 19 - result 20 - } 21 - 22 10 fn generate_type(&self, ty: &Type, optional: bool, ctx: &GeneratorContext) -> Result<String, String> { 23 11 let base_type = match ty { 24 12 Type::Primitive { kind, .. } => match kind {
+1 -1
codegen-plugins/mlf-codegen-rust/src/lib.rs
··· 67 67 // Otherwise use serde_json::Value for flexibility 68 68 "serde_json::Value".to_string() 69 69 } 70 - Type::Object { fields, .. } => { 70 + Type::Object { .. } => { 71 71 // Inline struct types aren't idiomatic in Rust 72 72 // We'd need to generate a named type 73 73 // For now, use serde_json::Value
+30 -7
mlf-cli/src/generate/code.rs
··· 1 1 use miette::Diagnostic; 2 - use mlf_codegen::plugin::{CodeGenerator, GeneratorContext}; 2 + use mlf_codegen::plugin::GeneratorContext; 3 3 use std::path::{Path, PathBuf}; 4 4 use thiserror::Error; 5 5 ··· 46 46 47 47 #[error("Code generation failed: {0}")] 48 48 #[diagnostic(code(mlf::generate::generation_failed))] 49 + #[allow(dead_code)] 49 50 GenerationFailed(String), 50 51 } 51 52 ··· 216 217 } 217 218 218 219 fn extract_namespace(file_path: &Path) -> String { 219 - // Namespace is derived solely from the filename 220 - file_path 221 - .file_stem() 222 - .and_then(|s| s.to_str()) 223 - .unwrap_or("unknown") 224 - .to_string() 220 + // Extract namespace from path components 221 + // e.g., com/atproto/admin/defs.mlf -> com.atproto.admin.defs 222 + 223 + let mut components = Vec::new(); 224 + 225 + for component in file_path.components() { 226 + match component { 227 + std::path::Component::Normal(os_str) => { 228 + if let Some(s) = os_str.to_str() { 229 + components.push(s); 230 + } 231 + } 232 + _ => continue, // Skip ., .., /, etc. 233 + } 234 + } 235 + 236 + // Remove the .mlf extension from the last component if present 237 + if let Some(last) = components.last_mut() { 238 + if let Some(stem) = last.strip_suffix(".mlf") { 239 + *last = stem; 240 + } 241 + } 242 + 243 + if components.is_empty() { 244 + return "unknown".to_string(); 245 + } 246 + 247 + components.join(".") 225 248 }
+29 -6
mlf-cli/src/generate/lexicon.rs
··· 6 6 pub enum GenerateError { 7 7 #[error("Failed to read file: {path}")] 8 8 #[diagnostic(code(mlf::generate::read_file))] 9 + #[allow(dead_code)] 9 10 ReadFile { 10 11 path: String, 11 12 #[source] ··· 153 154 } 154 155 155 156 fn extract_namespace(file_path: &Path) -> String { 156 - // Namespace is derived solely from the filename 157 - file_path 158 - .file_stem() 159 - .and_then(|s| s.to_str()) 160 - .unwrap_or("unknown") 161 - .to_string() 157 + // Extract namespace from path components 158 + // e.g., com/atproto/admin/defs.mlf -> com.atproto.admin.defs 159 + 160 + let mut components = Vec::new(); 161 + 162 + for component in file_path.components() { 163 + match component { 164 + std::path::Component::Normal(os_str) => { 165 + if let Some(s) = os_str.to_str() { 166 + components.push(s); 167 + } 168 + } 169 + _ => continue, // Skip ., .., /, etc. 170 + } 171 + } 172 + 173 + // Remove the .mlf extension from the last component if present 174 + if let Some(last) = components.last_mut() { 175 + if let Some(stem) = last.strip_suffix(".mlf") { 176 + *last = stem; 177 + } 178 + } 179 + 180 + if components.is_empty() { 181 + return "unknown".to_string(); 182 + } 183 + 184 + components.join(".") 162 185 }
+14 -11
mlf-wasm/src/lib.rs
··· 1 - use wasm_bindgen::prelude::*; 2 1 use serde::{Deserialize, Serialize}; 2 + use wasm_bindgen::prelude::*; 3 3 4 4 #[derive(Serialize, Deserialize)] 5 5 pub struct ParseResult { ··· 244 244 #[cfg(target_arch = "wasm32")] 245 245 web_sys::console::log_1(&format!("Found {} generators", generators.len()).into()); 246 246 247 - let generator_infos: Vec<GeneratorInfo> = generators.iter().map(|generator| { 248 - #[cfg(target_arch = "wasm32")] 249 - web_sys::console::log_1(&format!(" - {}", generator.name()).into()); 247 + let generator_infos: Vec<GeneratorInfo> = generators 248 + .iter() 249 + .map(|generator| { 250 + #[cfg(target_arch = "wasm32")] 251 + web_sys::console::log_1(&format!(" - {}", generator.name()).into()); 250 252 251 - GeneratorInfo { 252 - name: generator.name().to_string(), 253 - description: generator.description().to_string(), 254 - file_extension: generator.file_extension().to_string(), 255 - } 256 - }).collect(); 253 + GeneratorInfo { 254 + name: generator.name().to_string(), 255 + description: generator.description().to_string(), 256 + file_extension: generator.file_extension().to_string(), 257 + } 258 + }) 259 + .collect(); 257 260 258 261 let result = ListGeneratorsResult { 259 262 generators: generator_infos, ··· 266 269 #[wasm_bindgen] 267 270 pub fn generate_code(source: &str, namespace: &str, generator_name: &str) -> JsValue { 268 271 // Load standard library 269 - let mut workspace = match mlf_lang::Workspace::with_std() { 272 + let workspace = match mlf_lang::Workspace::with_std() { 270 273 Ok(ws) => ws, 271 274 Err(e) => { 272 275 let result = GenerateCodeResult {
+125 -5
website/content/docs/cli.md
··· 16 16 17 17 The binary will be at `target/release/mlf`. 18 18 19 - Optionally, install to your PATH: 19 + ### Install with cargo 20 20 21 21 ```bash 22 - # Option 1: Use cargo install 23 - cargo install --path mlf-cli 22 + # Install with all code generators (default: TypeScript, Go, Rust + JSON) 23 + cargo install --path mlf-cli --all-features 24 + 25 + # Install with only specific generators 26 + cargo install --path mlf-cli --no-default-features --features typescript 27 + cargo install --path mlf-cli --no-default-features --features typescript,go 24 28 25 - # Option 2: Manually copy the binary 26 - cp target/release/mlf /usr/local/bin/ 29 + # Install with JSON generation only (minimal) 30 + cargo install --path mlf-cli --no-default-features 27 31 ``` 32 + 33 + ### Available Features 34 + 35 + The CLI uses cargo features for optional code generators: 36 + 37 + - `typescript` - TypeScript type definitions and interfaces 38 + - `go` - Go structs with JSON tags 39 + - `rust` - Rust structs with serde derive macros 40 + - **default** = `["typescript", "go", "rust"]` - All generators enabled 41 + 42 + The JSON lexicon generator is always available (built into `mlf-codegen`). 28 43 29 44 ## Commands 30 45 ··· 112 127 # Generate from glob pattern 113 128 mlf generate lexicon -i "src/**/*.mlf" -o dist/lexicons/ 114 129 ``` 130 + 131 + --- 132 + 133 + ### `mlf generate code` 134 + 135 + Generate code in various programming languages from MLF files. 136 + 137 + ```bash 138 + mlf generate code --generator <GENERATOR> --output <OUTPUT> [OPTIONS] 139 + ``` 140 + 141 + **Options:** 142 + - `-g, --generator <GENERATOR>` - Generator to use: `json`, `typescript`, `go`, or `rust` (required) 143 + - `-i, --input <INPUT>` - Input MLF files (glob patterns supported, can be specified multiple times) 144 + - `-o, --output <OUTPUT>` - Output directory (required) 145 + - `--flat` - Use flat file structure (e.g., `com.example.thread.ts`) 146 + 147 + **Available Generators:** 148 + 149 + | Generator | Output | Description | 150 + |-----------|--------|-------------| 151 + | `json` | `.json` | AT Protocol JSON lexicons (always available) | 152 + | `typescript` | `.ts` | TypeScript interfaces with JSDoc comments | 153 + | `go` | `.go` | Go structs with JSON tags and proper capitalization | 154 + | `rust` | `.rs` | Rust structs with serde derive macros | 155 + 156 + **TypeScript Example:** 157 + 158 + ```bash 159 + mlf generate code -g typescript -i thread.mlf -o src/types/ 160 + # Creates: src/types/com/example/thread.ts 161 + ``` 162 + 163 + Generated TypeScript: 164 + ```typescript 165 + /** 166 + * Generated from com.example.thread 167 + * Do not edit manually 168 + */ 169 + 170 + export interface Thread { 171 + /** Thread title */ 172 + title: string; 173 + /** Creation timestamp */ 174 + createdAt: string; 175 + posts: Post[]; 176 + } 177 + ``` 178 + 179 + **Go Example:** 180 + 181 + ```bash 182 + mlf generate code -g go -i thread.mlf -o pkg/models/ 183 + # Creates: pkg/models/com/example/thread.go 184 + ``` 185 + 186 + Generated Go: 187 + ```go 188 + // Generated from com.example.thread 189 + // Do not edit manually 190 + 191 + package thread 192 + 193 + // Thread represents a discussion thread 194 + type Thread struct { 195 + // Thread title 196 + Title string `json:"title"` 197 + // Creation timestamp 198 + CreatedAt string `json:"createdAt"` 199 + Posts []Post `json:"posts"` 200 + } 201 + ``` 202 + 203 + **Rust Example:** 204 + 205 + ```bash 206 + mlf generate code -g rust -i thread.mlf -o src/models/ 207 + # Creates: src/models/com/example/thread.rs 208 + ``` 209 + 210 + Generated Rust: 211 + ```rust 212 + // Generated from com.example.thread 213 + // Do not edit manually 214 + 215 + use serde::{Deserialize, Serialize}; 216 + 217 + /// Thread represents a discussion thread 218 + #[derive(Debug, Clone, Serialize, Deserialize)] 219 + pub struct Thread { 220 + /// Thread title 221 + pub title: String, 222 + /// Creation timestamp 223 + #[serde(rename = "createdAt")] 224 + pub created_at: String, 225 + pub posts: Vec<Post>, 226 + } 227 + ``` 228 + 229 + **Notes:** 230 + 231 + - TypeScript: Uses interfaces for records, type aliases for defs, optional fields use `?` 232 + - Go: Uses structs with JSON tags, pointer types for optional fields, PascalCase for exports 233 + - Rust: Uses structs with serde, `Option<T>` for optional fields, snake_case with `#[serde(rename)]` 234 + - All generators handle doc comments, constraints are preserved in generated code where applicable 115 235 116 236 --- 117 237
-4
website/mlf-playground-wasm/Cargo.toml
··· 14 14 mlf-codegen-go = { path = "../../codegen-plugins/mlf-codegen-go" } 15 15 mlf-codegen-rust = { path = "../../codegen-plugins/mlf-codegen-rust" } 16 16 wasm-bindgen = "0.2" 17 - 18 - # Force inclusion of plugin crates 19 - [profile.release] 20 - lto = false