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