From 32a7691558f98455fe346bb1562d27b75943f31f Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Mon, 27 Oct 2025 15:01:29 -0500 Subject: [PATCH 1/3] (GH-538) Add `get_keyword_as_subschema` extension methods Prior to this change, the `dsc-lib-jsonschema` crate defined extension methods for schemas to retrieve keywords as various underlying data types when the caller knows the data type they need. However, the extension methods didn't include a way to retrieve values as _schemas_, so any use of those values requires the caller to reimplement the extension methods logic or to manually convert the value to a schema. This change adds two helper methods to retrieve a keyword as a `schemars::Schema` instance (borrowed and mutably borrowed) to make working with subschemas more ergonomic. --- .../src/schema_utility_extensions.rs | 107 +++++++++++++++++- .../tests/schema_utility_extensions/mod.rs | 6 + 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs index 550358f03..a178ed900 100644 --- a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs +++ b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs @@ -10,7 +10,7 @@ //! //! The rest of the utility methods work with specific keywords, like `$id` and `$defs`. -use core::{clone::Clone, iter::Iterator, option::Option::None}; +use core::{clone::Clone, convert::TryInto, iter::Iterator, option::Option::None}; use std::string::String; use schemars::Schema; @@ -537,6 +537,103 @@ pub trait SchemaUtilityExtensions { /// ) /// ``` fn get_keyword_as_string(&self, key: &str) -> Option; + /// Checks a JSON Schema for a given keyword and returns the value of that keyword, if it + /// exists, as a [`Schema`]. + /// + /// If the keyword doesn't exist or isn't a subchema, this function returns [`None`]. + /// + /// # Examples + /// + /// When the given keyword exists and is a subschema, the function returns the subschema. + /// + /// ```rust + /// use schemars::json_schema; + /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; + /// + /// let ref schema = json_schema!({ + /// "type": "array", + /// "items": { + /// "type": "string" + /// } + /// }); + /// assert_eq!( + /// schema.get_keyword_as_subschema("items"), + /// Some(&json_schema!({"type": "string"})) + /// ); + /// ``` + /// + /// When the given keyword doesn't exist or has the wrong data type, the function returns + /// [`None`]. + /// + /// ```rust + /// use schemars::json_schema; + /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; + /// + /// let ref schema = json_schema!({ + /// "items": "invalid" + /// }); + /// + /// assert_eq!( + /// schema.get_keyword_as_subschema("not_exist"), + /// None + /// ); + /// + /// assert_eq!( + /// schema.get_keyword_as_subschema("items"), + /// None + /// ) + /// ``` + fn get_keyword_as_subschema(&self, key: &str) -> Option<&Schema>; + /// Checks a JSON Schema for a given keyword and mutably borrows the value of that keyword, + /// if it exists, as a [`Schema`]. + /// + /// If the keyword doesn't exist or isn't a subschema, this function returns [`None`]. + /// + /// # Examples + /// + /// When the given keyword exists and is a subschema, the function returns the subschema. + /// + /// ```rust + /// use schemars::json_schema; + /// use serde_json::json; + /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; + /// + /// let ref mut subschema = json_schema!({ + /// "type": "string" + /// }); + /// let ref mut schema = json_schema!({ + /// "type": "array", + /// "items": subschema + /// }); + /// assert_eq!( + /// schema.get_keyword_as_subschema_mut("items"), + /// Some(subschema) + /// ); + /// ``` + /// + /// When the given keyword doesn't exist or has the wrong data type, the function returns + /// [`None`]. + /// + /// ```rust + /// use schemars::json_schema; + /// use serde_json::json; + /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; + /// + /// let ref mut schema = json_schema!({ + /// "items": "invalid" + /// }); + /// + /// assert_eq!( + /// schema.get_keyword_as_object_mut("not_exist"), + /// None + /// ); + /// + /// assert_eq!( + /// schema.get_keyword_as_object_mut("items"), + /// None + /// ) + /// ``` + fn get_keyword_as_subschema_mut(&mut self, key: &str) -> Option<&mut Schema>; /// Checks a JSON schema for a given keyword and returns the value of that keyword, if it /// exists, as a [`u64`]. /// @@ -1222,6 +1319,14 @@ impl SchemaUtilityExtensions for Schema { .and_then(Value::as_str) .map(std::string::ToString::to_string) } + fn get_keyword_as_subschema(&self, key: &str) -> Option<&Schema> { + self.get(key) + .and_then(|v| <&Value as TryInto<&Schema>>::try_into(v).ok()) + } + fn get_keyword_as_subschema_mut(&mut self, key: &str) -> Option<&mut Schema> { + self.get_mut(key) + .and_then(|v| <&mut Value as TryInto<&mut Schema>>::try_into(v).ok()) + } fn get_keyword_as_u64(&self, key: &str) -> Option { self.get(key) .and_then(Value::as_u64) diff --git a/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs b/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs index 6b7710a57..25b9ce309 100644 --- a/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs +++ b/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs @@ -26,6 +26,9 @@ static OBJECT_VALUE: LazyLock> = LazyLock::new(|| json!({ }).as_object().unwrap().clone()); static NULL_VALUE: () = (); static STRING_VALUE: &str = "value"; +static SUBSCHEMA_VALUE: LazyLock = LazyLock::new(|| json_schema!({ + "$id": "https://schema.contoso.com/test/get_keyword_as/subschema.json" +})); static TEST_SCHEMA: LazyLock = LazyLock::new(|| json_schema!({ "$id": "https://schema.contoso.com/test/get_keyword_as.json", "array": *ARRAY_VALUE, @@ -35,6 +38,7 @@ static TEST_SCHEMA: LazyLock = LazyLock::new(|| json_schema!({ "object": *OBJECT_VALUE, "null": null, "string": *STRING_VALUE, + "subschema": *SUBSCHEMA_VALUE, })); /// Defines test cases for a given `get_keyword_as` function (non-mutable). @@ -135,12 +139,14 @@ test_cases_for_get_keyword_as!( get_keyword_as_number: "array", "integer", Some(&(INTEGER_VALUE.into())), get_keyword_as_str: "array", "string", Some(STRING_VALUE), get_keyword_as_string: "array", "string", Some(STRING_VALUE.to_string()), + get_keyword_as_subschema: "array", "subschema", Some(&*SUBSCHEMA_VALUE), ); test_cases_for_get_keyword_as_mut!( get_keyword_as_array_mut: "boolean", "array", Some(&mut (*ARRAY_VALUE).clone()), get_keyword_as_object_mut: "array", "object", Some(&mut (*OBJECT_VALUE).clone()), + get_keyword_as_subschema_mut: "array", "subschema", Some(&mut (*SUBSCHEMA_VALUE).clone()), ); #[cfg(test)] mod get_id { From 87571d4f930ff0ab3340f0b5962c61c3c89f0446 Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Mon, 27 Oct 2025 15:07:41 -0500 Subject: [PATCH 2/3] (GH-538) Return `Schema` for subschema extension methods This change: - Updates the following functions to return instances of `schemars::Schema` instead of `Map`, since the returned data is _always_ a subschema, if it exists: - `get_defs_subschema_from_id()` - `get_defs_subschema_from_id_mut()` - `get_defs_subschema_from_reference()` - `get_defs_subschema_from_reference_mut()` - `get_property_subschema()` - `get_property_subschema_mut()` - Removes the type aliases `Object` (for `Map`) and `Array` (for `Vec`), as these conveniences weren't saving much typing and Rust Analyzer wasn't always plumbing them through for IntelliSense. The uses of these aliases now revert to calling the underlying types. - Updates documentation and tests for the modified functions. --- .../src/schema_utility_extensions.rs | 166 +++++++++--------- .../tests/schema_utility_extensions/mod.rs | 46 +++-- 2 files changed, 100 insertions(+), 112 deletions(-) diff --git a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs index a178ed900..1ea250f39 100644 --- a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs +++ b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs @@ -17,9 +17,6 @@ use schemars::Schema; use serde_json::{Map, Number, Value}; use url::{Position, Url}; -type Array = Vec; -type Object = Map; - /// Provides utility extension methods for [`schemars::Schema`]. pub trait SchemaUtilityExtensions { //********************** get_keyword_as_* functions **********************// @@ -68,7 +65,7 @@ pub trait SchemaUtilityExtensions { /// None /// ) /// ``` - fn get_keyword_as_array(&self, key: &str) -> Option<&Array>; + fn get_keyword_as_array(&self, key: &str) -> Option<&Vec>; /// Checks a JSON Schema for a given keyword and mutably borrows the value of that keyword, /// if it exists, as a [`Vec`]. /// @@ -115,7 +112,7 @@ pub trait SchemaUtilityExtensions { /// None /// ) /// ``` - fn get_keyword_as_array_mut(&mut self, key: &str) -> Option<&mut Array>; + fn get_keyword_as_array_mut(&mut self, key: &str) -> Option<&mut Vec>; /// Checks a JSON Schema for a given keyword and returns the value of that keyword, if it /// exists, as a [`bool`]. /// @@ -347,7 +344,7 @@ pub trait SchemaUtilityExtensions { /// None /// ) /// ``` - fn get_keyword_as_object(&self, key: &str) -> Option<&Object>; + fn get_keyword_as_object(&self, key: &str) -> Option<& Map>; /// Checks a JSON Schema for a given keyword and mutably borrows the value of that keyword, /// if it exists, as a [`Map`]. /// @@ -398,7 +395,7 @@ pub trait SchemaUtilityExtensions { /// None /// ) /// ``` - fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Object>; + fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map>; /// Checks a JSON schema for a given keyword and borrows the value of that keyword, if it /// exists, as a [`Number`]. /// @@ -803,7 +800,7 @@ pub trait SchemaUtilityExtensions { /// defs_json.as_object() /// ); /// ``` - fn get_defs(&self) -> Option<&Object>; + fn get_defs(&self) -> Option<& Map>; /// Retrieves the `$defs` keyword and mutably borrows the object if it exists. /// /// If the keyword isn't defined or isn't an object, the function returns [`None`]. @@ -831,9 +828,9 @@ pub trait SchemaUtilityExtensions { /// defs_json.as_object_mut() /// ); /// ``` - fn get_defs_mut(&mut self) -> Option<&mut Object>; - /// Looks up a reference in the `$defs` keyword by `$id` and returns the subschema entry as an - /// object if it exists. + fn get_defs_mut(&mut self) -> Option<&mut Map>; + /// Looks up a reference in the `$defs` keyword by `$id` and returns the subschema entry as a + /// [`Schema`] if it exists. /// /// The value for the `id` _must_ be the absolute URL of the target subschema's `$id` keyword. /// If the target subschema doesn't define the `$id` keyword, this function can't resolve the @@ -846,10 +843,9 @@ pub trait SchemaUtilityExtensions { /// /// ```rust /// use schemars::json_schema; - /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// - /// let ref definition = json!({ + /// let ref definition = json_schema!({ /// "$id": "https://contoso.com/schemas/example/foo.json", /// "title": "Foo property" /// }); @@ -861,7 +857,7 @@ pub trait SchemaUtilityExtensions { /// /// assert_eq!( /// schema.get_defs_subschema_from_id("https://contoso.com/schemas/example/foo.json"), - /// definition.as_object() + /// Some(definition) /// ); /// assert_eq!( /// schema.get_defs_subschema_from_id("/schemas/example/foo.json"), @@ -870,9 +866,9 @@ pub trait SchemaUtilityExtensions { /// ``` /// /// [`get_defs_subschema_from_reference()`]: SchemaUtilityExtensions::get_defs_subschema_from_reference - fn get_defs_subschema_from_id(&self, id: &str) -> Option<&Object>; + fn get_defs_subschema_from_id(&self, id: &str) -> Option<&Schema>; /// Looks up a reference in the `$defs` keyword by `$id` and mutably borrows the subschema - /// entry as an object if it exists. + /// entry as a [`Schema`] if it exists. /// /// The value for the `id` _must_ be the absolute URL of the target subschema's `$id` keyword. /// If the target subschema doesn't define the `$id` keyword, this function can't resolve the @@ -885,10 +881,9 @@ pub trait SchemaUtilityExtensions { /// /// ```rust /// use schemars::json_schema; - /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// - /// let ref mut definition = json!({ + /// let ref mut definition = json_schema!({ /// "$id": "https://contoso.com/schemas/example/foo.json", /// "title": "Foo property" /// }); @@ -900,7 +895,7 @@ pub trait SchemaUtilityExtensions { /// /// assert_eq!( /// schema.get_defs_subschema_from_id_mut("https://contoso.com/schemas/example/foo.json"), - /// definition.as_object_mut() + /// Some(definition) /// ); /// assert_eq!( /// schema.get_defs_subschema_from_id_mut("/schemas/example/foo.json"), @@ -909,9 +904,9 @@ pub trait SchemaUtilityExtensions { /// ``` /// /// [`get_defs_subschema_from_reference_mut()`]: SchemaUtilityExtensions::get_defs_subschema_from_reference_mut - fn get_defs_subschema_from_id_mut(&mut self, id: &str) -> Option<&mut Object>; - /// Looks up a reference in the `$defs` keyword and returns the subschema entry as an obect if - /// it exists. + fn get_defs_subschema_from_id_mut(&mut self, id: &str) -> Option<&mut Schema>; + /// Looks up a reference in the `$defs` keyword and returns the subschema entry as a [`Schema`] + /// if it exists. /// /// The reference can be any of the following: /// @@ -931,10 +926,9 @@ pub trait SchemaUtilityExtensions { /// /// ```rust /// use schemars::json_schema; - /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// - /// let ref definition = json!({ + /// let ref definition = json_schema!({ /// "$id": "https://contoso.com/schemas/example/foo.json", /// "title": "Foo property" /// }); @@ -947,17 +941,17 @@ pub trait SchemaUtilityExtensions { /// // Lookup with pointer: /// assert_eq!( /// schema.get_defs_subschema_from_reference("#/$defs/foo"), - /// definition.as_object() + /// Some(definition) /// ); /// // Lookup with absolute URL: /// assert_eq!( /// schema.get_defs_subschema_from_reference("https://contoso.com/schemas/example/foo.json"), - /// definition.as_object() + /// Some(definition) /// ); /// // Lookup with site-relative URL: /// assert_eq!( /// schema.get_defs_subschema_from_reference("/schemas/example/foo.json"), - /// definition.as_object() + /// Some(definition) /// ); /// ``` /// @@ -966,10 +960,9 @@ pub trait SchemaUtilityExtensions { /// /// ```rust /// use schemars::json_schema; - /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// - /// let ref definition = json!({ + /// let ref definition = json_schema!({ /// "$id": "https://contoso.com/schemas/example/foo.json", /// "title": "Foo property" /// }); @@ -981,12 +974,12 @@ pub trait SchemaUtilityExtensions { /// // Lookup with pointer: /// assert_eq!( /// schema.get_defs_subschema_from_reference("#/$defs/foo"), - /// definition.as_object() + /// Some(definition) /// ); /// // Lookup with absolute URL: /// assert_eq!( /// schema.get_defs_subschema_from_reference("https://contoso.com/schemas/example/foo.json"), - /// definition.as_object() + /// Some(definition) /// ); /// // Lookup with site-relative URL: /// assert_eq!( @@ -994,9 +987,9 @@ pub trait SchemaUtilityExtensions { /// None /// ); /// ``` - fn get_defs_subschema_from_reference(&self, reference: &str) -> Option<&Object>; - /// Looks up a reference in the `$defs` keyword and mutably borrows the subschema entry as an - /// object if it exists. + fn get_defs_subschema_from_reference(&self, reference: &str) -> Option<&Schema>; + /// Looks up a reference in the `$defs` keyword and mutably borrows the subschema entry as a + /// [`Schema`] if it exists. /// /// The reference can be any of the following: /// @@ -1022,10 +1015,9 @@ pub trait SchemaUtilityExtensions { /// /// ```rust /// use schemars::json_schema; - /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// - /// let ref mut definition = json!({ + /// let ref mut definition = json_schema!({ /// "$id": "https://contoso.com/schemas/example/foo.json", /// "title": "Foo property" /// }); @@ -1037,13 +1029,13 @@ pub trait SchemaUtilityExtensions { /// }); /// // Lookup with absolute URL: /// assert_eq!( - /// schema.get_defs_subschema_from_reference_mut("https://contoso.com/schemas/example/foo.json"), - /// definition.as_object_mut() + /// schema.get_defs_subschema_from_reference_mut("https://contoso.com/schemas/example/foo.json").unwrap(), + /// definition /// ); /// // Lookup with site-relative URL: /// assert_eq!( - /// schema.get_defs_subschema_from_reference_mut("/schemas/example/foo.json"), - /// definition.as_object_mut() + /// schema.get_defs_subschema_from_reference_mut("/schemas/example/foo.json").unwrap(), + /// definition /// ); /// ``` /// @@ -1052,10 +1044,9 @@ pub trait SchemaUtilityExtensions { /// /// ```rust /// use schemars::json_schema; - /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// - /// let ref mut definition = json!({ + /// let ref mut definition = json_schema!({ /// "$id": "https://contoso.com/schemas/example/foo.json", /// "title": "Foo property" /// }); @@ -1067,7 +1058,7 @@ pub trait SchemaUtilityExtensions { /// // Lookup with absolute URL: /// assert_eq!( /// schema.get_defs_subschema_from_reference_mut("https://contoso.com/schemas/example/foo.json"), - /// definition.as_object_mut() + /// Some(definition) /// ); /// // Lookup with site-relative URL: /// assert_eq!( @@ -1079,7 +1070,7 @@ pub trait SchemaUtilityExtensions { /// [`get_defs_subschema_from_reference()`]: SchemaUtilityExtensions::get_defs_subschema_from_reference /// [schemars#478]: https://github.com/GREsau/schemars/issues/478 /// [fixing PR]: https://github.com/GREsau/schemars/pull/479 - fn get_defs_subschema_from_reference_mut(&mut self, reference: &str) -> Option<&mut Object>; + fn get_defs_subschema_from_reference_mut(&mut self, reference: &str) -> Option<&mut Schema>; /// Inserts a subschema entry into the `$defs` keyword for the [`Schema`]. If an entry for the /// given key already exists, this function returns the old value as a map. /// @@ -1096,27 +1087,27 @@ pub trait SchemaUtilityExtensions { /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// - /// let original_definition = json!({ + /// let original_definition = json_schema!({ /// "title": "Foo property" - /// }).as_object().unwrap().clone(); - /// let mut new_definition = json!({ + /// }).clone(); + /// let mut new_definition = json_schema!({ /// "$id": "https://contoso.com/schemas/example/foo.json", - /// }).as_object().unwrap().clone(); + /// }).clone(); /// let ref mut schema = json_schema!({ /// "$defs": { /// "foo": original_definition /// } /// }); /// assert_eq!( - /// schema.insert_defs_subschema("foo", &new_definition), - /// Some(original_definition) + /// schema.insert_defs_subschema("foo", &new_definition.as_object().unwrap()), + /// original_definition.as_object().cloned() /// ); /// assert_eq!( /// schema.get_defs_subschema_from_reference_mut("https://contoso.com/schemas/example/foo.json"), /// Some(&mut new_definition) /// ) /// ``` - fn insert_defs_subschema(&mut self, definition_key: &str, definition_value: &Object) -> Option; + fn insert_defs_subschema(&mut self, definition_key: &str, definition_value: & Map) -> Option< Map>; /// Looks up a subschema in the `$defs` keyword by reference and, if it exists, renames the /// _key_ for the definition. /// @@ -1186,7 +1177,7 @@ pub trait SchemaUtilityExtensions { /// properties_json.as_object() /// ); /// ``` - fn get_properties(&self) -> Option<&Object>; + fn get_properties(&self) -> Option<& Map>; /// Retrieves the `properties` keyword and mutably borrows the object if it exists. /// /// If the keyword isn't defined or isn't an object, the function returns [`None`]. @@ -1214,11 +1205,11 @@ pub trait SchemaUtilityExtensions { /// properties_json.as_object_mut() /// ); /// ``` - fn get_properties_mut(&mut self) -> Option<&mut Object>; + fn get_properties_mut(&mut self) -> Option<&mut Map>; /// Looks up a property in the `properties` keyword by name and returns the subschema entry as - /// an object if it exists. + /// a [`Schema`] if it exists. /// - /// If the named property doesn't exist or isn't an object, this function returns [`None`]. + /// If the named property doesn't exist or isn't a valid subschema, this function returns [`None`]. /// /// # Examples /// @@ -1227,7 +1218,7 @@ pub trait SchemaUtilityExtensions { /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// - /// let ref property = json!({ + /// let ref property = json_schema!({ /// "$id": "https://contoso.com/schemas/example/foo.json", /// "title": "Foo property" /// }); @@ -1239,10 +1230,10 @@ pub trait SchemaUtilityExtensions { /// /// assert_eq!( /// schema.get_property_subschema("foo"), - /// property.as_object() + /// Some(property) /// ); /// ``` - fn get_property_subschema(&self, property_name: &str) -> Option<&Object>; + fn get_property_subschema(&self, property_name: &str) -> Option<&Schema>; /// Looks up a property in the `properties` keyword by name and mutably borrows the subschema /// entry as an object if it exists. /// @@ -1252,10 +1243,9 @@ pub trait SchemaUtilityExtensions { /// /// ```rust /// use schemars::json_schema; - /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// - /// let ref mut property = json!({ + /// let ref mut property = json_schema!({ /// "$id": "https://contoso.com/schemas/example/foo.json", /// "title": "Foo property" /// }); @@ -1267,18 +1257,18 @@ pub trait SchemaUtilityExtensions { /// /// assert_eq!( /// schema.get_property_subschema_mut("foo"), - /// property.as_object_mut() + /// Some(property) /// ); /// ``` - fn get_property_subschema_mut(&mut self, property_name: &str) -> Option<&mut Object>; + fn get_property_subschema_mut(&mut self, property_name: &str) -> Option<&mut Schema>; } impl SchemaUtilityExtensions for Schema { - fn get_keyword_as_array(&self, key: &str) -> Option<&Array> { + fn get_keyword_as_array(&self, key: &str) -> Option<&Vec> { self.get(key) .and_then(Value::as_array) } - fn get_keyword_as_array_mut(&mut self, key: &str) -> Option<&mut Array> { + fn get_keyword_as_array_mut(&mut self, key: &str) -> Option<&mut Vec> { self.get_mut(key) .and_then(Value::as_array_mut) } @@ -1302,11 +1292,11 @@ impl SchemaUtilityExtensions for Schema { self.get(key) .and_then(Value::as_number) } - fn get_keyword_as_object(&self, key: &str) -> Option<&Object> { + fn get_keyword_as_object(&self, key: &str) -> Option<& Map> { self.get(key) .and_then(Value::as_object) } - fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Object> { + fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map> { self.get_mut(key) .and_then(Value::as_object_mut) } @@ -1331,13 +1321,13 @@ impl SchemaUtilityExtensions for Schema { self.get(key) .and_then(Value::as_u64) } - fn get_defs(&self) -> Option<&Object> { + fn get_defs(&self) -> Option<& Map> { self.get_keyword_as_object("$defs") } - fn get_defs_mut(&mut self) -> Option<&mut Object> { + fn get_defs_mut(&mut self) -> Option<&mut Map> { self.get_keyword_as_object_mut("$defs") } - fn get_defs_subschema_from_id(&self, id: &str) -> Option<&Object> { + fn get_defs_subschema_from_id(&self, id: &str) -> Option<&Schema> { let defs = self.get_defs()?; for def in defs.values() { @@ -1345,14 +1335,14 @@ impl SchemaUtilityExtensions for Schema { let def_id = definition.get("$id").and_then(Value::as_str); if def_id == Some(id) { - return Some(definition); + return <&Value as TryInto<&Schema>>::try_into(def).ok() } } } None } - fn get_defs_subschema_from_id_mut(&mut self, id: &str) -> Option<&mut Object> { + fn get_defs_subschema_from_id_mut(&mut self, id: &str) -> Option<&mut Schema> { let defs = self.get_defs_mut()?; for def in defs.values_mut() { @@ -1360,17 +1350,19 @@ impl SchemaUtilityExtensions for Schema { let def_id = definition.get("$id").and_then(Value::as_str); if def_id == Some(id) { - return Some(definition); + return <&mut Value as TryInto<&mut Schema>>::try_into(def).ok() } } } None } - fn get_defs_subschema_from_reference(&self, reference: &str) -> Option<&Object> { + fn get_defs_subschema_from_reference(&self, reference: &str) -> Option<&Schema> { // If the reference is a normative pointer to $defs, short-circuit. if reference.to_string().starts_with("#/$defs/") { - return self.pointer(reference).and_then(Value::as_object); + return self.pointer(reference).and_then(|v| { + <&Value as TryInto<&Schema>>::try_into(v).ok() + }); } let id = reference.to_string(); @@ -1387,10 +1379,12 @@ impl SchemaUtilityExtensions for Schema { None } - fn get_defs_subschema_from_reference_mut(&mut self, reference: &str) -> Option<&mut Object> { + fn get_defs_subschema_from_reference_mut(&mut self, reference: &str) -> Option<&mut Schema> { // If the reference is a normative pointer to $defs, short-circuit. if reference.to_string().starts_with("#/$defs/") { - return self.pointer_mut(reference).and_then(Value::as_object_mut); + return self.pointer_mut(reference).and_then(|v| { + <&mut Value as TryInto<&mut Schema>>::try_into(v).ok() + }); } let id = reference.to_string(); @@ -1410,8 +1404,8 @@ impl SchemaUtilityExtensions for Schema { fn insert_defs_subschema( &mut self, definition_key: &str, - definition_value: &Object - ) -> Option { + definition_value: &Map + ) -> Option> { if let Some(defs) = self.get_defs_mut() { let old_value = defs.clone() .get(definition_key) @@ -1421,7 +1415,7 @@ impl SchemaUtilityExtensions for Schema { defs.insert(definition_key.to_string(), Value::Object(definition_value.clone())) .and(old_value) } else { - let defs: &mut Object = &mut Map::new(); + let defs: &mut Map = &mut Map::new(); defs.insert(definition_key.to_string(), Value::Object(definition_value.clone())); self.insert("$defs".to_string(), Value::Object(defs.clone())); @@ -1431,7 +1425,7 @@ impl SchemaUtilityExtensions for Schema { fn rename_defs_subschema_for_reference(&mut self, reference: &str, new_name: &str) { let lookup_self = self.clone(); // Lookup the reference. If unresolved, return immediately. - let Some(value) = lookup_self.get_defs_subschema_from_reference(reference) else { + let Some(value) = lookup_self.get_defs_subschema_from_reference(reference).and_then(Schema::as_object) else { return; }; // If defs can't be retrieved mutably, return immediately. @@ -1474,20 +1468,20 @@ impl SchemaUtilityExtensions for Schema { self.insert("$id".to_string(), Value::String(id_uri.to_string())) .and(old_id) } - fn get_properties(&self) -> Option<&Object> { + fn get_properties(&self) -> Option<& Map> { self.get_keyword_as_object("properties") } - fn get_properties_mut(&mut self) -> Option<&mut Object> { + fn get_properties_mut(&mut self) -> Option<&mut Map> { self.get_keyword_as_object_mut("properties") } - fn get_property_subschema(&self, property_name: &str) -> Option<&Object> { + fn get_property_subschema(&self, property_name: &str) -> Option<&Schema> { self.get_properties() .and_then(|properties| properties.get(property_name)) - .and_then(Value::as_object) + .and_then(|v| <&Value as TryInto<&Schema>>::try_into(v).ok()) } - fn get_property_subschema_mut(&mut self, property_name: &str) -> Option<&mut Object> { + fn get_property_subschema_mut(&mut self, property_name: &str) -> Option<&mut Schema> { self.get_properties_mut() .and_then(|properties| properties.get_mut(property_name)) - .and_then(Value::as_object_mut) + .and_then(|v| <&mut Value as TryInto<&mut Schema>>::try_into(v).ok()) } } diff --git a/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs b/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs index 25b9ce309..e368bb462 100644 --- a/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs +++ b/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs @@ -341,7 +341,6 @@ test_cases_for_get_keyword_as_mut!( use pretty_assertions::assert_eq; use schemars::json_schema; - use serde_json::json; use crate::schema_utility_extensions::SchemaUtilityExtensions; @@ -388,13 +387,13 @@ test_cases_for_get_keyword_as_mut!( } } }); - let ref expected = json!({ + let ref expected = json_schema!({ "$id": "https://contoso.com/schemas/foo.json", "title": "Foo" }); assert_eq!( schema.get_defs_subschema_from_id("https://contoso.com/schemas/foo.json"), - expected.as_object() + Some(expected) ); } } @@ -403,7 +402,6 @@ test_cases_for_get_keyword_as_mut!( use pretty_assertions::assert_eq; use schemars::json_schema; - use serde_json::json; use crate::schema_utility_extensions::SchemaUtilityExtensions; @@ -450,20 +448,19 @@ test_cases_for_get_keyword_as_mut!( } } }); - let ref mut expected = json!({ + let ref mut expected = json_schema!({ "$id": "https://contoso.com/schemas/foo.json", "title": "Foo" }); assert_eq!( schema.get_defs_subschema_from_id_mut("https://contoso.com/schemas/foo.json"), - expected.as_object_mut() + Some(expected) ); } } #[cfg(test)] mod get_defs_subschema_from_reference { use pretty_assertions::assert_eq; use schemars::json_schema; - use serde_json::json; use crate::schema_utility_extensions::SchemaUtilityExtensions; @@ -524,13 +521,13 @@ test_cases_for_get_keyword_as_mut!( } } }); - let expected = json!({ + let ref expected = json_schema!({ "$id": "https://contoso.com/schemas/foo.json", "title": "Foo" }); assert_eq!( schema.get_defs_subschema_from_reference("#/$defs/foo").unwrap(), - expected.as_object().unwrap() + expected ); } #[test] fn with_absolute_id_uri_reference() { @@ -545,13 +542,13 @@ test_cases_for_get_keyword_as_mut!( } } }); - let expected = json!({ + let ref expected = json_schema!({ "$id": "https://contoso.com/schemas/foo.json", "title": "Foo" }); assert_eq!( schema.get_defs_subschema_from_reference("/schemas/foo.json").unwrap(), - expected.as_object().unwrap() + expected ); } #[test] fn with_relative_id_uri_reference() { @@ -566,13 +563,13 @@ test_cases_for_get_keyword_as_mut!( } } }); - let expected = json!({ + let ref expected = json_schema!({ "$id": "https://contoso.com/schemas/foo.json", "title": "Foo" }); assert_eq!( schema.get_defs_subschema_from_reference("https://contoso.com/schemas/foo.json").unwrap(), - expected.as_object().unwrap() + expected ); } } @@ -580,7 +577,6 @@ test_cases_for_get_keyword_as_mut!( #[cfg(test)] mod get_defs_subschema_from_reference_mut { use pretty_assertions::assert_ne; use schemars::json_schema; - use serde_json::json; use crate::schema_utility_extensions::SchemaUtilityExtensions; @@ -648,14 +644,14 @@ test_cases_for_get_keyword_as_mut!( } } }); - let ref mut expected = json!({ + let ref mut expected = json_schema!({ "$id": "https://contoso.com/schemas/foo.json", "title": "Foo" }); assert_ne!( schema.get_defs_subschema_from_reference_mut("#/$defs/foo"), - expected.as_object_mut() + Some(expected) ); } #[test] fn with_absolute_id_uri_reference() { @@ -670,13 +666,13 @@ test_cases_for_get_keyword_as_mut!( } } }); - let ref mut expected = json!({ + let ref mut expected = json_schema!({ "$id": "https://contoso.com/schemas/foo.json", "title": "Foo" }); assert_eq!( schema.get_defs_subschema_from_reference_mut("/schemas/foo.json").unwrap(), - expected.as_object_mut().unwrap() + expected ); } #[test] fn with_relative_id_uri_reference() { @@ -691,13 +687,13 @@ test_cases_for_get_keyword_as_mut!( } } }); - let ref mut expected = json!({ + let ref mut expected = json_schema!({ "$id": "https://contoso.com/schemas/foo.json", "title": "Foo" }); assert_eq!( schema.get_defs_subschema_from_reference_mut("https://contoso.com/schemas/foo.json").unwrap(), - expected.as_object_mut().unwrap() + expected ); } } @@ -897,7 +893,6 @@ test_cases_for_get_keyword_as_mut!( use pretty_assertions::assert_eq; use schemars::json_schema; - use serde_json::json; use crate::schema_utility_extensions::SchemaUtilityExtensions; @@ -930,7 +925,7 @@ test_cases_for_get_keyword_as_mut!( assert_eq!(schema.get_property_subschema("foo"), None) } #[test] fn when_given_property_is_object() { - let ref property = json!({ + let ref property = json_schema!({ "title": "Foo property" }); let ref schema = json_schema!({ @@ -940,7 +935,7 @@ test_cases_for_get_keyword_as_mut!( }); assert_eq!( schema.get_property_subschema("foo").unwrap(), - property.as_object().unwrap() + property ) } } @@ -950,7 +945,6 @@ test_cases_for_get_keyword_as_mut!( use pretty_assertions::assert_eq; use schemars::json_schema; - use serde_json::json; use crate::schema_utility_extensions::SchemaUtilityExtensions; @@ -983,7 +977,7 @@ test_cases_for_get_keyword_as_mut!( assert_eq!(schema.get_property_subschema_mut("foo"), None) } #[test] fn when_given_property_is_object() { - let ref mut property = json!({ + let ref mut property = json_schema!({ "title": "Foo property" }); let ref mut schema = json_schema!({ @@ -993,7 +987,7 @@ test_cases_for_get_keyword_as_mut!( }); assert_eq!( schema.get_property_subschema_mut("foo").unwrap(), - property.as_object_mut().unwrap() + property ) } } From e7f8cf951b80f918ef96ebdc9b45b1494823cda5 Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Fri, 31 Oct 2025 14:35:44 -0500 Subject: [PATCH 3/3] (MAINT) Restructure `dsc-lib-jsonschema` This change refactors the `dsc-lib-jsonschema` library without modifying any behavior. This change: - Splits the functions in the `transforms` module out into submodules and re-exports them from `transforms` - this keeps referencing the functions the way it was before but makes it easier to navigate the files, given their length. - Makes the unit tests for `schema_utility_extensions` mirror the structure from the source code. - Makes the integration tests for `transform` mirror the structure from the source code. --- lib/dsc-lib-jsonschema/src/tests/mod.rs | 1 - .../mod.rs => schema_utility_extensions.rs} | 0 .../src/tests/transforms/mod.rs | 4 - .../idiomaticize_externally_tagged_enum.rs | 218 ++++++++ .../transforms/idiomaticize_string_enum.rs | 266 ++++++++++ lib/dsc-lib-jsonschema/src/transforms/mod.rs | 483 +----------------- ...=> idiomaticize_externally_tagged_enum.rs} | 0 ...ariants.rs => idiomaticize_string_enum.rs} | 0 .../transforms/idiomaticizing/enums/mod.rs | 7 - .../transforms/idiomaticizing/mod.rs | 8 - .../tests/integration/transforms/mod.rs | 3 +- 11 files changed, 490 insertions(+), 500 deletions(-) rename lib/dsc-lib-jsonschema/src/tests/{schema_utility_extensions/mod.rs => schema_utility_extensions.rs} (100%) delete mode 100644 lib/dsc-lib-jsonschema/src/tests/transforms/mod.rs create mode 100644 lib/dsc-lib-jsonschema/src/transforms/idiomaticize_externally_tagged_enum.rs create mode 100644 lib/dsc-lib-jsonschema/src/transforms/idiomaticize_string_enum.rs rename lib/dsc-lib-jsonschema/tests/integration/transforms/{idiomaticizing/enums/externally_tagged.rs => idiomaticize_externally_tagged_enum.rs} (100%) rename lib/dsc-lib-jsonschema/tests/integration/transforms/{idiomaticizing/enums/string_variants.rs => idiomaticize_string_enum.rs} (100%) delete mode 100644 lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/enums/mod.rs delete mode 100644 lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/mod.rs diff --git a/lib/dsc-lib-jsonschema/src/tests/mod.rs b/lib/dsc-lib-jsonschema/src/tests/mod.rs index 2dba6afbc..28ddd87a6 100644 --- a/lib/dsc-lib-jsonschema/src/tests/mod.rs +++ b/lib/dsc-lib-jsonschema/src/tests/mod.rs @@ -13,5 +13,4 @@ //! of the modules from the rest of the source tree. #[cfg(test)] mod schema_utility_extensions; -#[cfg(test)] mod transforms; #[cfg(test)] mod vscode; diff --git a/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs b/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions.rs similarity index 100% rename from lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs rename to lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions.rs diff --git a/lib/dsc-lib-jsonschema/src/tests/transforms/mod.rs b/lib/dsc-lib-jsonschema/src/tests/transforms/mod.rs deleted file mode 100644 index 596880853..000000000 --- a/lib/dsc-lib-jsonschema/src/tests/transforms/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Unit tests for [`dsc-lib-jsonschema::transforms`] diff --git a/lib/dsc-lib-jsonschema/src/transforms/idiomaticize_externally_tagged_enum.rs b/lib/dsc-lib-jsonschema/src/transforms/idiomaticize_externally_tagged_enum.rs new file mode 100644 index 000000000..0189a2d2f --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/transforms/idiomaticize_externally_tagged_enum.rs @@ -0,0 +1,218 @@ +use schemars::Schema; +use serde_json::{Map, Value, json}; + +use crate::vscode::VSCODE_KEYWORDS; + +/// Munges the generated schema for externally tagged enums into an idiomatic object schema. +/// +/// Schemars generates the schema for externally tagged enums as a schema with the `oneOf` +/// keyword where every tag is a different item in the array. Each item defines a type with a +/// single property, requires that property, and disallows specifying any other properties. +/// +/// This transformer returns the schema as a single object schema with each of the tags defined +/// as properties. It sets both the `minProperties` and `maxProperties` keywords to `1`. This +/// is more idiomatic, shorter to read and parse, easier to reason about, and matches the +/// underlying data semantics more accurately. +/// +/// This transformer should _only_ be used on externally tagged enums. You must specify it with the +/// [schemars `transform()` attribute][`transform`]. +/// +/// # Examples +/// +/// The following struct derives [`JsonSchema`] without specifying the [`transform`] attribute +/// with [`idiomaticize_externally_tagged_enum`]: +/// +/// ``` +/// use pretty_assertions::assert_eq; +/// use serde_json; +/// use schemars::{schema_for, JsonSchema, json_schema}; +/// #[derive(JsonSchema)] +/// pub enum ExternallyTaggedEnum { +/// Name(String), +/// Count(f32), +/// } +/// +/// let generated_schema = schema_for!(ExternallyTaggedEnum); +/// let expected_schema = json_schema!({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "title": "ExternallyTaggedEnum", +/// "oneOf": [ +/// { +/// "type": "object", +/// "properties": { +/// "Name": { +/// "type": "string" +/// } +/// }, +/// "additionalProperties": false, +/// "required": ["Name"] +/// }, +/// { +/// "type": "object", +/// "properties": { +/// "Count": { +/// "type": "number", +/// "format": "float" +/// } +/// }, +/// "additionalProperties": false, +/// "required": ["Count"] +/// } +/// ] +/// }); +/// assert_eq!(generated_schema, expected_schema); +/// ``` +/// +/// While the derived schema _does_ effectively validate the enum, it's difficult to understand +/// without deep familiarity with JSON Schema. Compare it to the same enum with the +/// [`idiomaticize_externally_tagged_enum`] transform applied: +/// +/// ``` +/// use pretty_assertions::assert_eq; +/// use serde_json; +/// use schemars::{schema_for, JsonSchema, json_schema}; +/// use dsc_lib_jsonschema::transforms::idiomaticize_externally_tagged_enum; +/// +/// #[derive(JsonSchema)] +/// #[schemars(transform = idiomaticize_externally_tagged_enum)] +/// pub enum ExternallyTaggedEnum { +/// Name(String), +/// Count(f32), +/// } +/// +/// let generated_schema = schema_for!(ExternallyTaggedEnum); +/// let expected_schema = json_schema!({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "title": "ExternallyTaggedEnum", +/// "type": "object", +/// "properties": { +/// "Name": { +/// "type": "string" +/// }, +/// "Count": { +/// "type": "number", +/// "format": "float" +/// } +/// }, +/// "minProperties": 1, +/// "maxProperties": 1, +/// "additionalProperties": false +/// }); +/// assert_eq!(generated_schema, expected_schema); +/// ``` +/// +/// The transformed schema is shorter, clearer, and idiomatic for JSON Schema draft 2019-09 and +/// later. It validates values as effectively as the default output for an externally tagged +/// enum, but is easier for your users and integrating developers to understand and work +/// with. +/// +/// # Panics +/// +/// This transform panics when called against a generated schema that doesn't define the `oneOf` +/// keyword. Schemars uses the `oneOf` keyword when generating subschemas for externally tagged +/// enums. This transform panics on an invalid application of the transform to prevent unexpected +/// behavior for the schema transformation. This ensures invalid applications are caught during +/// development and CI instead of shipping broken schemas. +/// +/// [`JsonSchema`]: schemars::JsonSchema +/// [`transform`]: derive@schemars::JsonSchema +pub fn idiomaticize_externally_tagged_enum(schema: &mut Schema) { + // First, retrieve the oneOf keyword entries. If this transformer was called against an invalid + // schema or subschema, it should fail fast. + let one_ofs = schema.get("oneOf") + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_externally_tagged_enum.applies_to", + transforming_schema = serde_json::to_string_pretty(schema).unwrap() + )) + .as_array() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_externally_tagged_enum.oneOf_array", + transforming_schema = serde_json::to_string_pretty(schema).unwrap() + )); + // Initialize the map of properties to fill in when introspecting on the items in the oneOf array. + let mut properties_map = Map::new(); + + for item in one_ofs { + let item_data: Map = item.as_object() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_externally_tagged_enum.oneOf_item_as_object", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(item).unwrap() + )) + .clone(); + // If we're accidentally operating on an invalid schema, short-circuit. + let item_data_type = item_data.get("type") + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_externally_tagged_enum.oneOf_item_define_type", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap() + )) + .as_str() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_externally_tagged_enum.oneOf_item_type_string", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap() + )); + assert_t!( + !item_data_type.ne("object"), + "transforms.idiomaticize_externally_tagged_enum.oneOf_item_not_object_type", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), + invalid_type = item_data_type + ); + // Retrieve the title and description from the top-level of the item, if any. Depending on + // the implementation, these values might be set on the item, in the property, or both. + let item_title = item_data.get("title"); + let item_desc = item_data.get("description"); + // Retrieve the property definitions. There should never be more than one property per item, + // but this implementation doesn't guard against that edge case.. + let properties_data = item_data.get("properties") + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_externally_tagged_enum.oneOf_item_properties_missing", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), + )) + .as_object() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_externally_tagged_enum.oneOf_item_properties_not_object", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), + )) + .clone(); + for property_name in properties_data.keys() { + // Retrieve the property definition to munge as needed. + let mut property_data = properties_data.get(property_name) + .unwrap() // can't fail because we're iterating on keys in the map + .as_object() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_externally_tagged_enum.oneOf_item_properties_entry_not_object", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), + name = property_name + )) + .clone(); + // Process the annotation keywords. If they are defined on the item but not the property, + // insert the item-defined keywords into the property data. + if let Some(t) = item_title && property_data.get("title").is_none() { + property_data.insert("title".into(), t.clone()); + } + if let Some(d) = item_desc && property_data.get("description").is_none() { + property_data.insert("description".into(), d.clone()); + } + for keyword in VSCODE_KEYWORDS { + if let Some(keyword_value) = item_data.get(keyword) && property_data.get(keyword).is_none() { + property_data.insert(keyword.to_string(), keyword_value.clone()); + } + } + // Insert the processed property into the top-level properties definition. + properties_map.insert(property_name.into(), serde_json::Value::Object(property_data)); + } + } + // Replace the oneOf array with an idiomatic object schema definition + schema.remove("oneOf"); + schema.insert("type".to_string(), json!("object")); + schema.insert("minProperties".to_string(), json!(1)); + schema.insert("maxProperties".to_string(), json!(1)); + schema.insert("additionalProperties".to_string(), json!(false)); + schema.insert("properties".to_string(), properties_map.into()); +} diff --git a/lib/dsc-lib-jsonschema/src/transforms/idiomaticize_string_enum.rs b/lib/dsc-lib-jsonschema/src/transforms/idiomaticize_string_enum.rs new file mode 100644 index 000000000..7395ec338 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/transforms/idiomaticize_string_enum.rs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::{assert, cmp::PartialEq, option::Option::None}; +use std::{ops::Index}; +use schemars::Schema; +use serde_json::{self, json}; + +/// Munges the generated schema for enums that only define string variants into an idiomatic string +/// schema. +/// +/// When an enum defines string variants without documenting any of the variants, Schemars correctly +/// generates the schema as a `string` subschema with the `enum` keyword. However, if you define any +/// documentation keywords for any variants, Schemars generates the schema with the `oneOf` keyword +/// where every variant is a different item in the array. Each item defines a type with a constant +/// string value, and all annotation keywords for that variant. +/// +/// This transformer returns the schema as a single string schema with each of the variants defined +/// as an item in the `enum` keyword. It hoists the per-variant documentation to the extended +/// keywords recognized by VS Code: `enumDescriptions` and `enumMarkdownDescriptions`. This is more +/// idiomatic, shorter to read and parse, easier to reason about, and matches the underlying data +/// semantics more accurately. +/// +/// # Examples +/// +/// The following struct derives [`JsonSchema`] without specifying the [`transform`] attribute +/// with [`idiomaticize_string_enum`]: +/// +/// ```rust +/// use pretty_assertions::assert_eq; +/// use serde_json; +/// use schemars::{schema_for, JsonSchema, json_schema}; +/// +/// #[derive(JsonSchema)] +/// #[serde(rename_all="camelCase")] +/// enum StringEnum { +/// /// # foo-title +/// /// +/// ///foo-description +/// Foo, +/// /// # bar-title +/// /// +/// /// bar-description +/// Bar, +/// /// # baz-title +/// /// +/// /// baz-description +/// Baz +/// } +/// +/// let generated_schema = schema_for!(StringEnum); +/// let expected_schema = json_schema!({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "title": "StringEnum", +/// "oneOf": [ +/// { +/// "type": "string", +/// "const": "foo", +/// "title": "foo-title", +/// "description": "foo-description" +/// }, +/// { +/// "type": "string", +/// "const": "bar", +/// "title": "bar-title", +/// "description": "bar-description", +/// }, +/// { +/// "type": "string", +/// "const": "baz", +/// "title": "baz-title", +/// "description": "baz-description", +/// } +/// ], +/// }); +/// assert_eq!(generated_schema, expected_schema); +/// ``` +/// +/// While the derived schema _does_ effectively validate the enum, it's difficult to understand +/// without deep familiarity with JSON Schema. Compare it to the same enum with the +/// [`idiomaticize_string_enum`] transform applied: +/// +/// ```rust +/// use pretty_assertions::assert_eq; +/// use serde_json; +/// use schemars::{schema_for, JsonSchema, json_schema}; +/// use dsc_lib_jsonschema::transforms::idiomaticize_string_enum; +/// +/// #[derive(JsonSchema)] +/// #[serde(rename_all="camelCase")] +/// #[schemars(transform = idiomaticize_string_enum)] +/// enum StringEnum { +/// /// # foo-title +/// /// +/// ///foo-description +/// Foo, +/// /// # bar-title +/// /// +/// /// bar-description +/// Bar, +/// /// # baz-title +/// /// +/// /// baz-description +/// Baz +/// } +/// +/// let generated_schema = schema_for!(StringEnum); +/// let expected_schema = json_schema!({ +/// "type": "string", +/// "enum": [ +/// "foo", +/// "bar", +/// "baz" +/// ], +/// "enumDescriptions": [ +/// "foo-description", +/// "bar-description", +/// "baz-description", +/// ], +/// "enumMarkdownDescriptions": [ +/// "foo-description", +/// "bar-description", +/// "baz-description", +/// ], +/// "title": "StringEnum", +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// }); +/// assert_eq!(generated_schema, expected_schema); +/// ``` +/// +/// # Panics +/// +/// If this transform is applied to a schema that defines the `enum` keyword, it immediately +/// returns without modifying the schema. Otherwise, it checks whether the schema defines the +/// `oneOf` keyword. If the generated schema doesn't define the `oneOf` keyword, this transform +/// panics. +/// +/// Schemars uses the `oneOf` keyword when generating subschemas for string enums with annotation +/// keywords. This transform panics on an invalid application of the transform to prevent +/// unexpectedbehavior for the schema transformation. This ensures invalid applications are caught +/// during development and CI instead of shipping broken schemas. +/// +/// [`JsonSchema`]: schemars::JsonSchema +/// [`transform`]: derive@schemars::JsonSchema#transform +pub fn idiomaticize_string_enum(schema: &mut Schema) { + #![allow(clippy::too_many_lines)] + // If this transform is called against a schema defining `enums`, there's nothing to do. + if schema.get("enum").is_some() { + return; + } + // First, retrieve the oneOf keyword entries. If this transformer was called against an invalid + // schema or subschema, it should fail fast. + let one_ofs = schema.get("oneOf") + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_string_enum.applies_to", + transforming_schema = serde_json::to_string_pretty(schema).unwrap() + )) + .as_array() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_string_enum.oneOf_array", + transforming_schema = serde_json::to_string_pretty(schema).unwrap() + )); + // Initialize the vectors for enums, their descriptions, and their markdown descriptions. + let mut enums: Vec = Vec::with_capacity(one_ofs.len()); + let mut enum_descriptions: Vec = Vec::with_capacity(one_ofs.len()); + let mut enum_markdown_descriptions: Vec = Vec::with_capacity(one_ofs.len()); + + // Iterate over the enums to add to the holding vectors. + for (index, item) in one_ofs.iter().enumerate() { + let item_data = item.as_object() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_string_enum.oneOf_item_as_object", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(item).unwrap() + )) + .clone(); + // If we're accidentally operating on an invalid schema, short-circuit. + let item_data_type = item_data.get("type") + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_string_enum.oneOf_item_define_type", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap() + )) + .as_str() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_string_enum.oneOf_item_type_string", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap() + )); + assert_t!( + !item_data_type.ne("string"), + "transforms.idiomaticize_string_enum.oneOf_item_not_string_type", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), + invalid_type = item_data_type + ); + // Retrieve the title, description, and markdownDescription from the item, if any. + let item_title = item_data.get("title").and_then(|v| v.as_str()); + let item_desc = item_data.get("description").and_then(|v| v.as_str()); + let item_md_desc = item_data.get("markdownDescription").and_then(|v| v.as_str()); + // Retrieve the value for the enum - schemars emits as a `const` for each item that has + // docs, and an enum with a single value for non-documented enums. + let item_enum: &str; + if let Some(item_enum_value) = item_data.get("enum") { + item_enum = item_enum_value.as_array() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_string_enum.oneOf_item_enum_not_array", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap() + )) + .index(0) + .as_str() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_string_enum.oneOf_item_enum_item_not_string", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap() + )); + } else { + item_enum = item_data.get("const") + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_string_enum.oneOf_item_const_missing", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap() + )) + .as_str() + .unwrap_or_else(|| panic_t!( + "transforms.idiomaticize_string_enum.oneOf_item_const_not_string", + transforming_schema = serde_json::to_string_pretty(schema).unwrap(), + invalid_item = serde_json::to_string_pretty(&item_data).unwrap() + )); + } + + enums.insert(index, item_enum.to_string()); + + // Define the enumDescription entry as description with title as fallback. If neither + // keyword is defined, add as an empty string. + let desc = match item_desc { + Some(d) => d, + None => item_title.unwrap_or_default(), + }; + enum_descriptions.insert(index, desc.to_string()); + // Define the enumMarkdownDescription entry as markdownDescription with description + // then title as fallback. If none of the keywords are defined, add as an empty string. + let md_desc = match item_md_desc { + Some(d) => d, + None => desc, + }; + enum_markdown_descriptions.insert(index, md_desc.to_string()); + } + // Replace the oneOf array with an idiomatic object schema definition + schema.remove("oneOf"); + schema.insert("type".to_string(), json!("string")); + schema.insert("enum".to_string(), serde_json::to_value(enums).unwrap()); + if enum_descriptions.iter().any(|e| !e.is_empty()) { + schema.insert( + "enumDescriptions".to_string(), + serde_json::to_value(enum_descriptions).unwrap() + ); + } + if enum_markdown_descriptions.iter().any(|e| !e.is_empty()) { + schema.insert( + "enumMarkdownDescriptions".to_string(), + serde_json::to_value(enum_markdown_descriptions).unwrap() + ); + } +} diff --git a/lib/dsc-lib-jsonschema/src/transforms/mod.rs b/lib/dsc-lib-jsonschema/src/transforms/mod.rs index e7311cea6..c93ac5943 100644 --- a/lib/dsc-lib-jsonschema/src/transforms/mod.rs +++ b/lib/dsc-lib-jsonschema/src/transforms/mod.rs @@ -6,482 +6,7 @@ //! //! [`Transform`]: schemars::transform -use core::{assert, cmp::PartialEq}; -use std::{ops::Index}; -use schemars::Schema; -use serde_json::{self, json, Map, Value}; - -use crate::vscode::VSCODE_KEYWORDS; - -/// Munges the generated schema for externally tagged enums into an idiomatic object schema. -/// -/// Schemars generates the schema for externally tagged enums as a schema with the `oneOf` -/// keyword where every tag is a different item in the array. Each item defines a type with a -/// single property, requires that property, and disallows specifying any other properties. -/// -/// This transformer returns the schema as a single object schema with each of the tags defined -/// as properties. It sets both the `minProperties` and `maxProperties` keywords to `1`. This -/// is more idiomatic, shorter to read and parse, easier to reason about, and matches the -/// underlying data semantics more accurately. -/// -/// This transformer should _only_ be used on externally tagged enums. You must specify it with the -/// [schemars `transform()` attribute][`transform`]. -/// -/// # Examples -/// -/// The following struct derives [`JsonSchema`] without specifying the [`transform`] attribute -/// with [`idiomaticize_externally_tagged_enum`]: -/// -/// ``` -/// use pretty_assertions::assert_eq; -/// use serde_json; -/// use schemars::{schema_for, JsonSchema, json_schema}; -/// #[derive(JsonSchema)] -/// pub enum ExternallyTaggedEnum { -/// Name(String), -/// Count(f32), -/// } -/// -/// let generated_schema = schema_for!(ExternallyTaggedEnum); -/// let expected_schema = json_schema!({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "title": "ExternallyTaggedEnum", -/// "oneOf": [ -/// { -/// "type": "object", -/// "properties": { -/// "Name": { -/// "type": "string" -/// } -/// }, -/// "additionalProperties": false, -/// "required": ["Name"] -/// }, -/// { -/// "type": "object", -/// "properties": { -/// "Count": { -/// "type": "number", -/// "format": "float" -/// } -/// }, -/// "additionalProperties": false, -/// "required": ["Count"] -/// } -/// ] -/// }); -/// assert_eq!(generated_schema, expected_schema); -/// ``` -/// -/// While the derived schema _does_ effectively validate the enum, it's difficult to understand -/// without deep familiarity with JSON Schema. Compare it to the same enum with the -/// [`idiomaticize_externally_tagged_enum`] transform applied: -/// -/// ``` -/// use pretty_assertions::assert_eq; -/// use serde_json; -/// use schemars::{schema_for, JsonSchema, json_schema}; -/// use dsc_lib_jsonschema::transforms::idiomaticize_externally_tagged_enum; -/// -/// #[derive(JsonSchema)] -/// #[schemars(transform = idiomaticize_externally_tagged_enum)] -/// pub enum ExternallyTaggedEnum { -/// Name(String), -/// Count(f32), -/// } -/// -/// let generated_schema = schema_for!(ExternallyTaggedEnum); -/// let expected_schema = json_schema!({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "title": "ExternallyTaggedEnum", -/// "type": "object", -/// "properties": { -/// "Name": { -/// "type": "string" -/// }, -/// "Count": { -/// "type": "number", -/// "format": "float" -/// } -/// }, -/// "minProperties": 1, -/// "maxProperties": 1, -/// "additionalProperties": false -/// }); -/// assert_eq!(generated_schema, expected_schema); -/// ``` -/// -/// The transformed schema is shorter, clearer, and idiomatic for JSON Schema draft 2019-09 and -/// later. It validates values as effectively as the default output for an externally tagged -/// enum, but is easier for your users and integrating developers to understand and work -/// with. -/// -/// # Panics -/// -/// This transform panics when called against a generated schema that doesn't define the `oneOf` -/// keyword. Schemars uses the `oneOf` keyword when generating subschemas for externally tagged -/// enums. This transform panics on an invalid application of the transform to prevent unexpected -/// behavior for the schema transformation. This ensures invalid applications are caught during -/// development and CI instead of shipping broken schemas. -/// -/// [`JsonSchema`]: schemars::JsonSchema -/// [`transform`]: derive@schemars::JsonSchema -pub fn idiomaticize_externally_tagged_enum(schema: &mut Schema) { - // First, retrieve the oneOf keyword entries. If this transformer was called against an invalid - // schema or subschema, it should fail fast. - let one_ofs = schema.get("oneOf") - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_externally_tagged_enum.applies_to", - transforming_schema = serde_json::to_string_pretty(schema).unwrap() - )) - .as_array() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_externally_tagged_enum.oneOf_array", - transforming_schema = serde_json::to_string_pretty(schema).unwrap() - )); - // Initialize the map of properties to fill in when introspecting on the items in the oneOf array. - let mut properties_map = Map::new(); - - for item in one_ofs { - let item_data: Map = item.as_object() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_externally_tagged_enum.oneOf_item_as_object", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(item).unwrap() - )) - .clone(); - // If we're accidentally operating on an invalid schema, short-circuit. - let item_data_type = item_data.get("type") - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_externally_tagged_enum.oneOf_item_define_type", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap() - )) - .as_str() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_externally_tagged_enum.oneOf_item_type_string", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap() - )); - assert_t!( - !item_data_type.ne("object"), - "transforms.idiomaticize_externally_tagged_enum.oneOf_item_not_object_type", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), - invalid_type = item_data_type - ); - // Retrieve the title and description from the top-level of the item, if any. Depending on - // the implementation, these values might be set on the item, in the property, or both. - let item_title = item_data.get("title"); - let item_desc = item_data.get("description"); - // Retrieve the property definitions. There should never be more than one property per item, - // but this implementation doesn't guard against that edge case.. - let properties_data = item_data.get("properties") - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_externally_tagged_enum.oneOf_item_properties_missing", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), - )) - .as_object() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_externally_tagged_enum.oneOf_item_properties_not_object", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), - )) - .clone(); - for property_name in properties_data.keys() { - // Retrieve the property definition to munge as needed. - let mut property_data = properties_data.get(property_name) - .unwrap() // can't fail because we're iterating on keys in the map - .as_object() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_externally_tagged_enum.oneOf_item_properties_entry_not_object", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), - name = property_name - )) - .clone(); - // Process the annotation keywords. If they are defined on the item but not the property, - // insert the item-defined keywords into the property data. - if let Some(t) = item_title && property_data.get("title").is_none() { - property_data.insert("title".into(), t.clone()); - } - if let Some(d) = item_desc && property_data.get("description").is_none() { - property_data.insert("description".into(), d.clone()); - } - for keyword in VSCODE_KEYWORDS { - if let Some(keyword_value) = item_data.get(keyword) && property_data.get(keyword).is_none() { - property_data.insert(keyword.to_string(), keyword_value.clone()); - } - } - // Insert the processed property into the top-level properties definition. - properties_map.insert(property_name.into(), serde_json::Value::Object(property_data)); - } - } - // Replace the oneOf array with an idiomatic object schema definition - schema.remove("oneOf"); - schema.insert("type".to_string(), json!("object")); - schema.insert("minProperties".to_string(), json!(1)); - schema.insert("maxProperties".to_string(), json!(1)); - schema.insert("additionalProperties".to_string(), json!(false)); - schema.insert("properties".to_string(), properties_map.into()); -} - -/// Munges the generated schema for enums that only define string variants into an idiomatic string -/// schema. -/// -/// When an enum defines string variants without documenting any of the variants, Schemars correctly -/// generates the schema as a `string` subschema with the `enum` keyword. However, if you define any -/// documentation keywords for any variants, Schemars generates the schema with the `oneOf` keyword -/// where every variant is a different item in the array. Each item defines a type with a constant -/// string value, and all annotation keywords for that variant. -/// -/// This transformer returns the schema as a single string schema with each of the variants defined -/// as an item in the `enum` keyword. It hoists the per-variant documentation to the extended -/// keywords recognized by VS Code: `enumDescriptions` and `enumMarkdownDescriptions`. This is more -/// idiomatic, shorter to read and parse, easier to reason about, and matches the underlying data -/// semantics more accurately. -/// -/// # Examples -/// -/// The following struct derives [`JsonSchema`] without specifying the [`transform`] attribute -/// with [`idiomaticize_string_enum`]: -/// -/// ```rust -/// use pretty_assertions::assert_eq; -/// use serde_json; -/// use schemars::{schema_for, JsonSchema, json_schema}; -/// -/// #[derive(JsonSchema)] -/// #[serde(rename_all="camelCase")] -/// enum StringEnum { -/// /// # foo-title -/// /// -/// ///foo-description -/// Foo, -/// /// # bar-title -/// /// -/// /// bar-description -/// Bar, -/// /// # baz-title -/// /// -/// /// baz-description -/// Baz -/// } -/// -/// let generated_schema = schema_for!(StringEnum); -/// let expected_schema = json_schema!({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "title": "StringEnum", -/// "oneOf": [ -/// { -/// "type": "string", -/// "const": "foo", -/// "title": "foo-title", -/// "description": "foo-description" -/// }, -/// { -/// "type": "string", -/// "const": "bar", -/// "title": "bar-title", -/// "description": "bar-description", -/// }, -/// { -/// "type": "string", -/// "const": "baz", -/// "title": "baz-title", -/// "description": "baz-description", -/// } -/// ], -/// }); -/// assert_eq!(generated_schema, expected_schema); -/// ``` -/// -/// While the derived schema _does_ effectively validate the enum, it's difficult to understand -/// without deep familiarity with JSON Schema. Compare it to the same enum with the -/// [`idiomaticize_string_enum`] transform applied: -/// -/// ```rust -/// use pretty_assertions::assert_eq; -/// use serde_json; -/// use schemars::{schema_for, JsonSchema, json_schema}; -/// use dsc_lib_jsonschema::transforms::idiomaticize_string_enum; -/// -/// #[derive(JsonSchema)] -/// #[serde(rename_all="camelCase")] -/// #[schemars(transform = idiomaticize_string_enum)] -/// enum StringEnum { -/// /// # foo-title -/// /// -/// ///foo-description -/// Foo, -/// /// # bar-title -/// /// -/// /// bar-description -/// Bar, -/// /// # baz-title -/// /// -/// /// baz-description -/// Baz -/// } -/// -/// let generated_schema = schema_for!(StringEnum); -/// let expected_schema = json_schema!({ -/// "type": "string", -/// "enum": [ -/// "foo", -/// "bar", -/// "baz" -/// ], -/// "enumDescriptions": [ -/// "foo-description", -/// "bar-description", -/// "baz-description", -/// ], -/// "enumMarkdownDescriptions": [ -/// "foo-description", -/// "bar-description", -/// "baz-description", -/// ], -/// "title": "StringEnum", -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// }); -/// assert_eq!(generated_schema, expected_schema); -/// ``` -/// -/// # Panics -/// -/// If this transform is applied to a schema that defines the `enum` keyword, it immediately -/// returns without modifying the schema. Otherwise, it checks whether the schema defines the -/// `oneOf` keyword. If the generated schema doesn't define the `oneOf` keyword, this transform -/// panics. -/// -/// Schemars uses the `oneOf` keyword when generating subschemas for string enums with annotation -/// keywords. This transform panics on an invalid application of the transform to prevent -/// unexpectedbehavior for the schema transformation. This ensures invalid applications are caught -/// during development and CI instead of shipping broken schemas. -/// -/// [`JsonSchema`]: schemars::JsonSchema -/// [`transform`]: derive@schemars::JsonSchema#transform -pub fn idiomaticize_string_enum(schema: &mut Schema) { - #![allow(clippy::too_many_lines)] - // If this transform is called against a schema defining `enums`, there's nothing to do. - if schema.get("enum").is_some() { - return; - } - // First, retrieve the oneOf keyword entries. If this transformer was called against an invalid - // schema or subschema, it should fail fast. - let one_ofs = schema.get("oneOf") - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_string_enum.applies_to", - transforming_schema = serde_json::to_string_pretty(schema).unwrap() - )) - .as_array() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_string_enum.oneOf_array", - transforming_schema = serde_json::to_string_pretty(schema).unwrap() - )); - // Initialize the vectors for enums, their descriptions, and their markdown descriptions. - let mut enums: Vec = Vec::with_capacity(one_ofs.len()); - let mut enum_descriptions: Vec = Vec::with_capacity(one_ofs.len()); - let mut enum_markdown_descriptions: Vec = Vec::with_capacity(one_ofs.len()); - - // Iterate over the enums to add to the holding vectors. - for (index, item) in one_ofs.iter().enumerate() { - let item_data = item.as_object() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_string_enum.oneOf_item_as_object", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(item).unwrap() - )) - .clone(); - // If we're accidentally operating on an invalid schema, short-circuit. - let item_data_type = item_data.get("type") - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_string_enum.oneOf_item_define_type", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap() - )) - .as_str() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_string_enum.oneOf_item_type_string", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap() - )); - assert_t!( - !item_data_type.ne("string"), - "transforms.idiomaticize_string_enum.oneOf_item_not_string_type", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap(), - invalid_type = item_data_type - ); - // Retrieve the title, description, and markdownDescription from the item, if any. - let item_title = item_data.get("title").and_then(|v| v.as_str()); - let item_desc = item_data.get("description").and_then(|v| v.as_str()); - let item_md_desc = item_data.get("markdownDescription").and_then(|v| v.as_str()); - // Retrieve the value for the enum - schemars emits as a `const` for each item that has - // docs, and an enum with a single value for non-documented enums. - let item_enum: &str; - if let Some(item_enum_value) = item_data.get("enum") { - item_enum = item_enum_value.as_array() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_string_enum.oneOf_item_enum_not_array", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap() - )) - .index(0) - .as_str() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_string_enum.oneOf_item_enum_item_not_string", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap() - )); - } else { - item_enum = item_data.get("const") - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_string_enum.oneOf_item_const_missing", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap() - )) - .as_str() - .unwrap_or_else(|| panic_t!( - "transforms.idiomaticize_string_enum.oneOf_item_const_not_string", - transforming_schema = serde_json::to_string_pretty(schema).unwrap(), - invalid_item = serde_json::to_string_pretty(&item_data).unwrap() - )); - } - - enums.insert(index, item_enum.to_string()); - - // Define the enumDescription entry as description with title as fallback. If neither - // keyword is defined, add as an empty string. - let desc = match item_desc { - Some(d) => d, - None => item_title.unwrap_or_default(), - }; - enum_descriptions.insert(index, desc.to_string()); - // Define the enumMarkdownDescription entry as markdownDescription with description - // then title as fallback. If none of the keywords are defined, add as an empty string. - let md_desc = match item_md_desc { - Some(d) => d, - None => desc, - }; - enum_markdown_descriptions.insert(index, md_desc.to_string()); - } - // Replace the oneOf array with an idiomatic object schema definition - schema.remove("oneOf"); - schema.insert("type".to_string(), json!("string")); - schema.insert("enum".to_string(), serde_json::to_value(enums).unwrap()); - if enum_descriptions.iter().any(|e| !e.is_empty()) { - schema.insert( - "enumDescriptions".to_string(), - serde_json::to_value(enum_descriptions).unwrap() - ); - } - if enum_markdown_descriptions.iter().any(|e| !e.is_empty()) { - schema.insert( - "enumMarkdownDescriptions".to_string(), - serde_json::to_value(enum_markdown_descriptions).unwrap() - ); - } -} +mod idiomaticize_externally_tagged_enum; +pub use idiomaticize_externally_tagged_enum::idiomaticize_externally_tagged_enum; +mod idiomaticize_string_enum; +pub use idiomaticize_string_enum::idiomaticize_string_enum; diff --git a/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/enums/externally_tagged.rs b/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticize_externally_tagged_enum.rs similarity index 100% rename from lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/enums/externally_tagged.rs rename to lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticize_externally_tagged_enum.rs diff --git a/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/enums/string_variants.rs b/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticize_string_enum.rs similarity index 100% rename from lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/enums/string_variants.rs rename to lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticize_string_enum.rs diff --git a/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/enums/mod.rs b/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/enums/mod.rs deleted file mode 100644 index 71958fe2d..000000000 --- a/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/enums/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Integration tests for idiomaticizing the generated schemas for `enum` items. - -#[cfg(test)] mod string_variants; -#[cfg(test)] mod externally_tagged; diff --git a/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/mod.rs b/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/mod.rs deleted file mode 100644 index 9b7c511ff..000000000 --- a/lib/dsc-lib-jsonschema/tests/integration/transforms/idiomaticizing/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Integration tests for idiomaticizing the generated schemas. The schemas that [`schemars`] -//! generates are sometimes non-idiomatic, especially when you use annotation keywords for variants -//! and fields. - -#[cfg(test)] mod enums; diff --git a/lib/dsc-lib-jsonschema/tests/integration/transforms/mod.rs b/lib/dsc-lib-jsonschema/tests/integration/transforms/mod.rs index f50483bed..94d0e6fec 100644 --- a/lib/dsc-lib-jsonschema/tests/integration/transforms/mod.rs +++ b/lib/dsc-lib-jsonschema/tests/integration/transforms/mod.rs @@ -5,4 +5,5 @@ //! a user can add with the `#[schemars(transform = )]` attribute to modify the //! generated schema. -#[cfg(test)] mod idiomaticizing; +#[cfg(test)] mod idiomaticize_externally_tagged_enum; +#[cfg(test)] mod idiomaticize_string_enum;