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    /// An unresolved reference to a user-defined type, exactly as parsed.
345    ///
346    /// The parser cannot know whether a bare identifier names a record, an
347    /// enum, or an interface, so every user-type reference starts life as
348    /// `Named`. The resolution pass (`weaveffi_core::validate::resolve`)
349    /// rewrites each occurrence into [`Record`](Self::Record),
350    /// [`RichEnum`](Self::RichEnum), [`Enum`](Self::Enum), or
351    /// [`Interface`](Self::Interface); after a successful validate-and-resolve
352    /// no `Named` reference remains, and generators may treat one as a bug.
353    Named(String),
354    /// A user record (struct). Crosses the C ABI as an opaque object pointer:
355    /// borrowed as a parameter, owned (and eventually destroyed) as a return.
356    Record(String),
357    /// An algebraic (rich) enum: a sum type with at least one payload-carrying
358    /// variant. Crosses the C ABI as an opaque object pointer exactly like a
359    /// [`Record`](Self::Record); its declaration additionally lowers a tag
360    /// getter and per-variant constructors and field getters.
361    RichEnum(String),
362    /// A C-style integer enum (no variant payloads). Lowers by value.
363    Enum(String),
364    /// A user interface (see [`InterfaceDef`]): an opaque object reference.
365    /// As a parameter the object is borrowed for the call; as a return the
366    /// caller receives a new owned reference it must eventually release.
367    ///
368    /// The IDL spells an interface reference as its bare (or dotted-qualified)
369    /// name, exactly like a record; the resolution pass rewrites the parsed
370    /// [`Named`](Self::Named) into this variant when the name resolves to an
371    /// interface declaration.
372    Interface(String),
373    /// Borrowed string slice (`&str`): a non-owning view valid only for the
374    /// duration of a call, used to pass input without copying.
375    BorrowedStr,
376    /// Borrowed byte slice (`&[u8]`): a non-owning view valid only for the
377    /// duration of a call.
378    BorrowedBytes,
379    /// Optional value (`T?`): either the inner type or nothing.
380    Optional(Box<TypeRef>),
381    /// Homogeneous list (`[T]`) of the inner element type.
382    List(Box<TypeRef>),
383    /// Map (`{K:V}`) from a key type to a value type. Crosses the C ABI as
384    /// parallel key and value arrays.
385    Map(Box<TypeRef>, Box<TypeRef>),
386    /// Lazy sequence (`iter<T>`) of the inner type, lowered to a next/destroy
387    /// iterator object rather than a materialized collection.
388    Iterator(Box<TypeRef>),
389}
390
391/// Parse the IDL's compact type syntax into a [`TypeRef`].
392///
393/// Handles primitive names (`i32`, `string`, `bytes`, `handle`, and so on),
394/// borrowed forms (`&str`, `&[u8]`), typed handles (`handle<Name>`), iterators
395/// (`iter<T>`), lists (`[T]`), maps (`{K:V}`), and the optional suffix (`T?`).
396/// Any other bare identifier is taken to be a user-defined record, enum, or
397/// interface name and returned as [`TypeRef::Named`]; the
398/// record-versus-enum-versus-interface distinction is resolved later against
399/// the module's declarations.
400///
401/// # Errors
402///
403/// Returns an error message when `s` is empty or only whitespace, or when a map
404/// type (`{K:V}`) is missing its `:` separator. The same errors propagate up
405/// from a malformed inner type of a list, map, optional, or iterator.
406pub fn parse_type_ref(s: &str) -> Result<TypeRef, String> {
407    let s = s.trim();
408    if s.is_empty() {
409        return Err("empty type reference".to_string());
410    }
411    if s.starts_with('[') && s.ends_with(']') {
412        let inner = &s[1..s.len() - 1];
413        return parse_type_ref(inner).map(|t| TypeRef::List(Box::new(t)));
414    }
415    if s.starts_with('{') && s.ends_with('}') {
416        let inner = &s[1..s.len() - 1];
417        let colon = inner
418            .find(':')
419            .ok_or_else(|| "map type missing ':' separator".to_string())?;
420        let key = parse_type_ref(&inner[..colon])?;
421        let val = parse_type_ref(&inner[colon + 1..])?;
422        return Ok(TypeRef::Map(Box::new(key), Box::new(val)));
423    }
424    if let Some(inner) = s.strip_suffix('?') {
425        return parse_type_ref(inner).map(|t| TypeRef::Optional(Box::new(t)));
426    }
427    if let Some(inner) = s
428        .strip_prefix("handle<")
429        .and_then(|rest| rest.strip_suffix('>'))
430    {
431        return Ok(TypeRef::TypedHandle(inner.into()));
432    }
433    if let Some(inner) = s
434        .strip_prefix("iter<")
435        .and_then(|rest| rest.strip_suffix('>'))
436    {
437        return parse_type_ref(inner).map(|t| TypeRef::Iterator(Box::new(t)));
438    }
439    match s {
440        "i8" => Ok(TypeRef::I8),
441        "i16" => Ok(TypeRef::I16),
442        "i32" => Ok(TypeRef::I32),
443        "i64" => Ok(TypeRef::I64),
444        "u8" => Ok(TypeRef::U8),
445        "u16" => Ok(TypeRef::U16),
446        "u32" => Ok(TypeRef::U32),
447        "u64" => Ok(TypeRef::U64),
448        "f32" => Ok(TypeRef::F32),
449        "f64" => Ok(TypeRef::F64),
450        "bool" => Ok(TypeRef::Bool),
451        "string" => Ok(TypeRef::StringUtf8),
452        "bytes" => Ok(TypeRef::Bytes),
453        "handle" => Ok(TypeRef::Handle),
454        "&str" => Ok(TypeRef::BorrowedStr),
455        "&[u8]" => Ok(TypeRef::BorrowedBytes),
456        name => Ok(TypeRef::Named(name.to_string())),
457    }
458}
459
460impl TypeRef {
461    /// The referenced user-type name for any user-defined reference variant
462    /// ([`Named`](Self::Named), [`Record`](Self::Record),
463    /// [`RichEnum`](Self::RichEnum), [`Enum`](Self::Enum),
464    /// [`Interface`](Self::Interface), or
465    /// [`TypedHandle`](Self::TypedHandle)), or `None` for every other type.
466    pub fn user_name(&self) -> Option<&str> {
467        match self {
468            TypeRef::Named(n)
469            | TypeRef::Record(n)
470            | TypeRef::RichEnum(n)
471            | TypeRef::Enum(n)
472            | TypeRef::Interface(n)
473            | TypeRef::TypedHandle(n) => Some(n),
474            _ => None,
475        }
476    }
477
478    /// `true` when this reference lowers across the C ABI as an opaque object
479    /// pointer to a user-declared data type: a [`Record`](Self::Record) or a
480    /// [`RichEnum`](Self::RichEnum). Interfaces also cross as opaque pointers
481    /// but carry a distinct ownership convention, so they are excluded.
482    pub fn is_object_ref(&self) -> bool {
483        matches!(self, TypeRef::Record(_) | TypeRef::RichEnum(_))
484    }
485}
486
487fn type_ref_to_string(ty: &TypeRef) -> String {
488    match ty {
489        TypeRef::I8 => "i8".to_string(),
490        TypeRef::I16 => "i16".to_string(),
491        TypeRef::I32 => "i32".to_string(),
492        TypeRef::I64 => "i64".to_string(),
493        TypeRef::U8 => "u8".to_string(),
494        TypeRef::U16 => "u16".to_string(),
495        TypeRef::U32 => "u32".to_string(),
496        TypeRef::U64 => "u64".to_string(),
497        TypeRef::F32 => "f32".to_string(),
498        TypeRef::F64 => "f64".to_string(),
499        TypeRef::Bool => "bool".to_string(),
500        TypeRef::StringUtf8 => "string".to_string(),
501        TypeRef::Bytes => "bytes".to_string(),
502        TypeRef::BorrowedStr => "&str".to_string(),
503        TypeRef::BorrowedBytes => "&[u8]".to_string(),
504        TypeRef::Handle => "handle".to_string(),
505        TypeRef::TypedHandle(name) => format!("handle<{name}>"),
506        TypeRef::Named(name)
507        | TypeRef::Record(name)
508        | TypeRef::RichEnum(name)
509        | TypeRef::Enum(name)
510        | TypeRef::Interface(name) => name.clone(),
511        TypeRef::Optional(inner) => format!("{}?", type_ref_to_string(inner)),
512        TypeRef::List(inner) => format!("[{}]", type_ref_to_string(inner)),
513        TypeRef::Map(k, v) => format!("{{{}:{}}}", type_ref_to_string(k), type_ref_to_string(v)),
514        TypeRef::Iterator(inner) => format!("iter<{}>", type_ref_to_string(inner)),
515    }
516}
517
518impl Serialize for TypeRef {
519    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
520    where
521        S: serde::Serializer,
522    {
523        serializer.serialize_str(&type_ref_to_string(self))
524    }
525}
526
527impl<'de> Deserialize<'de> for TypeRef {
528    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
529    where
530        D: serde::Deserializer<'de>,
531    {
532        let s = String::deserialize(deserializer)?;
533        parse_type_ref(&s).map_err(serde::de::Error::custom)
534    }
535}
536
537/// Manual `JsonSchema` impl because `TypeRef` (de)serializes as a string with
538/// custom syntax: primitive names (`i32`, `string`, ...), `&str`, `&[u8]`,
539/// `handle<{name}>`, `iter<{T}>`, `[{T}]`, `{ {K}: {V} }`, `{name}?`, or any
540/// user-defined struct/enum name.
541impl JsonSchema for TypeRef {
542    fn schema_name() -> String {
543        "TypeRef".to_string()
544    }
545
546    fn schema_id() -> std::borrow::Cow<'static, str> {
547        std::borrow::Cow::Borrowed(concat!(module_path!(), "::TypeRef"))
548    }
549
550    fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
551        let mut schema = schemars::schema::SchemaObject {
552            instance_type: Some(schemars::schema::InstanceType::String.into()),
553            ..Default::default()
554        };
555        let meta = schema.metadata();
556        meta.title = Some("TypeRef".to_string());
557        meta.description = Some(
558            "Reference to a type. Encoded as a string with custom syntax: \
559             primitives (`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, \
560             `f32`, `f64`, `bool`, `string`, `bytes`, `handle`), \
561             borrowed types (`&str`, `&[u8]`), typed handles (`handle<{name}>`), \
562             iterators (`iter<{T}>`), lists (`[{T}]`), maps (`{{K:V}}`), \
563             optionals (`{T}?`), or any user-defined struct/enum/interface name."
564                .to_string(),
565        );
566        schema.into()
567    }
568}
569
570/// An enum type. C-style when every variant is a bare discriminant; an
571/// algebraic sum type when any variant declares fields (see
572/// [`is_rich`](Self::is_rich)).
573///
574/// A C-style enum lowers across the C ABI by value as an integer, while an
575/// algebraic enum lowers as an opaque object with a tag getter plus per-variant
576/// constructors and field getters.
577// `Eq` is omitted because a variant field's `default` may hold `serde_yaml::Value` (an `f64`), matching `StructDef`.
578#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
579#[schemars(
580    description = "An enum type. C-style when every variant is a bare discriminant; an algebraic sum type when any variant declares fields."
581)]
582pub struct EnumDef {
583    /// Enum type name (for example `Color`).
584    pub name: String,
585    /// Human-readable documentation, propagated to the generated bindings.
586    /// `None` when undocumented.
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub doc: Option<String>,
589    /// The variants in declaration order. Whether any of them carries fields
590    /// decides if this is a C-style or an algebraic enum.
591    pub variants: Vec<EnumVariant>,
592}
593
594impl EnumDef {
595    /// `true` when this is an *algebraic* enum (a sum type): at least one
596    /// variant carries associated data. Such enums lower across the C ABI as
597    /// opaque objects (a tag getter plus per-variant constructors and field
598    /// getters); a C-style enum (every variant a bare discriminant) lowers by
599    /// value as an integer.
600    pub fn is_rich(&self) -> bool {
601        self.variants.iter().any(|v| !v.fields.is_empty())
602    }
603}
604
605/// A single variant of an [`EnumDef`].
606// `Eq` is omitted because a variant field's `default` may hold `serde_yaml::Value` (an `f64`).
607#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
608pub struct EnumVariant {
609    /// Variant name (for example `Red`).
610    pub name: String,
611    /// Integer discriminant. Doubles as the C-style enum value and as the
612    /// runtime tag that distinguishes the variants of an algebraic enum.
613    pub value: i32,
614    /// Human-readable documentation, propagated to the generated bindings.
615    /// `None` when undocumented.
616    #[serde(default, skip_serializing_if = "Option::is_none")]
617    pub doc: Option<String>,
618    /// Associated data carried by this variant. Empty for a unit variant or a
619    /// C-style enum; non-empty makes the owning enum a sum type (see
620    /// [`EnumDef::is_rich`]). Variant fields reuse [`StructField`] but ignore
621    /// the `default` slot (a sum-type payload has no defaultable fields).
622    #[serde(default, skip_serializing_if = "Vec::is_empty")]
623    pub fields: Vec<StructField>,
624}
625
626/// A struct (record) type with named fields.
627// `Eq` is omitted because `StructField::default` holds `serde_yaml::Value` (an `f64`).
628#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
629#[schemars(description = "A struct (record) type with named fields.")]
630pub struct StructDef {
631    /// Struct type name (for example `Contact`).
632    pub name: String,
633    /// Human-readable documentation, propagated to the generated bindings.
634    /// `None` when undocumented.
635    #[serde(default, skip_serializing_if = "Option::is_none")]
636    pub doc: Option<String>,
637    /// The fields in declaration order; order is preserved in the generated
638    /// type and its constructors.
639    pub fields: Vec<StructField>,
640    /// Whether to also emit a builder API for constructing the struct field by
641    /// field, alongside the all-fields constructor. Defaults to `false`.
642    #[serde(default, skip_serializing_if = "is_false")]
643    pub builder: bool,
644}
645
646/// A named field of a [`StructDef`], or the payload of an algebraic
647/// [`EnumVariant`].
648#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
649pub struct StructField {
650    /// Field name (for example `email`).
651    pub name: String,
652    /// Field type. Serialized under the IDL key `type`.
653    #[serde(rename = "type")]
654    pub ty: TypeRef,
655    /// Human-readable documentation, propagated to the generated bindings.
656    /// `None` when undocumented.
657    #[serde(default, skip_serializing_if = "Option::is_none")]
658    pub doc: Option<String>,
659    /// Default value used when the field is omitted, kept as a raw YAML value so
660    /// any literal the field's type accepts can be expressed. Ignored for
661    /// algebraic [`EnumVariant`] payloads, which aren't defaultable. `None` when
662    /// the field has no default.
663    #[serde(default, skip_serializing_if = "Option::is_none")]
664    #[schemars(with = "Option<serde_json::Value>")]
665    pub default: Option<serde_yaml::Value>,
666}
667
668/// A module's error domain: the named set of error codes its fallible functions
669/// can report.
670#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
671pub struct ErrorDomain {
672    /// Error domain name, used to name the generated error type (for example
673    /// `ContactErrors`).
674    pub name: String,
675    /// The error codes that belong to this domain.
676    pub codes: Vec<ErrorCode>,
677}
678
679/// A single named error within an [`ErrorDomain`].
680#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
681pub struct ErrorCode {
682    /// Error code name, lowered to a variant or constant on the generated error
683    /// type (for example `not_found`).
684    pub name: String,
685    /// Stable numeric value carried across the C ABI to identify this error.
686    pub code: i32,
687    /// Default human-readable message describing the error.
688    pub message: String,
689    /// Human-readable documentation, propagated to the generated bindings.
690    /// `None` when undocumented.
691    #[serde(default, skip_serializing_if = "Option::is_none")]
692    pub doc: Option<String>,
693}
694
695#[cfg(test)]
696mod tests {
697    use super::*;
698
699    #[test]
700    fn struct_def_round_trip_yaml() {
701        let yaml = r#"
702version: "0.5.0"
703modules:
704  - name: geometry
705    functions: []
706    structs:
707      - name: Point
708        doc: "A 2D point"
709        fields:
710          - name: x
711            type: f64
712          - name: "y"
713            type: f64
714            doc: "Y coordinate"
715"#;
716        let api: Api = serde_yaml::from_str(yaml).unwrap();
717        let m = &api.modules[0];
718        assert_eq!(m.structs.len(), 1);
719        let s = &m.structs[0];
720        assert_eq!(s.name, "Point");
721        assert_eq!(s.doc.as_deref(), Some("A 2D point"));
722        assert_eq!(s.fields.len(), 2);
723        assert_eq!(s.fields[0].name, "x");
724        assert_eq!(s.fields[0].ty, TypeRef::F64);
725        assert_eq!(s.fields[0].doc, None);
726        assert_eq!(s.fields[1].name, "y");
727        assert_eq!(s.fields[1].doc.as_deref(), Some("Y coordinate"));
728    }
729
730    #[test]
731    fn struct_def_round_trip_json() {
732        let json = r#"{
733            "version": "0.5.0",
734            "modules": [{
735                "name": "geo",
736                "functions": [],
737                "structs": [{
738                    "name": "Rect",
739                    "fields": [
740                        {"name": "width", "type": "i32"},
741                        {"name": "height", "type": "i32"}
742                    ]
743                }]
744            }]
745        }"#;
746        let api: Api = serde_json::from_str(json).unwrap();
747        let s = &api.modules[0].structs[0];
748        assert_eq!(s.name, "Rect");
749        assert_eq!(s.doc, None);
750        assert_eq!(s.fields[0].ty, TypeRef::I32);
751    }
752
753    #[test]
754    fn structs_default_to_empty() {
755        let yaml = r#"
756version: "0.5.0"
757modules:
758  - name: math
759    functions: []
760"#;
761        let api: Api = serde_yaml::from_str(yaml).unwrap();
762        assert!(api.modules[0].structs.is_empty());
763    }
764
765    #[test]
766    fn package_block_round_trips_yaml() {
767        let yaml = r#"
768version: "0.5.0"
769package:
770  name: kvstore
771  version: 1.2.0
772  description: "An embedded key/value store"
773  license: MIT
774  authors:
775    - "Ada Lovelace <ada@example.com>"
776  homepage: "https://example.com/kvstore"
777  repository: "https://github.com/example/kvstore"
778modules:
779  - name: kv
780    functions: []
781"#;
782        let api: Api = serde_yaml::from_str(yaml).unwrap();
783        let pkg = api.package.as_ref().expect("package should parse");
784        assert_eq!(pkg.name, "kvstore");
785        assert_eq!(pkg.version, "1.2.0");
786        assert_eq!(
787            pkg.description.as_deref(),
788            Some("An embedded key/value store")
789        );
790        assert_eq!(pkg.license.as_deref(), Some("MIT"));
791        assert_eq!(pkg.authors, vec!["Ada Lovelace <ada@example.com>"]);
792        assert_eq!(pkg.homepage.as_deref(), Some("https://example.com/kvstore"));
793        assert_eq!(
794            pkg.repository.as_deref(),
795            Some("https://github.com/example/kvstore")
796        );
797
798        // Re-serialize and confirm the block survives the round trip.
799        let out = serde_yaml::to_string(&api).unwrap();
800        assert!(out.contains("name: kvstore"));
801        assert!(out.contains("version: 1.2.0"));
802    }
803
804    #[test]
805    fn package_is_optional() {
806        let yaml = r#"
807version: "0.5.0"
808modules:
809  - name: math
810    functions: []
811"#;
812        let api: Api = serde_yaml::from_str(yaml).unwrap();
813        assert!(api.package.is_none());
814        // Absent package must not appear in the canonical serialization.
815        let out = serde_yaml::to_string(&api).unwrap();
816        assert!(!out.contains("package:"));
817    }
818
819    #[test]
820    fn package_minimal_requires_name_and_version() {
821        let yaml = r#"
822version: "0.5.0"
823package:
824  name: tiny
825  version: 0.0.1
826modules: []
827"#;
828        let api: Api = serde_yaml::from_str(yaml).unwrap();
829        let pkg = api.package.as_ref().unwrap();
830        assert_eq!(pkg.name, "tiny");
831        assert_eq!(pkg.version, "0.0.1");
832        assert!(pkg.description.is_none());
833        assert!(pkg.authors.is_empty());
834    }
835
836    #[test]
837    fn typeref_struct_variant_serializes() {
838        let ty = TypeRef::Named("Point".to_string());
839        let json = serde_json::to_string(&ty).unwrap();
840        assert_eq!(json, r#""Point""#);
841        let back: TypeRef = serde_json::from_str(&json).unwrap();
842        assert_eq!(back, ty);
843    }
844
845    #[test]
846    fn struct_field_with_struct_type() {
847        let field = StructField {
848            name: "origin".to_string(),
849            ty: TypeRef::Named("Point".to_string()),
850            doc: None,
851            default: None,
852        };
853        let json = serde_json::to_string(&field).unwrap();
854        let back: StructField = serde_json::from_str(&json).unwrap();
855        assert_eq!(back, field);
856    }
857
858    #[test]
859    fn typeref_is_not_copy() {
860        let a = TypeRef::Named("Foo".to_string());
861        let b = a.clone();
862        assert_eq!(a, b);
863    }
864
865    #[test]
866    fn enum_def_round_trip_yaml() {
867        let yaml = r#"
868version: "0.5.0"
869modules:
870  - name: graphics
871    functions: []
872    enums:
873      - name: Color
874        doc: "Primary colors"
875        variants:
876          - name: Red
877            value: 0
878          - name: Green
879            value: 1
880            doc: "The color green"
881          - name: Blue
882            value: 2
883"#;
884        let api: Api = serde_yaml::from_str(yaml).unwrap();
885        let m = &api.modules[0];
886        assert_eq!(m.enums.len(), 1);
887        let e = &m.enums[0];
888        assert_eq!(e.name, "Color");
889        assert_eq!(e.doc.as_deref(), Some("Primary colors"));
890        assert_eq!(e.variants.len(), 3);
891        assert_eq!(e.variants[0].name, "Red");
892        assert_eq!(e.variants[0].value, 0);
893        assert_eq!(e.variants[0].doc, None);
894        assert_eq!(e.variants[1].name, "Green");
895        assert_eq!(e.variants[1].value, 1);
896        assert_eq!(e.variants[1].doc.as_deref(), Some("The color green"));
897        assert_eq!(e.variants[2].name, "Blue");
898        assert_eq!(e.variants[2].value, 2);
899    }
900
901    #[test]
902    fn enum_def_round_trip_json() {
903        let json = r#"{
904            "version": "0.5.0",
905            "modules": [{
906                "name": "status",
907                "functions": [],
908                "enums": [{
909                    "name": "Status",
910                    "variants": [
911                        {"name": "Ok", "value": 0},
912                        {"name": "Error", "value": 1}
913                    ]
914                }]
915            }]
916        }"#;
917        let api: Api = serde_json::from_str(json).unwrap();
918        let e = &api.modules[0].enums[0];
919        assert_eq!(e.name, "Status");
920        assert_eq!(e.doc, None);
921        assert_eq!(e.variants.len(), 2);
922        assert_eq!(e.variants[1].value, 1);
923    }
924
925    #[test]
926    fn enums_default_to_empty() {
927        let yaml = r#"
928version: "0.5.0"
929modules:
930  - name: math
931    functions: []
932"#;
933        let api: Api = serde_yaml::from_str(yaml).unwrap();
934        assert!(api.modules[0].enums.is_empty());
935    }
936
937    #[test]
938    fn typeref_enum_variant_serializes_as_name() {
939        let ty = TypeRef::Enum("Color".to_string());
940        let json = serde_json::to_string(&ty).unwrap();
941        assert_eq!(json, r#""Color""#);
942    }
943
944    #[test]
945    fn enum_def_clone_and_eq() {
946        let e = EnumDef {
947            name: "Direction".to_string(),
948            doc: Some("Cardinal directions".to_string()),
949            variants: vec![
950                EnumVariant {
951                    name: "North".to_string(),
952                    value: 0,
953                    doc: None,
954                    fields: vec![],
955                },
956                EnumVariant {
957                    name: "South".to_string(),
958                    value: 1,
959                    doc: None,
960                    fields: vec![],
961                },
962            ],
963        };
964        assert_eq!(e, e.clone());
965        assert!(!e.is_rich());
966    }
967
968    #[test]
969    fn enum_def_is_rich_when_a_variant_has_fields() {
970        let e = EnumDef {
971            name: "Shape".to_string(),
972            doc: None,
973            variants: vec![
974                EnumVariant {
975                    name: "Circle".to_string(),
976                    value: 0,
977                    doc: None,
978                    fields: vec![StructField {
979                        name: "radius".to_string(),
980                        ty: TypeRef::F64,
981                        doc: None,
982                        default: None,
983                    }],
984                },
985                EnumVariant {
986                    name: "Empty".to_string(),
987                    value: 1,
988                    doc: None,
989                    fields: vec![],
990                },
991            ],
992        };
993        assert!(e.is_rich());
994    }
995
996    #[test]
997    fn struct_def_clone_and_eq() {
998        let s = StructDef {
999            name: "Color".to_string(),
1000            doc: Some("RGB color".to_string()),
1001            fields: vec![
1002                StructField {
1003                    name: "r".to_string(),
1004                    ty: TypeRef::U32,
1005                    doc: None,
1006                    default: None,
1007                },
1008                StructField {
1009                    name: "g".to_string(),
1010                    ty: TypeRef::U32,
1011                    doc: None,
1012                    default: None,
1013                },
1014                StructField {
1015                    name: "b".to_string(),
1016                    ty: TypeRef::U32,
1017                    doc: None,
1018                    default: None,
1019                },
1020            ],
1021            builder: false,
1022        };
1023        assert_eq!(s, s.clone());
1024    }
1025
1026    #[test]
1027    fn parse_type_ref_primitives() {
1028        assert_eq!(parse_type_ref("i32"), Ok(TypeRef::I32));
1029        assert_eq!(parse_type_ref("u32"), Ok(TypeRef::U32));
1030        assert_eq!(parse_type_ref("i64"), Ok(TypeRef::I64));
1031        assert_eq!(parse_type_ref("f64"), Ok(TypeRef::F64));
1032        assert_eq!(parse_type_ref("bool"), Ok(TypeRef::Bool));
1033        assert_eq!(parse_type_ref("string"), Ok(TypeRef::StringUtf8));
1034        assert_eq!(parse_type_ref("bytes"), Ok(TypeRef::Bytes));
1035        assert_eq!(parse_type_ref("handle"), Ok(TypeRef::Handle));
1036    }
1037
1038    #[test]
1039    fn parse_type_ref_struct() {
1040        assert_eq!(
1041            parse_type_ref("Contact"),
1042            Ok(TypeRef::Named("Contact".into()))
1043        );
1044        assert_eq!(
1045            parse_type_ref("MyWidget"),
1046            Ok(TypeRef::Named("MyWidget".into()))
1047        );
1048    }
1049
1050    #[test]
1051    fn parse_type_ref_optional() {
1052        assert_eq!(
1053            parse_type_ref("string?"),
1054            Ok(TypeRef::Optional(Box::new(TypeRef::StringUtf8)))
1055        );
1056        assert_eq!(
1057            parse_type_ref("i32?"),
1058            Ok(TypeRef::Optional(Box::new(TypeRef::I32)))
1059        );
1060        assert_eq!(
1061            parse_type_ref("Contact?"),
1062            Ok(TypeRef::Optional(Box::new(TypeRef::Named(
1063                "Contact".into()
1064            ))))
1065        );
1066    }
1067
1068    #[test]
1069    fn parse_type_ref_list() {
1070        assert_eq!(
1071            parse_type_ref("[i32]"),
1072            Ok(TypeRef::List(Box::new(TypeRef::I32)))
1073        );
1074        assert_eq!(
1075            parse_type_ref("[string]"),
1076            Ok(TypeRef::List(Box::new(TypeRef::StringUtf8)))
1077        );
1078        assert_eq!(
1079            parse_type_ref("[Contact]"),
1080            Ok(TypeRef::List(Box::new(TypeRef::Named("Contact".into()))))
1081        );
1082    }
1083
1084    #[test]
1085    fn parse_type_ref_nested() {
1086        assert_eq!(
1087            parse_type_ref("[i32?]"),
1088            Ok(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
1089                TypeRef::I32
1090            )))))
1091        );
1092        assert_eq!(
1093            parse_type_ref("[Contact]?"),
1094            Ok(TypeRef::Optional(Box::new(TypeRef::List(Box::new(
1095                TypeRef::Named("Contact".into())
1096            )))))
1097        );
1098    }
1099
1100    #[test]
1101    fn parse_type_ref_empty_is_error() {
1102        assert!(parse_type_ref("").is_err());
1103        assert!(parse_type_ref("  ").is_err());
1104    }
1105
1106    #[test]
1107    fn typeref_primitive_round_trips() {
1108        for ty in [
1109            TypeRef::I32,
1110            TypeRef::U32,
1111            TypeRef::I64,
1112            TypeRef::F64,
1113            TypeRef::Bool,
1114            TypeRef::StringUtf8,
1115            TypeRef::Bytes,
1116            TypeRef::Handle,
1117        ] {
1118            let json = serde_json::to_string(&ty).unwrap();
1119            let back: TypeRef = serde_json::from_str(&json).unwrap();
1120            assert_eq!(back, ty);
1121        }
1122    }
1123
1124    #[test]
1125    fn typeref_optional_round_trip() {
1126        let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
1127        let json = serde_json::to_string(&ty).unwrap();
1128        assert_eq!(json, r#""string?""#);
1129        let back: TypeRef = serde_json::from_str(&json).unwrap();
1130        assert_eq!(back, ty);
1131    }
1132
1133    #[test]
1134    fn typeref_list_round_trip() {
1135        let ty = TypeRef::List(Box::new(TypeRef::I32));
1136        let json = serde_json::to_string(&ty).unwrap();
1137        assert_eq!(json, r#""[i32]""#);
1138        let back: TypeRef = serde_json::from_str(&json).unwrap();
1139        assert_eq!(back, ty);
1140    }
1141
1142    #[test]
1143    fn typeref_optional_struct_round_trip() {
1144        let ty = TypeRef::Optional(Box::new(TypeRef::Named("Contact".into())));
1145        let json = serde_json::to_string(&ty).unwrap();
1146        assert_eq!(json, r#""Contact?""#);
1147        let back: TypeRef = serde_json::from_str(&json).unwrap();
1148        assert_eq!(back, ty);
1149    }
1150
1151    #[test]
1152    fn typeref_list_struct_round_trip() {
1153        let ty = TypeRef::List(Box::new(TypeRef::Named("Contact".into())));
1154        let json = serde_json::to_string(&ty).unwrap();
1155        assert_eq!(json, r#""[Contact]""#);
1156        let back: TypeRef = serde_json::from_str(&json).unwrap();
1157        assert_eq!(back, ty);
1158    }
1159
1160    #[test]
1161    fn typeref_optional_yaml_deser() {
1162        let yaml = r#"
1163version: "0.5.0"
1164modules:
1165  - name: contacts
1166    functions:
1167      - name: find
1168        params:
1169          - name: id
1170            type: i32
1171        return: "Contact?"
1172"#;
1173        let api: Api = serde_yaml::from_str(yaml).unwrap();
1174        let f = &api.modules[0].functions[0];
1175        assert_eq!(
1176            f.returns,
1177            Some(TypeRef::Optional(Box::new(TypeRef::Named(
1178                "Contact".into()
1179            ))))
1180        );
1181    }
1182
1183    #[test]
1184    fn typeref_list_yaml_deser() {
1185        let yaml = r#"
1186version: "0.5.0"
1187modules:
1188  - name: contacts
1189    functions:
1190      - name: list_all
1191        params: []
1192        return: "[Contact]"
1193"#;
1194        let api: Api = serde_yaml::from_str(yaml).unwrap();
1195        let f = &api.modules[0].functions[0];
1196        assert_eq!(
1197            f.returns,
1198            Some(TypeRef::List(Box::new(TypeRef::Named("Contact".into()))))
1199        );
1200    }
1201
1202    #[test]
1203    fn typeref_hash_works_with_box_variants() {
1204        use std::collections::HashSet;
1205        let mut set = HashSet::new();
1206        set.insert(TypeRef::I32);
1207        set.insert(TypeRef::Optional(Box::new(TypeRef::I32)));
1208        set.insert(TypeRef::List(Box::new(TypeRef::I32)));
1209        set.insert(TypeRef::Optional(Box::new(TypeRef::Named("Foo".into()))));
1210        set.insert(TypeRef::Map(
1211            Box::new(TypeRef::StringUtf8),
1212            Box::new(TypeRef::I32),
1213        ));
1214        assert_eq!(set.len(), 5);
1215    }
1216
1217    #[test]
1218    fn parse_type_ref_map_primitives() {
1219        assert_eq!(
1220            parse_type_ref("{string:i32}"),
1221            Ok(TypeRef::Map(
1222                Box::new(TypeRef::StringUtf8),
1223                Box::new(TypeRef::I32)
1224            ))
1225        );
1226    }
1227
1228    #[test]
1229    fn parse_type_ref_map_struct_value() {
1230        assert_eq!(
1231            parse_type_ref("{string:Contact}"),
1232            Ok(TypeRef::Map(
1233                Box::new(TypeRef::StringUtf8),
1234                Box::new(TypeRef::Named("Contact".into()))
1235            ))
1236        );
1237    }
1238
1239    #[test]
1240    fn parse_type_ref_map_nested_value() {
1241        assert_eq!(
1242            parse_type_ref("{string:[i32]}"),
1243            Ok(TypeRef::Map(
1244                Box::new(TypeRef::StringUtf8),
1245                Box::new(TypeRef::List(Box::new(TypeRef::I32)))
1246            ))
1247        );
1248    }
1249
1250    #[test]
1251    fn parse_type_ref_map_missing_colon() {
1252        assert!(parse_type_ref("{string}").is_err());
1253    }
1254
1255    #[test]
1256    fn typeref_map_round_trip() {
1257        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
1258        let json = serde_json::to_string(&ty).unwrap();
1259        assert_eq!(json, r#""{string:i32}""#);
1260        let back: TypeRef = serde_json::from_str(&json).unwrap();
1261        assert_eq!(back, ty);
1262    }
1263
1264    #[test]
1265    fn typeref_map_struct_round_trip() {
1266        let ty = TypeRef::Map(
1267            Box::new(TypeRef::StringUtf8),
1268            Box::new(TypeRef::Named("Contact".into())),
1269        );
1270        let json = serde_json::to_string(&ty).unwrap();
1271        assert_eq!(json, r#""{string:Contact}""#);
1272        let back: TypeRef = serde_json::from_str(&json).unwrap();
1273        assert_eq!(back, ty);
1274    }
1275
1276    #[test]
1277    fn typeref_map_yaml_deser() {
1278        let yaml = r#"
1279version: "0.5.0"
1280modules:
1281  - name: contacts
1282    functions:
1283      - name: get_metadata
1284        params: []
1285        return: "{string:i32}"
1286"#;
1287        let api: Api = serde_yaml::from_str(yaml).unwrap();
1288        let f = &api.modules[0].functions[0];
1289        assert_eq!(
1290            f.returns,
1291            Some(TypeRef::Map(
1292                Box::new(TypeRef::StringUtf8),
1293                Box::new(TypeRef::I32)
1294            ))
1295        );
1296    }
1297
1298    #[test]
1299    fn typeref_optional_map_round_trip() {
1300        let ty = TypeRef::Optional(Box::new(TypeRef::Map(
1301            Box::new(TypeRef::StringUtf8),
1302            Box::new(TypeRef::I32),
1303        )));
1304        let json = serde_json::to_string(&ty).unwrap();
1305        assert_eq!(json, r#""{string:i32}?""#);
1306        let back: TypeRef = serde_json::from_str(&json).unwrap();
1307        assert_eq!(back, ty);
1308    }
1309
1310    #[test]
1311    fn parse_map_string_to_i32() {
1312        assert_eq!(
1313            parse_type_ref("{string:i32}"),
1314            Ok(TypeRef::Map(
1315                Box::new(TypeRef::StringUtf8),
1316                Box::new(TypeRef::I32),
1317            ))
1318        );
1319    }
1320
1321    #[test]
1322    fn parse_map_string_to_struct() {
1323        assert_eq!(
1324            parse_type_ref("{string:Contact}"),
1325            Ok(TypeRef::Map(
1326                Box::new(TypeRef::StringUtf8),
1327                Box::new(TypeRef::Named("Contact".into())),
1328            ))
1329        );
1330    }
1331
1332    #[test]
1333    fn parse_map_roundtrip() {
1334        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
1335        let json = serde_json::to_string(&ty).unwrap();
1336        let back: TypeRef = serde_json::from_str(&json).unwrap();
1337        assert_eq!(back, ty);
1338    }
1339
1340    #[test]
1341    fn parse_optional_map() {
1342        assert_eq!(
1343            parse_type_ref("{string:i32}?"),
1344            Ok(TypeRef::Optional(Box::new(TypeRef::Map(
1345                Box::new(TypeRef::StringUtf8),
1346                Box::new(TypeRef::I32),
1347            ))))
1348        );
1349    }
1350
1351    #[test]
1352    fn parse_map_of_lists() {
1353        assert_eq!(
1354            parse_type_ref("{string:[i32]}"),
1355            Ok(TypeRef::Map(
1356                Box::new(TypeRef::StringUtf8),
1357                Box::new(TypeRef::List(Box::new(TypeRef::I32))),
1358            ))
1359        );
1360    }
1361
1362    #[test]
1363    fn parse_type_ref_iterator() {
1364        assert_eq!(
1365            parse_type_ref("iter<i32>"),
1366            Ok(TypeRef::Iterator(Box::new(TypeRef::I32)))
1367        );
1368        assert_eq!(
1369            parse_type_ref("iter<string>"),
1370            Ok(TypeRef::Iterator(Box::new(TypeRef::StringUtf8)))
1371        );
1372        assert_eq!(
1373            parse_type_ref("iter<Contact>"),
1374            Ok(TypeRef::Iterator(Box::new(TypeRef::Named(
1375                "Contact".into()
1376            ))))
1377        );
1378    }
1379
1380    #[test]
1381    fn typeref_iterator_round_trip() {
1382        let ty = TypeRef::Iterator(Box::new(TypeRef::I32));
1383        let json = serde_json::to_string(&ty).unwrap();
1384        assert_eq!(json, r#""iter<i32>""#);
1385        let back: TypeRef = serde_json::from_str(&json).unwrap();
1386        assert_eq!(back, ty);
1387    }
1388
1389    #[test]
1390    fn typeref_iterator_struct_round_trip() {
1391        let ty = TypeRef::Iterator(Box::new(TypeRef::Named("Contact".into())));
1392        let json = serde_json::to_string(&ty).unwrap();
1393        assert_eq!(json, r#""iter<Contact>""#);
1394        let back: TypeRef = serde_json::from_str(&json).unwrap();
1395        assert_eq!(back, ty);
1396    }
1397
1398    #[test]
1399    fn parse_type_ref_borrowed() {
1400        assert_eq!(parse_type_ref("&str"), Ok(TypeRef::BorrowedStr));
1401        assert_eq!(parse_type_ref("&[u8]"), Ok(TypeRef::BorrowedBytes));
1402    }
1403
1404    #[test]
1405    fn typeref_borrowed_round_trip() {
1406        for ty in [TypeRef::BorrowedStr, TypeRef::BorrowedBytes] {
1407            let json = serde_json::to_string(&ty).unwrap();
1408            let back: TypeRef = serde_json::from_str(&json).unwrap();
1409            assert_eq!(back, ty);
1410        }
1411    }
1412
1413    #[test]
1414    fn typeref_borrowed_str_serializes_as_ampersand_str() {
1415        let json = serde_json::to_string(&TypeRef::BorrowedStr).unwrap();
1416        assert_eq!(json, r#""&str""#);
1417    }
1418
1419    #[test]
1420    fn typeref_borrowed_bytes_serializes_as_ampersand_u8() {
1421        let json = serde_json::to_string(&TypeRef::BorrowedBytes).unwrap();
1422        assert_eq!(json, r#""&[u8]""#);
1423    }
1424
1425    #[test]
1426    fn typeref_borrowed_yaml_deser() {
1427        let yaml = r#"
1428version: "0.5.0"
1429modules:
1430  - name: io
1431    functions:
1432      - name: write
1433        params:
1434          - name: data
1435            type: "&str"
1436          - name: raw
1437            type: "&[u8]"
1438"#;
1439        let api: Api = serde_yaml::from_str(yaml).unwrap();
1440        let f = &api.modules[0].functions[0];
1441        assert_eq!(f.params[0].ty, TypeRef::BorrowedStr);
1442        assert_eq!(f.params[1].ty, TypeRef::BorrowedBytes);
1443    }
1444
1445    #[test]
1446    fn parse_typed_handle() {
1447        assert_eq!(
1448            parse_type_ref("handle<Session>"),
1449            Ok(TypeRef::TypedHandle("Session".into()))
1450        );
1451        assert_eq!(parse_type_ref("handle"), Ok(TypeRef::Handle));
1452    }
1453
1454    #[test]
1455    fn generators_field_parses_from_yaml() {
1456        let yaml = r#"
1457version: "0.5.0"
1458modules:
1459  - name: math
1460    functions: []
1461generators:
1462  swift:
1463    module_name: MySwiftModule
1464  android:
1465    package: com.example.app
1466"#;
1467        let api: Api = serde_yaml::from_str(yaml).unwrap();
1468        let generators = api.generators.as_ref().unwrap();
1469        let swift = generators["swift"].as_table().unwrap();
1470        assert_eq!(swift["module_name"].as_str(), Some("MySwiftModule"));
1471        let android = generators["android"].as_table().unwrap();
1472        assert_eq!(android["package"].as_str(), Some("com.example.app"));
1473    }
1474
1475    #[test]
1476    fn generators_defaults_to_none() {
1477        let yaml = r#"
1478version: "0.5.0"
1479modules:
1480  - name: math
1481    functions: []
1482"#;
1483        let api: Api = serde_yaml::from_str(yaml).unwrap();
1484        assert!(api.generators.is_none());
1485    }
1486
1487    #[test]
1488    fn parse_typed_handle_roundtrip() {
1489        let ty = TypeRef::TypedHandle("Connection".into());
1490        let json = serde_json::to_string(&ty).unwrap();
1491        assert_eq!(json, r#""handle<Connection>""#);
1492        let back: TypeRef = serde_json::from_str(&json).unwrap();
1493        assert_eq!(back, ty);
1494    }
1495
1496    #[test]
1497    fn callback_def_round_trip_yaml() {
1498        let yaml = r#"
1499version: "0.5.0"
1500modules:
1501  - name: events
1502    functions: []
1503    callbacks:
1504      - name: on_data
1505        params:
1506          - name: payload
1507            type: string
1508        doc: "Fired when data arrives"
1509"#;
1510        let api: Api = serde_yaml::from_str(yaml).unwrap();
1511        let m = &api.modules[0];
1512        assert_eq!(m.callbacks.len(), 1);
1513        let cb = &m.callbacks[0];
1514        assert_eq!(cb.name, "on_data");
1515        assert_eq!(cb.params.len(), 1);
1516        assert_eq!(cb.params[0].name, "payload");
1517        assert_eq!(cb.params[0].ty, TypeRef::StringUtf8);
1518        assert_eq!(cb.doc.as_deref(), Some("Fired when data arrives"));
1519    }
1520
1521    #[test]
1522    fn listener_def_round_trip_yaml() {
1523        let yaml = r#"
1524version: "0.5.0"
1525modules:
1526  - name: events
1527    functions: []
1528    callbacks:
1529      - name: on_data
1530        params: []
1531    listeners:
1532      - name: data_stream
1533        event_callback: on_data
1534        doc: "Subscribe to data events"
1535"#;
1536        let api: Api = serde_yaml::from_str(yaml).unwrap();
1537        let m = &api.modules[0];
1538        assert_eq!(m.listeners.len(), 1);
1539        let l = &m.listeners[0];
1540        assert_eq!(l.name, "data_stream");
1541        assert_eq!(l.event_callback, "on_data");
1542        assert_eq!(l.doc.as_deref(), Some("Subscribe to data events"));
1543    }
1544
1545    #[test]
1546    fn callbacks_and_listeners_default_to_empty() {
1547        let yaml = r#"
1548version: "0.5.0"
1549modules:
1550  - name: math
1551    functions: []
1552"#;
1553        let api: Api = serde_yaml::from_str(yaml).unwrap();
1554        assert!(api.modules[0].callbacks.is_empty());
1555        assert!(api.modules[0].listeners.is_empty());
1556    }
1557
1558    #[test]
1559    fn callback_def_json_round_trip() {
1560        let cb = CallbackDef {
1561            name: "on_event".to_string(),
1562            params: vec![Param {
1563                name: "data".to_string(),
1564                ty: TypeRef::I32,
1565                mutable: false,
1566                doc: None,
1567            }],
1568            doc: Some("event callback".to_string()),
1569        };
1570        let json = serde_json::to_string(&cb).unwrap();
1571        let back: CallbackDef = serde_json::from_str(&json).unwrap();
1572        assert_eq!(back, cb);
1573    }
1574
1575    #[test]
1576    fn listener_def_json_round_trip() {
1577        let l = ListenerDef {
1578            name: "watcher".to_string(),
1579            event_callback: "on_change".to_string(),
1580            doc: None,
1581        };
1582        let json = serde_json::to_string(&l).unwrap();
1583        let back: ListenerDef = serde_json::from_str(&json).unwrap();
1584        assert_eq!(back, l);
1585    }
1586
1587    #[test]
1588    fn builder_defaults_to_false() {
1589        let yaml = r#"
1590version: "0.5.0"
1591modules:
1592  - name: contacts
1593    functions: []
1594    structs:
1595      - name: Contact
1596        fields:
1597          - name: name
1598            type: string
1599"#;
1600        let api: Api = serde_yaml::from_str(yaml).unwrap();
1601        assert!(!api.modules[0].structs[0].builder);
1602    }
1603
1604    #[test]
1605    fn builder_true_round_trip() {
1606        let yaml = r#"
1607version: "0.5.0"
1608modules:
1609  - name: contacts
1610    functions: []
1611    structs:
1612      - name: Contact
1613        fields:
1614          - name: name
1615            type: string
1616        builder: true
1617"#;
1618        let api: Api = serde_yaml::from_str(yaml).unwrap();
1619        assert!(api.modules[0].structs[0].builder);
1620
1621        let json = serde_json::to_string(&api).unwrap();
1622        let back: Api = serde_json::from_str(&json).unwrap();
1623        assert!(back.modules[0].structs[0].builder);
1624    }
1625
1626    #[test]
1627    fn builder_false_explicit() {
1628        let json = r#"{
1629            "version": "0.5.0",
1630            "modules": [{
1631                "name": "geo",
1632                "functions": [],
1633                "structs": [{
1634                    "name": "Point",
1635                    "fields": [{"name": "x", "type": "f64"}],
1636                    "builder": false
1637                }]
1638            }]
1639        }"#;
1640        let api: Api = serde_json::from_str(json).unwrap();
1641        assert!(!api.modules[0].structs[0].builder);
1642    }
1643
1644    #[test]
1645    fn param_mutable_defaults_to_false() {
1646        let yaml = r#"
1647version: "0.5.0"
1648modules:
1649  - name: io
1650    functions:
1651      - name: write
1652        params:
1653          - name: data
1654            type: string
1655"#;
1656        let api: Api = serde_yaml::from_str(yaml).unwrap();
1657        assert!(!api.modules[0].functions[0].params[0].mutable);
1658    }
1659
1660    #[test]
1661    fn param_mutable_true_round_trip() {
1662        let yaml = r#"
1663version: "0.5.0"
1664modules:
1665  - name: io
1666    functions:
1667      - name: fill_buffer
1668        params:
1669          - name: buf
1670            type: bytes
1671            mutable: true
1672"#;
1673        let api: Api = serde_yaml::from_str(yaml).unwrap();
1674        assert!(api.modules[0].functions[0].params[0].mutable);
1675
1676        let json = serde_json::to_string(&api).unwrap();
1677        let back: Api = serde_json::from_str(&json).unwrap();
1678        assert!(back.modules[0].functions[0].params[0].mutable);
1679    }
1680
1681    #[test]
1682    fn param_mutable_false_explicit() {
1683        let json = r#"{
1684            "version": "0.5.0",
1685            "modules": [{
1686                "name": "io",
1687                "functions": [{
1688                    "name": "read",
1689                    "params": [{"name": "buf", "type": "bytes", "mutable": false}]
1690                }]
1691            }]
1692        }"#;
1693        let api: Api = serde_json::from_str(json).unwrap();
1694        assert!(!api.modules[0].functions[0].params[0].mutable);
1695    }
1696
1697    #[test]
1698    fn deprecated_and_since_default_to_none() {
1699        let yaml = r#"
1700version: "0.5.0"
1701modules:
1702  - name: math
1703    functions:
1704      - name: add
1705        params: []
1706"#;
1707        let api: Api = serde_yaml::from_str(yaml).unwrap();
1708        let f = &api.modules[0].functions[0];
1709        assert_eq!(f.deprecated, None);
1710        assert_eq!(f.since, None);
1711    }
1712
1713    #[test]
1714    fn deprecated_and_since_round_trip() {
1715        let yaml = r#"
1716version: "0.5.0"
1717modules:
1718  - name: math
1719    functions:
1720      - name: add_old
1721        params: []
1722        deprecated: "Use add_v2 instead"
1723        since: "0.1.0"
1724"#;
1725        let api: Api = serde_yaml::from_str(yaml).unwrap();
1726        let f = &api.modules[0].functions[0];
1727        assert_eq!(f.deprecated.as_deref(), Some("Use add_v2 instead"));
1728        assert_eq!(f.since.as_deref(), Some("0.1.0"));
1729
1730        let json = serde_json::to_string(&api).unwrap();
1731        let back: Api = serde_json::from_str(&json).unwrap();
1732        let f2 = &back.modules[0].functions[0];
1733        assert_eq!(f2.deprecated.as_deref(), Some("Use add_v2 instead"));
1734        assert_eq!(f2.since.as_deref(), Some("0.1.0"));
1735    }
1736
1737    #[test]
1738    fn struct_field_default_value_round_trip() {
1739        let yaml = r#"
1740version: "0.5.0"
1741modules:
1742  - name: contacts
1743    functions: []
1744    structs:
1745      - name: Contact
1746        fields:
1747          - name: name
1748            type: string
1749          - name: age
1750            type: i32
1751            default: 0
1752"#;
1753        let api: Api = serde_yaml::from_str(yaml).unwrap();
1754        let fields = &api.modules[0].structs[0].fields;
1755        assert!(fields[0].default.is_none());
1756        assert_eq!(
1757            fields[1].default,
1758            Some(serde_yaml::Value::Number(serde_yaml::Number::from(0)))
1759        );
1760    }
1761
1762    #[test]
1763    fn serialization_omits_defaulted_fields() {
1764        // A minimal API whose every optional/defaulted field is at its
1765        // default must serialize without emitting those fields, so the
1766        // canonical IDL produced by `weaveffi format`/`extract` stays terse.
1767        let api = Api {
1768            version: "0.5.0".into(),
1769            modules: vec![Module {
1770                name: "calc".into(),
1771                functions: vec![Function {
1772                    name: "add".into(),
1773                    params: vec![Param {
1774                        name: "a".into(),
1775                        ty: TypeRef::I32,
1776                        mutable: false,
1777                        doc: None,
1778                    }],
1779                    returns: Some(TypeRef::I32),
1780                    doc: None,
1781                    throws: false,
1782                    r#async: false,
1783                    cancellable: false,
1784                    deprecated: None,
1785                    since: None,
1786                }],
1787                interfaces: vec![],
1788                structs: vec![],
1789                enums: vec![],
1790                callbacks: vec![],
1791                listeners: vec![],
1792                errors: None,
1793                modules: vec![],
1794            }],
1795            generators: None,
1796            package: None,
1797        };
1798        let yaml = serde_yaml::to_string(&api).unwrap();
1799        for needle in [
1800            "generators",
1801            "interfaces",
1802            "structs",
1803            "enums",
1804            "callbacks",
1805            "listeners",
1806            "errors",
1807            "modules:\n", // nested module list (top-level key is "modules")
1808            "doc",
1809            "throws",
1810            "async",
1811            "cancellable",
1812            "deprecated",
1813            "since",
1814            "mutable",
1815            "null",
1816            "[]",
1817            "false",
1818        ] {
1819            // `modules:` appears once at the top level; assert the *nested*
1820            // empty module list under a module is gone by checking it never
1821            // shows an empty sequence.
1822            if needle == "modules:\n" {
1823                continue;
1824            }
1825            assert!(
1826                !yaml.contains(needle),
1827                "default field `{needle}` leaked into canonical YAML:\n{yaml}"
1828            );
1829        }
1830        // Round-trips back to an equal value.
1831        let back: Api = serde_yaml::from_str(&yaml).unwrap();
1832        assert_eq!(back, api);
1833    }
1834
1835    #[test]
1836    fn parse_type_ref_does_not_yield_callback() {
1837        assert_eq!(
1838            parse_type_ref("callback"),
1839            Ok(TypeRef::Named("callback".into()))
1840        );
1841    }
1842
1843    #[test]
1844    fn api_json_schema_derives() {
1845        let schema = schemars::schema_for!(Api);
1846        let json = serde_json::to_value(&schema).unwrap();
1847        assert!(json.get("$schema").is_some());
1848        assert!(json.get("properties").is_some());
1849        assert_eq!(json.get("title").and_then(|v| v.as_str()), Some("Api"));
1850        let defs = json
1851            .get("definitions")
1852            .and_then(|v| v.as_object())
1853            .expect("definitions");
1854        assert!(defs.contains_key("Module"));
1855        assert!(defs.contains_key("Function"));
1856        assert!(defs.contains_key("Param"));
1857        assert!(defs.contains_key("TypeRef"));
1858        assert!(defs.contains_key("InterfaceDef"));
1859        assert!(defs.contains_key("StructDef"));
1860        assert!(defs.contains_key("StructField"));
1861        assert!(defs.contains_key("EnumDef"));
1862        assert!(defs.contains_key("EnumVariant"));
1863        assert!(defs.contains_key("CallbackDef"));
1864        assert!(defs.contains_key("ListenerDef"));
1865        assert!(defs.contains_key("ErrorDomain"));
1866        assert!(defs.contains_key("ErrorCode"));
1867    }
1868
1869    #[test]
1870    fn interface_round_trip_yaml() {
1871        let yaml = r#"
1872version: "0.5.0"
1873modules:
1874  - name: kv
1875    interfaces:
1876      - name: Store
1877        doc: "A key/value store"
1878        constructors:
1879          - name: open
1880            params:
1881              - name: path
1882                type: string
1883            throws: true
1884        methods:
1885          - name: put
1886            params:
1887              - name: key
1888                type: string
1889              - name: value
1890                type: bytes
1891            return: bool
1892            throws: true
1893          - name: count
1894            return: i64
1895        statics:
1896          - name: default_path
1897            return: string
1898"#;
1899        let api: Api = serde_yaml::from_str(yaml).unwrap();
1900        let m = &api.modules[0];
1901        assert!(m.functions.is_empty());
1902        assert_eq!(m.interfaces.len(), 1);
1903        let i = &m.interfaces[0];
1904        assert_eq!(i.name, "Store");
1905        assert_eq!(i.doc.as_deref(), Some("A key/value store"));
1906        assert_eq!(i.constructors.len(), 1);
1907        assert!(i.constructors[0].throws);
1908        assert_eq!(i.constructors[0].returns, None);
1909        assert_eq!(i.methods.len(), 2);
1910        assert!(i.methods[0].throws);
1911        assert!(!i.methods[1].throws);
1912        assert_eq!(i.methods[1].returns, Some(TypeRef::I64));
1913        assert_eq!(i.statics.len(), 1);
1914        assert_eq!(i.statics[0].returns, Some(TypeRef::StringUtf8));
1915
1916        // Round trip preserves the interface block.
1917        let out = serde_yaml::to_string(&api).unwrap();
1918        let back: Api = serde_yaml::from_str(&out).unwrap();
1919        assert_eq!(back, api);
1920    }
1921
1922    #[test]
1923    fn interfaces_default_to_empty() {
1924        let yaml = r#"
1925version: "0.5.0"
1926modules:
1927  - name: math
1928    functions: []
1929"#;
1930        let api: Api = serde_yaml::from_str(yaml).unwrap();
1931        assert!(api.modules[0].interfaces.is_empty());
1932    }
1933
1934    #[test]
1935    fn throws_defaults_to_false_and_round_trips() {
1936        let yaml = r#"
1937version: "0.5.0"
1938modules:
1939  - name: kv
1940    functions:
1941      - name: open
1942        params: []
1943        throws: true
1944      - name: count
1945        params: []
1946"#;
1947        let api: Api = serde_yaml::from_str(yaml).unwrap();
1948        assert!(api.modules[0].functions[0].throws);
1949        assert!(!api.modules[0].functions[1].throws);
1950        let json = serde_json::to_string(&api).unwrap();
1951        let back: Api = serde_json::from_str(&json).unwrap();
1952        assert!(back.modules[0].functions[0].throws);
1953        assert!(!back.modules[0].functions[1].throws);
1954    }
1955
1956    #[test]
1957    fn typeref_interface_serializes_as_name() {
1958        let ty = TypeRef::Interface("Store".to_string());
1959        let json = serde_json::to_string(&ty).unwrap();
1960        assert_eq!(json, r#""Store""#);
1961        // Deserialization yields `Named`; the resolver rewrites it later.
1962        let back: TypeRef = serde_json::from_str(&json).unwrap();
1963        assert_eq!(back, TypeRef::Named("Store".into()));
1964    }
1965
1966    #[test]
1967    fn typeref_json_schema_is_string_with_description() {
1968        let schema = schemars::schema_for!(TypeRef);
1969        let json = serde_json::to_value(&schema).unwrap();
1970        assert_eq!(json.get("type").and_then(|v| v.as_str()), Some("string"));
1971        assert!(json
1972            .get("description")
1973            .and_then(|v| v.as_str())
1974            .is_some_and(|s| s.contains("handle<") && s.contains("iter<")));
1975    }
1976}