use std::collections::HashMap;
use serial_test::serial;
use super::*;
fn create_test_struct_metadata(name: &str, definition: &str) -> StructMetadata {
StructMetadata::new(name.to_string(), definition.to_string())
}
fn to_storage(items: Vec<StructMetadata>) -> HashMap<String, StructMetadata> {
items.into_iter().map(|s| (s.name.clone(), s)).collect()
}
#[test]
fn test_generate_schema_code_simple_struct() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(User);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_ok());
let output = result.unwrap().to_string();
assert!(output.contains("properties"));
assert!(output.contains("Schema"));
}
#[test]
fn test_generate_schema_code_with_omit() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub password: String }",
)]);
let tokens = quote!(User, omit = ["password"]);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_ok());
let output = result.unwrap().to_string();
assert!(output.contains("properties"));
}
#[test]
fn test_generate_schema_code_with_pick() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub email: String }",
)]);
let tokens = quote!(User, pick = ["id", "name"]);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_ok());
let output = result.unwrap().to_string();
assert!(output.contains("properties"));
}
#[test]
fn test_generate_schema_code_type_not_found() {
let storage: HashMap<String, StructMetadata> = HashMap::new();
let tokens = quote!(NonExistent);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not found"));
}
#[test]
fn test_generate_schema_code_malformed_definition() {
let storage = to_storage(vec![create_test_struct_metadata(
"BadStruct",
"this is not valid rust code {{{",
)]);
let tokens = quote!(BadStruct);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("failed to parse"));
}
#[test]
fn test_generate_schema_type_code_pick_nonexistent_field() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(NewUser from User, pick = ["nonexistent"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("does not exist"));
assert!(err.contains("nonexistent"));
}
#[test]
fn test_generate_schema_type_code_omit_nonexistent_field() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(NewUser from User, omit = ["nonexistent"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("does not exist"));
assert!(err.contains("nonexistent"));
}
#[test]
fn test_generate_schema_type_code_rename_nonexistent_field() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(NewUser from User, rename = [("nonexistent", "new_name")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("does not exist"));
assert!(err.contains("nonexistent"));
}
#[test]
fn test_generate_schema_type_code_type_not_found() {
let storage: HashMap<String, StructMetadata> = HashMap::new();
let tokens = quote!(NewUser from NonExistent);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not found"));
}
#[test]
fn test_generate_schema_type_code_success() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(CreateUser from User, pick = ["name"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("CreateUser"));
assert!(output.contains("name"));
}
#[test]
fn test_generate_schema_type_code_with_omit() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub password: String }",
)]);
let tokens = quote!(SafeUser from User, omit = ["password"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("SafeUser"));
assert!(!output.contains("password"));
}
#[test]
fn test_generate_schema_type_code_with_add() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UserWithExtra from User, add = [("extra": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("UserWithExtra"));
assert!(output.contains("extra"));
}
#[test]
fn test_generate_schema_type_code_generates_from_impl() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UserResponse from User, pick = ["id", "name"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("impl From"));
assert!(output.contains("for UserResponse"));
}
#[test]
fn test_generate_schema_type_code_no_from_impl_with_add() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UserWithExtra from User, add = [("extra": String)]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(
output.contains("UserWithExtra"),
"expected struct UserWithExtra in output: {output}"
);
assert!(
!output.contains("impl From"),
"expected no From impl when `add` is used: {output}"
);
}
#[test]
fn test_is_parseable_type_primitives() {
for ty_str in &[
"i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
"f32", "f64", "bool", "String", "Decimal",
] {
let ty: syn::Type = syn::parse_str(ty_str).unwrap();
assert!(is_parseable_type(&ty), "{ty_str} should be parseable");
}
}
#[test]
fn test_is_parseable_type_non_parseable() {
let ty: syn::Type = syn::parse_str("MyEnum").unwrap();
assert!(!is_parseable_type(&ty));
}
#[test]
fn test_is_parseable_type_non_path() {
let ty: syn::Type = syn::parse_str("&str").unwrap();
assert!(!is_parseable_type(&ty));
}
#[test]
fn test_sea_orm_default_attrs_optional_field_skips() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "42")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("i32").unwrap();
let mut fns = Vec::new();
let (serde, schema) =
generate_sea_orm_default_attrs(&attrs, &struct_name, "count", &ty, &ty, true, &mut fns);
assert!(serde.is_empty());
assert!(schema.is_empty());
assert!(fns.is_empty());
}
#[test]
fn test_sea_orm_default_attrs_no_default_and_no_pk() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(unique)])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("String").unwrap();
let mut fns = Vec::new();
let (serde, schema) =
generate_sea_orm_default_attrs(&attrs, &struct_name, "email", &ty, &ty, false, &mut fns);
assert!(serde.is_empty());
assert!(schema.is_empty());
assert!(fns.is_empty());
}
#[test]
fn test_sea_orm_default_attrs_primary_key_generates_defaults() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(primary_key)])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("i32").unwrap();
let mut fns = Vec::new();
let (serde, schema) =
generate_sea_orm_default_attrs(&attrs, &struct_name, "id", &ty, &ty, false, &mut fns);
let serde_str = serde.to_string();
assert!(
serde_str.contains("serde"),
"primary_key should generate serde default: {serde_str}"
);
let schema_str = schema.to_string();
assert!(
schema_str.contains('0'),
"primary_key i32 should have schema default 0: {schema_str}"
);
assert_eq!(fns.len(), 1, "should generate a default function");
}
#[test]
fn test_sea_orm_default_attrs_sql_function_generates_defaults() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "NOW()")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("DateTimeWithTimeZone").unwrap();
let mut fns = Vec::new();
let (serde, schema) = generate_sea_orm_default_attrs(
&attrs,
&struct_name,
"created_at",
&ty,
&ty,
false,
&mut fns,
);
let serde_str = serde.to_string();
assert!(
serde_str.contains("serde"),
"SQL function default should generate serde default: {serde_str}"
);
let schema_str = schema.to_string();
assert!(
schema_str.contains("1970-01-01"),
"DateTimeWithTimeZone should have epoch default: {schema_str}"
);
assert_eq!(fns.len(), 1, "should generate a default function");
}
#[test]
fn test_sea_orm_default_attrs_sql_function_uuid() {
let attrs: Vec<syn::Attribute> =
vec![syn::parse_quote!(#[sea_orm(primary_key, default_value = "gen_random_uuid()")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("Uuid").unwrap();
let mut fns = Vec::new();
let (serde, schema) =
generate_sea_orm_default_attrs(&attrs, &struct_name, "id", &ty, &ty, false, &mut fns);
let serde_str = serde.to_string();
assert!(
serde_str.contains("serde"),
"UUID SQL default should generate serde default: {serde_str}"
);
let schema_str = schema.to_string();
assert!(
schema_str.contains("00000000-0000-0000-0000-000000000000"),
"Uuid should have nil UUID default: {schema_str}"
);
assert_eq!(fns.len(), 1);
}
#[test]
fn test_sea_orm_default_attrs_sql_function_unknown_type_skips() {
let attrs: Vec<syn::Attribute> =
vec![syn::parse_quote!(#[sea_orm(default_value = "SOME_FUNC()")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("MyCustomType").unwrap();
let mut fns = Vec::new();
let (serde, schema) =
generate_sea_orm_default_attrs(&attrs, &struct_name, "field", &ty, &ty, false, &mut fns);
assert!(serde.is_empty(), "unknown type should skip serde default");
assert!(schema.is_empty(), "unknown type should skip schema default");
assert!(fns.is_empty());
}
#[test]
fn test_sea_orm_default_attrs_existing_serde_default() {
let attrs: Vec<syn::Attribute> = vec![
syn::parse_quote!(#[sea_orm(default_value = "42")]),
syn::parse_quote!(#[serde(default)]),
];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("i32").unwrap();
let mut fns = Vec::new();
let (serde, schema) =
generate_sea_orm_default_attrs(&attrs, &struct_name, "count", &ty, &ty, false, &mut fns);
assert!(serde.is_empty());
let schema_str = schema.to_string();
assert!(
schema_str.contains("schema"),
"should have schema attr: {schema_str}"
);
assert!(
fns.is_empty(),
"no default fn needed when serde(default) exists"
);
}
#[test]
fn test_sea_orm_default_attrs_non_parseable_type() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "Active")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("MyEnum").unwrap();
let mut fns = Vec::new();
let (serde, schema) =
generate_sea_orm_default_attrs(&attrs, &struct_name, "status", &ty, &ty, false, &mut fns);
assert!(serde.is_empty());
let schema_str = schema.to_string();
assert!(
schema_str.contains("schema"),
"should have schema attr: {schema_str}"
);
assert!(fns.is_empty());
}
#[test]
fn test_sea_orm_default_attrs_full_generation() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "42")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("i32").unwrap();
let mut fns = Vec::new();
let (serde, schema) =
generate_sea_orm_default_attrs(&attrs, &struct_name, "count", &ty, &ty, false, &mut fns);
let serde_str = serde.to_string();
assert!(
serde_str.contains("serde"),
"should have serde attr: {serde_str}"
);
assert!(
serde_str.contains("default_Test_count"),
"should reference generated fn: {serde_str}"
);
let schema_str = schema.to_string();
assert!(
schema_str.contains("schema"),
"should have schema attr: {schema_str}"
);
assert_eq!(fns.len(), 1, "should generate one default function");
let fn_str = fns[0].to_string();
assert!(
fn_str.contains("default_Test_count"),
"fn name should match: {fn_str}"
);
}
#[test]
fn test_generate_schema_type_code_with_partial_all() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub bio: Option<String> }",
)]);
let tokens = quote!(UpdateUser from User, partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("Option < i32 >"));
assert!(output.contains("Option < String >"));
}
#[test]
fn test_generate_schema_type_code_with_partial_fields() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String, pub email: String }",
)]);
let tokens = quote!(UpdateUser from User, partial = ["name"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(
output.contains("UpdateUser"),
"should contain generated struct name: {output}"
);
}
#[test]
fn test_generate_schema_type_code_with_omit_default() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
r#"#[sea_orm(table_name = "items")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
#[sea_orm(default_value = "NOW()")]
pub created_at: DateTimeWithTimeZone,
}"#,
)]);
let tokens = quote!(CreateItemRequest from Model, omit_default);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(
!output.contains("id :"),
"id should be omitted by omit_default: {output}"
);
assert!(
!output.contains("created_at"),
"created_at should be omitted by omit_default: {output}"
);
assert!(output.contains("name"), "name should remain: {output}");
}
#[test]
fn test_sea_orm_default_attrs_sql_function_with_existing_serde_default() {
let attrs: Vec<syn::Attribute> = vec![
syn::parse_quote!(#[sea_orm(default_value = "NOW()")]),
syn::parse_quote!(#[serde(default)]),
];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("DateTimeWithTimeZone").unwrap();
let mut fns = Vec::new();
let (serde, schema) = generate_sea_orm_default_attrs(
&attrs,
&struct_name,
"created_at",
&ty,
&ty,
false,
&mut fns,
);
assert!(serde.is_empty());
let schema_str = schema.to_string();
assert!(
schema_str.contains("schema"),
"should have schema attr: {schema_str}"
);
assert!(
schema_str.contains("1970-01-01"),
"should have epoch default: {schema_str}"
);
assert!(
fns.is_empty(),
"no default fn needed when serde(default) exists"
);
}
#[test]
fn test_sea_orm_default_attrs_sql_function_non_path_type() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "NOW()")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("&str").unwrap();
let mut fns = Vec::new();
let (serde, schema) =
generate_sea_orm_default_attrs(&attrs, &struct_name, "field", &ty, &ty, false, &mut fns);
assert!(serde.is_empty(), "non-Path type should skip serde default");
assert!(
schema.is_empty(),
"non-Path type should skip schema default"
);
assert!(fns.is_empty());
}
#[test]
fn test_sea_orm_default_attrs_sql_function_datetime() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "NOW()")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("DateTime").unwrap();
let mut fns = Vec::new();
let (serde, schema) = generate_sea_orm_default_attrs(
&attrs,
&struct_name,
"created_at",
&ty,
&ty,
false,
&mut fns,
);
let serde_str = serde.to_string();
assert!(
serde_str.contains("serde"),
"DateTime should generate serde default: {serde_str}"
);
let schema_str = schema.to_string();
assert!(
schema_str.contains("1970-01-01T00:00:00+00:00"),
"DateTime should have epoch default: {schema_str}"
);
assert_eq!(fns.len(), 1);
}
#[test]
fn test_sea_orm_default_attrs_sql_function_naive_datetime() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "NOW()")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("NaiveDateTime").unwrap();
let mut fns = Vec::new();
let (serde, schema) = generate_sea_orm_default_attrs(
&attrs,
&struct_name,
"created_at",
&ty,
&ty,
false,
&mut fns,
);
let serde_str = serde.to_string();
assert!(
serde_str.contains("serde"),
"NaiveDateTime should generate serde default: {serde_str}"
);
let schema_str = schema.to_string();
assert!(
schema_str.contains("1970-01-01T00:00:00"),
"NaiveDateTime should have epoch default: {schema_str}"
);
assert_eq!(fns.len(), 1);
}
#[test]
fn test_sea_orm_default_attrs_sql_function_naive_date() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "NOW()")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("NaiveDate").unwrap();
let mut fns = Vec::new();
let (serde, schema) = generate_sea_orm_default_attrs(
&attrs,
&struct_name,
"date_field",
&ty,
&ty,
false,
&mut fns,
);
let serde_str = serde.to_string();
assert!(
serde_str.contains("serde"),
"NaiveDate should generate serde default: {serde_str}"
);
let schema_str = schema.to_string();
assert!(
schema_str.contains("1970-01-01"),
"NaiveDate should have date default: {schema_str}"
);
assert_eq!(fns.len(), 1);
}
#[test]
fn test_sea_orm_default_attrs_sql_function_naive_time() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "NOW()")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("NaiveTime").unwrap();
let mut fns = Vec::new();
let (serde, schema) = generate_sea_orm_default_attrs(
&attrs,
&struct_name,
"time_field",
&ty,
&ty,
false,
&mut fns,
);
let serde_str = serde.to_string();
assert!(
serde_str.contains("serde"),
"NaiveTime should generate serde default: {serde_str}"
);
let schema_str = schema.to_string();
assert!(
schema_str.contains("00:00:00"),
"NaiveTime should have time default: {schema_str}"
);
assert_eq!(fns.len(), 1);
}
#[test]
fn test_sea_orm_default_attrs_sql_function_time_type() {
let attrs: Vec<syn::Attribute> = vec![syn::parse_quote!(#[sea_orm(default_value = "NOW()")])];
let struct_name = syn::Ident::new("Test", proc_macro2::Span::call_site());
let ty: syn::Type = syn::parse_str("Time").unwrap();
let mut fns = Vec::new();
let (serde, schema) = generate_sea_orm_default_attrs(
&attrs,
&struct_name,
"time_field",
&ty,
&ty,
false,
&mut fns,
);
let serde_str = serde.to_string();
assert!(
serde_str.contains("serde"),
"Time should generate serde default: {serde_str}"
);
let schema_str = schema.to_string();
assert!(
schema_str.contains("00:00:00"),
"Time should have time default: {schema_str}"
);
assert_eq!(fns.len(), 1);
}
#[test]
fn test_is_parseable_type_empty_segments() {
let ty = syn::Type::Path(syn::TypePath {
qself: None,
path: syn::Path {
leading_colon: None,
segments: syn::punctuated::Punctuated::new(),
},
});
assert!(!is_parseable_type(&ty));
}
#[test]
fn test_generate_schema_type_code_partial_nonexistent_field() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UpdateUser from User, partial = ["nonexistent"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("does not exist"));
assert!(err.contains("nonexistent"));
}
#[test]
fn test_generate_schema_type_code_partial_from_impl_wraps_some() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UpdateUser from User, partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("Some (source . id)"));
assert!(output.contains("Some (source . name)"));
}
#[test]
fn test_generate_schema_type_code_preserves_struct_doc() {
let input = SchemaTypeInput {
new_type: syn::Ident::new("NewUser", proc_macro2::Span::call_site()),
source_type: syn::parse_str("User").unwrap(),
omit: None,
pick: None,
rename: None,
add: None,
derive_clone: true,
partial: None,
schema_name: None,
ignore_schema: false,
rename_all: None,
multipart: false,
omit_default: false,
};
let struct_def = StructMetadata {
name: "User".to_string(),
definition: r"
/// User struct documentation
pub struct User {
/// The user ID
pub id: i32,
/// The user name
pub name: String,
}
"
.to_string(),
include_in_openapi: true,
field_defaults: std::collections::BTreeMap::new(),
};
let storage = to_storage(vec![struct_def]);
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _) = result.unwrap();
let tokens_str = tokens.to_string();
assert!(tokens_str.contains("User struct documentation") || tokens_str.contains("doc"));
}
#[test]
fn test_generate_schema_type_code_inherits_source_rename_all() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
r#"#[serde(rename_all = "snake_case")]
pub struct User { pub id: i32, pub user_name: String }"#,
)]);
let tokens = quote!(UserResponse from User);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("rename_all"));
assert!(output.contains("snake_case"));
}
#[test]
fn test_generate_schema_type_code_override_rename_all() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
r#"#[serde(rename_all = "snake_case")]
pub struct User { pub id: i32, pub user_name: String }"#,
)]);
let tokens = quote!(UserResponse from User, rename_all = "camelCase");
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("camelCase"));
}
#[test]
fn test_generate_schema_type_code_with_rename() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UserDTO from User, rename = [("id", "user_id")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("user_id"));
assert!(output.contains("From"));
}
#[test]
fn test_generate_schema_type_code_rename_preserves_serde_rename() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
r#"pub struct User {
pub id: i32,
#[serde(rename = "userName")]
pub name: String
}"#,
)]);
let tokens = quote!(UserDTO from User, rename = [("name", "user_name")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("user_name"));
assert!(output.contains("userName") || output.contains("rename"));
}
#[test]
fn test_generate_schema_type_code_with_ignore_schema() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UserInternal from User, ignore);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(!output.contains("vespera :: Schema"));
}
#[test]
fn test_generate_schema_type_code_with_custom_name() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UserResponse from User, name = "CustomUserSchema");
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("schema"));
assert!(output.contains("CustomUserSchema"));
assert!(metadata.is_some());
let meta = metadata.unwrap();
assert_eq!(meta.name, "CustomUserSchema");
}
#[test]
fn test_generate_schema_type_code_with_clone_false() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UserNonClone from User, clone = false);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(!output.contains("Clone ,"));
}
#[test]
fn test_generate_schema_type_code_seaorm_model_detection() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
r#"#[sea_orm(table_name = "users")]
pub struct Model { pub id: i32, pub name: String }"#,
)]);
let tokens = quote!(UserSchema from Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("UserSchema"));
}
#[test]
fn test_generate_schema_type_code_tuple_struct() {
let storage = to_storage(vec![create_test_struct_metadata(
"Point",
"pub struct Point(pub i32, pub i32);",
)]);
let tokens = quote!(PointDTO from Point);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("PointDTO"));
}
#[test]
fn test_generate_schema_type_code_raw_identifier_field() {
let storage = to_storage(vec![create_test_struct_metadata(
"Config",
"pub struct Config { pub id: i32, pub r#type: String }",
)]);
let tokens = quote!(ConfigDTO from Config);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("ConfigDTO"));
}
#[test]
fn test_generate_schema_type_code_partial_no_double_option() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
"pub struct User { pub id: i32, pub bio: Option<String> }",
)]);
let tokens = quote!(UpdateUser from User, partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(!output.contains("Option < Option"));
}
#[test]
fn test_generate_schema_code_excludes_serde_skip_fields() {
let storage = to_storage(vec![create_test_struct_metadata(
"User",
r"pub struct User {
pub id: i32,
#[serde(skip)]
pub internal_state: String,
pub name: String
}",
)]);
let tokens = quote!(User);
let input: SchemaInput = syn::parse2(tokens).unwrap();
let result = generate_schema_code(&input, &storage);
assert!(result.is_ok());
let output = result.unwrap().to_string();
assert!(!output.contains("internal_state"));
assert!(output.contains("name"));
}
#[test]
fn test_generate_schema_type_code_qualified_path_storage_lookup() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
"pub struct Model { pub id: i32, pub name: String }",
)]);
let tokens = quote!(UserSchema from crate::models::user::Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("UserSchema"));
}
#[test]
fn test_generate_schema_type_code_qualified_path_not_found() {
let storage: HashMap<String, StructMetadata> = HashMap::new();
let tokens = quote!(UserSchema from crate::models::user::NonExistent);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not found"));
}
#[test]
fn test_generate_schema_type_code_has_many_excluded_by_default() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
r#"#[sea_orm(table_name = "users")]
pub struct Model {
pub id: i32,
pub name: String,
pub memos: HasMany<super::memo::Entity>
}"#,
)]);
let tokens = quote!(UserSchema from Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(!output.contains("memos"));
assert!(output.contains("name"));
}
#[test]
fn test_generate_schema_type_code_relation_conversion_failure() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
r#"#[sea_orm(table_name = "users")]
pub struct Model {
pub id: i32,
pub name: String,
pub broken: HasMany
}"#,
)]);
let tokens = quote!(UserSchema from Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(!output.contains("broken"));
assert!(output.contains("name"));
}
#[test]
fn test_generate_schema_type_code_belongs_to_relation() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
r#"#[sea_orm(table_name = "memos")]
pub struct Model {
pub id: i32,
pub user_id: i32,
#[sea_orm(belongs_to = "super::user::Entity", from = "user_id")]
pub user: BelongsTo<super::user::Entity>
}"#,
)]);
let tokens = quote!(MemoSchema from Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("user"));
}
#[test]
fn test_generate_schema_type_code_has_one_relation() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
r#"#[sea_orm(table_name = "users")]
pub struct Model {
pub id: i32,
pub name: String,
pub profile: HasOne<super::profile::Entity>
}"#,
)]);
let tokens = quote!(UserSchema from Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("profile"));
}
#[test]
fn test_generate_schema_type_code_seaorm_model_with_relation_generates_from_model() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
r#"#[sea_orm(table_name = "memos")]
pub struct Model {
pub id: i32,
pub title: String,
pub user: BelongsTo<super::user::Entity>
}"#,
)]);
let tokens = quote!(MemoSchema from Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("user"));
}
#[test]
fn test_generate_schema_type_code_from_model_generation() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
r#"#[sea_orm(table_name = "memos")]
pub struct Model {
pub id: i32,
pub user: BelongsTo<super::user::Entity>
}"#,
)]);
let tokens = quote!(MemoSchema from Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("user"));
}
#[test]
#[serial]
fn test_generate_schema_type_code_qualified_path_file_lookup_success() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let user_model = r"
pub struct Model {
pub id: i32,
pub name: String,
pub email: String,
}
";
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let tokens = quote!(UserSchema from crate::models::user::Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let storage: HashMap<String, StructMetadata> = HashMap::new();
let result = generate_schema_type_code(&input, &storage);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("UserSchema"));
assert!(output.contains("id"));
assert!(output.contains("name"));
assert!(output.contains("email"));
}
#[test]
#[serial]
fn test_generate_schema_type_code_simple_name_file_lookup_fallback() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let user_model = r"
pub struct Model {
pub id: i32,
pub username: String,
}
";
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let tokens = quote!(Schema from Model, name = "UserSchema");
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let storage: HashMap<String, StructMetadata> = HashMap::new();
let result = generate_schema_type_code(&input, &storage);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_ok());
let (tokens, metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("Schema"));
assert!(output.contains("id"));
assert!(output.contains("username"));
assert!(metadata.is_some());
assert_eq!(metadata.unwrap().name, "UserSchema");
}
#[test]
#[serial]
fn test_generate_schema_type_code_has_many_explicit_pick_inline_type() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let memo_model = r"
pub struct Model {
pub id: i32,
pub title: String,
pub content: String,
}
";
std::fs::write(models_dir.join("memo.rs"), memo_model).unwrap();
let user_model = r#"
#[sea_orm(table_name = "users")]
pub struct Model {
pub id: i32,
pub name: String,
pub memos: HasMany<super::memo::Entity>,
}
"#;
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let tokens = quote!(UserSchema from crate::models::user::Model, pick = ["id", "name", "memos"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let storage: HashMap<String, StructMetadata> = HashMap::new();
let result = generate_schema_type_code(&input, &storage);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("UserSchema"));
assert!(output.contains("memos"));
assert!(output.contains("Vec <"));
}
#[test]
#[serial]
fn test_generate_schema_type_code_has_many_explicit_pick_file_not_found() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let user_model = r#"
#[sea_orm(table_name = "users")]
pub struct Model {
pub id: i32,
pub name: String,
pub items: HasMany<super::nonexistent::Entity>,
}
"#;
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let tokens = quote!(UserSchema from crate::models::user::Model, pick = ["id", "name", "items"]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let storage: HashMap<String, StructMetadata> = HashMap::new();
let result = generate_schema_type_code(&input, &storage);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(!output.contains("items"));
assert!(output.contains("id"));
assert!(output.contains("name"));
}
#[test]
#[serial]
fn test_generate_schema_type_code_belongs_to_circular_inline_optional() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let user_model = r#"
#[sea_orm(table_name = "users")]
pub struct Model {
pub id: i32,
pub name: String,
pub memo: BelongsTo<super::memo::Entity>,
}
"#;
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let memo_model = r#"
#[sea_orm(table_name = "memos")]
pub struct Model {
pub id: i32,
pub title: String,
pub user_id: i32,
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("memo.rs"), memo_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let tokens = quote!(MemoSchema from crate::models::memo::Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let storage: HashMap<String, StructMetadata> = HashMap::new();
let result = generate_schema_type_code(&input, &storage);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("MemoSchema"));
assert!(output.contains("user"));
assert!(output.contains("Option < Box <"));
}
#[test]
#[serial]
fn test_generate_schema_type_code_has_one_circular_inline_required() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let profile_model = r#"
#[sea_orm(table_name = "profiles")]
pub struct Model {
pub id: i32,
pub bio: String,
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("profile.rs"), profile_model).unwrap();
let user_model = r#"
#[sea_orm(table_name = "users")]
pub struct Model {
pub id: i32,
pub name: String,
pub profile_id: i32,
#[sea_orm(has_one = "super::profile::Entity", from = "profile_id")]
pub profile: HasOne<super::profile::Entity>,
}
"#;
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let tokens = quote!(UserSchema from crate::models::user::Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let storage: HashMap<String, StructMetadata> = HashMap::new();
let result = generate_schema_type_code(&input, &storage);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("UserSchema"));
assert!(output.contains("profile"));
assert!(output.contains("Box <"));
}
#[test]
#[serial]
fn test_generate_schema_type_code_belongs_to_circular_inline_required_file() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let user_model = r#"
#[sea_orm(table_name = "users")]
pub struct Model {
pub id: i32,
pub name: String,
pub memo_id: i32,
#[sea_orm(belongs_to, from = "memo_id", to = "id")]
pub memo: BelongsTo<super::memo::Entity>,
}
"#;
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let memo_model = r#"
#[sea_orm(table_name = "memos")]
pub struct Model {
pub id: i32,
pub title: String,
pub user_id: i32,
#[sea_orm(belongs_to, from = "user_id", to = "id")]
pub user: BelongsTo<super::user::Entity>,
}
"#;
std::fs::write(models_dir.join("memo.rs"), memo_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let tokens = quote!(MemoSchema from crate::models::memo::Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let storage: HashMap<String, StructMetadata> = HashMap::new();
let result = generate_schema_type_code(&input, &storage);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_ok(), "Should generate schema: {:?}", result.err());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(
output.contains("MemoSchema"),
"Should contain MemoSchema: {output}"
);
assert!(
output.contains("user"),
"Should contain user field: {output}"
);
assert!(
output.contains("pub user : Box <"),
"BelongsTo with required FK should generate Box<>, not Option<Box<>>. Output: {output}"
);
}
#[test]
fn test_seaorm_relation_required_fk_directly() {
use crate::schema_macro::seaorm::{
convert_relation_type_to_schema_with_info, extract_belongs_to_from_field,
is_field_optional_in_struct,
};
let struct_def = r#"
#[sea_orm(table_name = "memos")]
pub struct Model {
pub id: i32,
pub user_id: i32,
#[sea_orm(belongs_to, from = "user_id", to = "id")]
pub user: BelongsTo<super::user::Entity>,
}
"#;
let parsed_struct: syn::ItemStruct = syn::parse_str(struct_def).unwrap();
let syn::Fields::Named(fields_named) = &parsed_struct.fields else {
panic!("Expected named fields")
};
let user_field = fields_named
.named
.iter()
.find(|f| f.ident.as_ref().is_some_and(|i| i == "user"))
.expect("user field not found");
let fk_field = extract_belongs_to_from_field(&user_field.attrs);
assert_eq!(
fk_field,
Some("user_id".to_string()),
"Should extract FK field from attribute"
);
let is_fk_optional = is_field_optional_in_struct(&parsed_struct, "user_id");
assert!(!is_fk_optional, "user_id: i32 should not be optional");
let result = convert_relation_type_to_schema_with_info(
&user_field.ty,
&user_field.attrs,
&parsed_struct,
&[
"crate".to_string(),
"models".to_string(),
"memo".to_string(),
],
user_field.ident.clone().unwrap(),
);
assert!(result.is_some(), "Should convert BelongsTo relation");
let (_, rel_info) = result.unwrap();
assert_eq!(rel_info.relation_type, "BelongsTo");
assert!(
!rel_info.is_optional,
"BelongsTo with required FK (user_id: i32) should have is_optional = false"
);
}
#[test]
fn test_extract_belongs_to_from_field_with_equals_value() {
use crate::schema_macro::seaorm::extract_belongs_to_from_field;
let attrs1: Vec<syn::Attribute> = vec![syn::parse_quote!(
#[sea_orm(belongs_to, from = "user_id", to = "id")]
)];
let result1 = extract_belongs_to_from_field(&attrs1);
assert_eq!(
result1,
Some("user_id".to_string()),
"Flag style should work"
);
let attrs2: Vec<syn::Attribute> = vec![syn::parse_quote!(
#[sea_orm(belongs_to = "super::user::Entity", from = "user_id", to = "id")]
)];
let result2 = extract_belongs_to_from_field(&attrs2);
assert_eq!(
result2,
Some("user_id".to_string()),
"Value style should also work"
);
}
#[test]
fn test_generate_schema_type_code_multipart_basic() {
let storage = to_storage(vec![create_test_struct_metadata(
"UploadRequest",
"pub struct UploadRequest { pub name: String, pub description: Option<String> }",
)]);
let tokens = quote!(PatchUpload from UploadRequest, multipart);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("Multipart"));
assert!(!output.contains("impl From"));
assert!(output.contains("name"));
assert!(output.contains("description"));
}
#[test]
fn test_generate_schema_type_code_multipart_with_rename() {
let storage = to_storage(vec![create_test_struct_metadata(
"UploadRequest",
"pub struct UploadRequest { pub name: String, pub file_path: String }",
)]);
let tokens = quote!(RenamedUpload from UploadRequest, multipart, rename = [("file_path", "document_path")]);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("Multipart"));
assert!(output.contains("document_path"));
assert!(!output.contains("file_path"));
}
#[test]
fn test_generate_schema_type_code_multipart_with_form_data_attrs() {
let storage = to_storage(vec![create_test_struct_metadata(
"UploadRequest",
r#"pub struct UploadRequest {
pub name: String,
#[form_data(limit = "10MiB")]
pub file: String
}"#,
)]);
let tokens = quote!(PatchUpload from UploadRequest, multipart);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("form_data"));
assert!(output.contains("limit"));
}
#[test]
fn test_generate_schema_type_code_multipart_skips_relations() {
let storage = to_storage(vec![create_test_struct_metadata(
"Model",
r#"#[sea_orm(table_name = "memos")]
pub struct Model {
pub id: i32,
pub title: String,
pub user: BelongsTo<super::user::Entity>
}"#,
)]);
let tokens = quote!(MemoUpload from Model, multipart);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(!output.contains("user"));
assert!(output.contains("id"));
assert!(output.contains("title"));
assert!(output.contains("Multipart"));
}
#[test]
fn test_generate_schema_type_code_multipart_partial() {
let storage = to_storage(vec![create_test_struct_metadata(
"UploadRequest",
"pub struct UploadRequest { pub name: String, pub tags: String }",
)]);
let tokens = quote!(PatchUpload from UploadRequest, multipart, partial);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let result = generate_schema_type_code(&input, &storage);
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("Multipart"));
assert!(output.contains("Option"));
assert!(!output.contains("impl From"));
}
#[test]
#[serial]
fn test_generate_schema_type_code_qualified_path_with_nonempty_module_path() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
let models_dir = src_dir.join("models");
std::fs::create_dir_all(&models_dir).unwrap();
let user_model = r"
pub struct Model {
pub id: i32,
pub name: String,
}
";
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let tokens = quote!(UserSchema from crate::models::user::Model);
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
let storage: HashMap<String, StructMetadata> = HashMap::new();
let result = generate_schema_type_code(&input, &storage);
unsafe {
if let Some(dir) = original_manifest_dir {
std::env::set_var("CARGO_MANIFEST_DIR", dir);
} else {
std::env::remove_var("CARGO_MANIFEST_DIR");
}
}
assert!(result.is_ok());
let (tokens, _metadata) = result.unwrap();
let output = tokens.to_string();
assert!(output.contains("UserSchema"));
}