weaveffi-model 0.25.0

IR, parsers, Rust extractor, validation, and the validated model for WeaveFFI API definitions
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
//! In-memory intermediate representation: the data model a parsed WeaveFFI IDL
//! document becomes.
//!
//! This is the *document* model: [`Api`] is the root and owns a forest of
//! [`Module`]s, each grouping [`Function`]s, [`InterfaceDef`]s,
//! [`CallbackInterfaceDef`]s, [`StructDef`]s, [`EnumDef`]s, and an optional
//! [`ErrorDomain`]. Types are referenced throughout by [`TypeRef`], which
//! (de)serializes as a compact string (`i32`, `[string]`, `{string:i32}`,
//! `Contact?`, and so on) rather than as a tagged object.
//!
//! The document model is deliberately *unresolved*: every user-defined type
//! reference is a [`TypeRef::Named`] carrying the bare name exactly as
//! written. Type names are global across the API, so a name alone identifies
//! a declaration; there are no module-qualified (`a.b.T`) references.
//! Whether that name is a record, an enum, an interface, or a callback
//! interface is decided by the [validator](crate::validate), which lowers the
//! document into the [`Model`](crate::model::Model) generators consume.
//! Keeping the two representations distinct means an IDL document always
//! round-trips losslessly through this module.
//!
//! Package identity and per-generator options are not part of the IDL: they
//! describe how bindings are *shipped*, not what the API *is*, and live in the
//! `weaveffi.toml` project configuration instead.

#[cfg(feature = "idl")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::ty::Prim;

/// The current IR schema version that the parser, validator, and every
/// generator expect.
///
/// Pre-1.0 there is exactly one supported schema version: the current one.
/// Older schema revisions are not accepted and have no automated migration
/// path: update the `version` field and adjust the document to the current
/// schema by hand. Post-1.0, schema bumps will ship with a migration tool and
/// [`SUPPORTED_VERSIONS`] will widen accordingly.
///
/// See [`docs/src/stability.md`](https://github.com/weavefoundry/weaveffi/blob/main/docs/src/stability.md)
/// for the full schema policy and the surfaces covered by SemVer.
pub const CURRENT_SCHEMA_VERSION: &str = "0.11.0";

/// Every IR schema version the current tools accept.
///
/// Pre-1.0 this holds exactly one entry, [`CURRENT_SCHEMA_VERSION`]; a document
/// declaring any other `version` is rejected. Post-1.0 it widens as migrations
/// land, letting the parser accept a range of historical schema revisions.
pub const SUPPORTED_VERSIONS: &[&str] = &[CURRENT_SCHEMA_VERSION];

/// `skip_serializing_if` predicate for `bool` fields that default to `false`.
/// Keeps the canonical IDL emitted by `weaveffi extract` minimal by omitting
/// flags the user never set (e.g. `async: false`, `throws: false`).
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool {
    !*b
}

/// Top-level WeaveFFI API definition: the root of a parsed IDL document.
///
/// This is the value an entire `.yml`, `.json`, or `.toml` IDL file
/// deserializes into (see [`crate::parse`]) and the single input the validator
/// consumes. It pairs the schema version with the module forest.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
    feature = "idl",
    schemars(description = "Top-level WeaveFFI API definition.")
)]
pub struct Api {
    /// IR schema version this document targets (for example `0.11.0`).
    /// Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].
    pub version: String,
    /// Top-level modules that make up the API surface. Each is an independent
    /// namespace; modules may nest further through [`Module::modules`].
    pub modules: Vec<Module>,
}

