Skip to content

Commit b924a23

Browse files
committed
Fix lint
1 parent a97e58c commit b924a23

6 files changed

Lines changed: 353 additions & 5 deletions

File tree

crates/vespera_macro/src/parser/schema/struct_schema.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,4 +552,49 @@ mod tests {
552552
);
553553
assert!(schema.properties.is_some());
554554
}
555+
556+
#[test]
557+
fn test_parse_struct_to_schema_transparent_tuple_wrapper_uses_ref_schema() {
558+
let struct_item: syn::ItemStruct = syn::parse_str(
559+
r"
560+
#[serde(transparent)]
561+
struct Wrapper(User);
562+
",
563+
)
564+
.unwrap();
565+
566+
let mut struct_defs = HashMap::new();
567+
struct_defs.insert("User".to_string(), "struct User { id: i32 }".to_string());
568+
let mut known = HashSet::new();
569+
known.insert("User".to_string());
570+
571+
let schema = parse_struct_to_schema(&struct_item, &known, &struct_defs);
572+
assert!(schema.all_of.is_some());
573+
let all_of = schema.all_of.unwrap();
574+
assert_eq!(all_of.len(), 1);
575+
match &all_of[0] {
576+
SchemaRef::Ref(reference) => {
577+
assert_eq!(reference.ref_path, "#/components/schemas/User");
578+
}
579+
SchemaRef::Inline(_) => {
580+
panic!("expected $ref wrapper for transparent tuple known schema")
581+
}
582+
}
583+
}
584+
585+
#[test]
586+
fn test_parse_struct_to_schema_transparent_multi_field_tuple_falls_back() {
587+
let struct_item: syn::ItemStruct = syn::parse_str(
588+
r"
589+
#[serde(transparent)]
590+
struct Wrapper(String, String);
591+
",
592+
)
593+
.unwrap();
594+
595+
let schema = parse_struct_to_schema(&struct_item, &HashSet::new(), &HashMap::new());
596+
assert_eq!(schema.schema_type, Some(SchemaType::Object));
597+
assert!(schema.properties.is_none());
598+
assert!(schema.all_of.is_none());
599+
}
555600
}

crates/vespera_macro/src/parser/schema/type_schema.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,4 +1470,36 @@ mod tests {
14701470
let schema_ref = parse_type_to_schema_ref(&ty, &HashSet::new(), &HashMap::new());
14711471
assert!(matches!(schema_ref, SchemaRef::Inline(_)));
14721472
}
1473+
1474+
#[test]
1475+
fn test_known_schema_ref_override_returns_inline_ref_schema() {
1476+
let mut known = HashSet::new();
1477+
known.insert("UserSchema".to_string());
1478+
1479+
let mut defs = HashMap::new();
1480+
defs.insert(
1481+
"UserSchema".to_string(),
1482+
r#"
1483+
#[schema(ref = "ExternalUser", nullable)]
1484+
struct UserSchema {
1485+
id: i32,
1486+
}
1487+
"#
1488+
.to_string(),
1489+
);
1490+
1491+
let ty: Type = syn::parse_str("UserSchema").unwrap();
1492+
let schema_ref = parse_type_to_schema_ref(&ty, &known, &defs);
1493+
1494+
match schema_ref {
1495+
SchemaRef::Inline(schema) => {
1496+
assert_eq!(
1497+
schema.ref_path.as_deref(),
1498+
Some("#/components/schemas/ExternalUser")
1499+
);
1500+
assert_eq!(schema.nullable, Some(true));
1501+
}
1502+
SchemaRef::Ref(_) => panic!("expected inline schema ref override"),
1503+
}
1504+
}
14731505
}

crates/vespera_macro/src/schema_impl.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,4 +630,20 @@ struct Config {
630630
let defaults = extract_field_defaults_from_path(&input, Path::new("/dummy.rs"));
631631
assert!(defaults.is_empty(), "Enum should return empty defaults");
632632
}
633+
634+
#[test]
635+
fn test_process_derive_schema_ref_override_excludes_openapi() {
636+
let input: syn::DeriveInput = syn::parse_quote! {
637+
#[derive(Clone)]
638+
#[schema(ref = "ExternalUser")]
639+
struct UserSchema {
640+
id: i32,
641+
}
642+
};
643+
644+
let (metadata, tokens) = process_derive_schema(&input);
645+
assert_eq!(metadata.name, "UserSchema");
646+
assert!(!metadata.include_in_openapi);
647+
assert!(tokens.is_empty());
648+
}
633649
}

