Skip to main content

myelin_macros/
lib.rs

1//! Proc macros for the [`myelin`](https://docs.rs/myelin) crate.
2//!
3//! This crate is an implementation detail: always invoke its macros via the
4//! `myelin` re-exports (e.g. `#[myelin::service]`). See the
5//! [`service`] attribute below for the full input/output contract.
6//!
7//! # Worked example
8//!
9//! ```ignore
10//! use myelin::service;
11//!
12//! #[service(api_id = 0x0001)]
13//! pub trait GreeterService {
14//!     async fn greet(&self, name: String) -> String;
15//!     fn health(&self) -> bool;   // sync — dispatched without `.await`
16//! }
17//! ```
18//!
19//! expands (roughly) to:
20//!
21//! - `GreeterRequest` / `GreeterResponse` enums (serde-gated `#[derive]`s)
22//! - `GreeterService` (async, preserved verbatim) + `GreeterServiceSync`
23//!   (sync mirror — `async` stripped from every method)
24//! - `GreeterClient<T>` + `GreeterClientSync<T, B>` with one client method
25//!   per trait method
26//! - `greeter_dispatch` / `greeter_dispatch_sync` and
27//!   `greeter_serve` / `greeter_serve_sync`
28//! - `GreeterTokioService` / `GreeterEmbassyService<M, N>` /
29//!   `GreeterEmbassyClientTransport<'a, M, N>` type aliases
30//!   (feature-gated in the consuming crate)
31//! - `greeter_embassy_service!` `macro_rules!` (embassy-gated) for static
32//!   instantiation, with nested `*_client!` / `*_server!` / `*_client_sync!`
33//!   helpers
34//! - `GREETER_API_ID: u16 = 0x0001` (or an FNV-1a hash of the trait ident
35//!   when the `api_id` argument is omitted)
36//!
37//! Multiple services can be multiplexed onto a single transport with
38//! [`myelin::compose_service!`].
39//!
40//! Methods may be either `async fn` or plain `fn`; in the generated async
41//! dispatch, sync methods are called directly (no spurious `.await`).
42//! Cross-service clients always expose `async` method stubs regardless of
43//! whether the underlying trait method was sync, because the transport is
44//! always async.
45
46mod emit;
47mod parse;
48
49use proc_macro::TokenStream;
50use quote::quote;
51
52/// Generate channel-API plumbing from a trait definition.
53///
54/// Applied to an async-style trait, emits:
55///
56/// 1. `{Stem}Request` enum — one struct-like variant per method, whose fields
57///    are the method arguments by name and type.
58/// 2. `{Stem}Response` enum — one tuple variant per method wrapping the
59///    method's return type (unit returns produce a unit variant).
60/// 3. The original trait, preserved verbatim.
61/// 4. `{Stem}ServiceSync` — a sync mirror of the trait with every method's
62///    `async` stripped.
63/// 5. `{Stem}Client<T>` — async client struct with one `async fn` per trait
64///    method that calls `T::call(...).await` and unwraps the matching
65///    response variant.
66/// 6. `{Stem}ClientSync<T, B>` — sync wrapper around `{Stem}Client<T>` that
67///    blocks each async call via `B: BlockOn`.
68/// 7. `{stem}_dispatch` / `{stem}_dispatch_sync` — match on a request and
69///    invoke the corresponding service trait method, returning a response.
70/// 8. `{stem}_serve` / `{stem}_serve_sync` — `recv → dispatch → reply` loops
71///    over a `ServerTransport`.
72/// 9. Transport convenience type aliases, emitted unconditionally in the
73///    output (no `#[cfg]` attributes). Whether they appear at all is
74///    decided at proc-macro build time by `myelin-macros`'s own
75///    `tokio`/`embassy` features — `myelin` forwards its own
76///    `tokio`/`embassy` features to these, so Cargo's per-package feature
77///    unification keeps the emission and the supporting transport modules
78///    in `myelin` in lockstep.
79///    - `{Stem}TokioService` — `TokioService<{Stem}Request, {Stem}Response>`
80///    - `{Stem}EmbassyService<M, const CHANNEL_DEPTH: usize>`
81///    - `{Stem}EmbassyClientTransport<'a, M, const CHANNEL_DEPTH: usize>`
82/// 10. `{stem}_embassy_service!` — a `#[macro_export] macro_rules!` that
83///     instantiates a static embassy service plus three nested
84///     `*_client!` / `*_server!` / `*_client_sync!` helper macros. Takes
85///     `($name:ident, $mutex:ty, $depth:expr)`. Like item 9, this is
86///     emitted unconditionally in the output when the `embassy` feature of
87///     `myelin-macros` itself is on (i.e. when `myelin/embassy` is on).
88/// 11. `{STEM_UPPER}_API_ID: u16` — wire-level API identifier. Defaults to a
89///     16-bit FNV-1a hash of the trait ident; override with
90///     `#[myelin::service(api_id = 0x1234)]`. Since the id space is only
91///     2^16 wide, collisions are possible across unrelated services — for
92///     production deployments override explicitly.
93///
94/// The "stem" is derived by stripping a trailing `Service` from the trait
95/// name (e.g. `GreeterService` → `Greeter`). The trait name must end in
96/// `Service`; anything else is a compile error.
97///
98/// Both enums derive `Debug`, and derive `serde::Serialize` and
99/// `serde::Deserialize` when the downstream crate's `serde` feature is
100/// enabled.
101///
102/// # Transport feature gating
103///
104/// Items 9 and 10 carry **no** `#[cfg]` attributes in the emitted tokens.
105/// Their presence is controlled entirely at proc-macro build time via
106/// `myelin-macros`'s own `tokio`/`embassy` features. In the supported
107/// usage pattern (depend on `myelin`, invoke `#[myelin::service]` via the
108/// re-export), enabling `myelin/tokio` or `myelin/embassy` automatically
109/// flips the matching `myelin-macros` feature through Cargo's feature
110/// forwards, so the emitted aliases / `macro_rules!` appear exactly when
111/// the supporting `::myelin::transport_tokio` / `::myelin::transport_embassy`
112/// modules are available. Downstream crates therefore do **not** need to
113/// declare `tokio` / `embassy` features of their own.
114///
115/// The embassy instantiation `macro_rules!` refers to `::myelin::paste` and
116/// `::myelin::static_cell` by absolute path. `myelin` re-exports `paste`
117/// unconditionally and `static_cell` under its own `embassy` feature, so no
118/// additional consumer-side wiring is required.
119///
120/// # Input constraints
121///
122/// - Method receiver must be plain `&self`.
123/// - No trait generics, lifetimes, supertraits, or where-clauses.
124/// - Arg patterns must be `ident: Type`.
125/// - Argument types must be owned (no `&T`/`&mut T`).
126/// - Return types are passed through verbatim.
127///
128/// # Attribute arguments
129///
130/// Currently recognised:
131///
132/// - `api_id = <int literal>` — override the wire-level API id constant.
133///   Must fit in a `u16`. Accepts hex (`0x1234`), decimal, or underscored
134///   literals. When omitted, a 16-bit FNV-1a hash of the trait ident is
135///   used.
136///
137/// Any other key is a hard error; this is intentional so future knobs can
138/// be added without silently accepting typos.
139#[proc_macro_attribute]
140pub fn service(attr: TokenStream, item: TokenStream) -> TokenStream {
141    let args = match parse::ServiceArgs::parse(attr.into()) {
142        Ok(a) => a,
143        Err(e) => return e.to_compile_error().into(),
144    };
145
146    let item_trait = match syn::parse::<syn::ItemTrait>(item) {
147        Ok(t) => t,
148        Err(e) => return e.to_compile_error().into(),
149    };
150
151    match parse::ServiceTrait::parse(item_trait) {
152        Ok(svc) => {
153            let req = emit::request_enum(&svc);
154            let resp = emit::response_enum(&svc);
155            let async_t = emit::async_trait(&svc);
156            let sync_t = emit::sync_trait(&svc);
157            let client = emit::client_struct(&svc);
158            let client_sync = emit::client_sync_struct(&svc);
159            let dispatch = emit::dispatch_fn(&svc);
160            let dispatch_sync = emit::dispatch_sync_fn(&svc);
161            let serve = emit::serve_fn(&svc);
162            let serve_sync = emit::serve_sync_fn(&svc);
163
164            // Transport aliases / instantiation macros are gated on
165            // `myelin-macros`'s own `tokio`/`embassy` features so the
166            // emitted tokens don't carry `#[cfg]` attributes (see the
167            // docstring above). When the feature is off, substitute an
168            // empty `TokenStream`.
169            #[cfg(feature = "tokio")]
170            let tokio_alias = emit::tokio_transport_alias(&svc);
171            #[cfg(not(feature = "tokio"))]
172            let tokio_alias = quote! {};
173
174            #[cfg(feature = "embassy")]
175            let embassy_aliases = emit::embassy_transport_aliases(&svc);
176            #[cfg(not(feature = "embassy"))]
177            let embassy_aliases = quote! {};
178
179            #[cfg(feature = "embassy")]
180            let embassy_instantiation = emit::embassy_instantiation(&svc);
181            #[cfg(not(feature = "embassy"))]
182            let embassy_instantiation = quote! {};
183
184            let api_id = emit::api_id_const(&svc, args.api_id);
185            quote! {
186                #req
187                #resp
188                #async_t
189                #sync_t
190                #client
191                #client_sync
192                #dispatch
193                #dispatch_sync
194                #serve
195                #serve_sync
196                #tokio_alias
197                #embassy_aliases
198                #embassy_instantiation
199                #api_id
200            }
201            .into()
202        }
203        Err(e) => e.to_compile_error().into(),
204    }
205}
206
207// =============================================================================
208// Unit tests — exercise the parse+emit pipeline directly, since proc macros
209// cannot be invoked from within their own crate. UI (`trybuild`) tests live
210// downstream in subtask 5.
211// =============================================================================
212
213#[cfg(test)]
214mod tests {
215    use super::{
216        emit,
217        parse::{ServiceArgs, ServiceTrait},
218    };
219    use quote::quote;
220
221    fn canon(ts: proc_macro2::TokenStream) -> String {
222        ts.to_string()
223    }
224
225    // ----- happy path -----
226
227    #[test]
228    fn request_enum_shape() {
229        let item: syn::ItemTrait = syn::parse_quote! {
230            pub trait GreeterService {
231                async fn greet(&self, name: String) -> String;
232                async fn health(&self) -> bool;
233            }
234        };
235        let svc = ServiceTrait::parse(item).unwrap();
236        let got = canon(emit::request_enum(&svc));
237        assert!(got.contains("enum GreeterRequest"), "got: {got}");
238        assert!(got.contains("Greet { name : String }"), "got: {got}");
239        // Zero-arg method → unit variant (no braces).
240        assert!(got.contains("Health"), "got: {got}");
241        assert!(!got.contains("Health {"), "got: {got}");
242        // Derives present.
243        assert!(got.contains("derive (Debug)"), "got: {got}");
244        assert!(
245            got.contains("serde :: Serialize , serde :: Deserialize"),
246            "got: {got}"
247        );
248    }
249
250    #[test]
251    fn response_enum_shape() {
252        let item: syn::ItemTrait = syn::parse_quote! {
253            pub trait GreeterService {
254                async fn greet(&self, name: String) -> String;
255                async fn health(&self) -> bool;
256            }
257        };
258        let svc = ServiceTrait::parse(item).unwrap();
259        let got = canon(emit::response_enum(&svc));
260        assert!(got.contains("enum GreeterResponse"), "got: {got}");
261        assert!(got.contains("Greet (String)"), "got: {got}");
262        assert!(got.contains("Health (bool)"), "got: {got}");
263    }
264
265    #[test]
266    fn response_unit_variant_for_unit_return() {
267        let item: syn::ItemTrait = syn::parse_quote! {
268            pub trait PingService {
269                fn ping(&self);
270            }
271        };
272        let svc = ServiceTrait::parse(item).unwrap();
273        let got = canon(emit::response_enum(&svc));
274        assert!(got.contains("enum PingResponse"), "got: {got}");
275        assert!(got.contains("Ping"), "got: {got}");
276        // No tuple payload for unit return.
277        assert!(!got.contains("Ping ("), "got: {got}");
278    }
279
280    #[test]
281    fn async_trait_is_preserved_verbatim() {
282        let item: syn::ItemTrait = syn::parse_quote! {
283            pub trait GreeterService {
284                async fn greet(&self, name: String) -> String;
285                fn health(&self) -> bool;
286            }
287        };
288        let svc = ServiceTrait::parse(item).unwrap();
289        let got = canon(emit::async_trait(&svc));
290        assert!(got.contains("trait GreeterService"), "got: {got}");
291        assert!(got.contains("async fn greet"), "got: {got}");
292        assert!(got.contains("fn health"), "got: {got}");
293    }
294
295    #[test]
296    fn sync_trait_strips_async_and_renames() {
297        let item: syn::ItemTrait = syn::parse_quote! {
298            pub trait GreeterService {
299                async fn greet(&self, name: String) -> String;
300                fn health(&self) -> bool;
301            }
302        };
303        let svc = ServiceTrait::parse(item).unwrap();
304        let got = canon(emit::sync_trait(&svc));
305        assert!(got.contains("trait GreeterServiceSync"), "got: {got}");
306        assert!(!got.contains("async fn"), "got: {got}");
307        assert!(got.contains("fn greet"), "got: {got}");
308        assert!(got.contains("fn health"), "got: {got}");
309    }
310
311    #[test]
312    fn snake_case_method_to_pascal_variant() {
313        let item: syn::ItemTrait = syn::parse_quote! {
314            pub trait FooService {
315                async fn do_thing(&self, n: u32) -> u32;
316            }
317        };
318        let svc = ServiceTrait::parse(item).unwrap();
319        let got = canon(emit::request_enum(&svc));
320        assert!(got.contains("DoThing { n : u32 }"), "got: {got}");
321    }
322
323    #[test]
324    fn result_return_type_passed_through() {
325        let item: syn::ItemTrait = syn::parse_quote! {
326            pub trait FooService {
327                async fn maybe(&self) -> Result<String, u32>;
328            }
329        };
330        let svc = ServiceTrait::parse(item).unwrap();
331        let got = canon(emit::response_enum(&svc));
332        assert!(
333            got.contains("Maybe (Result < String , u32 >)"),
334            "got: {got}"
335        );
336    }
337
338    #[test]
339    fn preserves_complex_arg_type_verbatim() {
340        let item: syn::ItemTrait = syn::parse_quote! {
341            pub trait FooService {
342                async fn put(&self, s: heapless::String<64>) -> bool;
343            }
344        };
345        let svc = ServiceTrait::parse(item).unwrap();
346        let got = canon(emit::request_enum(&svc));
347        assert!(
348            got.contains("Put { s : heapless :: String < 64 > }"),
349            "got: {got}"
350        );
351    }
352
353    #[test]
354    fn visibility_propagates_to_generated_items() {
355        let item: syn::ItemTrait = syn::parse_quote! {
356            pub(crate) trait FooService {
357                fn a(&self) -> u32;
358            }
359        };
360        let svc = ServiceTrait::parse(item).unwrap();
361        assert!(
362            canon(emit::request_enum(&svc)).starts_with("# [derive"),
363            "expected attrs before vis"
364        );
365        assert!(canon(emit::request_enum(&svc)).contains("pub (crate) enum FooRequest"));
366        assert!(canon(emit::response_enum(&svc)).contains("pub (crate) enum FooResponse"));
367        assert!(canon(emit::sync_trait(&svc)).contains("pub (crate) trait FooServiceSync"));
368    }
369
370    // ----- error paths -----
371
372    fn err(item: syn::ItemTrait) -> String {
373        match ServiceTrait::parse(item) {
374            Ok(_) => panic!("expected parse error"),
375            Err(e) => e.to_string(),
376        }
377    }
378
379    #[test]
380    fn rejects_non_service_trait_name() {
381        let msg = err(syn::parse_quote! {
382            pub trait Greeter {
383                fn a(&self);
384            }
385        });
386        assert!(msg.contains("must end in `Service`"), "msg: {msg}");
387    }
388
389    #[test]
390    fn rejects_empty_stem() {
391        let msg = err(syn::parse_quote! {
392            pub trait Service {
393                fn a(&self);
394            }
395        });
396        assert!(msg.contains("non-empty stem"), "msg: {msg}");
397    }
398
399    #[test]
400    fn rejects_mut_self() {
401        let msg = err(syn::parse_quote! {
402            pub trait FooService {
403                fn a(&mut self);
404            }
405        });
406        assert!(msg.contains("&self"), "msg: {msg}");
407    }
408
409    #[test]
410    fn rejects_self_by_value() {
411        let msg = err(syn::parse_quote! {
412            pub trait FooService {
413                fn a(self);
414            }
415        });
416        assert!(msg.contains("&self"), "msg: {msg}");
417    }
418
419    #[test]
420    fn rejects_missing_receiver() {
421        let msg = err(syn::parse_quote! {
422            pub trait FooService {
423                fn a(x: u32);
424            }
425        });
426        assert!(msg.contains("&self"), "msg: {msg}");
427    }
428
429    #[test]
430    fn rejects_borrowed_arg() {
431        let msg = err(syn::parse_quote! {
432            pub trait FooService {
433                fn a(&self, name: &str);
434            }
435        });
436        assert!(msg.contains("owned"), "msg: {msg}");
437    }
438
439    #[test]
440    fn rejects_destructured_arg() {
441        let msg = err(syn::parse_quote! {
442            pub trait FooService {
443                fn a(&self, (x, y): (u32, u32));
444            }
445        });
446        assert!(msg.contains("plain `ident: Type`"), "msg: {msg}");
447    }
448
449    #[test]
450    fn rejects_underscore_arg() {
451        let msg = err(syn::parse_quote! {
452            pub trait FooService {
453                fn a(&self, _: u32);
454            }
455        });
456        assert!(msg.contains("plain `ident: Type`"), "msg: {msg}");
457    }
458
459    #[test]
460    fn rejects_mut_arg() {
461        let msg = err(syn::parse_quote! {
462            pub trait FooService {
463                fn a(&self, mut x: u32);
464            }
465        });
466        assert!(msg.contains("plain `ident: Type`"), "msg: {msg}");
467    }
468
469    #[test]
470    fn rejects_trait_generics() {
471        let msg = err(syn::parse_quote! {
472            pub trait FooService<T> {
473                fn a(&self) -> T;
474            }
475        });
476        assert!(msg.contains("trait generics"), "msg: {msg}");
477    }
478
479    #[test]
480    fn rejects_trait_lifetime() {
481        let msg = err(syn::parse_quote! {
482            pub trait FooService<'a> {
483                fn a(&self);
484            }
485        });
486        // Lifetimes are generic params → caught by trait generics rule.
487        assert!(msg.contains("trait generics"), "msg: {msg}");
488    }
489
490    #[test]
491    fn rejects_where_clause() {
492        let msg = err(syn::parse_quote! {
493            pub trait FooService where Self: Sized {
494                fn a(&self);
495            }
496        });
497        assert!(msg.contains("where-clauses"), "msg: {msg}");
498    }
499
500    #[test]
501    fn rejects_supertraits() {
502        let msg = err(syn::parse_quote! {
503            pub trait FooService: Sized {
504                fn a(&self);
505            }
506        });
507        assert!(msg.contains("supertraits"), "msg: {msg}");
508    }
509
510    #[test]
511    fn rejects_method_generics() {
512        let msg = err(syn::parse_quote! {
513            pub trait FooService {
514                fn a<T>(&self, t: T);
515            }
516        });
517        assert!(msg.contains("method generics"), "msg: {msg}");
518    }
519
520    #[test]
521    fn rejects_default_body() {
522        let msg = err(syn::parse_quote! {
523            pub trait FooService {
524                fn a(&self) -> u32 { 0 }
525            }
526        });
527        assert!(msg.contains("default body"), "msg: {msg}");
528    }
529
530    #[test]
531    fn rejects_non_fn_trait_items() {
532        let msg = err(syn::parse_quote! {
533            pub trait FooService {
534                type Assoc;
535                fn a(&self);
536            }
537        });
538        assert!(msg.contains("only `fn` items"), "msg: {msg}");
539    }
540
541    // =========================================================================
542    // Emit tests for client / client-sync / dispatch / serve.
543    // We assert shape via substring matching against the canonical
544    // (whitespace-padded) `TokenStream::to_string()` output. Compile-level
545    // verification happens downstream in subtask 5's trybuild fixtures.
546    // =========================================================================
547
548    fn greeter() -> ServiceTrait {
549        let item: syn::ItemTrait = syn::parse_quote! {
550            pub trait GreeterService {
551                async fn greet(&self, name: String) -> String;
552                fn health(&self) -> bool;
553            }
554        };
555        ServiceTrait::parse(item).unwrap()
556    }
557
558    #[test]
559    fn client_struct_shape() {
560        let svc = greeter();
561        let got = canon(emit::client_struct(&svc));
562        assert!(got.contains("pub struct GreeterClient < T >"), "got: {got}");
563        assert!(
564            got.contains(":: myelin :: ClientTransport < GreeterRequest , GreeterResponse >"),
565            "got: {got}"
566        );
567        assert!(got.contains("pub fn new (transport : T)"), "got: {got}");
568        // Async client method even for sync trait method.
569        assert!(
570            got.contains("pub async fn greet (& self , name : String)"),
571            "got: {got}"
572        );
573        assert!(got.contains("pub async fn health (& self ,)"), "got: {got}");
574        // Return-type shape uses `TransportResult<Ret>::Output`.
575        assert!(
576            got.contains("< T :: Error as :: myelin :: TransportResult < String >> :: Output"),
577            "got: {got}"
578        );
579        assert!(
580            got.contains("< T :: Error as :: myelin :: TransportResult < bool >> :: Output"),
581            "got: {got}"
582        );
583        // Body dispatches through transport.call.
584        assert!(
585            got.contains("self . transport . call (GreeterRequest :: Greet { name }) . await"),
586            "got: {got}"
587        );
588        assert!(
589            got.contains("self . transport . call (GreeterRequest :: Health) . await"),
590            "got: {got}"
591        );
592        assert!(
593            got.contains("GreeterResponse :: Greet (v) => v"),
594            "got: {got}"
595        );
596    }
597
598    #[test]
599    fn client_struct_unit_return_uses_unit_param() {
600        let item: syn::ItemTrait = syn::parse_quote! {
601            pub trait PingService {
602                fn ping(&self);
603            }
604        };
605        let svc = ServiceTrait::parse(item).unwrap();
606        let got = canon(emit::client_struct(&svc));
607        // Unit return → TransportResult<()>.
608        assert!(
609            got.contains(":: myelin :: TransportResult < () >"),
610            "got: {got}"
611        );
612        // Match arm pattern must NOT have a payload — response variant is unit.
613        assert!(
614            got.contains("PingResponse :: Ping => ()"),
615            "expected unit-variant pattern; got: {got}"
616        );
617        assert!(
618            !got.contains("PingResponse :: Ping (v)"),
619            "must not pattern-match a payload on unit response variant; got: {got}"
620        );
621    }
622
623    #[test]
624    fn client_sync_struct_shape() {
625        let svc = greeter();
626        let got = canon(emit::client_sync_struct(&svc));
627        assert!(
628            got.contains("pub struct GreeterClientSync < T , B >"),
629            "got: {got}"
630        );
631        assert!(got.contains("inner : GreeterClient < T >"), "got: {got}");
632        assert!(got.contains("block_on : B"), "got: {got}");
633        assert!(got.contains(": :: myelin :: BlockOn"), "got: {got}");
634        // Sync wrapper methods are plain `fn` with same return-type bound.
635        assert!(
636            got.contains("pub fn greet (& self , name : String)"),
637            "got: {got}"
638        );
639        assert!(
640            got.contains("self . block_on . block_on (self . inner . greet (name))"),
641            "got: {got}"
642        );
643        // Where-clause carries through.
644        assert!(
645            got.contains("T :: Error : :: myelin :: TransportResult < String >"),
646            "got: {got}"
647        );
648    }
649
650    #[test]
651    fn dispatch_fn_async_and_sync_method_calls() {
652        let svc = greeter();
653        let got = canon(emit::dispatch_fn(&svc));
654        assert!(
655            got.contains("pub async fn greeter_dispatch < S : GreeterService >"),
656            "got: {got}"
657        );
658        assert!(got.contains("req : GreeterRequest"), "got: {got}");
659        // Async trait method gets `.await`.
660        assert!(
661            got.contains("GreeterResponse :: Greet (svc . greet (name) . await)"),
662            "got: {got}"
663        );
664        // Sync trait method must NOT get `.await` from the async dispatch fn.
665        assert!(
666            got.contains("GreeterResponse :: Health (svc . health ())"),
667            "got: {got}"
668        );
669        assert!(
670            !got.contains("svc . health () . await"),
671            "sync method must not be awaited; got: {got}"
672        );
673    }
674
675    #[test]
676    fn dispatch_sync_fn_no_await_anywhere() {
677        let svc = greeter();
678        let got = canon(emit::dispatch_sync_fn(&svc));
679        assert!(
680            got.contains("pub fn greeter_dispatch_sync < S : GreeterServiceSync >"),
681            "got: {got}"
682        );
683        assert!(!got.contains(". await"), "got: {got}");
684        assert!(
685            got.contains("GreeterResponse :: Greet (svc . greet (name))"),
686            "got: {got}"
687        );
688        assert!(
689            got.contains("GreeterResponse :: Health (svc . health ())"),
690            "got: {got}"
691        );
692    }
693
694    #[test]
695    fn dispatch_unit_return_emits_unit_variant() {
696        let item: syn::ItemTrait = syn::parse_quote! {
697            pub trait PingService {
698                async fn ping(&self);
699            }
700        };
701        let svc = ServiceTrait::parse(item).unwrap();
702        let got = canon(emit::dispatch_fn(&svc));
703        // Side-effecting call followed by unit variant — never `Ping(())`.
704        assert!(got.contains("svc . ping () . await ;"), "got: {got}");
705        assert!(got.contains("PingResponse :: Ping"), "got: {got}");
706        assert!(!got.contains("PingResponse :: Ping ("), "got: {got}");
707    }
708
709    #[test]
710    fn dispatch_multi_arg_struct_pattern() {
711        let item: syn::ItemTrait = syn::parse_quote! {
712            pub trait MathService {
713                async fn add(&self, a: i32, b: i32) -> i64;
714            }
715        };
716        let svc = ServiceTrait::parse(item).unwrap();
717        let got = canon(emit::dispatch_fn(&svc));
718        assert!(got.contains("MathRequest :: Add { a , b }"), "got: {got}");
719        assert!(
720            got.contains("MathResponse :: Add (svc . add (a , b) . await)"),
721            "got: {got}"
722        );
723    }
724
725    #[test]
726    fn serve_fn_shape() {
727        let svc = greeter();
728        let got = canon(emit::serve_fn(&svc));
729        assert!(
730            got.contains("pub async fn greeter_serve < S , T >"),
731            "got: {got}"
732        );
733        assert!(got.contains("S : GreeterService"), "got: {got}");
734        assert!(
735            got.contains("T : :: myelin :: ServerTransport < GreeterRequest , GreeterResponse >"),
736            "got: {got}"
737        );
738        assert!(got.contains("transport . recv () . await ?"), "got: {got}");
739        assert!(
740            got.contains("greeter_dispatch (svc , req) . await"),
741            "got: {got}"
742        );
743        assert!(
744            got.contains("transport . reply (token , resp) . await"),
745            "got: {got}"
746        );
747    }
748
749    #[test]
750    fn serve_sync_fn_shape() {
751        let svc = greeter();
752        let got = canon(emit::serve_sync_fn(&svc));
753        assert!(
754            got.contains("pub fn greeter_serve_sync < S , T , B >"),
755            "got: {got}"
756        );
757        assert!(got.contains("S : GreeterServiceSync"), "got: {got}");
758        assert!(got.contains("B : :: myelin :: BlockOn"), "got: {got}");
759        // recv & reply are wrapped in block_on; dispatch_sync called directly.
760        assert!(
761            got.contains("block_on . block_on (transport . recv ()) ?"),
762            "got: {got}"
763        );
764        assert!(
765            got.contains("greeter_dispatch_sync (svc , req)"),
766            "got: {got}"
767        );
768        assert!(
769            got.contains("block_on . block_on (transport . reply (token , resp))"),
770            "got: {got}"
771        );
772    }
773
774    #[test]
775    fn snake_case_stem_for_dispatch_idents() {
776        let item: syn::ItemTrait = syn::parse_quote! {
777            pub trait FooBarService {
778                fn a(&self) -> u32;
779            }
780        };
781        let svc = ServiceTrait::parse(item).unwrap();
782        assert!(
783            canon(emit::dispatch_fn(&svc)).contains("fn foo_bar_dispatch <"),
784            "expected snake_case stem in dispatch fn ident"
785        );
786        assert!(
787            canon(emit::serve_fn(&svc)).contains("fn foo_bar_serve <"),
788            "expected snake_case stem in serve fn ident"
789        );
790    }
791
792    #[test]
793    fn visibility_propagates_to_clients_and_dispatch() {
794        let item: syn::ItemTrait = syn::parse_quote! {
795            pub(crate) trait FooService {
796                fn a(&self) -> u32;
797            }
798        };
799        let svc = ServiceTrait::parse(item).unwrap();
800        assert!(
801            canon(emit::client_struct(&svc)).contains("pub (crate) struct FooClient"),
802            "vis on client struct"
803        );
804        assert!(
805            canon(emit::client_sync_struct(&svc)).contains("pub (crate) struct FooClientSync"),
806            "vis on sync client struct"
807        );
808        assert!(
809            canon(emit::dispatch_fn(&svc)).contains("pub (crate) async fn foo_dispatch"),
810            "vis on dispatch fn"
811        );
812        assert!(
813            canon(emit::serve_sync_fn(&svc)).contains("pub (crate) fn foo_serve_sync"),
814            "vis on serve_sync fn"
815        );
816    }
817
818    // =========================================================================
819    // Transport aliases (item 7).
820    //
821    // The new emit functions (`tokio_transport_alias` /
822    // `embassy_transport_aliases`) only exist when `myelin-macros` is built
823    // with its own matching feature, so the tests live inside feature-gated
824    // submodules.
825    // =========================================================================
826
827    #[cfg(feature = "tokio")]
828    mod tokio_transport_alias_tests {
829        use super::*;
830
831        #[test]
832        fn tokio_transport_alias_shape() {
833            let svc = greeter();
834            let got = canon(emit::tokio_transport_alias(&svc));
835            // No `#[cfg]` attribute in the emitted tokens.
836            assert!(!got.contains("# [cfg (feature"), "got: {got}");
837            assert!(
838                got.contains(
839                    "pub type GreeterTokioService = \
840                     :: myelin :: transport_tokio :: TokioService < GreeterRequest , GreeterResponse >"
841                ),
842                "got: {got}"
843            );
844        }
845
846        #[test]
847        fn tokio_transport_alias_visibility_propagates() {
848            let item: syn::ItemTrait = syn::parse_quote! {
849                pub(crate) trait FooService {
850                    fn a(&self) -> u32;
851                }
852            };
853            let svc = ServiceTrait::parse(item).unwrap();
854            let got = canon(emit::tokio_transport_alias(&svc));
855            assert!(!got.contains("# [cfg (feature"), "got: {got}");
856            assert!(
857                got.contains("pub (crate) type FooTokioService"),
858                "got: {got}"
859            );
860        }
861    }
862
863    #[cfg(feature = "embassy")]
864    mod embassy_transport_aliases_tests {
865        use super::*;
866
867        #[test]
868        fn embassy_transport_aliases_shape() {
869            let svc = greeter();
870            let got = canon(emit::embassy_transport_aliases(&svc));
871            // No `#[cfg]` attribute in the emitted tokens.
872            assert!(!got.contains("# [cfg (feature"), "got: {got}");
873            assert!(
874                got.contains(
875                    "pub type GreeterEmbassyService < M , const CHANNEL_DEPTH : usize > = \
876                     :: myelin :: transport_embassy :: EmbassyService < \
877                     M , GreeterRequest , GreeterResponse , CHANNEL_DEPTH >"
878                ),
879                "got: {got}"
880            );
881            assert!(
882                got.contains(
883                    "pub type GreeterEmbassyClientTransport < 'a , M , const CHANNEL_DEPTH : usize > = \
884                     :: myelin :: transport_embassy :: EmbassyClient < \
885                     'a , M , GreeterRequest , GreeterResponse , CHANNEL_DEPTH >"
886                ),
887                "got: {got}"
888            );
889        }
890
891        #[test]
892        fn embassy_transport_aliases_visibility_propagates() {
893            let item: syn::ItemTrait = syn::parse_quote! {
894                pub(crate) trait FooService {
895                    fn a(&self) -> u32;
896                }
897            };
898            let svc = ServiceTrait::parse(item).unwrap();
899            let got = canon(emit::embassy_transport_aliases(&svc));
900            assert!(!got.contains("# [cfg (feature"), "got: {got}");
901            assert!(
902                got.contains("pub (crate) type FooEmbassyService"),
903                "got: {got}"
904            );
905            assert!(
906                got.contains("pub (crate) type FooEmbassyClientTransport"),
907                "got: {got}"
908            );
909        }
910    }
911
912    // =========================================================================
913    // Embassy instantiation `macro_rules!` (item 8).
914    //
915    // We assert via substring matching. The resulting token stream contains
916    // literal `$` metavars and nested `macro_rules!` declarations which only
917    // become meaningful once a downstream crate invokes them; compile-level
918    // verification lives in subtask 5's downstream test fixtures.
919    //
920    // `emit::embassy_instantiation` only exists when `myelin-macros` is
921    // built with its own `embassy` feature, so these tests live inside a
922    // feature-gated submodule.
923    // =========================================================================
924
925    #[cfg(feature = "embassy")]
926    mod embassy_instantiation_tests {
927        use super::*;
928
929        #[test]
930        fn embassy_instantiation_outer_macro_shape() {
931            let svc = greeter();
932            let got = canon(emit::embassy_instantiation(&svc));
933            // No `#[cfg]` attribute on the emitted macro — the gating
934            // happens at proc-macro build time, not at consumer build time.
935            assert!(
936                !got.contains("# [cfg (feature = \"embassy\")]"),
937                "expected no `#[cfg]` on emitted macro; got: {got}"
938            );
939            assert!(got.contains("# [macro_export]"), "got: {got}");
940            assert!(
941                got.contains("macro_rules ! greeter_embassy_service"),
942                "got: {got}"
943            );
944            // Outer arm pattern.
945            assert!(
946                got.contains("($ name : ident , $ mutex : ty , $ depth : expr)"),
947                "got: {got}"
948            );
949            // `paste` is reached via ::myelin::paste::paste!.
950            assert!(
951                got.contains(":: myelin :: paste :: paste !"),
952                "expected absolute ::myelin::paste::paste! path; got: {got}"
953            );
954        }
955
956        #[test]
957        fn embassy_instantiation_static_service_cell() {
958            let svc = greeter();
959            let got = canon(emit::embassy_instantiation(&svc));
960            // Static service ident prefix pasted with $name:upper.
961            assert!(
962                got.contains("[< __GREETER_SERVICE_ $ name : upper >]"),
963                "got: {got}"
964            );
965            // Full EmbassyService<…> type with stem-derived req/resp baked in.
966            assert!(
967                got.contains(
968                    ":: myelin :: transport_embassy :: EmbassyService < \
969                     $ mutex , GreeterRequest , GreeterResponse , $ depth , >"
970                ),
971                "got: {got}"
972            );
973            assert!(
974                got.contains(":: myelin :: transport_embassy :: EmbassyService :: new ()"),
975                "got: {got}"
976            );
977        }
978
979        #[test]
980        fn embassy_instantiation_nested_client_macro() {
981            let svc = greeter();
982            let got = canon(emit::embassy_instantiation(&svc));
983            assert!(
984                got.contains("macro_rules ! [< $ name _client >]"),
985                "got: {got}"
986            );
987            // StaticCell via absolute ::myelin::static_cell::StaticCell path.
988            assert!(
989                got.contains(":: myelin :: static_cell :: StaticCell"),
990                "got: {got}"
991            );
992            // Baked-in client ident.
993            assert!(
994                got.contains(
995                    "GreeterClient :: new (& * CELL . init \
996                     ([< __GREETER_SERVICE_ $ name : upper >] . client () ,) ,)"
997                ),
998                "got: {got}"
999            );
1000        }
1001
1002        #[test]
1003        fn embassy_instantiation_nested_server_macro() {
1004            let svc = greeter();
1005            let got = canon(emit::embassy_instantiation(&svc));
1006            assert!(
1007                got.contains("macro_rules ! [< $ name _server >]"),
1008                "got: {got}"
1009            );
1010            assert!(
1011                got.contains("[< __GREETER_SERVICE_ $ name : upper >] . server ()"),
1012                "got: {got}"
1013            );
1014        }
1015
1016        #[test]
1017        fn embassy_instantiation_nested_client_sync_macro() {
1018            let svc = greeter();
1019            let got = canon(emit::embassy_instantiation(&svc));
1020            assert!(
1021                got.contains("macro_rules ! [< $ name _client_sync >]"),
1022                "got: {got}"
1023            );
1024            assert!(got.contains("($ block_on : expr)"), "got: {got}");
1025            // Baked-in sync client + async client refs.
1026            assert!(got.contains("GreeterClientSync :: new ("), "got: {got}");
1027            assert!(
1028                got.contains("GreeterClient :: new (& * CELL . init"),
1029                "got: {got}"
1030            );
1031            assert!(got.contains("$ block_on ,"), "got: {got}");
1032        }
1033
1034        #[test]
1035        fn embassy_instantiation_snake_and_screaming_stem() {
1036            let item: syn::ItemTrait = syn::parse_quote! {
1037                pub trait FooBarService {
1038                    async fn a(&self) -> u32;
1039                }
1040            };
1041            let svc = ServiceTrait::parse(item).unwrap();
1042            let got = canon(emit::embassy_instantiation(&svc));
1043            assert!(
1044                got.contains("macro_rules ! foo_bar_embassy_service"),
1045                "expected snake_case stem in outer macro ident; got: {got}"
1046            );
1047            assert!(
1048                got.contains("[< __FOO_BAR_SERVICE_ $ name : upper >]"),
1049                "expected SCREAMING_SNAKE stem in static prefix; got: {got}"
1050            );
1051            assert!(
1052                got.contains("FooBarClient :: new"),
1053                "expected PascalCase stem in nested client body; got: {got}"
1054            );
1055            assert!(got.contains("FooBarClientSync :: new"), "got: {got}");
1056        }
1057    }
1058
1059    // ----- subtask 4: api_id constant & attribute parsing -----
1060
1061    /// Known-good FNV-1a16 values. Regression-pins the hash function so
1062    /// that future refactors don't silently shift the wire id of existing
1063    /// services. (Spot-checked by hand-tracing the algorithm.)
1064    #[test]
1065    fn fnv1a16_known_values() {
1066        // Both values are non-zero and distinct — the doc-comment on the
1067        // emitted constant promises both properties for "reasonable" trait
1068        // names.
1069        let greeter = emit::fnv1a16("GreeterService");
1070        let math = emit::fnv1a16("MathService");
1071        assert_ne!(greeter, 0);
1072        assert_ne!(math, 0);
1073        assert_ne!(greeter, math);
1074
1075        // Pinned literals — recompute and paste if you intentionally change
1076        // the hash definition.
1077        assert_eq!(emit::fnv1a16("GreeterService"), 0x237d);
1078        assert_eq!(emit::fnv1a16("MathService"), 0x90b0);
1079        assert_eq!(emit::fnv1a16(""), 0x1cd9); // FNV offset basis folded
1080    }
1081
1082    #[test]
1083    fn api_id_const_default_hash() {
1084        let item: syn::ItemTrait = syn::parse_quote! {
1085            pub trait GreeterService {
1086                async fn greet(&self, name: String) -> String;
1087            }
1088        };
1089        let svc = ServiceTrait::parse(item).unwrap();
1090        let got = canon(emit::api_id_const(&svc, None));
1091        // Constant ident is SCREAMING_SNAKE of the stem + `_API_ID`.
1092        assert!(
1093            got.contains("pub const GREETER_API_ID : u16 ="),
1094            "got: {got}"
1095        );
1096        // Default value is the FNV-1a hash of the full trait ident.
1097        let want = emit::fnv1a16("GreeterService");
1098        assert!(
1099            got.contains(&format!("= {want}u16")) || got.contains(&format!("= {want} u16")),
1100            "expected literal `{want}u16` in: {got}"
1101        );
1102    }
1103
1104    #[test]
1105    fn api_id_const_override() {
1106        let item: syn::ItemTrait = syn::parse_quote! {
1107            pub trait GreeterService {
1108                async fn greet(&self, name: String) -> String;
1109            }
1110        };
1111        let svc = ServiceTrait::parse(item).unwrap();
1112        let got = canon(emit::api_id_const(&svc, Some(0x1234)));
1113        assert!(
1114            got.contains("pub const GREETER_API_ID : u16 ="),
1115            "got: {got}"
1116        );
1117        // The literal lands verbatim in the tokens as `4660u16` (decimal
1118        // form used by quote! for integer literals).
1119        assert!(
1120            got.contains("4660u16") || got.contains("4660 u16"),
1121            "got: {got}"
1122        );
1123    }
1124
1125    #[test]
1126    fn api_id_const_multi_word_stem() {
1127        let item: syn::ItemTrait = syn::parse_quote! {
1128            pub trait FooBarService {
1129                fn ping(&self);
1130            }
1131        };
1132        let svc = ServiceTrait::parse(item).unwrap();
1133        let got = canon(emit::api_id_const(&svc, None));
1134        assert!(
1135            got.contains("pub const FOO_BAR_API_ID : u16 ="),
1136            "expected SCREAMING_SNAKE multi-word stem; got: {got}"
1137        );
1138    }
1139
1140    #[test]
1141    fn api_id_const_carries_doc_comment() {
1142        let item: syn::ItemTrait = syn::parse_quote! {
1143            pub trait GreeterService {
1144                fn ping(&self);
1145            }
1146        };
1147        let svc = ServiceTrait::parse(item).unwrap();
1148        let got = canon(emit::api_id_const(&svc, None));
1149        // Doc comment mentions the stem *and* points users at the override.
1150        assert!(
1151            got.contains("Wire-level API identifier for the Greeter service"),
1152            "got: {got}"
1153        );
1154        assert!(got.contains("api_id = 0x0001"), "got: {got}");
1155    }
1156
1157    #[test]
1158    fn api_id_const_preserves_visibility() {
1159        // Private trait (no `pub`) should yield a private const.
1160        let item: syn::ItemTrait = syn::parse_quote! {
1161            trait GreeterService {
1162                fn ping(&self);
1163            }
1164        };
1165        let svc = ServiceTrait::parse(item).unwrap();
1166        let got = canon(emit::api_id_const(&svc, None));
1167        assert!(got.contains("const GREETER_API_ID : u16 ="), "got: {got}");
1168        assert!(
1169            !got.contains("pub const GREETER_API_ID"),
1170            "expected private const; got: {got}"
1171        );
1172    }
1173
1174    // ----- attribute parser -----
1175
1176    fn parse_args(ts: proc_macro2::TokenStream) -> syn::Result<ServiceArgs> {
1177        ServiceArgs::parse(ts)
1178    }
1179
1180    #[test]
1181    fn service_args_empty() {
1182        let args = parse_args(quote! {}).unwrap();
1183        assert!(args.api_id.is_none());
1184    }
1185
1186    #[test]
1187    fn service_args_hex_api_id() {
1188        let args = parse_args(quote! { api_id = 0x1234 }).unwrap();
1189        assert_eq!(args.api_id, Some(0x1234));
1190    }
1191
1192    #[test]
1193    fn service_args_decimal_api_id() {
1194        let args = parse_args(quote! { api_id = 42 }).unwrap();
1195        assert_eq!(args.api_id, Some(42));
1196    }
1197
1198    #[test]
1199    fn service_args_api_id_max() {
1200        let args = parse_args(quote! { api_id = 0xffff }).unwrap();
1201        assert_eq!(args.api_id, Some(u16::MAX));
1202    }
1203
1204    #[test]
1205    fn service_args_api_id_overflow() {
1206        let err = parse_args(quote! { api_id = 70000 }).unwrap_err();
1207        let msg = err.to_string();
1208        assert!(msg.contains("does not fit in u16"), "got: {msg}");
1209    }
1210
1211    #[test]
1212    fn service_args_unknown_key() {
1213        let err = parse_args(quote! { unknown = 1 }).unwrap_err();
1214        let msg = err.to_string();
1215        assert!(msg.contains("unknown"), "got: {msg}");
1216    }
1217
1218    #[test]
1219    fn service_args_duplicate_api_id() {
1220        let err = parse_args(quote! { api_id = 1, api_id = 2 }).unwrap_err();
1221        let msg = err.to_string();
1222        assert!(msg.contains("more than once"), "got: {msg}");
1223    }
1224
1225    #[test]
1226    fn service_args_api_id_non_integer_literal() {
1227        // A string literal in the value position is rejected by `LitInt`.
1228        let err = parse_args(quote! { api_id = "nope" }).unwrap_err();
1229        // Error message comes from `syn` itself; we just want *some* error.
1230        let _ = err.to_string();
1231    }
1232}