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}