mirror of
https://github.com/leptos-rs/leptos
synced 2024-09-20 06:21:57 +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-macro-error = { version = "1", default-features = false }
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
syn = { version = "2", features = ["full"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
leptos_router = { workspace = true }
|
leptos_router = { workspace = true }
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
use proc_macro::{TokenStream, TokenTree};
|
use proc_macro::{TokenStream, TokenTree};
|
||||||
|
use proc_macro2::Span;
|
||||||
|
use proc_macro_error::abort;
|
||||||
use quote::{quote, ToTokens};
|
use quote::{quote, ToTokens};
|
||||||
use std::borrow::Cow;
|
|
||||||
use syn::{
|
const RFC3986_UNRESERVED: [char; 4] = ['-', '.', '_', '~'];
|
||||||
parse::{Parse, ParseStream},
|
const RFC3986_PCHAR_OTHER: [char; 1] = ['@'];
|
||||||
parse_macro_input,
|
|
||||||
token::Token,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[proc_macro_error::proc_macro_error]
|
#[proc_macro_error::proc_macro_error]
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
|
@ -21,12 +20,13 @@ struct Segments(pub Vec<Segment>);
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum Segment {
|
enum Segment {
|
||||||
Static(Cow<'static, str>),
|
Static(String),
|
||||||
|
Param(String),
|
||||||
|
Wildcard(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SegmentParser {
|
struct SegmentParser {
|
||||||
input: proc_macro::token_stream::IntoIter,
|
input: proc_macro::token_stream::IntoIter,
|
||||||
current_str: Option<String>,
|
|
||||||
segments: Vec<Segment>,
|
segments: Vec<Segment>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,6 @@ impl SegmentParser {
|
||||||
pub fn new(input: TokenStream) -> Self {
|
pub fn new(input: TokenStream) -> Self {
|
||||||
Self {
|
Self {
|
||||||
input: input.into_iter(),
|
input: input.into_iter(),
|
||||||
current_str: None,
|
|
||||||
segments: Vec::new(),
|
segments: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,32 +44,106 @@ impl SegmentParser {
|
||||||
for input in self.input.by_ref() {
|
for input in self.input.by_ref() {
|
||||||
match input {
|
match input {
|
||||||
TokenTree::Literal(lit) => {
|
TokenTree::Literal(lit) => {
|
||||||
|
let lit = lit.to_string();
|
||||||
|
if lit.contains("//") {
|
||||||
|
abort!(
|
||||||
|
proc_macro2::Span::call_site(),
|
||||||
|
"Consecutive '/' is not allowed"
|
||||||
|
);
|
||||||
|
}
|
||||||
Self::parse_str(
|
Self::parse_str(
|
||||||
lit.to_string()
|
|
||||||
.trim_start_matches(['"', '/'])
|
|
||||||
.trim_end_matches(['"', '/']),
|
|
||||||
&mut self.segments,
|
&mut self.segments,
|
||||||
|
lit.trim_start_matches(['"', '/'])
|
||||||
|
.trim_end_matches(['"', '/']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
TokenTree::Group(_) => todo!(),
|
TokenTree::Group(_) => unimplemented!(),
|
||||||
TokenTree::Ident(_) => todo!(),
|
TokenTree::Ident(_) => unimplemented!(),
|
||||||
TokenTree::Punct(_) => todo!(),
|
TokenTree::Punct(_) => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_str(current_str: &str, segments: &mut Vec<Segment>) {
|
pub fn parse_str(segments: &mut Vec<Segment>, current_str: &str) {
|
||||||
let mut chars = current_str.chars();
|
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 {
|
impl ToTokens for Segments {
|
||||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
let children = quote! {};
|
self.ensure_valid();
|
||||||
if self.0.len() != 1 {
|
match self.0.as_slice() {
|
||||||
tokens.extend(quote! { (#children) });
|
[] => tokens.extend(quote! { () }),
|
||||||
} else {
|
[segment] => tokens.extend(quote! { (#segment,) }),
|
||||||
tokens.extend(children)
|
segments => tokens.extend(quote! { (#(#segments),*) }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,170 @@
|
||||||
use routing::StaticSegment;
|
use leptos_router::ParamSegment;
|
||||||
use routing_macro::path;
|
use leptos_router::StaticSegment;
|
||||||
|
use leptos_router::WildcardSegment;
|
||||||
|
use leptos_router_macro::path;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_empty_list() {
|
fn parses_empty_string() {
|
||||||
let output = path!("");
|
let output = path!("");
|
||||||
assert_eq!(output, ());
|
assert!(output.eq(&()));
|
||||||
//let segments: Segments = syn::parse(path.into()).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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