Skip to content

Commit 4ffd08d

Browse files
authored
Merge pull request #53 from dev-five-git/nested-generic
nested generic
2 parents ff878c4 + 2919bc1 commit 4ffd08d

7 files changed

Lines changed: 612 additions & 29 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespera_core/Cargo.toml":"Patch","crates/vespera/Cargo.toml":"Patch","crates/vespera_macro/Cargo.toml":"Patch"},"note":"Support nested generic","date":"2026-01-26T07:06:46.307922700Z"}

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vespera_macro/src/parser/schema.rs

Lines changed: 209 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -699,35 +699,118 @@ pub fn parse_struct_to_schema(
699699
}
700700

701701
fn substitute_type(ty: &Type, generic_params: &[String], concrete_types: &[&Type]) -> Type {
702-
// Check if this is a generic parameter
703-
if let Type::Path(type_path) = ty
704-
&& let Some(segment) = type_path.path.segments.last()
705-
{
706-
let ident_str = segment.ident.to_string();
707-
if generic_params.contains(&ident_str) && segment.arguments.is_none() {
708-
// Find the index and substitute
709-
if let Some(index) = generic_params.iter().position(|p| p == &ident_str)
710-
&& let Some(concrete_ty) = concrete_types.get(index)
711-
{
712-
return (*concrete_ty).clone();
702+
match ty {
703+
Type::Path(type_path) => {
704+
let path = &type_path.path;
705+
if path.segments.is_empty() {
706+
return ty.clone();
713707
}
714-
}
715-
}
716708

717-
// For complex types, use quote! to regenerate with substitutions
718-
let tokens = quote::quote! { #ty };
719-
let mut new_tokens = tokens.to_string();
709+
// Check if this is a direct generic parameter (e.g., just "T" with no arguments)
710+
if path.segments.len() == 1 {
711+
let segment = &path.segments[0];
712+
let ident_str = segment.ident.to_string();
713+
714+
if let syn::PathArguments::None = &segment.arguments {
715+
// Direct generic parameter substitution
716+
if let Some(index) = generic_params.iter().position(|p| p == &ident_str)
717+
&& let Some(concrete_ty) = concrete_types.get(index) {
718+
return (*concrete_ty).clone();
719+
}
720+
}
721+
}
720722

721-
// Replace generic parameter names with concrete types
722-
for (param, concrete_ty) in generic_params.iter().zip(concrete_types.iter()) {
723-
// Replace standalone generic parameter (not part of another identifier)
724-
let pattern = format!(r"\b{}\b", param);
725-
let replacement = quote::quote! { #concrete_ty }.to_string();
726-
new_tokens = new_tokens.replace(&pattern, &replacement);
727-
}
723+
// For types with generic arguments (e.g., Vec<T>, Option<T>, HashMap<K, V>),
724+
// recursively substitute the type arguments
725+
let mut new_segments = syn::punctuated::Punctuated::new();
726+
for segment in &path.segments {
727+
let new_arguments = match &segment.arguments {
728+
syn::PathArguments::AngleBracketed(args) => {
729+
let mut new_args = syn::punctuated::Punctuated::new();
730+
for arg in &args.args {
731+
let new_arg = match arg {
732+
syn::GenericArgument::Type(inner_ty) => syn::GenericArgument::Type(
733+
substitute_type(inner_ty, generic_params, concrete_types),
734+
),
735+
other => other.clone(),
736+
};
737+
new_args.push(new_arg);
738+
}
739+
syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
740+
colon2_token: args.colon2_token,
741+
lt_token: args.lt_token,
742+
args: new_args,
743+
gt_token: args.gt_token,
744+
})
745+
}
746+
other => other.clone(),
747+
};
748+
749+
new_segments.push(syn::PathSegment {
750+
ident: segment.ident.clone(),
751+
arguments: new_arguments,
752+
});
753+
}
728754

729-
// Parse the substituted type
730-
syn::parse_str::<Type>(&new_tokens).unwrap_or_else(|_| ty.clone())
755+
Type::Path(syn::TypePath {
756+
qself: type_path.qself.clone(),
757+
path: syn::Path {
758+
leading_colon: path.leading_colon,
759+
segments: new_segments,
760+
},
761+
})
762+
}
763+
Type::Reference(type_ref) => {
764+
// Handle &T, &mut T
765+
Type::Reference(syn::TypeReference {
766+
and_token: type_ref.and_token,
767+
lifetime: type_ref.lifetime.clone(),
768+
mutability: type_ref.mutability,
769+
elem: Box::new(substitute_type(
770+
&type_ref.elem,
771+
generic_params,
772+
concrete_types,
773+
)),
774+
})
775+
}
776+
Type::Slice(type_slice) => {
777+
// Handle [T]
778+
Type::Slice(syn::TypeSlice {
779+
bracket_token: type_slice.bracket_token,
780+
elem: Box::new(substitute_type(
781+
&type_slice.elem,
782+
generic_params,
783+
concrete_types,
784+
)),
785+
})
786+
}
787+
Type::Array(type_array) => {
788+
// Handle [T; N]
789+
Type::Array(syn::TypeArray {
790+
bracket_token: type_array.bracket_token,
791+
elem: Box::new(substitute_type(
792+
&type_array.elem,
793+
generic_params,
794+
concrete_types,
795+
)),
796+
semi_token: type_array.semi_token,
797+
len: type_array.len.clone(),
798+
})
799+
}
800+
Type::Tuple(type_tuple) => {
801+
// Handle (T1, T2, ...)
802+
let new_elems = type_tuple
803+
.elems
804+
.iter()
805+
.map(|elem| substitute_type(elem, generic_params, concrete_types))
806+
.collect();
807+
Type::Tuple(syn::TypeTuple {
808+
paren_token: type_tuple.paren_token,
809+
elems: new_elems,
810+
})
811+
}
812+
_ => ty.clone(),
813+
}
731814
}
732815

733816
pub(super) fn is_primitive_type(ty: &Type) -> bool {
@@ -1561,6 +1644,107 @@ mod tests {
15611644
assert_eq!(substituted, ty);
15621645
}
15631646

1647+
#[rstest]
1648+
// Direct generic param substitution
1649+
#[case("T", &["T"], &["String"], "String")]
1650+
// Vec<T> substitution
1651+
#[case("Vec<T>", &["T"], &["String"], "Vec < String >")]
1652+
// Option<T> substitution
1653+
#[case("Option<T>", &["T"], &["i32"], "Option < i32 >")]
1654+
// Nested: Vec<Option<T>>
1655+
#[case("Vec<Option<T>>", &["T"], &["String"], "Vec < Option < String > >")]
1656+
// Deeply nested: Option<Vec<Option<T>>>
1657+
#[case("Option<Vec<Option<T>>>", &["T"], &["bool"], "Option < Vec < Option < bool > > >")]
1658+
// Multiple generic params
1659+
#[case("HashMap<K, V>", &["K", "V"], &["String", "i32"], "HashMap < String , i32 >")]
1660+
// Generic param not in list (unchanged)
1661+
#[case("Vec<U>", &["T"], &["String"], "Vec < U >")]
1662+
// Non-generic type (unchanged)
1663+
#[case("String", &["T"], &["i32"], "String")]
1664+
// Reference type: &T
1665+
#[case("&T", &["T"], &["String"], "& String")]
1666+
// Mutable reference: &mut T
1667+
#[case("&mut T", &["T"], &["i32"], "& mut i32")]
1668+
// Slice type: [T]
1669+
#[case("[T]", &["T"], &["String"], "[String]")]
1670+
// Array type: [T; 5]
1671+
#[case("[T; 5]", &["T"], &["u8"], "[u8 ; 5]")]
1672+
// Tuple type: (T, U)
1673+
#[case("(T, U)", &["T", "U"], &["String", "i32"], "(String , i32)")]
1674+
// Complex nested tuple
1675+
#[case("(Vec<T>, Option<U>)", &["T", "U"], &["String", "bool"], "(Vec < String > , Option < bool >)")]
1676+
// Reference to Vec<T>
1677+
#[case("&Vec<T>", &["T"], &["String"], "& Vec < String >")]
1678+
// Multi-segment path (no substitution for crate::Type)
1679+
#[case("std::vec::Vec<T>", &["T"], &["String"], "std :: vec :: Vec < String >")]
1680+
fn test_substitute_type_comprehensive(
1681+
#[case] input: &str,
1682+
#[case] params: &[&str],
1683+
#[case] concrete: &[&str],
1684+
#[case] expected: &str,
1685+
) {
1686+
let ty: Type = syn::parse_str(input).unwrap();
1687+
let generic_params: Vec<String> = params.iter().map(|s| s.to_string()).collect();
1688+
let concrete_types: Vec<Type> = concrete.iter().map(|s| syn::parse_str(s).unwrap()).collect();
1689+
let concrete_refs: Vec<&Type> = concrete_types.iter().collect();
1690+
1691+
let result = substitute_type(&ty, &generic_params, &concrete_refs);
1692+
let result_str = quote::quote!(#result).to_string();
1693+
1694+
assert_eq!(result_str, expected, "Input: {}", input);
1695+
}
1696+
1697+
#[test]
1698+
fn test_substitute_type_empty_path_segments() {
1699+
// Create a TypePath with empty segments
1700+
let ty = Type::Path(syn::TypePath {
1701+
qself: None,
1702+
path: syn::Path {
1703+
leading_colon: None,
1704+
segments: syn::punctuated::Punctuated::new(),
1705+
},
1706+
});
1707+
let concrete: Type = syn::parse_str("String").unwrap();
1708+
let result = substitute_type(&ty, &[String::from("T")], &[&concrete]);
1709+
// Should return the original type unchanged
1710+
assert_eq!(result, ty);
1711+
}
1712+
1713+
#[test]
1714+
fn test_substitute_type_with_lifetime_generic_argument() {
1715+
// Test type with lifetime: Cow<'static, T>
1716+
// The lifetime argument should be preserved while T is substituted
1717+
let ty: Type = syn::parse_str("std::borrow::Cow<'static, T>").unwrap();
1718+
let concrete: Type = syn::parse_str("String").unwrap();
1719+
let result = substitute_type(&ty, &[String::from("T")], &[&concrete]);
1720+
let result_str = quote::quote!(#result).to_string();
1721+
// Lifetime 'static should be preserved, T should be substituted
1722+
assert_eq!(result_str, "std :: borrow :: Cow < 'static , String >");
1723+
}
1724+
1725+
#[test]
1726+
fn test_substitute_type_parenthesized_args() {
1727+
// Fn(T) -> U style (parenthesized arguments)
1728+
// This tests the `other => other.clone()` branch for PathArguments
1729+
let ty: Type = syn::parse_str("fn(T) -> U").unwrap();
1730+
let concrete_t: Type = syn::parse_str("String").unwrap();
1731+
let concrete_u: Type = syn::parse_str("i32").unwrap();
1732+
let result = substitute_type(&ty, &[String::from("T"), String::from("U")], &[&concrete_t, &concrete_u]);
1733+
// Type::BareFn doesn't go through the Path branch, falls to _ => ty.clone()
1734+
assert_eq!(result, ty);
1735+
}
1736+
1737+
#[test]
1738+
fn test_substitute_type_path_without_angle_brackets() {
1739+
// Test path with parenthesized arguments: Fn(T) -> U as a trait
1740+
let ty: Type = syn::parse_str("dyn Fn(T) -> U").unwrap();
1741+
let concrete_t: Type = syn::parse_str("String").unwrap();
1742+
let concrete_u: Type = syn::parse_str("i32").unwrap();
1743+
let result = substitute_type(&ty, &[String::from("T"), String::from("U")], &[&concrete_t, &concrete_u]);
1744+
// Type::TraitObject falls to _ => ty.clone()
1745+
assert_eq!(result, ty);
1746+
}
1747+
15641748
#[rstest]
15651749
#[case("&i32")]
15661750
#[case("std::string::String")]

0 commit comments

Comments
 (0)