wobbly doubly, yet dynamic and easy to fix impl for openapi spec to wit

This commit is contained in:
Sandipsinh Rathod 2024-12-22 16:14:20 -05:00
parent d3658fd6f0
commit 9534daf4f0
7 changed files with 179 additions and 56 deletions

@ -137,6 +137,7 @@ fn fix_field<'a>() -> TryFold<'a, (&'a SchemaDocument, &'a Interface, &'a Record
};
Valid::succeed(new_field)
}
WitType::FieldTy(_) => Valid::succeed(o),
// Ideally this case should never happen,
// but we should always throw an error to avoid infinite recursion
_ => return Valid::fail(anyhow::anyhow!("Unknown type: {:?}", field.field_type)),

@ -1,49 +1,104 @@
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashMap};
use anyhow::{anyhow, Error};
use tailcall_valid::{Valid, Validator};
use crate::config::schema_document::{SchemaDocument, Field, Interface, Record};
use crate::config::wit_types::WitType;
use crate::ser::OpenApiSpec;
use crate::openapi_spec::{OpenApiSpec, Resolved};
pub fn handle_types(config: SchemaDocument, spec: &OpenApiSpec) -> Valid<SchemaDocument, Error, Error> {
Valid::succeed(config).and_then(|mut config: SchemaDocument| {
Valid::from_option(spec.components.as_ref(), anyhow!("Components are required"))
.and_then(|v| Valid::from_option(v.schemas.as_ref(), anyhow!("Schemas are required")))
.and_then(|schemas| {
Valid::from_iter(schemas.iter(), |(record_name, v)| {
Valid::from_option(v.type_.as_ref(), anyhow!("Type is required"))
.and_then(|type_| {
Valid::from_option(v.properties.as_ref(), anyhow!("Properties are required"))
.and_then(|a| {
Valid::from_iter(a, |(field_name, v)| {
Valid::from(WitType::from_schema(v, spec))
.and_then(|wit_type| {
let field = Field {
name: field_name.clone(),
field_type: wit_type,
};
Valid::succeed(field)
pub fn handle_types(mut config: SchemaDocument, spec: &OpenApiSpec<Resolved>) -> Valid<SchemaDocument, Error, Error> {
let mut generated_records = BTreeSet::new();
fn process_wit_type(
wit_type: WitType,
parent_name: &str,
field_name: &str,
generated_records: &mut BTreeSet<Record>,
) -> WitType {
match wit_type {
WitType::Record(fields) => {
let record_name = format!("{}-{}", parent_name, field_name);
let new_record_fields: BTreeSet<_> = fields
.into_iter()
.map(|(nested_field_name, nested_type)| {
let processed_type =
process_wit_type(nested_type, &record_name, &nested_field_name, generated_records);
Field {
name: nested_field_name,
field_type: processed_type,
}
})
.collect();
let new_record = Record {
name: record_name.clone(),
fields: new_record_fields,
added_fields: Default::default(),
};
generated_records.insert(new_record);
WitType::FieldTy(record_name)
}
WitType::Option(inner) => WitType::Option(Box::new(process_wit_type(*inner, parent_name, field_name, generated_records))),
WitType::Result(ok, err) => WitType::Result(
Box::new(process_wit_type(*ok, parent_name, field_name, generated_records)),
Box::new(process_wit_type(*err, parent_name, field_name, generated_records)),
),
WitType::List(inner) => WitType::List(Box::new(process_wit_type(*inner, parent_name, field_name, generated_records))),
WitType::Tuple(elements) => WitType::Tuple(
elements
.into_iter()
.enumerate()
.map(|(i, t)| process_wit_type(t, parent_name, &format!("{}_tuple_{}", field_name, i), generated_records))
.collect(),
),
other => other,
}
}
Valid::from_option(spec.components.as_ref(), anyhow!("Components are required"))
.and_then(|components| Valid::from_option(components.schemas.as_ref(), anyhow!("Schemas are required")))
.and_then(|schemas| {
Valid::from_iter(schemas.iter(), |(record_name, schema)| {
Valid::from_option(schema.type_.as_ref(), anyhow!("Type is required"))
.and_then(|type_| {
Valid::from_option(schema.properties.as_ref(), anyhow!("Properties are required"))
.and_then(|properties| {
Valid::from_iter(properties, |(field_name, field_schema)| {
Valid::from(WitType::from_schema(field_schema, spec))
.and_then(|wit_type| {
let processed_type = process_wit_type(
wit_type,
record_name,
field_name,
&mut generated_records,
);
Valid::succeed(Field {
name: field_name.clone(),
field_type: processed_type,
})
})
})
})
.and_then(|fields| {
let record = Record {
name: record_name.clone(),
fields: fields.into_iter().collect::<BTreeSet<_>>(),
added_fields: Default::default(),
};
Valid::succeed(record)
})
})
}).and_then(|records| {
let interface = Interface {
name: "types".to_string(),
records: records.into_iter().collect::<BTreeSet<_>>(),
..Default::default()
};
config.interfaces.insert(interface);
Valid::succeed(())
})
}).and_then(|_| Valid::succeed(config))
})
})
.and_then(|fields| {
let record = Record {
name: record_name.clone(),
fields: fields.into_iter().collect(),
added_fields: Default::default(),
};
Valid::succeed(record)
})
})
})
})
.and_then(|mut records| {
records.extend(generated_records.into_iter());
let interface = Interface {
name: "types".to_string(),
records: records.into_iter().collect(),
..Default::default()
};
config.interfaces.insert(interface);
Valid::succeed(config)
})
}

@ -275,6 +275,7 @@ impl WitType {
),
WitType::Handle(name) => format!("handle<{}>", generate_wit_name(name)),
WitType::TypeAlias(name, inner) => format!("type {} = {}", generate_wit_name(name), inner.to_wit()),
WitType::FieldTy(name) => generate_wit_name(name)
}
}
}

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use tailcall_valid::{Valid, Validator};
use crate::ser::{OpenApiSpec, Schema};
use crate::openapi_spec::{OpenApiSpec, Resolved, Schema};
#[derive(Debug, Clone, Default, PartialEq, Eq, strum_macros::Display, Serialize, Deserialize)]
pub enum WitType {
@ -36,6 +36,7 @@ pub enum WitType {
// Special Types
Handle(String), // Handle<Resource>
TypeAlias(String, Box<WitType>), // TypeAlias { alias_name, type }
FieldTy(String) // Custom type to resolve field types
}
impl WitType {
@ -61,7 +62,7 @@ impl WitType {
}
pub fn from_schema(
schema: &Schema,
openapi: &OpenApiSpec,
openapi: &OpenApiSpec<Resolved>,
) -> Valid<WitType, anyhow::Error, anyhow::Error> {
if let Some(reference) = &schema.ref_ {
return Valid::from_option(
@ -101,7 +102,7 @@ impl WitType {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::ser::Components;
use crate::openapi_spec::Components;
use super::*;
#[test]

@ -1,6 +1,6 @@
mod value;
mod proto;
mod ser;
mod openapi;
mod openapi_spec;
mod config;
mod transformer;
mod transform;

@ -4,9 +4,10 @@ use tailcall_valid::{Valid, Validator};
use crate::config::schema_document::SchemaDocument;
use crate::config::fixargs::fix_args;
use crate::config::handle_files::handle_types;
use crate::ser::OpenApiSpec;
use crate::openapi_spec::OpenApiSpec;
fn to_config(spec: OpenApiSpec) -> Valid<SchemaDocument, Error, Error> {
let spec = spec.into_resolved();
Valid::succeed(SchemaDocument::default())
.and_then(|config| handle_types(config, &spec))
.and_then(|config| fix_args(config))
@ -15,9 +16,10 @@ fn to_config(spec: OpenApiSpec) -> Valid<SchemaDocument, Error, Error> {
#[cfg(test)]
mod t {
use tailcall_valid::Validator;
use wit_parser::Resolve;
use crate::proto::to_config;
use crate::ser::OpenApiSpec;
use crate::openapi::to_config;
use crate::openapi_spec::OpenApiSpec;
#[test]
fn tp() {
@ -28,12 +30,12 @@ mod t {
let mut res = to_config(y).to_result();
match res.as_mut() {
Ok(v) => {
println!("{}", serde_json::to_string_pretty(v).unwrap());
// println!("{}", serde_json::to_string_pretty(v).unwrap());
v.package = "api:todos@1.0.0".to_string();
println!("{}", v.to_wit());
// let mut resolve = Resolve::new();
// resolve.push_str("foox.wit", &v.to_wit()).expect("TODO: panic message`");
// println!("{:#?}", resolve);
let mut resolve = Resolve::new();
resolve.push_str("foox.wit", &v.to_wit()).expect("TODO: panic message`");
println!("{:#?}", resolve);
}
Err(e) => {
println!("err: {:?}", e);

@ -1,23 +1,39 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use anyhow::Error;
use convert_case::{Case, Casing};
use schemars::JsonSchema;
use tailcall_valid::{Valid, Validator};
use crate::config::fixargs::fix_args;
use crate::config::handle_files::handle_types;
use crate::config::schema_document::SchemaDocument;
#[derive(Default)]
pub struct Unresolved;
#[derive(Default)]
pub struct Resolved;
#[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct OpenApiSpec {
pub struct OpenApiSpec<Resolved = Unresolved> {
pub openapi: Option<String>,
pub info: Option<Info>,
pub paths: Option<HashMap<String, PathItem>>,
pub components: Option<Components>,
pub servers: Option<Vec<Server>>,
#[serde(skip)]
pub _marker: std::marker::PhantomData<Resolved>,
}
impl OpenApiSpec {
impl<T> OpenApiSpec<T> {
pub fn resolve_ref(&self, reference: &str) -> Option<&Schema> {
if let Some(components) = &self.components {
if reference.starts_with("#/components/schemas/") {
let name = reference.trim_start_matches("#/components/schemas/");
return components.schemas.as_ref()?.get(name);
let name = name.to_case(Case::Kebab);
return components.schemas.as_ref()?.get(&name);
}
}
None
@ -229,3 +245,50 @@ pub struct XML {
pub attribute: Option<bool>,
pub wrapped: Option<bool>,
}
impl OpenApiSpec<Resolved> {
fn to_config(&self) -> Valid<SchemaDocument, Error, Error> {
Valid::succeed(SchemaDocument::default())
.and_then(|config| handle_types(config, &self))
.and_then(|config| fix_args(config))
}
}
impl OpenApiSpec<Unresolved> {
fn into_config(self) -> Valid<SchemaDocument, Error, Error> {
self.into_resolved()
.to_config()
}
pub fn into_resolved(self) -> OpenApiSpec<Resolved> {
OpenApiSpec {
openapi: self.openapi,
info: self.info,
paths: self.paths,
components: self.components.map(|components| Components {
schemas: components.schemas.map(|schemas| {
schemas.into_iter().map(|(k, v)| (k.to_case(Case::Kebab), Self::process_schema(v))).collect()
}),
responses: components.responses,
parameters: components.parameters,
examples: components.examples,
request_bodies: components.request_bodies,
headers: components.headers,
security_schemes: components.security_schemes,
}),
servers: self.servers,
_marker: Default::default(),
}
}
// TODO: it's a poor and unsafe implementation,
// maybe add Marker for all structs here and implement into_resolve for all of them.
fn process_schema(mut schema: Schema) -> Schema {
schema.ref_ = schema.ref_.map(|t| t.to_case(Case::Kebab));
schema.type_ = schema.type_.map(|t| t.to_case(Case::Kebab));
schema.properties = schema.properties.map(|properties| {
properties.into_iter().map(|(k, v)| (k.to_case(Case::Kebab), Self::process_schema(v))).collect()
});
schema
}
}