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