#![forbid(unsafe_code)]
pub mod bindings;
pub mod convert;
use forge_ir as ir;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BindgenError {
DanglingTypeRef(String),
DanglingDiscriminator { type_ref: String },
DuplicateTypeId(String),
BadStatusRange(u8),
DanglingValueRef(u32),
}
impl core::fmt::Display for BindgenError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
BindgenError::DanglingTypeRef(r) => write!(f, "dangling type ref: {r}"),
BindgenError::DanglingDiscriminator { type_ref } => {
write!(f, "dangling discriminator type ref: {type_ref}")
}
BindgenError::DuplicateTypeId(id) => write!(f, "duplicate type id: {id}"),
BindgenError::BadStatusRange(c) => write!(f, "invalid status range class: {c}"),
BindgenError::DanglingValueRef(r) => write!(f, "dangling value ref: {r}"),
}
}
}
impl std::error::Error for BindgenError {}
pub fn validate_refs(ir: &ir::Ir) -> Result<(), BindgenError> {
let mut ids = std::collections::HashSet::with_capacity(ir.types.len());
for t in &ir.types {
if !ids.insert(t.id.as_str()) {
return Err(BindgenError::DuplicateTypeId(t.id.clone()));
}
}
let check = |r: &str| -> Result<(), BindgenError> {
if ids.contains(r) {
Ok(())
} else {
Err(BindgenError::DanglingTypeRef(r.to_string()))
}
};
for t in &ir.types {
match &t.definition {
ir::TypeDef::Array(a) => check(&a.items)?,
ir::TypeDef::Object(o) => {
for p in &o.properties {
check(&p.r#type)?;
}
if let ir::AdditionalProperties::Typed { r#type } = &o.additional_properties {
check(r#type)?;
}
}
ir::TypeDef::Union(u) => {
for v in &u.variants {
check(&v.r#type)?;
}
if let Some(d) = &u.discriminator {
for (_, r) in &d.mapping {
if !ids.contains(r.as_str()) {
return Err(BindgenError::DanglingDiscriminator {
type_ref: r.clone(),
});
}
}
}
}
ir::TypeDef::Primitive(_)
| ir::TypeDef::EnumString(_)
| ir::TypeDef::EnumInt(_)
| ir::TypeDef::Null => {}
}
}
for op in &ir.operations {
for p in op
.path_params
.iter()
.chain(&op.query_params)
.chain(&op.header_params)
.chain(&op.cookie_params)
{
check(&p.r#type)?;
}
if let Some(b) = &op.request_body {
for c in &b.content {
check(&c.r#type)?;
}
}
for r in &op.responses {
for c in &r.content {
check(&c.r#type)?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dangling_ref_is_rejected() {
let bad = ir::Ir {
info: ir::ApiInfo {
title: "x".into(),
version: "0".into(),
description: None,
summary: None,
terms_of_service: None,
contact: None,
license_name: None,
license_url: None,
license_identifier: None,
extensions: vec![],
},
operations: vec![],
types: vec![ir::NamedType {
id: "Wrap".into(),
original_name: None,
documentation: None,
title: None,
read_only: false,
write_only: false,
external_docs: None,
default: None,
examples: vec![],
xml: None,
definition: ir::TypeDef::Array(ir::ArrayType {
items: "Missing".into(),
constraints: ir::ArrayConstraints::default(),
}),
extensions: vec![],
location: None,
}],
security_schemes: vec![],
servers: vec![],
webhooks: vec![],
external_docs: None,
tags: vec![],
json_schema_dialect: None,
self_url: None,
values: vec![],
};
assert_eq!(
validate_refs(&bad).unwrap_err(),
BindgenError::DanglingTypeRef("Missing".into())
);
}
}