improve and simplify openapiv3 -> schemars conversion (#592)

This commit is contained in:
Adam Leventhal 2023-10-04 08:12:59 -07:00 committed by GitHub
parent fc783273e0
commit 4dcba5928c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 269 additions and 150 deletions

View File

@ -10,6 +10,8 @@ keywords = ["openapi", "openapiv3", "sdk", "generator"]
categories = ["api-bindings", "development-tools::cargo-plugins"] categories = ["api-bindings", "development-tools::cargo-plugins"]
build = "build.rs" build = "build.rs"
default-run = "cargo-progenitor"
[dependencies] [dependencies]
progenitor = { version = "0.4.0", path = "../progenitor" } progenitor = { version = "0.4.0", path = "../progenitor" }
progenitor-client = { version = "0.4.0", path = "../progenitor-client" } progenitor-client = { version = "0.4.0", path = "../progenitor-client" }

View File

@ -2,6 +2,7 @@
use indexmap::IndexMap; use indexmap::IndexMap;
use openapiv3::AnySchema; use openapiv3::AnySchema;
use schemars::schema::SingleOrVec;
use serde_json::Value; use serde_json::Value;
pub trait ToSchema { pub trait ToSchema {
@ -402,7 +403,7 @@ impl Convert<schemars::schema::Schema> for openapiv3::Schema {
schemars::schema::SchemaObject { schemars::schema::SchemaObject {
metadata, metadata,
extensions, extensions,
..schemars::schema::Schema::Bool(true).into_object() ..Default::default()
} }
} }
@ -450,170 +451,220 @@ impl Convert<schemars::schema::Schema> for openapiv3::Schema {
} }
} }
// Malformed object with 'type' not set.
openapiv3::SchemaKind::Any(AnySchema { openapiv3::SchemaKind::Any(AnySchema {
typ: None, typ,
pattern: None, pattern,
multiple_of: None,
exclusive_minimum: None,
exclusive_maximum: None,
minimum: None,
maximum: None,
properties,
required,
additional_properties,
min_properties,
max_properties,
items: None,
min_items: None,
max_items: None,
unique_items: None,
format: None,
enumeration,
min_length: None,
max_length: None,
one_of,
all_of,
any_of,
not: None,
}) if enumeration.is_empty()
&& one_of.is_empty()
&& all_of.is_empty()
&& any_of.is_empty() =>
{
let object = openapiv3::Schema {
schema_data: self.schema_data.clone(),
schema_kind: openapiv3::SchemaKind::Type(
openapiv3::Type::Object(openapiv3::ObjectType {
properties: properties.clone(),
required: required.clone(),
additional_properties: additional_properties
.clone(),
min_properties: *min_properties,
max_properties: *max_properties,
}),
),
};
object.convert().into()
}
// Malformed array with 'type' not set.
openapiv3::SchemaKind::Any(AnySchema {
typ: None,
pattern: None,
multiple_of: None,
exclusive_minimum: None,
exclusive_maximum: None,
minimum: None,
maximum: None,
properties,
required,
additional_properties: None,
min_properties: None,
max_properties: None,
items: items @ Some(_),
min_items,
max_items,
unique_items,
format: None,
enumeration,
min_length: None,
max_length: None,
one_of,
all_of,
any_of,
not: None,
}) if properties.is_empty()
&& required.is_empty()
&& enumeration.is_empty()
&& one_of.is_empty()
&& all_of.is_empty()
&& any_of.is_empty() =>
{
let array = openapiv3::Schema {
schema_data: self.schema_data.clone(),
schema_kind: openapiv3::SchemaKind::Type(
openapiv3::Type::Array(openapiv3::ArrayType {
items: items.clone(),
min_items: *min_items,
max_items: *max_items,
unique_items: unique_items.unwrap_or(false),
}),
),
};
array.convert().into()
}
// Handle integers with floating-point constraint values
// (multiple_of, minimum, or maximum). We could check that these
// values are integers... but schemars::schema::NumberValidation
// doesn't care so neither do we.
openapiv3::SchemaKind::Any(AnySchema {
typ: Some(typ),
pattern: _,
multiple_of, multiple_of,
exclusive_minimum, exclusive_minimum,
exclusive_maximum, exclusive_maximum,
minimum, minimum,
maximum, maximum,
properties: _, properties,
required: _, required,
additional_properties: _, additional_properties,
min_properties: _, min_properties,
max_properties: _, max_properties,
items: _, items,
min_items: _, min_items,
max_items: _, max_items,
unique_items: _, unique_items,
enumeration, enumeration,
format, format,
min_length: _, min_length,
max_length: _, max_length,
one_of, one_of,
all_of, all_of,
any_of, any_of,
not: None, not,
}) if typ == "integer" }) => {
&& one_of.is_empty() let mut so = schemars::schema::SchemaObject {
&& all_of.is_empty()
&& any_of.is_empty() =>
{
let (maximum, exclusive_maximum) =
match (maximum, exclusive_maximum) {
(v, Some(true)) => (None, *v),
(v, _) => (*v, None),
};
let (minimum, exclusive_minimum) =
match (minimum, exclusive_minimum) {
(v, Some(true)) => (None, *v),
(v, _) => (*v, None),
};
schemars::schema::SchemaObject {
metadata, metadata,
instance_type: instance_type(
schemars::schema::InstanceType::Integer,
nullable,
),
format: format.clone(),
enum_values: (!enumeration.is_empty())
.then(|| enumeration.clone()),
number: Some(Box::new(
schemars::schema::NumberValidation {
multiple_of: *multiple_of,
maximum,
exclusive_maximum,
minimum,
exclusive_minimum,
},
))
.reduce(),
extensions, extensions,
..Default::default() ..Default::default()
};
// General
if let Some(format) = format {
so.format = Some(format.clone());
}
if !enumeration.is_empty() {
so.enum_values = Some(enumeration.clone());
}
// String
if let Some(pattern) = pattern {
so.string().pattern = Some(pattern.clone());
}
if let Some(min_length) = min_length {
so.string().min_length = Some(*min_length as u32);
}
if let Some(max_length) = max_length {
so.string().max_length = Some(*max_length as u32);
}
// Number
if let Some(multiple_of) = multiple_of {
so.number().multiple_of = Some(*multiple_of);
}
match (minimum, exclusive_minimum) {
(None, Some(true)) => {
todo!("exclusive_minimum set without minimum");
}
(None, _) => (),
(Some(minimum), Some(true)) => {
so.number().exclusive_minimum = Some(*minimum);
}
(Some(minimum), _) => {
so.number().minimum = Some(*minimum);
}
}
match (maximum, exclusive_maximum) {
(None, Some(true)) => {
todo!("exclusive_maximum set without maximum");
}
(None, _) => (),
(Some(maximum), Some(true)) => {
so.number().exclusive_maximum = Some(*maximum);
}
(Some(maximum), _) => {
so.number().maximum = Some(*maximum);
} }
} }
openapiv3::SchemaKind::Any(_) => { // Object
panic!("not clear what we could usefully do here {:#?}", self) if !properties.is_empty() {
so.object().properties = properties.convert();
}
if !required.is_empty() {
so.object().required = required.convert();
}
if additional_properties.is_some() {
so.object().additional_properties =
additional_properties.convert();
}
if let Some(min_properties) = min_properties {
so.object().min_properties = Some(*min_properties as u32);
}
if let Some(max_properties) = max_properties {
so.object().max_properties = Some(*max_properties as u32);
}
// Array
if items.is_some() {
so.array().items =
items.convert().clone().map(SingleOrVec::Single);
}
if let Some(min_items) = min_items {
so.array().min_items = Some(*min_items as u32);
}
if let Some(max_items) = max_items {
so.array().max_items = Some(*max_items as u32);
}
if let Some(unique_items) = unique_items {
so.array().unique_items = Some(*unique_items);
}
// Subschemas
if !one_of.is_empty() {
so.subschemas().one_of = one_of.convert();
}
if !all_of.is_empty() {
so.subschemas().all_of = all_of.convert();
}
if !any_of.is_empty() {
so.subschemas().any_of = any_of.convert();
}
if not.is_some() {
so.subschemas().not = not.convert();
}
// We do this last so we can infer types if none are specified.
match (typ.as_deref(), nullable) {
(Some("boolean"), _) => {
so.instance_type = instance_type(
schemars::schema::InstanceType::Boolean,
nullable,
);
}
(Some("object"), _) => {
so.instance_type = instance_type(
schemars::schema::InstanceType::Object,
nullable,
);
}
(Some("array"), _) => {
so.instance_type = instance_type(
schemars::schema::InstanceType::Array,
nullable,
);
}
(Some("number"), _) => {
so.instance_type = instance_type(
schemars::schema::InstanceType::Number,
nullable,
);
}
(Some("string"), _) => {
so.instance_type = instance_type(
schemars::schema::InstanceType::String,
nullable,
);
}
(Some("integer"), _) => {
so.instance_type = instance_type(
schemars::schema::InstanceType::Integer,
nullable,
);
}
(Some(typ), _) => todo!("invalid type: {}", typ),
// No types
(None, false) => (),
// We only try to infer types if we need to add in an
// additional null type; otherwise we can leave the type
// implied.
(None, true) => {
let instance_types = [
so.object.is_some().then_some(
schemars::schema::InstanceType::Object,
),
so.array.is_some().then_some(
schemars::schema::InstanceType::Array,
),
so.number.is_some().then_some(
schemars::schema::InstanceType::Array,
),
so.string.is_some().then_some(
schemars::schema::InstanceType::Array,
),
nullable.then_some(
schemars::schema::InstanceType::Null,
),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
// TODO we could also look at enumerated values.
so.instance_type = match (
instance_types.first(),
instance_types.len(),
) {
(Some(typ), 1) => {
Some(SingleOrVec::Single(Box::new(*typ)))
}
(Some(_), _) => {
Some(SingleOrVec::Vec(instance_types))
}
(None, _) => None,
};
}
};
so
} }
} }
.into() .into()
@ -814,4 +865,70 @@ mod tests {
let conv_schema = oa_schema.convert(); let conv_schema = oa_schema.convert();
assert_eq!(conv_schema, js_schema); assert_eq!(conv_schema, js_schema);
} }
#[test]
fn test_object_no_type() {
let schema_value = json!({
"properties": {
"foo": {}
}
});
let oa_schema =
serde_json::from_value::<openapiv3::Schema>(schema_value.clone())
.unwrap();
let js_schema =
serde_json::from_value::<schemars::schema::Schema>(schema_value)
.unwrap();
let conv_schema = oa_schema.convert();
assert_eq!(conv_schema, js_schema);
}
#[test]
fn test_array_no_type() {
let schema_value = json!({
"items": {}
});
let oa_schema =
serde_json::from_value::<openapiv3::Schema>(schema_value.clone())
.unwrap();
let js_schema =
serde_json::from_value::<schemars::schema::Schema>(schema_value)
.unwrap();
let conv_schema = oa_schema.convert();
assert_eq!(conv_schema, js_schema);
}
#[test]
fn test_number_no_type() {
let schema_value = json!({
"minimum": 100.0
});
let oa_schema =
serde_json::from_value::<openapiv3::Schema>(schema_value.clone())
.unwrap();
let js_schema =
serde_json::from_value::<schemars::schema::Schema>(schema_value)
.unwrap();
let conv_schema = oa_schema.convert();
assert_eq!(conv_schema, js_schema);
}
#[test]
fn test_solo_enum() {
let schema_value = json!({
"enum": ["one"]
});
let oa_schema =
serde_json::from_value::<openapiv3::Schema>(schema_value.clone())
.unwrap();
let js_schema =
serde_json::from_value::<schemars::schema::Schema>(schema_value)
.unwrap();
let conv_schema = oa_schema.convert();
assert_eq!(conv_schema, js_schema);
}
} }