From 5a82498e2847f9e192ec74908f40a5f3c410a786 Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Sun, 13 Oct 2019 18:30:05 +0100 Subject: [PATCH] Change $ref to be part of a SchemaObject. This allows other keyworlds to be used alongside $ref, as allowed in Json Schema 2019-09 --- schemars/src/flatten.rs | 6 +- schemars/src/gen.rs | 102 +++++++++---------- schemars/src/json_schema_impls/core.rs | 9 +- schemars/src/schema.rs | 67 +++++++++--- schemars/tests/expected/schema-openapi3.json | 45 +------- schemars/tests/expected/schema.json | 23 ++--- 6 files changed, 115 insertions(+), 137 deletions(-) diff --git a/schemars/src/flatten.rs b/schemars/src/flatten.rs index bde467c..90b15c6 100644 --- a/schemars/src/flatten.rs +++ b/schemars/src/flatten.rs @@ -4,9 +4,9 @@ use crate::{JsonSchemaError, Map, Result, Set}; impl Schema { pub fn flatten(self, other: Self) -> Result { if is_null_type(&self) { - return Ok(other) + return Ok(other); } else if is_null_type(&other) { - return Ok(self) + return Ok(self); } let s1 = ensure_object_type(self)?; let s2 = ensure_object_type(other)?; @@ -38,7 +38,7 @@ impl_merge!(SchemaObject { merge: definitions extensions instance_type enum_values number string array object, 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 { diff --git a/schemars/src/gen.rs b/schemars/src/gen.rs index 4c4dd1d..6fbe574 100644 --- a/schemars/src/gen.rs +++ b/schemars/src/gen.rs @@ -18,6 +18,16 @@ pub enum BoolSchemas { impl Default for SchemaSettings { fn default() -> SchemaSettings { + SchemaSettings::new() + } +} + +impl SchemaSettings { + pub fn new() -> SchemaSettings { + Self::draft07() + } + + pub fn draft07() -> SchemaSettings { SchemaSettings { option_nullable: false, option_add_null_type: true, @@ -25,14 +35,7 @@ impl Default for SchemaSettings { definitions_path: "#/definitions/".to_owned(), } } -} -impl SchemaSettings { - pub fn new() -> SchemaSettings { - SchemaSettings { - ..Default::default() - } - } pub fn openapi3() -> SchemaSettings { SchemaSettings { option_nullable: true, @@ -66,21 +69,20 @@ impl SchemaGenerator { } pub fn schema_for_any(&self) -> Schema { + let schema: Schema = true.into(); if self.settings().bool_schemas == BoolSchemas::Enable { - true.into() + schema } else { - Schema::Object(Default::default()) + Schema::Object(schema.into()) } } pub fn schema_for_none(&self) -> Schema { + let schema: Schema = false.into(); if self.settings().bool_schemas == BoolSchemas::Enable { - false.into() + schema } else { - Schema::Object(SchemaObject { - not: Some(Schema::Object(Default::default()).into()), - ..Default::default() - }) + Schema::Object(schema.into()) } } @@ -94,7 +96,7 @@ impl SchemaGenerator { if !self.definitions.contains_key(&name) { self.insert_new_subschema_for::(name)?; } - Ok(Ref { reference }.into()) + Ok(Schema::new_ref(reference)) } fn insert_new_subschema_for(&mut self, name: String) -> Result<()> { @@ -148,49 +150,41 @@ impl SchemaGenerator { }) } - pub fn get_schema_object(&self, mut schema: Schema) -> Result { + pub fn dereference_once(&self, schema: Schema) -> Result { + 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 { + if !schema.is_ref() { + return Ok(schema); + } for _ in 0..100 { - match schema { - Schema::Object(obj) => return Ok(obj), - Schema::Bool(true) => return Ok(Default::default()), - 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, - )); - } - } + schema = self.dereference_once(schema)?; + if !schema.is_ref() { + return Ok(schema); } } 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, )) } diff --git a/schemars/src/json_schema_impls/core.rs b/schemars/src/json_schema_impls/core.rs index e4bedc0..e029344 100644 --- a/schemars/src/json_schema_impls/core.rs +++ b/schemars/src/json_schema_impls/core.rs @@ -34,7 +34,7 @@ impl JsonSchema for Option { } } 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)); schema = Schema::Object(deref); }; @@ -110,12 +110,7 @@ mod tests { assert_eq!(schema.any_of.is_some(), true); let any_of = schema.any_of.unwrap(); assert_eq!(any_of.len(), 2); - assert_eq!( - any_of[0], - Schema::Ref(Ref { - reference: "#/definitions/Foo".to_string() - }) - ); + assert_eq!(any_of[0], Schema::new_ref("#/definitions/Foo".to_string())); assert_eq!(any_of[1], schema_for::<()>()); } diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs index c6ae2dd..7749f88 100644 --- a/schemars/src/schema.rs +++ b/schemars/src/schema.rs @@ -7,10 +7,22 @@ use serde_json::Value; #[serde(untagged)] pub enum Schema { Bool(bool), - Ref(Ref), 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 for Schema { fn from(o: SchemaObject) -> Self { Schema::Object(o) @@ -23,18 +35,6 @@ impl From for Schema { } } -impl From 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)] #[serde(rename_all = "camelCase", default)] pub struct SchemaObject { @@ -68,7 +68,7 @@ pub struct SchemaObject { pub then_schema: Option>, #[serde(rename = "else", skip_serializing_if = "Option::is_none")] pub else_schema: Option>, - #[serde(skip_serializing_if = "Map::is_empty")] + #[serde(alias = "$defs", skip_serializing_if = "Map::is_empty")] pub definitions: Map, #[serde(flatten)] pub number: NumberValidation, @@ -78,10 +78,45 @@ pub struct SchemaObject { pub array: ArrayValidation, #[serde(flatten)] pub object: ObjectValidation, + #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")] + pub reference: Option, #[serde(flatten)] pub extensions: Map, } +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 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)] #[serde(rename_all = "camelCase", default)] pub struct NumberValidation { @@ -144,7 +179,9 @@ pub struct ObjectValidation { pub property_names: Option>, } -#[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")] pub enum InstanceType { Null, diff --git a/schemars/tests/expected/schema-openapi3.json b/schemars/tests/expected/schema-openapi3.json index 9ad9317..f110998 100644 --- a/schemars/tests/expected/schema-openapi3.json +++ b/schemars/tests/expected/schema-openapi3.json @@ -5,9 +5,6 @@ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } @@ -24,25 +21,11 @@ "integer" ] }, - "Ref": { - "type": "object", - "required": [ - "$ref" - ], - "properties": { - "$ref": { - "type": "string" - } - } - }, "Schema": { "anyOf": [ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } @@ -55,6 +38,10 @@ "type": "string", "nullable": true }, + "$ref": { + "type": "string", + "nullable": true + }, "$schema": { "type": "string", "nullable": true @@ -64,9 +51,6 @@ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } @@ -78,9 +62,6 @@ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } @@ -109,9 +90,6 @@ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } @@ -133,9 +111,6 @@ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } @@ -166,9 +141,6 @@ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } @@ -239,9 +211,6 @@ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } @@ -276,9 +245,6 @@ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } @@ -296,9 +262,6 @@ { "type": "boolean" }, - { - "$ref": "#/components/schemas/Ref" - }, { "$ref": "#/components/schemas/SchemaObject" } diff --git a/schemars/tests/expected/schema.json b/schemars/tests/expected/schema.json index 2256828..5aaf3bf 100644 --- a/schemars/tests/expected/schema.json +++ b/schemars/tests/expected/schema.json @@ -5,9 +5,6 @@ { "type": "boolean" }, - { - "$ref": "#/definitions/Ref" - }, { "$ref": "#/definitions/SchemaObject" } @@ -24,25 +21,11 @@ "integer" ] }, - "Ref": { - "type": "object", - "required": [ - "$ref" - ], - "properties": { - "$ref": { - "type": "string" - } - } - }, "Schema": { "anyOf": [ { "type": "boolean" }, - { - "$ref": "#/definitions/Ref" - }, { "$ref": "#/definitions/SchemaObject" } @@ -57,6 +40,12 @@ "null" ] }, + "$ref": { + "type": [ + "string", + "null" + ] + }, "$schema": { "type": [ "string",