impl Api {
    /// Call `f` on every type reference written anywhere in the document:
    /// parameters and returns of every function, interface member, and
    /// callback method, and the fields of every record, enum variant, and
    /// error code, in declaration order (outermost references only; use
    /// [`TypeRef::walk`] to reach nested ones).
    pub fn for_each_type_ref<'a>(&'a self, f: &mut dyn FnMut(&'a TypeRef)) {
        fn functions<'a>(fs: &'a [Function], f: &mut dyn FnMut(&'a TypeRef)) {
            for func in fs {
                for p in &func.params {
                    f(&p.ty);
                }
                if let Some(r) = &func.returns {
                    f(r);
                }
            }
        }
        fn fields<'a>(fields: &'a [StructField], f: &mut dyn FnMut(&'a TypeRef)) {
            for field in fields {
                f(&field.ty);
            }
        }
        fn modules<'a>(ms: &'a [Module], f: &mut dyn FnMut(&'a TypeRef)) {
            for m in ms {
                functions(&m.functions, f);
                for i in &m.interfaces {
                    functions(&i.constructors, f);
                    functions(&i.methods, f);
                    functions(&i.statics, f);
                }
                for c in &m.callback_interfaces {
                    functions(&c.methods, f);
                }
                for s in &m.structs {
                    fields(&s.fields, f);
                }
                for v in m.enums.iter().flat_map(|e| &e.variants) {
                    fields(&v.fields, f);
                }
                for c in m.errors.iter().flat_map(|d| &d.codes) {
                    fields(&c.fields, f);
                }
                modules(&m.modules, f);
            }
        }
        modules(&self.modules, f);
    }
}

/// A module: a named namespace grouping related functions, types, callback
/// interfaces, and an error domain.
///
/// Modules are the IDL's unit of organization and map onto each target
/// language's natural grouping construct (a namespace, a submodule, a symbol
/// prefix, and so on). They may nest through [`modules`](Self::modules) to
/// mirror a package hierarchy.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
    feature = "idl",
    schemars(
        description = "A WeaveFFI module: a named group of functions, types, callback interfaces, and errors."
    )
)]
pub struct Module {
    /// Module name, used as a namespace segment and a symbol-prefix component
    /// in generated code (for example `contacts`).
    pub name: String,
    /// Human-readable documentation for the module as a whole, propagated to
    /// the generated namespace, class, or file header. `None` when
    /// undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
    /// Free functions this module exports across the FFI boundary.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub functions: Vec<Function>,
    /// Interface (object) types declared in this module: stateful resources
    /// with constructors, methods, and static functions.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub interfaces: Vec<InterfaceDef>,
    /// Callback interfaces declared in this module: method sets the consumer
    /// implements and the producer calls.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub callback_interfaces: Vec<CallbackInterfaceDef>,
    /// Record (struct) types declared in this module.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub structs: Vec<StructDef>,
    /// Enum types, C-style or algebraic, declared in this module.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub enums: Vec<EnumDef>,
    /// Optional error domain: the named codes this module's fallible functions
    /// report. `None` when the module declares no errors.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub errors: Option<ErrorDomain>,
    /// Nested submodules, forming a tree that mirrors a package hierarchy.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub modules: Vec<Module>,
}

/// A function exported across the FFI boundary.
///
/// Each function becomes a C ABI entry point plus an idiomatic wrapper in every
/// target language. The `async` and `cancellable` flags change how the symbol
/// is lowered (a completion callback, an extra cancel-token parameter) without
/// altering the parameter and return shape declared here. The same shape also
/// describes an interface's constructors, methods, and statics (see
/// [`InterfaceDef`]) and a callback interface's methods (see
/// [`CallbackInterfaceDef`]).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Function {
    /// Function name, lowered to a per-language symbol (for example
    /// `create_contact`).
    pub name: String,
    /// Ordered parameter list; order is preserved in every generated signature.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub params: Vec<Param>,
    /// Return type, or `None` for a function that returns nothing. Serialized
    /// under the IDL key `return`.
    #[serde(rename = "return", default, skip_serializing_if = "Option::is_none")]
    pub returns: Option<TypeRef>,
    /// Human-readable documentation, propagated to the generated bindings' doc
    /// comments. `None` when undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
    /// Whether the function can fail with a domain error. A throwing function
    /// surfaces as `throws`/`raises` in the idiomatic bindings, reporting the
    /// owning module's error domain; a non-throwing function has a plain
    /// signature and treats any error as a producer bug (a trap, not a typed
    /// error). Defaults to `false`.
    #[serde(default, skip_serializing_if = "is_false")]
    pub throws: bool,
    /// Whether the function is asynchronous, lowering to a completion-callback
    /// form rather than a blocking call. Serialized under the IDL key `async`.
    #[serde(default, rename = "async", skip_serializing_if = "is_false")]
    pub r#async: bool,
    /// Whether an async call accepts a cancellation token so callers can request
    /// that an in-flight operation stop early. Defaults to `false`.
    #[serde(default, skip_serializing_if = "is_false")]
    pub cancellable: bool,
    /// Deprecation notice; when set, generators emit a deprecation annotation
    /// carrying this message. `None` means the function is current.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<String>,
}

