Prevent stack overflow when using inline_subschemas

This commit is contained in:
Graham Esau 2021-03-21 20:09:32 +00:00
parent d85eec3b7a
commit 1017506ce6
4 changed files with 170 additions and 31 deletions

View file

@ -11,7 +11,7 @@ use crate::flatten::Merge;
use crate::schema::*; use crate::schema::*;
use crate::{visit::*, JsonSchema, Map}; use crate::{visit::*, JsonSchema, Map};
use dyn_clone::DynClone; use dyn_clone::DynClone;
use std::{any::Any, fmt::Debug}; use std::{any::Any, collections::HashSet, fmt::Debug};
/// Settings to customize how Schemas are generated. /// Settings to customize how Schemas are generated.
/// ///
@ -146,10 +146,21 @@ impl SchemaSettings {
/// let gen = SchemaGenerator::default(); /// let gen = SchemaGenerator::default();
/// let schema = gen.into_root_schema_for::<MyStruct>(); /// let schema = gen.into_root_schema_for::<MyStruct>();
/// ``` /// ```
#[derive(Debug, Default, Clone)] #[derive(Debug, Default)]
pub struct SchemaGenerator { pub struct SchemaGenerator {
settings: SchemaSettings, settings: SchemaSettings,
definitions: Map<String, Schema>, definitions: Map<String, Schema>,
pending_schema_names: HashSet<String>,
}
impl Clone for SchemaGenerator {
fn clone(&self) -> Self {
Self {
settings: self.settings.clone(),
definitions: self.definitions.clone(),
pending_schema_names: HashSet::new(),
}
}
} }
impl From<SchemaSettings> for SchemaGenerator { impl From<SchemaSettings> for SchemaGenerator {
@ -203,23 +214,28 @@ impl SchemaGenerator {
/// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
/// add them to the `SchemaGenerator`'s schema definitions. /// add them to the `SchemaGenerator`'s schema definitions.
pub fn subschema_for<T: ?Sized + JsonSchema>(&mut self) -> Schema { pub fn subschema_for<T: ?Sized + JsonSchema>(&mut self) -> Schema {
if !T::is_referenceable() || self.settings.inline_subschemas {
return T::json_schema(self);
}
let name = T::schema_name(); let name = T::schema_name();
let reference = format!("{}{}", self.settings().definitions_path, name); let return_ref = T::is_referenceable()
if !self.definitions.contains_key(&name) { && (!self.settings.inline_subschemas || self.pending_schema_names.contains(&name));
self.insert_new_subschema_for::<T>(name);
if return_ref {
let reference = format!("{}{}", self.settings().definitions_path, name);
if !self.definitions.contains_key(&name) {
self.insert_new_subschema_for::<T>(name);
}
Schema::new_ref(reference)
} else {
self.json_schema_internal::<T>(&name)
} }
Schema::new_ref(reference)
} }
fn insert_new_subschema_for<T: ?Sized + JsonSchema>(&mut self, name: String) { fn insert_new_subschema_for<T: ?Sized + JsonSchema>(&mut self, name: String) {
let dummy = Schema::Bool(false); let dummy = Schema::Bool(false);
// insert into definitions BEFORE calling json_schema to avoid infinite recursion // insert into definitions BEFORE calling json_schema to avoid infinite recursion
self.definitions.insert(name.clone(), dummy); self.definitions.insert(name.clone(), dummy);
let schema = T::json_schema(self);
let schema = self.json_schema_internal::<T>(&name);
self.definitions.insert(name, schema); self.definitions.insert(name, schema);
} }
@ -259,8 +275,9 @@ impl SchemaGenerator {
/// add them to the `SchemaGenerator`'s schema definitions and include them in the returned `SchemaObject`'s /// add them to the `SchemaGenerator`'s schema definitions and include them in the returned `SchemaObject`'s
/// [`definitions`](../schema/struct.Metadata.html#structfield.definitions) /// [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema { pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema {
let mut schema = T::json_schema(self).into_object(); let name = T::schema_name();
schema.metadata().title.get_or_insert_with(T::schema_name); let mut schema = self.json_schema_internal::<T>(&name).into_object();
schema.metadata().title.get_or_insert(name);
let mut root = RootSchema { let mut root = RootSchema {
meta_schema: self.settings.meta_schema.clone(), meta_schema: self.settings.meta_schema.clone(),
definitions: self.definitions.clone(), definitions: self.definitions.clone(),
@ -279,8 +296,9 @@ impl SchemaGenerator {
/// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
/// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions) /// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> RootSchema { pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> RootSchema {
let mut schema = T::json_schema(&mut self).into_object(); let name = T::schema_name();
schema.metadata().title.get_or_insert_with(T::schema_name); let mut schema = self.json_schema_internal::<T>(&name).into_object();
schema.metadata().title.get_or_insert(name);
let mut root = RootSchema { let mut root = RootSchema {
meta_schema: self.settings.meta_schema, meta_schema: self.settings.meta_schema,
definitions: self.definitions, definitions: self.definitions,
@ -352,6 +370,14 @@ impl SchemaGenerator {
} }
} }
} }
fn json_schema_internal<T: ?Sized + JsonSchema>(&mut self, name: &str) -> Schema {
self.pending_schema_names.insert(name.to_owned());
let schema = T::json_schema(self);
// FIXME schema name not removed if previous line panics
self.pending_schema_names.remove(name);
schema
}
} }
/// A [Visitor](Visitor) which implements additional traits required to be included in a [SchemaSettings]. /// A [Visitor](Visitor) which implements additional traits required to be included in a [SchemaSettings].

View file

@ -0,0 +1,95 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "RecursiveOuter",
"type": "object",
"properties": {
"direct": {
"anyOf": [
{
"$ref": "#/definitions/RecursiveOuter"
},
{
"type": "null"
}
]
},
"indirect": {
"type": [
"object",
"null"
],
"required": [
"recursive"
],
"properties": {
"recursive": {
"type": "object",
"properties": {
"direct": {
"anyOf": [
{
"$ref": "#/definitions/RecursiveOuter"
},
{
"type": "null"
}
]
},
"indirect": {
"anyOf": [
{
"$ref": "#/definitions/RecursiveInner"
},
{
"type": "null"
}
]
}
}
}
}
}
},
"definitions": {
"RecursiveOuter": {
"type": "object",
"properties": {
"direct": {
"anyOf": [
{
"$ref": "#/definitions/RecursiveOuter"
},
{
"type": "null"
}
]
},
"indirect": {
"type": [
"object",
"null"
],
"required": [
"recursive"
],
"properties": {
"recursive": {
"$ref": "#/definitions/RecursiveOuter"
}
}
}
}
},
"RecursiveInner": {
"type": "object",
"required": [
"recursive"
],
"properties": {
"recursive": {
"$ref": "#/definitions/RecursiveOuter"
}
}
}
}
}

View file

@ -1,23 +1,23 @@
{ {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "MyJob",
"type": "object",
"required": [
"spec"
],
"properties": { "properties": {
"spec": { "spec": {
"properties": { "type": "object",
"replicas": {
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [ "required": [
"replicas" "replicas"
], ],
"type": "object" "properties": {
"replicas": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
}
} }
}, }
"required": [
"spec"
],
"title": "MyJob",
"type": "object"
} }

View file

@ -15,7 +15,25 @@ pub struct MyJobSpec {
#[test] #[test]
fn struct_normal() -> TestResult { fn struct_normal() -> TestResult {
let mut settings = SchemaSettings::openapi3(); let mut settings = SchemaSettings::default();
settings.inline_subschemas = true; settings.inline_subschemas = true;
test_generated_schema::<MyJob>("inline-subschemas", settings) test_generated_schema::<MyJob>("inline-subschemas", settings)
} }
#[derive(Debug, JsonSchema)]
pub struct RecursiveOuter {
pub direct: Option<Box<RecursiveOuter>>,
pub indirect: Option<Box<RecursiveInner>>,
}
#[derive(Debug, JsonSchema)]
pub struct RecursiveInner {
pub recursive: RecursiveOuter,
}
#[test]
fn struct_recursive() -> TestResult {
let mut settings = SchemaSettings::default();
settings.inline_subschemas = true;
test_generated_schema::<RecursiveOuter>("inline-subschemas-recursive", settings)
}