Skip to main content

path_scan_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4    Expr, Ident, LitStr, Token,
5    parse::{Parse, ParseStream},
6    parse_macro_input,
7};
8
9/// 1 つの腕(arm)
10/// - `patterns`: 1 つ以上の LitStr(`|` で連結)。デフォルト腕の場合は空。
11/// - `expr`: 変換式(ブロック・単一式どちらも可)
12/// - `cond`: オプションの if 条件
13struct Arm {
14    patterns: Vec<LitStr>,
15    cond: Option<Expr>,
16    expr: Expr,
17}
18
19impl Parse for Arm {
20    fn parse(input: ParseStream) -> syn::Result<Self> {
21        let mut patterns = Vec::new();
22        // 最初のトークン:LitStr か "_" か
23        if input.peek(LitStr) {
24            patterns.push(input.parse()?);
25        } else if input.peek(Token![_]) {
26            // default 腕
27            input.parse::<Token![_]>()?;
28        } else {
29            return Err(input.error("Expected string literal or '_' for default arm"));
30        }
31        // 続いて、もし '|' があれば追加のパターンを読み込む
32        while input.peek(Token![|]) {
33            input.parse::<Token![|]>()?;
34            // 必ず LitStr として
35            patterns.push(input.parse()?);
36        }
37        // オプション:if 条件
38        let cond = if input.peek(Token![if]) {
39            input.parse::<Token![if]>()?;
40            Some(input.parse()?)
41        } else {
42            None
43        };
44        // 区切りとして "=>" または ":" を期待
45        if input.peek(Token![=>]) {
46            input.parse::<Token![=>]>()?;
47        } else if input.peek(Token![:]) {
48            input.parse::<Token![:]>()?;
49        } else {
50            return Err(input.error("Expected '=>' or ':' after patterns"));
51        }
52        // 変換式部分をパース(ブロックでも単一式でも受け付ける)
53        let expr: Expr = input.parse()?;
54
55        Ok(Arm { patterns, expr, cond })
56    }
57}
58
59/// 複数の腕(arm)を読み込む。各腕はカンマまたは改行で区切られてもよい。
60struct Arms {
61    arms: Vec<Arm>,
62    completeness: bool,
63}
64
65impl Parse for Arms {
66    fn parse(input: ParseStream) -> syn::Result<Self> {
67        let (mut arms, mut completeness) = (Vec::new(), false);
68        while !input.is_empty() {
69            let arm = input.parse::<Arm>()?;
70            if arm.patterns.is_empty() && arm.cond.is_none() {
71                completeness = true;
72            }
73            arms.push(arm);
74            // カンマはオプション
75            if input.peek(Token![,]) {
76                input.parse::<Token![,]>()?;
77            }
78        }
79        Ok(Arms { arms, completeness })
80    }
81}
82
83/// パターン文字列を正規表現に変換し、キャプチャする識別子のリストを返す。
84/// - リテラル部分は `escape` でエスケープ
85/// - プレースホルダーは、`:identifier` または `{identifier}` の形式に対応し、
86///   いずれも `(?P<identifier>[^/]+)` として変換する。
87fn compile_pattern(pattern: &str) -> (String, Vec<String>) {
88    let mut regex_pattern = String::new();
89    let mut identifiers = Vec::new();
90    regex_pattern.push('^');
91
92    let mut chars = pattern.chars().peekable();
93    while let Some(ch) = chars.next() {
94        match ch {
95            '\\' => {
96                if let Some(next_ch) = chars.next() {
97                    regex_pattern.push_str(&regex::escape(&next_ch.to_string()));
98                }
99            }
100            '{' => {
101                let mut ident = String::new();
102                while let Some(ch2) = chars.next() {
103                    if ch2 == '}' {
104                        break;
105                    } else {
106                        ident.push(ch2);
107                    }
108                }
109                if !ident.is_empty() {
110                    identifiers.push(ident.clone());
111                    regex_pattern.push_str(&format!("(?P<{}>[^/]+)", ident));
112                }
113            }
114            ':' => {
115                let mut ident = String::new();
116                while let Some(&ch2) = chars.peek() {
117                    if ch2.is_alphanumeric() || ch2 == '_' {
118                        ident.push(ch2);
119                        chars.next();
120                    } else {
121                        break;
122                    }
123                }
124                if !ident.is_empty() {
125                    identifiers.push(ident.clone());
126                    regex_pattern.push_str(&format!("(?P<{}>[^/]+)", ident));
127                } else {
128                    regex_pattern.push_str(&regex::escape(&":".to_string()));
129                }
130            }
131            _ => {
132                regex_pattern.push_str(&regex::escape(&ch.to_string()));
133            }
134        }
135    }
136    regex_pattern.push('$');
137    (regex_pattern, identifiers)
138}
139
140/// `path_scan!` macro parses an input string using the provided patterns
141/// and returns a closure that yields the result of the first matching arm.
142///
143/// Patterns are specified as follows:
144///
145/// - `"pattern string" => expression`
146///   In the pattern string, placeholders like `:identifier` and `{identifier}` are
147///   captured and become available as variables.
148///
149/// - Multiple patterns can be combined using `"pattern1" | "pattern2" => expression`.
150///
151/// - An optional `if` condition may follow the pattern(s) (e.g., `"pattern" if condition => expression`).
152///   The arm is considered a match only if the condition evaluates to true.
153///
154/// - The default arm is specified as `_ => expression`.
155///
156/// **Note:**
157/// If an if-less default arm is not provided—that is, if a default arm without an `if` condition is missing—
158/// the closure returns `None` on failure, making the overall expression type `Option<T>`.
159///
160/// # Examples
161///
162/// ```rust
163/// use path_scan::path_scan;
164///
165/// // With a default arm, the return type is a concrete type (e.g., String)
166/// let scanner = path_scan! {
167///     "blog/:slug/index" => format!("blog: {}", slug),
168///     "other/:slug" if slug.len() == 5 => format!("short blog: {}", slug),
169///     _ => format!("default")
170/// };
171///
172/// assert_eq!(scanner("blog/hello/index"), "blog: hello");
173/// assert_eq!(scanner("other/short"), "short blog: short");
174/// assert_eq!(scanner("unknown/path"), "default");
175///
176/// // Without a default arm, the return type is `Option<String>`
177/// let scanner_opt = path_scan! {
178///     "blog/:slug/index" => format!("blog: {}", slug)
179/// };
180///
181/// assert_eq!(scanner_opt("blog/world/index"), Some("blog: world".to_string()));
182/// assert_eq!(scanner_opt("unknown/path"), None);
183/// ```
184#[proc_macro]
185pub fn path_scan(input: TokenStream) -> TokenStream {
186    let Arms { arms, completeness } = parse_macro_input!(input as Arms);
187
188    let mut arm_match_tokens = Vec::new();
189    // 各腕について処理(Arm を直接分解)
190    for Arm { patterns, expr, cond } in arms.into_iter() {
191        let value = if completeness {
192            quote! { #expr }
193        } else {
194            quote! { Some({ #expr }) }
195        };
196        if !patterns.is_empty() {
197            for pat_lit in patterns.into_iter() {
198                let pattern_str = pat_lit.value();
199                let (regex_str, idents) = compile_pattern(&pattern_str);
200                let regex_lit = LitStr::new(&regex_str, pat_lit.span());
201                let mut bindings = Vec::new();
202                for ident in idents {
203                    let ident_token = Ident::new(&ident, pat_lit.span());
204                    bindings.push(quote! {
205                        let #ident_token = __caps.name(#ident).map(|m| m.as_str()).unwrap();
206                    });
207                }
208                let cond_check = if let Some(cond_expr) = &cond {
209                    quote! { if #cond_expr }
210                } else {
211                    quote! { if true }
212                };
213                arm_match_tokens.push(quote! {
214                    if let Some(__caps) = ::path_scan::regex::Regex::new(#regex_lit).unwrap().captures(input) {
215                        #(#bindings)*
216                        #cond_check {
217                            return #value;
218                        }
219                    }
220                });
221            }
222        } else {
223            // デフォルト腕の場合
224            let cond_check = if let Some(cond_expr) = &cond {
225                quote! { if #cond_expr }
226            } else {
227                quote! { if true }
228            };
229            arm_match_tokens.push(quote! {
230                #cond_check {
231                    return #value;
232                }
233            });
234        }
235    }
236
237    let last = if completeness {
238        quote! {unreachable!()}
239    } else {
240        quote! {None}
241    };
242    let expanded = quote! {
243        {
244            move |input: &str| {
245                #(#arm_match_tokens)*
246                #last
247            }
248        }
249    };
250    TokenStream::from(expanded)
251}
252
253/// path_scan_val! マクロの入力をパースするための構文要素
254struct PathScanValInput {
255    input_expr: Expr,
256    arms: Arms,
257}
258
259impl Parse for PathScanValInput {
260    fn parse(input: ParseStream) -> syn::Result<Self> {
261        let input_expr: Expr = input.parse()?;
262        // カンマはオプション
263        if input.peek(Token![,]) {
264            input.parse::<Token![,]>()?;
265        }
266        if input.peek(syn::token::Brace) {
267            let content;
268            let _brace_token = syn::braced!(content in input);
269            let arms: Arms = content.parse()?;
270            Ok(PathScanValInput { input_expr, arms })
271        } else {
272            let arms: Arms = input.parse()?;
273            Ok(PathScanValInput { input_expr, arms })
274        }
275    }
276}
277
278/// `path_scan_val!` macro parses an input string using the provided patterns
279/// and returns the result of the first matching arm directly.
280///
281/// This macro supports two forms:
282///
283/// 1. **Comma Form**
284///    ```ignore
285///    path_scan_val!("input string", "pattern string" => expression, ... )
286///    ```
287///
288/// 2. **Brace Form**
289///    ```ignore
290///    path_scan_val!("input string" { "pattern string" => expression, ... })
291///    ```
292///
293/// Patterns are specified as follows:
294///
295/// - `"pattern string" => expression`
296///   In the pattern string, placeholders like `:identifier` and `{identifier}` are
297///   captured and become available as variables.
298///
299/// - Multiple patterns can be combined using `"pattern1" | "pattern2" => expression`.
300///
301/// - An optional `if` condition may follow the pattern(s) (e.g., `"pattern" if condition => expression`).
302///   The arm is considered a match only if the condition evaluates to true.
303///
304/// - The default arm is specified as `_ => expression`.
305///
306/// **Note:**
307/// If an if-less default arm is not provided—that is, if a default arm without an `if` condition is missing—
308/// the macro returns `None` on failure, making the overall expression type `Option<T>`.
309/// Commas between arms are optional.
310///
311/// # Examples
312///
313/// **Comma Form**
314/// ```rust
315/// # use path_scan::path_scan_val;
316/// let result = path_scan_val!("blog/hello/index",
317///     "blog/{slug}/index" => format!("blog: {}", slug),
318///     _ => format!("default")
319/// );
320/// assert_eq!(result, format!("blog: hello"));
321/// ```
322///
323/// **Brace Form**
324/// ```rust
325/// # use path_scan::path_scan_val;
326/// let result = path_scan_val!("blog/world/index" {
327///     "blog/{slug}/index" => format!("blog: {}", slug),
328///     _ => format!("default")
329/// });
330/// assert_eq!(result, format!("blog: world"));
331/// ```
332///
333/// **Without a Default Arm (returns `Option<T>`)**
334/// ```rust
335/// # use path_scan::path_scan_val;
336/// let result = path_scan_val!("blog/foo/index",
337///     "blog/{slug}/index" => format!("blog: {}", slug)
338/// );
339/// assert_eq!(result, Some(format!("blog: foo")));
340///
341/// let result2 = path_scan_val!("unknown/path",
342///     "blog/{slug}/index" => format!("blog: {}", slug)
343/// );
344/// assert_eq!(result2, None);
345/// ```
346#[proc_macro]
347pub fn path_scan_val(input: TokenStream) -> TokenStream {
348    let PathScanValInput { input_expr, arms } = parse_macro_input!(input as PathScanValInput);
349    let Arms { arms, completeness } = arms;
350
351    let mut arm_match_tokens = Vec::new();
352    for (arm_index, Arm { patterns, expr, cond }) in arms.into_iter().enumerate() {
353        let value = if completeness {
354            quote! { #expr }
355        } else {
356            quote! { Some({ #expr }) }
357        };
358        if !patterns.is_empty() {
359            for (pat_index, pat_lit) in patterns.into_iter().enumerate() {
360                let pattern_str = pat_lit.value();
361                let (regex_str, idents) = compile_pattern(&pattern_str);
362                let regex_lit = LitStr::new(&regex_str, pat_lit.span());
363                let mut bindings = Vec::new();
364                let regex_var = format_ident!("__regex_{}_{}", arm_index, pat_index);
365                for ident in idents {
366                    let ident_token = Ident::new(&ident, pat_lit.span());
367                    bindings.push(quote! {
368                        let #ident_token = __caps.name(#ident).map(|m| m.as_str()).unwrap();
369                    });
370                }
371                let cond_check = if let Some(ref cond_expr) = cond {
372                    quote! { if #cond_expr }
373                } else {
374                    quote! { if true }
375                };
376                arm_match_tokens.push(quote! {
377                    static #regex_var: ::path_scan::once_cell::sync::Lazy<::path_scan::regex::Regex> = ::path_scan::once_cell::sync::Lazy::new(|| ::path_scan::regex::Regex::new(#regex_lit).unwrap());
378                    if let Some(__caps) = #regex_var.captures(__input) {
379                        #(#bindings)*
380                        #cond_check {
381                            break 'x #value;
382                        }
383                    }
384                });
385            }
386        } else {
387            let cond_check = if let Some(ref cond_expr) = cond {
388                quote! { if #cond_expr }
389            } else {
390                quote! { if true }
391            };
392            arm_match_tokens.push(quote! {
393                #cond_check {
394                    break 'x #value;
395                }
396            });
397        }
398    }
399
400    let last = if completeness {
401        quote! { unreachable!() }
402    } else {
403        quote! { None }
404    };
405
406    let expanded = quote! {
407        {
408            let __input = #input_expr;
409            #[allow(unreachable_code)]
410            let __result = 'x: {
411                #(#arm_match_tokens)*
412                break 'x #last;
413            };
414            __result
415        }
416    };
417
418    TokenStream::from(expanded)
419}