Port example 07

This commit is contained in:
2026-03-22 13:12:00 -04:00
parent cf53a9f86d
commit 28e140b302
4 changed files with 610 additions and 55 deletions

View File

@@ -50,22 +50,57 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
.attrs
.push(syn::parse_quote!(#[allow(non_snake_case)]));
let props = function
let mut inputs = function
.sig
.inputs
.iter()
.map(parse_prop)
.collect::<Result<Vec<_>>>()?;
let child_contract = match inputs.last() {
Some(prop) if prop.ident == "children" => {
let prop = inputs.pop().expect("last input should exist");
let kind = parse_children_contract_kind(&prop.ty)?;
Some(ChildContract {
ty: prop.ty,
kind,
})
}
_ => None,
};
let props = inputs;
let builder_name = format_ident!("__{}Builder", component_name);
let child_field_ident = child_contract.as_ref().map(|_| format_ident!("children"));
let builder_tokens = if props.is_empty() {
quote! {
#vis struct #builder_name;
match child_contract.as_ref() {
None => {
quote! {
#vis struct #builder_name;
impl #builder_name {
#vis fn children(self, _children: ()) -> #component_name {
#component_name
impl #builder_name {
#vis fn children(self, _children: ()) -> #component_name {
#component_name
}
}
}
}
Some(contract) => {
let child_arg_ty = child_builder_arg_type_tokens(contract.kind);
let child_value = wrap_special_value_tokens(contract.kind, &format_ident!("children"));
let child_field_ident = child_field_ident
.as_ref()
.expect("child field ident should exist");
quote! {
#vis struct #builder_name;
impl #builder_name {
#vis fn children(self, children: #child_arg_ty) -> #component_name {
#component_name {
#child_field_ident: #child_value,
}
}
}
}
}
}
@@ -98,9 +133,10 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
let builder_methods = props.iter().enumerate().map(|(index, prop)| {
let value_ident = &prop.ident;
let value_ty = &prop.ty;
let missing_at_index = &missing_names[index];
let present_at_index = &present_names[index];
let setter_arg_ty = prop_builder_arg_type_tokens(prop);
let stored_value = wrap_special_value_tokens(prop.kind, value_ident);
let impl_generics = state_idents
.iter()
@@ -138,7 +174,7 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
.enumerate()
.map(|(current, field_ident)| {
if current == index {
quote! { #field_ident: #present_at_index(#value_ident) }
quote! { #field_ident: #present_at_index(#stored_value) }
} else {
quote! { #field_ident: self.#field_ident }
}
@@ -148,7 +184,7 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
if impl_generics.is_empty() {
quote! {
impl #builder_name<#(#self_builder_args),*> {
#vis fn #value_ident(self, #value_ident: #value_ty) -> #builder_name<#(#next_builder_args),*> {
#vis fn #value_ident(self, #value_ident: #setter_arg_ty) -> #builder_name<#(#next_builder_args),*> {
#builder_name {
#(#field_initializers),*
}
@@ -158,7 +194,7 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
} else {
quote! {
impl<#(#impl_generics),*> #builder_name<#(#self_builder_args),*> {
#vis fn #value_ident(self, #value_ident: #value_ty) -> #builder_name<#(#next_builder_args),*> {
#vis fn #value_ident(self, #value_ident: #setter_arg_ty) -> #builder_name<#(#next_builder_args),*> {
#builder_name {
#(#field_initializers),*
}
@@ -179,6 +215,59 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
let field_extractors = field_idents
.iter()
.map(|field_ident| quote! { #field_ident: builder.#field_ident.0 });
let component_render_args = props
.iter()
.map(|prop| {
let ident = &prop.ident;
quote! { ::core::clone::Clone::clone(&self.#ident) }
})
.chain(child_field_ident.iter().map(|ident| {
quote! { ::core::clone::Clone::clone(&self.#ident) }
}))
.collect::<Vec<_>>();
let children_method = match child_contract.as_ref() {
None => quote! {
#vis fn children(self, _children: ()) -> #component_name {
#component_name::from_builder(self)
}
},
Some(contract) => {
let child_arg_ty = child_builder_arg_type_tokens(contract.kind);
let child_value =
wrap_special_value_tokens(contract.kind, &format_ident!("children"));
quote! {
#vis fn children(self, children: #child_arg_ty) -> #component_name {
#component_name::from_builder(self, #child_value)
}
}
}
};
let from_builder = match child_contract.as_ref() {
None => quote! {
fn from_builder(builder: #builder_name<#(#ready_builder_args),*>) -> Self {
Self {
#(#field_extractors),*
}
}
},
Some(contract) => {
let child_ty = &contract.ty;
let child_field_ident = child_field_ident
.as_ref()
.expect("child field ident should exist");
quote! {
fn from_builder(
builder: #builder_name<#(#ready_builder_args),*>,
#child_field_ident: #child_ty,
) -> Self {
Self {
#(#field_extractors),*,
#child_field_ident,
}
}
}
}
};
quote! {
#(#marker_structs)*
@@ -190,17 +279,11 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
#(#builder_methods)*
impl #builder_name<#(#ready_builder_args),*> {
#vis fn children(self, _children: ()) -> #component_name {
#component_name::from_builder(self)
}
#children_method
}
impl #component_name {
fn from_builder(builder: #builder_name<#(#ready_builder_args),*>) -> Self {
Self {
#(#field_extractors),*
}
}
#from_builder
}
impl ::ruin_app::Component for #component_name {
@@ -214,47 +297,82 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
fn render(&self) -> ::ruin_app::View {
::ruin_app::IntoView::into_view(#render_name(
#(::core::clone::Clone::clone(&self.#field_idents)),*
#(#component_render_args),*
))
}
}
}
};
let render_call = if props.is_empty() {
quote! { #render_name() }
} else {
let field_idents = props.iter().map(|prop| &prop.ident);
quote! { #render_name(#(::core::clone::Clone::clone(&self.#field_idents)),*) }
};
let render_args = props
.iter()
.map(|prop| {
let ident = &prop.ident;
quote! { ::core::clone::Clone::clone(&self.#ident) }
})
.chain(child_field_ident.iter().map(|ident| {
quote! { ::core::clone::Clone::clone(&self.#ident) }
}))
.collect::<Vec<_>>();
let render_call = quote! { #render_name(#(#render_args),*) };
let component_tokens = if props.is_empty() {
quote! {
#vis struct #component_name;
let component_tokens = match (props.is_empty(), child_contract.as_ref()) {
(true, None) => {
quote! {
#vis struct #component_name;
#builder_tokens
#builder_tokens
impl ::ruin_app::Component for #component_name {
type Builder = #builder_name;
impl ::ruin_app::Component for #component_name {
type Builder = #builder_name;
fn builder() -> Self::Builder {
#builder_name
}
fn builder() -> Self::Builder {
#builder_name
}
fn render(&self) -> ::ruin_app::View {
::ruin_app::IntoView::into_view(#render_call)
fn render(&self) -> ::ruin_app::View {
::ruin_app::IntoView::into_view(#render_call)
}
}
}
}
} else {
let field_idents = props.iter().map(|prop| &prop.ident);
let field_types = props.iter().map(|prop| &prop.ty);
quote! {
#vis struct #component_name {
#( #field_idents: #field_types ),*
}
_ => {
let field_idents = props
.iter()
.map(|prop| prop.ident.clone())
.chain(child_field_ident.iter().cloned())
.collect::<Vec<_>>();
let field_types = props
.iter()
.map(|prop| prop.ty.clone())
.chain(child_contract.iter().map(|contract| contract.ty.clone()))
.collect::<Vec<_>>();
let component_impl = if props.is_empty() {
quote! {
impl ::ruin_app::Component for #component_name {
type Builder = #builder_name;
#builder_tokens
fn builder() -> Self::Builder {
#builder_name
}
fn render(&self) -> ::ruin_app::View {
::ruin_app::IntoView::into_view(#render_call)
}
}
}
} else {
quote! {}
};
quote! {
#vis struct #component_name {
#( #field_idents: #field_types ),*
}
#builder_tokens
#component_impl
}
}
};
@@ -333,7 +451,8 @@ fn validate_component_function(function: &ItemFn) -> Result<()> {
"components cannot be variadic",
));
}
for input in &signature.inputs {
let input_count = signature.inputs.len();
for (index, input) in signature.inputs.iter().enumerate() {
let FnArg::Typed(typed) = input else {
return Err(Error::new_spanned(
input,
@@ -347,10 +466,13 @@ fn validate_component_function(function: &ItemFn) -> Result<()> {
));
};
if ident == "children" {
return Err(Error::new_spanned(
ident,
"`children` is reserved for the generated builder finalizer",
));
if index + 1 != input_count {
return Err(Error::new_spanned(
ident,
"component `children` contracts must be the final parameter",
));
}
parse_children_contract_kind(typed.ty.as_ref())?;
}
}
@@ -366,6 +488,7 @@ fn validate_component_function(function: &ItemFn) -> Result<()> {
struct Prop {
ident: Ident,
ty: Type,
kind: SpecialValueKind,
}
fn parse_prop(input: &FnArg) -> Result<Prop> {
@@ -385,9 +508,83 @@ fn parse_prop(input: &FnArg) -> Result<Prop> {
Ok(Prop {
ident: ident.clone(),
ty: (*typed.ty).clone(),
kind: parse_special_value_kind(typed.ty.as_ref()),
})
}
#[derive(Clone)]
struct ChildContract {
ty: Type,
kind: SpecialValueKind,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum SpecialValueKind {
Plain,
ChildViews,
TextValue,
}
fn parse_special_value_kind(ty: &Type) -> SpecialValueKind {
match terminal_type_ident(ty).as_deref() {
Some("ChildViews") => SpecialValueKind::ChildViews,
Some("TextValue") => SpecialValueKind::TextValue,
_ => SpecialValueKind::Plain,
}
}
fn parse_children_contract_kind(ty: &Type) -> Result<SpecialValueKind> {
match parse_special_value_kind(ty) {
SpecialValueKind::ChildViews => Ok(SpecialValueKind::ChildViews),
SpecialValueKind::TextValue => Ok(SpecialValueKind::TextValue),
SpecialValueKind::Plain => Err(Error::new_spanned(
ty,
"component `children` contracts must use `ruin_app::ChildViews` or `ruin_app::TextValue`",
)),
}
}
fn terminal_type_ident(ty: &Type) -> Option<String> {
let Type::Path(type_path) = ty else {
return None;
};
type_path
.path
.segments
.last()
.map(|segment| segment.ident.to_string())
}
fn prop_builder_arg_type_tokens(prop: &Prop) -> proc_macro2::TokenStream {
match prop.kind {
SpecialValueKind::Plain => {
let ty = &prop.ty;
quote! { #ty }
}
SpecialValueKind::ChildViews => quote! { impl ::ruin_app::Children },
SpecialValueKind::TextValue => quote! { impl ::ruin_app::TextChildren },
}
}
fn child_builder_arg_type_tokens(kind: SpecialValueKind) -> proc_macro2::TokenStream {
match kind {
SpecialValueKind::Plain => quote! { () },
SpecialValueKind::ChildViews => quote! { impl ::ruin_app::Children },
SpecialValueKind::TextValue => quote! { impl ::ruin_app::TextChildren },
}
}
fn wrap_special_value_tokens(
kind: SpecialValueKind,
ident: &Ident,
) -> proc_macro2::TokenStream {
match kind {
SpecialValueKind::Plain => quote! { #ident },
SpecialValueKind::ChildViews => quote! { ::ruin_app::ChildViews::from_children(#ident) },
SpecialValueKind::TextValue => quote! { ::ruin_app::TextValue::from_text(#ident) },
}
}
struct ViewRoot {
root: Node,
}