From 9534daf4f012b219360e393855c917496ddb0ed0 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Sun, 22 Dec 2024 16:14:20 -0500 Subject: [PATCH] wobbly doubly, yet dynamic and easy to fix impl for openapi spec to wit --- src/config/fixargs.rs | 1 + src/config/handle_files.rs | 137 ++++++++++++++++++++++---------- src/config/to_wit.rs | 1 + src/config/wit_types.rs | 7 +- src/main.rs | 4 +- src/{proto.rs => openapi.rs} | 16 ++-- src/{ser.rs => openapi_spec.rs} | 69 +++++++++++++++- 7 files changed, 179 insertions(+), 56 deletions(-) rename src/{proto.rs => openapi.rs} (69%) rename src/{ser.rs => openapi_spec.rs} (75%) diff --git a/src/config/fixargs.rs b/src/config/fixargs.rs index 8a51934..957b3f1 100644 --- a/src/config/fixargs.rs +++ b/src/config/fixargs.rs @@ -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)), diff --git a/src/config/handle_files.rs b/src/config/handle_files.rs index b68b8ee..fbf4d13 100644 --- a/src/config/handle_files.rs +++ b/src/config/handle_files.rs @@ -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 { - 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) -> Valid { + let mut generated_records = BTreeSet::new(); + + fn process_wit_type( + wit_type: WitType, + parent_name: &str, + field_name: &str, + generated_records: &mut BTreeSet, + ) -> 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::>(), - added_fields: Default::default(), - }; - Valid::succeed(record) - }) - }) - }).and_then(|records| { - let interface = Interface { - name: "types".to_string(), - records: records.into_iter().collect::>(), - ..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) + }) } diff --git a/src/config/to_wit.rs b/src/config/to_wit.rs index ea9aaaa..62e05c3 100644 --- a/src/config/to_wit.rs +++ b/src/config/to_wit.rs @@ -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) } } } diff --git a/src/config/wit_types.rs b/src/config/wit_types.rs index 26705af..e492d1a 100644 --- a/src/config/wit_types.rs +++ b/src/config/wit_types.rs @@ -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 TypeAlias(String, Box), // 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, ) -> Valid { 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] diff --git a/src/main.rs b/src/main.rs index 42975ef..30d9a63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod value; -mod proto; -mod ser; +mod openapi; +mod openapi_spec; mod config; mod transformer; mod transform; diff --git a/src/proto.rs b/src/openapi.rs similarity index 69% rename from src/proto.rs rename to src/openapi.rs index acf8a2e..584d3fd 100644 --- a/src/proto.rs +++ b/src/openapi.rs @@ -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 { + 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 { #[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); diff --git a/src/ser.rs b/src/openapi_spec.rs similarity index 75% rename from src/ser.rs rename to src/openapi_spec.rs index ea0416e..8daf08b 100644 --- a/src/ser.rs +++ b/src/openapi_spec.rs @@ -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 { pub openapi: Option, pub info: Option, pub paths: Option>, pub components: Option, pub servers: Option>, + + #[serde(skip)] + pub _marker: std::marker::PhantomData, } -impl OpenApiSpec { +impl OpenApiSpec { 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, pub wrapped: Option, } + +impl OpenApiSpec { + fn to_config(&self) -> Valid { + Valid::succeed(SchemaDocument::default()) + .and_then(|config| handle_types(config, &self)) + .and_then(|config| fix_args(config)) + } +} + +impl OpenApiSpec { + fn into_config(self) -> Valid { + self.into_resolved() + .to_config() + } + pub fn into_resolved(self) -> OpenApiSpec { + 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 + } +}