use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{GenericParam, Generics};
use crate::component::{
attrs::DefaultValue,
diagnostic,
field::{ComponentField, FieldKind, PropField, SlotField},
model::Component,
};
pub(crate) fn derive(component: syn::Result<Component>) -> TokenStream {
match component {
Ok(component) => match derive_bon_v1(&component) {
Ok(tokens) => TokenStream::from(tokens),
Err(tokens) => diagnostic::tokens(tokens),
},
Err(err) => diagnostic::tokens(diagnostic::from_model_error(err)),
}
}
fn derive_bon_v1(component: &Component) -> Result<TokenStream2, TokenStream2> {
let default_slot = component.default_slot();
for field in &component.fields {
match &field.kind {
FieldKind::Slot(slot) => {
if slot.optional || slot.repeated || slot.each.is_some() {
if Some(&field.name) == default_slot.map(|field| &field.name) {
return Err(diagnostic::unsupported_in_v1(
component,
&field.name.to_string(),
"optional or repeated default slot semantics",
));
}
if !(slot.optional && !slot.repeated && slot.each.is_none()
|| !slot.optional && slot.repeated && slot.each.is_some())
{
return Err(diagnostic::unsupported_in_v1(
component,
&field.name.to_string(),
"repeated or advanced named slot semantics",
));
}
}
if Some(&field.name) == default_slot.map(|field| &field.name) {
if !is_default_slot_storage(&field.ty) {
return Err(diagnostic::unsupported_in_v1(
component,
&field.name.to_string(),
"default slots with unsupported storage types",
));
}
} else if slot.repeated {
if !is_repeated_slot_storage(&field.ty) {
return Err(diagnostic::unsupported_in_v1(
component,
&field.name.to_string(),
"repeated slots with unsupported storage types",
));
}
} else if slot.optional {
if !is_named_optional_slot_storage(&field.ty) {
return Err(diagnostic::unsupported_in_v1(
component,
&field.name.to_string(),
"named optional slots with unsupported storage types",
));
}
} else if !is_named_slot_storage(&field.ty) {
return Err(diagnostic::unsupported_in_v1(
component,
&field.name.to_string(),
"named slots with unsupported storage types",
));
}
}
FieldKind::Prop(PropField { each: Some(_), .. }) => {
return Err(diagnostic::unsupported_in_v1(
component,
&field.name.to_string(),
"`each` setters",
));
}
FieldKind::Prop(_) => {}
}
}
let name = &component.name;
let builder_name = format_ident!("__Mx{}Builder", name);
let state_mod_name = format_ident!("__mx_{}_builder", name.to_string().to_lowercase());
let (impl_generics, ty_generics, where_clause) = component.generics.split_for_impl();
let builder_impl_generics = builder_impl_generics(&component.generics, &state_mod_name);
let builder_type_generics = builder_type_generics(&component.generics);
let params = component
.fields
.iter()
.map(prop_param)
.collect::<Result<Vec<_>, _>>()?;
let slot_methods = default_slot
.map(|field| {
default_slot_methods(field, &component.generics, &builder_name, &state_mod_name)
})
.transpose()?;
let named_slot_methods = component
.named_slots()
.map(|field| match field.slot() {
Some(SlotField { repeated: true, .. }) => {
repeated_slot_methods(field, &component.generics, &builder_name, &state_mod_name)
}
_ => named_slot_methods(
field,
&component.generics,
&builder_name,
&state_mod_name,
),
})
.collect::<Result<Vec<_>, _>>()?;
let inits = component
.fields
.iter()
.map(|field| {
let field_name = &field.name;
let builder_name = &field.builder_name;
quote! { #field_name: #builder_name }
})
.collect::<Vec<_>>();
Ok(quote! {
#[::maud_extensions::bon::bon]
impl #impl_generics #name #ty_generics #where_clause {
#[builder(
builder_type(name = #builder_name, vis = "pub"),
state_mod(name = #state_mod_name, vis = "pub"),
start_fn(name = new, vis = "pub"),
finish_fn(name = build, vis = "pub"),
on(String, into)
)]
fn __mx_component_new(
#( #params ),*
) -> Self {
Self {
#( #inits ),*
}
}
}
impl #builder_impl_generics #builder_name #builder_type_generics {
#slot_methods
#( #named_slot_methods )*
pub fn render(self) -> ::maud::Markup
where
S: #state_mod_name::IsComplete,
#name #ty_generics: ::maud::Render,
{
::maud::Render::render(&self.build())
}
}
})
}
fn prop_param(field: &ComponentField) -> Result<TokenStream2, TokenStream2> {
let builder_name = &field.builder_name;
let ty = &field.ty;
match &field.kind {
FieldKind::Prop(prop) => {
let default_attr = match &prop.default {
Some(DefaultValue {
expr: Some(expr), ..
}) => quote! { #[builder(default = #expr)] },
Some(DefaultValue { expr: None, .. }) => quote! { #[builder(default)] },
None => TokenStream2::new(),
};
Ok(quote! {
#default_attr
#builder_name: #ty
})
}
FieldKind::Slot(SlotField { default: true, .. }) => {
let internal_name = field.bon_required_internal_setter_ident();
let maybe_into_attr = slot_into_attr(&field.ty);
Ok(quote! {
#maybe_into_attr
#[builder(setters(name = #internal_name, vis = ""))]
#builder_name: #ty
})
}
FieldKind::Slot(SlotField {
default: false,
repeated: true,
..
}) => {
let getter_internal = format_ident!("__mx_get_{}_internal", builder_name);
let maybe_into_attr = slot_into_attr(&field.ty);
Ok(quote! {
#maybe_into_attr
#[builder(default, overwritable, getter(name = #getter_internal, vis = ""))]
#builder_name: #ty
})
}
FieldKind::Slot(SlotField { default: false, .. }) => {
let maybe_into_attr = slot_into_attr(&field.ty);
if field.slot_inner_ty().is_some() || !field.slot().is_some_and(|slot| slot.optional) {
let internal_name = field.bon_required_internal_setter_ident();
let maybe_internal = format_ident!("__mx_maybe_{}_internal", builder_name);
Ok(quote! {
#maybe_into_attr
#[builder(default, setters(some_fn(name = #internal_name, vis = ""), option_fn(name = #maybe_internal, vis = "")))]
#builder_name: #ty
})
} else {
let internal_name = field.bon_optional_some_setter_ident();
Ok(quote! {
#maybe_into_attr
#[builder(default)]
#[builder(setters(some_fn(name = #internal_name, vis = "")))]
#builder_name: #ty
})
}
}
}
}
fn default_slot_methods(
field: &ComponentField,
generics: &Generics,
builder_name: &syn::Ident,
state_mod_name: &syn::Ident,
) -> Result<TokenStream2, TokenStream2> {
let slot_name = &field.name;
let internal_name = field.bon_required_internal_setter_ident();
let state_assoc = field.state_assoc_ident();
let set_state = field.set_state_ident();
let input_ty = slot_input_ty(field)?;
let builder_after_slot = builder_type_with_state(
generics,
builder_name,
quote! { #state_mod_name::#set_state<S> },
);
let normalized_child = slot_normalized_expr(field, quote! { child });
Ok(quote! {
pub fn #slot_name(self, child: #input_ty) -> #builder_after_slot
where
S::#state_assoc: #state_mod_name::IsUnset,
{
self.#internal_name(#normalized_child)
}
pub fn child(self, child: #input_ty) -> #builder_after_slot
where
S::#state_assoc: #state_mod_name::IsUnset,
{
self.#slot_name(child)
}
})
}
fn repeated_slot_methods(
field: &ComponentField,
_generics: &Generics,
_builder_name: &syn::Ident,
_state_mod_name: &syn::Ident,
) -> Result<TokenStream2, TokenStream2> {
let item_setter = field
.slot()
.and_then(|slot| slot.each.as_ref())
.expect("repeated slot methods require `each = ...`");
let collection_setter = &field.name;
let getter_internal = format_ident!("__mx_get_{}_internal", collection_setter);
let item_ty = field
.repeated_slot_item_ty()
.expect("validated repeated slot should have an item type");
let push_item = push_repeated_slot(quote! { child });
Ok(quote! {
pub fn #item_setter(self, child: #item_ty) -> Self {
let mut items = self.#getter_internal().cloned().unwrap_or_default();
#push_item
self.#collection_setter(items)
}
})
}
fn named_slot_methods(
field: &ComponentField,
generics: &Generics,
builder_name: &syn::Ident,
state_mod_name: &syn::Ident,
) -> Result<TokenStream2, TokenStream2> {
let slot_name = &field.name;
let internal_name = if field.slot().is_some_and(|slot| slot.optional) {
field.bon_optional_some_setter_ident()
} else {
field.bon_required_internal_setter_ident()
};
let state_assoc = field.state_assoc_ident();
let set_state = field.set_state_ident();
let input_ty = slot_input_ty(field)?;
let builder_after_slot = builder_type_with_state(
generics,
builder_name,
quote! { #state_mod_name::#set_state<S> },
);
let normalized_child = slot_normalized_expr(field, quote! { child });
Ok(quote! {
pub fn #slot_name(self, child: #input_ty) -> #builder_after_slot
where
S::#state_assoc: #state_mod_name::IsUnset,
{
self.#internal_name(#normalized_child)
}
})
}
fn is_default_slot_storage(ty: &syn::Type) -> bool {
is_maud_markup(ty) || is_slot_of_maud_markup(ty)
}
fn is_named_optional_slot_storage(ty: &syn::Type) -> bool {
is_optional_maud_markup(ty)
|| is_optional_maud_extensions_slot(ty)
|| is_slot_of_optional_maud_markup(ty)
}
fn is_named_slot_storage(ty: &syn::Type) -> bool {
is_maud_markup(ty) || is_maud_extensions_slot(ty) || is_slot_of_maud_markup(ty)
}
fn is_repeated_slot_storage(ty: &syn::Type) -> bool {
is_vec_of_maud_markup(ty) || is_maud_extensions_slots(ty) || is_slot_of_vec_of_maud_markup(ty)
}
fn is_maud_markup(ty: &syn::Type) -> bool {
path_ends_with(ty, &["maud", "Markup"]) || path_ends_with(ty, &["Markup"])
}
fn is_maud_extensions_slot(ty: &syn::Type) -> bool {
path_ends_with(ty, &["maud_extensions", "Slot"]) || path_ends_with(ty, &["Slot"])
}
fn is_maud_extensions_slots(ty: &syn::Type) -> bool {
path_ends_with(ty, &["maud_extensions", "Slots"]) || path_ends_with(ty, &["Slots"])
}
fn is_optional_maud_markup(ty: &syn::Type) -> bool {
let syn::Type::Path(type_path) = ty else {
return false;
};
let Some(option_segment) = type_path.path.segments.last() else {
return false;
};
if option_segment.ident != "Option" {
return false;
}
let syn::PathArguments::AngleBracketed(arguments) = &option_segment.arguments else {
return false;
};
let Some(syn::GenericArgument::Type(inner)) = arguments.args.first() else {
return false;
};
is_maud_markup(inner)
}
fn is_optional_maud_extensions_slot(ty: &syn::Type) -> bool {
optional_inner(ty).is_some_and(is_maud_extensions_slot)
}
fn is_slot_of_maud_markup(ty: &syn::Type) -> bool {
slot_inner(ty).is_some_and(is_maud_markup)
}
fn is_slot_of_optional_maud_markup(ty: &syn::Type) -> bool {
slot_inner(ty).is_some_and(is_optional_maud_markup)
}
fn is_slot_of_vec_of_maud_markup(ty: &syn::Type) -> bool {
slot_inner(ty).is_some_and(is_vec_of_maud_markup)
}
fn is_vec_of_maud_markup(ty: &syn::Type) -> bool {
let syn::Type::Path(type_path) = ty else {
return false;
};
let Some(vec_segment) = type_path.path.segments.last() else {
return false;
};
if vec_segment.ident != "Vec" {
return false;
}
let syn::PathArguments::AngleBracketed(arguments) = &vec_segment.arguments else {
return false;
};
let Some(syn::GenericArgument::Type(inner)) = arguments.args.first() else {
return false;
};
is_maud_markup(inner)
}
fn slot_into_attr(slot_ty: &syn::Type) -> TokenStream2 {
if is_maud_extensions_slot(slot_ty)
|| is_optional_maud_extensions_slot(slot_ty)
|| is_maud_extensions_slots(slot_ty)
|| is_slot_of_maud_markup(slot_ty)
|| is_slot_of_optional_maud_markup(slot_ty)
|| is_slot_of_vec_of_maud_markup(slot_ty)
{
quote! { #[builder(into)] }
} else {
TokenStream2::new()
}
}
fn push_repeated_slot(child_expr: TokenStream2) -> TokenStream2 {
quote! {
items.push(#child_expr);
}
}
fn slot_inner(ty: &syn::Type) -> Option<&syn::Type> {
let syn::Type::Path(type_path) = ty else {
return None;
};
let Some(segment) = type_path.path.segments.last() else {
return None;
};
if segment.ident != "Slot" {
return None;
}
let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
return None;
};
let Some(syn::GenericArgument::Type(inner)) = arguments.args.first() else {
return None;
};
Some(inner)
}
fn optional_inner(ty: &syn::Type) -> Option<&syn::Type> {
let syn::Type::Path(type_path) = ty else {
return None;
};
let Some(option_segment) = type_path.path.segments.last() else {
return None;
};
if option_segment.ident != "Option" {
return None;
}
let syn::PathArguments::AngleBracketed(arguments) = &option_segment.arguments else {
return None;
};
let Some(syn::GenericArgument::Type(inner)) = arguments.args.first() else {
return None;
};
Some(inner)
}
fn path_ends_with(ty: &syn::Type, suffix: &[&str]) -> bool {
let syn::Type::Path(type_path) = ty else {
return false;
};
let segments = type_path.path.segments.iter().collect::<Vec<_>>();
if segments.len() < suffix.len() {
return false;
}
segments[segments.len() - suffix.len()..]
.iter()
.zip(suffix)
.all(|(segment, expected)| segment.ident == *expected)
}
fn slot_input_ty(field: &ComponentField) -> Result<TokenStream2, TokenStream2> {
if let Some(inner) = field.slot_inner_ty() {
if vec_inner(inner).is_some() {
unreachable!("single-slot input typing should not see repeated slot content");
}
return Ok(quote! { #inner });
}
Ok(field_ty(field))
}
fn slot_normalized_expr(_field: &ComponentField, child_expr: TokenStream2) -> TokenStream2 {
child_expr
}
fn field_ty(field: &ComponentField) -> TokenStream2 {
let ty = &field.ty;
quote! { #ty }
}
fn vec_inner<'a>(ty: &'a syn::Type) -> Option<&'a syn::Type> {
let syn::Type::Path(type_path) = ty else {
return None;
};
let segment = type_path.path.segments.last()?;
if segment.ident != "Vec" {
return None;
}
let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
return None;
};
let first = arguments.args.first()?;
let syn::GenericArgument::Type(inner) = first else {
return None;
};
Some(inner)
}
fn builder_impl_generics(generics: &Generics, state_mod_name: &syn::Ident) -> TokenStream2 {
let params = generics.params.iter();
if generics.params.is_empty() {
quote! { <S: #state_mod_name::State> }
} else {
quote! { <#(#params,)* S: #state_mod_name::State> }
}
}
fn builder_type_generics(generics: &Generics) -> TokenStream2 {
let args = generics.params.iter().map(generic_arg);
if generics.params.is_empty() {
quote! { <S> }
} else {
quote! { <#(#args,)* S> }
}
}
fn builder_type_with_state(
generics: &Generics,
builder_name: &syn::Ident,
state: TokenStream2,
) -> TokenStream2 {
let args = generics.params.iter().map(generic_arg);
if generics.params.is_empty() {
quote! { #builder_name<#state> }
} else {
quote! { #builder_name<#(#args,)* #state> }
}
}
fn generic_arg(param: &GenericParam) -> TokenStream2 {
match param {
GenericParam::Lifetime(lifetime) => {
let lifetime = &lifetime.lifetime;
quote! { #lifetime }
}
GenericParam::Type(ty) => {
let ident = &ty.ident;
quote! { #ident }
}
GenericParam::Const(_) => unreachable!("const generics are rejected in the model"),
}
}