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(®ex::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(®ex::escape(&":".to_string()));
129 }
130 }
131 _ => {
132 regex_pattern.push_str(®ex::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(®ex_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(®ex_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}