diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c331bc8..fbe0cde0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## Unreleased + +#### 🚀 Updates + +- Added new JSONSchema renderer options: + - `allow_newlines_in_description` - Allows newlines in descriptions, otherwise strips them. + Defaults to `false`. + - `mark_struct_fields_required` - Mark all non-option struct fields as required. Defaults to + `true` for backwards compatibility. + - `set_field_name_as_title` - Sets the field's name as the `title` of each schema entry. Defaults + to `false`. + +#### ⚙️ Internal + +- Updated to Rust v1.76. +- Updated dependencies. + ## 0.14.0 #### 💥 Breaking diff --git a/Cargo.lock b/Cargo.lock index aba4f8df..a3d40f6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,9 +255,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "android-tzdata", "iana-time-zone", @@ -351,9 +351,9 @@ checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "darling" -version = "0.20.5" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8" +checksum = "c376d08ea6aa96aafe61237c7200d1241cb177b7d3a542d791f2d118e9cbb955" dependencies = [ "darling_core", "darling_macro", @@ -361,9 +361,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.5" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3" +checksum = "33043dcd19068b8192064c704b3f83eb464f91f1ff527b44a4e2b08d9cdb8855" dependencies = [ "fnv", "ident_case", @@ -375,9 +375,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.5" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77" +checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be" dependencies = [ "darling_core", "quote", @@ -822,9 +822,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -1388,9 +1388,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.34.2" +version = "1.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755392e1a2f77afd95580d3f0d0e94ac83eeeb7167552c9b5bca549e61a94d83" +checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" dependencies = [ "arrayvec", "borsh", @@ -1961,18 +1961,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c86b57f4..1f8742ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,15 +3,15 @@ resolver = "2" members = ["crates/*"] [workspace.dependencies] -chrono = "0.4.33" -indexmap = "2.2.2" +chrono = "0.4.34" +indexmap = "2.2.3" miette = "7.0.0" regex = "1.10.3" relative-path = "1.9.2" -rust_decimal = "1.34.2" +rust_decimal = "1.34.3" semver = "1.0.21" serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.113" serde_yaml = "0.9.31" -toml = "0.8.9" +toml = "0.8.10" url = "2.5.0" diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index a6d99ded..4debc41c 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -15,7 +15,7 @@ proc-macro = true [dependencies] convert_case = "0.6.0" -darling = "0.20.5" +darling = "0.20.6" proc-macro2 = "1.0.78" quote = "1.0.35" syn = { version = "2.0.48", features = ["full"] } diff --git a/crates/schematic/Cargo.toml b/crates/schematic/Cargo.toml index 793df676..24cada47 100644 --- a/crates/schematic/Cargo.toml +++ b/crates/schematic/Cargo.toml @@ -20,7 +20,7 @@ all-features = true schematic_macros = { version = "0.14.0", path = "../macros" } schematic_types = { version = "0.6.0", path = "../types" } miette = { workspace = true } -thiserror = "1.0.56" +thiserror = "1.0.57" tracing = "0.1.40" # config diff --git a/crates/schematic/src/schema/renderers/json_schema.rs b/crates/schematic/src/schema/renderers/json_schema.rs index 242576c1..c0a37178 100644 --- a/crates/schematic/src/schema/renderers/json_schema.rs +++ b/crates/schematic/src/schema/renderers/json_schema.rs @@ -1,13 +1,47 @@ use crate::schema::{RenderResult, SchemaRenderer}; use indexmap::IndexMap; use miette::IntoDiagnostic; -use schemars::gen::SchemaSettings; +use schemars::gen::{GenVisitor, SchemaSettings}; use schemars::schema::*; use schematic_types::*; use serde_json::{Number, Value}; use std::collections::{BTreeMap, BTreeSet, HashSet}; -pub type JsonSchemaOptions = SchemaSettings; +pub struct JsonSchemaOptions { + /// Allows newlines in descriptions, otherwise strips them. + pub allow_newlines_in_description: bool, + /// Marks all non-option struct fields as required. + pub mark_struct_fields_required: bool, + /// Sets the field's name as the `title` of each schema entry. + /// This overrides any `title` manually defined by a type. + pub set_field_name_as_title: bool, + + // Inherited from schemars. + pub option_nullable: bool, + pub option_add_null_type: bool, + pub definitions_path: String, + pub meta_schema: Option, + pub visitors: Vec>, + pub inline_subschemas: bool, +} + +impl Default for JsonSchemaOptions { + fn default() -> Self { + let settings = SchemaSettings::draft07(); + + Self { + allow_newlines_in_description: false, + mark_struct_fields_required: true, + set_field_name_as_title: false, + option_nullable: settings.option_nullable, + option_add_null_type: settings.option_add_null_type, + definitions_path: settings.definitions_path, + meta_schema: settings.meta_schema, + visitors: settings.visitors, + inline_subschemas: settings.inline_subschemas, + } + } +} /// Renders JSON schema documents from a schema. #[derive(Default)] @@ -16,8 +50,14 @@ pub struct JsonSchemaRenderer { references: HashSet, } -fn clean_comment(comment: String) -> String { - comment.trim().replace('\n', " ") +fn clean_comment(comment: String, allow_newlines: bool) -> String { + let comment = comment.trim(); + + if allow_newlines { + comment.to_owned() + } else { + comment.replace('\n', " ") + } } fn lit_to_value(lit: &LiteralValue) -> Value { @@ -44,8 +84,15 @@ impl JsonSchemaRenderer { if let Schema::Object(ref mut inner) = schema { let mut metadata = Metadata { - // title: field.name.clone(), - description: field.description.clone().map(clean_comment), + title: if self.options.set_field_name_as_title { + Some(field.name.clone()) + } else { + None + }, + description: field + .description + .clone() + .map(|desc| clean_comment(desc, self.options.allow_newlines_in_description)), deprecated: field.deprecated.is_some(), read_only: field.read_only, write_only: field.write_only, @@ -139,7 +186,10 @@ impl SchemaRenderer for JsonSchemaRenderer { let metadata = Metadata { title: enu.name.clone(), - description: enu.description.clone().map(clean_comment), + description: enu + .description + .clone() + .map(|desc| clean_comment(desc, self.options.allow_newlines_in_description)), ..Default::default() }; @@ -267,7 +317,7 @@ impl SchemaRenderer for JsonSchemaRenderer { continue; } - if !field.optional { + if !field.optional && self.options.mark_struct_fields_required { required.insert(field.name.clone()); } @@ -278,7 +328,10 @@ impl SchemaRenderer for JsonSchemaRenderer { instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), metadata: Some(Box::new(Metadata { title: structure.name.clone(), - description: structure.description.clone().map(clean_comment), + description: structure + .description + .clone() + .map(|desc| clean_comment(desc, self.options.allow_newlines_in_description)), ..Default::default() })), object: Some(Box::new(ObjectValidation { @@ -319,7 +372,10 @@ impl SchemaRenderer for JsonSchemaRenderer { let mut metadata = Metadata { title: uni.name.clone(), - description: uni.description.clone().map(clean_comment), + description: uni + .description + .clone() + .map(|desc| clean_comment(desc, self.options.allow_newlines_in_description)), ..Default::default() }; diff --git a/crates/schematic/tests/generator_test.rs b/crates/schematic/tests/generator_test.rs index ac100d89..c568fa77 100644 --- a/crates/schematic/tests/generator_test.rs +++ b/crates/schematic/tests/generator_test.rs @@ -163,9 +163,45 @@ mod json_schema { assert_snapshot!(fs::read_to_string(file).unwrap()); } + + #[test] + fn not_required() { + let sandbox = create_empty_sandbox(); + let file = sandbox.path().join("schema.json"); + + create_generator() + .generate( + &file, + JsonSchemaRenderer::new(JsonSchemaOptions { + mark_struct_fields_required: false, + ..JsonSchemaOptions::default() + }), + ) + .unwrap(); + + assert_snapshot!(fs::read_to_string(file).unwrap()); + } + + #[test] + fn with_titles() { + let sandbox = create_empty_sandbox(); + let file = sandbox.path().join("schema.json"); + + create_generator() + .generate( + &file, + JsonSchemaRenderer::new(JsonSchemaOptions { + set_field_name_as_title: true, + ..JsonSchemaOptions::default() + }), + ) + .unwrap(); + + assert_snapshot!(fs::read_to_string(file).unwrap()); + } } -#[cfg(all(feature = "template", feature = "json"))] +#[cfg(all(feature = "renderer_template", feature = "json"))] mod template_json { use super::*; use schematic::schema::*; @@ -195,7 +231,7 @@ mod template_json { } } -#[cfg(all(feature = "template", feature = "toml"))] +#[cfg(all(feature = "renderer_template", feature = "toml"))] mod template_toml { use super::*; use schematic::schema::*; @@ -213,7 +249,7 @@ mod template_toml { } } -#[cfg(all(feature = "template", feature = "yaml"))] +#[cfg(all(feature = "renderer_template", feature = "yaml"))] mod template_yaml { use super::*; use schematic::schema::*; diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap new file mode 100644 index 00000000..b153ad81 --- /dev/null +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap @@ -0,0 +1,207 @@ +--- +source: crates/schematic/tests/generator_test.rs +expression: "fs::read_to_string(file).unwrap()" +--- +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GenConfig", + "type": "object", + "properties": { + "boolean": { + "type": "boolean" + }, + "date": { + "type": "string", + "format": "date" + }, + "datetime": { + "type": "string", + "format": "date-time" + }, + "decimal": { + "type": "string", + "format": "decimal" + }, + "enums": { + "default": "foo", + "allOf": [ + { + "$ref": "#/definitions/BasicEnum" + } + ] + }, + "float32": { + "type": "number" + }, + "float64": { + "type": "number" + }, + "indexmap": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "propertyNames": { + "type": "string" + } + }, + "indexset": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "jsonValue": { + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] + }, + "map": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "propertyNames": { + "type": "string" + } + }, + "nested": { + "allOf": [ + { + "$ref": "#/definitions/AnotherConfig" + } + ] + }, + "number": { + "type": "number" + }, + "path": { + "type": "string", + "format": "path" + }, + "relPath": { + "type": "string", + "format": "path" + }, + "string": { + "type": "string" + }, + "time": { + "type": "string", + "format": "time" + }, + "tomlValue": { + "anyOf": [ + { + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] + }, + { + "type": "null" + } + ] + }, + "url": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ] + }, + "vector": { + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "versionReq": { + "type": "string" + }, + "yamlValue": { + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] + } + }, + "additionalProperties": false, + "definitions": { + "AnotherConfig": { + "title": "AnotherConfig", + "description": "Some comment.", + "type": "object", + "properties": { + "enums": { + "description": "An optional enum.", + "default": "foo", + "anyOf": [ + { + "$ref": "#/definitions/BasicEnum" + }, + { + "type": "null" + } + ] + }, + "opt": { + "description": "An optional string.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "BasicEnum": { + "title": "BasicEnum", + "type": "string", + "enum": [ + "foo", + "bar", + "baz" + ] + } + } +} + diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap new file mode 100644 index 00000000..88d1b286 --- /dev/null +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap @@ -0,0 +1,261 @@ +--- +source: crates/schematic/tests/generator_test.rs +expression: "fs::read_to_string(file).unwrap()" +--- +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GenConfig", + "type": "object", + "required": [ + "boolean", + "date", + "datetime", + "decimal", + "enums", + "float32", + "float64", + "indexmap", + "indexset", + "jsonValue", + "map", + "nested", + "number", + "path", + "relPath", + "string", + "time", + "tomlValue", + "url", + "vector", + "version", + "versionReq", + "yamlValue" + ], + "properties": { + "boolean": { + "title": "boolean", + "type": "boolean" + }, + "date": { + "title": "date", + "type": "string", + "format": "date" + }, + "datetime": { + "title": "datetime", + "type": "string", + "format": "date-time" + }, + "decimal": { + "title": "decimal", + "type": "string", + "format": "decimal" + }, + "enums": { + "title": "enums", + "default": "foo", + "allOf": [ + { + "$ref": "#/definitions/BasicEnum" + } + ] + }, + "float32": { + "title": "float32", + "type": "number" + }, + "float64": { + "title": "float64", + "type": "number" + }, + "indexmap": { + "title": "indexmap", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "propertyNames": { + "type": "string" + } + }, + "indexset": { + "title": "indexset", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "jsonValue": { + "title": "jsonValue", + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] + }, + "map": { + "title": "map", + "type": "object", + "additionalProperties": { + "type": "number" + }, + "propertyNames": { + "type": "string" + } + }, + "nested": { + "title": "nested", + "allOf": [ + { + "$ref": "#/definitions/AnotherConfig" + } + ] + }, + "number": { + "title": "number", + "type": "number" + }, + "path": { + "title": "path", + "type": "string", + "format": "path" + }, + "relPath": { + "title": "relPath", + "type": "string", + "format": "path" + }, + "string": { + "title": "string", + "type": "string" + }, + "time": { + "title": "time", + "type": "string", + "format": "time" + }, + "tomlValue": { + "title": "tomlValue", + "anyOf": [ + { + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] + }, + { + "type": "null" + } + ] + }, + "url": { + "title": "url", + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ] + }, + "vector": { + "title": "vector", + "type": "array", + "items": { + "type": "string" + } + }, + "version": { + "title": "version", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "versionReq": { + "title": "versionReq", + "type": "string" + }, + "yamlValue": { + "title": "yamlValue", + "type": [ + "boolean", + "object", + "array", + "number", + "string", + "integer" + ] + } + }, + "additionalProperties": false, + "definitions": { + "AnotherConfig": { + "title": "AnotherConfig", + "description": "Some comment.", + "type": "object", + "required": [ + "enums", + "opt" + ], + "properties": { + "enums": { + "title": "enums", + "description": "An optional enum.", + "default": "foo", + "anyOf": [ + { + "$ref": "#/definitions/BasicEnum" + }, + { + "type": "null" + } + ] + }, + "opt": { + "title": "opt", + "description": "An optional string.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "BasicEnum": { + "title": "BasicEnum", + "type": "string", + "enum": [ + "foo", + "bar", + "baz" + ] + } + } +} + diff --git a/crates/schematic/tests/validate_test.rs b/crates/schematic/tests/validate_test.rs index 8b119c98..c97502e2 100644 --- a/crates/schematic/tests/validate_test.rs +++ b/crates/schematic/tests/validate_test.rs @@ -3,7 +3,7 @@ use schematic::*; use std::collections::HashMap; -fn test_string(value: &String, _: &T, _: &C, _: bool) -> Result<(), ValidateError> { +fn test_string(value: &str, _: &T, _: &C, _: bool) -> Result<(), ValidateError> { if value.is_empty() { return Ok(()); } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 93c4cbac..51b24eea 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] profile = "default" -channel = "1.75.0" +channel = "1.76.0"