use proc_macro::TokenStream as TokenStream1;
use proc_macro2::{Ident, TokenStream};
use proc_macro_crate::{FoundCrate, crate_name};
use quote::{format_ident, quote, quote_spanned};
use guinea_route_dsl::{Segment, parse_pattern, same_type, spelled, type_ident};
fn guinea_crate_path() -> proc_macro2::TokenStream {
match crate_name("guinea") {
Ok(FoundCrate::Itself) => quote!(crate),
Ok(FoundCrate::Name(name)) => {
let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
quote!(::#ident)
}
Err(_) => quote!(::guinea),
}
}
fn declared_at(ty: &syn::Type) -> TokenStream {
let gc = crate::handler::guinea_core_crate_path();
quote_spanned! {syn::spanned::Spanned::span(ty)=>
#gc::actor::shape::Declared {
file: ::core::file!(),
line: ::core::line!(),
column: ::core::column!(),
crate_dir: ::core::env!("CARGO_MANIFEST_DIR"),
}
}
}
fn router_path(guinea: &TokenStream) -> TokenStream {
match crate_name("guinea-router") {
Ok(FoundCrate::Itself) => quote!(crate::router),
Ok(FoundCrate::Name(name)) => {
let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
quote!(::#ident::router)
}
Err(_) => quote!(#guinea::router),
}
}
fn link_path(guinea: &TokenStream) -> TokenStream {
module_of_router(guinea, "link")
}
fn enter_path(guinea: &TokenStream) -> TokenStream {
module_of_router(guinea, "enter")
}
fn restore_path(guinea: &TokenStream) -> TokenStream {
module_of_router(guinea, "restore")
}
fn part_chain_name(tree: &Ident, leaf: &Ident, at: usize, part: usize) -> Ident {
format_ident!("__routes_part_{}_{}_{}_{}", tree, leaf, at, part)
}
pub(crate) fn slot_impl(item: TokenStream1) -> TokenStream1 {
let item = syn::parse_macro_input!(item as syn::ItemStruct);
let name = &item.ident;
let (impl_generics, type_generics, where_clause) = item.generics.split_for_impl();
let slot = module_of_router(&guinea_crate_path(), "slot");
quote! {
#item
impl #impl_generics #slot::Slot for #name #type_generics #where_clause {}
}
.into()
}
fn module_of_router(guinea: &TokenStream, module: &str) -> TokenStream {
let module = syn::Ident::new(module, proc_macro2::Span::call_site());
match crate_name("guinea-router") {
Ok(FoundCrate::Itself) => quote!(crate::#module),
Ok(FoundCrate::Name(name)) => {
let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
quote!(::#ident::#module)
}
Err(_) => quote!(#guinea::#module),
}
}
fn feature_path(guinea: &TokenStream) -> TokenStream {
match crate_name("guinea-app") {
Ok(FoundCrate::Itself) => quote!(crate::feature),
Ok(FoundCrate::Name(name)) => {
let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
quote!(::#ident::feature)
}
Err(_) => quote!(#guinea::feature),
}
}
fn backend_module(ty: &syn::Type) -> TokenStream {
let syn::Type::Path(path) = ty else {
panic!("routes!: backend must be a path, like `guinea_ratatui::Tui`");
};
let mut module = path.path.clone();
module.segments.pop();
let module: Vec<syn::PathSegment> = module.segments.into_iter().collect();
if module.is_empty() {
panic!("routes!: backend needs the module too, as in `guinea_ratatui::Tui`");
}
quote!(#(#module)::*)
}
fn identity_impls(enum_ident: &Ident, leaves: &[guinea_route_dsl::Leaf]) -> TokenStream {
let eq_arms = leaves.iter().map(|leaf| {
let variant = type_ident(&leaf.ty);
let kept: Vec<&Ident> = leaf
.fields
.iter()
.filter(|field| field.identity)
.map(|field| &field.name)
.collect();
let theirs: Vec<Ident> = kept.iter().map(|name| format_ident!("__other_{}", name)).collect();
quote! {
(
#enum_ident::#variant { #(#kept,)* .. },
#enum_ident::#variant { #(#kept: #theirs,)* .. },
) => #(#kept == #theirs &&)* true
}
});
let debug_arms = leaves.iter().map(|leaf| {
let variant = type_ident(&leaf.ty);
let kept: Vec<&Ident> = leaf
.fields
.iter()
.filter(|field| field.identity)
.map(|field| &field.name)
.collect();
let labels = kept.iter().map(|name| name.to_string());
let title = variant.to_string();
let whole = leaf.fields.len() == kept.len();
let finish = if whole {
quote!(.finish())
} else {
quote!(.finish_non_exhaustive())
};
quote! {
#enum_ident::#variant { #(#kept,)* .. } => f
.debug_struct(#title)
#(.field(#labels, #kept))*
#finish
}
});
quote! {
impl ::std::cmp::PartialEq for #enum_ident {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
#(#eq_arms,)*
_ => false,
}
}
}
impl ::std::fmt::Debug for #enum_ident {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match self {
#(#debug_arms),*
}
}
}
}
}
fn params_struct(name: &Ident, fields: &[guinea_route_dsl::Field]) -> TokenStream {
let defs = fields.iter().map(|field| {
let (name, ty) = (&field.name, &field.ty);
quote! { pub #name: #ty }
});
if fields.is_empty() {
return quote! {
pub type #name = ();
};
}
if fields.iter().all(|field| field.identity) {
return quote! {
#[derive(Clone, Debug, PartialEq)]
pub struct #name { #(#defs),* }
};
}
let kept: Vec<&Ident> = fields
.iter()
.filter(|field| field.identity)
.map(|field| &field.name)
.collect();
let labels = kept.iter().map(|name| name.to_string());
let title = name.to_string();
quote! {
#[derive(Clone)]
pub struct #name { #(#defs),* }
impl ::std::fmt::Debug for #name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
f.debug_struct(#title)
#(.field(#labels, &self.#kept))*
.finish_non_exhaustive()
}
}
impl ::std::cmp::PartialEq for #name {
fn eq(&self, _other: &Self) -> bool {
false
}
}
}
}
fn joined<E: std::fmt::Display>(errors: &[E]) -> String {
errors
.iter()
.map(|error| error.to_string())
.collect::<Vec<_>>()
.join("\n")
}
pub fn routes_impl(input: TokenStream1) -> TokenStream1 {
let tree = guinea_route_dsl::parse(input.into());
let enum_ident = tree.name.clone();
let leaves = tree.leaves();
let field_errors = guinea_route_dsl::matcher::check_fields(&leaves);
if !field_errors.is_empty() {
panic!("{}", joined(&field_errors));
}
let guard_errors = guinea_route_dsl::check_guards(&tree);
if !guard_errors.is_empty() {
panic!("{}", joined(&guard_errors));
}
let match_tree = match guinea_route_dsl::matcher::build(&leaves) {
Ok(tree) => tree,
Err(conflicts) => panic!("{}", joined(&conflicts)),
};
let guinea = guinea_crate_path();
let router = router_path(&guinea);
let link_mod = link_path(&guinea);
let enter = enter_path(&guinea);
let restore_mod = restore_path(&guinea);
let parse_fn = guinea_route_dsl::matcher::emit_parse(&match_tree, &enum_ident, &link_mod);
let variant_idents: Vec<Ident> = leaves.iter().map(|l| type_ident(&l.ty)).collect();
let whole = leaves
.iter()
.all(|leaf| leaf.fields.iter().all(|field| field.identity));
let (enum_derives, enum_impls) = if whole {
(quote!(#[derive(Debug, PartialEq)]), quote!())
} else {
(quote!(), identity_impls(&enum_ident, &leaves))
};
let variant_defs = leaves.iter().zip(&variant_idents).map(|(leaf, ident)| {
let field_defs = leaf.fields.iter().map(|field| {
let (name, ty) = (&field.name, &field.ty);
quote! { #name: #ty }
});
quote! { #ident { #(#field_defs),* } }
});
let params_idents: Vec<Ident> = variant_idents
.iter()
.map(|ident| format_ident!("{}Params", ident))
.collect();
let params_defs = leaves
.iter()
.zip(¶ms_idents)
.map(|(leaf, params)| params_struct(params, &leaf.fields));
let layouts = tree.layout_params();
let layout_params_idents: Vec<Ident> = layouts
.iter()
.map(|layout| format_ident!("{}Params", type_ident(&layout.ty)))
.collect();
let layout_params_defs = layouts
.iter()
.zip(&layout_params_idents)
.map(|(layout, params)| params_struct(params, &layout.fields));
let params_arms = leaves
.iter()
.zip(&variant_idents)
.zip(¶ms_idents)
.map(|((leaf, ident), params)| {
let names: Vec<&Ident> = leaf.fields.iter().map(|field| &field.name).collect();
let ancestors = leaf.ancestors.iter().map(|ancestor| {
let position = layouts
.iter()
.position(|layout| same_type(&layout.ty, ancestor))
.expect("every ancestor of a leaf is a layout of this tree");
let params = &layout_params_idents[position];
let carried = &layouts[position].fields;
let made = match carried.is_empty() {
true => quote!(()),
false => {
let taken = carried.iter().map(|field| {
let name = &field.name;
quote! { #name: #name.clone() }
});
quote!(#params { #(#taken),* })
}
};
quote! {
::std::boxed::Box::new(#made)
as ::std::boxed::Box<dyn ::std::any::Any>
}
});
let made = match names.is_empty() {
true => quote!(()),
false => quote!(#params { #(#names: #names.clone()),* }),
};
quote! {
#enum_ident::#ident { #(#names),* } => vec![
#(#ancestors,)*
::std::boxed::Box::new(#made)
as ::std::boxed::Box<dyn ::std::any::Any>
]
}
});
let restorable = leaves.iter().any(|leaf| leaf.restorable);
let save_arms = leaves.iter().zip(&variant_idents).map(|(leaf, ident)| {
if !leaf.restorable {
return quote! { #enum_ident::#ident { .. } => None };
}
let names: Vec<&Ident> = leaf.fields.iter().map(|field| &field.name).collect();
let route = ident.to_string();
let writes = leaf.fields.iter().map(|field| {
let name = &field.name;
let key = name.to_string();
quote_spanned! {name.span()=>
__saving.field(#key, #name)?;
}
});
quote! {
#enum_ident::#ident { #(#names),* } => {
let mut __saving = #restore_mod::Saving::new(#route);
#(#writes)*
__saving.finish()
}
}
});
let restore_arms = leaves
.iter()
.zip(&variant_idents)
.filter(|(leaf, _)| leaf.restorable)
.map(|(leaf, ident)| {
let route = ident.to_string();
let reads = leaf.fields.iter().map(|field| {
let name = &field.name;
let key = name.to_string();
quote_spanned! {name.span()=>
#name: __fields.field(#key)?
}
});
quote! {
#route => Some(#enum_ident::#ident { #(#reads),* })
}
});
let link_arms = leaves.iter().zip(&variant_idents).map(|(leaf, ident)| {
let Some(link) = &leaf.link else {
return quote! { #enum_ident::#ident { .. } => None };
};
let field_pats: Vec<&Ident> = leaf.fields.iter().map(|field| &field.name).collect();
let parts = parse_pattern(link).into_iter().map(|seg| match seg {
Segment::Literal(lit) => quote! { #lit.to_string() },
Segment::Capture(name) => {
let field = format_ident!("{}", name);
quote! { #link_mod::LinkValue::encode(#field) }
}
});
quote! {
#enum_ident::#ident { #(#field_pats),* } => {
let parts: Vec<String> = vec![#(#parts),*];
Some(format!("/{}", parts.join("/")))
}
}
});
let tree_name = enum_ident.to_string();
let declared_links = leaves.iter().filter_map(|leaf| {
let link = leaf.link.as_ref()?;
let route = type_ident(&leaf.ty).to_string();
let guard_names = leaf.guards.iter().map(spelled);
let leaf_restorable = leaf.restorable;
let captures = parse_pattern(link).into_iter().filter_map(|seg| {
let Segment::Capture(name) = seg else {
return None;
};
let ty = leaf
.fields
.iter()
.find(|field| field.name == name)
.map(|field| &field.ty)
.expect("check_fields has passed, so every capture has a field");
quote! {
#link_mod::Capture {
name: #name,
ty: <#ty as #link_mod::LinkValue>::NAME,
}
}
.into()
});
Some(quote! {
#link_mod::DeepLink {
tree: #tree_name,
route: #route,
path: #link,
captures: &[#(#captures),*],
guards: &[#(#guard_names),*],
restorable: #leaf_restorable,
}
})
});
let name_arms = variant_idents.iter().map(|ident| {
let name = ident.to_string();
quote! { #enum_ident::#ident { .. } => #name }
});
let (backend_ty, backend_mod) = match &tree.backend {
Some(ty) => (quote!(#ty), backend_module(ty)),
None => (quote!(#guinea::Backend), quote!(#guinea::backend)),
};
let chain_consts = leaves.iter().zip(&variant_idents).map(|(leaf, ident)| {
let const_name = format_ident!("__routes_chain_{}_{}", enum_ident, ident);
let leaf_ty = &leaf.ty;
let kept = |at: usize| match leaf.keep.get(at) {
Some(true) => quote!(.kept()),
_ => quote!(),
};
let parts_name = |at: usize| format_ident!("__routes_parts_{}_{}_{}", enum_ident, ident, at);
let ancestor_entries = leaf.ancestors.iter().enumerate().map(|(at, ty)| {
let declared = declared_at(ty);
let kept = kept(at);
let parts = match leaf.parts.get(at) {
Some(parts) if !parts.is_empty() => {
let name = parts_name(at);
quote!(.parts(&#name))
}
_ => quote!(),
};
quote! { #backend_mod::layout_entry::<#ty>().at(#declared) #kept #parts }
});
let len = leaf.ancestors.len() + 1;
let declared = declared_at(leaf_ty);
let leaf_kept = kept(leaf.ancestors.len());
let parts_consts = leaf.parts.iter().enumerate().filter(|(_, parts)| !parts.is_empty()).map(|(at, parts)| {
let name = parts_name(at);
let count = parts.len();
let above = leaf.ancestors[..=at].iter().map(|ty| {
let declared = declared_at(ty);
quote! { #backend_mod::layout_entry::<#ty>().at(#declared) }
}).collect::<Vec<_>>();
let chains = parts.iter().enumerate().map(|(n, part)| {
let chain = part_chain_name(&enum_ident, ident, at, n);
let part_ty = &part.ty;
let declared = declared_at(part_ty);
let chain_len = at + 2;
quote! {
#[allow(non_upper_case_globals)]
const #chain: [#router::SegmentEntry<#backend_ty>; #chain_len] = [
#(#above,)*
#backend_mod::segment_entry::<#part_ty>().at(#declared),
];
}
});
let placed = parts.iter().enumerate().map(|(n, part)| {
let chain = part_chain_name(&enum_ident, ident, at, n);
let slot = &part.slot;
quote! { #router::Part::new::<#slot>(&#chain) }
});
quote! {
#(#chains)*
#[allow(non_upper_case_globals)]
const #name: [#router::Part<#backend_ty>; #count] = [#(#placed),*];
}
}).collect::<Vec<_>>();
quote! {
#(#parts_consts)*
#[allow(non_upper_case_globals)]
const #const_name: [#router::SegmentEntry<#backend_ty>; #len] = [
#(#ancestor_entries,)*
#backend_mod::segment_entry::<#leaf_ty>().at(#declared) #leaf_kept,
];
}
});
let feature = feature_path(&guinea);
let application = match &tree.app {
Some(app) => {
let name = spelled(app);
quote! {
::core::option::Option::Some(#router::AppItem {
name: #name,
installed: #feature::Scope::has_feature::<#app>,
})
}
}
None => quote!(::core::option::Option::None),
};
let guard_consts = leaves.iter().zip(&variant_idents).map(|(leaf, ident)| {
let const_name = format_ident!("__routes_guards_{}_{}", enum_ident, ident);
let len = leaf.guards.len();
let stands = leaf.guards.iter().map(|ty| {
quote! {
&const { #enter::Stands::<#ty>(::std::marker::PhantomData) }
}
});
quote! {
#[allow(non_upper_case_globals)]
const #const_name: [&'static dyn #enter::EnterGuard; #len] = [#(#stands),*];
}
});
let guard_arms = leaves.iter().zip(&variant_idents).map(|(_leaf, ident)| {
let const_name = format_ident!("__routes_guards_{}_{}", enum_ident, ident);
quote! { #enum_ident::#ident { .. } => &#const_name }
});
let chain_arms = leaves.iter().zip(&variant_idents).map(|(_leaf, ident)| {
let const_name = format_ident!("__routes_chain_{}_{}", enum_ident, ident);
quote! { #enum_ident::#ident { .. } => &#const_name }
});
let leaves_name = format_ident!("__routes_leaves_{}", enum_ident);
let leaf_chains = variant_idents.iter().map(|ident| {
let const_name = format_ident!("__routes_chain_{}_{}", enum_ident, ident);
quote! { &#const_name }
});
let reached_arms = leaves.iter().zip(&variant_idents).enumerate().flat_map(|(index, (leaf, ident))| {
let layouts = &layouts;
let layout_params_idents = &layout_params_idents;
let enum_ident = &enum_ident;
leaf.ancestors.iter().enumerate().filter_map(move |(at, ancestor)| {
let position = layouts
.iter()
.position(|layout| same_type(&layout.ty, ancestor))
.expect("every ancestor of a leaf is a layout of this tree");
let carried = &layouts[position].fields;
let reachable = leaf.fields.iter().all(|field| {
field.identity
&& carried
.iter()
.any(|other| other.name == field.name && same_type(&other.ty, &field.ty))
});
if !reachable {
return None;
}
let params = &layout_params_idents[position];
let taken = leaf.fields.iter().map(|field| {
let name = &field.name;
quote! { #name: params.#name.clone() }
});
Some(quote! {
(#index, #at) => params
.downcast_ref::<#params>()
.map(|params| #enum_ident::#ident { #(#taken),* })
})
})
}).collect::<Vec<_>>();
let sample_mod = module_of_router(&guinea, "sample");
let samples = leaves.iter().zip(&variant_idents).map(|(leaf, ident)| {
let name = ident.to_string();
let made = leaf.fields.iter().map(|field| {
let (field, ty) = (&field.name, &field.ty);
quote! { #field: (&#sample_mod::Probe::<#ty>::new()).make()? }
});
quote! {
(|| ::core::option::Option::Some(#enum_ident::#ident { #(#made),* }))().ok_or(#name)
}
});
let tree_name = format_ident!("__routes_tree_{}", enum_ident);
let tree_chains = leaves.iter().zip(&variant_idents).flat_map(|(leaf, ident)| {
let tree = &enum_ident;
let const_name = format_ident!("__routes_chain_{}_{}", tree, ident);
let part_chains = leaf.parts.iter().enumerate().flat_map(|(at, parts)| {
(0..parts.len()).map(move |n| {
let chain = part_chain_name(tree, ident, at, n);
quote! { &#chain }
})
});
std::iter::once(quote! { &#const_name }).chain(part_chains).collect::<Vec<_>>()
}).collect::<Vec<_>>();
let expanded = quote! {
#[derive(Clone)]
#enum_derives
pub enum #enum_ident {
#(#variant_defs),*
}
#enum_impls
impl #enum_ident {
pub fn link(&self) -> Option<String> {
match self {
#(#link_arms),*
}
}
pub fn deep_links() -> &'static [#link_mod::DeepLink] {
&[#(#declared_links),*]
}
pub fn name(&self) -> &'static str {
match self {
#(#name_arms),*
}
}
pub const RESTORABLE: bool = #restorable;
pub fn save(&self) -> Option<String> {
match self {
#(#save_arms),*
}
}
pub fn restore(text: &str) -> Option<Self> {
let (__route, __fields) = #restore_mod::Restoring::open(text)?;
match __route.as_str() {
#(#restore_arms,)*
_ => None,
}
}
#parse_fn
pub fn params(&self) -> Vec<::std::boxed::Box<dyn ::std::any::Any>> {
match self {
#(#params_arms),*
}
}
}
#(#params_defs)*
#(#layout_params_defs)*
#(#chain_consts)*
#(#guard_consts)*
#[allow(non_upper_case_globals)]
const #tree_name: &[&[#router::SegmentEntry<#backend_ty>]] = &[#(#tree_chains),*];
#[allow(non_upper_case_globals)]
const #leaves_name: &[&[#router::SegmentEntry<#backend_ty>]] = &[#(#leaf_chains),*];
impl #router::RouteChain<#backend_ty> for #enum_ident {
fn chain(&self) -> &'static [#router::SegmentEntry<#backend_ty>] {
match self {
#(#chain_arms),*
}
}
fn params(&self) -> Vec<::std::boxed::Box<dyn ::std::any::Any>> {
#enum_ident::params(self)
}
fn name(&self) -> &'static str {
#enum_ident::name(self)
}
fn describe(&self) -> String {
format!("{self:?}")
}
fn link(&self) -> Option<String> {
#enum_ident::link(self)
}
fn guards(&self) -> &'static [&'static dyn #enter::EnterGuard] {
match self {
#(#guard_arms),*
}
}
fn application(&self) -> ::core::option::Option<#router::AppItem> {
#application
}
fn tree() -> &'static [&'static [#router::SegmentEntry<#backend_ty>]] {
#tree_name
}
fn leaves() -> &'static [&'static [#router::SegmentEntry<#backend_ty>]] {
#leaves_name
}
#[allow(clippy::redundant_closure_call)]
fn samples() -> ::std::vec::Vec<::core::result::Result<Self, &'static str>> {
#[allow(unused_imports)]
use #sample_mod::{ByDefault as _, Without as _};
::std::vec![#(#samples),*]
}
#[allow(unused_variables)]
fn reached_from(
leaf: usize,
at: usize,
params: &dyn ::std::any::Any,
) -> ::core::option::Option<Self> {
match (leaf, at) {
#(#reached_arms,)*
_ => ::core::option::Option::None,
}
}
}
};
expanded.into()
}