From a236d7aee042e0053f79c826efac8fb418c02a20 Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Sun, 8 Sep 2019 13:29:45 +0100 Subject: [PATCH] Implement JsonSchema for chrono types Requires chrono feature. --- schemars/Cargo.toml | 8 +++ schemars/src/json_schema_impls/chrono.rs | 59 ++++++++++++++++++++ schemars/src/json_schema_impls/mod.rs | 2 + schemars/src/schema.rs | 3 + schemars/tests/chrono.rs | 18 ++++++ schemars/tests/expected/chrono-types.json | 42 ++++++++++++++ schemars/tests/expected/schema-openapi3.json | 4 ++ schemars/tests/expected/schema.json | 32 +++++++---- schemars/tests/util/mod.rs | 38 +++++++------ 9 files changed, 178 insertions(+), 28 deletions(-) create mode 100644 schemars/src/json_schema_impls/chrono.rs create mode 100644 schemars/tests/chrono.rs create mode 100644 schemars/tests/expected/chrono-types.json diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml index bab20cd..e102cc2 100644 --- a/schemars/Cargo.toml +++ b/schemars/Cargo.toml @@ -12,6 +12,14 @@ keywords = ["rust", "json-schema", "serde"] schemars_derive = { version = "0.1.7", path = "../schemars_derive" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +chrono = { version = "0.4", default-features = false, optional = true } [dev-dependencies] pretty_assertions = "0.6.1" + +[[test]] +name = "chrono" +required-features = ["chrono"] + +[package.metadata.docs.rs] +all-features = true diff --git a/schemars/src/json_schema_impls/chrono.rs b/schemars/src/json_schema_impls/chrono.rs new file mode 100644 index 0000000..b59e305 --- /dev/null +++ b/schemars/src/json_schema_impls/chrono.rs @@ -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", JsonSchema for DateTime); diff --git a/schemars/src/json_schema_impls/mod.rs b/schemars/src/json_schema_impls/mod.rs index af5ba60..0f178d9 100644 --- a/schemars/src/json_schema_impls/mod.rs +++ b/schemars/src/json_schema_impls/mod.rs @@ -7,6 +7,8 @@ macro_rules! no_ref_schema { } mod array; +#[cfg(feature = "chrono")] +mod chrono; mod core; mod deref; mod maps; diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs index 79fc1a1..1cc637c 100644 --- a/schemars/src/schema.rs +++ b/schemars/src/schema.rs @@ -50,6 +50,7 @@ impl Schema { extensions: extend(s1.extensions, s2.extensions), // TODO do the following make sense? instance_type: s1.instance_type.or(s2.instance_type), + format: s1.format.or(s2.format), enum_values: s1.enum_values.or(s2.enum_values), all_of: s1.all_of.or(s2.all_of), any_of: s1.any_of.or(s2.any_of), @@ -105,6 +106,8 @@ pub struct SchemaObject { pub description: Option, #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub instance_type: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] pub enum_values: Option>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/schemars/tests/chrono.rs b/schemars/tests/chrono.rs new file mode 100644 index 0000000..056e6fe --- /dev/null +++ b/schemars/tests/chrono.rs @@ -0,0 +1,18 @@ +mod util; +use chrono::prelude::*; +use schemars::JsonSchema; +use util::*; + +#[derive(Debug, JsonSchema)] +struct ChronoTypes { + weekday: Weekday, + date_time: DateTime, + naive_date: NaiveDate, + naive_date_time: NaiveDateTime, + naive_time: NaiveTime, +} + +#[test] +fn chrono_types() -> TestResult { + test_default_generated_schema::("chrono-types") +} diff --git a/schemars/tests/expected/chrono-types.json b/schemars/tests/expected/chrono-types.json new file mode 100644 index 0000000..b96b00c --- /dev/null +++ b/schemars/tests/expected/chrono-types.json @@ -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" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/schema-openapi3.json b/schemars/tests/expected/schema-openapi3.json index 9176340..77aaaa3 100644 --- a/schemars/tests/expected/schema-openapi3.json +++ b/schemars/tests/expected/schema-openapi3.json @@ -88,6 +88,10 @@ "items": {}, "nullable": true }, + "format": { + "type": "string", + "nullable": true + }, "items": { "anyOf": [ { diff --git a/schemars/tests/expected/schema.json b/schemars/tests/expected/schema.json index f50e5de..8bce3f4 100644 --- a/schemars/tests/expected/schema.json +++ b/schemars/tests/expected/schema.json @@ -24,6 +24,17 @@ "integer" ] }, + "Ref": { + "type": "object", + "properties": { + "$ref": { + "type": "string" + } + }, + "required": [ + "$ref" + ] + }, "Schema": { "anyOf": [ { @@ -113,6 +124,16 @@ } ] }, + "format": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "items": { "anyOf": [ { @@ -181,17 +202,6 @@ }, "additionalProperties": true }, - "Ref": { - "type": "object", - "properties": { - "$ref": { - "type": "string" - } - }, - "required": [ - "$ref" - ] - }, "SingleOrVec_For_InstanceType": { "anyOf": [ { diff --git a/schemars/tests/util/mod.rs b/schemars/tests/util/mod.rs index 9c3f84c..c6d5ed4 100644 --- a/schemars/tests/util/mod.rs +++ b/schemars/tests/util/mod.rs @@ -1,5 +1,5 @@ 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::fs; use std::panic; @@ -8,32 +8,36 @@ pub type TestResult = Result<(), Box>; #[allow(dead_code)] // https://github.com/rust-lang/rust/issues/46379 pub fn test_generated_schema(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::()?; - - 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(()) + test_schema(&actual, file) } #[allow(dead_code)] // https://github.com/rust-lang/rust/issues/46379 pub fn test_default_generated_schema(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)?; + 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 { - let actual_json = serde_json::to_string_pretty(&actual)?; - fs::write(format!("tests/actual/{}.json", file), actual_json)?; + write_actual_to_file(actual, file)?; } assert_eq!(actual, expected); 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(()) +}