feat: add path! macro in router to parse string paths into tuples (#2694)

This commit is contained in:
boyswan 2024-07-19 14:13:01 +01:00 committed by Greg Johnston
parent 873aec5787
commit f4f129caaf
3 changed files with 261 additions and 28 deletions

View file

@ -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 }

View file

@ -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<Segment>);
#[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<String>,
segments: Vec<Segment>,
}
@ -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) => {
Self::parse_str(
lit.to_string()
.trim_start_matches(['"', '/'])
.trim_end_matches(['"', '/']),
&mut self.segments,
let lit = lit.to_string();
if lit.contains("//") {
abort!(
proc_macro2::Span::call_site(),
"Consecutive '/' is not allowed"
);
}
TokenTree::Group(_) => todo!(),
TokenTree::Ident(_) => todo!(),
TokenTree::Punct(_) => todo!(),
Self::parse_str(
&mut self.segments,
lit.trim_start_matches(['"', '/'])
.trim_end_matches(['"', '/']),
);
}
TokenTree::Group(_) => unimplemented!(),
TokenTree::Ident(_) => unimplemented!(),
TokenTree::Punct(_) => unimplemented!(),
}
}
}
pub fn parse_str(current_str: &str, segments: &mut Vec<Segment>) {
let mut chars = current_str.chars();
pub fn parse_str(segments: &mut Vec<Segment>, current_str: &str) {
if ["", "*"].contains(&current_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),*) }),
}
}
}

View file

@ -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*");
// }