Skip to main content

weaveffi_ir/
ir.rs

1//! In-memory intermediate representation: the data model a parsed WeaveFFI IDL
2//! document becomes.
3//!
4//! Backends read this tree, never the raw IDL text. [`Api`] is the root and
5//! owns a forest of [`Module`]s, each grouping [`Function`]s,
6//! [`InterfaceDef`]s, [`StructDef`]s, [`EnumDef`]s, [`CallbackDef`]s,
7//! [`ListenerDef`]s, and an optional [`ErrorDomain`]. Types are referenced
8//! throughout by [`TypeRef`], which (de)serializes as a compact string (`i32`,
9//! `[string]`, `{string:i32}`, `Contact?`, and so on) rather than as a tagged
10//! object.
11
12use std::collections::BTreeMap;
13
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17/// The current IR schema version that the parser, validator, and every
18/// generator expect.
19///
20/// Pre-1.0 there is exactly one supported schema version: the current one.
21/// Older schema revisions (0.1.0 through 0.5.0) are not accepted and have no
22/// automated migration path: update the `version` field and adjust the
23/// document to the current schema by hand. Post-1.0, schema bumps will ship
24/// with a migration tool and [`SUPPORTED_VERSIONS`] will widen accordingly.
25///
26/// See [`docs/src/stability.md`](https://github.com/weavefoundry/weaveffi/blob/main/docs/src/stability.md)
27/// for the full schema policy and the surfaces covered by SemVer.
28pub const CURRENT_SCHEMA_VERSION: &str = "0.5.0";
29
30/// Every IR schema version the current tools accept.
31///
32/// Pre-1.0 this holds exactly one entry, [`CURRENT_SCHEMA_VERSION`]; a document
33/// declaring any other `version` is rejected. Post-1.0 it widens as migrations
34/// land, letting the parser accept a range of historical schema revisions.
35pub const SUPPORTED_VERSIONS: &[&str] = &[CURRENT_SCHEMA_VERSION];
36
37/// `skip_serializing_if` predicate for `bool` fields that default to `false`.
38/// Keeps the canonical IDL emitted by `weaveffi format`/`extract` minimal by
39/// omitting flags the user never set (e.g. `async: false`, `mutable: false`).
40#[allow(clippy::trivially_copy_pass_by_ref)]
41fn is_false(b: &bool) -> bool {
42    !*b
43}
44
45/// Top-level WeaveFFI API definition: the root of a parsed IDL document.
46///
47/// This is the value an entire `.yml`, `.json`, or `.toml` IDL file
48/// deserializes into (see [`crate::parse`]) and the single input every code
49/// generator consumes. It pairs the schema version with the module forest,
50/// optional package identity, and any per-generator overrides.
51// `Eq` is omitted because `generators` holds `toml::Value`, which contains `f64`.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
53#[schemars(description = "Top-level WeaveFFI API definition.")]
54pub struct Api {
55    /// IR schema version this document targets (for example `0.5.0`).
56    /// Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].
57    pub version: String,
58    /// Package identity used to name, version, and describe every generated
59    /// consumer package (npm, PyPI, gem, NuGet, pub.dev, SwiftPM, Gradle, Go).
60    /// When omitted, generators fall back to the IDL file stem and version
61    /// `0.1.0`, but publishable artifacts should always set this explicitly.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub package: Option<Package>,
64    /// Top-level modules that make up the API surface. Each is an independent
65    /// namespace; modules may nest further through [`Module::modules`].
66    pub modules: Vec<Module>,
67    /// Per-generator configuration keyed by backend name (for example `swift`
68    /// or `python`). The opaque [`toml::Value`] payload is interpreted by each
69    /// generator, so unrecognized keys pass through untouched. `None` when the
70    /// IDL declares no `generators:` block.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    #[schemars(with = "Option<BTreeMap<String, serde_json::Value>>")]
73    pub generators: Option<BTreeMap<String, toml::Value>>,
74}
75
76/// Package identity for the generated consumer artifacts.
77///
78/// A single `package:` block in the IDL is the source of truth for the
79/// name, version, and metadata stamped into every ecosystem manifest
80/// (`package.json`, `pyproject.toml`, `*.gemspec`, `*.csproj`, `pubspec.yaml`,
81/// `Package.swift`, `build.gradle`, `go.mod`). This is what makes the
82/// generated packages standalone and publishable rather than all sharing a
83/// placeholder identity.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85#[schemars(description = "Package identity for the generated consumer artifacts.")]
86pub struct Package {
87    /// Canonical package name (e.g. `kvstore`). Per-target name overrides in
88    /// `generators:` (such as `python.package_name`) still take precedence.
89    pub name: String,
90    /// Semantic version stamped into every manifest (e.g. `1.2.0`).
91    pub version: String,
92    /// Short summary written into each manifest's description field. Omitted
93    /// from generated manifests when absent.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub description: Option<String>,
96    /// License identifier, typically an SPDX expression such as `MIT` or
97    /// `Apache-2.0`, written into each manifest's license field.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub license: Option<String>,
100    /// Package authors, each commonly formatted as `Name <email>`, mapped to
101    /// whatever author or maintainer field the target ecosystem uses. Empty by
102    /// default.
103    #[serde(default, skip_serializing_if = "Vec::is_empty")]
104    pub authors: Vec<String>,
105    /// Project homepage URL recorded in manifests that expose one.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub homepage: Option<String>,
108    /// Source repository URL recorded in manifests that expose one.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub repository: Option<String>,
111}
112
113/// A module: a named namespace grouping related functions, types, callbacks,
114/// listeners, and an error domain.
115///
116/// Modules are the IDL's unit of organization and map onto each target
117/// language's natural grouping construct (a namespace, a submodule, a symbol
118/// prefix, and so on). They may nest through [`modules`](Self::modules) to
119/// mirror a package hierarchy.
120// `Eq` is omitted because a nested `StructField::default` holds `serde_yaml::Value` (an `f64`).
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
122#[schemars(
123    description = "A WeaveFFI module: a named group of functions, types, callbacks, listeners, and errors."
124)]
125pub struct Module {
126    /// Module name, used as a namespace segment and a symbol-prefix component
127    /// in generated code (for example `contacts`).
128    pub name: String,
129    /// Free functions this module exports across the FFI boundary.
130    #[serde(default, skip_serializing_if = "Vec::is_empty")]
131    pub functions: Vec<Function>,
132    /// Interface (object) types declared in this module: stateful resources
133    /// with constructors, methods, and static functions.
134    #[serde(default, skip_serializing_if = "Vec::is_empty")]
135    pub interfaces: Vec<InterfaceDef>,
136    /// Record (struct) types declared in this module.
137    #[serde(default, skip_serializing_if = "Vec::is_empty")]
138    pub structs: Vec<StructDef>,
139    /// Enum types, C-style or algebraic, declared in this module.
140    #[serde(default, skip_serializing_if = "Vec::is_empty")]
141    pub enums: Vec<EnumDef>,
142    /// Callback signatures this module's functions and listeners can invoke.
143    #[serde(default, skip_serializing_if = "Vec::is_empty")]
144    pub callbacks: Vec<CallbackDef>,
145    /// Event listeners (subscribe and unsubscribe endpoints) this module exposes.
146    #[serde(default, skip_serializing_if = "Vec::is_empty")]
147    pub listeners: Vec<ListenerDef>,
148    /// Optional error domain: the named codes this module's fallible functions
149    /// report. `None` when the module declares no errors.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub errors: Option<ErrorDomain>,
152    /// Nested submodules, forming a tree that mirrors a package hierarchy.
153    #[serde(default, skip_serializing_if = "Vec::is_empty")]
154    pub modules: Vec<Module>,
155}
156
157/// A function exported across the FFI boundary.
158///
159/// Each function becomes a C ABI entry point plus an idiomatic wrapper in every
160/// target language. The `async` and `cancellable` flags change how the symbol
161/// is lowered (a completion callback, an extra cancel-token parameter) without
162/// altering the parameter and return shape declared here. The same shape also
163/// describes an interface's constructors, methods, and statics (see
164/// [`InterfaceDef`]).
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
166pub struct Function {
167    /// Function name, lowered to a per-language symbol (for example
168    /// `create_contact`).
169    pub name: String,
170    /// Ordered parameter list; order is preserved in every generated signature.
171    #[serde(default, skip_serializing_if = "Vec::is_empty")]
172    pub params: Vec<Param>,
173    /// Return type, or `None` for a function that returns nothing. Serialized
174    /// under the IDL key `return`.
175    #[serde(rename = "return", default, skip_serializing_if = "Option::is_none")]
176    pub returns: Option<TypeRef>,
177    /// Human-readable documentation, propagated to the generated bindings' doc
178    /// comments. `None` when undocumented.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub doc: Option<String>,
181    /// Whether the function can fail with a domain error. A throwing function
182    /// surfaces as `throws`/`raises` in the idiomatic bindings, reporting the
183    /// owning module's error domain; a non-throwing function has a plain
184    /// signature and treats any error as a producer bug (a trap, not a typed
185    /// error). Defaults to `false`.
186    #[serde(default, skip_serializing_if = "is_false")]
187    pub throws: bool,
188    /// Whether the function is asynchronous, lowering to a completion-callback
189    /// form rather than a blocking call. Serialized under the IDL key `async`.
190    #[serde(default, rename = "async", skip_serializing_if = "is_false")]
191    pub r#async: bool,
192    /// Whether an async call accepts a cancellation token so callers can request
193    /// that an in-flight operation stop early. Defaults to `false`.
194    #[serde(default, skip_serializing_if = "is_false")]
195    pub cancellable: bool,
196    /// Deprecation notice; when set, generators emit a deprecation annotation
197    /// carrying this message. `None` means the function is current.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub deprecated: Option<String>,
200    /// Version in which the function was introduced (for example `0.2.0`),
201    /// surfaced as a "since" annotation where the target language supports one.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub since: Option<String>,
204}
205
206/// An interface: an opaque, stateful object type with constructors, instance
207/// methods, and static functions.
208///
209/// An interface value lives behind the FFI boundary and crosses it as an
210/// opaque pointer; consumers see a class (or the target's closest analogue)
211/// whose methods call back into the producer. This is the primary way to model
212/// resources with identity and behavior (stores, sessions, connections), in
213/// contrast to a [`StructDef`], which models a plain data record with fields.
214///
215/// Every interface also receives an implicit destructor symbol
216/// (`{tag}_destroy`); generated wrappers release the underlying object through
217/// their language's natural disposal hook (`Drop`, `__del__`, `Disposable`,
218/// finalizers, `close()`).
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
220#[schemars(
221    description = "An interface: an opaque object type with constructors, methods, and statics."
222)]
223pub struct InterfaceDef {
224    /// Interface type name (for example `Store`).
225    pub name: String,
226    /// Human-readable documentation, propagated to the generated bindings.
227    /// `None` when undocumented.
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub doc: Option<String>,
230    /// Constructors: static functions returning a new instance. A constructor
231    /// declares no `return` (the instance is implicit) and may not be `async`.
232    #[serde(default, skip_serializing_if = "Vec::is_empty")]
233    pub constructors: Vec<Function>,
234    /// Instance methods. Each lowers with an implicit leading `self` slot
235    /// (a pointer to the interface object) before the declared parameters.
236    #[serde(default, skip_serializing_if = "Vec::is_empty")]
237    pub methods: Vec<Function>,
238    /// Static functions namespaced under the interface but taking no `self`.
239    #[serde(default, skip_serializing_if = "Vec::is_empty")]
240    pub statics: Vec<Function>,
241}
242
243/// A single parameter of a [`Function`] or [`CallbackDef`].
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
245pub struct Param {
246    /// Parameter name as it appears in generated signatures (for example `id`).
247    pub name: String,
248    /// Parameter type. Serialized under the IDL key `type`.
249    #[serde(rename = "type")]
250    pub ty: TypeRef,
251    /// Whether the callee may write back through this parameter (for example a
252    /// buffer filled in place). Defaults to `false`.
253    #[serde(default, skip_serializing_if = "is_false")]
254    pub mutable: bool,
255    /// Human-readable documentation for the parameter, propagated to the
256    /// generated bindings. `None` when undocumented.
257    #[serde(default, skip_serializing_if = "Option::is_none")]
258    pub doc: Option<String>,
259}
260
261/// A callback signature: a function shape the host implements and native code
262/// invokes.
263///
264/// Callbacks are declared at module scope rather than as a [`TypeRef`] so the C
265/// ABI can represent them uniformly as a function pointer plus a context
266/// pointer. A [`ListenerDef`] references one by name to model an event stream.
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
268pub struct CallbackDef {
269    /// Callback name, used to name the generated function-pointer type and
270    /// referenced by [`ListenerDef::event_callback`] (for example `on_message`).
271    pub name: String,
272    /// Parameters passed to the callback each time it fires.
273    pub params: Vec<Param>,
274    /// Human-readable documentation, propagated to the generated bindings.
275    /// `None` when undocumented.
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub doc: Option<String>,
278}
279
280/// An event listener: a subscribe and unsubscribe endpoint that delivers events
281/// through a [`CallbackDef`].
282///
283/// Generators expand a listener into register and unregister functions; the
284/// register call takes the named callback and returns a subscription id the
285/// caller later hands to unregister.
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
287pub struct ListenerDef {
288    /// Listener name, lowered into the generated `register_*` and
289    /// `unregister_*` function names (for example `messages`).
290    pub name: String,
291    /// Name of the [`CallbackDef`] invoked for each event. Must match a callback
292    /// declared on the same [`Module`].
293    pub event_callback: String,
294    /// Human-readable documentation, propagated to the generated bindings.
295    /// `None` when undocumented.
296    #[serde(default, skip_serializing_if = "Option::is_none")]
297    pub doc: Option<String>,
298}
299
300/// A reference to a type in the IDL.
301///
302/// Callback-style behavior is **not** expressed as a `TypeRef` variant.
303/// Instead, callbacks and listeners are declared at the module level via
304/// `Module.callbacks` (see [`CallbackDef`]) and `Module.listeners` (see
305/// [`ListenerDef`]), and asynchronous functions use `async: true`. These
306/// primitives cover every pattern the FFI boundary needs to support, and
307/// keep the type system free of function-typed values that the C ABI
308/// cannot represent uniformly.
309#[derive(Debug, Clone, PartialEq, Eq, Hash)]
310pub enum TypeRef {
311    /// Signed 8-bit integer (`i8`).
312    I8,
313    /// Signed 16-bit integer (`i16`).
314    I16,
315    /// Signed 32-bit integer (`i32`).
316    I32,
317    /// Signed 64-bit integer (`i64`).
318    I64,
319    /// Unsigned 8-bit integer (`u8`).
320    U8,
321    /// Unsigned 16-bit integer (`u16`).
322    U16,
323    /// Unsigned 32-bit integer (`u32`).
324    U32,
325    /// Unsigned 64-bit integer (`u64`).
326    U64,
327    /// 32-bit IEEE 754 floating-point number (`f32`).
328    F32,
329    /// 64-bit IEEE 754 floating-point number (`f64`).
330    F64,
331    /// Boolean (`bool`).
332    Bool,
333    /// Owned UTF-8 string (`string`).
334    StringUtf8,
335    /// Owned byte buffer (`bytes`).
336    Bytes,
337    /// Opaque, untyped resource handle (`handle`). See
338    /// [`TypedHandle`](Self::TypedHandle) for the form tagged with a referent
339    /// name.
340    Handle,
341    /// Opaque resource handle tagged with the name of what it refers to
342    /// (`handle<Name>`), giving generators a distinct type per resource kind.
343    TypedHandle(String),
344    /// A user struct *or* an algebraic (rich) enum. Both cross the C ABI as an
345    /// opaque object pointer, so a reference to either is represented the same
346    /// way here; whether the referent is a struct or a sum type is recovered
347    /// from its definition (`module.structs` vs `module.enums` /
348    /// [`EnumDef::is_rich`]) when a generator emits its *declaration*.
349    ///
350    /// The resolution pass leaves a rich-enum reference as `Struct` (it only
351    /// rewrites *C-style* enum references into [`Enum`](Self::Enum), which lower
352    /// by value, and interface references into [`Interface`](Self::Interface));
353    /// see `weaveffi_core::validate::resolve`.
354    Struct(String),
355    /// A C-style integer enum (no variant payloads). Lowers by value.
356    Enum(String),
357    /// A user interface (see [`InterfaceDef`]): an opaque object reference.
358    /// As a parameter the object is borrowed for the call; as a return the
359    /// caller receives a new owned reference it must eventually release.
360    ///
361    /// The IDL spells an interface reference as its bare (or dotted-qualified)
362    /// name, exactly like a struct; the resolution pass rewrites the parsed
363    /// [`Struct`](Self::Struct) into this variant when the name resolves to an
364    /// interface declaration.
365    Interface(String),
366    /// Borrowed string slice (`&str`): a non-owning view valid only for the
367    /// duration of a call, used to pass input without copying.
368    BorrowedStr,
369    /// Borrowed byte slice (`&[u8]`): a non-owning view valid only for the
370    /// duration of a call.
371    BorrowedBytes,
372    /// Optional value (`T?`): either the inner type or nothing.
373    Optional(Box<TypeRef>),
374    /// Homogeneous list (`[T]`) of the inner element type.
375    List(Box<TypeRef>),
376    /// Map (`{K:V}`) from a key type to a value type. Crosses the C ABI as
377    /// parallel key and value arrays.
378    Map(Box<TypeRef>, Box<TypeRef>),
379    /// Lazy sequence (`iter<T>`) of the inner type, lowered to a next/destroy
380    /// iterator object rather than a materialized collection.
381    Iterator(Box<TypeRef>),
382}
383
384/// Parse the IDL's compact type syntax into a [`TypeRef`].
385///
386/// Handles primitive names (`i32`, `string`, `bytes`, `handle`, and so on),
387/// borrowed forms (`&str`, `&[u8]`), typed handles (`handle<Name>`), iterators
388/// (`iter<T>`), lists (`[T]`), maps (`{K:V}`), and the optional suffix (`T?`).
389/// Any other bare identifier is taken to be a user-defined struct, enum, or
390/// interface name and returned as [`TypeRef::Struct`]; the
391/// struct-versus-enum-versus-interface distinction is resolved later against
392/// the module's declarations.
393///
394/// # Errors
395///
396/// Returns an error message when `s` is empty or only whitespace, or when a map
397/// type (`{K:V}`) is missing its `:` separator. The same errors propagate up
398/// from a malformed inner type of a list, map, optional, or iterator.
399pub fn parse_type_ref(s: &str) -> Result<TypeRef, String> {
400    let s = s.trim();
401    if s.is_empty() {
402        return Err("empty type reference".to_string());
403    }
404    if s.starts_with('[') && s.ends_with(']') {
405        let inner = &s[1..s.len() - 1];
406        return parse_type_ref(inner).map(|t| TypeRef::List(Box::new(t)));
407    }
408    if s.starts_with('{') && s.ends_with('}') {
409        let inner = &s[1..s.len() - 1];
410        let colon = inner
411            .find(':')
412            .ok_or_else(|| "map type missing ':' separator".to_string())?;
413        let key = parse_type_ref(&inner[..colon])?;
414        let val = parse_type_ref(&inner[colon + 1..])?;
415        return Ok(TypeRef::Map(Box::new(key), Box::new(val)));
416    }
417    if let Some(inner) = s.strip_suffix('?') {
418        return parse_type_ref(inner).map(|t| TypeRef::Optional(Box::new(t)));
419    }
420    if let Some(inner) = s
421        .strip_prefix("handle<")
422        .and_then(|rest| rest.strip_suffix('>'))
423    {
424        return Ok(TypeRef::TypedHandle(inner.into()));
425    }
426    if let Some(inner) = s
427        .strip_prefix("iter<")
428        .and_then(|rest| rest.strip_suffix('>'))
429    {
430        return parse_type_ref(inner).map(|t| TypeRef::Iterator(Box::new(t)));
431    }
432    match s {
433        "i8" => Ok(TypeRef::I8),
434        "i16" => Ok(TypeRef::I16),
435        "i32" => Ok(TypeRef::I32),
436        "i64" => Ok(TypeRef::I64),
437        "u8" => Ok(TypeRef::U8),
438        "u16" => Ok(TypeRef::U16),
439        "u32" => Ok(TypeRef::U32),
440        "u64" => Ok(TypeRef::U64),
441        "f32" => Ok(TypeRef::F32),
442        "f64" => Ok(TypeRef::F64),
443        "bool" => Ok(TypeRef::Bool),
444        "string" => Ok(TypeRef::StringUtf8),
445        "bytes" => Ok(TypeRef::Bytes),
446        "handle" => Ok(TypeRef::Handle),
447        "&str" => Ok(TypeRef::BorrowedStr),
448        "&[u8]" => Ok(TypeRef::BorrowedBytes),
449        name => Ok(TypeRef::Struct(name.to_string())),
450    }
451}
452
453fn type_ref_to_string(ty: &TypeRef) -> String {
454    match ty {
455        TypeRef::I8 => "i8".to_string(),
456        TypeRef::I16 => "i16".to_string(),
457        TypeRef::I32 => "i32".to_string(),
458        TypeRef::I64 => "i64".to_string(),
459        TypeRef::U8 => "u8".to_string(),
460        TypeRef::U16 => "u16".to_string(),
461        TypeRef::U32 => "u32".to_string(),
462        TypeRef::U64 => "u64".to_string(),
463        TypeRef::F32 => "f32".to_string(),
464        TypeRef::F64 => "f64".to_string(),
465        TypeRef::Bool => "bool".to_string(),
466        TypeRef::StringUtf8 => "string".to_string(),
467        TypeRef::Bytes => "bytes".to_string(),
468        TypeRef::BorrowedStr => "&str".to_string(),
469        TypeRef::BorrowedBytes => "&[u8]".to_string(),
470        TypeRef::Handle => "handle".to_string(),
471        TypeRef::TypedHandle(name) => format!("handle<{name}>"),
472        TypeRef::Struct(name) | TypeRef::Enum(name) | TypeRef::Interface(name) => name.clone(),
473        TypeRef::Optional(inner) => format!("{}?", type_ref_to_string(inner)),
474        TypeRef::List(inner) => format!("[{}]", type_ref_to_string(inner)),
475        TypeRef::Map(k, v) => format!("{{{}:{}}}", type_ref_to_string(k), type_ref_to_string(v)),
476        TypeRef::Iterator(inner) => format!("iter<{}>", type_ref_to_string(inner)),
477    }
478}
479
480impl Serialize for TypeRef {
481    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
482    where
483        S: serde::Serializer,
484    {
485        serializer.serialize_str(&type_ref_to_string(self))
486    }
487}
488
489impl<'de> Deserialize<'de> for TypeRef {
490    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
491    where
492        D: serde::Deserializer<'de>,
493    {
494        let s = String::deserialize(deserializer)?;
495        parse_type_ref(&s).map_err(serde::de::Error::custom)
496    }
497}
498
499/// Manual `JsonSchema` impl because `TypeRef` (de)serializes as a string with
500/// custom syntax: primitive names (`i32`, `string`, ...), `&str`, `&[u8]`,
501/// `handle<{name}>`, `iter<{T}>`, `[{T}]`, `{ {K}: {V} }`, `{name}?`, or any
502/// user-defined struct/enum name.
503impl JsonSchema for TypeRef {
504    fn schema_name() -> String {
505        "TypeRef".to_string()
506    }
507
508    fn schema_id() -> std::borrow::Cow<'static, str> {
509        std::borrow::Cow::Borrowed(concat!(module_path!(), "::TypeRef"))
510    }
511
512    fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
513        let mut schema = schemars::schema::SchemaObject {
514            instance_type: Some(schemars::schema::InstanceType::String.into()),
515            ..Default::default()
516        };
517        let meta = schema.metadata();
518        meta.title = Some("TypeRef".to_string());
519        meta.description = Some(
520            "Reference to a type. Encoded as a string with custom syntax: \
521             primitives (`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, \
522             `f32`, `f64`, `bool`, `string`, `bytes`, `handle`), \
523             borrowed types (`&str`, `&[u8]`), typed handles (`handle<{name}>`), \
524             iterators (`iter<{T}>`), lists (`[{T}]`), maps (`{{K:V}}`), \
525             optionals (`{T}?`), or any user-defined struct/enum/interface name."
526                .to_string(),
527        );
528        schema.into()
529    }
530}
531
532/// An enum type. C-style when every variant is a bare discriminant; an
533/// algebraic sum type when any variant declares fields (see
534/// [`is_rich`](Self::is_rich)).
535///
536/// A C-style enum lowers across the C ABI by value as an integer, while an
537/// algebraic enum lowers as an opaque object with a tag getter plus per-variant
538/// constructors and field getters.
539// `Eq` is omitted because a variant field's `default` may hold `serde_yaml::Value` (an `f64`), matching `StructDef`.
540#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
541#[schemars(
542    description = "An enum type. C-style when every variant is a bare discriminant; an algebraic sum type when any variant declares fields."
543)]
544pub struct EnumDef {
545    /// Enum type name (for example `Color`).
546    pub name: String,
547    /// Human-readable documentation, propagated to the generated bindings.
548    /// `None` when undocumented.
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    pub doc: Option<String>,
551    /// The variants in declaration order. Whether any of them carries fields
552    /// decides if this is a C-style or an algebraic enum.
553    pub variants: Vec<EnumVariant>,
554}
555
556impl EnumDef {
557    /// `true` when this is an *algebraic* enum (a sum type): at least one
558    /// variant carries associated data. Such enums lower across the C ABI as
559    /// opaque objects (a tag getter plus per-variant constructors and field
560    /// getters); a C-style enum (every variant a bare discriminant) lowers by
561    /// value as an integer.
562    pub fn is_rich(&self) -> bool {
563        self.variants.iter().any(|v| !v.fields.is_empty())
564    }
565}
566
567/// A single variant of an [`EnumDef`].
568// `Eq` is omitted because a variant field's `default` may hold `serde_yaml::Value` (an `f64`).
569#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
570pub struct EnumVariant {
571    /// Variant name (for example `Red`).
572    pub name: String,
573    /// Integer discriminant. Doubles as the C-style enum value and as the
574    /// runtime tag that distinguishes the variants of an algebraic enum.
575    pub value: i32,
576    /// Human-readable documentation, propagated to the generated bindings.
577    /// `None` when undocumented.
578    #[serde(default, skip_serializing_if = "Option::is_none")]
579    pub doc: Option<String>,
580    /// Associated data carried by this variant. Empty for a unit variant or a
581    /// C-style enum; non-empty makes the owning enum a sum type (see
582    /// [`EnumDef::is_rich`]). Variant fields reuse [`StructField`] but ignore
583    /// the `default` slot (a sum-type payload has no defaultable fields).
584    #[serde(default, skip_serializing_if = "Vec::is_empty")]
585    pub fields: Vec<StructField>,
586}
587
588/// A struct (record) type with named fields.
589// `Eq` is omitted because `StructField::default` holds `serde_yaml::Value` (an `f64`).
590#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
591#[schemars(description = "A struct (record) type with named fields.")]
592pub struct StructDef {
593    /// Struct type name (for example `Contact`).
594    pub name: String,
595    /// Human-readable documentation, propagated to the generated bindings.
596    /// `None` when undocumented.
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    pub doc: Option<String>,
599    /// The fields in declaration order; order is preserved in the generated
600    /// type and its constructors.
601    pub fields: Vec<StructField>,
602    /// Whether to also emit a builder API for constructing the struct field by
603    /// field, alongside the all-fields constructor. Defaults to `false`.
604    #[serde(default, skip_serializing_if = "is_false")]
605    pub builder: bool,
606}
607
608/// A named field of a [`StructDef`], or the payload of an algebraic
609/// [`EnumVariant`].
610#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
611pub struct StructField {
612    /// Field name (for example `email`).
613    pub name: String,
614    /// Field type. Serialized under the IDL key `type`.
615    #[serde(rename = "type")]
616    pub ty: TypeRef,
617    /// Human-readable documentation, propagated to the generated bindings.
618    /// `None` when undocumented.
619    #[serde(default, skip_serializing_if = "Option::is_none")]
620    pub doc: Option<String>,
621    /// Default value used when the field is omitted, kept as a raw YAML value so
622    /// any literal the field's type accepts can be expressed. Ignored for
623    /// algebraic [`EnumVariant`] payloads, which aren't defaultable. `None` when
624    /// the field has no default.
625    #[serde(default, skip_serializing_if = "Option::is_none")]
626    #[schemars(with = "Option<serde_json::Value>")]
627    pub default: Option<serde_yaml::Value>,
628}
629
630/// A module's error domain: the named set of error codes its fallible functions
631/// can report.
632#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
633pub struct ErrorDomain {
634    /// Error domain name, used to name the generated error type (for example
635    /// `ContactErrors`).
636    pub name: String,
637    /// The error codes that belong to this domain.
638    pub codes: Vec<ErrorCode>,
639}
640
641/// A single named error within an [`ErrorDomain`].
642#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
643pub struct ErrorCode {
644    /// Error code name, lowered to a variant or constant on the generated error
645    /// type (for example `not_found`).
646    pub name: String,
647    /// Stable numeric value carried across the C ABI to identify this error.
648    pub code: i32,
649    /// Default human-readable message describing the error.
650    pub message: String,
651    /// Human-readable documentation, propagated to the generated bindings.
652    /// `None` when undocumented.
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub doc: Option<String>,
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660
661    #[test]
662    fn struct_def_round_trip_yaml() {
663        let yaml = r#"
664version: "0.5.0"
665modules:
666  - name: geometry
667    functions: []
668    structs:
669      - name: Point
670        doc: "A 2D point"
671        fields:
672          - name: x
673            type: f64
674          - name: "y"
675            type: f64
676            doc: "Y coordinate"
677"#;
678        let api: Api = serde_yaml::from_str(yaml).unwrap();
679        let m = &api.modules[0];
680        assert_eq!(m.structs.len(), 1);
681        let s = &m.structs[0];
682        assert_eq!(s.name, "Point");
683        assert_eq!(s.doc.as_deref(), Some("A 2D point"));
684        assert_eq!(s.fields.len(), 2);
685        assert_eq!(s.fields[0].name, "x");
686        assert_eq!(s.fields[0].ty, TypeRef::F64);
687        assert_eq!(s.fields[0].doc, None);
688        assert_eq!(s.fields[1].name, "y");
689        assert_eq!(s.fields[1].doc.as_deref(), Some("Y coordinate"));
690    }
691
692    #[test]
693    fn struct_def_round_trip_json() {
694        let json = r#"{
695            "version": "0.5.0",
696            "modules": [{
697                "name": "geo",
698                "functions": [],
699                "structs": [{
700                    "name": "Rect",
701                    "fields": [
702                        {"name": "width", "type": "i32"},
703                        {"name": "height", "type": "i32"}
704                    ]
705                }]
706            }]
707        }"#;
708        let api: Api = serde_json::from_str(json).unwrap();
709        let s = &api.modules[0].structs[0];
710        assert_eq!(s.name, "Rect");
711        assert_eq!(s.doc, None);
712        assert_eq!(s.fields[0].ty, TypeRef::I32);
713    }
714
715    #[test]
716    fn structs_default_to_empty() {
717        let yaml = r#"
718version: "0.5.0"
719modules:
720  - name: math
721    functions: []
722"#;
723        let api: Api = serde_yaml::from_str(yaml).unwrap();
724        assert!(api.modules[0].structs.is_empty());
725    }
726
727    #[test]
728    fn package_block_round_trips_yaml() {
729        let yaml = r#"
730version: "0.5.0"
731package:
732  name: kvstore
733  version: 1.2.0
734  description: "An embedded key/value store"
735  license: MIT
736  authors:
737    - "Ada Lovelace <ada@example.com>"
738  homepage: "https://example.com/kvstore"
739  repository: "https://github.com/example/kvstore"
740modules:
741  - name: kv
742    functions: []
743"#;
744        let api: Api = serde_yaml::from_str(yaml).unwrap();
745        let pkg = api.package.as_ref().expect("package should parse");
746        assert_eq!(pkg.name, "kvstore");
747        assert_eq!(pkg.version, "1.2.0");
748        assert_eq!(
749            pkg.description.as_deref(),
750            Some("An embedded key/value store")
751        );
752        assert_eq!(pkg.license.as_deref(), Some("MIT"));
753        assert_eq!(pkg.authors, vec!["Ada Lovelace <ada@example.com>"]);
754        assert_eq!(pkg.homepage.as_deref(), Some("https://example.com/kvstore"));
755        assert_eq!(
756            pkg.repository.as_deref(),
757            Some("https://github.com/example/kvstore")
758        );
759
760        // Re-serialize and confirm the block survives the round trip.
761        let out = serde_yaml::to_string(&api).unwrap();
762        assert!(out.contains("name: kvstore"));
763        assert!(out.contains("version: 1.2.0"));
764    }
765
766    #[test]
767    fn package_is_optional() {
768        let yaml = r#"
769version: "0.5.0"
770modules:
771  - name: math
772    functions: []
773"#;
774        let api: Api = serde_yaml::from_str(yaml).unwrap();
775        assert!(api.package.is_none());
776        // Absent package must not appear in the canonical serialization.
777        let out = serde_yaml::to_string(&api).unwrap();
778        assert!(!out.contains("package:"));
779    }
780
781    #[test]
782    fn package_minimal_requires_name_and_version() {
783        let yaml = r#"
784version: "0.5.0"
785package:
786  name: tiny
787  version: 0.0.1
788modules: []
789"#;
790        let api: Api = serde_yaml::from_str(yaml).unwrap();
791        let pkg = api.package.as_ref().unwrap();
792        assert_eq!(pkg.name, "tiny");
793        assert_eq!(pkg.version, "0.0.1");
794        assert!(pkg.description.is_none());
795        assert!(pkg.authors.is_empty());
796    }
797
798    #[test]
799    fn typeref_struct_variant_serializes() {
800        let ty = TypeRef::Struct("Point".to_string());
801        let json = serde_json::to_string(&ty).unwrap();
802        assert_eq!(json, r#""Point""#);
803        let back: TypeRef = serde_json::from_str(&json).unwrap();
804        assert_eq!(back, ty);
805    }
806
807    #[test]
808    fn struct_field_with_struct_type() {
809        let field = StructField {
810            name: "origin".to_string(),
811            ty: TypeRef::Struct("Point".to_string()),
812            doc: None,
813            default: None,
814        };
815        let json = serde_json::to_string(&field).unwrap();
816        let back: StructField = serde_json::from_str(&json).unwrap();
817        assert_eq!(back, field);
818    }
819
820    #[test]
821    fn typeref_is_not_copy() {
822        let a = TypeRef::Struct("Foo".to_string());
823        let b = a.clone();
824        assert_eq!(a, b);
825    }
826
827    #[test]
828    fn enum_def_round_trip_yaml() {
829        let yaml = r#"
830version: "0.5.0"
831modules:
832  - name: graphics
833    functions: []
834    enums:
835      - name: Color
836        doc: "Primary colors"
837        variants:
838          - name: Red
839            value: 0
840          - name: Green
841            value: 1
842            doc: "The color green"
843          - name: Blue
844            value: 2
845"#;
846        let api: Api = serde_yaml::from_str(yaml).unwrap();
847        let m = &api.modules[0];
848        assert_eq!(m.enums.len(), 1);
849        let e = &m.enums[0];
850        assert_eq!(e.name, "Color");
851        assert_eq!(e.doc.as_deref(), Some("Primary colors"));
852        assert_eq!(e.variants.len(), 3);
853        assert_eq!(e.variants[0].name, "Red");
854        assert_eq!(e.variants[0].value, 0);
855        assert_eq!(e.variants[0].doc, None);
856        assert_eq!(e.variants[1].name, "Green");
857        assert_eq!(e.variants[1].value, 1);
858        assert_eq!(e.variants[1].doc.as_deref(), Some("The color green"));
859        assert_eq!(e.variants[2].name, "Blue");
860        assert_eq!(e.variants[2].value, 2);
861    }
862
863    #[test]
864    fn enum_def_round_trip_json() {
865        let json = r#"{
866            "version": "0.5.0",
867            "modules": [{
868                "name": "status",
869                "functions": [],
870                "enums": [{
871                    "name": "Status",
872                    "variants": [
873                        {"name": "Ok", "value": 0},
874                        {"name": "Error", "value": 1}
875                    ]
876                }]
877            }]
878        }"#;
879        let api: Api = serde_json::from_str(json).unwrap();
880        let e = &api.modules[0].enums[0];
881        assert_eq!(e.name, "Status");
882        assert_eq!(e.doc, None);
883        assert_eq!(e.variants.len(), 2);
884        assert_eq!(e.variants[1].value, 1);
885    }
886
887    #[test]
888    fn enums_default_to_empty() {
889        let yaml = r#"
890version: "0.5.0"
891modules:
892  - name: math
893    functions: []
894"#;
895        let api: Api = serde_yaml::from_str(yaml).unwrap();
896        assert!(api.modules[0].enums.is_empty());
897    }
898
899    #[test]
900    fn typeref_enum_variant_serializes_as_name() {
901        let ty = TypeRef::Enum("Color".to_string());
902        let json = serde_json::to_string(&ty).unwrap();
903        assert_eq!(json, r#""Color""#);
904    }
905
906    #[test]
907    fn enum_def_clone_and_eq() {
908        let e = EnumDef {
909            name: "Direction".to_string(),
910            doc: Some("Cardinal directions".to_string()),
911            variants: vec![
912                EnumVariant {
913                    name: "North".to_string(),
914                    value: 0,
915                    doc: None,
916                    fields: vec![],
917                },
918                EnumVariant {
919                    name: "South".to_string(),
920                    value: 1,
921                    doc: None,
922                    fields: vec![],
923                },
924            ],
925        };
926        assert_eq!(e, e.clone());
927        assert!(!e.is_rich());
928    }
929
930    #[test]
931    fn enum_def_is_rich_when_a_variant_has_fields() {
932        let e = EnumDef {
933            name: "Shape".to_string(),
934            doc: None,
935            variants: vec![
936                EnumVariant {
937                    name: "Circle".to_string(),
938                    value: 0,
939                    doc: None,
940                    fields: vec![StructField {
941                        name: "radius".to_string(),
942                        ty: TypeRef::F64,
943                        doc: None,
944                        default: None,
945                    }],
946                },
947                EnumVariant {
948                    name: "Empty".to_string(),
949                    value: 1,
950                    doc: None,
951                    fields: vec![],
952                },
953            ],
954        };
955        assert!(e.is_rich());
956    }
957
958    #[test]
959    fn struct_def_clone_and_eq() {
960        let s = StructDef {
961            name: "Color".to_string(),
962            doc: Some("RGB color".to_string()),
963            fields: vec![
964                StructField {
965                    name: "r".to_string(),
966                    ty: TypeRef::U32,
967                    doc: None,
968                    default: None,
969                },
970                StructField {
971                    name: "g".to_string(),
972                    ty: TypeRef::U32,
973                    doc: None,
974                    default: None,
975                },
976                StructField {
977                    name: "b".to_string(),
978                    ty: TypeRef::U32,
979                    doc: None,
980                    default: None,
981                },
982            ],
983            builder: false,
984        };
985        assert_eq!(s, s.clone());
986    }
987
988    #[test]
989    fn parse_type_ref_primitives() {
990        assert_eq!(parse_type_ref("i32"), Ok(TypeRef::I32));
991        assert_eq!(parse_type_ref("u32"), Ok(TypeRef::U32));
992        assert_eq!(parse_type_ref("i64"), Ok(TypeRef::I64));
993        assert_eq!(parse_type_ref("f64"), Ok(TypeRef::F64));
994        assert_eq!(parse_type_ref("bool"), Ok(TypeRef::Bool));
995        assert_eq!(parse_type_ref("string"), Ok(TypeRef::StringUtf8));
996        assert_eq!(parse_type_ref("bytes"), Ok(TypeRef::Bytes));
997        assert_eq!(parse_type_ref("handle"), Ok(TypeRef::Handle));
998    }
999
1000    #[test]
1001    fn parse_type_ref_struct() {
1002        assert_eq!(
1003            parse_type_ref("Contact"),
1004            Ok(TypeRef::Struct("Contact".into()))
1005        );
1006        assert_eq!(
1007            parse_type_ref("MyWidget"),
1008            Ok(TypeRef::Struct("MyWidget".into()))
1009        );
1010    }
1011
1012    #[test]
1013    fn parse_type_ref_optional() {
1014        assert_eq!(
1015            parse_type_ref("string?"),
1016            Ok(TypeRef::Optional(Box::new(TypeRef::StringUtf8)))
1017        );
1018        assert_eq!(
1019            parse_type_ref("i32?"),
1020            Ok(TypeRef::Optional(Box::new(TypeRef::I32)))
1021        );
1022        assert_eq!(
1023            parse_type_ref("Contact?"),
1024            Ok(TypeRef::Optional(Box::new(TypeRef::Struct(
1025                "Contact".into()
1026            ))))
1027        );
1028    }
1029
1030    #[test]
1031    fn parse_type_ref_list() {
1032        assert_eq!(
1033            parse_type_ref("[i32]"),
1034            Ok(TypeRef::List(Box::new(TypeRef::I32)))
1035        );
1036        assert_eq!(
1037            parse_type_ref("[string]"),
1038            Ok(TypeRef::List(Box::new(TypeRef::StringUtf8)))
1039        );
1040        assert_eq!(
1041            parse_type_ref("[Contact]"),
1042            Ok(TypeRef::List(Box::new(TypeRef::Struct("Contact".into()))))
1043        );
1044    }
1045
1046    #[test]
1047    fn parse_type_ref_nested() {
1048        assert_eq!(
1049            parse_type_ref("[i32?]"),
1050            Ok(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
1051                TypeRef::I32
1052            )))))
1053        );
1054        assert_eq!(
1055            parse_type_ref("[Contact]?"),
1056            Ok(TypeRef::Optional(Box::new(TypeRef::List(Box::new(
1057                TypeRef::Struct("Contact".into())
1058            )))))
1059        );
1060    }
1061
1062    #[test]
1063    fn parse_type_ref_empty_is_error() {
1064        assert!(parse_type_ref("").is_err());
1065        assert!(parse_type_ref("  ").is_err());
1066    }
1067
1068    #[test]
1069    fn typeref_primitive_round_trips() {
1070        for ty in [
1071            TypeRef::I32,
1072            TypeRef::U32,
1073            TypeRef::I64,
1074            TypeRef::F64,
1075            TypeRef::Bool,
1076            TypeRef::StringUtf8,
1077            TypeRef::Bytes,
1078            TypeRef::Handle,
1079        ] {
1080            let json = serde_json::to_string(&ty).unwrap();
1081            let back: TypeRef = serde_json::from_str(&json).unwrap();
1082            assert_eq!(back, ty);
1083        }
1084    }
1085
1086    #[test]
1087    fn typeref_optional_round_trip() {
1088        let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
1089        let json = serde_json::to_string(&ty).unwrap();
1090        assert_eq!(json, r#""string?""#);
1091        let back: TypeRef = serde_json::from_str(&json).unwrap();
1092        assert_eq!(back, ty);
1093    }
1094
1095    #[test]
1096    fn typeref_list_round_trip() {
1097        let ty = TypeRef::List(Box::new(TypeRef::I32));
1098        let json = serde_json::to_string(&ty).unwrap();
1099        assert_eq!(json, r#""[i32]""#);
1100        let back: TypeRef = serde_json::from_str(&json).unwrap();
1101        assert_eq!(back, ty);
1102    }
1103
1104    #[test]
1105    fn typeref_optional_struct_round_trip() {
1106        let ty = TypeRef::Optional(Box::new(TypeRef::Struct("Contact".into())));
1107        let json = serde_json::to_string(&ty).unwrap();
1108        assert_eq!(json, r#""Contact?""#);
1109        let back: TypeRef = serde_json::from_str(&json).unwrap();
1110        assert_eq!(back, ty);
1111    }
1112
1113    #[test]
1114    fn typeref_list_struct_round_trip() {
1115        let ty = TypeRef::List(Box::new(TypeRef::Struct("Contact".into())));
1116        let json = serde_json::to_string(&ty).unwrap();
1117        assert_eq!(json, r#""[Contact]""#);
1118        let back: TypeRef = serde_json::from_str(&json).unwrap();
1119        assert_eq!(back, ty);
1120    }
1121
1122    #[test]
1123    fn typeref_optional_yaml_deser() {
1124        let yaml = r#"
1125version: "0.5.0"
1126modules:
1127  - name: contacts
1128    functions:
1129      - name: find
1130        params:
1131          - name: id
1132            type: i32
1133        return: "Contact?"
1134"#;
1135        let api: Api = serde_yaml::from_str(yaml).unwrap();
1136        let f = &api.modules[0].functions[0];
1137        assert_eq!(
1138            f.returns,
1139            Some(TypeRef::Optional(Box::new(TypeRef::Struct(
1140                "Contact".into()
1141            ))))
1142        );
1143    }
1144
1145    #[test]
1146    fn typeref_list_yaml_deser() {
1147        let yaml = r#"
1148version: "0.5.0"
1149modules:
1150  - name: contacts
1151    functions:
1152      - name: list_all
1153        params: []
1154        return: "[Contact]"
1155"#;
1156        let api: Api = serde_yaml::from_str(yaml).unwrap();
1157        let f = &api.modules[0].functions[0];
1158        assert_eq!(
1159            f.returns,
1160            Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into()))))
1161        );
1162    }
1163
1164    #[test]
1165    fn typeref_hash_works_with_box_variants() {
1166        use std::collections::HashSet;
1167        let mut set = HashSet::new();
1168        set.insert(TypeRef::I32);
1169        set.insert(TypeRef::Optional(Box::new(TypeRef::I32)));
1170        set.insert(TypeRef::List(Box::new(TypeRef::I32)));
1171        set.insert(TypeRef::Optional(Box::new(TypeRef::Struct("Foo".into()))));
1172        set.insert(TypeRef::Map(
1173            Box::new(TypeRef::StringUtf8),
1174            Box::new(TypeRef::I32),
1175        ));
1176        assert_eq!(set.len(), 5);
1177    }
1178
1179    #[test]
1180    fn parse_type_ref_map_primitives() {
1181        assert_eq!(
1182            parse_type_ref("{string:i32}"),
1183            Ok(TypeRef::Map(
1184                Box::new(TypeRef::StringUtf8),
1185                Box::new(TypeRef::I32)
1186            ))
1187        );
1188    }
1189
1190    #[test]
1191    fn parse_type_ref_map_struct_value() {
1192        assert_eq!(
1193            parse_type_ref("{string:Contact}"),
1194            Ok(TypeRef::Map(
1195                Box::new(TypeRef::StringUtf8),
1196                Box::new(TypeRef::Struct("Contact".into()))
1197            ))
1198        );
1199    }
1200
1201    #[test]
1202    fn parse_type_ref_map_nested_value() {
1203        assert_eq!(
1204            parse_type_ref("{string:[i32]}"),
1205            Ok(TypeRef::Map(
1206                Box::new(TypeRef::StringUtf8),
1207                Box::new(TypeRef::List(Box::new(TypeRef::I32)))
1208            ))
1209        );
1210    }
1211
1212    #[test]
1213    fn parse_type_ref_map_missing_colon() {
1214        assert!(parse_type_ref("{string}").is_err());
1215    }
1216
1217    #[test]
1218    fn typeref_map_round_trip() {
1219        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
1220        let json = serde_json::to_string(&ty).unwrap();
1221        assert_eq!(json, r#""{string:i32}""#);
1222        let back: TypeRef = serde_json::from_str(&json).unwrap();
1223        assert_eq!(back, ty);
1224    }
1225
1226    #[test]
1227    fn typeref_map_struct_round_trip() {
1228        let ty = TypeRef::Map(
1229            Box::new(TypeRef::StringUtf8),
1230            Box::new(TypeRef::Struct("Contact".into())),
1231        );
1232        let json = serde_json::to_string(&ty).unwrap();
1233        assert_eq!(json, r#""{string:Contact}""#);
1234        let back: TypeRef = serde_json::from_str(&json).unwrap();
1235        assert_eq!(back, ty);
1236    }
1237
1238    #[test]
1239    fn typeref_map_yaml_deser() {
1240        let yaml = r#"
1241version: "0.5.0"
1242modules:
1243  - name: contacts
1244    functions:
1245      - name: get_metadata
1246        params: []
1247        return: "{string:i32}"
1248"#;
1249        let api: Api = serde_yaml::from_str(yaml).unwrap();
1250        let f = &api.modules[0].functions[0];
1251        assert_eq!(
1252            f.returns,
1253            Some(TypeRef::Map(
1254                Box::new(TypeRef::StringUtf8),
1255                Box::new(TypeRef::I32)
1256            ))
1257        );
1258    }
1259
1260    #[test]
1261    fn typeref_optional_map_round_trip() {
1262        let ty = TypeRef::Optional(Box::new(TypeRef::Map(
1263            Box::new(TypeRef::StringUtf8),
1264            Box::new(TypeRef::I32),
1265        )));
1266        let json = serde_json::to_string(&ty).unwrap();
1267        assert_eq!(json, r#""{string:i32}?""#);
1268        let back: TypeRef = serde_json::from_str(&json).unwrap();
1269        assert_eq!(back, ty);
1270    }
1271
1272    #[test]
1273    fn parse_map_string_to_i32() {
1274        assert_eq!(
1275            parse_type_ref("{string:i32}"),
1276            Ok(TypeRef::Map(
1277                Box::new(TypeRef::StringUtf8),
1278                Box::new(TypeRef::I32),
1279            ))
1280        );
1281    }
1282
1283    #[test]
1284    fn parse_map_string_to_struct() {
1285        assert_eq!(
1286            parse_type_ref("{string:Contact}"),
1287            Ok(TypeRef::Map(
1288                Box::new(TypeRef::StringUtf8),
1289                Box::new(TypeRef::Struct("Contact".into())),
1290            ))
1291        );
1292    }
1293
1294    #[test]
1295    fn parse_map_roundtrip() {
1296        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
1297        let json = serde_json::to_string(&ty).unwrap();
1298        let back: TypeRef = serde_json::from_str(&json).unwrap();
1299        assert_eq!(back, ty);
1300    }
1301
1302    #[test]
1303    fn parse_optional_map() {
1304        assert_eq!(
1305            parse_type_ref("{string:i32}?"),
1306            Ok(TypeRef::Optional(Box::new(TypeRef::Map(
1307                Box::new(TypeRef::StringUtf8),
1308                Box::new(TypeRef::I32),
1309            ))))
1310        );
1311    }
1312
1313    #[test]
1314    fn parse_map_of_lists() {
1315        assert_eq!(
1316            parse_type_ref("{string:[i32]}"),
1317            Ok(TypeRef::Map(
1318                Box::new(TypeRef::StringUtf8),
1319                Box::new(TypeRef::List(Box::new(TypeRef::I32))),
1320            ))
1321        );
1322    }
1323
1324    #[test]
1325    fn parse_type_ref_iterator() {
1326        assert_eq!(
1327            parse_type_ref("iter<i32>"),
1328            Ok(TypeRef::Iterator(Box::new(TypeRef::I32)))
1329        );
1330        assert_eq!(
1331            parse_type_ref("iter<string>"),
1332            Ok(TypeRef::Iterator(Box::new(TypeRef::StringUtf8)))
1333        );
1334        assert_eq!(
1335            parse_type_ref("iter<Contact>"),
1336            Ok(TypeRef::Iterator(Box::new(TypeRef::Struct(
1337                "Contact".into()
1338            ))))
1339        );
1340    }
1341
1342    #[test]
1343    fn typeref_iterator_round_trip() {
1344        let ty = TypeRef::Iterator(Box::new(TypeRef::I32));
1345        let json = serde_json::to_string(&ty).unwrap();
1346        assert_eq!(json, r#""iter<i32>""#);
1347        let back: TypeRef = serde_json::from_str(&json).unwrap();
1348        assert_eq!(back, ty);
1349    }
1350
1351    #[test]
1352    fn typeref_iterator_struct_round_trip() {
1353        let ty = TypeRef::Iterator(Box::new(TypeRef::Struct("Contact".into())));
1354        let json = serde_json::to_string(&ty).unwrap();
1355        assert_eq!(json, r#""iter<Contact>""#);
1356        let back: TypeRef = serde_json::from_str(&json).unwrap();
1357        assert_eq!(back, ty);
1358    }
1359
1360    #[test]
1361    fn parse_type_ref_borrowed() {
1362        assert_eq!(parse_type_ref("&str"), Ok(TypeRef::BorrowedStr));
1363        assert_eq!(parse_type_ref("&[u8]"), Ok(TypeRef::BorrowedBytes));
1364    }
1365
1366    #[test]
1367    fn typeref_borrowed_round_trip() {
1368        for ty in [TypeRef::BorrowedStr, TypeRef::BorrowedBytes] {
1369            let json = serde_json::to_string(&ty).unwrap();
1370            let back: TypeRef = serde_json::from_str(&json).unwrap();
1371            assert_eq!(back, ty);
1372        }
1373    }
1374
1375    #[test]
1376    fn typeref_borrowed_str_serializes_as_ampersand_str() {
1377        let json = serde_json::to_string(&TypeRef::BorrowedStr).unwrap();
1378        assert_eq!(json, r#""&str""#);
1379    }
1380
1381    #[test]
1382    fn typeref_borrowed_bytes_serializes_as_ampersand_u8() {
1383        let json = serde_json::to_string(&TypeRef::BorrowedBytes).unwrap();
1384        assert_eq!(json, r#""&[u8]""#);
1385    }
1386
1387    #[test]
1388    fn typeref_borrowed_yaml_deser() {
1389        let yaml = r#"
1390version: "0.5.0"
1391modules:
1392  - name: io
1393    functions:
1394      - name: write
1395        params:
1396          - name: data
1397            type: "&str"
1398          - name: raw
1399            type: "&[u8]"
1400"#;
1401        let api: Api = serde_yaml::from_str(yaml).unwrap();
1402        let f = &api.modules[0].functions[0];
1403        assert_eq!(f.params[0].ty, TypeRef::BorrowedStr);
1404        assert_eq!(f.params[1].ty, TypeRef::BorrowedBytes);
1405    }
1406
1407    #[test]
1408    fn parse_typed_handle() {
1409        assert_eq!(
1410            parse_type_ref("handle<Session>"),
1411            Ok(TypeRef::TypedHandle("Session".into()))
1412        );
1413        assert_eq!(parse_type_ref("handle"), Ok(TypeRef::Handle));
1414    }
1415
1416    #[test]
1417    fn generators_field_parses_from_yaml() {
1418        let yaml = r#"
1419version: "0.5.0"
1420modules:
1421  - name: math
1422    functions: []
1423generators:
1424  swift:
1425    module_name: MySwiftModule
1426  android:
1427    package: com.example.app
1428"#;
1429        let api: Api = serde_yaml::from_str(yaml).unwrap();
1430        let generators = api.generators.as_ref().unwrap();
1431        let swift = generators["swift"].as_table().unwrap();
1432        assert_eq!(swift["module_name"].as_str(), Some("MySwiftModule"));
1433        let android = generators["android"].as_table().unwrap();
1434        assert_eq!(android["package"].as_str(), Some("com.example.app"));
1435    }
1436
1437    #[test]
1438    fn generators_defaults_to_none() {
1439        let yaml = r#"
1440version: "0.5.0"
1441modules:
1442  - name: math
1443    functions: []
1444"#;
1445        let api: Api = serde_yaml::from_str(yaml).unwrap();
1446        assert!(api.generators.is_none());
1447    }
1448
1449    #[test]
1450    fn parse_typed_handle_roundtrip() {
1451        let ty = TypeRef::TypedHandle("Connection".into());
1452        let json = serde_json::to_string(&ty).unwrap();
1453        assert_eq!(json, r#""handle<Connection>""#);
1454        let back: TypeRef = serde_json::from_str(&json).unwrap();
1455        assert_eq!(back, ty);
1456    }
1457
1458    #[test]
1459    fn callback_def_round_trip_yaml() {
1460        let yaml = r#"
1461version: "0.5.0"
1462modules:
1463  - name: events
1464    functions: []
1465    callbacks:
1466      - name: on_data
1467        params:
1468          - name: payload
1469            type: string
1470        doc: "Fired when data arrives"
1471"#;
1472        let api: Api = serde_yaml::from_str(yaml).unwrap();
1473        let m = &api.modules[0];
1474        assert_eq!(m.callbacks.len(), 1);
1475        let cb = &m.callbacks[0];
1476        assert_eq!(cb.name, "on_data");
1477        assert_eq!(cb.params.len(), 1);
1478        assert_eq!(cb.params[0].name, "payload");
1479        assert_eq!(cb.params[0].ty, TypeRef::StringUtf8);
1480        assert_eq!(cb.doc.as_deref(), Some("Fired when data arrives"));
1481    }
1482
1483    #[test]
1484    fn listener_def_round_trip_yaml() {
1485        let yaml = r#"
1486version: "0.5.0"
1487modules:
1488  - name: events
1489    functions: []
1490    callbacks:
1491      - name: on_data
1492        params: []
1493    listeners:
1494      - name: data_stream
1495        event_callback: on_data
1496        doc: "Subscribe to data events"
1497"#;
1498        let api: Api = serde_yaml::from_str(yaml).unwrap();
1499        let m = &api.modules[0];
1500        assert_eq!(m.listeners.len(), 1);
1501        let l = &m.listeners[0];
1502        assert_eq!(l.name, "data_stream");
1503        assert_eq!(l.event_callback, "on_data");
1504        assert_eq!(l.doc.as_deref(), Some("Subscribe to data events"));
1505    }
1506
1507    #[test]
1508    fn callbacks_and_listeners_default_to_empty() {
1509        let yaml = r#"
1510version: "0.5.0"
1511modules:
1512  - name: math
1513    functions: []
1514"#;
1515        let api: Api = serde_yaml::from_str(yaml).unwrap();
1516        assert!(api.modules[0].callbacks.is_empty());
1517        assert!(api.modules[0].listeners.is_empty());
1518    }
1519
1520    #[test]
1521    fn callback_def_json_round_trip() {
1522        let cb = CallbackDef {
1523            name: "on_event".to_string(),
1524            params: vec![Param {
1525                name: "data".to_string(),
1526                ty: TypeRef::I32,
1527                mutable: false,
1528                doc: None,
1529            }],
1530            doc: Some("event callback".to_string()),
1531        };
1532        let json = serde_json::to_string(&cb).unwrap();
1533        let back: CallbackDef = serde_json::from_str(&json).unwrap();
1534        assert_eq!(back, cb);
1535    }
1536
1537    #[test]
1538    fn listener_def_json_round_trip() {
1539        let l = ListenerDef {
1540            name: "watcher".to_string(),
1541            event_callback: "on_change".to_string(),
1542            doc: None,
1543        };
1544        let json = serde_json::to_string(&l).unwrap();
1545        let back: ListenerDef = serde_json::from_str(&json).unwrap();
1546        assert_eq!(back, l);
1547    }
1548
1549    #[test]
1550    fn builder_defaults_to_false() {
1551        let yaml = r#"
1552version: "0.5.0"
1553modules:
1554  - name: contacts
1555    functions: []
1556    structs:
1557      - name: Contact
1558        fields:
1559          - name: name
1560            type: string
1561"#;
1562        let api: Api = serde_yaml::from_str(yaml).unwrap();
1563        assert!(!api.modules[0].structs[0].builder);
1564    }
1565
1566    #[test]
1567    fn builder_true_round_trip() {
1568        let yaml = r#"
1569version: "0.5.0"
1570modules:
1571  - name: contacts
1572    functions: []
1573    structs:
1574      - name: Contact
1575        fields:
1576          - name: name
1577            type: string
1578        builder: true
1579"#;
1580        let api: Api = serde_yaml::from_str(yaml).unwrap();
1581        assert!(api.modules[0].structs[0].builder);
1582
1583        let json = serde_json::to_string(&api).unwrap();
1584        let back: Api = serde_json::from_str(&json).unwrap();
1585        assert!(back.modules[0].structs[0].builder);
1586    }
1587
1588    #[test]
1589    fn builder_false_explicit() {
1590        let json = r#"{
1591            "version": "0.5.0",
1592            "modules": [{
1593                "name": "geo",
1594                "functions": [],
1595                "structs": [{
1596                    "name": "Point",
1597                    "fields": [{"name": "x", "type": "f64"}],
1598                    "builder": false
1599                }]
1600            }]
1601        }"#;
1602        let api: Api = serde_json::from_str(json).unwrap();
1603        assert!(!api.modules[0].structs[0].builder);
1604    }
1605
1606    #[test]
1607    fn param_mutable_defaults_to_false() {
1608        let yaml = r#"
1609version: "0.5.0"
1610modules:
1611  - name: io
1612    functions:
1613      - name: write
1614        params:
1615          - name: data
1616            type: string
1617"#;
1618        let api: Api = serde_yaml::from_str(yaml).unwrap();
1619        assert!(!api.modules[0].functions[0].params[0].mutable);
1620    }
1621
1622    #[test]
1623    fn param_mutable_true_round_trip() {
1624        let yaml = r#"
1625version: "0.5.0"
1626modules:
1627  - name: io
1628    functions:
1629      - name: fill_buffer
1630        params:
1631          - name: buf
1632            type: bytes
1633            mutable: true
1634"#;
1635        let api: Api = serde_yaml::from_str(yaml).unwrap();
1636        assert!(api.modules[0].functions[0].params[0].mutable);
1637
1638        let json = serde_json::to_string(&api).unwrap();
1639        let back: Api = serde_json::from_str(&json).unwrap();
1640        assert!(back.modules[0].functions[0].params[0].mutable);
1641    }
1642
1643    #[test]
1644    fn param_mutable_false_explicit() {
1645        let json = r#"{
1646            "version": "0.5.0",
1647            "modules": [{
1648                "name": "io",
1649                "functions": [{
1650                    "name": "read",
1651                    "params": [{"name": "buf", "type": "bytes", "mutable": false}]
1652                }]
1653            }]
1654        }"#;
1655        let api: Api = serde_json::from_str(json).unwrap();
1656        assert!(!api.modules[0].functions[0].params[0].mutable);
1657    }
1658
1659    #[test]
1660    fn deprecated_and_since_default_to_none() {
1661        let yaml = r#"
1662version: "0.5.0"
1663modules:
1664  - name: math
1665    functions:
1666      - name: add
1667        params: []
1668"#;
1669        let api: Api = serde_yaml::from_str(yaml).unwrap();
1670        let f = &api.modules[0].functions[0];
1671        assert_eq!(f.deprecated, None);
1672        assert_eq!(f.since, None);
1673    }
1674
1675    #[test]
1676    fn deprecated_and_since_round_trip() {
1677        let yaml = r#"
1678version: "0.5.0"
1679modules:
1680  - name: math
1681    functions:
1682      - name: add_old
1683        params: []
1684        deprecated: "Use add_v2 instead"
1685        since: "0.1.0"
1686"#;
1687        let api: Api = serde_yaml::from_str(yaml).unwrap();
1688        let f = &api.modules[0].functions[0];
1689        assert_eq!(f.deprecated.as_deref(), Some("Use add_v2 instead"));
1690        assert_eq!(f.since.as_deref(), Some("0.1.0"));
1691
1692        let json = serde_json::to_string(&api).unwrap();
1693        let back: Api = serde_json::from_str(&json).unwrap();
1694        let f2 = &back.modules[0].functions[0];
1695        assert_eq!(f2.deprecated.as_deref(), Some("Use add_v2 instead"));
1696        assert_eq!(f2.since.as_deref(), Some("0.1.0"));
1697    }
1698
1699    #[test]
1700    fn struct_field_default_value_round_trip() {
1701        let yaml = r#"
1702version: "0.5.0"
1703modules:
1704  - name: contacts
1705    functions: []
1706    structs:
1707      - name: Contact
1708        fields:
1709          - name: name
1710            type: string
1711          - name: age
1712            type: i32
1713            default: 0
1714"#;
1715        let api: Api = serde_yaml::from_str(yaml).unwrap();
1716        let fields = &api.modules[0].structs[0].fields;
1717        assert!(fields[0].default.is_none());
1718        assert_eq!(
1719            fields[1].default,
1720            Some(serde_yaml::Value::Number(serde_yaml::Number::from(0)))
1721        );
1722    }
1723
1724    #[test]
1725    fn serialization_omits_defaulted_fields() {
1726        // A minimal API whose every optional/defaulted field is at its
1727        // default must serialize without emitting those fields, so the
1728        // canonical IDL produced by `weaveffi format`/`extract` stays terse.
1729        let api = Api {
1730            version: "0.5.0".into(),
1731            modules: vec![Module {
1732                name: "calc".into(),
1733                functions: vec![Function {
1734                    name: "add".into(),
1735                    params: vec![Param {
1736                        name: "a".into(),
1737                        ty: TypeRef::I32,
1738                        mutable: false,
1739                        doc: None,
1740                    }],
1741                    returns: Some(TypeRef::I32),
1742                    doc: None,
1743                    throws: false,
1744                    r#async: false,
1745                    cancellable: false,
1746                    deprecated: None,
1747                    since: None,
1748                }],
1749                interfaces: vec![],
1750                structs: vec![],
1751                enums: vec![],
1752                callbacks: vec![],
1753                listeners: vec![],
1754                errors: None,
1755                modules: vec![],
1756            }],
1757            generators: None,
1758            package: None,
1759        };
1760        let yaml = serde_yaml::to_string(&api).unwrap();
1761        for needle in [
1762            "generators",
1763            "interfaces",
1764            "structs",
1765            "enums",
1766            "callbacks",
1767            "listeners",
1768            "errors",
1769            "modules:\n", // nested module list (top-level key is "modules")
1770            "doc",
1771            "throws",
1772            "async",
1773            "cancellable",
1774            "deprecated",
1775            "since",
1776            "mutable",
1777            "null",
1778            "[]",
1779            "false",
1780        ] {
1781            // `modules:` appears once at the top level; assert the *nested*
1782            // empty module list under a module is gone by checking it never
1783            // shows an empty sequence.
1784            if needle == "modules:\n" {
1785                continue;
1786            }
1787            assert!(
1788                !yaml.contains(needle),
1789                "default field `{needle}` leaked into canonical YAML:\n{yaml}"
1790            );
1791        }
1792        // Round-trips back to an equal value.
1793        let back: Api = serde_yaml::from_str(&yaml).unwrap();
1794        assert_eq!(back, api);
1795    }
1796
1797    #[test]
1798    fn parse_type_ref_does_not_yield_callback() {
1799        assert_eq!(
1800            parse_type_ref("callback"),
1801            Ok(TypeRef::Struct("callback".into()))
1802        );
1803    }
1804
1805    #[test]
1806    fn api_json_schema_derives() {
1807        let schema = schemars::schema_for!(Api);
1808        let json = serde_json::to_value(&schema).unwrap();
1809        assert!(json.get("$schema").is_some());
1810        assert!(json.get("properties").is_some());
1811        assert_eq!(json.get("title").and_then(|v| v.as_str()), Some("Api"));
1812        let defs = json
1813            .get("definitions")
1814            .and_then(|v| v.as_object())
1815            .expect("definitions");
1816        assert!(defs.contains_key("Module"));
1817        assert!(defs.contains_key("Function"));
1818        assert!(defs.contains_key("Param"));
1819        assert!(defs.contains_key("TypeRef"));
1820        assert!(defs.contains_key("InterfaceDef"));
1821        assert!(defs.contains_key("StructDef"));
1822        assert!(defs.contains_key("StructField"));
1823        assert!(defs.contains_key("EnumDef"));
1824        assert!(defs.contains_key("EnumVariant"));
1825        assert!(defs.contains_key("CallbackDef"));
1826        assert!(defs.contains_key("ListenerDef"));
1827        assert!(defs.contains_key("ErrorDomain"));
1828        assert!(defs.contains_key("ErrorCode"));
1829    }
1830
1831    #[test]
1832    fn interface_round_trip_yaml() {
1833        let yaml = r#"
1834version: "0.5.0"
1835modules:
1836  - name: kv
1837    interfaces:
1838      - name: Store
1839        doc: "A key/value store"
1840        constructors:
1841          - name: open
1842            params:
1843              - name: path
1844                type: string
1845            throws: true
1846        methods:
1847          - name: put
1848            params:
1849              - name: key
1850                type: string
1851              - name: value
1852                type: bytes
1853            return: bool
1854            throws: true
1855          - name: count
1856            return: i64
1857        statics:
1858          - name: default_path
1859            return: string
1860"#;
1861        let api: Api = serde_yaml::from_str(yaml).unwrap();
1862        let m = &api.modules[0];
1863        assert!(m.functions.is_empty());
1864        assert_eq!(m.interfaces.len(), 1);
1865        let i = &m.interfaces[0];
1866        assert_eq!(i.name, "Store");
1867        assert_eq!(i.doc.as_deref(), Some("A key/value store"));
1868        assert_eq!(i.constructors.len(), 1);
1869        assert!(i.constructors[0].throws);
1870        assert_eq!(i.constructors[0].returns, None);
1871        assert_eq!(i.methods.len(), 2);
1872        assert!(i.methods[0].throws);
1873        assert!(!i.methods[1].throws);
1874        assert_eq!(i.methods[1].returns, Some(TypeRef::I64));
1875        assert_eq!(i.statics.len(), 1);
1876        assert_eq!(i.statics[0].returns, Some(TypeRef::StringUtf8));
1877
1878        // Round trip preserves the interface block.
1879        let out = serde_yaml::to_string(&api).unwrap();
1880        let back: Api = serde_yaml::from_str(&out).unwrap();
1881        assert_eq!(back, api);
1882    }
1883
1884    #[test]
1885    fn interfaces_default_to_empty() {
1886        let yaml = r#"
1887version: "0.5.0"
1888modules:
1889  - name: math
1890    functions: []
1891"#;
1892        let api: Api = serde_yaml::from_str(yaml).unwrap();
1893        assert!(api.modules[0].interfaces.is_empty());
1894    }
1895
1896    #[test]
1897    fn throws_defaults_to_false_and_round_trips() {
1898        let yaml = r#"
1899version: "0.5.0"
1900modules:
1901  - name: kv
1902    functions:
1903      - name: open
1904        params: []
1905        throws: true
1906      - name: count
1907        params: []
1908"#;
1909        let api: Api = serde_yaml::from_str(yaml).unwrap();
1910        assert!(api.modules[0].functions[0].throws);
1911        assert!(!api.modules[0].functions[1].throws);
1912        let json = serde_json::to_string(&api).unwrap();
1913        let back: Api = serde_json::from_str(&json).unwrap();
1914        assert!(back.modules[0].functions[0].throws);
1915        assert!(!back.modules[0].functions[1].throws);
1916    }
1917
1918    #[test]
1919    fn typeref_interface_serializes_as_name() {
1920        let ty = TypeRef::Interface("Store".to_string());
1921        let json = serde_json::to_string(&ty).unwrap();
1922        assert_eq!(json, r#""Store""#);
1923        // Deserialization yields `Struct`; the resolver rewrites it later.
1924        let back: TypeRef = serde_json::from_str(&json).unwrap();
1925        assert_eq!(back, TypeRef::Struct("Store".into()));
1926    }
1927
1928    #[test]
1929    fn typeref_json_schema_is_string_with_description() {
1930        let schema = schemars::schema_for!(TypeRef);
1931        let json = serde_json::to_value(&schema).unwrap();
1932        assert_eq!(json.get("type").and_then(|v| v.as_str()), Some("string"));
1933        assert!(json
1934            .get("description")
1935            .and_then(|v| v.as_str())
1936            .is_some_and(|s| s.contains("handle<") && s.contains("iter<")));
1937    }
1938}