Skip to main content

typewriter_macros/
lib.rs

1//! # typewriter-macros
2//!
3//! Proc macro crate for the typewriter type sync SDK.
4//! Provides `#[derive(TypeWriter)]` with `#[sync_to(...)]` and `#[tw(...)]` attributes.
5//!
6//! ## Overview
7//!
8//! This crate implements the `#[derive(TypeWriter)]` proc macro that generates
9//! type definitions in multiple target languages from a single Rust struct or enum definition.
10//!
11//! ## Supported Attributes
12//!
13//! ### `#[sync_to(...)]`
14//!
15//! Specifies which target languages to generate types for:
16//!
17//! ```rust,ignore
18//! #[derive(TypeWriter)]
19//! #[sync_to(typescript, python, go)]           // Generate TS, Python, and Go types
20//! #[sync_to(typescript)]                         // TypeScript only
21//! #[sync_to(typescript, python, go, swift, kotlin, graphql, json_schema)]  // All languages
22//! pub struct MyType { ... }
23//! ```
24//!
25//! **Supported languages:**
26//! - `typescript` / `ts` - TypeScript interfaces and Zod schemas
27//! - `python` / `py` - Python Pydantic models
28//! - `go` / `golang` - Go structs with JSON tags
29//! - `swift` - Swift Codable structs
30//! - `kotlin` / `kt` - Kotlin data classes
31//! - `graphql` / `gql` - GraphQL SDL types
32//! - `json_schema` / `jsonschema` - JSON Schema definitions
33//!
34//! ### `#[tw(...)]`
35//!
36//! Fine-tune the generated output per-type or per-field:
37//!
38//! | Attribute | Description |
39//! |-----------|-------------|
40//! | `#[tw(skip)]` | Exclude field from generated output |
41//! | `#[tw(rename = "name")]` | Override field/variant name in output |
42//! | `#[tw(optional)]` | Force field to be optional |
43//! | `#[tw(type = "custom")]` | Override the generated type string |
44//! | `#[tw(zod)]` | Enable Zod schema generation (TypeScript only) |
45//! | `#[tw(zod = false)]` | Disable Zod schema generation (TypeScript only) |
46//!
47//! ## Example
48//!
49//! ```rust,ignore
50//! use typebridge::TypeWriter;
51//! use serde::{Serialize, Deserialize};
52//!
53//! /// A user profile with all supported features.
54//! #[derive(Serialize, Deserialize, TypeWriter)]
55//! #[sync_to(typescript, python)]
56//! #[tw(zod)]  // Enable Zod schema generation
57//! pub struct UserProfile {
58//!     pub id: Uuid,
59//!     
60//!     /// User's email address
61//!     pub email: String,
62//!     
63//!     #[tw(skip)]  // Not included in generated types
64//!     pub password_hash: String,
65//!     
66//!     #[tw(rename = "displayName")]  // Renamed in output
67//!     pub username: String,
68//!     
69//!     pub age: Option<u32>,
70//! }
71//! ```
72//!
73//! This generates:
74//! - `./generated/typescript/user-profile.ts` - TypeScript interface
75//! - `./generated/typescript/user-profile.schema.ts` - Zod schema
76//! - `./generated/python/user_profile.py` - Python Pydantic model
77//!
78//! ## Build-Time Behavior
79//!
80//! Type files are generated during `cargo build`. The macro:
81//! 1. Parses the annotated struct/enum
82//! 2. Reads `typewriter.toml` for configuration (if present)
83//! 3. Generates type definitions for each target language
84//! 4. Writes files to the configured output directories
85
86use proc_macro::TokenStream;
87use std::path::PathBuf;
88
89/// Derive macro for typewriter type synchronization.
90///
91/// This macro generates type definitions in target languages from Rust structs and enums.
92///
93/// # Usage
94///
95/// ```rust,ignore
96/// use typebridge::TypeWriter;
97///
98/// #[derive(TypeWriter)]
99/// #[sync_to(typescript, python)]
100/// pub struct UserProfile {
101///     pub id: Uuid,
102///     pub email: String,
103///     pub age: Option<u32>,
104/// }
105/// ```
106///
107/// # Errors
108///
109/// The macro will produce a compile error if:
110/// - `#[sync_to(...)]` is missing (required)
111/// - An unsupported language is specified
112/// - The type is a union (not supported)
113///
114/// # Output
115///
116/// On successful compilation, type files are generated:
117/// - TypeScript: `generated/typescript/<type-name>.ts` (+ `.schema.ts` for Zod)
118/// - Python: `generated/python/<type_name>.py`
119/// - Go: `generated/go/<type_name>.go`
120/// - And more for other target languages
121#[proc_macro_derive(TypeWriter, attributes(sync_to, tw))]
122pub fn derive_typewriter(input: TokenStream) -> TokenStream {
123    let input = syn::parse_macro_input!(input as syn::DeriveInput);
124
125    match typewriter_impl(&input) {
126        Ok(_) => TokenStream::new(),
127        Err(err) => err.to_compile_error().into(),
128    }
129}
130
131fn typewriter_impl(input: &syn::DeriveInput) -> syn::Result<()> {
132    let type_def = typewriter_engine::parser::parse_type_def(input)?;
133    let targets = typewriter_engine::parser::parse_sync_to_attr(input)?;
134    let zod_schema = typewriter_engine::parser::parse_tw_zod_attr(input)?;
135
136    if targets.is_empty() {
137        return Err(syn::Error::new_spanned(
138            &input.ident,
139            "typewriter: #[sync_to(...)] attribute is required. \
140             Example: #[sync_to(typescript, python)]",
141        ));
142    }
143
144    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
145    let manifest_dir = PathBuf::from(manifest_dir);
146    let project_root = typewriter_engine::project::discover_macro_root(&manifest_dir);
147    let config = typewriter_engine::project::load_config_or_default(&project_root);
148
149    let spec = typewriter_engine::TypeSpec {
150        type_def,
151        targets,
152        source_path: manifest_dir.join("<proc-macro>"),
153        zod_schema,
154    };
155
156    let files =
157        match typewriter_engine::emit::render_specs(&[spec], &project_root, &config, &[], true) {
158            Ok(files) => files,
159            Err(err) => {
160                eprintln!("typewriter: generation failed for {}: {}", input.ident, err);
161                return Ok(());
162            }
163        };
164
165    if let Err(err) = typewriter_engine::emit::write_generated_files(&files) {
166        eprintln!("typewriter: failed to write generated files: {}", err);
167        return Ok(());
168    }
169
170    for file in files {
171        eprintln!(
172            "  typewriter: {} → {}",
173            file.type_name,
174            file.output_path.display()
175        );
176    }
177
178    Ok(())
179}