/// An interface: an opaque, stateful object type with constructors, instance
/// methods, and static functions.
///
/// An interface value lives behind the FFI boundary and crosses it as an
/// opaque pointer; consumers see a class (or the target's closest analogue)
/// whose methods call back into the producer. This is the primary way to model
/// resources with identity and behavior (stores, sessions, connections), in
/// contrast to a [`StructDef`], which models a plain data record with fields.
///
/// Objects are reference counted by the producer. Every interface receives
/// implicit `{tag}_clone` and `{tag}_destroy` symbols; generated wrappers hold
/// one strong reference each and release it through their language's natural
/// disposal hook (`Drop`, `__del__`, `Disposable`, finalizers, `close()`).
/// Because the count lives in the producer, an interface value may appear
/// anywhere a type can: as a parameter, a return, an iterator element, a record
/// field, a list element, or a map value.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
    feature = "idl",
    schemars(
        description = "An interface: an opaque object type with constructors, methods, and statics."
    )
)]
pub struct InterfaceDef {
    /// Interface type name (for example `Store`).
    pub name: String,
    /// Human-readable documentation, propagated to the generated bindings.
    /// `None` when undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
    /// Deprecation notice for the whole type; when set, generators annotate
    /// the emitted class. `None` means the interface is current.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<String>,
    /// Constructors: static functions returning a new instance. A constructor
    /// declares no `return` (the instance is implicit) and may not be `async`.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub constructors: Vec<Function>,
    /// Instance methods. Each lowers with an implicit leading `self` slot
    /// (a pointer to the interface object) before the declared parameters.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub methods: Vec<Function>,
    /// Static functions namespaced under the interface but taking no `self`.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub statics: Vec<Function>,
}

/// A callback interface: a set of methods the **consumer** implements and the
/// **producer** invokes.
///
/// A callback interface is the inverse of an [`InterfaceDef`]. A consumer
/// passes an implementation (a class instance, a closure record, a trait
/// object) wherever the type appears as a parameter; the producer keeps it
/// alive as long as it needs to and calls its methods, possibly from any
/// thread. At the C ABI it lowers to a context pointer plus a vtable of
/// function pointers; see the C ABI contract.
///
/// Methods are synchronous (never `async` or `cancellable`) and return
/// nothing or any type except an iterator or a callback interface; a string,
/// bytes, or buffer return is a run the consumer allocates and the producer
/// adopts. A method may declare `throws` when an error domain is in scope,
/// letting the consumer report that domain's codes. Parameters may use any
/// type other than a callback interface or an iterator. A callback interface
/// may itself be passed, bare or optional (`Listener?`), as a parameter of a
/// function or interface member.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
    feature = "idl",
    schemars(
        description = "A callback interface: a set of methods the consumer implements and the producer calls."
    )
)]
pub struct CallbackInterfaceDef {
    /// Callback interface type name (for example `MessageListener`).
    pub name: String,
    /// Human-readable documentation, propagated to the generated bindings.
    /// `None` when undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
    /// Deprecation notice for the whole type; when set, generators annotate
    /// the emitted type. `None` means the callback interface is current.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<String>,
    /// Methods the consumer implements, in vtable order. Must be non-empty.
    pub methods: Vec<Function>,
}

/// A single parameter of a [`Function`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Param {
    /// Parameter name as it appears in generated signatures (for example `id`).
    pub name: String,
    /// Parameter type. Serialized under the IDL key `type`.
    #[serde(rename = "type")]
    pub ty: TypeRef,
    /// Human-readable documentation for the parameter, propagated to the
    /// generated bindings. `None` when undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
}

/// A reference to a type in the IDL, exactly as written.
///
/// Every user-defined type (record, enum, interface, or callback interface) is
/// a [`Named`](Self::Named) reference at this level; the validator resolves the
/// name against the declarations in scope and lowers it into the resolved kind
/// generators consume. Keeping the document model unresolved is what makes an
/// IDL round-trip losslessly.
///
/// There is no function-typed `TypeRef`. Consumer-implemented behavior is
/// expressed as a [`CallbackInterfaceDef`] referenced by name, and
/// asynchronous producer work uses `async: true`. Together these cover every
/// pattern the FFI boundary needs and keep the type system free of values the
/// C ABI can't represent uniformly.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TypeRef {
    /// A built-in primitive (`i32`, `bool`, `string`, `bytes`, and so on).
    /// Strings and bytes are borrowed when passed in and owned when returned.
    Prim(Prim),
    /// A reference to a user-defined type (record, enum, interface, or
    /// callback interface) by its bare name (`Contact`), exactly as parsed.
    /// Resolution happens in the validator, which rejects a dotted
    /// (module-qualified) name: type names are global.
    Named(String),
    /// Optional value (`T?`): either the inner type or nothing.
    Optional(Box<TypeRef>),
    /// Homogeneous list (`[T]`) of the inner element type.
    List(Box<TypeRef>),
    /// Map (`{K:V}`) from a key type to a value type.
    Map(Box<TypeRef>, Box<TypeRef>),
    /// Lazy sequence (`iter<T>`) of the inner type, lowered to a next/destroy
    /// iterator object rather than a materialized collection.
    Iterator(Box<TypeRef>),
}

/// Parse the IDL's compact type syntax into a [`TypeRef`].
///
/// Handles primitive names (`i32`, `string`, `bytes`, and so on), iterators
/// (`iter<T>`), lists (`[T]`), maps (`{K:V}`), and the optional suffix (`T?`).
/// Any other bare identifier is taken to be a user-defined record, enum,
/// interface, or callback interface name and returned as [`TypeRef::Named`].
///
/// # Errors
///
/// Returns an error message when `s` is empty or only whitespace, when a map
/// type (`{K:V}`) is missing its `:` separator. The same errors propagate up
/// from a malformed inner type of a list, map, optional, or iterator.
pub fn parse_type_ref(s: &str) -> Result<TypeRef, String> {
    let s = s.trim();
    if s.is_empty() {
        return Err("empty type reference".to_string());
    }
    if s.starts_with('[') && s.ends_with(']') {
        let inner = &s[1..s.len() - 1];
        return parse_type_ref(inner).map(|t| match t {
            // `[u8]` canonicalizes to `bytes`: the two encode identically
            // inside value buffers (u32 count + raw bytes), and `bytes` is
            // what Rust producers declare (`Vec<u8>`), so canonicalizing here
            // keeps the top-level ABI slots consistent between an IDL-driven
            // consumer and a macro-driven producer.
            TypeRef::Prim(Prim::U8) => TypeRef::Prim(Prim::Bytes),
            t => TypeRef::List(Box::new(t)),
        });
    }
    if s.starts_with('{') && s.ends_with('}') {
        let inner = &s[1..s.len() - 1];
        let colon =
            map_separator(inner).ok_or_else(|| "map type missing ':' separator".to_string())?;
        let key = parse_type_ref(&inner[..colon])?;
        let val = parse_type_ref(&inner[colon + 1..])?;
        return Ok(TypeRef::Map(Box::new(key), Box::new(val)));
    }
    if let Some(inner) = s.strip_suffix('?') {
        return parse_type_ref(inner).map(|t| TypeRef::Optional(Box::new(t)));
    }
    if let Some(inner) = s
        .strip_prefix("iter<")
        .and_then(|rest| rest.strip_suffix('>'))
    {
        return parse_type_ref(inner).map(|t| TypeRef::Iterator(Box::new(t)));
    }
    Ok(Prim::from_name(s).map_or_else(|| TypeRef::Named(s.to_string()), TypeRef::Prim))
}

