autumn-macros 0.9.0

Proc macros for the Autumn web framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
//! `#[edge]` proc macro implementation (#1790).
//!
//! `#[edge]` is a *marker* attribute: it declares that a read-path handler is
//! eligible to run inside the edge capsule (a `wasm32-wasip1` artifact built
//! alongside the native binary). Like [`#[public]`](crate::public) it injects
//! no runtime guard and does not rewrite the handler signature — it only emits
//! marker consts that the route macros (`#[get]`, …) read back:
//!
//! - `const __AUTUMN_EDGE: () = ();` — the route opted in;
//! - `const __AUTUMN_EDGE_NEEDS_KV: () = ();` — it also declared `needs(kv)`.
//!
//! The markers mirror `#[secured]`'s `__AUTUMN_SECURED_ROLES` mechanism so the
//! route macro can detect the declaration whether `#[edge]` sits above or below
//! the route attribute in the stack — attribute expansion order is not
//! guaranteed, and an undetected opt-in would silently drop the route from the
//! capsule.
//!
//! Everything this module knows about is *declaration*; the code generation for
//! an edge route lives in [`crate::route`], and the refusals for route macros
//! that can never serve from the edge live with those macros.

use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned as _;
use syn::{Ident, ItemFn, Stmt, Token, parse_quote};

/// Marker const injected for every `#[edge]` handler.
pub const EDGE_MARKER: &str = "__AUTUMN_EDGE";
/// Marker const injected for `#[edge(needs(kv))]`.
pub const EDGE_NEEDS_KV_MARKER: &str = "__AUTUMN_EDGE_NEEDS_KV";
/// Marker const injected for `#[edge(needs(identity))]`.
pub const EDGE_NEEDS_IDENTITY_MARKER: &str = "__AUTUMN_EDGE_NEEDS_IDENTITY";

/// The grammar `#[edge]` accepts, quoted verbatim in every rejection so a typo
/// is answered with the whole (small) surface rather than a hint.
const SUPPORTED_GRAMMAR: &str = "`#[edge]` or `#[edge(needs(kv, identity))]`";

/// Capabilities that may appear inside `needs(...)`. The edge host mediates
/// each one; anything else is a platform seam that does not exist yet.
const SUPPORTED_CAPABILITIES: &[&str] = &["kv", "identity"];

/// Parsed `#[edge(...)]` arguments.
#[derive(Default, Clone, Copy)]
pub struct EdgeAttrArgs {
    /// Whether `needs(kv)` was declared.
    pub needs_kv: bool,
    /// Whether `needs(identity)` was declared.
    pub needs_identity: bool,
}

impl Parse for EdgeAttrArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut declared: Vec<String> = Vec::new();
        let mut seen_needs = false;

        while !input.is_empty() {
            let span = input.span();
            let key: Ident = input.parse().map_err(|_| {
                syn::Error::new(
                    span,
                    format!("unknown `#[edge]` option. Supported forms: {SUPPORTED_GRAMMAR}."),
                )
            })?;

            if key != "needs" {
                return Err(syn::Error::new(
                    key.span(),
                    format!(
                        "unknown `#[edge]` option `{key}`. Supported forms: {SUPPORTED_GRAMMAR}."
                    ),
                ));
            }
            if !input.peek(syn::token::Paren) {
                return Err(syn::Error::new(
                    key.span(),
                    "`needs` takes parenthesized capabilities, e.g. `needs(kv)`.",
                ));
            }
            if seen_needs {
                return Err(syn::Error::new(
                    key.span(),
                    "duplicate `needs(...)` argument. Declare all capabilities in one `needs(...)`.",
                ));
            }
            seen_needs = true;
            declared = parse_needs_group(input)?;

            if input.peek(Token![,]) {
                let _comma: Token![,] = input.parse()?;
            } else {
                break;
            }
        }

        if !input.is_empty() {
            return Err(input.error(format!(
                "unexpected trailing tokens in `#[edge(...)]`. Supported forms: {SUPPORTED_GRAMMAR}."
            )));
        }

        Ok(Self {
            needs_kv: declared.iter().any(|name| name == "kv"),
            needs_identity: declared.iter().any(|name| name == "identity"),
        })
    }
}

