use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
use crate::openapi::serde_helpers::deserialize_enum_helper;
use super::{
ref_or::{ExpectedWhenParsing, RefOr},
set_or_scalar, Scope, Transpile,
};
pub trait Nullable: Sized {
fn new_schema_matching_only_null_for_merge_patch() -> Self;
fn new_schema_with_merge_patch_documentation(&self) -> (Self, Option<String>);
fn new_schema_matching_current_or_null_for_merge_patch(
&self,
scope: &Scope,
) -> Self;
fn allows_local_null(&self) -> bool;
}
pub type Schema = RefOr<BasicSchema>;
impl Schema {
pub fn matches_only_empty_object(&self) -> bool {
match self {
RefOr::Ref(_) | RefOr::InterfaceRef(_) => false,
RefOr::Value(s) => s.matches_only_empty_object(),
}
}
}
impl Nullable for Schema {
fn new_schema_matching_only_null_for_merge_patch() -> Self {
RefOr::Value(BasicSchema::new_schema_matching_only_null_for_merge_patch())
}
fn new_schema_matching_current_or_null_for_merge_patch(
&self,
scope: &Scope,
) -> Schema {
match self {
RefOr::Ref(_) | RefOr::InterfaceRef(_)
if scope.use_nullable_for_merge_patch =>
{
RefOr::Value(BasicSchema::OneOf(
OneOf::new_nullable_schema_for_merge_patch(self),
))
}
RefOr::Ref(_) | RefOr::InterfaceRef(_) => RefOr::Value(
BasicSchema::OneOf(OneOf::new_schema_or_null_for_merge_patch(self)),
),
RefOr::Value(val) => RefOr::Value(
val.new_schema_matching_current_or_null_for_merge_patch(scope),
),
}
}
fn new_schema_with_merge_patch_documentation(&self) -> (Self, Option<String>) {
match self {
RefOr::Ref(_) | RefOr::InterfaceRef(_) => (self.clone(), None),
RefOr::Value(val) => {
let (schema, description) =
val.new_schema_with_merge_patch_documentation();
(RefOr::Value(schema), description)
}
}
}
fn allows_local_null(&self) -> bool {
match self {
RefOr::Ref(_) | RefOr::InterfaceRef(_) => false,
RefOr::Value(value) => value.allows_local_null(),
}
}
}
#[test]
fn allowing_null_turns_refs_into_oneof() {
use super::ref_or::Ref;
let schema =
RefOr::<BasicSchema>::Ref(Ref::new("#/components/schemas/widget", None));
let scope = Scope::default();
assert_eq!(
schema.new_schema_matching_current_or_null_for_merge_patch(&scope),
RefOr::Value(BasicSchema::OneOf(OneOf {
r#type: None,
schemas: vec![
schema,
Schema::new_schema_matching_only_null_for_merge_patch()
],
description: None,
title: None,
discriminator: None,
nullable: None,
unknown_fields: Default::default(),
}))
)
}
#[test]
fn deserializes_array_schema() {
let yaml = r#"
type: array
items:
$interface: "Dataset"
"#;
serde_yaml::from_str::<Schema>(yaml).unwrap();
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum BasicSchema {
AllOf(AllOf),
OneOf(OneOf),
Primitive(Box<PrimitiveSchema>),
}
impl BasicSchema {
fn matches_only_empty_object(&self) -> bool {
match self {
BasicSchema::AllOf(all_of) => {
all_of.schemas.iter().any(|s| s.matches_only_empty_object())
}
BasicSchema::OneOf(one_of) => {
one_of.schemas.iter().all(|s| s.matches_only_empty_object())
}
BasicSchema::Primitive(s) => s.matches_only_empty_object(),
}
}
}
impl<'de> Deserialize<'de> for BasicSchema {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
use serde_yaml::{Mapping, Value};
let yaml = Mapping::deserialize(deserializer)?;
let yaml_str = |s| Value::String(String::from(s));
if yaml.contains_key(&yaml_str("allOf")) {
Ok(BasicSchema::AllOf(deserialize_enum_helper::<D, _>(
"allOf schema",
Value::Mapping(yaml),
)?))
} else if yaml.contains_key(&yaml_str("oneOf")) {
Ok(BasicSchema::OneOf(deserialize_enum_helper::<D, _>(
"oneOf schema",
Value::Mapping(yaml),
)?))
} else if yaml.contains_key(&yaml_str("type")) {
Ok(BasicSchema::Primitive(deserialize_enum_helper::<D, _>(
"schema",
Value::Mapping(yaml),
)?))
} else {
Err(D::Error::custom(format!(
"one of allOf, oneOf, or type in:\n{}",
serde_yaml::to_string(&yaml).expect("error serializing YAML")
)))
}
}
}
impl ExpectedWhenParsing for BasicSchema {
fn expected_when_parsing() -> &'static str {
"a schema with one of allOf, oneOf, or type"
}
}
impl Nullable for BasicSchema {
fn new_schema_matching_only_null_for_merge_patch() -> Self {
BasicSchema::Primitive(Box::new(PrimitiveSchema::null_for_merge_patch()))
}
fn new_schema_with_merge_patch_documentation(&self) -> (Self, Option<String>) {
match self {
BasicSchema::AllOf(_) | BasicSchema::OneOf(_) => (self.clone(), None),
BasicSchema::Primitive(base) => {
let (base, description) = base.with_merge_patch_documentation();
(BasicSchema::Primitive(Box::new(base)), description)
}
}
}
fn allows_local_null(&self) -> bool {
match self {
BasicSchema::AllOf(all_of) => {
all_of.schemas.iter().all(|s| s.allows_local_null())
}
BasicSchema::OneOf(one_of) => {
one_of.schemas.iter().any(|s| s.allows_local_null())
}
BasicSchema::Primitive(base) => base.types.contains(&Type::Null),
}
}
fn new_schema_matching_current_or_null_for_merge_patch(
&self,
scope: &Scope,
) -> BasicSchema {
if scope.use_nullable_for_merge_patch {
match self {
BasicSchema::Primitive(base) => {
let mut base = base.clone();
base.nullable = Some(true);
BasicSchema::Primitive(base)
}
BasicSchema::OneOf(one_of) => {
let mut base = one_of.clone();
base.nullable = Some(true);
BasicSchema::OneOf(base)
}
BasicSchema::AllOf(all_of) => {
let mut base = all_of.clone();
base.nullable = Some(true);
BasicSchema::AllOf(base)
}
}
} else {
match self {
schema if schema.allows_local_null() => schema.to_owned(),
BasicSchema::Primitive(base) if base.types.len() != 1 => {
let mut base = base.as_ref().to_owned();
base.types.insert(Type::Null);
BasicSchema::Primitive(Box::new(base))
}
BasicSchema::OneOf(one_of) => {
let mut one_of = one_of.to_owned();
one_of
.schemas
.push(Schema::new_schema_matching_only_null_for_merge_patch());
BasicSchema::OneOf(one_of)
}
_ => BasicSchema::OneOf(OneOf::new_schema_or_null_for_merge_patch(
&RefOr::Value(self.to_owned()),
)),
}
}
}
}
impl Transpile for BasicSchema {
type Output = Self;
fn transpile(&self, scope: &Scope) -> anyhow::Result<Self::Output> {
match self {
BasicSchema::AllOf(all_of) => {
Ok(BasicSchema::AllOf(all_of.transpile(scope)?))
}
BasicSchema::OneOf(one_of) => {
Ok(BasicSchema::OneOf(one_of.transpile(scope)?))
}
BasicSchema::Primitive(schema) => {
Ok(BasicSchema::Primitive(Box::new(schema.transpile(scope)?)))
}
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AllOf {
#[serde(rename = "allOf")]
schemas: Vec<Schema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nullable: Option<bool>,
#[serde(flatten)]
unknown_fields: BTreeMap<String, Value>,
}
impl Transpile for AllOf {
type Output = Self;
fn transpile(&self, scope: &Scope) -> anyhow::Result<Self::Output> {
Ok(Self {
schemas: self.schemas.transpile(scope)?,
nullable: self.nullable,
unknown_fields: self.unknown_fields.clone(),
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct OneOf {
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<Type>,
#[serde(rename = "oneOf")]
pub schemas: Vec<Schema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discriminator: Option<Discriminator>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nullable: Option<bool>,
#[serde(flatten)]
pub unknown_fields: BTreeMap<String, Value>,
}
impl OneOf {
fn new_schema_or_null_for_merge_patch(schema: &Schema) -> OneOf {
let (schema, description) = schema.new_schema_with_merge_patch_documentation();
let schemas = vec![
schema,
Schema::new_schema_matching_only_null_for_merge_patch(),
];
OneOf {
r#type: None,
schemas,
description,
title: None,
discriminator: None,
nullable: None,
unknown_fields: Default::default(),
}
}
fn new_nullable_schema_for_merge_patch(schema: &Schema) -> OneOf {
let (schema, description) = schema.new_schema_with_merge_patch_documentation();
let schemas = vec![schema];
OneOf {
r#type: None,
schemas,
description,
title: None,
discriminator: None,
nullable: Some(true),
unknown_fields: Default::default(),
}
}
}
impl Transpile for OneOf {
type Output = Self;
fn transpile(&self, scope: &Scope) -> anyhow::Result<Self::Output> {
Ok(Self {
r#type: self.r#type.clone(),
schemas: self.schemas.transpile(scope)?,
description: self.description.clone(),
title: self.title.clone(),
discriminator: self.discriminator.clone(),
nullable: self.nullable,
unknown_fields: self.unknown_fields.clone(),
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Discriminator {
pub property_name: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub mapping: BTreeMap<String, String>,
#[serde(flatten)]
pub unknown_fields: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PrimitiveSchema {
#[serde(rename = "type", with = "set_or_scalar")]
pub types: BTreeSet<Type>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub properties: BTreeMap<String, Schema>,
#[serde(default, skip_serializing_if = "AdditionalProperties::is_default")]
pub additional_properties: AdditionalProperties,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub items: Option<Schema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nullable: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#const: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub example: Option<Value>,
#[serde(flatten)]
pub unknown_fields: BTreeMap<String, Value>,
}
impl PrimitiveSchema {
fn matches_only_empty_object(&self) -> bool {
self.types.contains(&Type::Object)
&& self.types.len() == 1
&& self.properties.is_empty()
&& self.additional_properties == AdditionalProperties::Bool(false)
}
fn null() -> PrimitiveSchema {
let mut types = BTreeSet::new();
types.insert(Type::Null);
PrimitiveSchema {
types,
required: Default::default(),
properties: Default::default(),
additional_properties: Default::default(),
items: Default::default(),
nullable: None,
description: Default::default(),
title: Default::default(),
r#const: Default::default(),
example: Default::default(),
unknown_fields: Default::default(),
}
}
fn null_for_merge_patch() -> Self {
let mut null_schema = Self::null();
null_schema.title = Some("Clear".to_owned());
null_schema.description =
Some("Pass `null` to clear this field's existing value.".to_owned());
null_schema
}
fn with_merge_patch_documentation(&self) -> (Self, Option<String>) {
let mut merge_patch_schema = self.clone();
merge_patch_schema.title = Some("Overwrite".to_owned());
merge_patch_schema.description =
Some("Pass this value to overwrite the existing value.".to_owned());
(merge_patch_schema, self.description.clone())
}
}
impl Transpile for PrimitiveSchema {
type Output = Self;
fn transpile(&self, scope: &Scope) -> anyhow::Result<Self::Output> {
let mut types = self.types.clone();
if self.nullable == Some(true) {
types.insert(Type::Null);
}
Ok(Self {
types,
required: self.required.clone(),
properties: self.properties.transpile(scope)?,
additional_properties: self.additional_properties.transpile(scope)?,
items: self.items.transpile(scope)?,
nullable: None,
description: self.description.clone(),
title: self.title.clone(),
r#const: self.r#const.clone(),
example: self.example.clone(),
unknown_fields: self.unknown_fields.clone(),
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "lowercase")]
#[allow(clippy::missing_docs_in_private_items)]
pub enum Type {
String,
Number,
Integer,
Object,
Array,
Boolean,
Null,
}
impl ExpectedWhenParsing for Type {
fn expected_when_parsing() -> &'static str {
"one of `string`, `number`, `integer`, `object`, `array`, `boolean` or `\"null\"`"
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(untagged)]
pub enum AdditionalProperties {
Bool(bool),
Schema(Schema),
}
impl AdditionalProperties {
fn is_default(&self) -> bool {
matches!(self, &AdditionalProperties::Bool(true))
}
}
impl Default for AdditionalProperties {
fn default() -> Self {
AdditionalProperties::Bool(true)
}
}
impl<'de> Deserialize<'de> for AdditionalProperties {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde_yaml::Value;
let yaml = Value::deserialize(deserializer)?;
if let Value::Bool(b) = &yaml {
Ok(AdditionalProperties::Bool(*b))
} else {
Ok(AdditionalProperties::Schema(
deserialize_enum_helper::<D, _>("additionalProperties", yaml)?,
))
}
}
}
impl Transpile for AdditionalProperties {
type Output = Self;
fn transpile(&self, scope: &Scope) -> anyhow::Result<Self::Output> {
match self {
AdditionalProperties::Bool(b) => Ok(AdditionalProperties::Bool(*b)),
AdditionalProperties::Schema(s) => {
Ok(AdditionalProperties::Schema(s.transpile(scope)?))
}
}
}
}