/// The byte offset of the top-level `:` separating a map's key from its value,
/// skipping any `:` nested inside a bracketed key such as `{ {a:b}: c }`.
fn map_separator(inner: &str) -> Option<usize> {
    let mut depth = 0usize;
    for (i, ch) in inner.char_indices() {
        match ch {
            '[' | '{' | '<' => depth += 1,
            ']' | '}' | '>' => depth = depth.saturating_sub(1),
            ':' if depth == 0 => return Some(i),
            _ => {}
        }
    }
    None
}

impl TypeRef {
    /// The referenced user-type name for a [`Named`](Self::Named) reference,
    /// or `None` for every other type.
    pub fn user_name(&self) -> Option<&str> {
        match self {
            TypeRef::Named(n) => Some(n),
            _ => None,
        }
    }

    /// Walk this type and every type nested inside it (optional payloads, list
    /// elements, map keys and values, iterator items), outermost first.
    pub fn walk<'a>(&'a self, f: &mut dyn FnMut(&'a TypeRef)) {
        f(self);
        match self {
            TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
                inner.walk(f);
            }
            TypeRef::Map(k, v) => {
                k.walk(f);
                v.walk(f);
            }
            _ => {}
        }
    }
}

fn type_ref_to_string(ty: &TypeRef) -> String {
    match ty {
        TypeRef::Prim(p) => p.snake().to_string(),
        TypeRef::Named(name) => name.clone(),
        TypeRef::Optional(inner) => format!("{}?", type_ref_to_string(inner)),
        TypeRef::List(inner) => format!("[{}]", type_ref_to_string(inner)),
        TypeRef::Map(k, v) => format!("{{{}:{}}}", type_ref_to_string(k), type_ref_to_string(v)),
        TypeRef::Iterator(inner) => format!("iter<{}>", type_ref_to_string(inner)),
    }
}

impl std::fmt::Display for TypeRef {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&type_ref_to_string(self))
    }
}

impl Serialize for TypeRef {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&type_ref_to_string(self))
    }
}

impl<'de> Deserialize<'de> for TypeRef {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        parse_type_ref(&s).map_err(serde::de::Error::custom)
    }
}

/// Manual `JsonSchema` impl because `TypeRef` (de)serializes as a string with
/// custom syntax: primitive names (`i32`, `string`, ...), `iter<{T}>`,
/// `[{T}]`, `{ {K}: {V} }`, `{name}?`, or a user-defined type's bare name.
#[cfg(feature = "idl")]
impl JsonSchema for TypeRef {
    fn schema_name() -> String {
        "TypeRef".to_string()
    }

    fn schema_id() -> std::borrow::Cow<'static, str> {
        std::borrow::Cow::Borrowed(concat!(module_path!(), "::TypeRef"))
    }

    fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        let mut schema = schemars::schema::SchemaObject {
            instance_type: Some(schemars::schema::InstanceType::String.into()),
            ..Default::default()
        };
        let meta = schema.metadata();
        meta.title = Some("TypeRef".to_string());
        meta.description = Some(
            "Reference to a type. Encoded as a string with custom syntax: \
             primitives (`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, \
             `f32`, `f64`, `bool`, `string`, `bytes`), iterators (`iter<{T}>`), \
             lists (`[{T}]`), maps (`{{K:V}}`), optionals (`{T}?`), or the bare \
             (unqualified) name of a user-defined struct, enum, interface, or callback \
             interface."
                .to_string(),
        );
        schema.into()
    }
}

