zod_gen 1.4.0

Generate Zod schemas and TypeScript types from Rust types. Use with zod_gen_derive for automatic #[derive(ZodSchema)] support and serde rename compatibility.
Documentation
//! # Zod Schema Generation for Rust Types
//!
//! Generate [Zod](https://zod.dev) schemas and TypeScript types from Rust types with full serde rename support.
//!
//! ## Quick Start with Derive Macro (Recommended)
//!
//! ```rust,ignore
//! use zod_gen::{ZodSchema, ZodGenerator};
//! use zod_gen_derive::ZodSchema;
//! use serde::{Serialize, Deserialize};
//!
//! #[derive(ZodSchema, Serialize, Deserialize)]
//! struct User {
//!     id: u32,
//!     name: String,
//!     email: Option<String>,
//! }
//!
//! #[derive(ZodSchema, Serialize, Deserialize)]
//! enum Status {
//!     #[serde(rename = "active")]
//!     Active,
//!     #[serde(rename = "inactive")]
//!     Inactive,
//! }
//!
//! let mut generator = ZodGenerator::new();
//! generator.add_schema::<User>("User");
//! generator.add_schema::<Status>("Status");
//!
//! let typescript = generator.generate();
//! // typescript contains the generated Zod schemas
//! ```
//!
//! This generates:
//!
//! ```typescript
//! // Automatically generated by zod_gen
//! import * as z from 'zod';
//!
//! export const UserSchema = z.object({
//!   id: z.number(),
//!   name: z.string(),
//!   email: z.string().nullable()
//! });
//! export type User = z.infer<typeof UserSchema>;
//!
//! export const StatusSchema = z.union([z.literal("active"), z.literal("inactive")]);
//! export type Status = z.infer<typeof StatusSchema>;
//! ```
//!
//! ## Manual Implementation
//!
//! For custom schema generation, implement the `ZodSchema` trait manually:
//!
//! ```rust
//! use zod_gen::{ZodSchema, zod_object, zod_string};
//!
//! struct CustomType {
//!     field: String,
//! }
//!
//! impl ZodSchema for CustomType {
//!     fn zod_schema() -> String {
//!         zod_object(&[("field", zod_string())])
//!     }
//! }
//! ```
//!
//! ## Features
//!
//! - **Derive Macro**: Automatic schema generation with `#[derive(ZodSchema)]`
//! - **Serde Rename Support**: Full support for `#[serde(rename = "...")]` attributes
//! - **Serde Enum Representations**: Supports externally tagged, internally tagged, adjacently tagged, and untagged enums
//! - **Type Safety**: Generated TypeScript types match your Rust types exactly
//! - **Generic Types**: Built-in support for `Option<T>`, `Vec<T>`, `HashMap<String, T>`
//! - **Batch Generation**: Generate multiple schemas in a single TypeScript file
//!
//! Note: Internally tagged newtype variants that wrap structs are flattened via `z.intersection(...)`.
//! Tuple variants are rejected for internally tagged enums to match Serde's rules.
//!
//! ## Installation
//!
//! Add both crates to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! zod_gen = "1.4.0"
//! zod_gen_derive = "1.4.0"
//! serde = { version = "1.0", features = ["derive"] }
//! ```

use std::collections::{BTreeMap, HashMap};

/// Trait for Rust types that can produce a Zod schema
pub trait ZodSchema {
    /// Returns the Zod schema as a string
    fn zod_schema() -> String;
}

/// Marker trait for types that produce an object-like Zod schema (z.object(...)).
/// Used for internally tagged newtype variants to properly flatten payload fields.
pub trait ZodObjectSchema: ZodSchema {}

/// Helper functions for building Zod schema strings
pub fn zod_string() -> &'static str {
    "z.string()"
}
pub fn zod_number() -> &'static str {
    "z.number()"
}
pub fn zod_bigint() -> &'static str {
    "z.bigint()"
}
pub fn zod_boolean() -> &'static str {
    "z.boolean()"
}

pub fn zod_nullable(inner: &str) -> String {
    format!("{inner}.nullable()")
}
pub fn zod_array(inner: &str) -> String {
    format!("z.array({inner})")
}
pub fn zod_record(value: &str) -> String {
    format!("z.record(z.string(), {value})")
}

pub fn zod_object(fields: &[(&str, &str)]) -> String {
    let items: Vec<String> = fields.iter().map(|(k, v)| format!("  {k}: {v}")).collect();
    format!("z.object({{\n{}\n}})", items.join(",\n"))
}

pub fn zod_literal(value: &str) -> String {
    format!("z.literal('{value}')")
}

pub fn zod_union(variants: &[&str]) -> String {
    format!("z.union([{}])", variants.join(", "))
}

pub fn zod_discriminated_union(tag_key: &str, variants: &[&str]) -> String {
    format!(
        "z.discriminatedUnion('{tag_key}', [{}])",
        variants.join(", ")
    )
}

pub fn zod_tuple(items: &[&str]) -> String {
    format!("z.tuple([{}])", items.join(", "))
}

pub fn zod_null() -> &'static str {
    "z.null()"
}

pub fn zod_intersection(a: &str, b: &str) -> String {
    format!("z.intersection({a}, {b})")
}

