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}