Skip to main content

weaveffi_ir/
ir.rs

1use std::collections::BTreeMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// The current IR schema version that the parser, validator, and every
7/// generator expect.
8///
9/// Schema bumps are tied to this crate's minor version: each minor release
10/// of `weaveffi-ir` may introduce at most one new schema version, and
11/// [`SUPPORTED_VERSIONS`] always lists every version the upgrader can
12/// read. The CLI's `weaveffi upgrade` subcommand guarantees an N-1 → N
13/// migration path between consecutive versions.
14///
15/// See [`docs/src/stability.md`](https://github.com/weavefoundry/weaveffi/blob/main/docs/src/stability.md)
16/// for the full schema-migration policy and the surfaces covered by
17/// SemVer.
18pub const CURRENT_SCHEMA_VERSION: &str = "0.3.0";
19
20pub const SUPPORTED_VERSIONS: &[&str] = &["0.1.0", "0.2.0", "0.3.0"];
21
22/// `skip_serializing_if` predicate for `bool` fields that default to `false`.
23/// Keeps the canonical IDL emitted by `weaveffi format`/`extract` minimal by
24/// omitting flags the user never set (e.g. `async: false`, `mutable: false`).
25#[allow(clippy::trivially_copy_pass_by_ref)]
26fn is_false(b: &bool) -> bool {
27    !*b
28}
29
30/// `Eq` is omitted because `toml::Value` contains `f64`.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
32#[schemars(description = "Top-level WeaveFFI API definition.")]
33pub struct Api {
34    pub version: String,
35    /// Package identity used to name, version, and describe every generated
36    /// consumer package (npm, PyPI, gem, NuGet, pub.dev, SwiftPM, Gradle, Go).
37    /// When omitted, generators fall back to the IDL file stem and version
38    /// `0.1.0`, but publishable artifacts should always set this explicitly.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub package: Option<Package>,
41    pub modules: Vec<Module>,
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    #[schemars(with = "Option<BTreeMap<String, serde_json::Value>>")]
44    pub generators: Option<BTreeMap<String, toml::Value>>,
45}
46
47/// Package identity for the generated consumer artifacts.
48///
49/// A single `package:` block in the IDL is the source of truth for the
50/// name, version, and metadata stamped into every ecosystem manifest
51/// (`package.json`, `pyproject.toml`, `*.gemspec`, `*.csproj`, `pubspec.yaml`,
52/// `Package.swift`, `build.gradle`, `go.mod`). This is what makes the
53/// generated packages standalone and publishable rather than all sharing a
54/// placeholder identity.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
56#[schemars(description = "Package identity for the generated consumer artifacts.")]
57pub struct Package {
58    /// Canonical package name (e.g. `kvstore`). Per-target name overrides in
59    /// `generators:` (such as `python.package_name`) still take precedence.
60    pub name: String,
61    /// Semantic version stamped into every manifest (e.g. `1.2.0`).
62    pub version: String,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub description: Option<String>,
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub license: Option<String>,
67    #[serde(default, skip_serializing_if = "Vec::is_empty")]
68    pub authors: Vec<String>,
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub homepage: Option<String>,
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub repository: Option<String>,
73}
74
75/// `Eq` is omitted because `StructField::default` contains `serde_yaml::Value`.
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
77#[schemars(
78    description = "A WeaveFFI module: a named group of functions, types, callbacks, listeners, and errors."
79)]
80pub struct Module {
81    pub name: String,
82    pub functions: Vec<Function>,
83    #[serde(default, skip_serializing_if = "Vec::is_empty")]
84    pub structs: Vec<StructDef>,
85    #[serde(default, skip_serializing_if = "Vec::is_empty")]
86    pub enums: Vec<EnumDef>,
87    #[serde(default, skip_serializing_if = "Vec::is_empty")]
88    pub callbacks: Vec<CallbackDef>,
89    #[serde(default, skip_serializing_if = "Vec::is_empty")]
90    pub listeners: Vec<ListenerDef>,
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub errors: Option<ErrorDomain>,
93    #[serde(default, skip_serializing_if = "Vec::is_empty")]
94    pub modules: Vec<Module>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
98pub struct Function {
99    pub name: String,
100    pub params: Vec<Param>,
101    #[serde(rename = "return", default, skip_serializing_if = "Option::is_none")]
102    pub returns: Option<TypeRef>,
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub doc: Option<String>,
105    #[serde(default, rename = "async", skip_serializing_if = "is_false")]
106    pub r#async: bool,
107    #[serde(default, skip_serializing_if = "is_false")]
108    pub cancellable: bool,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub deprecated: Option<String>,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub since: Option<String>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
116pub struct Param {
117    pub name: String,
118    #[serde(rename = "type")]
119    pub ty: TypeRef,
120    #[serde(default, skip_serializing_if = "is_false")]
121    pub mutable: bool,
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub doc: Option<String>,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
127pub struct CallbackDef {
128    pub name: String,
129    pub params: Vec<Param>,
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub doc: Option<String>,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
135pub struct ListenerDef {
136    pub name: String,
137    pub event_callback: String,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub doc: Option<String>,
140}
141
142/// A reference to a type in the IDL.
143///
144/// Callback-style behavior is **not** expressed as a `TypeRef` variant.
145/// Instead, callbacks and listeners are declared at the module level via
146/// `Module.callbacks` (see [`CallbackDef`]) and `Module.listeners` (see
147/// [`ListenerDef`]), and asynchronous functions use `async: true`. These
148/// primitives cover every pattern the FFI boundary needs to support, and
149/// keep the type system free of function-typed values that the C ABI
150/// cannot represent uniformly.
151#[derive(Debug, Clone, PartialEq, Eq, Hash)]
152pub enum TypeRef {
153    I32,
154    U32,
155    I64,
156    F64,
157    Bool,
158    StringUtf8,
159    Bytes,
160    Handle,
161    TypedHandle(String),
162    Struct(String),
163    Enum(String),
164    BorrowedStr,
165    BorrowedBytes,
166    Optional(Box<TypeRef>),
167    List(Box<TypeRef>),
168    Map(Box<TypeRef>, Box<TypeRef>),
169    Iterator(Box<TypeRef>),
170}
171
172pub fn parse_type_ref(s: &str) -> Result<TypeRef, String> {
173    let s = s.trim();
174    if s.is_empty() {
175        return Err("empty type reference".to_string());
176    }
177    if s.starts_with('[') && s.ends_with(']') {
178        let inner = &s[1..s.len() - 1];
179        return parse_type_ref(inner).map(|t| TypeRef::List(Box::new(t)));
180    }
181    if s.starts_with('{') && s.ends_with('}') {
182        let inner = &s[1..s.len() - 1];
183        let colon = inner
184            .find(':')
185            .ok_or_else(|| "map type missing ':' separator".to_string())?;
186        let key = parse_type_ref(&inner[..colon])?;
187        let val = parse_type_ref(&inner[colon + 1..])?;
188        return Ok(TypeRef::Map(Box::new(key), Box::new(val)));
189    }
190    if let Some(inner) = s.strip_suffix('?') {
191        return parse_type_ref(inner).map(|t| TypeRef::Optional(Box::new(t)));
192    }
193    if let Some(inner) = s
194        .strip_prefix("handle<")
195        .and_then(|rest| rest.strip_suffix('>'))
196    {
197        return Ok(TypeRef::TypedHandle(inner.into()));
198    }
199    if let Some(inner) = s
200        .strip_prefix("iter<")
201        .and_then(|rest| rest.strip_suffix('>'))
202    {
203        return parse_type_ref(inner).map(|t| TypeRef::Iterator(Box::new(t)));
204    }
205    match s {
206        "i32" => Ok(TypeRef::I32),
207        "u32" => Ok(TypeRef::U32),
208        "i64" => Ok(TypeRef::I64),
209        "f64" => Ok(TypeRef::F64),
210        "bool" => Ok(TypeRef::Bool),
211        "string" => Ok(TypeRef::StringUtf8),
212        "bytes" => Ok(TypeRef::Bytes),
213        "handle" => Ok(TypeRef::Handle),
214        "&str" => Ok(TypeRef::BorrowedStr),
215        "&[u8]" => Ok(TypeRef::BorrowedBytes),
216        name => Ok(TypeRef::Struct(name.to_string())),
217    }
218}
219
220fn type_ref_to_string(ty: &TypeRef) -> String {
221    match ty {
222        TypeRef::I32 => "i32".to_string(),
223        TypeRef::U32 => "u32".to_string(),
224        TypeRef::I64 => "i64".to_string(),
225        TypeRef::F64 => "f64".to_string(),
226        TypeRef::Bool => "bool".to_string(),
227        TypeRef::StringUtf8 => "string".to_string(),
228        TypeRef::Bytes => "bytes".to_string(),
229        TypeRef::BorrowedStr => "&str".to_string(),
230        TypeRef::BorrowedBytes => "&[u8]".to_string(),
231        TypeRef::Handle => "handle".to_string(),
232        TypeRef::TypedHandle(name) => format!("handle<{name}>"),
233        TypeRef::Struct(name) | TypeRef::Enum(name) => name.clone(),
234        TypeRef::Optional(inner) => format!("{}?", type_ref_to_string(inner)),
235        TypeRef::List(inner) => format!("[{}]", type_ref_to_string(inner)),
236        TypeRef::Map(k, v) => format!("{{{}:{}}}", type_ref_to_string(k), type_ref_to_string(v)),
237        TypeRef::Iterator(inner) => format!("iter<{}>", type_ref_to_string(inner)),
238    }
239}
240
241impl Serialize for TypeRef {
242    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
243    where
244        S: serde::Serializer,
245    {
246        serializer.serialize_str(&type_ref_to_string(self))
247    }
248}
249
250impl<'de> Deserialize<'de> for TypeRef {
251    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
252    where
253        D: serde::Deserializer<'de>,
254    {
255        let s = String::deserialize(deserializer)?;
256        parse_type_ref(&s).map_err(serde::de::Error::custom)
257    }
258}
259
260/// Manual `JsonSchema` impl because `TypeRef` (de)serializes as a string with
261/// custom syntax: primitive names (`i32`, `string`, ...), `&str`, `&[u8]`,
262/// `handle<{name}>`, `iter<{T}>`, `[{T}]`, `{ {K}: {V} }`, `{name}?`, or any
263/// user-defined struct/enum name.
264impl JsonSchema for TypeRef {
265    fn schema_name() -> String {
266        "TypeRef".to_string()
267    }
268
269    fn schema_id() -> std::borrow::Cow<'static, str> {
270        std::borrow::Cow::Borrowed(concat!(module_path!(), "::TypeRef"))
271    }
272
273    fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
274        let mut schema = schemars::schema::SchemaObject {
275            instance_type: Some(schemars::schema::InstanceType::String.into()),
276            ..Default::default()
277        };
278        let meta = schema.metadata();
279        meta.title = Some("TypeRef".to_string());
280        meta.description = Some(
281            "Reference to a type. Encoded as a string with custom syntax: \
282             primitives (`i32`, `u32`, `i64`, `f64`, `bool`, `string`, `bytes`, `handle`), \
283             borrowed types (`&str`, `&[u8]`), typed handles (`handle<{name}>`), \
284             iterators (`iter<{T}>`), lists (`[{T}]`), maps (`{{K:V}}`), \
285             optionals (`{T}?`), or any user-defined struct/enum name."
286                .to_string(),
287        );
288        schema.into()
289    }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
293pub struct EnumDef {
294    pub name: String,
295    #[serde(default, skip_serializing_if = "Option::is_none")]
296    pub doc: Option<String>,
297    pub variants: Vec<EnumVariant>,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
301pub struct EnumVariant {
302    pub name: String,
303    pub value: i32,
304    #[serde(default, skip_serializing_if = "Option::is_none")]
305    pub doc: Option<String>,
306}
307
308/// `Eq` is omitted because `StructField::default` contains `serde_yaml::Value`.
309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
310#[schemars(description = "A struct (record) type with named fields.")]
311pub struct StructDef {
312    pub name: String,
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub doc: Option<String>,
315    pub fields: Vec<StructField>,
316    #[serde(default, skip_serializing_if = "is_false")]
317    pub builder: bool,
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
321pub struct StructField {
322    pub name: String,
323    #[serde(rename = "type")]
324    pub ty: TypeRef,
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub doc: Option<String>,
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    #[schemars(with = "Option<serde_json::Value>")]
329    pub default: Option<serde_yaml::Value>,
330}
331
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
333pub struct ErrorDomain {
334    pub name: String,
335    pub codes: Vec<ErrorCode>,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
339pub struct ErrorCode {
340    pub name: String,
341    pub code: i32,
342    pub message: String,
343    #[serde(default, skip_serializing_if = "Option::is_none")]
344    pub doc: Option<String>,
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn struct_def_round_trip_yaml() {
353        let yaml = r#"
354version: "0.1.0"
355modules:
356  - name: geometry
357    functions: []
358    structs:
359      - name: Point
360        doc: "A 2D point"
361        fields:
362          - name: x
363            type: f64
364          - name: "y"
365            type: f64
366            doc: "Y coordinate"
367"#;
368        let api: Api = serde_yaml::from_str(yaml).unwrap();
369        let m = &api.modules[0];
370        assert_eq!(m.structs.len(), 1);
371        let s = &m.structs[0];
372        assert_eq!(s.name, "Point");
373        assert_eq!(s.doc.as_deref(), Some("A 2D point"));
374        assert_eq!(s.fields.len(), 2);
375        assert_eq!(s.fields[0].name, "x");
376        assert_eq!(s.fields[0].ty, TypeRef::F64);
377        assert_eq!(s.fields[0].doc, None);
378        assert_eq!(s.fields[1].name, "y");
379        assert_eq!(s.fields[1].doc.as_deref(), Some("Y coordinate"));
380    }
381
382    #[test]
383    fn struct_def_round_trip_json() {
384        let json = r#"{
385            "version": "0.1.0",
386            "modules": [{
387                "name": "geo",
388                "functions": [],
389                "structs": [{
390                    "name": "Rect",
391                    "fields": [
392                        {"name": "width", "type": "i32"},
393                        {"name": "height", "type": "i32"}
394                    ]
395                }]
396            }]
397        }"#;
398        let api: Api = serde_json::from_str(json).unwrap();
399        let s = &api.modules[0].structs[0];
400        assert_eq!(s.name, "Rect");
401        assert_eq!(s.doc, None);
402        assert_eq!(s.fields[0].ty, TypeRef::I32);
403    }
404
405    #[test]
406    fn structs_default_to_empty() {
407        let yaml = r#"
408version: "0.1.0"
409modules:
410  - name: math
411    functions: []
412"#;
413        let api: Api = serde_yaml::from_str(yaml).unwrap();
414        assert!(api.modules[0].structs.is_empty());
415    }
416
417    #[test]
418    fn package_block_round_trips_yaml() {
419        let yaml = r#"
420version: "0.3.0"
421package:
422  name: kvstore
423  version: 1.2.0
424  description: "An embedded key/value store"
425  license: MIT
426  authors:
427    - "Ada Lovelace <ada@example.com>"
428  homepage: "https://example.com/kvstore"
429  repository: "https://github.com/example/kvstore"
430modules:
431  - name: kv
432    functions: []
433"#;
434        let api: Api = serde_yaml::from_str(yaml).unwrap();
435        let pkg = api.package.as_ref().expect("package should parse");
436        assert_eq!(pkg.name, "kvstore");
437        assert_eq!(pkg.version, "1.2.0");
438        assert_eq!(
439            pkg.description.as_deref(),
440            Some("An embedded key/value store")
441        );
442        assert_eq!(pkg.license.as_deref(), Some("MIT"));
443        assert_eq!(pkg.authors, vec!["Ada Lovelace <ada@example.com>"]);
444        assert_eq!(pkg.homepage.as_deref(), Some("https://example.com/kvstore"));
445        assert_eq!(
446            pkg.repository.as_deref(),
447            Some("https://github.com/example/kvstore")
448        );
449
450        // Re-serialize and confirm the block survives the round trip.
451        let out = serde_yaml::to_string(&api).unwrap();
452        assert!(out.contains("name: kvstore"));
453        assert!(out.contains("version: 1.2.0"));
454    }
455
456    #[test]
457    fn package_is_optional() {
458        let yaml = r#"
459version: "0.3.0"
460modules:
461  - name: math
462    functions: []
463"#;
464        let api: Api = serde_yaml::from_str(yaml).unwrap();
465        assert!(api.package.is_none());
466        // Absent package must not appear in the canonical serialization.
467        let out = serde_yaml::to_string(&api).unwrap();
468        assert!(!out.contains("package:"));
469    }
470
471    #[test]
472    fn package_minimal_requires_name_and_version() {
473        let yaml = r#"
474version: "0.3.0"
475package:
476  name: tiny
477  version: 0.0.1
478modules: []
479"#;
480        let api: Api = serde_yaml::from_str(yaml).unwrap();
481        let pkg = api.package.as_ref().unwrap();
482        assert_eq!(pkg.name, "tiny");
483        assert_eq!(pkg.version, "0.0.1");
484        assert!(pkg.description.is_none());
485        assert!(pkg.authors.is_empty());
486    }
487
488    #[test]
489    fn typeref_struct_variant_serializes() {
490        let ty = TypeRef::Struct("Point".to_string());
491        let json = serde_json::to_string(&ty).unwrap();
492        assert_eq!(json, r#""Point""#);
493        let back: TypeRef = serde_json::from_str(&json).unwrap();
494        assert_eq!(back, ty);
495    }
496
497    #[test]
498    fn struct_field_with_struct_type() {
499        let field = StructField {
500            name: "origin".to_string(),
501            ty: TypeRef::Struct("Point".to_string()),
502            doc: None,
503            default: None,
504        };
505        let json = serde_json::to_string(&field).unwrap();
506        let back: StructField = serde_json::from_str(&json).unwrap();
507        assert_eq!(back, field);
508    }
509
510    #[test]
511    fn typeref_is_not_copy() {
512        let a = TypeRef::Struct("Foo".to_string());
513        let b = a.clone();
514        assert_eq!(a, b);
515    }
516
517    #[test]
518    fn enum_def_round_trip_yaml() {
519        let yaml = r#"
520version: "0.1.0"
521modules:
522  - name: graphics
523    functions: []
524    enums:
525      - name: Color
526        doc: "Primary colors"
527        variants:
528          - name: Red
529            value: 0
530          - name: Green
531            value: 1
532            doc: "The color green"
533          - name: Blue
534            value: 2
535"#;
536        let api: Api = serde_yaml::from_str(yaml).unwrap();
537        let m = &api.modules[0];
538        assert_eq!(m.enums.len(), 1);
539        let e = &m.enums[0];
540        assert_eq!(e.name, "Color");
541        assert_eq!(e.doc.as_deref(), Some("Primary colors"));
542        assert_eq!(e.variants.len(), 3);
543        assert_eq!(e.variants[0].name, "Red");
544        assert_eq!(e.variants[0].value, 0);
545        assert_eq!(e.variants[0].doc, None);
546        assert_eq!(e.variants[1].name, "Green");
547        assert_eq!(e.variants[1].value, 1);
548        assert_eq!(e.variants[1].doc.as_deref(), Some("The color green"));
549        assert_eq!(e.variants[2].name, "Blue");
550        assert_eq!(e.variants[2].value, 2);
551    }
552
553    #[test]
554    fn enum_def_round_trip_json() {
555        let json = r#"{
556            "version": "0.1.0",
557            "modules": [{
558                "name": "status",
559                "functions": [],
560                "enums": [{
561                    "name": "Status",
562                    "variants": [
563                        {"name": "Ok", "value": 0},
564                        {"name": "Error", "value": 1}
565                    ]
566                }]
567            }]
568        }"#;
569        let api: Api = serde_json::from_str(json).unwrap();
570        let e = &api.modules[0].enums[0];
571        assert_eq!(e.name, "Status");
572        assert_eq!(e.doc, None);
573        assert_eq!(e.variants.len(), 2);
574        assert_eq!(e.variants[1].value, 1);
575    }
576
577    #[test]
578    fn enums_default_to_empty() {
579        let yaml = r#"
580version: "0.1.0"
581modules:
582  - name: math
583    functions: []
584"#;
585        let api: Api = serde_yaml::from_str(yaml).unwrap();
586        assert!(api.modules[0].enums.is_empty());
587    }
588
589    #[test]
590    fn typeref_enum_variant_serializes_as_name() {
591        let ty = TypeRef::Enum("Color".to_string());
592        let json = serde_json::to_string(&ty).unwrap();
593        assert_eq!(json, r#""Color""#);
594    }
595
596    #[test]
597    fn enum_def_clone_and_eq() {
598        let e = EnumDef {
599            name: "Direction".to_string(),
600            doc: Some("Cardinal directions".to_string()),
601            variants: vec![
602                EnumVariant {
603                    name: "North".to_string(),
604                    value: 0,
605                    doc: None,
606                },
607                EnumVariant {
608                    name: "South".to_string(),
609                    value: 1,
610                    doc: None,
611                },
612            ],
613        };
614        assert_eq!(e, e.clone());
615    }
616
617    #[test]
618    fn struct_def_clone_and_eq() {
619        let s = StructDef {
620            name: "Color".to_string(),
621            doc: Some("RGB color".to_string()),
622            fields: vec![
623                StructField {
624                    name: "r".to_string(),
625                    ty: TypeRef::U32,
626                    doc: None,
627                    default: None,
628                },
629                StructField {
630                    name: "g".to_string(),
631                    ty: TypeRef::U32,
632                    doc: None,
633                    default: None,
634                },
635                StructField {
636                    name: "b".to_string(),
637                    ty: TypeRef::U32,
638                    doc: None,
639                    default: None,
640                },
641            ],
642            builder: false,
643        };
644        assert_eq!(s, s.clone());
645    }
646
647    #[test]
648    fn parse_type_ref_primitives() {
649        assert_eq!(parse_type_ref("i32"), Ok(TypeRef::I32));
650        assert_eq!(parse_type_ref("u32"), Ok(TypeRef::U32));
651        assert_eq!(parse_type_ref("i64"), Ok(TypeRef::I64));
652        assert_eq!(parse_type_ref("f64"), Ok(TypeRef::F64));
653        assert_eq!(parse_type_ref("bool"), Ok(TypeRef::Bool));
654        assert_eq!(parse_type_ref("string"), Ok(TypeRef::StringUtf8));
655        assert_eq!(parse_type_ref("bytes"), Ok(TypeRef::Bytes));
656        assert_eq!(parse_type_ref("handle"), Ok(TypeRef::Handle));
657    }
658
659    #[test]
660    fn parse_type_ref_struct() {
661        assert_eq!(
662            parse_type_ref("Contact"),
663            Ok(TypeRef::Struct("Contact".into()))
664        );
665        assert_eq!(
666            parse_type_ref("MyWidget"),
667            Ok(TypeRef::Struct("MyWidget".into()))
668        );
669    }
670
671    #[test]
672    fn parse_type_ref_optional() {
673        assert_eq!(
674            parse_type_ref("string?"),
675            Ok(TypeRef::Optional(Box::new(TypeRef::StringUtf8)))
676        );
677        assert_eq!(
678            parse_type_ref("i32?"),
679            Ok(TypeRef::Optional(Box::new(TypeRef::I32)))
680        );
681        assert_eq!(
682            parse_type_ref("Contact?"),
683            Ok(TypeRef::Optional(Box::new(TypeRef::Struct(
684                "Contact".into()
685            ))))
686        );
687    }
688
689    #[test]
690    fn parse_type_ref_list() {
691        assert_eq!(
692            parse_type_ref("[i32]"),
693            Ok(TypeRef::List(Box::new(TypeRef::I32)))
694        );
695        assert_eq!(
696            parse_type_ref("[string]"),
697            Ok(TypeRef::List(Box::new(TypeRef::StringUtf8)))
698        );
699        assert_eq!(
700            parse_type_ref("[Contact]"),
701            Ok(TypeRef::List(Box::new(TypeRef::Struct("Contact".into()))))
702        );
703    }
704
705    #[test]
706    fn parse_type_ref_nested() {
707        assert_eq!(
708            parse_type_ref("[i32?]"),
709            Ok(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
710                TypeRef::I32
711            )))))
712        );
713        assert_eq!(
714            parse_type_ref("[Contact]?"),
715            Ok(TypeRef::Optional(Box::new(TypeRef::List(Box::new(
716                TypeRef::Struct("Contact".into())
717            )))))
718        );
719    }
720
721    #[test]
722    fn parse_type_ref_empty_is_error() {
723        assert!(parse_type_ref("").is_err());
724        assert!(parse_type_ref("  ").is_err());
725    }
726
727    #[test]
728    fn typeref_primitive_round_trips() {
729        for ty in [
730            TypeRef::I32,
731            TypeRef::U32,
732            TypeRef::I64,
733            TypeRef::F64,
734            TypeRef::Bool,
735            TypeRef::StringUtf8,
736            TypeRef::Bytes,
737            TypeRef::Handle,
738        ] {
739            let json = serde_json::to_string(&ty).unwrap();
740            let back: TypeRef = serde_json::from_str(&json).unwrap();
741            assert_eq!(back, ty);
742        }
743    }
744
745    #[test]
746    fn typeref_optional_round_trip() {
747        let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
748        let json = serde_json::to_string(&ty).unwrap();
749        assert_eq!(json, r#""string?""#);
750        let back: TypeRef = serde_json::from_str(&json).unwrap();
751        assert_eq!(back, ty);
752    }
753
754    #[test]
755    fn typeref_list_round_trip() {
756        let ty = TypeRef::List(Box::new(TypeRef::I32));
757        let json = serde_json::to_string(&ty).unwrap();
758        assert_eq!(json, r#""[i32]""#);
759        let back: TypeRef = serde_json::from_str(&json).unwrap();
760        assert_eq!(back, ty);
761    }
762
763    #[test]
764    fn typeref_optional_struct_round_trip() {
765        let ty = TypeRef::Optional(Box::new(TypeRef::Struct("Contact".into())));
766        let json = serde_json::to_string(&ty).unwrap();
767        assert_eq!(json, r#""Contact?""#);
768        let back: TypeRef = serde_json::from_str(&json).unwrap();
769        assert_eq!(back, ty);
770    }
771
772    #[test]
773    fn typeref_list_struct_round_trip() {
774        let ty = TypeRef::List(Box::new(TypeRef::Struct("Contact".into())));
775        let json = serde_json::to_string(&ty).unwrap();
776        assert_eq!(json, r#""[Contact]""#);
777        let back: TypeRef = serde_json::from_str(&json).unwrap();
778        assert_eq!(back, ty);
779    }
780
781    #[test]
782    fn typeref_optional_yaml_deser() {
783        let yaml = r#"
784version: "0.1.0"
785modules:
786  - name: contacts
787    functions:
788      - name: find
789        params:
790          - name: id
791            type: i32
792        return: "Contact?"
793"#;
794        let api: Api = serde_yaml::from_str(yaml).unwrap();
795        let f = &api.modules[0].functions[0];
796        assert_eq!(
797            f.returns,
798            Some(TypeRef::Optional(Box::new(TypeRef::Struct(
799                "Contact".into()
800            ))))
801        );
802    }
803
804    #[test]
805    fn typeref_list_yaml_deser() {
806        let yaml = r#"
807version: "0.1.0"
808modules:
809  - name: contacts
810    functions:
811      - name: list_all
812        params: []
813        return: "[Contact]"
814"#;
815        let api: Api = serde_yaml::from_str(yaml).unwrap();
816        let f = &api.modules[0].functions[0];
817        assert_eq!(
818            f.returns,
819            Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into()))))
820        );
821    }
822
823    #[test]
824    fn typeref_hash_works_with_box_variants() {
825        use std::collections::HashSet;
826        let mut set = HashSet::new();
827        set.insert(TypeRef::I32);
828        set.insert(TypeRef::Optional(Box::new(TypeRef::I32)));
829        set.insert(TypeRef::List(Box::new(TypeRef::I32)));
830        set.insert(TypeRef::Optional(Box::new(TypeRef::Struct("Foo".into()))));
831        set.insert(TypeRef::Map(
832            Box::new(TypeRef::StringUtf8),
833            Box::new(TypeRef::I32),
834        ));
835        assert_eq!(set.len(), 5);
836    }
837
838    #[test]
839    fn parse_type_ref_map_primitives() {
840        assert_eq!(
841            parse_type_ref("{string:i32}"),
842            Ok(TypeRef::Map(
843                Box::new(TypeRef::StringUtf8),
844                Box::new(TypeRef::I32)
845            ))
846        );
847    }
848
849    #[test]
850    fn parse_type_ref_map_struct_value() {
851        assert_eq!(
852            parse_type_ref("{string:Contact}"),
853            Ok(TypeRef::Map(
854                Box::new(TypeRef::StringUtf8),
855                Box::new(TypeRef::Struct("Contact".into()))
856            ))
857        );
858    }
859
860    #[test]
861    fn parse_type_ref_map_nested_value() {
862        assert_eq!(
863            parse_type_ref("{string:[i32]}"),
864            Ok(TypeRef::Map(
865                Box::new(TypeRef::StringUtf8),
866                Box::new(TypeRef::List(Box::new(TypeRef::I32)))
867            ))
868        );
869    }
870
871    #[test]
872    fn parse_type_ref_map_missing_colon() {
873        assert!(parse_type_ref("{string}").is_err());
874    }
875
876    #[test]
877    fn typeref_map_round_trip() {
878        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
879        let json = serde_json::to_string(&ty).unwrap();
880        assert_eq!(json, r#""{string:i32}""#);
881        let back: TypeRef = serde_json::from_str(&json).unwrap();
882        assert_eq!(back, ty);
883    }
884
885    #[test]
886    fn typeref_map_struct_round_trip() {
887        let ty = TypeRef::Map(
888            Box::new(TypeRef::StringUtf8),
889            Box::new(TypeRef::Struct("Contact".into())),
890        );
891        let json = serde_json::to_string(&ty).unwrap();
892        assert_eq!(json, r#""{string:Contact}""#);
893        let back: TypeRef = serde_json::from_str(&json).unwrap();
894        assert_eq!(back, ty);
895    }
896
897    #[test]
898    fn typeref_map_yaml_deser() {
899        let yaml = r#"
900version: "0.1.0"
901modules:
902  - name: contacts
903    functions:
904      - name: get_metadata
905        params: []
906        return: "{string:i32}"
907"#;
908        let api: Api = serde_yaml::from_str(yaml).unwrap();
909        let f = &api.modules[0].functions[0];
910        assert_eq!(
911            f.returns,
912            Some(TypeRef::Map(
913                Box::new(TypeRef::StringUtf8),
914                Box::new(TypeRef::I32)
915            ))
916        );
917    }
918
919    #[test]
920    fn typeref_optional_map_round_trip() {
921        let ty = TypeRef::Optional(Box::new(TypeRef::Map(
922            Box::new(TypeRef::StringUtf8),
923            Box::new(TypeRef::I32),
924        )));
925        let json = serde_json::to_string(&ty).unwrap();
926        assert_eq!(json, r#""{string:i32}?""#);
927        let back: TypeRef = serde_json::from_str(&json).unwrap();
928        assert_eq!(back, ty);
929    }
930
931    #[test]
932    fn parse_map_string_to_i32() {
933        assert_eq!(
934            parse_type_ref("{string:i32}"),
935            Ok(TypeRef::Map(
936                Box::new(TypeRef::StringUtf8),
937                Box::new(TypeRef::I32),
938            ))
939        );
940    }
941
942    #[test]
943    fn parse_map_string_to_struct() {
944        assert_eq!(
945            parse_type_ref("{string:Contact}"),
946            Ok(TypeRef::Map(
947                Box::new(TypeRef::StringUtf8),
948                Box::new(TypeRef::Struct("Contact".into())),
949            ))
950        );
951    }
952
953    #[test]
954    fn parse_map_roundtrip() {
955        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
956        let json = serde_json::to_string(&ty).unwrap();
957        let back: TypeRef = serde_json::from_str(&json).unwrap();
958        assert_eq!(back, ty);
959    }
960
961    #[test]
962    fn parse_optional_map() {
963        assert_eq!(
964            parse_type_ref("{string:i32}?"),
965            Ok(TypeRef::Optional(Box::new(TypeRef::Map(
966                Box::new(TypeRef::StringUtf8),
967                Box::new(TypeRef::I32),
968            ))))
969        );
970    }
971
972    #[test]
973    fn parse_map_of_lists() {
974        assert_eq!(
975            parse_type_ref("{string:[i32]}"),
976            Ok(TypeRef::Map(
977                Box::new(TypeRef::StringUtf8),
978                Box::new(TypeRef::List(Box::new(TypeRef::I32))),
979            ))
980        );
981    }
982
983    #[test]
984    fn parse_type_ref_iterator() {
985        assert_eq!(
986            parse_type_ref("iter<i32>"),
987            Ok(TypeRef::Iterator(Box::new(TypeRef::I32)))
988        );
989        assert_eq!(
990            parse_type_ref("iter<string>"),
991            Ok(TypeRef::Iterator(Box::new(TypeRef::StringUtf8)))
992        );
993        assert_eq!(
994            parse_type_ref("iter<Contact>"),
995            Ok(TypeRef::Iterator(Box::new(TypeRef::Struct(
996                "Contact".into()
997            ))))
998        );
999    }
1000
1001    #[test]
1002    fn typeref_iterator_round_trip() {
1003        let ty = TypeRef::Iterator(Box::new(TypeRef::I32));
1004        let json = serde_json::to_string(&ty).unwrap();
1005        assert_eq!(json, r#""iter<i32>""#);
1006        let back: TypeRef = serde_json::from_str(&json).unwrap();
1007        assert_eq!(back, ty);
1008    }
1009
1010    #[test]
1011    fn typeref_iterator_struct_round_trip() {
1012        let ty = TypeRef::Iterator(Box::new(TypeRef::Struct("Contact".into())));
1013        let json = serde_json::to_string(&ty).unwrap();
1014        assert_eq!(json, r#""iter<Contact>""#);
1015        let back: TypeRef = serde_json::from_str(&json).unwrap();
1016        assert_eq!(back, ty);
1017    }
1018
1019    #[test]
1020    fn parse_type_ref_borrowed() {
1021        assert_eq!(parse_type_ref("&str"), Ok(TypeRef::BorrowedStr));
1022        assert_eq!(parse_type_ref("&[u8]"), Ok(TypeRef::BorrowedBytes));
1023    }
1024
1025    #[test]
1026    fn typeref_borrowed_round_trip() {
1027        for ty in [TypeRef::BorrowedStr, TypeRef::BorrowedBytes] {
1028            let json = serde_json::to_string(&ty).unwrap();
1029            let back: TypeRef = serde_json::from_str(&json).unwrap();
1030            assert_eq!(back, ty);
1031        }
1032    }
1033
1034    #[test]
1035    fn typeref_borrowed_str_serializes_as_ampersand_str() {
1036        let json = serde_json::to_string(&TypeRef::BorrowedStr).unwrap();
1037        assert_eq!(json, r#""&str""#);
1038    }
1039
1040    #[test]
1041    fn typeref_borrowed_bytes_serializes_as_ampersand_u8() {
1042        let json = serde_json::to_string(&TypeRef::BorrowedBytes).unwrap();
1043        assert_eq!(json, r#""&[u8]""#);
1044    }
1045
1046    #[test]
1047    fn typeref_borrowed_yaml_deser() {
1048        let yaml = r#"
1049version: "0.1.0"
1050modules:
1051  - name: io
1052    functions:
1053      - name: write
1054        params:
1055          - name: data
1056            type: "&str"
1057          - name: raw
1058            type: "&[u8]"
1059"#;
1060        let api: Api = serde_yaml::from_str(yaml).unwrap();
1061        let f = &api.modules[0].functions[0];
1062        assert_eq!(f.params[0].ty, TypeRef::BorrowedStr);
1063        assert_eq!(f.params[1].ty, TypeRef::BorrowedBytes);
1064    }
1065
1066    #[test]
1067    fn parse_typed_handle() {
1068        assert_eq!(
1069            parse_type_ref("handle<Session>"),
1070            Ok(TypeRef::TypedHandle("Session".into()))
1071        );
1072        assert_eq!(parse_type_ref("handle"), Ok(TypeRef::Handle));
1073    }
1074
1075    #[test]
1076    fn generators_field_parses_from_yaml() {
1077        let yaml = r#"
1078version: "0.1.0"
1079modules:
1080  - name: math
1081    functions: []
1082generators:
1083  swift:
1084    module_name: MySwiftModule
1085  android:
1086    package: com.example.app
1087"#;
1088        let api: Api = serde_yaml::from_str(yaml).unwrap();
1089        let generators = api.generators.as_ref().unwrap();
1090        let swift = generators["swift"].as_table().unwrap();
1091        assert_eq!(swift["module_name"].as_str(), Some("MySwiftModule"));
1092        let android = generators["android"].as_table().unwrap();
1093        assert_eq!(android["package"].as_str(), Some("com.example.app"));
1094    }
1095
1096    #[test]
1097    fn generators_defaults_to_none() {
1098        let yaml = r#"
1099version: "0.1.0"
1100modules:
1101  - name: math
1102    functions: []
1103"#;
1104        let api: Api = serde_yaml::from_str(yaml).unwrap();
1105        assert!(api.generators.is_none());
1106    }
1107
1108    #[test]
1109    fn parse_typed_handle_roundtrip() {
1110        let ty = TypeRef::TypedHandle("Connection".into());
1111        let json = serde_json::to_string(&ty).unwrap();
1112        assert_eq!(json, r#""handle<Connection>""#);
1113        let back: TypeRef = serde_json::from_str(&json).unwrap();
1114        assert_eq!(back, ty);
1115    }
1116
1117    #[test]
1118    fn callback_def_round_trip_yaml() {
1119        let yaml = r#"
1120version: "0.1.0"
1121modules:
1122  - name: events
1123    functions: []
1124    callbacks:
1125      - name: on_data
1126        params:
1127          - name: payload
1128            type: string
1129        doc: "Fired when data arrives"
1130"#;
1131        let api: Api = serde_yaml::from_str(yaml).unwrap();
1132        let m = &api.modules[0];
1133        assert_eq!(m.callbacks.len(), 1);
1134        let cb = &m.callbacks[0];
1135        assert_eq!(cb.name, "on_data");
1136        assert_eq!(cb.params.len(), 1);
1137        assert_eq!(cb.params[0].name, "payload");
1138        assert_eq!(cb.params[0].ty, TypeRef::StringUtf8);
1139        assert_eq!(cb.doc.as_deref(), Some("Fired when data arrives"));
1140    }
1141
1142    #[test]
1143    fn listener_def_round_trip_yaml() {
1144        let yaml = r#"
1145version: "0.1.0"
1146modules:
1147  - name: events
1148    functions: []
1149    callbacks:
1150      - name: on_data
1151        params: []
1152    listeners:
1153      - name: data_stream
1154        event_callback: on_data
1155        doc: "Subscribe to data events"
1156"#;
1157        let api: Api = serde_yaml::from_str(yaml).unwrap();
1158        let m = &api.modules[0];
1159        assert_eq!(m.listeners.len(), 1);
1160        let l = &m.listeners[0];
1161        assert_eq!(l.name, "data_stream");
1162        assert_eq!(l.event_callback, "on_data");
1163        assert_eq!(l.doc.as_deref(), Some("Subscribe to data events"));
1164    }
1165
1166    #[test]
1167    fn callbacks_and_listeners_default_to_empty() {
1168        let yaml = r#"
1169version: "0.1.0"
1170modules:
1171  - name: math
1172    functions: []
1173"#;
1174        let api: Api = serde_yaml::from_str(yaml).unwrap();
1175        assert!(api.modules[0].callbacks.is_empty());
1176        assert!(api.modules[0].listeners.is_empty());
1177    }
1178
1179    #[test]
1180    fn callback_def_json_round_trip() {
1181        let cb = CallbackDef {
1182            name: "on_event".to_string(),
1183            params: vec![Param {
1184                name: "data".to_string(),
1185                ty: TypeRef::I32,
1186                mutable: false,
1187                doc: None,
1188            }],
1189            doc: Some("event callback".to_string()),
1190        };
1191        let json = serde_json::to_string(&cb).unwrap();
1192        let back: CallbackDef = serde_json::from_str(&json).unwrap();
1193        assert_eq!(back, cb);
1194    }
1195
1196    #[test]
1197    fn listener_def_json_round_trip() {
1198        let l = ListenerDef {
1199            name: "watcher".to_string(),
1200            event_callback: "on_change".to_string(),
1201            doc: None,
1202        };
1203        let json = serde_json::to_string(&l).unwrap();
1204        let back: ListenerDef = serde_json::from_str(&json).unwrap();
1205        assert_eq!(back, l);
1206    }
1207
1208    #[test]
1209    fn builder_defaults_to_false() {
1210        let yaml = r#"
1211version: "0.1.0"
1212modules:
1213  - name: contacts
1214    functions: []
1215    structs:
1216      - name: Contact
1217        fields:
1218          - name: name
1219            type: string
1220"#;
1221        let api: Api = serde_yaml::from_str(yaml).unwrap();
1222        assert!(!api.modules[0].structs[0].builder);
1223    }
1224
1225    #[test]
1226    fn builder_true_round_trip() {
1227        let yaml = r#"
1228version: "0.1.0"
1229modules:
1230  - name: contacts
1231    functions: []
1232    structs:
1233      - name: Contact
1234        fields:
1235          - name: name
1236            type: string
1237        builder: true
1238"#;
1239        let api: Api = serde_yaml::from_str(yaml).unwrap();
1240        assert!(api.modules[0].structs[0].builder);
1241
1242        let json = serde_json::to_string(&api).unwrap();
1243        let back: Api = serde_json::from_str(&json).unwrap();
1244        assert!(back.modules[0].structs[0].builder);
1245    }
1246
1247    #[test]
1248    fn builder_false_explicit() {
1249        let json = r#"{
1250            "version": "0.1.0",
1251            "modules": [{
1252                "name": "geo",
1253                "functions": [],
1254                "structs": [{
1255                    "name": "Point",
1256                    "fields": [{"name": "x", "type": "f64"}],
1257                    "builder": false
1258                }]
1259            }]
1260        }"#;
1261        let api: Api = serde_json::from_str(json).unwrap();
1262        assert!(!api.modules[0].structs[0].builder);
1263    }
1264
1265    #[test]
1266    fn param_mutable_defaults_to_false() {
1267        let yaml = r#"
1268version: "0.1.0"
1269modules:
1270  - name: io
1271    functions:
1272      - name: write
1273        params:
1274          - name: data
1275            type: string
1276"#;
1277        let api: Api = serde_yaml::from_str(yaml).unwrap();
1278        assert!(!api.modules[0].functions[0].params[0].mutable);
1279    }
1280
1281    #[test]
1282    fn param_mutable_true_round_trip() {
1283        let yaml = r#"
1284version: "0.1.0"
1285modules:
1286  - name: io
1287    functions:
1288      - name: fill_buffer
1289        params:
1290          - name: buf
1291            type: bytes
1292            mutable: true
1293"#;
1294        let api: Api = serde_yaml::from_str(yaml).unwrap();
1295        assert!(api.modules[0].functions[0].params[0].mutable);
1296
1297        let json = serde_json::to_string(&api).unwrap();
1298        let back: Api = serde_json::from_str(&json).unwrap();
1299        assert!(back.modules[0].functions[0].params[0].mutable);
1300    }
1301
1302    #[test]
1303    fn param_mutable_false_explicit() {
1304        let json = r#"{
1305            "version": "0.1.0",
1306            "modules": [{
1307                "name": "io",
1308                "functions": [{
1309                    "name": "read",
1310                    "params": [{"name": "buf", "type": "bytes", "mutable": false}]
1311                }]
1312            }]
1313        }"#;
1314        let api: Api = serde_json::from_str(json).unwrap();
1315        assert!(!api.modules[0].functions[0].params[0].mutable);
1316    }
1317
1318    #[test]
1319    fn deprecated_and_since_default_to_none() {
1320        let yaml = r#"
1321version: "0.1.0"
1322modules:
1323  - name: math
1324    functions:
1325      - name: add
1326        params: []
1327"#;
1328        let api: Api = serde_yaml::from_str(yaml).unwrap();
1329        let f = &api.modules[0].functions[0];
1330        assert_eq!(f.deprecated, None);
1331        assert_eq!(f.since, None);
1332    }
1333
1334    #[test]
1335    fn deprecated_and_since_round_trip() {
1336        let yaml = r#"
1337version: "0.1.0"
1338modules:
1339  - name: math
1340    functions:
1341      - name: add_old
1342        params: []
1343        deprecated: "Use add_v2 instead"
1344        since: "0.1.0"
1345"#;
1346        let api: Api = serde_yaml::from_str(yaml).unwrap();
1347        let f = &api.modules[0].functions[0];
1348        assert_eq!(f.deprecated.as_deref(), Some("Use add_v2 instead"));
1349        assert_eq!(f.since.as_deref(), Some("0.1.0"));
1350
1351        let json = serde_json::to_string(&api).unwrap();
1352        let back: Api = serde_json::from_str(&json).unwrap();
1353        let f2 = &back.modules[0].functions[0];
1354        assert_eq!(f2.deprecated.as_deref(), Some("Use add_v2 instead"));
1355        assert_eq!(f2.since.as_deref(), Some("0.1.0"));
1356    }
1357
1358    #[test]
1359    fn struct_field_default_value_round_trip() {
1360        let yaml = r#"
1361version: "0.1.0"
1362modules:
1363  - name: contacts
1364    functions: []
1365    structs:
1366      - name: Contact
1367        fields:
1368          - name: name
1369            type: string
1370          - name: age
1371            type: i32
1372            default: 0
1373"#;
1374        let api: Api = serde_yaml::from_str(yaml).unwrap();
1375        let fields = &api.modules[0].structs[0].fields;
1376        assert!(fields[0].default.is_none());
1377        assert_eq!(
1378            fields[1].default,
1379            Some(serde_yaml::Value::Number(serde_yaml::Number::from(0)))
1380        );
1381    }
1382
1383    #[test]
1384    fn serialization_omits_defaulted_fields() {
1385        // A minimal API whose every optional/defaulted field is at its
1386        // default must serialize without emitting those fields, so the
1387        // canonical IDL produced by `weaveffi format`/`extract` stays terse.
1388        let api = Api {
1389            version: "0.3.0".into(),
1390            modules: vec![Module {
1391                name: "calc".into(),
1392                functions: vec![Function {
1393                    name: "add".into(),
1394                    params: vec![Param {
1395                        name: "a".into(),
1396                        ty: TypeRef::I32,
1397                        mutable: false,
1398                        doc: None,
1399                    }],
1400                    returns: Some(TypeRef::I32),
1401                    doc: None,
1402                    r#async: false,
1403                    cancellable: false,
1404                    deprecated: None,
1405                    since: None,
1406                }],
1407                structs: vec![],
1408                enums: vec![],
1409                callbacks: vec![],
1410                listeners: vec![],
1411                errors: None,
1412                modules: vec![],
1413            }],
1414            generators: None,
1415            package: None,
1416        };
1417        let yaml = serde_yaml::to_string(&api).unwrap();
1418        for needle in [
1419            "generators",
1420            "structs",
1421            "enums",
1422            "callbacks",
1423            "listeners",
1424            "errors",
1425            "modules:\n", // nested module list (top-level key is "modules")
1426            "doc",
1427            "async",
1428            "cancellable",
1429            "deprecated",
1430            "since",
1431            "mutable",
1432            "null",
1433            "[]",
1434            "false",
1435        ] {
1436            // `modules:` appears once at the top level; assert the *nested*
1437            // empty module list under a module is gone by checking it never
1438            // shows an empty sequence.
1439            if needle == "modules:\n" {
1440                continue;
1441            }
1442            assert!(
1443                !yaml.contains(needle),
1444                "default field `{needle}` leaked into canonical YAML:\n{yaml}"
1445            );
1446        }
1447        // Round-trips back to an equal value.
1448        let back: Api = serde_yaml::from_str(&yaml).unwrap();
1449        assert_eq!(back, api);
1450    }
1451
1452    #[test]
1453    fn parse_type_ref_does_not_yield_callback() {
1454        assert_eq!(
1455            parse_type_ref("callback"),
1456            Ok(TypeRef::Struct("callback".into()))
1457        );
1458    }
1459
1460    #[test]
1461    fn api_json_schema_derives() {
1462        let schema = schemars::schema_for!(Api);
1463        let json = serde_json::to_value(&schema).unwrap();
1464        assert!(json.get("$schema").is_some());
1465        assert!(json.get("properties").is_some());
1466        assert_eq!(json.get("title").and_then(|v| v.as_str()), Some("Api"));
1467        let defs = json
1468            .get("definitions")
1469            .and_then(|v| v.as_object())
1470            .expect("definitions");
1471        assert!(defs.contains_key("Module"));
1472        assert!(defs.contains_key("Function"));
1473        assert!(defs.contains_key("Param"));
1474        assert!(defs.contains_key("TypeRef"));
1475        assert!(defs.contains_key("StructDef"));
1476        assert!(defs.contains_key("StructField"));
1477        assert!(defs.contains_key("EnumDef"));
1478        assert!(defs.contains_key("EnumVariant"));
1479        assert!(defs.contains_key("CallbackDef"));
1480        assert!(defs.contains_key("ListenerDef"));
1481        assert!(defs.contains_key("ErrorDomain"));
1482        assert!(defs.contains_key("ErrorCode"));
1483    }
1484
1485    #[test]
1486    fn typeref_json_schema_is_string_with_description() {
1487        let schema = schemars::schema_for!(TypeRef);
1488        let json = serde_json::to_value(&schema).unwrap();
1489        assert_eq!(json.get("type").and_then(|v| v.as_str()), Some("string"));
1490        assert!(json
1491            .get("description")
1492            .and_then(|v| v.as_str())
1493            .is_some_and(|s| s.contains("handle<") && s.contains("iter<")));
1494    }
1495}