use crate::parser::{FieldType, ParsedField, ParsedType, ValidationRule};
use anyhow::Result;
pub fn generate(types: &[ParsedType]) -> Result<String> {
let mut output = String::new();
output.push_str("/**\n");
output.push_str(" * AUTO-GENERATED by domainstack-cli\n");
output.push_str(" * DO NOT EDIT MANUALLY\n");
output.push_str(" *\n");
output.push_str(&format!(
" * Generated: {}\n",
chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ")
));
output.push_str(" */\n\n");
output.push_str("import { z } from \"zod\";\n\n");
for parsed_type in types {
generate_type_schema(&mut output, parsed_type)?;
output.push('\n');
}
Ok(output)
}
fn generate_type_schema(output: &mut String, parsed_type: &ParsedType) -> Result<()> {
let schema_name = format!("{}Schema", parsed_type.name);
output.push_str(&format!("export const {} = z.object({{\n", schema_name));
for field in &parsed_type.fields {
output.push_str(" ");
output.push_str(&field.name);
output.push_str(": ");
generate_field_schema(output, field)?;
output.push_str(",\n");
}
output.push_str("});\n\n");
output.push_str(&format!(
"export type {} = z.infer<typeof {}>;\n",
parsed_type.name, schema_name
));
Ok(())
}
fn generate_field_schema(output: &mut String, field: &ParsedField) -> Result<()> {
let is_optional = matches!(field.ty, FieldType::Option(_));
let base_schema = if is_optional {
if let FieldType::Option(inner) = &field.ty {
generate_base_type(inner)
} else {
generate_base_type(&field.ty)
}
} else {
generate_base_type(&field.ty)
};
output.push_str(&base_schema);
for rule in &field.validation_rules {
generate_validation_rule(output, rule, &field.ty)?;
}
if is_optional {
output.push_str(".optional()");
}
Ok(())
}
fn generate_base_type(field_type: &FieldType) -> String {
match field_type {
FieldType::String => "z.string()".to_string(),
FieldType::Bool => "z.boolean()".to_string(),
FieldType::U8
| FieldType::U16
| FieldType::U32
| FieldType::I8
| FieldType::I16
| FieldType::I32
| FieldType::F32
| FieldType::F64 => "z.number()".to_string(),
FieldType::U64 | FieldType::U128 | FieldType::I64 | FieldType::I128 => {
"z.number() /* Warning: Large integers may lose precision in JavaScript */".to_string()
}
FieldType::Option(inner) => {
generate_base_type(inner)
}
FieldType::Vec(inner) => {
format!("z.array({})", generate_base_type(inner))
}
FieldType::Custom(name) => {
format!("{}Schema /* Custom type */", name)
}
}
}
fn generate_validation_rule(
output: &mut String,
rule: &ValidationRule,
_field_type: &FieldType,
) -> Result<()> {
match rule {
ValidationRule::Email => output.push_str(".email()"),
ValidationRule::Url => output.push_str(".url()"),
ValidationRule::MinLen(min) => output.push_str(&format!(".min({})", min)),
ValidationRule::MaxLen(max) => output.push_str(&format!(".max({})", max)),
ValidationRule::Length { min, max } => {
output.push_str(&format!(".min({}).max({})", min, max));
}
ValidationRule::NonEmpty => output.push_str(".min(1)"),
ValidationRule::NonBlank => output.push_str(".trim().min(1)"),
ValidationRule::Alphanumeric => output.push_str(".regex(/^[a-zA-Z0-9]*$/)"),
ValidationRule::AlphaOnly => output.push_str(".regex(/^[a-zA-Z]*$/)"),
ValidationRule::NumericString => output.push_str(".regex(/^[0-9]*$/)"),
ValidationRule::Ascii => output.push_str(".regex(/^[\\x00-\\x7F]*$/)"),
ValidationRule::StartsWith(prefix) => {
output.push_str(&format!(".startsWith(\"{}\")", escape_string(prefix)));
}
ValidationRule::EndsWith(suffix) => {
output.push_str(&format!(".endsWith(\"{}\")", escape_string(suffix)));
}
ValidationRule::Contains(substring) => {
output.push_str(&format!(".includes(\"{}\")", escape_string(substring)));
}
ValidationRule::MatchesRegex(pattern) => {
output.push_str(&format!(".regex(/{}/))", escape_regex(pattern)));
}
ValidationRule::NoWhitespace => output.push_str(".regex(/^\\S*$/)"),
ValidationRule::Range { min, max } => {
output.push_str(&format!(".min({}).max({})", min, max));
}
ValidationRule::Min(min) => output.push_str(&format!(".min({})", min)),
ValidationRule::Max(max) => output.push_str(&format!(".max({})", max)),
ValidationRule::Positive => output.push_str(".positive()"),
ValidationRule::Negative => output.push_str(".negative()"),
ValidationRule::NonZero => {
output.push_str(".refine(n => n !== 0, { message: \"Must be non-zero\" })")
}
ValidationRule::MultipleOf(divisor) => {
output.push_str(&format!(".multipleOf({})", divisor));
}
ValidationRule::Finite => output.push_str(".finite()"),
ValidationRule::Custom(name) => {
output.push_str(&format!(" /* Custom validation: {} not supported */", name));
}
}
Ok(())
}
fn escape_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn escape_regex(pattern: &str) -> String {
let pattern = pattern.strip_prefix('^').unwrap_or(pattern);
let pattern = pattern.strip_suffix('$').unwrap_or(pattern);
pattern.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_base_type_primitives() {
assert_eq!(generate_base_type(&FieldType::String), "z.string()");
assert_eq!(generate_base_type(&FieldType::Bool), "z.boolean()");
assert_eq!(generate_base_type(&FieldType::U8), "z.number()");
assert_eq!(generate_base_type(&FieldType::I32), "z.number()");
assert_eq!(generate_base_type(&FieldType::F64), "z.number()");
}
#[test]
fn test_generate_base_type_large_integers() {
let result = generate_base_type(&FieldType::U64);
assert!(result.contains("z.number()"));
assert!(result.contains("Warning"));
assert!(result.contains("precision"));
let result = generate_base_type(&FieldType::I128);
assert!(result.contains("z.number()"));
assert!(result.contains("Warning"));
}
#[test]
fn test_generate_base_type_option() {
let result = generate_base_type(&FieldType::Option(Box::new(FieldType::String)));
assert_eq!(result, "z.string()");
let result = generate_base_type(&FieldType::Option(Box::new(FieldType::U32)));
assert_eq!(result, "z.number()");
}
#[test]
fn test_generate_base_type_vec() {
let result = generate_base_type(&FieldType::Vec(Box::new(FieldType::String)));
assert_eq!(result, "z.array(z.string())");
let result = generate_base_type(&FieldType::Vec(Box::new(FieldType::U32)));
assert_eq!(result, "z.array(z.number())");
}
#[test]
fn test_generate_base_type_custom() {
let result = generate_base_type(&FieldType::Custom("User".to_string()));
assert_eq!(result, "UserSchema /* Custom type */");
}
#[test]
fn test_string_validation_rules() {
let mut output = String::new();
generate_validation_rule(&mut output, &ValidationRule::Email, &FieldType::String).unwrap();
assert_eq!(output, ".email()");
output.clear();
generate_validation_rule(&mut output, &ValidationRule::Url, &FieldType::String).unwrap();
assert_eq!(output, ".url()");
output.clear();
generate_validation_rule(&mut output, &ValidationRule::MinLen(5), &FieldType::String)
.unwrap();
assert_eq!(output, ".min(5)");
output.clear();
generate_validation_rule(
&mut output,
&ValidationRule::MaxLen(100),
&FieldType::String,
)
.unwrap();
assert_eq!(output, ".max(100)");
}
#[test]
fn test_length_validation() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::Length { min: 3, max: 20 },
&FieldType::String,
)
.unwrap();
assert_eq!(output, ".min(3).max(20)");
}
#[test]
fn test_string_pattern_rules() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::Alphanumeric,
&FieldType::String,
)
.unwrap();
assert!(output.contains(".regex(/^[a-zA-Z0-9]*$/)"));
output.clear();
generate_validation_rule(&mut output, &ValidationRule::AlphaOnly, &FieldType::String)
.unwrap();
assert!(output.contains(".regex(/^[a-zA-Z]*$/)"));
output.clear();
generate_validation_rule(
&mut output,
&ValidationRule::NumericString,
&FieldType::String,
)
.unwrap();
assert!(output.contains(".regex(/^[0-9]*$/)"));
}
#[test]
fn test_string_content_rules() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::StartsWith("https://".to_string()),
&FieldType::String,
)
.unwrap();
assert_eq!(output, ".startsWith(\"https://\")");
output.clear();
generate_validation_rule(
&mut output,
&ValidationRule::EndsWith(".com".to_string()),
&FieldType::String,
)
.unwrap();
assert_eq!(output, ".endsWith(\".com\")");
output.clear();
generate_validation_rule(
&mut output,
&ValidationRule::Contains("example".to_string()),
&FieldType::String,
)
.unwrap();
assert_eq!(output, ".includes(\"example\")");
}
#[test]
fn test_numeric_validation_rules() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::Range {
min: "18".to_string(),
max: "120".to_string(),
},
&FieldType::U8,
)
.unwrap();
assert_eq!(output, ".min(18).max(120)");
output.clear();
generate_validation_rule(
&mut output,
&ValidationRule::Min("0".to_string()),
&FieldType::I32,
)
.unwrap();
assert_eq!(output, ".min(0)");
output.clear();
generate_validation_rule(
&mut output,
&ValidationRule::Max("100".to_string()),
&FieldType::I32,
)
.unwrap();
assert_eq!(output, ".max(100)");
output.clear();
generate_validation_rule(&mut output, &ValidationRule::Positive, &FieldType::I32).unwrap();
assert_eq!(output, ".positive()");
output.clear();
generate_validation_rule(&mut output, &ValidationRule::Negative, &FieldType::I32).unwrap();
assert_eq!(output, ".negative()");
}
#[test]
fn test_non_zero_validation() {
let mut output = String::new();
generate_validation_rule(&mut output, &ValidationRule::NonZero, &FieldType::I32).unwrap();
assert!(output.contains(".refine"));
assert!(output.contains("n !== 0"));
assert!(output.contains("Must be non-zero"));
}
#[test]
fn test_multiple_of_validation() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::MultipleOf("5".to_string()),
&FieldType::I32,
)
.unwrap();
assert_eq!(output, ".multipleOf(5)");
}
#[test]
fn test_finite_validation() {
let mut output = String::new();
generate_validation_rule(&mut output, &ValidationRule::Finite, &FieldType::F64).unwrap();
assert_eq!(output, ".finite()");
}
#[test]
fn test_custom_validation() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::Custom("my_validator".to_string()),
&FieldType::String,
)
.unwrap();
assert!(output.contains("Custom validation"));
assert!(output.contains("my_validator"));
assert!(output.contains("not supported"));
}
#[test]
fn test_escape_string() {
assert_eq!(escape_string("hello"), "hello");
assert_eq!(escape_string("hello\\world"), "hello\\\\world");
assert_eq!(escape_string("hello\"world"), "hello\\\"world");
assert_eq!(escape_string("hello\nworld"), "hello\\nworld");
assert_eq!(escape_string("hello\tworld"), "hello\\tworld");
}
#[test]
fn test_escape_regex() {
assert_eq!(escape_regex("^[a-z]+$"), "[a-z]+");
assert_eq!(escape_regex("^[a-z]+"), "[a-z]+");
assert_eq!(escape_regex("[a-z]+$"), "[a-z]+");
assert_eq!(escape_regex("[a-z]+"), "[a-z]+");
}
#[test]
fn test_field_schema_with_validations() {
let mut output = String::new();
let field = ParsedField {
name: "email".to_string(),
ty: FieldType::String,
validation_rules: vec![ValidationRule::Email, ValidationRule::MaxLen(255)],
};
generate_field_schema(&mut output, &field).unwrap();
assert_eq!(output, "z.string().email().max(255)");
}
#[test]
fn test_field_schema_optional_with_validations() {
let mut output = String::new();
let field = ParsedField {
name: "website".to_string(),
ty: FieldType::Option(Box::new(FieldType::String)),
validation_rules: vec![ValidationRule::Url],
};
generate_field_schema(&mut output, &field).unwrap();
assert_eq!(output, "z.string().url().optional()");
}
#[test]
fn test_field_schema_multiple_rules() {
let mut output = String::new();
let field = ParsedField {
name: "username".to_string(),
ty: FieldType::String,
validation_rules: vec![
ValidationRule::Length { min: 3, max: 20 },
ValidationRule::Alphanumeric,
],
};
generate_field_schema(&mut output, &field).unwrap();
assert!(output.contains("z.string()"));
assert!(output.contains(".min(3).max(20)"));
assert!(output.contains(".regex(/^[a-zA-Z0-9]*$/)"));
}
#[test]
fn test_generate_complete_schema() {
let types = vec![ParsedType {
name: "User".to_string(),
fields: vec![
ParsedField {
name: "email".to_string(),
ty: FieldType::String,
validation_rules: vec![ValidationRule::Email],
},
ParsedField {
name: "age".to_string(),
ty: FieldType::U8,
validation_rules: vec![ValidationRule::Range {
min: "18".to_string(),
max: "120".to_string(),
}],
},
],
}];
let result = generate(&types).unwrap();
assert!(result.contains("AUTO-GENERATED"));
assert!(result.contains("DO NOT EDIT"));
assert!(result.contains("import { z } from \"zod\""));
assert!(result.contains("export const UserSchema = z.object({"));
assert!(result.contains("email: z.string().email()"));
assert!(result.contains("age: z.number().min(18).max(120)"));
assert!(result.contains("});"));
assert!(result.contains("export type User = z.infer<typeof UserSchema>"));
}
#[test]
fn test_non_empty_validation() {
let mut output = String::new();
generate_validation_rule(&mut output, &ValidationRule::NonEmpty, &FieldType::String)
.unwrap();
assert_eq!(output, ".min(1)");
}
#[test]
fn test_non_blank_validation() {
let mut output = String::new();
generate_validation_rule(&mut output, &ValidationRule::NonBlank, &FieldType::String)
.unwrap();
assert_eq!(output, ".trim().min(1)");
}
#[test]
fn test_ascii_validation() {
let mut output = String::new();
generate_validation_rule(&mut output, &ValidationRule::Ascii, &FieldType::String).unwrap();
assert!(output.contains(".regex"));
assert!(output.contains("\\x00-\\x7F"));
}
#[test]
fn test_no_whitespace_validation() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::NoWhitespace,
&FieldType::String,
)
.unwrap();
assert!(output.contains(".regex"));
assert!(output.contains("\\S"));
}
#[test]
fn test_matches_regex_validation() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::MatchesRegex("^[a-z]+$".to_string()),
&FieldType::String,
)
.unwrap();
assert!(output.contains(".regex(/[a-z]+/)"));
}
#[test]
fn test_nested_option_type() {
let nested = FieldType::Option(Box::new(FieldType::Vec(Box::new(FieldType::String))));
let result = generate_base_type(&nested);
assert_eq!(result, "z.array(z.string())");
}
#[test]
fn test_nested_vec_type() {
let nested = FieldType::Vec(Box::new(FieldType::Vec(Box::new(FieldType::U32))));
let result = generate_base_type(&nested);
assert_eq!(result, "z.array(z.array(z.number()))");
}
#[test]
fn test_generate_multiple_types() {
let types = vec![
ParsedType {
name: "User".to_string(),
fields: vec![ParsedField {
name: "id".to_string(),
ty: FieldType::U32,
validation_rules: vec![],
}],
},
ParsedType {
name: "Order".to_string(),
fields: vec![ParsedField {
name: "total".to_string(),
ty: FieldType::F64,
validation_rules: vec![],
}],
},
];
let result = generate(&types).unwrap();
assert!(result.contains("export const UserSchema"));
assert!(result.contains("export const OrderSchema"));
assert!(result.contains("export type User"));
assert!(result.contains("export type Order"));
}
#[test]
fn test_all_integer_types() {
assert_eq!(generate_base_type(&FieldType::U8), "z.number()");
assert_eq!(generate_base_type(&FieldType::U16), "z.number()");
assert_eq!(generate_base_type(&FieldType::U32), "z.number()");
assert_eq!(generate_base_type(&FieldType::I8), "z.number()");
assert_eq!(generate_base_type(&FieldType::I16), "z.number()");
assert_eq!(generate_base_type(&FieldType::I32), "z.number()");
assert_eq!(generate_base_type(&FieldType::F32), "z.number()");
}
#[test]
fn test_large_integer_u128() {
let result = generate_base_type(&FieldType::U128);
assert!(result.contains("z.number()"));
assert!(result.contains("Warning"));
}
#[test]
fn test_large_integer_i64() {
let result = generate_base_type(&FieldType::I64);
assert!(result.contains("z.number()"));
assert!(result.contains("precision"));
}
#[test]
fn test_escape_string_carriage_return() {
assert_eq!(escape_string("hello\rworld"), "hello\\rworld");
}
#[test]
fn test_escape_string_combined() {
assert_eq!(
escape_string("line1\nline2\ttab\"quote\\slash"),
"line1\\nline2\\ttab\\\"quote\\\\slash"
);
}
#[test]
fn test_vec_with_custom_type() {
let result = generate_base_type(&FieldType::Vec(Box::new(FieldType::Custom(
"Item".to_string(),
))));
assert_eq!(result, "z.array(ItemSchema /* Custom type */)");
}
#[test]
fn test_option_with_custom_type() {
let result = generate_base_type(&FieldType::Option(Box::new(FieldType::Custom(
"Address".to_string(),
))));
assert_eq!(result, "AddressSchema /* Custom type */");
}
#[test]
fn test_min_validation_numeric() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::Min("-10".to_string()),
&FieldType::I32,
)
.unwrap();
assert_eq!(output, ".min(-10)");
}
#[test]
fn test_max_validation_numeric() {
let mut output = String::new();
generate_validation_rule(
&mut output,
&ValidationRule::Max("999".to_string()),
&FieldType::U32,
)
.unwrap();
assert_eq!(output, ".max(999)");
}
#[test]
fn test_field_schema_array_with_validation() {
let mut output = String::new();
let field = ParsedField {
name: "tags".to_string(),
ty: FieldType::Vec(Box::new(FieldType::String)),
validation_rules: vec![],
};
generate_field_schema(&mut output, &field).unwrap();
assert_eq!(output, "z.array(z.string())");
}
#[test]
fn test_empty_types_generate() {
let types: Vec<ParsedType> = vec![];
let result = generate(&types).unwrap();
assert!(result.contains("import { z } from \"zod\""));
assert!(result.contains("AUTO-GENERATED"));
}
#[test]
fn test_type_with_no_fields() {
let types = vec![ParsedType {
name: "Empty".to_string(),
fields: vec![],
}];
let result = generate(&types).unwrap();
assert!(result.contains("export const EmptySchema = z.object({"));
assert!(result.contains("});"));
}
#[test]
fn test_field_with_no_validations() {
let mut output = String::new();
let field = ParsedField {
name: "plain".to_string(),
ty: FieldType::String,
validation_rules: vec![],
};
generate_field_schema(&mut output, &field).unwrap();
assert_eq!(output, "z.string()");
}
}