roam_macros_parse/
lib.rs

1//! Parser grammar for roam RPC service trait definitions.
2//!
3//! # This Is Just a Grammar
4//!
5//! This crate contains **only** the [unsynn] grammar for parsing Rust trait definitions
6//! that define roam RPC services. It does not:
7//!
8//! - Generate any code
9//! - Perform validation
10//! - Know anything about roam's wire protocol
11//! - Have opinions about how services should be implemented
12//!
13//! It simply parses syntax like:
14//!
15//! ```ignore
16//! pub trait Calculator {
17//!     /// Add two numbers.
18//!     async fn add(&self, a: i32, b: i32) -> i32;
19//! }
20//! ```
21//!
22//! ...and produces an AST ([`ServiceTrait`]) that downstream crates can inspect.
23//!
24//! # Why a Separate Crate?
25//!
26//! The grammar is extracted into its own crate so that:
27//!
28//! 1. **It can be tested independently** — We use [datatest-stable] + [insta] for
29//!    snapshot testing the parsed AST, which isn't possible in a proc-macro crate.
30//!
31//! 2. **It's reusable** — Other tools (linters, documentation generators, IDE plugins)
32//!    can parse service definitions without pulling in proc-macro dependencies.
33//!
34//! 3. **Separation of concerns** — The grammar is pure parsing; [`roam-macros`] handles
35//!    the proc-macro machinery; [`roam-codegen`] handles actual code generation.
36//!
37//! # The Bigger Picture
38//!
39//! ```text
40//! roam-macros-parse     roam-macros              roam-codegen
41//! ┌──────────────┐     ┌──────────────┐         ┌──────────────┐
42//! │              │     │              │         │              │
43//! │  unsynn      │────▶│  #[service]  │────────▶│  build.rs    │
44//! │  grammar     │     │  proc macro  │         │  code gen    │
45//! │              │     │              │         │              │
46//! └──────────────┘     └──────────────┘         └──────────────┘
47//!    just parsing         emit metadata          Rust, TS, Go...
48//! ```
49//!
50//! [unsynn]: https://docs.rs/unsynn
51//! [datatest-stable]: https://docs.rs/datatest-stable
52//! [insta]: https://docs.rs/insta
53//! [`roam-macros`]: https://docs.rs/roam-service-macros
54//! [`roam-codegen`]: https://docs.rs/roam-codegen
55
56pub use unsynn::Error as ParseError;
57pub use unsynn::ToTokens;
58
59use proc_macro2::TokenStream as TokenStream2;
60use unsynn::operator::names::{Assign, Colon, Comma, Gt, Lt, PathSep, Pound, RArrow, Semicolon};
61use unsynn::{
62    Any, BraceGroupContaining, BracketGroupContaining, CommaDelimitedVec, Cons, Either,
63    EndOfStream, Except, Ident, LiteralString, Many, Optional, ParenthesisGroupContaining, Parse,
64    ToTokenIter, TokenStream, keyword, operator, unsynn,
65};
66
67keyword! {
68    pub KAsync = "async";
69    pub KFn = "fn";
70    pub KTrait = "trait";
71    pub KSelfKw = "self";
72    pub KMut = "mut";
73    pub KDoc = "doc";
74    pub KPub = "pub";
75    pub KWhere = "where";
76}
77
78operator! {
79    pub Apostrophe = "'";
80}
81
82/// Parses tokens and groups until `C` is found, handling `<...>` correctly.
83type VerbatimUntil<C> = Many<Cons<Except<C>, AngleTokenTree>>;
84
85unsynn! {
86    /// Parses either a `TokenTree` or `<...>` grouping.
87    #[derive(Clone)]
88    pub struct AngleTokenTree(
89        pub Either<Cons<Lt, Vec<Cons<Except<Gt>, AngleTokenTree>>, Gt>, unsynn::TokenTree>,
90    );
91
92    pub struct RawAttribute {
93        pub _pound: Pound,
94        pub body: BracketGroupContaining<TokenStream>,
95    }
96
97    pub struct DocAttribute {
98        pub _doc: KDoc,
99        pub _assign: Assign,
100        pub value: LiteralString,
101    }
102
103    pub enum Visibility {
104        Pub(KPub),
105        PubRestricted(Cons<KPub, ParenthesisGroupContaining<TokenStream>>),
106    }
107
108    pub struct RefSelf {
109        pub _amp: unsynn::operator::names::And,
110        pub mutability: Option<KMut>,
111        pub name: KSelfKw,
112    }
113
114    pub struct MethodParam {
115        pub name: Ident,
116        pub _colon: Colon,
117        pub ty: Type,
118    }
119
120    pub struct GenericParams {
121        pub _lt: Lt,
122        pub params: VerbatimUntil<Gt>,
123        pub _gt: Gt,
124    }
125
126    #[derive(Clone)]
127    pub struct TypePath {
128        pub leading: Option<PathSep>,
129        pub first: Ident,
130        pub rest: Any<Cons<PathSep, Ident>>,
131    }
132
133    #[derive(Clone)]
134    pub enum Type {
135        Reference(TypeRef),
136        Tuple(TypeTuple),
137        PathWithGenerics(PathWithGenerics),
138        Path(TypePath),
139    }
140
141    #[derive(Clone)]
142    pub struct TypeRef {
143        pub _amp: unsynn::operator::names::And,
144        pub lifetime: Option<Cons<Apostrophe, Ident>>,
145        pub mutable: Option<KMut>,
146        pub inner: Box<Type>,
147    }
148
149    #[derive(Clone)]
150    pub struct TypeTuple(
151        pub ParenthesisGroupContaining<CommaDelimitedVec<Type>>,
152    );
153
154    #[derive(Clone)]
155    pub struct PathWithGenerics {
156        pub path: TypePath,
157        pub _lt: Lt,
158        pub args: CommaDelimitedVec<Type>,
159        pub _gt: Gt,
160    }
161
162    pub struct ReturnType {
163        pub _arrow: RArrow,
164        pub ty: Type,
165    }
166
167    pub struct WhereClause {
168        pub _where: KWhere,
169        pub bounds: VerbatimUntil<Semicolon>,
170    }
171
172    pub struct MethodParams {
173        pub receiver: RefSelf,
174        pub rest: Optional<Cons<Comma, CommaDelimitedVec<MethodParam>>>,
175    }
176
177    pub struct ServiceMethod {
178        pub attributes: Any<RawAttribute>,
179        pub _async: KAsync,
180        pub _fn: KFn,
181        pub name: Ident,
182        pub generics: Optional<GenericParams>,
183        pub params: ParenthesisGroupContaining<MethodParams>,
184        pub return_type: Optional<ReturnType>,
185        pub where_clause: Optional<WhereClause>,
186        pub _semi: Semicolon,
187    }
188
189    pub struct ServiceTrait {
190        pub attributes: Any<RawAttribute>,
191        pub vis: Optional<Visibility>,
192        pub _trait: KTrait,
193        pub name: Ident,
194        pub generics: Optional<GenericParams>,
195        pub body: BraceGroupContaining<Any<ServiceMethod>>,
196        pub _eos: EndOfStream,
197    }
198}
199
200// ============================================================================
201// Helper methods for Type
202// ============================================================================
203
204impl Type {
205    /// Extract Ok and Err types if this is Result<T, E>
206    pub fn as_result(&self) -> Option<(&Type, &Type)> {
207        match self {
208            Type::PathWithGenerics(PathWithGenerics { path, args, .. })
209                if path.last_segment().as_str() == "Result" && args.len() == 2 =>
210            {
211                let types = args.as_slice();
212                Some((&types[0].value, &types[1].value))
213            }
214            _ => None,
215        }
216    }
217
218    /// Check if type contains a lifetime anywhere in the tree
219    pub fn has_lifetime(&self) -> bool {
220        match self {
221            Type::Reference(TypeRef {
222                lifetime: Some(_), ..
223            }) => true,
224            Type::Reference(TypeRef { inner, .. }) => inner.has_lifetime(),
225            Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
226                args.iter().any(|t| t.value.has_lifetime())
227            }
228            Type::Tuple(TypeTuple(group)) => group.content.iter().any(|t| t.value.has_lifetime()),
229            Type::Path(_) => false,
230        }
231    }
232
233    /// Check if type contains Tx or Rx at any nesting level
234    ///
235    /// Note: This is a heuristic based on type names. Proper validation should
236    /// happen at codegen time when we can resolve types properly.
237    pub fn contains_channel(&self) -> bool {
238        match self {
239            Type::Reference(TypeRef { inner, .. }) => inner.contains_channel(),
240            Type::Tuple(TypeTuple(group)) => {
241                group.content.iter().any(|t| t.value.contains_channel())
242            }
243            Type::PathWithGenerics(PathWithGenerics { path, args, .. }) => {
244                let seg = path.last_segment();
245                if seg == "Tx" || seg == "Rx" {
246                    return true;
247                }
248                args.iter().any(|t| t.value.contains_channel())
249            }
250            Type::Path(path) => {
251                let seg = path.last_segment();
252                seg == "Tx" || seg == "Rx"
253            }
254        }
255    }
256}
257
258// ============================================================================
259// Helper methods for TypePath
260// ============================================================================
261
262impl TypePath {
263    /// Get the last segment (e.g., "Result" from "std::result::Result")
264    pub fn last_segment(&self) -> String {
265        self.rest
266            .iter()
267            .last()
268            .map(|seg| seg.value.second.to_string())
269            .unwrap_or_else(|| self.first.to_string())
270    }
271}
272
273// ============================================================================
274// Helper methods for ServiceTrait
275// ============================================================================
276
277impl ServiceTrait {
278    /// Get the trait name as a string.
279    pub fn name(&self) -> String {
280        self.name.to_string()
281    }
282
283    /// Get the trait's doc string (collected from #[doc = "..."] attributes).
284    pub fn doc(&self) -> Option<String> {
285        collect_doc_string(&self.attributes)
286    }
287
288    /// Get an iterator over the methods.
289    pub fn methods(&self) -> impl Iterator<Item = &ServiceMethod> {
290        self.body.content.iter().map(|entry| &entry.value)
291    }
292}
293
294// ============================================================================
295// Helper methods for ServiceMethod
296// ============================================================================
297
298impl ServiceMethod {
299    /// Get the method name as a string.
300    pub fn name(&self) -> String {
301        self.name.to_string()
302    }
303
304    /// Get the method's doc string (collected from #[doc = "..."] attributes).
305    pub fn doc(&self) -> Option<String> {
306        collect_doc_string(&self.attributes)
307    }
308
309    /// Get an iterator over the method's parameters (excluding &self).
310    pub fn args(&self) -> impl Iterator<Item = &MethodParam> {
311        self.params
312            .content
313            .rest
314            .iter()
315            .flat_map(|rest| rest.value.second.iter().map(|entry| &entry.value))
316    }
317
318    /// Get the return type, defaulting to () if not specified.
319    pub fn return_type(&self) -> Type {
320        self.return_type
321            .iter()
322            .next()
323            .map(|r| r.value.ty.clone())
324            .unwrap_or_else(unit_type)
325    }
326
327    /// Check if receiver is &mut self (not allowed for service methods).
328    pub fn is_mut_receiver(&self) -> bool {
329        self.params.content.receiver.mutability.is_some()
330    }
331
332    /// Check if method has generics.
333    pub fn has_generics(&self) -> bool {
334        !self.generics.is_empty()
335    }
336}
337
338// ============================================================================
339// Helper methods for MethodParam
340// ============================================================================
341
342impl MethodParam {
343    /// Get the parameter name as a string.
344    pub fn name(&self) -> String {
345        self.name.to_string()
346    }
347}
348
349// ============================================================================
350// Helper functions
351// ============================================================================
352
353/// Extract Ok and Err types from a return type.
354/// Returns (ok_type, Some(err_type)) for Result<T, E>, or (type, None) otherwise.
355pub fn method_ok_and_err_types(return_ty: &Type) -> (&Type, Option<&Type>) {
356    if let Some((ok, err)) = return_ty.as_result() {
357        (ok, Some(err))
358    } else {
359        (return_ty, None)
360    }
361}
362
363/// Returns the unit type `()`.
364fn unit_type() -> Type {
365    let mut iter = "()".to_token_iter();
366    Type::parse(&mut iter).expect("unit type should always parse")
367}
368
369/// Collect doc strings from attributes.
370fn collect_doc_string(attrs: &Any<RawAttribute>) -> Option<String> {
371    let mut docs = Vec::new();
372
373    for attr in attrs.iter() {
374        let mut body_iter = attr.value.body.content.clone().to_token_iter();
375        if let Ok(doc_attr) = DocAttribute::parse(&mut body_iter) {
376            let line = doc_attr.value.as_str().replace("\\\"", "\"");
377            docs.push(line);
378        }
379    }
380
381    if docs.is_empty() {
382        None
383    } else {
384        Some(docs.join("\n"))
385    }
386}
387
388/// Parse a trait definition from a token stream.
389#[allow(clippy::result_large_err)] // unsynn::Error is external, we can't box it
390pub fn parse_trait(tokens: &TokenStream2) -> Result<ServiceTrait, unsynn::Error> {
391    let mut iter = tokens.clone().to_token_iter();
392    ServiceTrait::parse(&mut iter)
393}