Change $ref to be part of a SchemaObject.

This allows other keyworlds to be used alongside $ref, as allowed in Json Schema 2019-09
This commit is contained in:
Graham Esau 2019-10-13 18:30:05 +01:00
parent 7d162a8fb5
commit 5a82498e28
6 changed files with 115 additions and 137 deletions

View file

@ -4,9 +4,9 @@ use crate::{JsonSchemaError, Map, Result, Set};
impl Schema { impl Schema {
pub fn flatten(self, other: Self) -> Result { pub fn flatten(self, other: Self) -> Result {
if is_null_type(&self) { if is_null_type(&self) {
return Ok(other) return Ok(other);
} else if is_null_type(&other) { } else if is_null_type(&other) {
return Ok(self) return Ok(self);
} }
let s1 = ensure_object_type(self)?; let s1 = ensure_object_type(self)?;
let s2 = ensure_object_type(other)?; let s2 = ensure_object_type(other)?;
@ -38,7 +38,7 @@ impl_merge!(SchemaObject {
merge: definitions extensions instance_type enum_values merge: definitions extensions instance_type enum_values
number string array object, number string array object,
or: schema id title description format const_value all_of any_of one_of not or: schema id title description format const_value all_of any_of one_of not
if_schema then_schema else_schema, if_schema then_schema else_schema reference,
}); });
impl_merge!(NumberValidation { impl_merge!(NumberValidation {

View file

@ -18,6 +18,16 @@ pub enum BoolSchemas {
impl Default for SchemaSettings { impl Default for SchemaSettings {
fn default() -> SchemaSettings { fn default() -> SchemaSettings {
SchemaSettings::new()
}
}
impl SchemaSettings {
pub fn new() -> SchemaSettings {
Self::draft07()
}
pub fn draft07() -> SchemaSettings {
SchemaSettings { SchemaSettings {
option_nullable: false, option_nullable: false,
option_add_null_type: true, option_add_null_type: true,
@ -25,14 +35,7 @@ impl Default for SchemaSettings {
definitions_path: "#/definitions/".to_owned(), definitions_path: "#/definitions/".to_owned(),
} }
} }
}
impl SchemaSettings {
pub fn new() -> SchemaSettings {
SchemaSettings {
..Default::default()
}
}
pub fn openapi3() -> SchemaSettings { pub fn openapi3() -> SchemaSettings {
SchemaSettings { SchemaSettings {
option_nullable: true, option_nullable: true,
@ -66,21 +69,20 @@ impl SchemaGenerator {
} }
pub fn schema_for_any(&self) -> Schema { pub fn schema_for_any(&self) -> Schema {
let schema: Schema = true.into();
if self.settings().bool_schemas == BoolSchemas::Enable { if self.settings().bool_schemas == BoolSchemas::Enable {
true.into() schema
} else { } else {
Schema::Object(Default::default()) Schema::Object(schema.into())
} }
} }
pub fn schema_for_none(&self) -> Schema { pub fn schema_for_none(&self) -> Schema {
let schema: Schema = false.into();
if self.settings().bool_schemas == BoolSchemas::Enable { if self.settings().bool_schemas == BoolSchemas::Enable {
false.into() schema
} else { } else {
Schema::Object(SchemaObject { Schema::Object(schema.into())
not: Some(Schema::Object(Default::default()).into()),
..Default::default()
})
} }
} }
@ -94,7 +96,7 @@ impl SchemaGenerator {
if !self.definitions.contains_key(&name) { if !self.definitions.contains_key(&name) {
self.insert_new_subschema_for::<T>(name)?; self.insert_new_subschema_for::<T>(name)?;
} }
Ok(Ref { reference }.into()) Ok(Schema::new_ref(reference))
} }
fn insert_new_subschema_for<T: ?Sized + JsonSchema>(&mut self, name: String) -> Result<()> { fn insert_new_subschema_for<T: ?Sized + JsonSchema>(&mut self, name: String) -> Result<()> {
@ -148,49 +150,41 @@ impl SchemaGenerator {
}) })
} }
pub fn get_schema_object(&self, mut schema: Schema) -> Result<SchemaObject> { pub fn dereference_once(&self, schema: Schema) -> Result<Schema> {
match schema {
Schema::Object(SchemaObject {
reference: Some(ref schema_ref),
..
}) => {
let definitions_path = &self.settings().definitions_path;
if !schema_ref.starts_with(definitions_path) {
return Err(JsonSchemaError::new(
"Could not extract referenced schema name.",
schema,
));
}
let name = &schema_ref[definitions_path.len()..];
self.definitions.get(name).cloned().ok_or_else(|| {
JsonSchemaError::new("Could not find referenced schema.", schema)
})
}
s => Ok(s),
}
}
pub fn dereference(&self, mut schema: Schema) -> Result<Schema> {
if !schema.is_ref() {
return Ok(schema);
}
for _ in 0..100 { for _ in 0..100 {
match schema { schema = self.dereference_once(schema)?;
Schema::Object(obj) => return Ok(obj), if !schema.is_ref() {
Schema::Bool(true) => return Ok(Default::default()), return Ok(schema);
Schema::Bool(false) => {
return Ok(SchemaObject {
not: Some(Schema::Bool(true).into()),
..Default::default()
})
}
Schema::Ref(schema_ref) => {
let definitions_path = &self.settings().definitions_path;
if !schema_ref.reference.starts_with(definitions_path) {
return Err(JsonSchemaError::new(
"Could not extract referenced schema name.",
Schema::Ref(schema_ref),
));
}
let name = &schema_ref.reference[definitions_path.len()..];
schema = self
.definitions
.get(name)
.ok_or_else(|| {
JsonSchemaError::new(
"Could not find referenced schema.",
Schema::Ref(schema_ref.clone()),
)
})?
.clone();
if schema == Schema::Ref(schema_ref) {
return Err(JsonSchemaError::new(
"Schema is referencing itself.",
schema,
));
}
}
} }
} }
Err(JsonSchemaError::new( Err(JsonSchemaError::new(
"Failed to dereference schema after 100 iterations - reference may be cyclic.", "Failed to dereference schema after 100 iterations - references may be cyclic.",
schema, schema,
)) ))
} }

View file

@ -34,7 +34,7 @@ impl<T: JsonSchema> JsonSchema for Option<T> {
} }
} }
if gen.settings().option_nullable { if gen.settings().option_nullable {
let mut deref = gen.get_schema_object(schema)?; let mut deref: SchemaObject = gen.dereference(schema)?.into();
deref.extensions.insert("nullable".to_owned(), json!(true)); deref.extensions.insert("nullable".to_owned(), json!(true));
schema = Schema::Object(deref); schema = Schema::Object(deref);
}; };
@ -110,12 +110,7 @@ mod tests {
assert_eq!(schema.any_of.is_some(), true); assert_eq!(schema.any_of.is_some(), true);
let any_of = schema.any_of.unwrap(); let any_of = schema.any_of.unwrap();
assert_eq!(any_of.len(), 2); assert_eq!(any_of.len(), 2);
assert_eq!( assert_eq!(any_of[0], Schema::new_ref("#/definitions/Foo".to_string()));
any_of[0],
Schema::Ref(Ref {
reference: "#/definitions/Foo".to_string()
})
);
assert_eq!(any_of[1], schema_for::<()>()); assert_eq!(any_of[1], schema_for::<()>());
} }

View file

@ -7,10 +7,22 @@ use serde_json::Value;
#[serde(untagged)] #[serde(untagged)]
pub enum Schema { pub enum Schema {
Bool(bool), Bool(bool),
Ref(Ref),
Object(SchemaObject), Object(SchemaObject),
} }
impl Schema {
pub fn new_ref(reference: String) -> Self {
SchemaObject::new_ref(reference).into()
}
pub fn is_ref(&self) -> bool {
match self {
Schema::Object(o) => o.is_ref(),
_ => false,
}
}
}
impl From<SchemaObject> for Schema { impl From<SchemaObject> for Schema {
fn from(o: SchemaObject) -> Self { fn from(o: SchemaObject) -> Self {
Schema::Object(o) Schema::Object(o)
@ -23,18 +35,6 @@ impl From<bool> for Schema {
} }
} }
impl From<Ref> for Schema {
fn from(r: Ref) -> Self {
Schema::Ref(r)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
pub struct Ref {
#[serde(rename = "$ref")]
pub reference: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[serde(rename_all = "camelCase", default)] #[serde(rename_all = "camelCase", default)]
pub struct SchemaObject { pub struct SchemaObject {
@ -68,7 +68,7 @@ pub struct SchemaObject {
pub then_schema: Option<Box<Schema>>, pub then_schema: Option<Box<Schema>>,
#[serde(rename = "else", skip_serializing_if = "Option::is_none")] #[serde(rename = "else", skip_serializing_if = "Option::is_none")]
pub else_schema: Option<Box<Schema>>, pub else_schema: Option<Box<Schema>>,
#[serde(skip_serializing_if = "Map::is_empty")] #[serde(alias = "$defs", skip_serializing_if = "Map::is_empty")]
pub definitions: Map<String, Schema>, pub definitions: Map<String, Schema>,
#[serde(flatten)] #[serde(flatten)]
pub number: NumberValidation, pub number: NumberValidation,
@ -78,10 +78,45 @@ pub struct SchemaObject {
pub array: ArrayValidation, pub array: ArrayValidation,
#[serde(flatten)] #[serde(flatten)]
pub object: ObjectValidation, pub object: ObjectValidation,
#[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
pub reference: Option<String>,
#[serde(flatten)] #[serde(flatten)]
pub extensions: Map<String, Value>, pub extensions: Map<String, Value>,
} }
impl SchemaObject {
pub fn new_ref(reference: String) -> Self {
SchemaObject {
reference: Some(reference),
..Default::default()
}
}
pub fn is_ref(&self) -> bool {
if self.reference.is_none() {
return false;
}
let only_ref = SchemaObject {
reference: self.reference.clone(),
..Default::default()
};
*self == only_ref
}
}
impl From<Schema> for SchemaObject {
fn from(schema: Schema) -> Self {
match schema {
Schema::Object(o) => o,
Schema::Bool(true) => SchemaObject::default(),
Schema::Bool(false) => SchemaObject {
not: Some(Schema::Object(Default::default()).into()),
..Default::default()
},
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[serde(rename_all = "camelCase", default)] #[serde(rename_all = "camelCase", default)]
pub struct NumberValidation { pub struct NumberValidation {
@ -144,7 +179,9 @@ pub struct ObjectValidation {
pub property_names: Option<Box<Schema>>, pub property_names: Option<Box<Schema>>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)] #[derive(
Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema,
)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum InstanceType { pub enum InstanceType {
Null, Null,

View file

@ -5,9 +5,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }
@ -24,25 +21,11 @@
"integer" "integer"
] ]
}, },
"Ref": {
"type": "object",
"required": [
"$ref"
],
"properties": {
"$ref": {
"type": "string"
}
}
},
"Schema": { "Schema": {
"anyOf": [ "anyOf": [
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }
@ -55,6 +38,10 @@
"type": "string", "type": "string",
"nullable": true "nullable": true
}, },
"$ref": {
"type": "string",
"nullable": true
},
"$schema": { "$schema": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@ -64,9 +51,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }
@ -78,9 +62,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }
@ -109,9 +90,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }
@ -133,9 +111,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }
@ -166,9 +141,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }
@ -239,9 +211,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }
@ -276,9 +245,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }
@ -296,9 +262,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/components/schemas/Ref"
},
{ {
"$ref": "#/components/schemas/SchemaObject" "$ref": "#/components/schemas/SchemaObject"
} }

View file

@ -5,9 +5,6 @@
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/definitions/Ref"
},
{ {
"$ref": "#/definitions/SchemaObject" "$ref": "#/definitions/SchemaObject"
} }
@ -24,25 +21,11 @@
"integer" "integer"
] ]
}, },
"Ref": {
"type": "object",
"required": [
"$ref"
],
"properties": {
"$ref": {
"type": "string"
}
}
},
"Schema": { "Schema": {
"anyOf": [ "anyOf": [
{ {
"type": "boolean" "type": "boolean"
}, },
{
"$ref": "#/definitions/Ref"
},
{ {
"$ref": "#/definitions/SchemaObject" "$ref": "#/definitions/SchemaObject"
} }
@ -57,6 +40,12 @@
"null" "null"
] ]
}, },
"$ref": {
"type": [
"string",
"null"
]
},
"$schema": { "$schema": {
"type": [ "type": [
"string", "string",