Skip to main content

weaveffi_model/
ir.rs

1//! In-memory intermediate representation: the data model a parsed WeaveFFI IDL
2//! document becomes.
3//!
4//! This is the *document* model: [`Api`] is the root and owns a forest of
5//! [`Module`]s, each grouping [`Function`]s, [`InterfaceDef`]s,
6//! [`CallbackInterfaceDef`]s, [`StructDef`]s, [`EnumDef`]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//!
11//! The document model is deliberately *unresolved*: every user-defined type
12//! reference is a [`TypeRef::Named`] carrying the name exactly as written.
13//! Whether that name is a record, an enum, an interface, or a callback
14//! interface is decided by the [validator](crate::validate), which lowers the
15//! document into the resolved binding model generators consume. Keeping the
16//! two representations distinct means an IDL document always round-trips
17//! losslessly through this module.
18//!
19//! Package identity and per-generator options are not part of the IDL: they
20//! describe how bindings are *shipped*, not what the API *is*, and live in the
21//! `weaveffi.toml` project configuration instead.
22
23#[cfg(feature = "idl")]
24use schemars::JsonSchema;
25use serde::{Deserialize, Serialize};
26
27/// The current IR schema version that the parser, validator, and every
28/// generator expect.
29///
30/// Pre-1.0 there is exactly one supported schema version: the current one.
31/// Older schema revisions are not accepted and have no automated migration
32/// path: update the `version` field and adjust the document to the current
33/// schema by hand. Post-1.0, schema bumps will ship with a migration tool and
34/// [`SUPPORTED_VERSIONS`] will widen accordingly.
35///
36/// See [`docs/src/stability.md`](https://github.com/weavefoundry/weaveffi/blob/main/docs/src/stability.md)
37/// for the full schema policy and the surfaces covered by SemVer.
38pub const CURRENT_SCHEMA_VERSION: &str = "0.10.0";
39
40/// Every IR schema version the current tools accept.
41///
42/// Pre-1.0 this holds exactly one entry, [`CURRENT_SCHEMA_VERSION`]; a document
43/// declaring any other `version` is rejected. Post-1.0 it widens as migrations
44/// land, letting the parser accept a range of historical schema revisions.
45pub const SUPPORTED_VERSIONS: &[&str] = &[CURRENT_SCHEMA_VERSION];
46
47/// `skip_serializing_if` predicate for `bool` fields that default to `false`.
48/// Keeps the canonical IDL emitted by `weaveffi extract` minimal by omitting
49/// flags the user never set (e.g. `async: false`, `throws: false`).
50#[allow(clippy::trivially_copy_pass_by_ref)]
51fn is_false(b: &bool) -> bool {
52    !*b
53}
54
55/// Top-level WeaveFFI API definition: the root of a parsed IDL document.
56///
57/// This is the value an entire `.yml`, `.json`, or `.toml` IDL file
58/// deserializes into (see [`crate::parse`]) and the single input the validator
59/// consumes. It pairs the schema version with the module forest.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[cfg_attr(feature = "idl", derive(JsonSchema))]
62#[serde(deny_unknown_fields)]
63#[cfg_attr(
64    feature = "idl",
65    schemars(description = "Top-level WeaveFFI API definition.")
66)]
67pub struct Api {
68    /// IR schema version this document targets (for example `0.10.0`).
69    /// Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].
70    pub version: String,
71    /// Top-level modules that make up the API surface. Each is an independent
72    /// namespace; modules may nest further through [`Module::modules`].
73    pub modules: Vec<Module>,
74}
75
76/// A module: a named namespace grouping related functions, types, callback
77/// interfaces, and an error domain.
78///
79/// Modules are the IDL's unit of organization and map onto each target
80/// language's natural grouping construct (a namespace, a submodule, a symbol
81/// prefix, and so on). They may nest through [`modules`](Self::modules) to
82/// mirror a package hierarchy.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[cfg_attr(feature = "idl", derive(JsonSchema))]
85#[serde(deny_unknown_fields)]
86#[cfg_attr(
87    feature = "idl",
88    schemars(
89        description = "A WeaveFFI module: a named group of functions, types, callback interfaces, and errors."
90    )
91)]
92pub struct Module {
93    /// Module name, used as a namespace segment and a symbol-prefix component
94    /// in generated code (for example `contacts`).
95    pub name: String,
96    /// Human-readable documentation for the module as a whole, propagated to
97    /// the generated namespace, class, or file header. `None` when
98    /// undocumented.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub doc: Option<String>,
101    /// Free functions this module exports across the FFI boundary.
102    #[serde(default, skip_serializing_if = "Vec::is_empty")]
103    pub functions: Vec<Function>,
104    /// Interface (object) types declared in this module: stateful resources
105    /// with constructors, methods, and static functions.
106    #[serde(default, skip_serializing_if = "Vec::is_empty")]
107    pub interfaces: Vec<InterfaceDef>,
108    /// Callback interfaces declared in this module: method sets the consumer
109    /// implements and the producer calls.
110    #[serde(default, skip_serializing_if = "Vec::is_empty")]
111    pub callback_interfaces: Vec<CallbackInterfaceDef>,
112    /// Record (struct) types declared in this module.
113    #[serde(default, skip_serializing_if = "Vec::is_empty")]
114    pub structs: Vec<StructDef>,
115    /// Enum types, C-style or algebraic, declared in this module.
116    #[serde(default, skip_serializing_if = "Vec::is_empty")]
117    pub enums: Vec<EnumDef>,
118    /// Optional error domain: the named codes this module's fallible functions
119    /// report. `None` when the module declares no errors.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub errors: Option<ErrorDomain>,
122    /// Nested submodules, forming a tree that mirrors a package hierarchy.
123    #[serde(default, skip_serializing_if = "Vec::is_empty")]
124    pub modules: Vec<Module>,
125}
126
127/// A function exported across the FFI boundary.
128///
129/// Each function becomes a C ABI entry point plus an idiomatic wrapper in every
130/// target language. The `async` and `cancellable` flags change how the symbol
131/// is lowered (a completion callback, an extra cancel-token parameter) without
132/// altering the parameter and return shape declared here. The same shape also
133/// describes an interface's constructors, methods, and statics (see
134/// [`InterfaceDef`]) and a callback interface's methods (see
135/// [`CallbackInterfaceDef`]).
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[cfg_attr(feature = "idl", derive(JsonSchema))]
138#[serde(deny_unknown_fields)]
139pub struct Function {
140    /// Function name, lowered to a per-language symbol (for example
141    /// `create_contact`).
142    pub name: String,
143    /// Ordered parameter list; order is preserved in every generated signature.
144    #[serde(default, skip_serializing_if = "Vec::is_empty")]
145    pub params: Vec<Param>,
146    /// Return type, or `None` for a function that returns nothing. Serialized
147    /// under the IDL key `return`.
148    #[serde(rename = "return", default, skip_serializing_if = "Option::is_none")]
149    pub returns: Option<TypeRef>,
150    /// Human-readable documentation, propagated to the generated bindings' doc
151    /// comments. `None` when undocumented.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub doc: Option<String>,
154    /// Whether the function can fail with a domain error. A throwing function
155    /// surfaces as `throws`/`raises` in the idiomatic bindings, reporting the
156    /// owning module's error domain; a non-throwing function has a plain
157    /// signature and treats any error as a producer bug (a trap, not a typed
158    /// error). Defaults to `false`.
159    #[serde(default, skip_serializing_if = "is_false")]
160    pub throws: bool,
161    /// Whether the function is asynchronous, lowering to a completion-callback
162    /// form rather than a blocking call. Serialized under the IDL key `async`.
163    #[serde(default, rename = "async", skip_serializing_if = "is_false")]
164    pub r#async: bool,
165    /// Whether an async call accepts a cancellation token so callers can request
166    /// that an in-flight operation stop early. Defaults to `false`.
167    #[serde(default, skip_serializing_if = "is_false")]
168    pub cancellable: bool,
169    /// Deprecation notice; when set, generators emit a deprecation annotation
170    /// carrying this message. `None` means the function is current.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub deprecated: Option<String>,
173}
174
175/// An interface: an opaque, stateful object type with constructors, instance
176/// methods, and static functions.
177///
178/// An interface value lives behind the FFI boundary and crosses it as an
179/// opaque pointer; consumers see a class (or the target's closest analogue)
180/// whose methods call back into the producer. This is the primary way to model
181/// resources with identity and behavior (stores, sessions, connections), in
182/// contrast to a [`StructDef`], which models a plain data record with fields.
183///
184/// Objects are reference counted by the producer. Every interface receives
185/// implicit `{tag}_clone` and `{tag}_destroy` symbols; generated wrappers hold
186/// one strong reference each and release it through their language's natural
187/// disposal hook (`Drop`, `__del__`, `Disposable`, finalizers, `close()`).
188/// Because the count lives in the producer, an interface value may appear
189/// anywhere a type can: as a parameter, a return, an iterator element, a record
190/// field, a list element, or a map value.
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192#[cfg_attr(feature = "idl", derive(JsonSchema))]
193#[serde(deny_unknown_fields)]
194#[cfg_attr(
195    feature = "idl",
196    schemars(
197        description = "An interface: an opaque object type with constructors, methods, and statics."
198    )
199)]
200pub struct InterfaceDef {
201    /// Interface type name (for example `Store`).
202    pub name: String,
203    /// Human-readable documentation, propagated to the generated bindings.
204    /// `None` when undocumented.
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub doc: Option<String>,
207    /// Deprecation notice for the whole type; when set, generators annotate
208    /// the emitted class. `None` means the interface is current.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub deprecated: Option<String>,
211    /// Constructors: static functions returning a new instance. A constructor
212    /// declares no `return` (the instance is implicit) and may not be `async`.
213    #[serde(default, skip_serializing_if = "Vec::is_empty")]
214    pub constructors: Vec<Function>,
215    /// Instance methods. Each lowers with an implicit leading `self` slot
216    /// (a pointer to the interface object) before the declared parameters.
217    #[serde(default, skip_serializing_if = "Vec::is_empty")]
218    pub methods: Vec<Function>,
219    /// Static functions namespaced under the interface but taking no `self`.
220    #[serde(default, skip_serializing_if = "Vec::is_empty")]
221    pub statics: Vec<Function>,
222}
223
224/// A callback interface: a set of methods the **consumer** implements and the
225/// **producer** invokes.
226///
227/// A callback interface is the inverse of an [`InterfaceDef`]. A consumer
228/// passes an implementation (a class instance, a closure record, a trait
229/// object) wherever the type appears as a parameter; the producer keeps it
230/// alive as long as it needs to and calls its methods, possibly from any
231/// thread. At the C ABI it lowers to a context pointer plus a vtable of
232/// function pointers; see the C ABI contract.
233///
234/// Methods are synchronous, can't declare `throws`, `async`, or `cancellable`,
235/// and return either nothing or a value in the direct family (integers,
236/// floats, `bool`, or a C-style enum). Parameters may use any type other than
237/// another callback interface or an iterator.
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239#[cfg_attr(feature = "idl", derive(JsonSchema))]
240#[serde(deny_unknown_fields)]
241#[cfg_attr(
242    feature = "idl",
243    schemars(
244        description = "A callback interface: a set of methods the consumer implements and the producer calls."
245    )
246)]
247pub struct CallbackInterfaceDef {
248    /// Callback interface type name (for example `MessageListener`).
249    pub name: String,
250    /// Human-readable documentation, propagated to the generated bindings.
251    /// `None` when undocumented.
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub doc: Option<String>,
254    /// Deprecation notice for the whole type; when set, generators annotate
255    /// the emitted type. `None` means the callback interface is current.
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub deprecated: Option<String>,
258    /// Methods the consumer implements, in vtable order. Must be non-empty.
259    pub methods: Vec<Function>,
260}
261
262/// A single parameter of a [`Function`].
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
264#[cfg_attr(feature = "idl", derive(JsonSchema))]
265#[serde(deny_unknown_fields)]
266pub struct Param {
267    /// Parameter name as it appears in generated signatures (for example `id`).
268    pub name: String,
269    /// Parameter type. Serialized under the IDL key `type`.
270    #[serde(rename = "type")]
271    pub ty: TypeRef,
272    /// Human-readable documentation for the parameter, propagated to the
273    /// generated bindings. `None` when undocumented.
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub doc: Option<String>,
276}
277
278/// A reference to a type in the IDL, exactly as written.
279///
280/// Every user-defined type (record, enum, interface, or callback interface) is
281/// a [`Named`](Self::Named) reference at this level; the validator resolves the
282/// name against the declarations in scope and lowers it into the resolved kind
283/// generators consume. Keeping the document model unresolved is what makes an
284/// IDL round-trip losslessly.
285///
286/// There is no function-typed `TypeRef`. Consumer-implemented behavior is
287/// expressed as a [`CallbackInterfaceDef`] referenced by name, and
288/// asynchronous producer work uses `async: true`. Together these cover every
289/// pattern the FFI boundary needs and keep the type system free of values the
290/// C ABI can't represent uniformly.
291#[derive(Debug, Clone, PartialEq, Eq, Hash)]
292pub enum TypeRef {
293    /// Signed 8-bit integer (`i8`).
294    I8,
295    /// Signed 16-bit integer (`i16`).
296    I16,
297    /// Signed 32-bit integer (`i32`).
298    I32,
299    /// Signed 64-bit integer (`i64`).
300    I64,
301    /// Unsigned 8-bit integer (`u8`).
302    U8,
303    /// Unsigned 16-bit integer (`u16`).
304    U16,
305    /// Unsigned 32-bit integer (`u32`).
306    U32,
307    /// Unsigned 64-bit integer (`u64`).
308    U64,
309    /// 32-bit IEEE 754 floating-point number (`f32`).
310    F32,
311    /// 64-bit IEEE 754 floating-point number (`f64`).
312    F64,
313    /// Boolean (`bool`).
314    Bool,
315    /// UTF-8 string (`string`). Borrowed when passed in, owned when returned.
316    StringUtf8,
317    /// Byte buffer (`bytes`). Borrowed when passed in, owned when returned.
318    Bytes,
319    /// A reference to a user-defined type (record, enum, interface, or callback
320    /// interface) by its bare or dot-qualified name (`Contact`,
321    /// `shared.Status`), exactly as parsed. Resolution happens in the
322    /// validator, not here.
323    Named(String),
324    /// Optional value (`T?`): either the inner type or nothing.
325    Optional(Box<TypeRef>),
326    /// Homogeneous list (`[T]`) of the inner element type.
327    List(Box<TypeRef>),
328    /// Map (`{K:V}`) from a key type to a value type.
329    Map(Box<TypeRef>, Box<TypeRef>),
330    /// Lazy sequence (`iter<T>`) of the inner type, lowered to a next/destroy
331    /// iterator object rather than a materialized collection.
332    Iterator(Box<TypeRef>),
333}
334
335/// Parse the IDL's compact type syntax into a [`TypeRef`].
336///
337/// Handles primitive names (`i32`, `string`, `bytes`, and so on), iterators
338/// (`iter<T>`), lists (`[T]`), maps (`{K:V}`), and the optional suffix (`T?`).
339/// Any other bare identifier is taken to be a user-defined record, enum,
340/// interface, or callback interface name and returned as [`TypeRef::Named`].
341///
342/// # Errors
343///
344/// Returns an error message when `s` is empty or only whitespace, when a map
345/// type (`{K:V}`) is missing its `:` separator. The same errors propagate up
346/// from a malformed inner type of a list, map, optional, or iterator.
347pub fn parse_type_ref(s: &str) -> Result<TypeRef, String> {
348    let s = s.trim();
349    if s.is_empty() {
350        return Err("empty type reference".to_string());
351    }
352    if s.starts_with('[') && s.ends_with(']') {
353        let inner = &s[1..s.len() - 1];
354        return parse_type_ref(inner).map(|t| match t {
355            // `[u8]` canonicalizes to `bytes`: the two encode identically
356            // inside value buffers (u32 count + raw bytes), and `bytes` is
357            // what Rust producers declare (`Vec<u8>`), so canonicalizing here
358            // keeps the top-level ABI slots consistent between an IDL-driven
359            // consumer and a macro-driven producer.
360            TypeRef::U8 => TypeRef::Bytes,
361            t => TypeRef::List(Box::new(t)),
362        });
363    }
364    if s.starts_with('{') && s.ends_with('}') {
365        let inner = &s[1..s.len() - 1];
366        let colon =
367            map_separator(inner).ok_or_else(|| "map type missing ':' separator".to_string())?;
368        let key = parse_type_ref(&inner[..colon])?;
369        let val = parse_type_ref(&inner[colon + 1..])?;
370        return Ok(TypeRef::Map(Box::new(key), Box::new(val)));
371    }
372    if let Some(inner) = s.strip_suffix('?') {
373        return parse_type_ref(inner).map(|t| TypeRef::Optional(Box::new(t)));
374    }
375    if let Some(inner) = s
376        .strip_prefix("iter<")
377        .and_then(|rest| rest.strip_suffix('>'))
378    {
379        return parse_type_ref(inner).map(|t| TypeRef::Iterator(Box::new(t)));
380    }
381    match s {
382        "i8" => Ok(TypeRef::I8),
383        "i16" => Ok(TypeRef::I16),
384        "i32" => Ok(TypeRef::I32),
385        "i64" => Ok(TypeRef::I64),
386        "u8" => Ok(TypeRef::U8),
387        "u16" => Ok(TypeRef::U16),
388        "u32" => Ok(TypeRef::U32),
389        "u64" => Ok(TypeRef::U64),
390        "f32" => Ok(TypeRef::F32),
391        "f64" => Ok(TypeRef::F64),
392        "bool" => Ok(TypeRef::Bool),
393        "string" => Ok(TypeRef::StringUtf8),
394        "bytes" => Ok(TypeRef::Bytes),
395        name => Ok(TypeRef::Named(name.to_string())),
396    }
397}
398
399/// The byte offset of the top-level `:` separating a map's key from its value,
400/// skipping any `:` nested inside a bracketed key such as `{ {a:b}: c }`.
401fn map_separator(inner: &str) -> Option<usize> {
402    let mut depth = 0usize;
403    for (i, ch) in inner.char_indices() {
404        match ch {
405            '[' | '{' | '<' => depth += 1,
406            ']' | '}' | '>' => depth = depth.saturating_sub(1),
407            ':' if depth == 0 => return Some(i),
408            _ => {}
409        }
410    }
411    None
412}
413
414impl TypeRef {
415    /// The referenced user-type name for a [`Named`](Self::Named) reference,
416    /// or `None` for every other type.
417    pub fn user_name(&self) -> Option<&str> {
418        match self {
419            TypeRef::Named(n) => Some(n),
420            _ => None,
421        }
422    }
423
424    /// Walk this type and every type nested inside it (optional payloads, list
425    /// elements, map keys and values, iterator items), outermost first.
426    pub fn walk<'a>(&'a self, f: &mut dyn FnMut(&'a TypeRef)) {
427        f(self);
428        match self {
429            TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
430                inner.walk(f);
431            }
432            TypeRef::Map(k, v) => {
433                k.walk(f);
434                v.walk(f);
435            }
436            _ => {}
437        }
438    }
439}
440
441fn type_ref_to_string(ty: &TypeRef) -> String {
442    match ty {
443        TypeRef::I8 => "i8".to_string(),
444        TypeRef::I16 => "i16".to_string(),
445        TypeRef::I32 => "i32".to_string(),
446        TypeRef::I64 => "i64".to_string(),
447        TypeRef::U8 => "u8".to_string(),
448        TypeRef::U16 => "u16".to_string(),
449        TypeRef::U32 => "u32".to_string(),
450        TypeRef::U64 => "u64".to_string(),
451        TypeRef::F32 => "f32".to_string(),
452        TypeRef::F64 => "f64".to_string(),
453        TypeRef::Bool => "bool".to_string(),
454        TypeRef::StringUtf8 => "string".to_string(),
455        TypeRef::Bytes => "bytes".to_string(),
456        TypeRef::Named(name) => name.clone(),
457        TypeRef::Optional(inner) => format!("{}?", type_ref_to_string(inner)),
458        TypeRef::List(inner) => format!("[{}]", type_ref_to_string(inner)),
459        TypeRef::Map(k, v) => format!("{{{}:{}}}", type_ref_to_string(k), type_ref_to_string(v)),
460        TypeRef::Iterator(inner) => format!("iter<{}>", type_ref_to_string(inner)),
461    }
462}
463
464impl std::fmt::Display for TypeRef {
465    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466        f.write_str(&type_ref_to_string(self))
467    }
468}
469
470impl Serialize for TypeRef {
471    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
472    where
473        S: serde::Serializer,
474    {
475        serializer.serialize_str(&type_ref_to_string(self))
476    }
477}
478
479impl<'de> Deserialize<'de> for TypeRef {
480    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
481    where
482        D: serde::Deserializer<'de>,
483    {
484        let s = String::deserialize(deserializer)?;
485        parse_type_ref(&s).map_err(serde::de::Error::custom)
486    }
487}
488
489/// Manual `JsonSchema` impl because `TypeRef` (de)serializes as a string with
490/// custom syntax: primitive names (`i32`, `string`, ...), `iter<{T}>`,
491/// `[{T}]`, `{ {K}: {V} }`, `{name}?`, or any user-defined type name.
492#[cfg(feature = "idl")]
493impl JsonSchema for TypeRef {
494    fn schema_name() -> String {
495        "TypeRef".to_string()
496    }
497
498    fn schema_id() -> std::borrow::Cow<'static, str> {
499        std::borrow::Cow::Borrowed(concat!(module_path!(), "::TypeRef"))
500    }
501
502    fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
503        let mut schema = schemars::schema::SchemaObject {
504            instance_type: Some(schemars::schema::InstanceType::String.into()),
505            ..Default::default()
506        };
507        let meta = schema.metadata();
508        meta.title = Some("TypeRef".to_string());
509        meta.description = Some(
510            "Reference to a type. Encoded as a string with custom syntax: \
511             primitives (`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, \
512             `f32`, `f64`, `bool`, `string`, `bytes`), iterators (`iter<{T}>`), \
513             lists (`[{T}]`), maps (`{{K:V}}`), optionals (`{T}?`), or any \
514             user-defined struct, enum, interface, or callback interface name."
515                .to_string(),
516        );
517        schema.into()
518    }
519}
520
521/// An enum type. C-style when every variant is a bare discriminant; an
522/// algebraic sum type when any variant declares fields (see
523/// [`is_rich`](Self::is_rich)).
524///
525/// A C-style enum lowers across the C ABI by value as an integer, while an
526/// algebraic enum is a value type that crosses the ABI serialized in a value
527/// buffer (an `i32` tag followed by the active variant's fields).
528#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
529#[cfg_attr(feature = "idl", derive(JsonSchema))]
530#[serde(deny_unknown_fields)]
531#[cfg_attr(
532    feature = "idl",
533    schemars(
534        description = "An enum type. C-style when every variant is a bare discriminant; an algebraic sum type when any variant declares fields."
535    )
536)]
537pub struct EnumDef {
538    /// Enum type name (for example `Color`).
539    pub name: String,
540    /// Human-readable documentation, propagated to the generated bindings.
541    /// `None` when undocumented.
542    #[serde(default, skip_serializing_if = "Option::is_none")]
543    pub doc: Option<String>,
544    /// Deprecation notice for the whole type; when set, generators annotate
545    /// the emitted enum. `None` means the enum is current.
546    #[serde(default, skip_serializing_if = "Option::is_none")]
547    pub deprecated: Option<String>,
548    /// The variants in declaration order. Whether any of them carries fields
549    /// decides if this is a C-style or an algebraic enum.
550    pub variants: Vec<EnumVariant>,
551}
552
553impl EnumDef {
554    /// `true` when this is an *algebraic* enum (a sum type): at least one
555    /// variant carries associated data. Such enums cross the C ABI as value
556    /// buffers; a C-style enum (every variant a bare discriminant) lowers by
557    /// value as an integer.
558    pub fn is_rich(&self) -> bool {
559        self.variants.iter().any(|v| !v.fields.is_empty())
560    }
561}
562
563/// A single variant of an [`EnumDef`].
564#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
565#[cfg_attr(feature = "idl", derive(JsonSchema))]
566#[serde(deny_unknown_fields)]
567pub struct EnumVariant {
568    /// Variant name (for example `Red`).
569    pub name: String,
570    /// Integer discriminant. Doubles as the C-style enum value and as the
571    /// runtime tag that distinguishes the variants of an algebraic enum.
572    pub value: i32,
573    /// Human-readable documentation, propagated to the generated bindings.
574    /// `None` when undocumented.
575    #[serde(default, skip_serializing_if = "Option::is_none")]
576    pub doc: Option<String>,
577    /// Associated data carried by this variant. Empty for a unit variant or a
578    /// C-style enum; non-empty makes the owning enum a sum type (see
579    /// [`EnumDef::is_rich`]).
580    #[serde(default, skip_serializing_if = "Vec::is_empty")]
581    pub fields: Vec<StructField>,
582}
583
584/// A struct (record) type with named fields.
585#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
586#[cfg_attr(feature = "idl", derive(JsonSchema))]
587#[serde(deny_unknown_fields)]
588#[cfg_attr(
589    feature = "idl",
590    schemars(description = "A struct (record) type with named fields.")
591)]
592pub struct StructDef {
593    /// Struct type name (for example `Contact`).
594    pub name: String,
595    /// Human-readable documentation, propagated to the generated bindings.
596    /// `None` when undocumented.
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    pub doc: Option<String>,
599    /// Deprecation notice for the whole type; when set, generators annotate
600    /// the emitted record. `None` means the struct is current.
601    #[serde(default, skip_serializing_if = "Option::is_none")]
602    pub deprecated: Option<String>,
603    /// The fields in declaration order; order is preserved in the generated
604    /// type, its constructors, and its serialized buffer encoding.
605    pub fields: Vec<StructField>,
606}
607
608/// A named field of a [`StructDef`], or the payload of an algebraic
609/// [`EnumVariant`].
610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
611#[cfg_attr(feature = "idl", derive(JsonSchema))]
612#[serde(deny_unknown_fields)]
613pub struct StructField {
614    /// Field name (for example `email`).
615    pub name: String,
616    /// Field type. Serialized under the IDL key `type`.
617    #[serde(rename = "type")]
618    pub ty: TypeRef,
619    /// Human-readable documentation, propagated to the generated bindings.
620    /// `None` when undocumented.
621    #[serde(default, skip_serializing_if = "Option::is_none")]
622    pub doc: Option<String>,
623}
624
625/// A module's error domain: the named set of error codes its fallible functions
626/// can report.
627#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
628#[cfg_attr(feature = "idl", derive(JsonSchema))]
629#[serde(deny_unknown_fields)]
630pub struct ErrorDomain {
631    /// Error domain name, used to name the generated error type (for example
632    /// `ContactErrors`).
633    pub name: String,
634    /// The error codes that belong to this domain.
635    pub codes: Vec<ErrorCode>,
636}
637
638/// A single named error within an [`ErrorDomain`].
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
640#[cfg_attr(feature = "idl", derive(JsonSchema))]
641#[serde(deny_unknown_fields)]
642pub struct ErrorCode {
643    /// Error code name, lowered to a variant or constant on the generated error
644    /// type (for example `not_found`).
645    pub name: String,
646    /// Stable numeric value carried across the C ABI to identify this error.
647    /// Must be positive; non-positive codes are reserved for the runtime.
648    pub code: i32,
649    /// Default human-readable message describing the error.
650    pub message: String,
651    /// Human-readable documentation, propagated to the generated bindings.
652    /// `None` when undocumented.
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub doc: Option<String>,
655    /// Structured payload fields this error carries beyond its code and
656    /// message, serialized across the ABI in the error's payload buffer.
657    /// Empty for a plain code.
658    #[serde(default, skip_serializing_if = "Vec::is_empty")]
659    pub fields: Vec<StructField>,
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665
666    fn rt(s: &str) -> TypeRef {
667        let ty = parse_type_ref(s).unwrap_or_else(|e| panic!("{s}: {e}"));
668        assert_eq!(ty.to_string(), s, "type syntax must round-trip");
669        ty
670    }
671
672    #[test]
673    fn primitives_round_trip() {
674        let cases: &[(&str, TypeRef)] = &[
675            ("i8", TypeRef::I8),
676            ("i16", TypeRef::I16),
677            ("i32", TypeRef::I32),
678            ("i64", TypeRef::I64),
679            ("u8", TypeRef::U8),
680            ("u16", TypeRef::U16),
681            ("u32", TypeRef::U32),
682            ("u64", TypeRef::U64),
683            ("f32", TypeRef::F32),
684            ("f64", TypeRef::F64),
685            ("bool", TypeRef::Bool),
686            ("string", TypeRef::StringUtf8),
687            ("bytes", TypeRef::Bytes),
688        ];
689        for (s, expected) in cases {
690            assert_eq!(&rt(s), expected);
691        }
692    }
693
694    #[test]
695    fn composites_round_trip() {
696        assert_eq!(rt("Contact"), TypeRef::Named("Contact".into()));
697        assert_eq!(rt("shared.Status"), TypeRef::Named("shared.Status".into()));
698        assert_eq!(
699            rt("Contact?"),
700            TypeRef::Optional(Box::new(TypeRef::Named("Contact".into())))
701        );
702        assert_eq!(rt("[string]"), TypeRef::List(Box::new(TypeRef::StringUtf8)));
703        assert_eq!(
704            rt("{string:i32}"),
705            TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32))
706        );
707        assert_eq!(
708            rt("iter<Contact>"),
709            TypeRef::Iterator(Box::new(TypeRef::Named("Contact".into())))
710        );
711        assert_eq!(
712            rt("[{string:[i32?]}]"),
713            TypeRef::List(Box::new(TypeRef::Map(
714                Box::new(TypeRef::StringUtf8),
715                Box::new(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
716                    TypeRef::I32
717                )))))
718            )))
719        );
720    }
721
722    #[test]
723    fn u8_list_canonicalizes_to_bytes() {
724        assert_eq!(parse_type_ref("[u8]").unwrap(), TypeRef::Bytes);
725        assert_eq!(
726            parse_type_ref("[[u8]]").unwrap(),
727            TypeRef::List(Box::new(TypeRef::Bytes))
728        );
729    }
730
731    #[test]
732    fn map_separator_skips_nested_colons() {
733        assert_eq!(
734            parse_type_ref("{{string:i32}:bool}").unwrap(),
735            TypeRef::Map(
736                Box::new(TypeRef::Map(
737                    Box::new(TypeRef::StringUtf8),
738                    Box::new(TypeRef::I32)
739                )),
740                Box::new(TypeRef::Bool)
741            )
742        );
743    }
744
745    #[test]
746    fn malformed_types_are_errors() {
747        assert!(parse_type_ref("").is_err());
748        assert!(parse_type_ref("   ").is_err());
749        assert!(parse_type_ref("{string}").is_err());
750        assert!(parse_type_ref("[]").is_err());
751        assert!(parse_type_ref("?").is_err());
752    }
753
754    #[test]
755    fn walk_visits_nested_types_outermost_first() {
756        let ty = parse_type_ref("{string:[Contact?]}").unwrap();
757        let mut seen = Vec::new();
758        ty.walk(&mut |t| seen.push(t.to_string()));
759        assert_eq!(
760            seen,
761            [
762                "{string:[Contact?]}",
763                "string",
764                "[Contact?]",
765                "Contact?",
766                "Contact"
767            ]
768        );
769    }
770
771    #[test]
772    fn document_round_trips_through_yaml_and_json() {
773        let yaml = r#"
774version: "0.10.0"
775modules:
776  - name: contacts
777    doc: Address book
778    structs:
779      - name: Contact
780        deprecated: use Person
781        fields:
782          - { name: name, type: string }
783          - { name: tags, type: "[string]" }
784          - { name: book, type: "Book?" }
785    enums:
786      - name: Shape
787        variants:
788          - { name: Circle, value: 0, fields: [{ name: r, type: f64 }] }
789          - { name: Dot, value: 1 }
790    interfaces:
791      - name: Book
792        constructors:
793          - { name: open, params: [{ name: path, type: string }], throws: true }
794        methods:
795          - { name: find, params: [{ name: q, type: string }], return: "Contact?" }
796          - { name: watch, params: [{ name: listener, type: ChangeListener }] }
797    callback_interfaces:
798      - name: ChangeListener
799        methods:
800          - { name: on_change, params: [{ name: id, type: i64 }] }
801          - { name: should_continue, params: [], return: bool }
802    errors:
803      name: BookError
804      codes:
805        - { name: NotFound, code: 1, message: missing, fields: [{ name: id, type: i64 }] }
806    functions:
807      - name: count
808        params: []
809        return: i64
810        async: true
811        cancellable: true
812        deprecated: gone
813    modules:
814      - name: inner
815        functions:
816          - { name: ping, params: [], return: string }
817"#;
818        let api: Api = serde_yaml::from_str(yaml).unwrap();
819        assert_eq!(api.version, CURRENT_SCHEMA_VERSION);
820        let m = &api.modules[0];
821        assert_eq!(m.doc.as_deref(), Some("Address book"));
822        assert_eq!(m.structs[0].deprecated.as_deref(), Some("use Person"));
823        assert!(m.enums[0].is_rich());
824        assert_eq!(
825            m.interfaces[0].methods[0].returns,
826            Some(TypeRef::Optional(Box::new(TypeRef::Named(
827                "Contact".into()
828            ))))
829        );
830        assert_eq!(m.callback_interfaces[0].methods.len(), 2);
831        assert_eq!(m.errors.as_ref().unwrap().codes[0].fields.len(), 1);
832        assert!(m.functions[0].r#async && m.functions[0].cancellable);
833        assert_eq!(m.modules[0].functions[0].name, "ping");
834
835        let json = serde_json::to_string(&api).unwrap();
836        let back: Api = serde_json::from_str(&json).unwrap();
837        assert_eq!(back, api);
838        let yaml2 = serde_yaml::to_string(&api).unwrap();
839        let back2: Api = serde_yaml::from_str(&yaml2).unwrap();
840        assert_eq!(back2, api);
841    }
842
843    #[test]
844    fn serialization_omits_defaulted_fields() {
845        let api = Api {
846            version: CURRENT_SCHEMA_VERSION.into(),
847            modules: vec![Module {
848                name: "m".into(),
849                doc: None,
850                functions: vec![Function {
851                    name: "f".into(),
852                    params: vec![],
853                    returns: None,
854                    doc: None,
855                    throws: false,
856                    r#async: false,
857                    cancellable: false,
858                    deprecated: None,
859                }],
860                interfaces: vec![],
861                callback_interfaces: vec![],
862                structs: vec![],
863                enums: vec![],
864                errors: None,
865                modules: vec![],
866            }],
867        };
868        let yaml = serde_yaml::to_string(&api).unwrap();
869        for noise in ["null", "[]", "false", "async", "throws", "doc", "params"] {
870            assert!(
871                !yaml.contains(noise),
872                "{noise} leaked into canonical form:\n{yaml}"
873            );
874        }
875    }
876
877    #[cfg(feature = "idl")]
878    #[test]
879    fn json_schema_types_are_strings() {
880        let schema = schemars::schema_for!(Api);
881        let json = serde_json::to_value(&schema).unwrap();
882        let type_ref = &json["definitions"]["TypeRef"];
883        assert_eq!(type_ref["type"], "string");
884        assert!(json["properties"].get("package").is_none());
885        assert!(json["properties"].get("generators").is_none());
886        assert!(json["definitions"].get("CallbackInterfaceDef").is_some());
887    }
888}