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}