improve and simplify openapiv3 -> schemars conversion (#592)
This commit is contained in:
parent
fc783273e0
commit
4dcba5928c
|
@ -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" }
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue