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