mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: add path!
macro in router to parse string paths into tuples (#2694)
This commit is contained in:
parent
873aec5787
commit
f4f129caaf
3 changed files with 261 additions and 28 deletions
|
@ -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 }
|
||||
|
|
|
@ -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(¤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),*) }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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*");
|
||||
// }
|
||||
|
|
Loading…
Reference in a new issue