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}