1mod emit;
47mod parse;
48
49use proc_macro::TokenStream;
50use quote::quote;
51
52#[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 #[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#[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 #[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 assert!(got.contains("Health"), "got: {got}");
241 assert!(!got.contains("Health {"), "got: {got}");
242 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 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 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 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 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 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 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 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 assert!(
609 got.contains(":: myelin :: TransportResult < () >"),
610 "got: {got}"
611 );
612 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 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 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 assert!(
661 got.contains("GreeterResponse :: Greet (svc . greet (name) . await)"),
662 "got: {got}"
663 );
664 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 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 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 #[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 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 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 #[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 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 assert!(
946 got.contains("($ name : ident , $ mutex : ty , $ depth : expr)"),
947 "got: {got}"
948 );
949 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 assert!(
962 got.contains("[< __GREETER_SERVICE_ $ name : upper >]"),
963 "got: {got}"
964 );
965 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 assert!(
989 got.contains(":: myelin :: static_cell :: StaticCell"),
990 "got: {got}"
991 );
992 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 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 #[test]
1065 fn fnv1a16_known_values() {
1066 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 assert_eq!(emit::fnv1a16("GreeterService"), 0x237d);
1078 assert_eq!(emit::fnv1a16("MathService"), 0x90b0);
1079 assert_eq!(emit::fnv1a16(""), 0x1cd9); }
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 assert!(
1093 got.contains("pub const GREETER_API_ID : u16 ="),
1094 "got: {got}"
1095 );
1096 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 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 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 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 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 let err = parse_args(quote! { api_id = "nope" }).unwrap_err();
1229 let _ = err.to_string();
1231 }
1232}