Skip to content

Commit

Permalink
Implement Schema as a newtype around Value
Browse files Browse the repository at this point in the history
  • Loading branch information
GREsau committed May 10, 2024
1 parent 7f6a7b7 commit 65fb44b
Show file tree
Hide file tree
Showing 31 changed files with 1,053 additions and 1,827 deletions.
33 changes: 27 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions schemars/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ bigdecimal04 = { version = "0.4", default-features = false, optional = true, pac
enumset = { version = "1.0", optional = true }
smol_str = { version = "0.1.17", optional = true }
semver = { version = "1.0.9", features = ["serde"], optional = true }
ref-cast = "1.0.22"

[dev-dependencies]
pretty_assertions = "1.2.1"
Expand Down
10 changes: 6 additions & 4 deletions schemars/examples/custom_serialization.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use schemars::schema::{Schema, SchemaObject};
use schemars::schema::Schema;
use schemars::{gen::SchemaGenerator, schema_for, JsonSchema};
use serde::{Deserialize, Serialize};

Expand All @@ -21,9 +21,11 @@ pub struct MyStruct {
}

fn make_custom_schema(gen: &mut SchemaGenerator) -> Schema {
let mut schema: SchemaObject = <String>::json_schema(gen).into();
schema.format = Some("boolean".to_owned());
schema.into()
let mut schema = String::json_schema(gen);
schema
.ensure_object()
.insert("format".into(), "boolean".into());
schema
}

fn eight() -> i32 {
Expand Down
2 changes: 1 addition & 1 deletion schemars/examples/schemars_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ pub enum MyEnum {
}

fn main() {
let schema = schema_for!(MyStruct);
let schema = schema_for!(MyEnum);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
173 changes: 84 additions & 89 deletions schemars/src/_private.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::gen::SchemaGenerator;
use crate::schema::{InstanceType, ObjectValidation, Schema, SchemaObject};
use crate::{JsonSchema, Map, Set};
use crate::schema::Schema;
use crate::JsonSchema;
use serde::Serialize;
use serde_json::Map;
use serde_json::Value;

// Helper for generating schemas for flattened `Option` fields.
Expand All @@ -12,12 +13,8 @@ pub fn json_schema_for_flatten<T: ?Sized + JsonSchema>(
let mut schema = T::_schemars_private_non_optional_json_schema(gen);

if T::_schemars_private_is_option() && !required {
if let Schema::Object(SchemaObject {
object: Some(ref mut object_validation),
..
}) = schema
{
object_validation.required.clear();
if let Some(object) = schema.as_object_mut() {
object.remove("required");
}
}

Expand Down Expand Up @@ -57,38 +54,22 @@ impl<T: Serialize> MaybeSerializeWrapper<T> {

/// Create a schema for a unit enum
pub fn new_unit_enum(variant: &str) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![variant.into()]),
..SchemaObject::default()
// TODO switch from single-valued "enum" to "const"
json_schema!({
"type": "string",
"enum": [variant],
})
}

/// Create a schema for an externally tagged enum
pub fn new_externally_tagged_enum(variant: &str, sub_schema: Schema) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties: {
let mut props = Map::new();
props.insert(variant.to_owned(), sub_schema);
props
},
required: {
let mut required = Set::new();
required.insert(variant.to_owned());
required
},
// Externally tagged variants must prohibit additional
// properties irrespective of the disposition of
// `deny_unknown_fields`. If additional properties were allowed
// one could easily construct an object that validated against
// multiple variants since here it's the properties rather than
// the values of a property that distingish between variants.
additional_properties: Some(Box::new(false.into())),
..Default::default()
})),
..SchemaObject::default()
json_schema!({
"type": "object",
"properties": {
variant: sub_schema
},
"required": [variant],
"additionalProperties": false,
})
}

Expand All @@ -98,74 +79,88 @@ pub fn new_internally_tagged_enum(
variant: &str,
deny_unknown_fields: bool,
) -> Schema {
let tag_schema = Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![variant.into()]),
..Default::default()
// TODO switch from single-valued "enum" to "const"
let mut schema = json_schema!({
"type": "object",
"properties": {
tag_name: {
"type": "string",
"enum": [variant],
}
},
"required": [tag_name],
"additionalProperties": false,
});
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties: {
let mut props = Map::new();
props.insert(tag_name.to_owned(), tag_schema);
props
},
required: {
let mut required = Set::new();
required.insert(tag_name.to_owned());
required
},
additional_properties: deny_unknown_fields.then(|| Box::new(false.into())),
..Default::default()
})),
..SchemaObject::default()
})

if deny_unknown_fields {
schema
.as_object_mut()
.unwrap()
.insert("additionalProperties".into(), false.into());
}

schema
}

pub fn insert_object_property<T: ?Sized + JsonSchema>(
obj: &mut ObjectValidation,
schema: &mut Schema,
key: &str,
has_default: bool,
required: bool,
schema: Schema,
sub_schema: Schema,
) {
obj.properties.insert(key.to_owned(), schema);
let obj = schema.ensure_object();
if let Some(properties) = obj
.entry("properties")
.or_insert(Value::Object(Map::new()))
.as_object_mut()
{
properties.insert(key.to_owned(), sub_schema.into());
}

if required || !(has_default || T::_schemars_private_is_option()) {
obj.required.insert(key.to_owned());
if let Some(req) = obj
.entry("required")
.or_insert(Value::Array(Vec::new()))
.as_array_mut()
{
req.push(key.into());
}
}
}

pub mod metadata {
use crate::Schema;
use serde_json::Value;

macro_rules! add_metadata_fn {
($method:ident, $name:ident, $ty:ty) => {
pub fn $method(schema: Schema, $name: impl Into<$ty>) -> Schema {
let value = $name.into();
if value == <$ty>::default() {
schema
} else {
let mut schema_obj = schema.into_object();
schema_obj.metadata().$name = value.into();
Schema::Object(schema_obj)
}
pub fn insert_validation_property(
schema: &mut Schema,
required_type: &str,
key: &str,
value: impl Into<Value>,
) {
if schema.has_type(required_type) || (required_type == "number" && schema.has_type("integer")) {
schema.ensure_object().insert(key.to_owned(), value.into());
}
}

pub fn append_required(schema: &mut Schema, key: &str) {
if schema.has_type("object") {
if let Value::Array(array) = schema
.ensure_object()
.entry("required")
.or_insert(Value::Array(Vec::new()))
{
let value = Value::from(key);
if !array.contains(&value) {
array.push(value);
}
};
}
}
}

add_metadata_fn!(add_description, description, String);
add_metadata_fn!(add_id, id, String);
add_metadata_fn!(add_title, title, String);
add_metadata_fn!(add_deprecated, deprecated, bool);
add_metadata_fn!(add_read_only, read_only, bool);
add_metadata_fn!(add_write_only, write_only, bool);
add_metadata_fn!(add_default, default, Value);

pub fn add_examples<I: IntoIterator<Item = Value>>(schema: Schema, examples: I) -> Schema {
let mut schema_obj = schema.into_object();
schema_obj.metadata().examples.extend(examples);
Schema::Object(schema_obj)
pub fn apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) {
if let Some(inner_schema) = schema
.as_object_mut()
.and_then(|o| o.get_mut("items"))
.and_then(|i| <&mut Schema>::try_from(i).ok())
{
f(inner_schema);
}
}
Loading

0 comments on commit 65fb44b

Please sign in to comment.