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            | ir::TypeDef::Any => {}
102        }
103    }
104    for op in &ir.operations {
105        for p in op
106            .path_params
107            .iter()
108            .chain(&op.query_params)
109            .chain(&op.header_params)
110            .chain(&op.cookie_params)
111        {
112            check(&p.r#type)?;
113        }
114        if let Some(b) = &op.request_body {
115            for c in &b.content {
116                check(&c.r#type)?;
117            }
118        }
119        for r in &op.responses {
120            for c in &r.content {
121                check(&c.r#type)?;
122            }
123        }
124    }
125    Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn dangling_ref_is_rejected() {
134        let bad = ir::Ir {
135            info: ir::ApiInfo {
136                title: "x".into(),
137                version: "0".into(),
138                summary: None,
139                description: None,
140                terms_of_service: None,
141                contact: None,
142                license_name: None,
143                license_url: None,
144                license_identifier: None,
145                extensions: vec![],
146            },
147            operations: vec![],
148            types: vec![ir::NamedType {
149                id: "Wrap".into(),
150                original_name: None,
151                title: None,
152                description: None,
153                deprecated: false,
154                read_only: false,
155                write_only: false,
156                external_docs: None,
157                default: None,
158                examples: vec![],
159                xml: None,
160                definition: ir::TypeDef::Array(ir::ArrayType {
161                    items: "Missing".into(),
162                    constraints: ir::ArrayConstraints::default(),
163                }),
164                extensions: vec![],
165                location: None,
166            }],
167            security_schemes: vec![],
168            servers: vec![],
169            webhooks: vec![],
170            external_docs: None,
171            tags: vec![],
172            json_schema_dialect: None,
173            self_url: None,
174            values: vec![],
175        };
176        assert_eq!(
177            validate_refs(&bad).unwrap_err(),
178            BindgenError::DanglingTypeRef("Missing".into())
179        );
180    }
181}