use std::collections::{BTreeMap, HashMap};
pub trait ZodSchema {
fn zod_schema() -> String;
}
pub trait ZodObjectSchema: ZodSchema {}
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(", "))
}
pub struct ZodGenerator {
schemas: BTreeMap<String, String>,
}
impl Default for ZodGenerator {
fn default() -> Self {
Self::new()
}
}
impl ZodGenerator {
pub fn new() -> Self {
Self {
schemas: BTreeMap::new(),
}
}
pub fn add_schema<T: ZodSchema>(&mut self, name: &str) {
let schema = T::zod_schema();
self.schemas.insert(name.to_string(), schema);
}
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
}
}
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() {
assert_eq!(
<HashMap<String, String>>::zod_schema(),
"z.record(z.string(), z.string())"
);
assert_eq!(
<HashMap<String, i32>>::zod_schema(),
"z.record(z.string(), z.number())"
);
assert_eq!(
<HashMap<String, Option<bool>>>::zod_schema(),
"z.record(z.string(), z.boolean().nullable())"
);
assert_eq!(
<HashMap<String, Vec<String>>>::zod_schema(),
"z.record(z.string(), z.array(z.string()))"
);
}
}