diff --git a/router_macro/Cargo.toml b/router_macro/Cargo.toml index dde88a1ed..2e893aec8 100644 --- a/router_macro/Cargo.toml +++ b/router_macro/Cargo.toml @@ -10,7 +10,6 @@ proc-macro = true proc-macro-error = { version = "1", default-features = false } proc-macro2 = "1" quote = "1" -syn = { version = "2", features = ["full"] } [dev-dependencies] leptos_router = { workspace = true } diff --git a/router_macro/src/lib.rs b/router_macro/src/lib.rs index 26a95f0ce..9afe47b24 100644 --- a/router_macro/src/lib.rs +++ b/router_macro/src/lib.rs @@ -1,11 +1,10 @@ use proc_macro::{TokenStream, TokenTree}; +use proc_macro2::Span; +use proc_macro_error::abort; use quote::{quote, ToTokens}; -use std::borrow::Cow; -use syn::{ - parse::{Parse, ParseStream}, - parse_macro_input, - token::Token, -}; + +const RFC3986_UNRESERVED: [char; 4] = ['-', '.', '_', '~']; +const RFC3986_PCHAR_OTHER: [char; 1] = ['@']; #[proc_macro_error::proc_macro_error] #[proc_macro] @@ -21,12 +20,13 @@ struct Segments(pub Vec); #[derive(Debug, PartialEq)] enum Segment { - Static(Cow<'static, str>), + Static(String), + Param(String), + Wildcard(String), } struct SegmentParser { input: proc_macro::token_stream::IntoIter, - current_str: Option, segments: Vec, } @@ -34,7 +34,6 @@ impl SegmentParser { pub fn new(input: TokenStream) -> Self { Self { input: input.into_iter(), - current_str: None, segments: Vec::new(), } } @@ -45,32 +44,106 @@ impl SegmentParser { for input in self.input.by_ref() { match input { TokenTree::Literal(lit) => { + let lit = lit.to_string(); + if lit.contains("//") { + abort!( + proc_macro2::Span::call_site(), + "Consecutive '/' is not allowed" + ); + } Self::parse_str( - lit.to_string() - .trim_start_matches(['"', '/']) - .trim_end_matches(['"', '/']), &mut self.segments, + lit.trim_start_matches(['"', '/']) + .trim_end_matches(['"', '/']), ); } - TokenTree::Group(_) => todo!(), - TokenTree::Ident(_) => todo!(), - TokenTree::Punct(_) => todo!(), + TokenTree::Group(_) => unimplemented!(), + TokenTree::Ident(_) => unimplemented!(), + TokenTree::Punct(_) => unimplemented!(), } } } - pub fn parse_str(current_str: &str, segments: &mut Vec) { - let mut chars = current_str.chars(); + pub fn parse_str(segments: &mut Vec, current_str: &str) { + if ["", "*"].contains(¤t_str) { + return; + } + + for segment in current_str.split('/') { + if let Some(segment) = segment.strip_prefix(':') { + segments.push(Segment::Param(segment.to_string())); + } else if let Some(segment) = segment.strip_prefix('*') { + segments.push(Segment::Wildcard(segment.to_string())); + } else { + segments.push(Segment::Static(segment.to_string())); + } + } + } +} + +impl Segment { + fn is_valid(segment: &str) -> bool { + segment.chars().all(|c| { + c.is_ascii_digit() + || c.is_ascii_lowercase() + || c.is_ascii_uppercase() + || RFC3986_UNRESERVED.contains(&c) + || RFC3986_PCHAR_OTHER.contains(&c) + }) + } + + fn ensure_valid(&self) { + match self { + Self::Wildcard(s) if !Self::is_valid(s) => { + abort!(Span::call_site(), "Invalid wildcard segment: {}", s) + } + Self::Static(s) if !Self::is_valid(s) => { + abort!(Span::call_site(), "Invalid static segment: {}", s) + } + Self::Param(s) if !Self::is_valid(s) => { + abort!(Span::call_site(), "Invalid param segment: {}", s) + } + _ => (), + } + } +} + +impl Segments { + fn ensure_valid(&self) { + if let Some((_last, segments)) = self.0.split_last() { + if let Some(Segment::Wildcard(s)) = + segments.iter().find(|s| matches!(s, Segment::Wildcard(_))) + { + abort!(Span::call_site(), "Wildcard must be at end: {}", s) + } + } + } +} + +impl ToTokens for Segment { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.ensure_valid(); + match self { + Segment::Wildcard(s) => { + tokens.extend(quote! { leptos_router::WildcardSegment(#s) }); + } + Segment::Static(s) => { + tokens.extend(quote! { leptos_router::StaticSegment(#s) }); + } + Segment::Param(p) => { + tokens.extend(quote! { leptos_router::ParamSegment(#p) }); + } + } } } impl ToTokens for Segments { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let children = quote! {}; - if self.0.len() != 1 { - tokens.extend(quote! { (#children) }); - } else { - tokens.extend(children) + self.ensure_valid(); + match self.0.as_slice() { + [] => tokens.extend(quote! { () }), + [segment] => tokens.extend(quote! { (#segment,) }), + segments => tokens.extend(quote! { (#(#segments),*) }), } } } diff --git a/router_macro/tests/path.rs b/router_macro/tests/path.rs index 5d80fd422..adb7666c9 100644 --- a/router_macro/tests/path.rs +++ b/router_macro/tests/path.rs @@ -1,9 +1,170 @@ -use routing::StaticSegment; -use routing_macro::path; +use leptos_router::ParamSegment; +use leptos_router::StaticSegment; +use leptos_router::WildcardSegment; +use leptos_router_macro::path; #[test] -fn parses_empty_list() { +fn parses_empty_string() { let output = path!(""); - assert_eq!(output, ()); - //let segments: Segments = syn::parse(path.into()).unwrap(); + assert!(output.eq(&())); } + +#[test] +fn parses_single_slash() { + let output = path!("/"); + assert!(output.eq(&())); +} + +#[test] +fn parses_single_asterisk() { + let output = path!("*"); + assert!(output.eq(&())); +} + +#[test] +fn parses_slash_asterisk() { + let output = path!("/*"); + assert!(output.eq(&())); +} + +#[test] +fn parses_asterisk_any() { + let output = path!("/foo/:bar/*any"); + assert_eq!( + output, + ( + StaticSegment("foo"), + ParamSegment("bar"), + WildcardSegment("any") + ) + ); +} + +#[test] +fn parses_hyphen() { + let output = path!("/foo/bar-baz"); + assert_eq!(output, (StaticSegment("foo"), StaticSegment("bar-baz"))); +} + +#[test] +fn parses_rfc3976_unreserved() { + let output = path!("/-._~"); + assert_eq!(output, (StaticSegment("-._~"),)); +} + + +#[test] +fn parses_rfc3976_pchar_other() { + let output = path!("/@"); + assert_eq!(output, (StaticSegment("@"),)); +} + +#[test] +fn parses_no_slashes() { + let output = path!("home"); + assert_eq!(output, (StaticSegment("home"),)); +} + +#[test] +fn parses_no_leading_slash() { + let output = path!("home/"); + assert_eq!(output, (StaticSegment("home"),)); +} + +#[test] +fn parses_trailing_slash() { + let output = path!("/home/"); + assert_eq!(output, (StaticSegment("home"),)); +} + +#[test] +fn parses_single_static() { + let output = path!("/home"); + assert_eq!(output, (StaticSegment("home"),)); +} + +#[test] +fn parses_single_param() { + let output = path!("/:id"); + assert_eq!(output, (ParamSegment("id"),)); +} + +#[test] +fn parses_static_and_param() { + let output = path!("/home/:id"); + assert_eq!(output, (StaticSegment("home"), ParamSegment("id"),)); +} + +#[test] +fn parses_mixed_segment_types() { + let output = path!("/foo/:bar/*baz"); + assert_eq!( + output, + ( + StaticSegment("foo"), + ParamSegment("bar"), + WildcardSegment("baz") + ) + ); +} + +#[test] +fn parses_consecutive_static() { + let output = path!("/foo/bar/baz"); + assert_eq!( + output, + ( + StaticSegment("foo"), + StaticSegment("bar"), + StaticSegment("baz") + ) + ); +} + +#[test] +fn parses_consecutive_param() { + let output = path!("/:foo/:bar/:baz"); + assert_eq!( + output, + ( + ParamSegment("foo"), + ParamSegment("bar"), + ParamSegment("baz") + ) + ); +} + +#[test] +fn parses_complex() { + let output = path!("/home/:id/foo/:bar/*any"); + assert_eq!( + output, + ( + StaticSegment("home"), + ParamSegment("id"), + StaticSegment("foo"), + ParamSegment("bar"), + WildcardSegment("any"), + ) + ); +} + +// #[test] +// fn deny_consecutive_slashes() { +// let _ = path!("/////foo///bar/////baz/"); +// } +// +// #[test] +// fn deny_invalid_segment() { +// let _ = path!("/foo/^/"); +// } +// +// #[test] +// fn deny_non_trailing_wildcard_segment() { +// let _ = path!("/home/*any/end"); +// } +// +// #[test] +// fn deny_invalid_wildcard() { +// let _ = path!("/home/any*"); +// }