/// Parse the body of a `needs(...)` argument, returning the capabilities named.
///
/// An empty `needs()` is rejected for the same reason an empty `seo()` is: it
/// is almost certainly an unfinished edit, and silently treating it as "no
/// capabilities" would ship a route that faults at the edge instead of failing
/// the build.
fn parse_needs_group(input: ParseStream) -> syn::Result<Vec<String>> {
    let span = input.span();
    let content;
    syn::parenthesized!(content in input);

    let mut declared: Vec<String> = Vec::new();
    while !content.is_empty() {
        let capability: Ident = content.parse()?;
        let name = capability.to_string();
        let name = name.strip_prefix("r#").unwrap_or(&name).to_owned();
        if !SUPPORTED_CAPABILITIES.contains(&name.as_str()) {
            return Err(syn::Error::new(
                capability.span(),
                format!(
                    "unknown `needs(...)` capability `{name}`. Supported capabilities: `{}`.",
                    SUPPORTED_CAPABILITIES.join("`, `")
                ),
            ));
        }
        if declared.contains(&name) {
            return Err(syn::Error::new(
                capability.span(),
                format!("duplicate `needs(...)` capability `{name}`. Declare each one once."),
            ));
        }
        declared.push(name);

        if content.peek(Token![,]) {
            let _comma: Token![,] = content.parse()?;
        } else {
            break;
        }
    }

    if !content.is_empty() {
        return Err(content.error("expected `,` between `needs(...)` capabilities"));
    }
    if declared.is_empty() {
        return Err(syn::Error::new(
            span,
            format!(
                "empty `needs(...)`. Name at least one capability, e.g. `needs(kv)`, or drop the \
                 argument entirely. Supported capabilities: `{}`.",
                SUPPORTED_CAPABILITIES.join("`, `")
            ),
        ));
    }

    Ok(declared)
}

/// Expand `#[edge]` / `#[edge(needs(kv))]` on a route handler.
pub fn edge_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
    let args: EdgeAttrArgs = match syn::parse2(attr) {
        Ok(args) => args,
        Err(err) => return err.to_compile_error(),
    };

    let mut input_fn: ItemFn = match syn::parse2(item.clone()) {
        Ok(f) => f,
        Err(_) => {
            return syn::Error::new_spanned(
                item,
                "`#[edge]` can only be applied to route handler functions",
            )
            .to_compile_error();
        }
    };

    // Prepend the markers so a route macro that expands *after* this one can
    // read the declaration back out of the handler body. When the route macro
    // is outermost instead, it detects the `#[edge]` attribute directly (see
    // `detect`). Every other attribute on the handler is passed through
    // untouched — `#[edge]` composes, it does not consume.
    let marker: Stmt = parse_quote! {
        // Route macros read this marker when #[edge] expands before #[get].
        const __AUTUMN_EDGE: () = ();
    };
    input_fn.block.stmts.insert(0, marker);
    if args.needs_kv {
        let needs_kv: Stmt = parse_quote! {
            const __AUTUMN_EDGE_NEEDS_KV: () = ();
        };
        input_fn.block.stmts.insert(1, needs_kv);
    }
    if args.needs_identity {
        let needs_identity: Stmt = parse_quote! {
            const __AUTUMN_EDGE_NEEDS_IDENTITY: () = ();
        };
        input_fn.block.stmts.insert(1, needs_identity);
    }

    quote! { #input_fn }
}

/// An `#[edge]` declaration found on a handler.
#[derive(Clone, Copy)]
pub struct EdgeMarking {
    /// Whether the handler declared `needs(kv)`.
    pub needs_kv: bool,
    /// Whether the handler declared `needs(identity)`.
    pub needs_identity: bool,
    /// Where to point a rejection: the `#[edge]` attribute when it is still
    /// live, otherwise the handler's name.
    pub span: Span,
}

/// Detect an `#[edge]` declaration on a handler.
///
/// Mirrors [`crate::api_doc::is_public`]'s attribute/marker duality:
/// 1. `#[edge]` (or `#[autumn_web::edge]`) still present as an attribute, which
///    happens when the route macro is outermost and `#[edge]` has not expanded
///    yet;
/// 2. the `__AUTUMN_EDGE` marker const emitted into the handler body when
///    `#[edge]` expanded before the route macro.
///
/// The marker is decoded structurally, so handler *text* that merely spells it
/// out never opts a route into the edge lane.
pub fn detect(input_fn: &ItemFn) -> Option<EdgeMarking> {
    for attr in &input_fn.attrs {
        if attr.path().is_ident("edge")
            || attr
                .path()
                .segments
                .last()
                .is_some_and(|segment| segment.ident == "edge")
        {
            // A malformed argument list is reported by `#[edge]` itself when it
            // expands on the handler this macro re-emits; falling back to the
            // default here keeps the diagnostic single, not doubled.
            let args = match &attr.meta {
                syn::Meta::List(list) => {
                    syn::parse2::<EdgeAttrArgs>(list.tokens.clone()).unwrap_or_default()
                }
                _ => EdgeAttrArgs::default(),
            };
            return Some(EdgeMarking {
                needs_kv: args.needs_kv,
                needs_identity: args.needs_identity,
                span: attr.path().span(),
            });
        }
    }

    if stmts_have_marker(&input_fn.block.stmts, EDGE_MARKER) {
        return Some(EdgeMarking {
            needs_kv: stmts_have_marker(&input_fn.block.stmts, EDGE_NEEDS_KV_MARKER),
            needs_identity: stmts_have_marker(&input_fn.block.stmts, EDGE_NEEDS_IDENTITY_MARKER),
            span: input_fn.sig.ident.span(),
        });
    }

    None
}

