Implement JsonSchema for chrono types

Requires chrono feature.
This commit is contained in:
Graham Esau 2019-09-08 13:29:45 +01:00
parent 7285dde99a
commit a236d7aee0
9 changed files with 178 additions and 28 deletions

View file

@ -12,6 +12,14 @@ keywords = ["rust", "json-schema", "serde"]
schemars_derive = { version = "0.1.7", path = "../schemars_derive" } schemars_derive = { version = "0.1.7", path = "../schemars_derive" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
chrono = { version = "0.4", default-features = false, optional = true }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "0.6.1" pretty_assertions = "0.6.1"
[[test]]
name = "chrono"
required-features = ["chrono"]
[package.metadata.docs.rs]
all-features = true

View file

@ -0,0 +1,59 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::{JsonSchema, Result};
use chrono::prelude::*;
use serde_json::json;
impl JsonSchema for Weekday {
no_ref_schema!();
fn schema_name() -> String {
"Weekday".to_owned()
}
fn json_schema(_: &mut SchemaGenerator) -> Result {
Ok(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![
json!("Mon"),
json!("Tue"),
json!("Wed"),
json!("Thu"),
json!("Fri"),
json!("Sat"),
json!("Sun"),
]),
..Default::default()
}
.into())
}
}
macro_rules! formatted_string_impl {
($ty:ident, $format:literal) => {
formatted_string_impl!($ty, $format, JsonSchema for $ty);
};
($ty:ident, $format:literal, $($desc:tt)+) => {
impl $($desc)+ {
no_ref_schema!();
fn schema_name() -> String {
stringify!($ty).to_owned()
}
fn json_schema(_: &mut SchemaGenerator) -> Result {
Ok(SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some($format.to_owned()),
..Default::default()
}
.into())
}
}
};
}
formatted_string_impl!(NaiveDate, "date");
formatted_string_impl!(NaiveDateTime, "partial-date-time");
formatted_string_impl!(NaiveTime, "partial-date-time");
formatted_string_impl!(DateTime, "date-time", <Tz: TimeZone> JsonSchema for DateTime<Tz>);

View file

@ -7,6 +7,8 @@ macro_rules! no_ref_schema {
} }
mod array; mod array;
#[cfg(feature = "chrono")]
mod chrono;
mod core; mod core;
mod deref; mod deref;
mod maps; mod maps;

View file

@ -50,6 +50,7 @@ impl Schema {
extensions: extend(s1.extensions, s2.extensions), extensions: extend(s1.extensions, s2.extensions),
// TODO do the following make sense? // TODO do the following make sense?
instance_type: s1.instance_type.or(s2.instance_type), instance_type: s1.instance_type.or(s2.instance_type),
format: s1.format.or(s2.format),
enum_values: s1.enum_values.or(s2.enum_values), enum_values: s1.enum_values.or(s2.enum_values),
all_of: s1.all_of.or(s2.all_of), all_of: s1.all_of.or(s2.all_of),
any_of: s1.any_of.or(s2.any_of), any_of: s1.any_of.or(s2.any_of),
@ -105,6 +106,8 @@ pub struct SchemaObject {
pub description: Option<String>, pub description: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")] #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub instance_type: Option<SingleOrVec<InstanceType>>, pub instance_type: Option<SingleOrVec<InstanceType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")] #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<Value>>, pub enum_values: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]

18
schemars/tests/chrono.rs Normal file
View file

@ -0,0 +1,18 @@
mod util;
use chrono::prelude::*;
use schemars::JsonSchema;
use util::*;
#[derive(Debug, JsonSchema)]
struct ChronoTypes {
weekday: Weekday,
date_time: DateTime<Utc>,
naive_date: NaiveDate,
naive_date_time: NaiveDateTime,
naive_time: NaiveTime,
}
#[test]
fn chrono_types() -> TestResult {
test_default_generated_schema::<ChronoTypes>("chrono-types")
}

View file

@ -0,0 +1,42 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ChronoTypes",
"type": "object",
"properties": {
"date_time": {
"type": "string",
"format": "date-time"
},
"naive_date": {
"type": "string",
"format": "date"
},
"naive_date_time": {
"type": "string",
"format": "partial-date-time"
},
"naive_time": {
"type": "string",
"format": "partial-date-time"
},
"weekday": {
"type": "string",
"enum": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"
]
}
},
"required": [
"date_time",
"naive_date",
"naive_date_time",
"naive_time",
"weekday"
]
}

