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) Valid::succeed(new_field)
} }
WitType::FieldTy(_) => Valid::succeed(o),
// Ideally this case should never happen, // Ideally this case should never happen,
// but we should always throw an error to avoid infinite recursion // but we should always throw an error to avoid infinite recursion
_ => return Valid::fail(anyhow::anyhow!("Unknown type: {:?}", field.field_type)), _ => 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 anyhow::{anyhow, Error};
use tailcall_valid::{Valid, Validator}; use tailcall_valid::{Valid, Validator};
use crate::config::schema_document::{SchemaDocument, Field, Interface, Record}; use crate::config::schema_document::{SchemaDocument, Field, Interface, Record};
use crate::config::wit_types::WitType; 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> { pub fn handle_types(mut config: SchemaDocument, spec: &OpenApiSpec<Resolved>) -> Valid<SchemaDocument, Error, Error> {
Valid::succeed(config).and_then(|mut config: SchemaDocument| { let mut generated_records = BTreeSet::new();
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"))) fn process_wit_type(
.and_then(|schemas| { wit_type: WitType,
Valid::from_iter(schemas.iter(), |(record_name, v)| { parent_name: &str,
Valid::from_option(v.type_.as_ref(), anyhow!("Type is required")) field_name: &str,
.and_then(|type_| { generated_records: &mut BTreeSet<Record>,
Valid::from_option(v.properties.as_ref(), anyhow!("Properties are required")) ) -> WitType {
.and_then(|a| { match wit_type {
Valid::from_iter(a, |(field_name, v)| { WitType::Record(fields) => {
Valid::from(WitType::from_schema(v, spec)) let record_name = format!("{}-{}", parent_name, field_name);
.and_then(|wit_type| { let new_record_fields: BTreeSet<_> = fields
let field = Field { .into_iter()
name: field_name.clone(), .map(|(nested_field_name, nested_type)| {
field_type: wit_type, let processed_type =
}; process_wit_type(nested_type, &record_name, &nested_field_name, generated_records);
Valid::succeed(field) 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 { .and_then(|fields| {
name: record_name.clone(), let record = Record {
fields: fields.into_iter().collect::<BTreeSet<_>>(), name: record_name.clone(),
added_fields: Default::default(), fields: fields.into_iter().collect(),
}; added_fields: Default::default(),
Valid::succeed(record) };
}) Valid::succeed(record)
}) })
}).and_then(|records| { })
let interface = Interface { })
name: "types".to_string(), })
records: records.into_iter().collect::<BTreeSet<_>>(), .and_then(|mut records| {
..Default::default() records.extend(generated_records.into_iter());
};
config.interfaces.insert(interface); let interface = Interface {
Valid::succeed(()) name: "types".to_string(),
}) records: records.into_iter().collect(),
}).and_then(|_| Valid::succeed(config)) ..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::Handle(name) => format!("handle<{}>", generate_wit_name(name)),
WitType::TypeAlias(name, inner) => format!("type {} = {}", generate_wit_name(name), inner.to_wit()), 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 serde::{Deserialize, Serialize};
use tailcall_valid::{Valid, Validator}; 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)] #[derive(Debug, Clone, Default, PartialEq, Eq, strum_macros::Display, Serialize, Deserialize)]
pub enum WitType { pub enum WitType {
@ -36,6 +36,7 @@ pub enum WitType {
// Special Types // Special Types
Handle(String), // Handle<Resource> Handle(String), // Handle<Resource>
TypeAlias(String, Box<WitType>), // TypeAlias { alias_name, type } TypeAlias(String, Box<WitType>), // TypeAlias { alias_name, type }
FieldTy(String) // Custom type to resolve field types
} }
impl WitType { impl WitType {
@ -61,7 +62,7 @@ impl WitType {
} }
pub fn from_schema( pub fn from_schema(
schema: &Schema, schema: &Schema,
openapi: &OpenApiSpec, openapi: &OpenApiSpec<Resolved>,
) -> Valid<WitType, anyhow::Error, anyhow::Error> { ) -> Valid<WitType, anyhow::Error, anyhow::Error> {
if let Some(reference) = &schema.ref_ { if let Some(reference) = &schema.ref_ {
return Valid::from_option( return Valid::from_option(
@ -101,7 +102,7 @@ impl WitType {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use crate::ser::Components; use crate::openapi_spec::Components;
use super::*; use super::*;
#[test] #[test]

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

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

@ -1,23 +1,39 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::Error;
use convert_case::{Case, Casing};
use schemars::JsonSchema; 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)] #[derive(Serialize, Clone, Deserialize, Debug, JsonSchema, Default)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OpenApiSpec { pub struct OpenApiSpec<Resolved = Unresolved> {
pub openapi: Option<String>, pub openapi: Option<String>,
pub info: Option<Info>, pub info: Option<Info>,
pub paths: Option<HashMap<String, PathItem>>, pub paths: Option<HashMap<String, PathItem>>,
pub components: Option<Components>, pub components: Option<Components>,
pub servers: Option<Vec<Server>>, 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> { pub fn resolve_ref(&self, reference: &str) -> Option<&Schema> {
if let Some(components) = &self.components { if let Some(components) = &self.components {
if reference.starts_with("#/components/schemas/") { if reference.starts_with("#/components/schemas/") {
let name = reference.trim_start_matches("#/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 None
@ -229,3 +245,50 @@ pub struct XML {
pub attribute: Option<bool>, pub attribute: Option<bool>,
pub wrapped: 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
}
}