/// Whether a generated marker const named `marker` is present in `stmts`.
///
/// Descends the wrappers other guards generate: `#[secured]`/`#[authorize]`/
/// `#[throttle]` re-home the original body inside
/// `let __autumn_inner: T = (async move { … }).await;`, which buries any marker
/// injected by an attribute that expanded before them.
pub fn stmts_have_marker(stmts: &[Stmt], marker: &str) -> bool {
    stmts.iter().any(|stmt| stmt_has_marker(stmt, marker))
}

fn stmt_has_marker(stmt: &Stmt, marker: &str) -> bool {
    match stmt {
        Stmt::Item(syn::Item::Const(item_const)) => item_const.ident == marker,
        Stmt::Expr(expr, _) => expr_has_marker(expr, marker),
        Stmt::Local(local) => local
            .init
            .as_ref()
            .is_some_and(|init| expr_has_marker(&init.expr, marker)),
        _ => false,
    }
}

fn expr_has_marker(expr: &syn::Expr, marker: &str) -> bool {
    match expr {
        syn::Expr::Block(block) => stmts_have_marker(&block.block.stmts, marker),
        syn::Expr::Unsafe(block) => stmts_have_marker(&block.block.stmts, marker),
        _ => crate::idempotency_guard::expr_nested_async_body(expr)
            .is_some_and(|block| stmts_have_marker(&block.stmts, marker)),
    }
}

/// Reject `#[edge]` on a route macro that can never serve from the edge lane.
///
/// `reason` is the complete diagnostic, spanned at the `#[edge]` attribute (or
/// at the handler's name once the attribute has expanded into its marker).
/// Returns `None` — carry on — when the handler is not edge-marked, and also
/// when the item is not a function, so the macro's own "this applies to
/// functions" error stays the one the author sees.
pub fn reject_if_edge(item: &TokenStream, reason: &str) -> Option<TokenStream> {
    let input_fn: ItemFn = syn::parse2(item.clone()).ok()?;
    let marking = detect(&input_fn)?;
    Some(syn::Error::new(marking.span, reason).to_compile_error())
}

#[cfg(test)]
mod tests {
    use quote::quote;

    use super::{EDGE_MARKER, EDGE_NEEDS_KV_MARKER, detect, edge_macro};

    fn parse_fn(tokens: proc_macro2::TokenStream) -> syn::ItemFn {
        syn::parse2(tokens).expect("test handler should parse")
    }

