diff --git a/README.md b/README.md index add1e0b..19a6d17 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,31 @@ namespace com.example.post { ### Install ```bash -cargo install --path mlf-cli +# Install with all code generators (default: TypeScript, Go, Rust) +cargo install --path mlf-cli --all-features + +# Install with only specific generators +cargo install --path mlf-cli --no-default-features --features typescript,go + +# Install with JSON generation only +cargo install --path mlf-cli --no-default-features ``` -### Generate JSON lexicons from MLF +### Generate code from MLF ```bash +# Generate TypeScript types +mlf generate code -g typescript -i examples/**/*.mlf -o output/ + +# Generate Go structs +mlf generate code -g go -i examples/**/*.mlf -o output/ + +# Generate Rust structs with serde +mlf generate code -g rust -i examples/**/*.mlf -o output/ + +# Generate JSON lexicons (always available) +mlf generate code -g json -i examples/**/*.mlf -o output/ +# Or use the legacy command: mlf generate lexicon -i examples/**/*.mlf -o output/ ``` @@ -44,24 +63,29 @@ mlf generate lexicon -i examples/**/*.mlf -o output/ mlf check examples/app.bsky.feed.post.mlf ``` -### Validate generated lexicons +### Validate JSON records ```bash -mlf validate output/app.bsky.feed.post.json +mlf validate examples/app.bsky.feed.post.mlf record.json ``` ## Project layout ``` mlf/ -├── mlf-cli/ # Command-line app -├── mlf-lang/ # Parser and lexer (no_std compatible) -├── mlf-codegen/ # JSON lexicon code generation -├── mlf-validation/ # Lexicon validation -├── mlf-diagnostics/ # Fancy error reporting -├── mlf-wasm/ # WASM bindings for browser use -├── tree-sitter-mlf/ # Tree-sitter grammar for syntax highlighting -└── website/ # Docs and playground +├── mlf-cli/ # Command-line app +├── mlf-lang/ # Parser and lexer (no_std compatible) +├── mlf-codegen/ # Core code generation with plugin system +├── codegen-plugins/ # Language-specific code generators +│ ├── mlf-codegen-typescript/ # TypeScript generator +│ ├── mlf-codegen-go/ # Go generator +│ └── mlf-codegen-rust/ # Rust generator +├── mlf-validation/ # Lexicon validation +├── mlf-diagnostics/ # Fancy error reporting +├── mlf-wasm/ # WASM bindings for browser use +├── tree-sitter-mlf/ # Tree-sitter grammar for syntax highlighting +└── website/ # Docs and playground + └── mlf-playground-wasm/ # Playground WASM with all generators ``` ## Documentation diff --git a/codegen-plugins/mlf-codegen-go/src/lib.rs b/codegen-plugins/mlf-codegen-go/src/lib.rs index d9a8eb8..e2b9392 100644 --- a/codegen-plugins/mlf-codegen-go/src/lib.rs +++ b/codegen-plugins/mlf-codegen-go/src/lib.rs @@ -7,18 +7,6 @@ pub struct GoGenerator; impl GoGenerator { pub const NAME: &'static str = "go"; - fn to_snake_case(&self, s: &str) -> String { - // Simple camelCase to snake_case conversion - let mut result = String::new(); - for (i, ch) in s.chars().enumerate() { - if ch.is_uppercase() && i > 0 { - result.push('_'); - } - result.push(ch.to_lowercase().next().unwrap()); - } - result - } - fn generate_type(&self, ty: &Type, optional: bool, ctx: &GeneratorContext) -> Result { let base_type = match ty { Type::Primitive { kind, .. } => match kind { diff --git a/codegen-plugins/mlf-codegen-rust/src/lib.rs b/codegen-plugins/mlf-codegen-rust/src/lib.rs index b494b42..0d09b7a 100644 --- a/codegen-plugins/mlf-codegen-rust/src/lib.rs +++ b/codegen-plugins/mlf-codegen-rust/src/lib.rs @@ -67,7 +67,7 @@ impl RustGenerator { // Otherwise use serde_json::Value for flexibility "serde_json::Value".to_string() } - Type::Object { fields, .. } => { + Type::Object { .. } => { // Inline struct types aren't idiomatic in Rust // We'd need to generate a named type // For now, use serde_json::Value diff --git a/mlf-cli/src/generate/code.rs b/mlf-cli/src/generate/code.rs index f5f3be6..91e6dd2 100644 --- a/mlf-cli/src/generate/code.rs +++ b/mlf-cli/src/generate/code.rs @@ -1,5 +1,5 @@ use miette::Diagnostic; -use mlf_codegen::plugin::{CodeGenerator, GeneratorContext}; +use mlf_codegen::plugin::GeneratorContext; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -46,6 +46,7 @@ pub enum GenerateError { #[error("Code generation failed: {0}")] #[diagnostic(code(mlf::generate::generation_failed))] + #[allow(dead_code)] GenerationFailed(String), } @@ -216,10 +217,32 @@ pub fn run( } fn extract_namespace(file_path: &Path) -> String { - // Namespace is derived solely from the filename - file_path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("unknown") - .to_string() + // Extract namespace from path components + // e.g., com/atproto/admin/defs.mlf -> com.atproto.admin.defs + + let mut components = Vec::new(); + + for component in file_path.components() { + match component { + std::path::Component::Normal(os_str) => { + if let Some(s) = os_str.to_str() { + components.push(s); + } + } + _ => continue, // Skip ., .., /, etc. + } + } + + // Remove the .mlf extension from the last component if present + if let Some(last) = components.last_mut() { + if let Some(stem) = last.strip_suffix(".mlf") { + *last = stem; + } + } + + if components.is_empty() { + return "unknown".to_string(); + } + + components.join(".") } diff --git a/mlf-cli/src/generate/lexicon.rs b/mlf-cli/src/generate/lexicon.rs index 79177e7..964baaa 100644 --- a/mlf-cli/src/generate/lexicon.rs +++ b/mlf-cli/src/generate/lexicon.rs @@ -6,6 +6,7 @@ use thiserror::Error; pub enum GenerateError { #[error("Failed to read file: {path}")] #[diagnostic(code(mlf::generate::read_file))] + #[allow(dead_code)] ReadFile { path: String, #[source] @@ -153,10 +154,32 @@ pub fn run(input_patterns: Vec, output_dir: PathBuf, flat: bool) -> Resu } fn extract_namespace(file_path: &Path) -> String { - // Namespace is derived solely from the filename - file_path - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("unknown") - .to_string() + // Extract namespace from path components + // e.g., com/atproto/admin/defs.mlf -> com.atproto.admin.defs + + let mut components = Vec::new(); + + for component in file_path.components() { + match component { + std::path::Component::Normal(os_str) => { + if let Some(s) = os_str.to_str() { + components.push(s); + } + } + _ => continue, // Skip ., .., /, etc. + } + } + + // Remove the .mlf extension from the last component if present + if let Some(last) = components.last_mut() { + if let Some(stem) = last.strip_suffix(".mlf") { + *last = stem; + } + } + + if components.is_empty() { + return "unknown".to_string(); + } + + components.join(".") } diff --git a/mlf-wasm/src/lib.rs b/mlf-wasm/src/lib.rs index 70f7b7e..2a2b015 100644 --- a/mlf-wasm/src/lib.rs +++ b/mlf-wasm/src/lib.rs @@ -1,5 +1,5 @@ -use wasm_bindgen::prelude::*; use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; #[derive(Serialize, Deserialize)] pub struct ParseResult { @@ -244,16 +244,19 @@ pub fn list_generators() -> JsValue { #[cfg(target_arch = "wasm32")] web_sys::console::log_1(&format!("Found {} generators", generators.len()).into()); - let generator_infos: Vec = generators.iter().map(|generator| { - #[cfg(target_arch = "wasm32")] - web_sys::console::log_1(&format!(" - {}", generator.name()).into()); + let generator_infos: Vec = generators + .iter() + .map(|generator| { + #[cfg(target_arch = "wasm32")] + web_sys::console::log_1(&format!(" - {}", generator.name()).into()); - GeneratorInfo { - name: generator.name().to_string(), - description: generator.description().to_string(), - file_extension: generator.file_extension().to_string(), - } - }).collect(); + GeneratorInfo { + name: generator.name().to_string(), + description: generator.description().to_string(), + file_extension: generator.file_extension().to_string(), + } + }) + .collect(); let result = ListGeneratorsResult { generators: generator_infos, @@ -266,7 +269,7 @@ pub fn list_generators() -> JsValue { #[wasm_bindgen] pub fn generate_code(source: &str, namespace: &str, generator_name: &str) -> JsValue { // Load standard library - let mut workspace = match mlf_lang::Workspace::with_std() { + let workspace = match mlf_lang::Workspace::with_std() { Ok(ws) => ws, Err(e) => { let result = GenerateCodeResult { diff --git a/website/content/docs/cli.md b/website/content/docs/cli.md index 5324bb7..2415c88 100644 --- a/website/content/docs/cli.md +++ b/website/content/docs/cli.md @@ -16,16 +16,31 @@ cargo build --release The binary will be at `target/release/mlf`. -Optionally, install to your PATH: +### Install with cargo ```bash -# Option 1: Use cargo install -cargo install --path mlf-cli +# Install with all code generators (default: TypeScript, Go, Rust + JSON) +cargo install --path mlf-cli --all-features -# Option 2: Manually copy the binary -cp target/release/mlf /usr/local/bin/ +# Install with only specific generators +cargo install --path mlf-cli --no-default-features --features typescript +cargo install --path mlf-cli --no-default-features --features typescript,go + +# Install with JSON generation only (minimal) +cargo install --path mlf-cli --no-default-features ``` +### Available Features + +The CLI uses cargo features for optional code generators: + +- `typescript` - TypeScript type definitions and interfaces +- `go` - Go structs with JSON tags +- `rust` - Rust structs with serde derive macros +- **default** = `["typescript", "go", "rust"]` - All generators enabled + +The JSON lexicon generator is always available (built into `mlf-codegen`). + ## Commands ### `mlf check` @@ -115,6 +130,111 @@ mlf generate lexicon -i "src/**/*.mlf" -o dist/lexicons/ --- +### `mlf generate code` + +Generate code in various programming languages from MLF files. + +```bash +mlf generate code --generator --output [OPTIONS] +``` + +**Options:** +- `-g, --generator ` - Generator to use: `json`, `typescript`, `go`, or `rust` (required) +- `-i, --input ` - Input MLF files (glob patterns supported, can be specified multiple times) +- `-o, --output ` - Output directory (required) +- `--flat` - Use flat file structure (e.g., `com.example.thread.ts`) + +**Available Generators:** + +| Generator | Output | Description | +|-----------|--------|-------------| +| `json` | `.json` | AT Protocol JSON lexicons (always available) | +| `typescript` | `.ts` | TypeScript interfaces with JSDoc comments | +| `go` | `.go` | Go structs with JSON tags and proper capitalization | +| `rust` | `.rs` | Rust structs with serde derive macros | + +**TypeScript Example:** + +```bash +mlf generate code -g typescript -i thread.mlf -o src/types/ +# Creates: src/types/com/example/thread.ts +``` + +Generated TypeScript: +```typescript +/** + * Generated from com.example.thread + * Do not edit manually + */ + +export interface Thread { + /** Thread title */ + title: string; + /** Creation timestamp */ + createdAt: string; + posts: Post[]; +} +``` + +**Go Example:** + +```bash +mlf generate code -g go -i thread.mlf -o pkg/models/ +# Creates: pkg/models/com/example/thread.go +``` + +Generated Go: +```go +// Generated from com.example.thread +// Do not edit manually + +package thread + +// Thread represents a discussion thread +type Thread struct { + // Thread title + Title string `json:"title"` + // Creation timestamp + CreatedAt string `json:"createdAt"` + Posts []Post `json:"posts"` +} +``` + +**Rust Example:** + +```bash +mlf generate code -g rust -i thread.mlf -o src/models/ +# Creates: src/models/com/example/thread.rs +``` + +Generated Rust: +```rust +// Generated from com.example.thread +// Do not edit manually + +use serde::{Deserialize, Serialize}; + +/// Thread represents a discussion thread +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Thread { + /// Thread title + pub title: String, + /// Creation timestamp + #[serde(rename = "createdAt")] + pub created_at: String, + pub posts: Vec, +} +``` + +**Notes:** + +- TypeScript: Uses interfaces for records, type aliases for defs, optional fields use `?` +- Go: Uses structs with JSON tags, pointer types for optional fields, PascalCase for exports +- Rust: Uses structs with serde, `Option` for optional fields, snake_case with `#[serde(rename)]` +- All generators handle doc comments, constraints are preserved in generated code where applicable + +--- + ## Error Messages MLF provides rich error messages with: diff --git a/website/mlf-playground-wasm/Cargo.toml b/website/mlf-playground-wasm/Cargo.toml index 3c5f80a..d4b2ea6 100644 --- a/website/mlf-playground-wasm/Cargo.toml +++ b/website/mlf-playground-wasm/Cargo.toml @@ -14,7 +14,3 @@ mlf-codegen-typescript = { path = "../../codegen-plugins/mlf-codegen-typescript" mlf-codegen-go = { path = "../../codegen-plugins/mlf-codegen-go" } mlf-codegen-rust = { path = "../../codegen-plugins/mlf-codegen-rust" } wasm-bindgen = "0.2" - -# Force inclusion of plugin crates -[profile.release] -lto = false