View file

@ -88,6 +88,10 @@
"items": {}, "items": {},
"nullable": true "nullable": true
}, },
"format": {
"type": "string",
"nullable": true
},
"items": { "items": {
"anyOf": [ "anyOf": [
{ {

View file

@ -24,6 +24,17 @@
"integer" "integer"
] ]
}, },
"Ref": {
"type": "object",
"properties": {
"$ref": {
"type": "string"
}
},
"required": [
"$ref"
]
},
"Schema": { "Schema": {
"anyOf": [ "anyOf": [
{ {
@ -113,6 +124,16 @@
} }
] ]
}, },
"format": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"items": { "items": {
"anyOf": [ "anyOf": [
{ {
@ -181,17 +202,6 @@
}, },
"additionalProperties": true "additionalProperties": true
}, },
"Ref": {
"type": "object",
"properties": {
"$ref": {
"type": "string"
}
},
"required": [
"$ref"
]
},
"SingleOrVec_For_InstanceType": { "SingleOrVec_For_InstanceType": {
"anyOf": [ "anyOf": [
{ {

View file

@ -1,5 +1,5 @@
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use schemars::{gen::SchemaSettings, schema_for, JsonSchema}; use schemars::{gen::SchemaSettings, schema::Schema, schema_for, JsonSchema};
use std::error::Error; use std::error::Error;
use std::fs; use std::fs;
use std::panic; use std::panic;
@ -8,32 +8,36 @@ pub type TestResult = Result<(), Box<dyn Error>>;
#[allow(dead_code)] // https://github.com/rust-lang/rust/issues/46379 #[allow(dead_code)] // https://github.com/rust-lang/rust/issues/46379
pub fn test_generated_schema<T: JsonSchema>(file: &str, settings: SchemaSettings) -> TestResult { pub fn test_generated_schema<T: JsonSchema>(file: &str, settings: SchemaSettings) -> TestResult {
let expected_json = fs::read_to_string(format!("tests/expected/{}.json", file))?;
let expected = serde_json::from_str(&expected_json)?;
let actual = settings.into_generator().into_root_schema_for::<T>()?; let actual = settings.into_generator().into_root_schema_for::<T>()?;
test_schema(&actual, file)
if actual != expected {
let actual_json = serde_json::to_string_pretty(&actual)?;
fs::write(format!("tests/actual/{}.json", file), actual_json)?;
}
assert_eq!(actual, expected);
Ok(())
} }
#[allow(dead_code)] // https://github.com/rust-lang/rust/issues/46379 #[allow(dead_code)] // https://github.com/rust-lang/rust/issues/46379
pub fn test_default_generated_schema<T: JsonSchema>(file: &str) -> TestResult { pub fn test_default_generated_schema<T: JsonSchema>(file: &str) -> TestResult {
let expected_json = fs::read_to_string(format!("tests/expected/{}.json", file))?;
let expected = serde_json::from_str(&expected_json)?;
let actual = schema_for!(T)?; let actual = schema_for!(T)?;
test_schema(&actual, file)
}
fn test_schema(actual: &Schema, file: &str) -> TestResult {
let expected_json = match fs::read_to_string(format!("tests/expected/{}.json", file)) {
Ok(j) => j,
Err(e) => {
write_actual_to_file(&actual, file)?;
return Err(Box::from(e));
}
};
let expected = &serde_json::from_str(&expected_json)?;
if actual != expected { if actual != expected {
let actual_json = serde_json::to_string_pretty(&actual)?; write_actual_to_file(actual, file)?;
fs::write(format!("tests/actual/{}.json", file), actual_json)?;
} }
assert_eq!(actual, expected); assert_eq!(actual, expected);
Ok(()) Ok(())
} }
fn write_actual_to_file(schema: &Schema, file: &str) -> TestResult {
let actual_json = serde_json::to_string_pretty(&schema)?;
fs::write(format!("tests/actual/{}.json", file), actual_json)?;
Ok(())
}