roam_schema/
lib.rs

1#![deny(unsafe_code)]
2
3//! Schema types for roam RPC service definitions.
4//!
5//! # Design Philosophy
6//!
7//! This crate uses `facet::Shape` directly for type information rather than
8//! defining a parallel type system. This means:
9//!
10//! - **No `TypeDetail`** — We use `&'static Shape` from facet instead
11//! - **Full type introspection** — Shape provides complete type information
12//! - **Zero conversion overhead** — Types are described by their Shape directly
13//!
14//! For type-specific queries (is this a stream? what are the struct fields?),
15//! use the `facet_core` API to inspect the `Shape`.
16
17use std::borrow::Cow;
18
19use facet::Facet;
20use facet_core::Shape;
21
22/// A complete service definition with all its methods.
23#[derive(Debug, Clone, Facet)]
24pub struct ServiceDetail {
25    /// Service name (e.g., "Calculator").
26    pub name: Cow<'static, str>,
27
28    /// Methods defined on this service.
29    pub methods: Vec<MethodDetail>,
30
31    /// Documentation string, if any.
32    pub doc: Option<Cow<'static, str>>,
33}
34
35/// A single method in a service definition.
36#[derive(Debug, Clone, Facet)]
37pub struct MethodDetail {
38    /// The service this method belongs to.
39    pub service_name: Cow<'static, str>,
40
41    /// Method name (e.g., "add").
42    pub method_name: Cow<'static, str>,
43
44    /// Method arguments (excluding `&self`).
45    pub args: Vec<ArgDetail>,
46
47    /// Return type shape.
48    ///
49    /// Use `facet_core` to inspect the shape:
50    /// - `shape.def` reveals if it's a struct, enum, primitive, etc.
51    /// - `shape.type_params` gives generic parameters
52    /// - Check for `#[facet(roam = "tx")]` attribute for streaming types
53    pub return_type: &'static Shape,
54
55    /// Documentation string, if any.
56    pub doc: Option<Cow<'static, str>>,
57}
58
59/// A single argument in a method signature.
60#[derive(Debug, Clone, Facet)]
61pub struct ArgDetail {
62    /// Argument name.
63    pub name: Cow<'static, str>,
64
65    /// Argument type shape.
66    pub ty: &'static Shape,
67}
68
69/// Summary information about a service (for listings/discovery).
70#[derive(Debug, Clone, PartialEq, Eq, Facet)]
71pub struct ServiceSummary {
72    pub name: Cow<'static, str>,
73    pub method_count: u32,
74    pub doc: Option<Cow<'static, str>>,
75}
76
77/// Summary information about a method (for listings/discovery).
78#[derive(Debug, Clone, PartialEq, Eq, Facet)]
79pub struct MethodSummary {
80    pub name: Cow<'static, str>,
81    pub method_id: u64,
82    pub doc: Option<Cow<'static, str>>,
83}
84
85/// Explanation of why a method call mismatched.
86#[repr(u8)]
87#[derive(Debug, Clone, Facet)]
88pub enum MismatchExplanation {
89    /// Service doesn't exist.
90    UnknownService { closest: Option<Cow<'static, str>> } = 0,
91
92    /// Service exists but method doesn't.
93    UnknownMethod {
94        service: Cow<'static, str>,
95        closest: Option<Cow<'static, str>>,
96    } = 1,
97
98    /// Method exists but signature differs.
99    ///
100    /// The `expected` field contains the server's method signature.
101    /// Compare with the client's signature to diagnose the mismatch.
102    SignatureMismatch {
103        service: Cow<'static, str>,
104        method: Cow<'static, str>,
105        expected: MethodDetail,
106    } = 2,
107}
108
109// ============================================================================
110// Helper functions for working with Shape
111// ============================================================================
112
113// TODO(facet): Replace these string comparisons with `decl_id` comparison once
114// facet supports declaration IDs. Declaration IDs would allow comparing generic
115// types like `Tx<i32>` and `Tx<String>` as "the same type declaration" without
116// string matching. See: https://github.com/facet-rs/facet/issues/XXXX
117//
118// For now, we use `fully_qualified_type_path()` which returns paths like
119// "roam_session::Tx<i32>" - we check if it starts with "roam_session::Tx<".
120// See also: https://github.com/facet-rs/facet/issues/1716
121
122/// Returns the fully qualified type path, e.g. "std::collections::HashMap<K, V>".
123///
124/// Combines module_path and type_identifier. For types without a module_path
125/// (primitives), returns just the type_identifier.
126pub fn fully_qualified_type_path(shape: &Shape) -> std::borrow::Cow<'static, str> {
127    match shape.module_path {
128        Some(module) => std::borrow::Cow::Owned(format!("{}::{}", module, shape.type_identifier)),
129        None => std::borrow::Cow::Borrowed(shape.type_identifier),
130    }
131}
132
133/// Check if a shape represents a Tx (caller→callee) stream.
134pub fn is_tx(shape: &Shape) -> bool {
135    shape.module_path == Some("roam_session") && shape.type_identifier == "Tx"
136}
137
138/// Check if a shape represents an Rx (callee→caller) stream.
139pub fn is_rx(shape: &Shape) -> bool {
140    shape.module_path == Some("roam_session") && shape.type_identifier == "Rx"
141}
142
143/// Check if a shape represents any streaming type (Tx or Rx).
144pub fn is_stream(shape: &Shape) -> bool {
145    is_tx(shape) || is_rx(shape)
146}
147
148/// Recursively check if a shape or any of its type parameters contains a stream.
149pub fn contains_stream(shape: &Shape) -> bool {
150    if is_stream(shape) {
151        return true;
152    }
153
154    // Check type parameters recursively
155    for param in shape.type_params {
156        if contains_stream(param.shape) {
157            return true;
158        }
159    }
160
161    false
162}
163
164// ============================================================================
165// Shape classification for codegen
166// ============================================================================
167
168use facet_core::{Def, ScalarType, StructKind, Type, UserType};
169
170/// Classification of a Shape for codegen purposes.
171///
172/// This provides a higher-level view than raw `Shape.ty` and `Shape.def`,
173/// combining both to give the semantic type category needed for code generation.
174#[derive(Debug, Clone, Copy)]
175pub enum ShapeKind<'a> {
176    /// Scalar/primitive type
177    Scalar(ScalarType),
178    /// List/Vec of elements
179    List { element: &'static Shape },
180    /// Fixed-size array
181    Array { element: &'static Shape, len: usize },
182    /// Slice (treated like list for codegen)
183    Slice { element: &'static Shape },
184    /// Optional value
185    Option { inner: &'static Shape },
186    /// Map/HashMap
187    Map {
188        key: &'static Shape,
189        value: &'static Shape,
190    },
191    /// Set/HashSet
192    Set { element: &'static Shape },
193    /// Named or anonymous struct
194    Struct(StructInfo<'a>),
195    /// Named or anonymous enum
196    Enum(EnumInfo<'a>),
197    /// Tuple (including unit) - from type_params
198    Tuple {
199        elements: &'a [facet_core::TypeParam],
200    },
201    /// Tuple struct - from struct fields (anonymous tuple like (i32, String))
202    TupleStruct { fields: &'a [facet_core::Field] },
203    /// Tx stream (caller → callee)
204    Tx { inner: &'static Shape },
205    /// Rx stream (callee → caller)
206    Rx { inner: &'static Shape },
207    /// Smart pointer (Box, Arc, etc.) - transparent
208    Pointer { pointee: &'static Shape },
209    /// Result type
210    Result {
211        ok: &'static Shape,
212        err: &'static Shape,
213    },
214    /// Unknown/opaque type
215    Opaque,
216}
217
218/// Information about a struct type.
219#[derive(Debug, Clone, Copy)]
220pub struct StructInfo<'a> {
221    /// Type name (e.g., "MyStruct"), or None for tuples/anonymous
222    pub name: Option<&'static str>,
223    /// Struct kind (unit, tuple struct, named struct)
224    pub kind: StructKind,
225    /// Fields in declaration order
226    pub fields: &'a [facet_core::Field],
227}
228
229/// Information about an enum type.
230#[derive(Debug, Clone, Copy)]
231pub struct EnumInfo<'a> {
232    /// Type name (e.g., "MyEnum")
233    pub name: Option<&'static str>,
234    /// Variants in declaration order
235    pub variants: &'a [facet_core::Variant],
236}
237
238/// Classify a Shape into a ShapeKind for codegen.
239pub fn classify_shape(shape: &'static Shape) -> ShapeKind<'static> {
240    // Check for roam streaming types first
241    if is_tx(shape)
242        && let Some(inner) = shape.type_params.first()
243    {
244        return ShapeKind::Tx { inner: inner.shape };
245    }
246    if is_rx(shape)
247        && let Some(inner) = shape.type_params.first()
248    {
249        return ShapeKind::Rx { inner: inner.shape };
250    }
251
252    // Check for transparent wrappers
253    if shape.is_transparent()
254        && let Some(inner) = shape.inner
255    {
256        return classify_shape(inner);
257    }
258
259    // Check scalars first
260    if let Some(scalar) = shape.scalar_type() {
261        return ShapeKind::Scalar(scalar);
262    }
263
264    // Check semantic definitions (containers)
265    match shape.def {
266        Def::List(list_def) => {
267            return ShapeKind::List {
268                element: list_def.t(),
269            };
270        }
271        Def::Array(array_def) => {
272            return ShapeKind::Array {
273                element: array_def.t(),
274                len: array_def.n,
275            };
276        }
277        Def::Slice(slice_def) => {
278            return ShapeKind::Slice {
279                element: slice_def.t(),
280            };
281        }
282        Def::Option(opt_def) => {
283            return ShapeKind::Option { inner: opt_def.t() };
284        }
285        Def::Map(map_def) => {
286            return ShapeKind::Map {
287                key: map_def.k(),
288                value: map_def.v(),
289            };
290        }
291        Def::Set(set_def) => {
292            return ShapeKind::Set {
293                element: set_def.t(),
294            };
295        }
296        Def::Result(result_def) => {
297            return ShapeKind::Result {
298                ok: result_def.t(),
299                err: result_def.e(),
300            };
301        }
302        Def::Pointer(ptr_def) => {
303            if let Some(pointee) = ptr_def.pointee {
304                return ShapeKind::Pointer { pointee };
305            }
306        }
307        _ => {}
308    }
309
310    // Check user-defined types (structs, enums)
311    match shape.ty {
312        Type::User(UserType::Struct(struct_type)) => {
313            // Check for tuple structs first - tuple element shapes are in fields, not type_params
314            if struct_type.kind == StructKind::Tuple {
315                return ShapeKind::TupleStruct {
316                    fields: struct_type.fields,
317                };
318            }
319            // Extract name from type_identifier (e.g., "my_crate::MyStruct" -> "MyStruct")
320            let name = extract_type_name(shape.type_identifier);
321            return ShapeKind::Struct(StructInfo {
322                name,
323                kind: struct_type.kind,
324                fields: struct_type.fields,
325            });
326        }
327        Type::User(UserType::Enum(enum_type)) => {
328            let name = extract_type_name(shape.type_identifier);
329            return ShapeKind::Enum(EnumInfo {
330                name,
331                variants: enum_type.variants,
332            });
333        }
334        Type::Pointer(_) => {
335            // Reference types - get inner from type_params
336            if let Some(inner) = shape.type_params.first() {
337                return classify_shape(inner.shape);
338            }
339        }
340        _ => {}
341    }
342
343    ShapeKind::Opaque
344}
345
346/// Get the type name if this is a named type.
347/// Returns None for anonymous types (tuples, arrays, primitives).
348fn extract_type_name(type_identifier: &'static str) -> Option<&'static str> {
349    // Skip anonymous/primitive patterns
350    if type_identifier.is_empty()
351        || type_identifier.starts_with('(')
352        || type_identifier.starts_with('[')
353    {
354        return None;
355    }
356
357    // type_identifier is already the simple name (e.g., "MyStruct", "Vec")
358    Some(type_identifier)
359}
360
361/// Information about an enum variant for codegen.
362#[derive(Debug, Clone, Copy)]
363pub enum VariantKind<'a> {
364    /// Unit variant: `Foo`
365    Unit,
366    /// Newtype/tuple variant with single field: `Foo(T)`
367    Newtype { inner: &'static Shape },
368    /// Tuple variant with multiple fields: `Foo(T1, T2)`
369    Tuple { fields: &'a [facet_core::Field] },
370    /// Struct variant: `Foo { x: T1, y: T2 }`
371    Struct { fields: &'a [facet_core::Field] },
372}
373
374/// Classify an enum variant.
375pub fn classify_variant(variant: &facet_core::Variant) -> VariantKind<'_> {
376    match variant.data.kind {
377        StructKind::Unit => VariantKind::Unit,
378        StructKind::TupleStruct | StructKind::Tuple => {
379            if variant.data.fields.len() == 1 {
380                VariantKind::Newtype {
381                    inner: variant.data.fields[0].shape(),
382                }
383            } else {
384                VariantKind::Tuple {
385                    fields: variant.data.fields,
386                }
387            }
388        }
389        StructKind::Struct => VariantKind::Struct {
390            fields: variant.data.fields,
391        },
392    }
393}
394
395/// Check if a shape represents bytes (`Vec<u8>` or `&[u8]`).
396pub fn is_bytes(shape: &Shape) -> bool {
397    match shape.def {
398        Def::List(list_def) => matches!(list_def.t().scalar_type(), Some(ScalarType::U8)),
399        Def::Slice(slice_def) => matches!(slice_def.t().scalar_type(), Some(ScalarType::U8)),
400        _ => false,
401    }
402}