/// An enum type. C-style when every variant is a bare discriminant; an
/// algebraic sum type when any variant declares fields (see
/// [`is_rich`](Self::is_rich)).
///
/// A C-style enum lowers across the C ABI by value as an integer, while an
/// algebraic enum is a value type that crosses the ABI serialized in a value
/// buffer (an `i32` tag followed by the active variant's fields).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
    feature = "idl",
    schemars(
        description = "An enum type. C-style when every variant is a bare discriminant; an algebraic sum type when any variant declares fields."
    )
)]
pub struct EnumDef {
    /// Enum type name (for example `Color`).
    pub name: String,
    /// Human-readable documentation, propagated to the generated bindings.
    /// `None` when undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
    /// Deprecation notice for the whole type; when set, generators annotate
    /// the emitted enum. `None` means the enum is current.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<String>,
    /// The variants in declaration order. Whether any of them carries fields
    /// decides if this is a C-style or an algebraic enum.
    pub variants: Vec<EnumVariant>,
}

impl EnumDef {
    /// `true` when this is an *algebraic* enum (a sum type): at least one
    /// variant carries associated data. Such enums cross the C ABI as value
    /// buffers; a C-style enum (every variant a bare discriminant) lowers by
    /// value as an integer.
    pub fn is_rich(&self) -> bool {
        self.variants.iter().any(|v| !v.fields.is_empty())
    }
}

/// A single variant of an [`EnumDef`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct EnumVariant {
    /// Variant name (for example `Red`).
    pub name: String,
    /// Integer discriminant. Doubles as the C-style enum value and as the
    /// runtime tag that distinguishes the variants of an algebraic enum.
    pub value: i32,
    /// Human-readable documentation, propagated to the generated bindings.
    /// `None` when undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
    /// Associated data carried by this variant. Empty for a unit variant or a
    /// C-style enum; non-empty makes the owning enum a sum type (see
    /// [`EnumDef::is_rich`]).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub fields: Vec<StructField>,
}

/// A struct (record) type with named fields.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
    feature = "idl",
    schemars(description = "A struct (record) type with named fields.")
)]
pub struct StructDef {
    /// Struct type name (for example `Contact`).
    pub name: String,
    /// Human-readable documentation, propagated to the generated bindings.
    /// `None` when undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
    /// Deprecation notice for the whole type; when set, generators annotate
    /// the emitted record. `None` means the struct is current.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deprecated: Option<String>,
    /// The fields in declaration order; order is preserved in the generated
    /// type, its constructors, and its serialized buffer encoding.
    pub fields: Vec<StructField>,
}

/// A named field of a [`StructDef`], or the payload of an algebraic
/// [`EnumVariant`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct StructField {
    /// Field name (for example `email`).
    pub name: String,
    /// Field type. Serialized under the IDL key `type`.
    #[serde(rename = "type")]
    pub ty: TypeRef,
    /// Human-readable documentation, propagated to the generated bindings.
    /// `None` when undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
}

/// A module's error domain: the named set of error codes its fallible functions
/// can report.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ErrorDomain {
    /// Error domain name, used to name the generated error type (for example
    /// `ContactErrors`).
    pub name: String,
    /// The error codes that belong to this domain.
    pub codes: Vec<ErrorCode>,
}

/// A single named error within an [`ErrorDomain`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ErrorCode {
    /// Error code name, lowered to a variant or constant on the generated error
    /// type (for example `not_found`).
    pub name: String,
    /// Stable numeric value carried across the C ABI to identify this error.
    /// Must be positive; non-positive codes are reserved for the runtime.
    pub code: i32,
    /// Default human-readable message describing the error.
    pub message: String,
    /// Human-readable documentation, propagated to the generated bindings.
    /// `None` when undocumented.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub doc: Option<String>,
    /// Structured payload fields this error carries beyond its code and
    /// message, serialized across the ABI in the error's payload buffer.
    /// Empty for a plain code.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub fields: Vec<StructField>,
}

#[cfg(test)]
mod tests {
    use super::*;

