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