diff --git a/Cargo.lock b/Cargo.lock index 67bf421..77e5854 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aide" -version = "0.14.2" +version = "0.16.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2477554ebf38aea815a9c4729100cfc32f766876c45b9c9c38ef221b9d1a703" +checksum = "390515b47251185fa076ac92a7a582d9d383b03e13cef0c801e7670cf928229b" dependencies = [ "cfg-if", "indexmap", @@ -79,12 +79,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bitflags" version = "2.9.1" @@ -372,16 +366,37 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ron" -version = "0.10.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" dependencies = [ - "base64", "bitflags", + "once_cell", "serde", "serde_derive", + "typeid", "unicode-ident", ] @@ -406,12 +421,13 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schemars" -version = "0.8.22" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "indexmap", + "ref-cast", "schemars_derive", "serde", "serde_json", @@ -419,9 +435,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.22" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ "proc-macro2", "quote", @@ -609,6 +625,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index 48bae14..8b52546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" license = "MIT" [dependencies] -aide = "0.14.1" +aide = "=0.16.0-alpha.4" anyhow = "1.0.94" camino = "1.1.9" clap = { version = "4.5.23", features = ["derive"] } @@ -14,8 +14,8 @@ heck = "0.5.0" indexmap = "2.7.0" itertools = "0.14.0" minijinja = { version = "2.8.0", features = ["json", "loader"] } -ron = "0.10.1" -schemars = "0.8.21" +ron = "0.12.1" +schemars = "1.2.1" serde = { version = "1.0.215", features = ["derive", "rc"] } serde_json = "1.0.133" tempfile = "3.14.0" diff --git a/src/api/mod.rs b/src/api/mod.rs index acc32cb..979cf97 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -108,8 +108,8 @@ fn merge_types(dst: &mut Types, src: Types) -> anyhow::Result<()> { Ok(()) } -pub(crate) fn get_schema_name(maybe_ref: Option<&str>) -> Option { - let r = maybe_ref?; +pub(crate) fn get_schema_name<'a>(maybe_ref: impl Into>) -> Option { + let r = maybe_ref.into()?; let schema_name = r.strip_prefix("#/components/schemas/"); if schema_name.is_none() { tracing::warn!( diff --git a/src/api/resources.rs b/src/api/resources.rs index c8c89e3..701c42c 100644 --- a/src/api/resources.rs +++ b/src/api/resources.rs @@ -5,7 +5,6 @@ use std::{ use aide::openapi::{self, ReferenceOr}; use anyhow::{Context as _, bail}; -use schemars::schema::{InstanceType, Schema}; use serde::{Deserialize, Serialize}; use crate::cli_v1::IncludeMode; @@ -315,18 +314,7 @@ impl Operation { .swap_remove("application/json") .expect("should have JSON body"); assert!(json_body.extensions.is_empty()); - match json_body.schema.expect("no json body schema?!").json_schema { - Schema::Bool(_) => { - tracing::error!("unexpected bool schema"); - None - } - Schema::Object(obj) => { - if !obj.is_ref() { - tracing::error!(?obj, "unexpected non-$ref json body schema"); - } - get_schema_name(obj.reference.as_deref()) - } - } + get_body_schema_name(json_body) } ReferenceOr::Reference { .. } => { tracing::error!("$ref request bodies are not currently supported"); @@ -387,15 +375,35 @@ impl Operation { } } +fn get_body_schema_name(json_body: openapi::MediaType) -> Option { + let Some(schema_object) = json_body.schema else { + tracing::error!("missing json body schema"); + return None; + }; + let Some(obj) = schema_object.json_schema.as_object() else { + tracing::error!("unexpected bool schema"); + return None; + }; + + match obj.get("$ref") { + Some(reference) => get_schema_name(reference.as_str()), + None => { + tracing::error!(?obj, "unexpected non-$ref json body schema"); + None + } + } +} + fn enforce_string_parameter(parameter_data: &openapi::ParameterData) -> anyhow::Result<()> { let openapi::ParameterSchemaOrContent::Schema(s) = ¶meter_data.format else { bail!("found unexpected 'content' data format"); }; - let Schema::Object(obj) = &s.json_schema else { - bail!("found unexpected `true` schema"); - }; - if obj.instance_type != Some(InstanceType::String.into()) { - bail!("unsupported path parameter type `{:?}`", obj.instance_type); + let ty = s + .json_schema + .get("type") + .context("missing path parameter type")?; + if ty != "string" { + bail!("unsupported path parameter type `{ty:?}`"); } Ok(()) @@ -415,18 +423,7 @@ fn response_body_schema_name(resp: ReferenceOr) -> Option { - tracing::error!("unexpected bool schema"); - None - } - Schema::Object(obj) => { - if !obj.is_ref() { - tracing::error!(?obj, "unexpected non-$ref json body schema"); - } - get_schema_name(obj.reference.as_deref()) - } - } + get_body_schema_name(json_body) } ReferenceOr::Reference { .. } => { tracing::error!("$ref response bodies are not currently supported"); diff --git a/src/api/struct_enum.rs b/src/api/struct_enum.rs index 2230bc5..30ec6e2 100644 --- a/src/api/struct_enum.rs +++ b/src/api/struct_enum.rs @@ -1,9 +1,12 @@ use anyhow::{Context as _, bail, ensure}; -use schemars::schema::{ObjectValidation, Schema, SchemaObject}; -use crate::api::{ - get_schema_name, - types::{EnumVariantType, Field, SimpleVariant, StructEnumRepr, TypeData}, +use crate::{ + JsonValue, + api::{ + get_schema_name, + types::{EnumVariantType, Field, SimpleVariant, StructEnumRepr, TypeData}, + }, + utils::get_properties, }; /// A wrapper around a Option @@ -25,23 +28,25 @@ impl SameString { } impl TypeData { - pub(super) fn inline_struct_enum(one_of: &[Schema], fields: &[Field]) -> anyhow::Result { + pub(super) fn inline_struct_enum(one_of: &JsonValue, fields: &[Field]) -> anyhow::Result { + let one_of = one_of.as_array().context("oneOf must be an array")?; + let mut discriminator_field = SameString(None); let mut content_field = SameString(None); let mut variants = vec![]; - let mut process_one_of = |s: &Schema| { - let variant = get_obj_validation(s)?; - - let (variant_discriminator_name, discriminator) = get_discriminator(variant)?; + let mut process_one_of = |variant: &JsonValue| { + let (variant_discriminator_name, discriminator) = + get_discriminator(variant).context("get struct-enum discriminator")?; discriminator_field.update(variant_discriminator_name)?; - let len = variant.properties.len(); + let properties = get_properties(variant).context("get struct-enum properties")?; + let len = properties.len(); ensure!( (1..=2).contains(&len), "Found struct enum variant with {len} properties, expected 1 or 2" ); - if variant.properties.len() == 1 { + if properties.len() == 1 { variants.push(SimpleVariant { name: discriminator, content: EnumVariantType::Ref { @@ -50,7 +55,8 @@ impl TypeData { }, }); } else { - let (variant_content_field, content) = get_content(variant)?; + let (variant_content_field, content) = + get_content(variant).context("get struct-enum content")?; content_field.update(variant_content_field)?; variants.push(SimpleVariant { @@ -81,23 +87,20 @@ impl TypeData { } } -fn get_content(variant: &ObjectValidation) -> anyhow::Result<(String, EnumVariantType)> { - for (p_name, p) in &variant.properties { - let schema_obj = get_schema_obj(p)?; - if let Some(obj) = &schema_obj.object { - let ty = TypeData::from_object_schema(*obj.clone(), schemars::Map::new(), None)?; +fn get_content(variant: &JsonValue) -> anyhow::Result<(String, EnumVariantType)> { + for (prop_name, prop_schema) in get_properties(variant)? { + if prop_schema["type"] == "object" { + let ty = TypeData::from_object_schema(prop_schema)?; let TypeData::Struct { fields } = ty else { bail!("Expected obj to be a struct"); }; - return Ok((p_name.to_owned(), EnumVariantType::Struct { fields })); - } - - if let Some(schema_ref) = &schema_obj.reference { + return Ok((prop_name.to_owned(), EnumVariantType::Struct { fields })); + } else if let Some(reference) = prop_schema["$ref"].as_str() { return Ok(( - p_name.to_owned(), + prop_name.to_owned(), EnumVariantType::Ref { - schema_ref: Some(get_schema_name(Some(schema_ref.as_str())).unwrap()), + schema_ref: Some(get_schema_name(reference).unwrap()), inner: None, }, )); @@ -107,22 +110,21 @@ fn get_content(variant: &ObjectValidation) -> anyhow::Result<(String, EnumVarian bail!("Failed to find content on struct enum") } -fn get_discriminator(obj: &ObjectValidation) -> anyhow::Result<(String, String)> { +fn get_discriminator(obj: &JsonValue) -> anyhow::Result<(String, String)> { let mut discriminator_field_name = None; let mut discriminator = None; - for (p_name, p) in &obj.properties { - let schema_obj = get_schema_obj(p).with_context(|| p_name.to_owned())?; - if let Some(enum_vals) = &schema_obj.enum_values - && enum_vals.len() == 1 + for (prop_name, prop_schema) in get_properties(obj)? { + if let Some(enum_value) = &prop_schema.get("enum") + && let Some(enum_list) = enum_value.as_array() + && let [value] = enum_list.as_slice() { - match &enum_vals[0].as_str() { - Some(v) => { - discriminator_field_name = Some(p_name.clone()); - discriminator = Some((*v).to_owned()); - } - None => bail!("Expected discriminator field name to be a string"), - } + let value = value + .as_str() + .context("Expected discriminator field name to be a string")?; + + discriminator_field_name = Some(prop_name.clone()); + discriminator = Some(value.to_owned()); } } @@ -135,17 +137,3 @@ fn get_discriminator(obj: &ObjectValidation) -> anyhow::Result<(String, String)> Ok((discriminator_field_name, discriminator)) } - -fn get_schema_obj(s: &Schema) -> anyhow::Result<&SchemaObject> { - match s { - Schema::Bool(_) => bail!("unsupported bool schema"), - Schema::Object(o) => Ok(o), - } -} - -fn get_obj_validation(s: &Schema) -> anyhow::Result<&ObjectValidation> { - let Some(obj) = get_schema_obj(s)?.object.as_ref() else { - bail!("unsupported: object type without further validation"); - }; - Ok(obj) -} diff --git a/src/api/types.rs b/src/api/types.rs index a7d47fa..9693c48 100644 --- a/src/api/types.rs +++ b/src/api/types.rs @@ -7,12 +7,9 @@ use std::{ use aide::openapi; use anyhow::{Context as _, bail, ensure}; use indexmap::IndexMap; -use schemars::schema::{ - InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation, -}; use serde::{Deserialize, Serialize}; -use crate::cli_v1::IncludeMode; +use crate::{JsonValue, cli_v1::IncludeMode, utils::get_properties}; use super::{ get_schema_name, @@ -45,15 +42,7 @@ pub(crate) fn from_referenced_components( return; }; - let obj = match s.json_schema { - Schema::Bool(_) => { - tracing::warn!(schema_name, "found $ref'erenced bool schema, wat?!"); - return; - } - Schema::Object(o) => o, - }; - - match Type::from_schema(schema_name.to_owned(), obj) { + match Type::from_schema(schema_name.to_owned(), s.json_schema.as_value()) { Ok(ty) => { extra_components.extend( ty.referenced_components() @@ -91,29 +80,34 @@ pub struct Type { } impl Type { - pub(crate) fn from_schema(name: String, s: SchemaObject) -> anyhow::Result { - let instance_type = match &s.instance_type { - Some(ty) => Some(ty), + pub(crate) fn from_schema(name: String, schema: &JsonValue) -> anyhow::Result { + ensure!(schema.is_object(), "schema must be an object"); + + let instance_type = match schema.get("type") { + Some(JsonValue::String(ty)) => Some(ty.as_str()), + Some(ty) => bail!("invalid / unsupported type `{ty:?}`"), None => { let mut result = None; - for variant in s - .subschemas + for variant in schema + .get("oneOf") .iter() - .filter_map(|s| s.one_of.as_ref()) + .filter_map(|v| v.as_array()) .flatten() { - let Schema::Object(schema) = variant else { - bail!("unsupported: boolean oneOf schema"); - }; - - if let Some(ty) = &schema.instance_type { - if let Some(res_ty) = result { - if res_ty != ty { - bail!("unsupported: oneOf schemas with different types"); + if let Some(ty) = &variant.get("type") { + match ty { + JsonValue::String(ty) => { + if let Some(res_ty) = result { + ensure!( + res_ty == ty, + "unsupported: oneOf schemas with different types" + ); + } else { + result = Some(ty.as_str()); + } } - } else { - result = Some(ty); + _ => bail!("invalid / unsupported type `{ty:?}` in oneOf"), } } } @@ -123,48 +117,43 @@ impl Type { }; let data = match instance_type { - Some(SingleOrVec::Single(it)) => match **it { - InstanceType::Object => { - let obj = s.object.unwrap_or_default(); - TypeData::from_object_schema(*obj, s.extensions, s.subschemas)? - } - InstanceType::Integer => { - let enum_varnames = s - .extensions - .get("x-enum-varnames") - .context("unsupported: integer type without enum varnames")? - .as_array() - .context("unsupported: integer type enum varnames should be a list")?; - let values = s - .enum_values - .context("unsupported: integer type without enum values")?; - if enum_varnames.len() != values.len() { - bail!( - "enum varnames length ({}) does not match values length ({})", - enum_varnames.len(), - values.len() - ); - } - TypeData::from_integer_enum(values, enum_varnames)? - } - InstanceType::String => { - let values = s - .enum_values - .context("unsupported: string type without enum values")?; - TypeData::from_string_enum(values)? + Some("object") => TypeData::from_object_schema(schema)?, + Some("integer") => { + let enum_varnames = schema + .get("x-enum-varnames") + .context("unsupported: integer type without enum varnames")? + .as_array() + .context("unsupported: integer type enum varnames should be a list")?; + let values = schema + .get("enum") + .context("unsupported: integer type without enum values")? + .as_array() + .context("enum must be an array")?; + if enum_varnames.len() != values.len() { + bail!( + "enum varnames length ({}) does not match values length ({})", + enum_varnames.len(), + values.len() + ); } - _ => bail!("unsupported type {it:?}"), - }, - Some(SingleOrVec::Vec(_)) => bail!("unsupported: multiple types"), + TypeData::from_integer_enum(values, enum_varnames)? + } + Some("string") => { + let values = schema + .get("enum") + .context("unsupported: string type without enum values")? + .as_array() + .context("enum must be an array")?; + TypeData::from_string_enum(values)? + } + Some(ty) => bail!("unsupported type {ty:?}"), None => bail!("unsupported: schema without a type"), }; - let metadata = s.metadata.unwrap_or_default(); - Ok(Self { name, - description: metadata.description, - deprecated: metadata.deprecated, + description: schema["description"].as_str().map(ToOwned::to_owned), + deprecated: schema["deprecated"].as_bool().unwrap_or(false), data, }) } @@ -216,77 +205,88 @@ pub enum TypeData { } impl TypeData { - pub(super) fn from_object_schema( - obj: ObjectValidation, - extensions: schemars::Map, - subschemas: Option>, - ) -> anyhow::Result { + pub(super) fn from_object_schema(obj: &JsonValue) -> anyhow::Result { ensure!( - obj.additional_properties.is_none(), + obj.get("additionalProperties").is_none(), "additionalProperties not yet supported" ); - ensure!(obj.max_properties.is_none(), "unsupported: maxProperties"); - ensure!(obj.min_properties.is_none(), "unsupported: minProperties"); ensure!( - obj.pattern_properties.is_empty(), + obj.get("maxProperties").is_none(), + "unsupported: maxProperties" + ); + ensure!( + obj.get("minProperties").is_none(), + "unsupported: minProperties" + ); + ensure!( + obj.get("patternProperties").is_none(), "unsupported: patternProperties" ); - ensure!(obj.property_names.is_none(), "unsupported: propertyNames"); + ensure!( + obj.get("propertyNames").is_none(), + "unsupported: propertyNames" + ); - let x_positional = extensions + let required = obj["required"].as_array().map(Vec::as_slice).unwrap_or(&[]); + let x_positional = obj .get("x-positional") .and_then(|ext| Some(ext.as_array()?.as_slice())) .unwrap_or(&[]); - let fields: Vec<_> = obj - .properties + + let fields: Vec<_> = get_properties(obj)? .into_iter() .map(|(name, schema)| { - let required = obj.required.contains(&name); - let positional = x_positional.iter().any(|p| *p == name); + let required = required.iter().any(|n| n == name); + let positional = x_positional.iter().any(|p| p == name); Field::from_schema(name.clone(), schema, required, positional) .with_context(|| format!("unsupported field `{name}`")) }) .collect::>()?; - if let Some(sub) = subschemas { - ensure!(sub.all_of.is_none(), "unsupported: allOf subschema"); - ensure!(sub.any_of.is_none(), "unsupported: anyOf subschema"); - ensure!(sub.not.is_none(), "unsupported: not subschema"); - ensure!(sub.if_schema.is_none(), "unsupported: if subschema"); - ensure!(sub.then_schema.is_none(), "unsupported: then subschema"); - ensure!(sub.else_schema.is_none(), "unsupported: else subschema"); + ensure!(obj.get("allOf").is_none(), "unsupported: allOf subschema"); + ensure!(obj.get("anyOf").is_none(), "unsupported: anyOf subschema"); + ensure!(obj.get("not").is_none(), "unsupported: not subschema"); + ensure!(obj.get("ifSchema").is_none(), "unsupported: if subschema"); + ensure!( + obj.get("thenSchema").is_none(), + "unsupported: then subschema" + ); + ensure!( + obj.get("elseSchema").is_none(), + "unsupported: else subschema" + ); - if let Some(one_of) = sub.one_of { - return Self::inline_struct_enum(&one_of, &fields); - } + if let Some(one_of) = obj.get("oneOf") { + return Self::inline_struct_enum(one_of, &fields); } Ok(Self::Struct { fields }) } - fn from_string_enum(values: Vec) -> anyhow::Result { + fn from_string_enum(values: &[JsonValue]) -> anyhow::Result { Ok(Self::StringEnum { values: values - .into_iter() + .iter() .enumerate() - .map(|(i, v)| match v { - serde_json::Value::String(s) => Ok(s), - _ => bail!("enum value {} is not a string", i + 1), + .map(|(i, v)| { + Ok(v.as_str() + .with_context(|| format!("enum value {} is not a string", i + 1))? + .to_owned()) }) .collect::>()?, }) } fn from_integer_enum( - values: Vec, - enum_varnames: &[serde_json::Value], + values: &[JsonValue], + enum_varnames: &[JsonValue], ) -> anyhow::Result { Ok(Self::IntegerEnum { variants: values - .into_iter() + .iter() .enumerate() .map(|(i, v)| match v { - serde_json::Value::Number(s) => { + JsonValue::Number(s) => { let num = s .as_i64() .with_context(|| format!("enum value {s} is not an integer"))?; @@ -346,7 +346,7 @@ pub struct Field { #[serde(serialize_with = "serialize_field_type")] pub r#type: FieldType, #[serde(skip_serializing_if = "Option::is_none")] - default: Option, + default: Option, #[serde(skip_serializing_if = "Option::is_none")] description: Option, required: bool, @@ -355,37 +355,31 @@ pub struct Field { #[serde(default)] positional: bool, #[serde(skip_serializing_if = "Option::is_none")] - example: Option, + example: Option, } impl Field { fn from_schema( name: String, - s: Schema, + schema: &JsonValue, required: bool, positional: bool, ) -> anyhow::Result { - let obj = match s { - Schema::Bool(_) => bail!("unsupported bool schema"), - Schema::Object(o) => o, - }; - let example = obj.extensions.get("example").cloned(); - let metadata = obj.metadata.clone().unwrap_or_default(); - - let nullable = obj - .extensions + let example = schema.get("example").cloned(); + let nullable = schema .get("nullable") .and_then(|v| v.as_bool()) .unwrap_or(false); + Ok(Self { name, - r#type: FieldType::from_schema_object(obj)?, - default: metadata.default, - description: metadata.description, + r#type: FieldType::from_schema(schema)?, + default: schema.get("default").cloned(), + description: schema["description"].as_str().map(ToOwned::to_owned), required, nullable, positional, - deprecated: metadata.deprecated, + deprecated: schema["deprecated"].as_bool().unwrap_or(false), example, }) } @@ -469,132 +463,129 @@ impl FieldType { let openapi::ParameterSchemaOrContent::Schema(s) = format else { bail!("found unexpected 'content' data format"); }; - Self::from_schema(s.json_schema) - } - - fn from_schema(s: Schema) -> anyhow::Result { - let Schema::Object(obj) = s else { - bail!("found unexpected `true` schema"); - }; - - Self::from_schema_object(obj) + Self::from_schema(s.json_schema.as_value()) } - fn from_schema_object(obj: SchemaObject) -> anyhow::Result { - let result = match &obj.instance_type { - Some(SingleOrVec::Single(ty)) => match **ty { - InstanceType::Boolean => Self::Bool, - InstanceType::Integer => match obj.format.as_deref() { - Some("int8") => Self::Int8, - Some("uint8") => Self::UInt8, - Some("int16") => Self::Int16, - Some("uint16") => Self::UInt16, - Some("int32") => Self::Int32, - Some("uint32") => Self::UInt32, - // FIXME: Why do we have int in the spec? - Some("int" | "int64") => Self::Int64, - // FIXME: Get rid of uint in the spec.. - Some("uint" | "uint64") => match obj.extensions.get("x-subtype") { - Some(s) if s == "DurationMs" => Self::DurationMs, - Some(s) if s == "UnixTimestampMs" => Self::UnixTimestampMs, - Some(s) => bail!("Unknown subtype {s}"), - None => Self::UInt64, + fn from_schema(schema: &JsonValue) -> anyhow::Result { + ensure!(schema.is_object(), "schema must be an object"); + + let result = match schema.get("type") { + Some(JsonValue::String(ty)) => { + match ty.as_str() { + "boolean" => Self::Bool, + "integer" => match schema["format"].as_str() { + Some("int8") => Self::Int8, + Some("uint8") => Self::UInt8, + Some("int16") => Self::Int16, + Some("uint16") => Self::UInt16, + Some("int32") => Self::Int32, + Some("uint32") => Self::UInt32, + // FIXME: Why do we have int in the spec? + Some("int" | "int64") => Self::Int64, + // FIXME: Get rid of uint in the spec.. + Some("uint" | "uint64") => match schema.get("x-subtype") { + Some(s) if s == "DurationMs" => Self::DurationMs, + Some(s) if s == "UnixTimestampMs" => Self::UnixTimestampMs, + Some(s) => bail!("Unknown subtype {s}"), + None => Self::UInt64, + }, + f => bail!("unsupported integer format: `{f:?}`"), }, - f => bail!("unsupported integer format: `{f:?}`"), - }, - InstanceType::Number => match obj.format.as_deref() { - Some("double") => Self::Float64, - f => bail!("unsupported number format: `{f:?}`"), - }, - InstanceType::String => { - // String consts are the only const / enum values we support, for now. - // Early return so we don't hit the checks for these two below. - if let Some(value) = obj.const_value { - let serde_json::Value::String(value) = value else { - bail!("unsupported: non-string constant as field type"); - }; - return Ok(Self::StringConst { value }); - } - if let Some(values) = obj.enum_values { - let Ok([value]): Result<[_; 1], _> = values.try_into() else { - bail!("unsupported: enum as field type"); - }; - let serde_json::Value::String(value) = value else { - bail!("unsupported: non-string constant as field type"); - }; - return Ok(Self::StringConst { value }); - } + "number" => match schema["format"].as_str() { + Some("double") => Self::Float64, + f => bail!("unsupported number format: `{f:?}`"), + }, + "string" => { + // String consts are the only const / enum values we support, for now. + // Early return so we don't hit the checks for these two below. + if let Some(value) = schema.get("const") { + let value = value + .as_str() + .context("unsupported: non-string constant as field type")? + .to_owned(); + return Ok(Self::StringConst { value }); + } + if let Some(values) = schema["enum"].as_array() { + let Ok([value]): Result<&[_; 1], _> = values.as_slice().try_into() + else { + bail!("unsupported: enum as field type"); + }; + let value = value + .as_str() + .context("unsupported: non-string constant as field type")? + .to_owned(); + return Ok(Self::StringConst { value }); + } - match obj.format.as_deref() { - None | Some("color") | Some("email") | Some("uuid") => Self::String, - Some("date-time") => Self::DateTime, - Some("uri") => Self::Uri, - Some(f) => bail!("unsupported string format: `{f:?}`"), + match schema["format"].as_str() { + None | Some("color") | Some("email") | Some("uuid") => Self::String, + Some("date-time") => Self::DateTime, + Some("uri") => Self::Uri, + Some(f) => bail!("unsupported string format: `{f:?}`"), + } } - } - InstanceType::Array => { - let array = obj.array.context("array type must have array props")?; - ensure!(array.additional_items.is_none(), "not supported"); - let inner = match array.items.context("array type must have items prop")? { - SingleOrVec::Single(ty) => ty, - SingleOrVec::Vec(types) => { - bail!("unsupported multi-typed array parameter: `{types:?}`") + "array" => { + ensure!(schema.get("additionalItems").is_none(), "not supported"); + let inner_schema = schema + .get("items") + .context("array type must have items prop")?; + let inner = Arc::new(Self::from_schema(inner_schema)?); + if schema.get("uniqueItems").is_some_and(|v| v == true) { + Self::Set { inner } + } else { + Self::List { inner } } - }; - let inner = Arc::new(Self::from_schema(*inner)?); - if array.unique_items == Some(true) { - Self::Set { inner } - } else { - Self::List { inner } } - } - InstanceType::Object => { - let obj = obj - .object - .context("unsupported: object type without further validation")?; - let additional_properties = obj - .additional_properties - .context("unsupported: object field type without additional_properties")?; - - ensure!(obj.max_properties.is_none(), "unsupported: max_properties"); - ensure!(obj.min_properties.is_none(), "unsupported: min_properties"); - ensure!( - obj.properties.is_empty(), - "unsupported: properties on field type" - ); - ensure!( - obj.pattern_properties.is_empty(), - "unsupported: pattern_properties" - ); - ensure!(obj.property_names.is_none(), "unsupported: property_names"); - ensure!( - obj.required.is_empty(), - "unsupported: required on field type" - ); + "object" => { + let additional_props_schema = schema.get("additionalProperties").context( + "unsupported: object field type without additional_properties", + )?; + + ensure!( + schema.get("maxProperties").is_none(), + "unsupported: maxProperties on field type" + ); + ensure!( + schema.get("minProperties").is_none(), + "unsupported: minProperties on field type" + ); + ensure!( + schema.get("properties").is_none(), + "unsupported: properties on field type" + ); + ensure!( + schema.get("patternProperties").is_none(), + "unsupported: patternProperties on field type" + ); + ensure!( + schema.get("propertyNames").is_none(), + "unsupported: propertyNames on field type" + ); + ensure!( + schema.get("required").is_none(), + "unsupported: required on field type" + ); - match *additional_properties { - Schema::Bool(true) => Self::JsonObject, - Schema::Bool(false) => bail!("unsupported `additional_properties: false`"), - Schema::Object(schema_object) => { - let value_ty = Arc::new(Self::from_schema_object(schema_object)?); + if let JsonValue::Bool(true) = additional_props_schema { + Self::JsonObject + } else { + let value_ty = Arc::new(Self::from_schema(additional_props_schema)?); Self::Map { value_ty } } } + ty => bail!("unsupported type: `{ty:?}`"), } - ty => bail!("unsupported type: `{ty:?}`"), - }, - Some(SingleOrVec::Vec(types)) => { - bail!("unsupported multi-typed parameter: `{types:?}`") } - None => match get_schema_name(obj.reference.as_deref()) { + Some(ty) => bail!("invalid / unsupported type `{ty:?}`"), + None => match get_schema_name(schema["$ref"].as_str()) { Some(name) => Self::SchemaRef { name, inner: None }, None => bail!("unsupported type-less parameter"), }, }; // If we didn't hit the early return above, check that there's no const or enum value(s). - ensure!(obj.const_value.is_none(), "unsupported const_value"); - ensure!(obj.enum_values.is_none(), "unsupported enum_values"); + ensure!(schema.get("const").is_none(), "unsupported const_value"); + ensure!(schema.get("enum").is_none(), "unsupported enum_values"); Ok(result) } diff --git a/src/cli_v1.rs b/src/cli_v1.rs index 3c231d1..3d45321 100644 --- a/src/cli_v1.rs +++ b/src/cli_v1.rs @@ -13,7 +13,6 @@ use anyhow::{Context as _, bail}; use camino::Utf8PathBuf; use clap::{Parser, Subcommand}; use fs_err::{self as fs}; -use schemars::schema::Schema; use tempfile::TempDir; use tracing::{Event, Level}; use tracing_subscriber::{ @@ -220,8 +219,8 @@ fn get_webhooks(spec: &OpenApi) -> Vec { && let Some(item) = body.as_item() && let Some(json_content) = item.content.get("application/json") && let Some(schema) = &json_content.schema - && let Schema::Object(obj) = &schema.json_schema - && let Some(reference) = &obj.reference + && let Some(reference_v) = &schema.json_schema.get("$ref") + && let Some(reference) = reference_v.as_str() && let Some(component_name) = reference.split('/').next_back() { referenced_components.insert(component_name.to_owned()); diff --git a/src/lib.rs b/src/lib.rs index fa1f1ef..035ba84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,10 @@ mod codesamples; mod generator; mod postprocessing; mod template; +mod utils; + +type JsonValue = serde_json::Value; +type JsonObject = serde_json::Map; pub use crate::{ cli_v1::{IncludeMode, run_cli_v1_main}, diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..953933f --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,14 @@ +use std::sync::LazyLock; + +use anyhow::Context as _; + +use crate::{JsonObject, JsonValue}; + +pub(crate) fn get_properties(obj: &JsonValue) -> anyhow::Result<&JsonObject> { + static EMPTY_OBJECT: LazyLock = LazyLock::new(JsonObject::new); + + match obj.get("properties") { + Some(v) => v.as_object().context("properties must be an object"), + None => Ok(&EMPTY_OBJECT), + } +}