    fn rt(s: &str) -> TypeRef {
        let ty = parse_type_ref(s).unwrap_or_else(|e| panic!("{s}: {e}"));
        assert_eq!(ty.to_string(), s, "type syntax must round-trip");
        ty
    }

    #[test]
    fn primitives_round_trip() {
        let cases: &[(&str, TypeRef)] = &[
            ("i8", TypeRef::Prim(Prim::I8)),
            ("i16", TypeRef::Prim(Prim::I16)),
            ("i32", TypeRef::Prim(Prim::I32)),
            ("i64", TypeRef::Prim(Prim::I64)),
            ("u8", TypeRef::Prim(Prim::U8)),
            ("u16", TypeRef::Prim(Prim::U16)),
            ("u32", TypeRef::Prim(Prim::U32)),
            ("u64", TypeRef::Prim(Prim::U64)),
            ("f32", TypeRef::Prim(Prim::F32)),
            ("f64", TypeRef::Prim(Prim::F64)),
            ("bool", TypeRef::Prim(Prim::Bool)),
            ("string", TypeRef::Prim(Prim::String)),
            ("bytes", TypeRef::Prim(Prim::Bytes)),
        ];
        for (s, expected) in cases {
            assert_eq!(&rt(s), expected);
        }
    }

    #[test]
    fn composites_round_trip() {
        assert_eq!(rt("Contact"), TypeRef::Named("Contact".into()));
        // A dotted name parses as written; validation rejects it with a
        // dedicated diagnostic (type names are global).
        assert_eq!(rt("shared.Status"), TypeRef::Named("shared.Status".into()));
        assert_eq!(
            rt("Contact?"),
            TypeRef::Optional(Box::new(TypeRef::Named("Contact".into())))
        );
        assert_eq!(
            rt("[string]"),
            TypeRef::List(Box::new(TypeRef::Prim(Prim::String)))
        );
        assert_eq!(
            rt("{string:i32}"),
            TypeRef::Map(
                Box::new(TypeRef::Prim(Prim::String)),
                Box::new(TypeRef::Prim(Prim::I32))
            )
        );
        assert_eq!(
            rt("iter<Contact>"),
            TypeRef::Iterator(Box::new(TypeRef::Named("Contact".into())))
        );
        assert_eq!(
            rt("[{string:[i32?]}]"),
            TypeRef::List(Box::new(TypeRef::Map(
                Box::new(TypeRef::Prim(Prim::String)),
                Box::new(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
                    TypeRef::Prim(Prim::I32)
                )))))
            )))
        );
    }

    #[test]
    fn u8_list_canonicalizes_to_bytes() {
        assert_eq!(parse_type_ref("[u8]").unwrap(), TypeRef::Prim(Prim::Bytes));
        assert_eq!(
            parse_type_ref("[[u8]]").unwrap(),
            TypeRef::List(Box::new(TypeRef::Prim(Prim::Bytes)))
        );
    }

    #[test]
    fn map_separator_skips_nested_colons() {
        assert_eq!(
            parse_type_ref("{{string:i32}:bool}").unwrap(),
            TypeRef::Map(
                Box::new(TypeRef::Map(
                    Box::new(TypeRef::Prim(Prim::String)),
                    Box::new(TypeRef::Prim(Prim::I32))
                )),
                Box::new(TypeRef::Prim(Prim::Bool))
            )
        );
    }

    #[test]
    fn malformed_types_are_errors() {
        assert!(parse_type_ref("").is_err());
        assert!(parse_type_ref("   ").is_err());
        assert!(parse_type_ref("{string}").is_err());
        assert!(parse_type_ref("[]").is_err());
        assert!(parse_type_ref("?").is_err());
    }

    #[test]
    fn walk_visits_nested_types_outermost_first() {
        let ty = parse_type_ref("{string:[Contact?]}").unwrap();
        let mut seen = Vec::new();
        ty.walk(&mut |t| seen.push(t.to_string()));
        assert_eq!(
            seen,
            [
                "{string:[Contact?]}",
                "string",
                "[Contact?]",
                "Contact?",
                "Contact"
            ]
        );
    }

    #[test]
    fn document_round_trips_through_yaml_and_json() {
        let yaml = r#"
version: "0.11.0"
modules:
  - name: contacts
    doc: Address book
    structs:
      - name: Contact
        deprecated: use Person
        fields:
          - { name: name, type: string }
          - { name: tags, type: "[string]" }
          - { name: book, type: "Book?" }
    enums:
      - name: Shape
        variants:
          - { name: Circle, value: 0, fields: [{ name: r, type: f64 }] }
          - { name: Dot, value: 1 }
    interfaces:
      - name: Book
        constructors:
          - { name: open, params: [{ name: path, type: string }], throws: true }
        methods:
          - { name: find, params: [{ name: q, type: string }], return: "Contact?" }
          - { name: watch, params: [{ name: listener, type: ChangeListener }] }
    callback_interfaces:
      - name: ChangeListener
        methods:
          - { name: on_change, params: [{ name: id, type: i64 }] }
          - { name: should_continue, params: [], return: bool }
    errors:
      name: BookError
      codes:
        - { name: NotFound, code: 1, message: missing, fields: [{ name: id, type: i64 }] }
    functions:
      - name: count
        params: []
        return: i64
        async: true
        cancellable: true
        deprecated: gone
    modules:
      - name: inner
        functions:
          - { name: ping, params: [], return: string }
"#;
        let api: Api = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(api.version, CURRENT_SCHEMA_VERSION);
        let m = &api.modules[0];
        assert_eq!(m.doc.as_deref(), Some("Address book"));
        assert_eq!(m.structs[0].deprecated.as_deref(), Some("use Person"));
        assert!(m.enums[0].is_rich());
        assert_eq!(
            m.interfaces[0].methods[0].returns,
            Some(TypeRef::Optional(Box::new(TypeRef::Named(
                "Contact".into()
            ))))
        );
        assert_eq!(m.callback_interfaces[0].methods.len(), 2);
        assert_eq!(m.errors.as_ref().unwrap().codes[0].fields.len(), 1);
        assert!(m.functions[0].r#async && m.functions[0].cancellable);
        assert_eq!(m.modules[0].functions[0].name, "ping");

        let json = serde_json::to_string(&api).unwrap();
        let back: Api = serde_json::from_str(&json).unwrap();
        assert_eq!(back, api);
        let yaml2 = serde_yaml::to_string(&api).unwrap();
        let back2: Api = serde_yaml::from_str(&yaml2).unwrap();
        assert_eq!(back2, api);
    }

    #[test]
    fn serialization_omits_defaulted_fields() {
        let api = Api {
            version: CURRENT_SCHEMA_VERSION.into(),
            modules: vec![Module {
                name: "m".into(),
                doc: None,
                functions: vec![Function {
                    name: "f".into(),
                    params: vec![],
                    returns: None,
                    doc: None,
                    throws: false,
                    r#async: false,
                    cancellable: false,
                    deprecated: None,
                }],
                interfaces: vec![],
                callback_interfaces: vec![],
                structs: vec![],
                enums: vec![],
                errors: None,
                modules: vec![],
            }],
        };
        let yaml = serde_yaml::to_string(&api).unwrap();
        for noise in ["null", "[]", "false", "async", "throws", "doc", "params"] {
            assert!(
                !yaml.contains(noise),
                "{noise} leaked into canonical form:\n{yaml}"
            );
        }
    }

    #[cfg(feature = "idl")]
    #[test]
    fn json_schema_types_are_strings() {
        let schema = schemars::schema_for!(Api);
        let json = serde_json::to_value(&schema).unwrap();
        let type_ref = &json["definitions"]["TypeRef"];
        assert_eq!(type_ref["type"], "string");
        assert!(json["properties"].get("package").is_none());
        assert!(json["properties"].get("generators").is_none());
        assert!(json["definitions"].get("CallbackInterfaceDef").is_some());
    }
}