Skip to main content

forge_ir_bindgen/
lib.rs

1//! WIT bindings for the OpenAPI Forge plugin worlds, plus conversions to and
2//! from [`forge_ir`].
3//!
4//! `wasmtime::component::bindgen!` produces one set of generated types per
5//! world. Both `ir-transformer` and `code-generator` import `host-api` and
6//! use the same `types` interface, but the macro generates *separate* Rust
7//! modules per world (the types are structurally identical but nominally
8//! distinct).
9//!
10//! This crate exposes both as `bindings::transformer` and
11//! `bindings::generator`, plus a `convert` module providing fallible
12//! conversions between the bindgen types and [`forge_ir`].
13
14#![forbid(unsafe_code)]
15
16pub mod bindings;
17pub mod convert;
18
19use forge_ir as ir;
20
21/// Errors returned when fallible conversions hit a boundary violation.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum BindgenError {
24    /// A `TypeRef` referred to a type id that did not appear in
25    /// [`forge_ir::Ir::types`].
26    DanglingTypeRef(String),
27    /// A discriminator mapping referenced a type id that did not appear in
28    /// [`forge_ir::Ir::types`].
29    DanglingDiscriminator { type_ref: String },
30    /// The same type id appeared more than once in [`forge_ir::Ir::types`].
31    DuplicateTypeId(String),
32    /// A status range outside `1..=5` was encountered.
33    BadStatusRange(u8),
34    /// A `ValueRef` index was out of bounds for [`forge_ir::Ir::values`].
35    DanglingValueRef(u32),
36}
37
38impl core::fmt::Display for BindgenError {
39    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40        match self {
41            BindgenError::DanglingTypeRef(r) => write!(f, "dangling type ref: {r}"),
42            BindgenError::DanglingDiscriminator { type_ref } => {
43                write!(f, "dangling discriminator type ref: {type_ref}")
44            }
45            BindgenError::DuplicateTypeId(id) => write!(f, "duplicate type id: {id}"),
46            BindgenError::BadStatusRange(c) => write!(f, "invalid status range class: {c}"),
47            BindgenError::DanglingValueRef(r) => write!(f, "dangling value ref: {r}"),
48        }
49    }
50}
51
52impl std::error::Error for BindgenError {}
53
54/// Validate that every `TypeRef` in `ir` resolves to a `NamedType.id`. The
55/// host runs this on every IR returned from a plugin before passing it to
56/// the next stage.
57pub fn validate_refs(ir: &ir::Ir) -> Result<(), BindgenError> {
58    let mut ids = std::collections::HashSet::with_capacity(ir.types.len());
59    for t in &ir.types {
60        if !ids.insert(t.id.as_str()) {
61            return Err(BindgenError::DuplicateTypeId(t.id.clone()));
62        }
63    }
64    let check = |r: &str| -> Result<(), BindgenError> {
65        if ids.contains(r) {
66            Ok(())
67        } else {
68            Err(BindgenError::DanglingTypeRef(r.to_string()))
69        }
70    };
71
72    for t in &ir.types {
73        match &t.definition {
74            ir::TypeDef::Array(a) => check(&a.items)?,
75            ir::TypeDef::Object(o) => {
76                for p in &o.properties {
77                    check(&p.r#type)?;
78                }
79                if let ir::AdditionalProperties::Typed { r#type } = &o.additional_properties {
80                    check(r#type)?;
81                }
82            }
83            ir::TypeDef::Union(u) => {
84                for v in &u.variants {
85                    check(&v.r#type)?;
86                }
87                if let Some(d) = &u.discriminator {
88                    for (_, r) in &d.mapping {
89                        if !ids.contains(r.as_str()) {
90                            return Err(BindgenError::DanglingDiscriminator {
91                                type_ref: r.clone(),
92                            });
93                        }
94                    }
95                }
96            }
97            ir::TypeDef::Primitive(_)
98            | ir::TypeDef::EnumString(_)
99            | ir::TypeDef::EnumInt(_)
100            | ir::TypeDef::Null => {}
101        }
102    }
103    for op in &ir.operations {
104        for p in op
105            .path_params
106            .iter()
107            .chain(&op.query_params)
108            .chain(&op.header_params)
109            .chain(&op.cookie_params)
110        {
111            check(&p.r#type)?;
112        }
113        if let Some(b) = &op.request_body {
114            for c in &b.content {
115                check(&c.r#type)?;
116            }
117        }
118        for r in &op.responses {
119            for c in &r.content {
120                check(&c.r#type)?;
121            }
122        }
123    }
124    Ok(())
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn dangling_ref_is_rejected() {
133        let bad = ir::Ir {
134            info: ir::ApiInfo {
135                title: "x".into(),
136                version: "0".into(),
137                description: None,
138                summary: None,
139                terms_of_service: None,
140                contact: None,
141                license_name: None,
142                license_url: None,
143                license_identifier: None,
144                extensions: vec![],
145            },
146            operations: vec![],
147            types: vec![ir::NamedType {
148                id: "Wrap".into(),
149                original_name: None,
150                documentation: None,
151                title: None,
152                read_only: false,
153                write_only: false,
154                external_docs: None,
155                default: None,
156                examples: vec![],
157                xml: None,
158                definition: ir::TypeDef::Array(ir::ArrayType {
159                    items: "Missing".into(),
160                    constraints: ir::ArrayConstraints::default(),
161                }),
162                extensions: vec![],
163                location: None,
164            }],
165            security_schemes: vec![],
166            servers: vec![],
167            webhooks: vec![],
168            external_docs: None,
169            tags: vec![],
170            json_schema_dialect: None,
171            self_url: None,
172            values: vec![],
173        };
174        assert_eq!(
175            validate_refs(&bad).unwrap_err(),
176            BindgenError::DanglingTypeRef("Missing".into())
177        );
178    }
179}