pub fn zod_enum(variants: &[&str]) -> String {
    let lits: Vec<String> = variants
        .iter()
        .map(|v| format!("z.literal('{v}')"))
        .collect();
    format!("z.union([{}])", lits.join(", "))
}

/// Generator that collects schemas and writes TypeScript files
///
/// The `ZodGenerator` generates Zod schemas with proper serde rename support
/// from Rust types, providing TypeScript type safety.
pub struct ZodGenerator {
    // Use a btreemap so we retain key order which is useful to ensure the
    // output is stable (e.g. if zod_gen is run on CI)
    schemas: BTreeMap<String, String>,
}

impl Default for ZodGenerator {
    fn default() -> Self {
        Self::new()
    }
}

impl ZodGenerator {
    pub fn new() -> Self {
        Self {
            schemas: BTreeMap::new(),
        }
    }

    /// Add a Zod schema for a Rust type
    pub fn add_schema<T: ZodSchema>(&mut self, name: &str) {
        let schema = T::zod_schema();
        self.schemas.insert(name.to_string(), schema);
    }

    /// Generate Zod schemas file
    ///
    /// Creates a TypeScript file with Zod schemas and inferred types.
    pub fn generate(&self) -> String {
        let mut output =
            String::from("// Automatically generated by zod_gen\nimport * as z from 'zod';\n\n");

        for (name, schema) in &self.schemas {
            output.push_str(&format!(
                "export const {name}Schema = {schema};\nexport type {name} = z.infer<typeof {name}Schema>;\n\n"
            ));
        }

        output
    }
}

// Implementations for common Rust types
impl ZodSchema for String {
    fn zod_schema() -> String {
        zod_string().to_string()
    }
}

impl ZodSchema for i32 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl ZodSchema for i64 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl ZodSchema for u32 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl ZodSchema for u64 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl ZodSchema for f32 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl ZodSchema for f64 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl ZodSchema for bool {
    fn zod_schema() -> String {
        zod_boolean().to_string()
    }
}

impl ZodSchema for u8 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl ZodSchema for u16 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl ZodSchema for i8 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl ZodSchema for i16 {
    fn zod_schema() -> String {
        zod_number().to_string()
    }
}

impl<T: ZodSchema> ZodSchema for Option<T> {
    fn zod_schema() -> String {
        zod_nullable(&T::zod_schema())
    }
}

impl<T: ZodSchema> ZodSchema for Vec<T> {
    fn zod_schema() -> String {
        zod_array(&T::zod_schema())
    }
}

impl<T: ZodSchema> ZodSchema for HashMap<String, T> {
    fn zod_schema() -> String {
        zod_record(&T::zod_schema())
    }
}

impl ZodSchema for serde_json::Value {
    fn zod_schema() -> String {
        "z.any()".to_string()
    }
}

impl<T: ZodSchema> ZodObjectSchema for T {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_primitives() {
        assert_eq!(zod_string(), "z.string()");
        assert_eq!(zod_number(), "z.number()");
        assert_eq!(zod_boolean(), "z.boolean()");
        assert_eq!(zod_bigint(), "z.bigint()");

        assert_eq!(i8::zod_schema(), "z.number()");
        assert_eq!(i16::zod_schema(), "z.number()");
        assert_eq!(i32::zod_schema(), "z.number()");
        assert_eq!(i64::zod_schema(), "z.number()");
        assert_eq!(u8::zod_schema(), "z.number()");
        assert_eq!(u16::zod_schema(), "z.number()");
        assert_eq!(u32::zod_schema(), "z.number()");
        assert_eq!(u64::zod_schema(), "z.number()");
        assert_eq!(f32::zod_schema(), "z.number()");
        assert_eq!(f64::zod_schema(), "z.number()");
    }

    #[derive(Debug)]
    struct Dummy;

    impl ZodSchema for Dummy {
        fn zod_schema() -> String {
            zod_string().into()
        }
    }

    #[test]
    fn test_generator() {
        let mut gen = ZodGenerator::new();
        gen.add_schema::<Dummy>("Dummy");
        let output = gen.generate();
        assert!(output.contains("DummySchema = z.string()"));
        assert!(output.contains("export type Dummy"));
    }

    #[test]
    fn test_generate_schemas() {
        let mut gen = ZodGenerator::new();
        gen.add_schema::<Dummy>("Dummy");
        let output = gen.generate();
        assert!(output.contains("// Automatically generated by zod_gen"));
        assert!(output.contains("import * as z from 'zod';"));
        assert!(output.contains("DummySchema = z.string()"));
    }

    #[test]
    fn test_generic_hashmap() {
        // Test HashMap<String, String>
        assert_eq!(
            <HashMap<String, String>>::zod_schema(),
            "z.record(z.string(), z.string())"
        );

        // Test HashMap<String, i32>
        assert_eq!(
            <HashMap<String, i32>>::zod_schema(),
            "z.record(z.string(), z.number())"
        );

        // Test HashMap<String, Option<bool>>
        assert_eq!(
            <HashMap<String, Option<bool>>>::zod_schema(),
            "z.record(z.string(), z.boolean().nullable())"
        );

        // Test HashMap<String, Vec<String>>
        assert_eq!(
            <HashMap<String, Vec<String>>>::zod_schema(),
            "z.record(z.string(), z.array(z.string()))"
        );
    }
}