1#![allow(clippy::result_large_err)]
2pub use unsynn::Error as ParseError;
58pub use unsynn::ToTokens;
59
60use proc_macro2::TokenStream as TokenStream2;
61use unsynn::operator::names::{
62 Assign, Colon, Comma, Gt, LifetimeTick, Lt, PathSep, Pound, RArrow, Semicolon,
63};
64use unsynn::{
65 Any, BraceGroupContaining, BracketGroupContaining, CommaDelimitedVec, Cons, Either,
66 EndOfStream, Except, Ident, LiteralString, Many, Optional, ParenthesisGroupContaining, Parse,
67 ToTokenIter, TokenStream, keyword, unsynn,
68};
69
70keyword! {
71 pub KAsync = "async";
72 pub KFn = "fn";
73 pub KTrait = "trait";
74 pub KSelfKw = "self";
75 pub KMut = "mut";
76 pub KDoc = "doc";
77 pub KPub = "pub";
78 pub KWhere = "where";
79}
80
81type VerbatimUntil<C> = Many<Cons<Except<C>, AngleTokenTree>>;
83
84unsynn! {
85 #[derive(Clone)]
87 pub struct AngleTokenTree(
88 pub Either<Cons<Lt, Vec<Cons<Except<Gt>, AngleTokenTree>>, Gt>, unsynn::TokenTree>,
89 );
90
91 pub struct RawAttribute {
92 pub _pound: Pound,
93 pub body: BracketGroupContaining<TokenStream>,
94 }
95
96 pub struct DocAttribute {
97 pub _doc: KDoc,
98 pub _assign: Assign,
99 pub value: LiteralString,
100 }
101
102 pub enum Visibility {
103 Pub(KPub),
104 PubRestricted(Cons<KPub, ParenthesisGroupContaining<TokenStream>>),
105 }
106
107 pub struct RefSelf {
108 pub _amp: unsynn::operator::names::And,
109 pub mutability: Option<KMut>,
110 pub name: KSelfKw,
111 }
112
113 pub struct MethodParam {
114 pub name: Ident,
115 pub _colon: Colon,
116 pub ty: Type,
117 }
118
119 pub struct GenericParams {
120 pub _lt: Lt,
121 pub params: VerbatimUntil<Gt>,
122 pub _gt: Gt,
123 }
124
125 #[derive(Clone)]
126 pub struct TypePath {
127 pub leading: Option<PathSep>,
128 pub first: Ident,
129 pub rest: Any<Cons<PathSep, Ident>>,
130 }
131
132 #[derive(Clone)]
133 pub struct Lifetime {
134 pub _apo: LifetimeTick,
135 pub ident: Ident,
136 }
137
138 #[derive(Clone)]
139 pub enum GenericArgument {
140 Lifetime(Lifetime),
141 Type(Type),
142 }
143
144 #[derive(Clone)]
145 pub enum Type {
146 Reference(TypeRef),
147 Tuple(TypeTuple),
148 PathWithGenerics(PathWithGenerics),
149 Path(TypePath),
150 }
151
152 #[derive(Clone)]
153 pub struct TypeRef {
154 pub _amp: unsynn::operator::names::And,
155 pub lifetime: Option<Cons<LifetimeTick, Ident>>,
156 pub mutable: Option<KMut>,
157 pub inner: Box<Type>,
158 }
159
160 #[derive(Clone)]
161 pub struct TypeTuple(
162 pub ParenthesisGroupContaining<CommaDelimitedVec<Type>>,
163 );
164
165 #[derive(Clone)]
166 pub struct PathWithGenerics {
167 pub path: TypePath,
168 pub _lt: Lt,
169 pub args: CommaDelimitedVec<GenericArgument>,
170 pub _gt: Gt,
171 }
172
173 pub struct ReturnType {
174 pub _arrow: RArrow,
175 pub ty: Type,
176 }
177
178 pub struct WhereClause {
179 pub _where: KWhere,
180 pub bounds: VerbatimUntil<Semicolon>,
181 }
182
183 pub struct MethodParams {
184 pub receiver: RefSelf,
185 pub rest: Optional<Cons<Comma, CommaDelimitedVec<MethodParam>>>,
186 }
187
188 pub struct ServiceMethod {
189 pub attributes: Any<RawAttribute>,
190 pub _async: KAsync,
191 pub _fn: KFn,
192 pub name: Ident,
193 pub generics: Optional<GenericParams>,
194 pub params: ParenthesisGroupContaining<MethodParams>,
195 pub return_type: Optional<ReturnType>,
196 pub where_clause: Optional<WhereClause>,
197 pub _semi: Semicolon,
198 }
199
200 pub struct ServiceTrait {
201 pub attributes: Any<RawAttribute>,
202 pub vis: Optional<Visibility>,
203 pub _trait: KTrait,
204 pub name: Ident,
205 pub generics: Optional<GenericParams>,
206 pub body: BraceGroupContaining<Any<ServiceMethod>>,
207 pub _eos: EndOfStream,
208 }
209}
210
211impl GenericArgument {
216 pub fn has_lifetime(&self) -> bool {
217 match self {
218 GenericArgument::Lifetime(_) => true,
219 GenericArgument::Type(ty) => ty.has_lifetime(),
220 }
221 }
222
223 pub fn has_named_lifetime(&self, name: &str) -> bool {
224 match self {
225 GenericArgument::Lifetime(lifetime) => lifetime.ident == name,
226 GenericArgument::Type(ty) => ty.has_named_lifetime(name),
227 }
228 }
229
230 pub fn has_non_named_lifetime(&self, name: &str) -> bool {
231 match self {
232 GenericArgument::Lifetime(lifetime) => lifetime.ident != name,
233 GenericArgument::Type(ty) => ty.has_non_named_lifetime(name),
234 }
235 }
236
237 pub fn has_elided_reference_lifetime(&self) -> bool {
238 match self {
239 GenericArgument::Lifetime(_) => false,
240 GenericArgument::Type(ty) => ty.has_elided_reference_lifetime(),
241 }
242 }
243
244 pub fn contains_channel(&self) -> bool {
245 match self {
246 GenericArgument::Lifetime(_) => false,
247 GenericArgument::Type(ty) => ty.contains_channel(),
248 }
249 }
250}
251
252impl Type {
257 pub fn as_result(&self) -> Option<(&Type, &Type)> {
259 match self {
260 Type::PathWithGenerics(PathWithGenerics { path, args, .. })
261 if path.last_segment().as_str() == "Result" && args.len() == 2 =>
262 {
263 let args_slice = args.as_slice();
264 match (&args_slice[0].value, &args_slice[1].value) {
265 (GenericArgument::Type(ok), GenericArgument::Type(err)) => Some((ok, err)),
266 _ => None,
267 }
268 }
269 _ => None,
270 }
271 }
272
273 pub fn has_lifetime(&self) -> bool {
275 match self {
276 Type::Reference(TypeRef {
277 lifetime: Some(_), ..
278 }) => true,
279 Type::Reference(TypeRef { inner, .. }) => inner.has_lifetime(),
280 Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
281 args.iter().any(|t| t.value.has_lifetime())
282 }
283 Type::Tuple(TypeTuple(group)) => group.content.iter().any(|t| t.value.has_lifetime()),
284 Type::Path(_) => false,
285 }
286 }
287
288 pub fn has_named_lifetime(&self, name: &str) -> bool {
290 match self {
291 Type::Reference(TypeRef {
292 lifetime: Some(lifetime),
293 ..
294 }) => lifetime.second == name,
295 Type::Reference(TypeRef { inner, .. }) => inner.has_named_lifetime(name),
296 Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
297 args.iter().any(|t| t.value.has_named_lifetime(name))
298 }
299 Type::Tuple(TypeTuple(group)) => group
300 .content
301 .iter()
302 .any(|t| t.value.has_named_lifetime(name)),
303 Type::Path(_) => false,
304 }
305 }
306
307 pub fn has_non_named_lifetime(&self, name: &str) -> bool {
309 match self {
310 Type::Reference(TypeRef {
311 lifetime: Some(lifetime),
312 ..
313 }) => lifetime.second != name,
314 Type::Reference(TypeRef { inner, .. }) => inner.has_non_named_lifetime(name),
315 Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
316 args.iter().any(|t| t.value.has_non_named_lifetime(name))
317 }
318 Type::Tuple(TypeTuple(group)) => group
319 .content
320 .iter()
321 .any(|t| t.value.has_non_named_lifetime(name)),
322 Type::Path(_) => false,
323 }
324 }
325
326 pub fn has_elided_reference_lifetime(&self) -> bool {
330 match self {
331 Type::Reference(TypeRef { lifetime: None, .. }) => true,
332 Type::Reference(TypeRef { inner, .. }) => inner.has_elided_reference_lifetime(),
333 Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
334 args.iter().any(|t| t.value.has_elided_reference_lifetime())
335 }
336 Type::Tuple(TypeTuple(group)) => group
337 .content
338 .iter()
339 .any(|t| t.value.has_elided_reference_lifetime()),
340 Type::Path(_) => false,
341 }
342 }
343
344 pub fn contains_channel(&self) -> bool {
349 match self {
350 Type::Reference(TypeRef { inner, .. }) => inner.contains_channel(),
351 Type::Tuple(TypeTuple(group)) => {
352 group.content.iter().any(|t| t.value.contains_channel())
353 }
354 Type::PathWithGenerics(PathWithGenerics { path, args, .. }) => {
355 let seg = path.last_segment();
356 if seg == "Tx" || seg == "Rx" {
357 return true;
358 }
359 args.iter().any(|t| t.value.contains_channel())
360 }
361 Type::Path(path) => {
362 let seg = path.last_segment();
363 seg == "Tx" || seg == "Rx"
364 }
365 }
366 }
367}
368
369impl TypePath {
374 pub fn last_segment(&self) -> String {
376 self.rest
377 .iter()
378 .last()
379 .map(|seg| seg.value.second.to_string())
380 .unwrap_or_else(|| self.first.to_string())
381 }
382}
383
384impl ServiceTrait {
389 pub fn name(&self) -> String {
391 self.name.to_string()
392 }
393
394 pub fn doc(&self) -> Option<String> {
396 collect_doc_string(&self.attributes)
397 }
398
399 pub fn methods(&self) -> impl Iterator<Item = &ServiceMethod> {
401 self.body.content.iter().map(|entry| &entry.value)
402 }
403}
404
405impl ServiceMethod {
410 pub fn name(&self) -> String {
412 self.name.to_string()
413 }
414
415 pub fn doc(&self) -> Option<String> {
417 collect_doc_string(&self.attributes)
418 }
419
420 pub fn args(&self) -> impl Iterator<Item = &MethodParam> {
422 self.params
423 .content
424 .rest
425 .iter()
426 .flat_map(|rest| rest.value.second.iter().map(|entry| &entry.value))
427 }
428
429 pub fn return_type(&self) -> Type {
431 self.return_type
432 .iter()
433 .next()
434 .map(|r| r.value.ty.clone())
435 .unwrap_or_else(unit_type)
436 }
437
438 pub fn is_mut_receiver(&self) -> bool {
440 self.params.content.receiver.mutability.is_some()
441 }
442
443 pub fn has_generics(&self) -> bool {
445 !self.generics.is_empty()
446 }
447
448 pub fn wants_context(&self) -> bool {
450 has_attr_path(&self.attributes, &["vox", "context"])
451 }
452
453 pub fn is_idem(&self) -> bool {
455 has_attr_helper(&self.attributes, &["vox"], "idem")
456 }
457
458 pub fn is_persist(&self) -> bool {
460 has_attr_helper(&self.attributes, &["vox"], "persist")
461 }
462}
463
464impl MethodParam {
469 pub fn name(&self) -> String {
471 self.name.to_string()
472 }
473}
474
475pub fn method_ok_and_err_types(return_ty: &Type) -> (&Type, Option<&Type>) {
482 if let Some((ok, err)) = return_ty.as_result() {
483 (ok, Some(err))
484 } else {
485 (return_ty, None)
486 }
487}
488
489fn unit_type() -> Type {
491 let mut iter = "()".to_token_iter();
492 Type::parse(&mut iter).expect("unit type should always parse")
493}
494
495fn collect_doc_string(attrs: &Any<RawAttribute>) -> Option<String> {
497 let mut docs = Vec::new();
498
499 for attr in attrs.iter() {
500 let mut body_iter = attr.value.body.content.clone().to_token_iter();
501 if let Ok(doc_attr) = DocAttribute::parse(&mut body_iter) {
502 let line = doc_attr
503 .value
504 .as_str()
505 .replace("\\\"", "\"")
506 .replace("\\'", "'");
507 docs.push(line);
508 }
509 }
510
511 if docs.is_empty() {
512 None
513 } else {
514 Some(docs.join("\n"))
515 }
516}
517
518fn has_attr_path(attrs: &Any<RawAttribute>, expected: &[&str]) -> bool {
519 attrs
520 .iter()
521 .any(|attr| attr_path_matches(&attr.value, expected))
522}
523
524fn has_attr_helper(attrs: &Any<RawAttribute>, path: &[&str], helper: &str) -> bool {
525 attrs
526 .iter()
527 .any(|attr| attr_helper_matches(&attr.value, path, helper))
528}
529
530fn attr_path_matches(attr: &RawAttribute, expected: &[&str]) -> bool {
531 let mut iter = attr.body.content.clone().to_token_iter();
532 let Ok(path) = TypePath::parse(&mut iter) else {
533 return false;
534 };
535 if EndOfStream::parse(&mut iter).is_err() {
536 return false;
537 }
538 path_matches(&path, expected)
539}
540
541fn attr_helper_matches(attr: &RawAttribute, expected_path: &[&str], expected_helper: &str) -> bool {
542 let mut iter = attr.body.content.clone().to_token_iter();
543 let Ok(path) = TypePath::parse(&mut iter) else {
544 return false;
545 };
546 if !path_matches(&path, expected_path) {
547 return false;
548 }
549
550 let Ok(group) = ParenthesisGroupContaining::<TokenStream>::parse(&mut iter) else {
551 return false;
552 };
553 if EndOfStream::parse(&mut iter).is_err() {
554 return false;
555 }
556
557 let mut inner = group.content.to_token_iter();
558 let Ok(helper) = Ident::parse(&mut inner) else {
559 return false;
560 };
561 if EndOfStream::parse(&mut inner).is_err() {
562 return false;
563 }
564 helper == expected_helper
565}
566
567fn path_matches(path: &TypePath, expected: &[&str]) -> bool {
568 let actual = std::iter::once(path.first.to_string())
569 .chain(path.rest.iter().map(|seg| seg.value.second.to_string()))
570 .collect::<Vec<_>>();
571
572 actual.len() == expected.len()
573 && actual
574 .iter()
575 .zip(expected.iter())
576 .all(|(actual, expected)| actual == expected)
577}
578
579#[allow(clippy::result_large_err)] pub fn parse_trait(tokens: &TokenStream2) -> Result<ServiceTrait, unsynn::Error> {
582 let mut iter = tokens.clone().to_token_iter();
583 ServiceTrait::parse(&mut iter)
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 fn parse(src: &str) -> ServiceTrait {
591 let ts: TokenStream2 = src.parse().expect("tokenstream parse");
592 parse_trait(&ts).expect("trait parse")
593 }
594
595 #[test]
596 fn parse_trait_exposes_docs_methods_and_args() {
597 let trait_def = parse(
598 r#"
599 #[doc = "Calculator service."]
600 pub trait Calculator {
601 #[doc = "Adds two numbers."]
602 async fn add(&self, a: i32, b: i32) -> Result<i64, String>;
603 }
604 "#,
605 );
606
607 assert_eq!(trait_def.name(), "Calculator");
608 assert_eq!(trait_def.doc(), Some("Calculator service.".to_string()));
609
610 let method = trait_def.methods().next().expect("method");
611 assert_eq!(method.name(), "add");
612 assert_eq!(method.doc(), Some("Adds two numbers.".to_string()));
613 assert_eq!(
614 method.args().map(|arg| arg.name()).collect::<Vec<_>>(),
615 vec!["a", "b"]
616 );
617
618 let ret = method.return_type();
619 let (ok, err) = method_ok_and_err_types(&ret);
620 assert!(ok.as_result().is_none());
621 assert!(err.is_some());
622 }
623
624 #[test]
625 fn return_type_defaults_to_unit_when_omitted() {
626 let trait_def = parse(
627 r#"
628 trait Svc {
629 async fn ping(&self);
630 }
631 "#,
632 );
633 let method = trait_def.methods().next().expect("method");
634 let ret = method.return_type();
635 match ret {
636 Type::Tuple(TypeTuple(group)) => assert!(group.content.is_empty()),
637 other => panic!(
638 "expected unit tuple return, got {}",
639 other.to_token_stream()
640 ),
641 }
642 }
643
644 #[test]
645 fn method_helpers_detect_generics_and_mut_receiver() {
646 let trait_def = parse(
647 r#"
648 trait Svc {
649 async fn bad<T>(&mut self, value: T) -> T;
650 }
651 "#,
652 );
653 let method = trait_def.methods().next().expect("method");
654 assert!(method.has_generics());
655 assert!(method.is_mut_receiver());
656 }
657
658 #[test]
659 fn method_helpers_detect_explicit_request_context_opt_in() {
660 let trait_def = parse(
661 r#"
662 trait Svc {
663 #[vox::context]
664 async fn contextual(&self) -> u32;
665
666 async fn plain(&self) -> u32;
667 }
668 "#,
669 );
670 let mut methods = trait_def.methods();
671 assert!(methods.next().expect("contextual method").wants_context());
672 assert!(!methods.next().expect("plain method").wants_context());
673 }
674
675 #[test]
676 fn method_helpers_detect_retry_helper_attributes() {
677 let trait_def = parse(
678 r#"
679 trait Svc {
680 #[vox(idem)]
681 async fn cached(&self) -> u32;
682
683 #[vox(persist)]
684 async fn durable(&self) -> u32;
685
686 async fn plain(&self) -> u32;
687 }
688 "#,
689 );
690 let mut methods = trait_def.methods();
691 let cached = methods.next().expect("cached");
692 assert!(cached.is_idem());
693 assert!(!cached.is_persist());
694
695 let durable = methods.next().expect("durable");
696 assert!(!durable.is_idem());
697 assert!(durable.is_persist());
698
699 let plain = methods.next().expect("plain");
700 assert!(!plain.is_idem());
701 assert!(!plain.is_persist());
702 }
703
704 #[test]
705 fn type_helpers_detect_result_lifetime_and_channel_nesting() {
706 let trait_def = parse(
707 r#"
708 trait Svc {
709 async fn stream(&self, input: &'static str) -> Result<Option<Tx<Vec<u8>>>, Rx<u32>>;
710 }
711 "#,
712 );
713 let method = trait_def.methods().next().expect("method");
714 let arg = method.args().next().expect("arg");
715 assert!(arg.ty.has_lifetime());
716 assert!(!arg.ty.contains_channel());
717
718 let ret = method.return_type();
719 let (ok, err) = method_ok_and_err_types(&ret);
720 assert!(ok.contains_channel());
721 assert!(err.expect("result err type").contains_channel());
722 }
723
724 #[test]
725 fn type_helpers_detect_named_and_elided_lifetimes() {
726 let trait_def = parse(
727 r#"
728 trait Svc {
729 async fn borrowed(&self) -> Result<&'vox str, Error>;
730 async fn bad_lifetime(&self) -> Result<&'a str, Error>;
731 async fn elided(&self) -> Result<&str, Error>;
732 }
733 "#,
734 );
735 let mut methods = trait_def.methods();
736
737 let borrowed = methods.next().expect("borrowed method").return_type();
738 let (borrowed_ok, _) = method_ok_and_err_types(&borrowed);
739 assert!(borrowed_ok.has_named_lifetime("vox"));
740 assert!(!borrowed_ok.has_non_named_lifetime("vox"));
741 assert!(!borrowed_ok.has_elided_reference_lifetime());
742
743 let bad_lifetime = methods.next().expect("bad_lifetime method").return_type();
744 let (bad_ok, _) = method_ok_and_err_types(&bad_lifetime);
745 assert!(!bad_ok.has_named_lifetime("vox"));
746 assert!(bad_ok.has_non_named_lifetime("vox"));
747 assert!(!bad_ok.has_elided_reference_lifetime());
748
749 let elided = methods.next().expect("elided method").return_type();
750 let (elided_ok, _) = method_ok_and_err_types(&elided);
751 assert!(!elided_ok.has_named_lifetime("vox"));
752 assert!(!elided_ok.has_non_named_lifetime("vox"));
753 assert!(elided_ok.has_elided_reference_lifetime());
754 }
755
756 #[test]
757 fn type_path_last_segment_uses_trailing_segment() {
758 let trait_def = parse(
759 r#"
760 trait Svc {
761 async fn f(&self) -> std::result::Result<u8, u8>;
762 }
763 "#,
764 );
765 let method = trait_def.methods().next().expect("method");
766 let ret = method.return_type();
767 let Type::PathWithGenerics(path_with_generics) = ret else {
768 panic!("expected path with generics");
769 };
770 assert_eq!(path_with_generics.path.last_segment(), "Result");
771 }
772}