    #[test]
    fn injects_the_edge_marker() {
        let generated =
            edge_macro(quote! {}, quote! { async fn h() -> &'static str { "ok" } }).to_string();
        assert!(
            generated.contains(EDGE_MARKER),
            "the edge marker must be emitted: {generated}"
        );
        assert!(
            !generated.contains(EDGE_NEEDS_KV_MARKER),
            "a bare #[edge] must not declare any capability: {generated}"
        );
    }

    #[test]
    fn injects_the_kv_capability_marker() {
        let generated = edge_macro(
            quote! { needs(kv) },
            quote! { async fn h() -> &'static str { "ok" } },
        )
        .to_string();
        assert!(
            generated.contains(EDGE_NEEDS_KV_MARKER),
            "needs(kv) must emit the capability marker: {generated}"
        );
    }

    #[test]
    fn preserves_handler_signature_and_other_attributes() {
        let generated = edge_macro(
            quote! {},
            quote! {
                #[public]
                #[api_doc(summary = "greet")]
                async fn h(id: Path<String>) -> &'static str { "ok" }
            },
        )
        .to_string();
        // No return-type rewrite and no injected extractors — the marker only.
        assert!(generated.contains("async fn h (id : Path < String >)"));
        assert!(!generated.contains("Response"));
        // Attributes below `#[edge]` must survive for the route macro to read.
        assert!(generated.contains("# [public]"), "{generated}");
        assert!(generated.contains("# [api_doc"), "{generated}");
    }

    #[test]
    fn marker_is_the_first_statement() {
        let generated = edge_macro(
            quote! { needs(kv) },
            quote! { async fn h() -> &'static str { "ok" } },
        )
        .to_string();
        let body = generated
            .split_once('{')
            .expect("handler should have a body")
            .1;
        assert!(
            body.trim_start().starts_with("const __AUTUMN_EDGE :"),
            "markers must lead the body so guards that wrap it cannot reorder them: {generated}"
        );
    }

    #[test]
    fn rejects_an_unknown_option() {
        let generated = edge_macro(
            quote! { cache = true },
            quote! { async fn h() -> &'static str { "ok" } },
        )
        .to_string();
        assert!(generated.contains("compile_error"), "{generated}");
        assert!(
            generated.contains("unknown `#[edge]` option `cache`"),
            "the error must name the offending option: {generated}"
        );
        assert!(
            generated.contains("`#[edge]` or `#[edge(needs(kv, identity))]`"),
            "the error must spell out the supported grammar: {generated}"
        );
    }

    #[test]
    fn rejects_an_unknown_capability() {
        let generated = edge_macro(
            quote! { needs(db) },
            quote! { async fn h() -> &'static str { "ok" } },
        )
        .to_string();
        assert!(generated.contains("compile_error"), "{generated}");
        assert!(
            generated.contains("unknown `needs(...)` capability `db`"),
            "the error must name the offending capability: {generated}"
        );
        assert!(
            generated.contains("Supported capabilities: `kv`, `identity`"),
            "the error must list what the edge host can mediate: {generated}"
        );
    }

    #[test]
    fn rejects_needs_without_parentheses() {
        let generated = edge_macro(
            quote! { needs = "kv" },
            quote! { async fn h() -> &'static str { "ok" } },
        )
        .to_string();
        assert!(generated.contains("compile_error"), "{generated}");
        assert!(
            generated.contains("parenthesized capabilities"),
            "the error must show the call-shaped form: {generated}"
        );
    }

    #[test]
    fn rejects_an_empty_needs_group() {
        let generated = edge_macro(
            quote! { needs() },
            quote! { async fn h() -> &'static str { "ok" } },
        )
        .to_string();
        assert!(generated.contains("compile_error"), "{generated}");
        assert!(generated.contains("empty `needs(...)`"), "{generated}");
    }

    #[test]
    fn rejects_a_duplicate_needs_group() {
        let generated = edge_macro(
            quote! { needs(kv), needs(kv) },
            quote! { async fn h() -> &'static str { "ok" } },
        )
        .to_string();
        assert!(generated.contains("compile_error"), "{generated}");
        assert!(
            generated.contains("duplicate `needs(...)` argument"),
            "{generated}"
        );
    }

    #[test]
    fn rejects_a_non_function_item() {
        let generated = edge_macro(quote! {}, quote! { struct NotAHandler; }).to_string();
        assert!(generated.contains("compile_error"), "{generated}");
        assert!(
            generated.contains("route handler functions"),
            "the error must say what #[edge] applies to: {generated}"
        );
    }

    #[test]
    fn detects_a_live_attribute() {
        let marking = detect(&parse_fn(quote! {
            #[edge]
            async fn h() -> &'static str { "ok" }
        }))
        .expect("a live #[edge] attribute must be detected");
        assert!(!marking.needs_kv);

        let marking = detect(&parse_fn(quote! {
            #[edge(needs(kv))]
            async fn h() -> &'static str { "ok" }
        }))
        .expect("a live #[edge(needs(kv))] attribute must be detected");
        assert!(marking.needs_kv);
    }

    #[test]
    fn detects_a_path_qualified_attribute() {
        assert!(
            detect(&parse_fn(quote! {
                #[autumn_web::edge]
                async fn h() -> &'static str { "ok" }
            }))
            .is_some(),
            "a path-qualified #[autumn_web::edge] must be detected"
        );
    }

    #[test]
    fn detects_an_expanded_marker() {
        let expanded = edge_macro(
            quote! { needs(kv) },
            quote! { async fn h() -> &'static str { "ok" } },
        );
        let marking = detect(&parse_fn(expanded)).expect("the marker must be detected");
        assert!(marking.needs_kv);
    }

    #[test]
    fn detects_a_marker_buried_by_a_later_guard() {
        // `#[edge]` above `#[secured]`: secured re-homes the body inside
        // `(async move { … }).await`, burying the marker one level down.
        let edged = edge_macro(quote! {}, quote! { async fn h() -> &'static str { "ok" } });
        let secured = crate::secured::secured_macro(quote! { "admin" }, edged);
        let secured_fn = crate::param_helpers::extract_fn_item(secured, "h");
        assert!(
            detect(&secured_fn).is_some(),
            "a buried #[edge] marker must still be detected"
        );
    }

    #[test]
    fn ignores_a_string_literal_that_spells_the_marker() {
        assert!(
            detect(&parse_fn(quote! {
                async fn h() -> &'static str {
                    let _ = "const __AUTUMN_EDGE: () = ();";
                    "ok"
                }
            }))
            .is_none(),
            "the marker is decoded structurally, never scanned for as text"
        );
    }

    #[test]
    fn ignores_an_unmarked_handler() {
        assert!(
            detect(&parse_fn(quote! {
                async fn h() -> &'static str { "ok" }
            }))
            .is_none()
        );
    }
}