crates/vespera_macro/src/schema_macro/mod.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ use crate::{
5454
parser::{extract_default, extract_field_rename, strip_raw_prefix_owned},
5555
};
5656

57+
#[cfg(test)]
58+
struct __VesperaSameFileLookupFixture {
59+
value: i32,
60+
}
61+
5762
fn derive_response_base_name(name: &str) -> String {
5863
for suffix in ["Response", "Request", "Schema"] {
5964
if let Some(stripped) = name.strip_suffix(suffix)
@@ -73,7 +78,17 @@ fn find_same_file_struct_metadata(
7378
return Some(metadata.clone());
7479
}
7580

76-
let file_path = proc_macro2::Span::call_site().local_file()?;
81+
let file_path = proc_macro2::Span::call_site().local_file();
82+
#[cfg(test)]
83+
let file_path = file_path.or_else(|| {
84+
Some(
85+
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
86+
.join("src")
87+
.join("schema_macro")
88+
.join("mod.rs"),
89+
)
90+
});
91+
let file_path = file_path?;
7792
let definition = file_cache::get_struct_definition(&file_path, struct_name)?;
7893
Some(StructMetadata::new(struct_name.to_string(), definition))
7994
}

crates/vespera_macro/src/schema_macro/tests.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,6 +1616,177 @@ pub struct Model {
16161616
assert!(output.contains("name"));
16171617
}
16181618

1619+
#[test]
1620+
fn test_derive_response_base_name_handles_known_suffixes_and_fallback() {
1621+
assert_eq!(derive_response_base_name("UserResponse"), "User");
1622+
assert_eq!(derive_response_base_name("UserRequest"), "User");
1623+
assert_eq!(derive_response_base_name("UserSchema"), "User");
1624+
assert_eq!(derive_response_base_name("User"), "User");
1625+
}
1626+
1627+
#[test]
1628+
fn test_find_same_file_struct_metadata_reads_test_fixture_from_current_module() {
1629+
let storage: HashMap<String, StructMetadata> = HashMap::new();
1630+
let metadata = find_same_file_struct_metadata("__VesperaSameFileLookupFixture", &storage)
1631+
.expect("fixture should be found in schema_macro/mod.rs");
1632+
1633+
assert_eq!(metadata.name, "__VesperaSameFileLookupFixture");
1634+
assert!(
1635+
metadata
1636+
.definition
1637+
.contains("__VesperaSameFileLookupFixture")
1638+
);
1639+
assert!(metadata.definition.contains("value"));
1640+
}
1641+
1642+
#[test]
1643+
fn test_has_derive_ignores_non_derive_attrs_and_detects_requested_derive() {
1644+
let struct_item: syn::ItemStruct = syn::parse_str(
1645+
r#"
1646+
#[serde(rename_all = "camelCase")]
1647+
#[derive(Clone, Debug)]
1648+
struct Sample {
1649+
value: i32,
1650+
}
1651+
"#,
1652+
)
1653+
.unwrap();
1654+
1655+
assert!(has_derive(&struct_item, "Clone"));
1656+
assert!(!has_derive(&struct_item, "Deserialize"));
1657+
}
1658+
1659+
#[test]
1660+
fn test_build_named_struct_field_assignments_rejects_tuple_structs() {
1661+
let struct_item: syn::ItemStruct = syn::parse_str("struct TupleDto(String);").unwrap();
1662+
let source_expr = quote!(source);
1663+
let error = build_named_struct_field_assignments(&struct_item, &source_expr).unwrap_err();
1664+
assert!(error.to_string().contains("named-field struct"));
1665+
}
1666+
1667+
#[test]
1668+
fn test_build_proxy_fields_rejects_tuple_structs() {
1669+
let struct_item: syn::ItemStruct = syn::parse_str("struct TupleDto(String);").unwrap();
1670+
let error = build_proxy_fields(&struct_item).unwrap_err();
1671+
assert!(error.to_string().contains("named-field struct"));
1672+
}
1673+
1674+
#[test]
1675+
fn test_build_proxy_to_dto_assignments_rejects_tuple_structs() {
1676+
let struct_item: syn::ItemStruct = syn::parse_str("struct TupleDto(String);").unwrap();
1677+
let error = build_proxy_to_dto_assignments(&struct_item).unwrap_err();
1678+
assert!(error.to_string().contains("named-field struct"));
1679+
}
1680+
1681+
#[test]
1682+
fn test_build_clone_assignments_rejects_tuple_structs() {
1683+
let struct_item: syn::ItemStruct = syn::parse_str("struct TupleDto(String);").unwrap();
1684+
let error = build_clone_assignments(&struct_item).unwrap_err();
1685+
assert!(error.to_string().contains("named-field struct"));
1686+
}
1687+
1688+
#[test]
1689+
fn test_maybe_generate_same_file_relation_override_returns_none_when_dto_is_missing() {
1690+
let rel_info = RelationFieldInfo {
1691+
field_name: syn::Ident::new("user", proc_macro2::Span::call_site()),
1692+
relation_type: "HasOne".to_string(),
1693+
schema_path: quote!(crate::models::user::Schema),
1694+
is_optional: true,
1695+
inline_type_info: None,
1696+
relation_enum: None,
1697+
fk_column: None,
1698+
via_rel: None,
1699+
};
1700+
1701+
let storage: HashMap<String, StructMetadata> = HashMap::new();
1702+
let new_type_name = syn::Ident::new("ArticleResponse", proc_macro2::Span::call_site());
1703+
1704+
let result =
1705+
maybe_generate_same_file_relation_override(&new_type_name, "user", &rel_info, &storage)
1706+
.expect("missing dto should not error");
1707+
assert!(result.is_none());
1708+
}
1709+
1710+
#[test]
1711+
fn test_maybe_generate_same_file_relation_override_returns_none_for_invalid_model_type() {
1712+
let rel_info = RelationFieldInfo {
1713+
field_name: syn::Ident::new("user", proc_macro2::Span::call_site()),
1714+
relation_type: "HasOne".to_string(),
1715+
schema_path: quote!(?),
1716+
is_optional: true,
1717+
inline_type_info: None,
1718+
relation_enum: None,
1719+
fk_column: None,
1720+
via_rel: None,
1721+
};
1722+
1723+
let storage = to_storage(vec![create_test_struct_metadata(
1724+
"UserInArticle",
1725+
"struct UserInArticle { id: i32 }",
1726+
)]);
1727+
let new_type_name = syn::Ident::new("ArticleResponse", proc_macro2::Span::call_site());
1728+
1729+
let result =
1730+
maybe_generate_same_file_relation_override(&new_type_name, "user", &rel_info, &storage)
1731+
.expect("invalid model type should not error");
1732+
assert!(result.is_none());
1733+
}
1734+
1735+
#[test]
1736+
fn test_generate_schema_type_code_normal_mode_relation_rename_and_custom_name() {
1737+
let storage = to_storage(vec![create_test_struct_metadata(
1738+
"Model",
1739+
r#"#[sea_orm(table_name = "articles")]
1740+
pub struct Model {
1741+
pub id: i32,
1742+
pub name: String,
1743+
pub owner: HasOne<super::user::Entity>
1744+
}"#,
1745+
)]);
1746+
1747+
let tokens = quote!(
1748+
ArticleResponse from Model,
1749+
name = "CustomArticleSchema",
1750+
rename = [("name", "display_name")]
1751+
);
1752+
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
1753+
let result = generate_schema_type_code(&input, &storage);
1754+
1755+
assert!(result.is_ok());
1756+
let (tokens, metadata) = result.unwrap();
1757+
let output = tokens.to_string();
1758+
assert!(output.contains("display_name"));
1759+
assert!(output.contains("owner"));
1760+
assert!(output.contains("Clone"));
1761+
assert!(output.contains("CustomArticleSchema"));
1762+
assert_eq!(metadata.unwrap().name, "CustomArticleSchema");
1763+
}
1764+
1765+
#[test]
1766+
fn test_generate_schema_type_code_multipart_with_add_and_custom_name() {
1767+
let storage = to_storage(vec![create_test_struct_metadata(
1768+
"Upload",
1769+
"pub struct Upload { pub id: i32, pub name: String }",
1770+
)]);
1771+
1772+
let tokens = quote!(
1773+
UploadForm from Upload,
1774+
multipart,
1775+
name = "UploadFormSchema",
1776+
add = [("extra": String)]
1777+
);
1778+
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
1779+
let result = generate_schema_type_code(&input, &storage);
1780+
1781+
assert!(result.is_ok());
1782+
let (tokens, metadata) = result.unwrap();
1783+
let output = tokens.to_string();
1784+
assert!(output.contains("vespera :: Multipart"));
1785+
assert!(output.contains("extra"));
1786+
assert!(output.contains("UploadFormSchema"));
1787+
assert_eq!(metadata.unwrap().name, "UploadFormSchema");
1788+
}
1789+
16191790
// ============================================================
16201791
// Tests for BelongsTo/HasOne circular reference inline types
16211792
// ============================================================

0 commit comments

Comments
 (0)