From 3ab124a0731e9c2698cc90642940596b115da408 Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Mon, 27 Oct 2025 15:01:29 -0500 Subject: [PATCH 1/6] (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 b08aa723e..4f389516b 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 192888a8b97a072cef0b72046be05d28dda4cfc6 Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Mon, 27 Oct 2025 15:07:41 -0500 Subject: [PATCH 2/6] (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 4f389516b..8c845da27 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,20 +563,19 @@ 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 ); } } #[cfg(test)] mod get_defs_subschema_from_reference_mut { use schemars::json_schema; - use serde_json::json; use crate::schema_utility_extensions::SchemaUtilityExtensions; @@ -640,14 +636,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_eq!( schema.get_defs_subschema_from_reference_mut("#/$defs/foo"), - expected.as_object_mut() + Some(expected) ); } #[test] fn with_absolute_id_uri_reference() { @@ -662,13 +658,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() { @@ -683,13 +679,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 ); } } @@ -889,7 +885,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; @@ -922,7 +917,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!({ @@ -932,7 +927,7 @@ test_cases_for_get_keyword_as_mut!( }); assert_eq!( schema.get_property_subschema("foo").unwrap(), - property.as_object().unwrap() + property ) } } @@ -942,7 +937,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; @@ -975,7 +969,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!({ @@ -985,7 +979,7 @@ test_cases_for_get_keyword_as_mut!( }); assert_eq!( schema.get_property_subschema_mut("foo").unwrap(), - property.as_object_mut().unwrap() + property ) } } From afb01af6baded2ce4f2ddc4e9953f9057a922950 Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Fri, 31 Oct 2025 14:35:44 -0500 Subject: [PATCH 3/6] (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. --- .../src/schema_utility_extensions.rs | 28 +- 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 +- 12 files changed, 504 insertions(+), 514 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/schema_utility_extensions.rs b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs index 1ea250f39..51d26981f 100644 --- a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs +++ b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs @@ -344,7 +344,7 @@ pub trait SchemaUtilityExtensions { /// None /// ) /// ``` - fn get_keyword_as_object(&self, key: &str) -> Option<& Map>; + 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`]. /// @@ -395,7 +395,7 @@ pub trait SchemaUtilityExtensions { /// None /// ) /// ``` - fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map>; + 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`]. /// @@ -537,7 +537,7 @@ pub trait SchemaUtilityExtensions { /// 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`]. + /// If the keyword doesn't exist or isn't a subschema, this function returns [`None`]. /// /// # Examples /// @@ -800,7 +800,7 @@ pub trait SchemaUtilityExtensions { /// defs_json.as_object() /// ); /// ``` - fn get_defs(&self) -> Option<& Map>; + 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`]. @@ -828,7 +828,7 @@ pub trait SchemaUtilityExtensions { /// defs_json.as_object_mut() /// ); /// ``` - fn get_defs_mut(&mut self) -> Option<&mut Map>; + 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. /// @@ -1107,7 +1107,7 @@ pub trait SchemaUtilityExtensions { /// Some(&mut new_definition) /// ) /// ``` - fn insert_defs_subschema(&mut self, definition_key: &str, definition_value: & Map) -> Option< Map>; + 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. /// @@ -1177,7 +1177,7 @@ pub trait SchemaUtilityExtensions { /// properties_json.as_object() /// ); /// ``` - fn get_properties(&self) -> Option<& Map>; + 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`]. @@ -1205,7 +1205,7 @@ pub trait SchemaUtilityExtensions { /// properties_json.as_object_mut() /// ); /// ``` - fn get_properties_mut(&mut self) -> Option<&mut Map>; + 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 /// a [`Schema`] if it exists. /// @@ -1292,11 +1292,11 @@ impl SchemaUtilityExtensions for Schema { self.get(key) .and_then(Value::as_number) } - fn get_keyword_as_object(&self, key: &str) -> Option<& Map> { + 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 Map> { + fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map> { self.get_mut(key) .and_then(Value::as_object_mut) } @@ -1321,10 +1321,10 @@ impl SchemaUtilityExtensions for Schema { self.get(key) .and_then(Value::as_u64) } - fn get_defs(&self) -> Option<& Map> { + fn get_defs(&self) -> Option<&Map> { self.get_keyword_as_object("$defs") } - fn get_defs_mut(&mut self) -> Option<&mut Map> { + 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<&Schema> { @@ -1468,10 +1468,10 @@ impl SchemaUtilityExtensions for Schema { self.insert("$id".to_string(), Value::String(id_uri.to_string())) .and(old_id) } - fn get_properties(&self) -> Option<& Map> { + fn get_properties(&self) -> Option<&Map> { self.get_keyword_as_object("properties") } - fn get_properties_mut(&mut self) -> Option<&mut Map> { + 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<&Schema> { 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..084b57eac --- /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 +/// 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#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; From 728fce61f859880b01e4152c2a404b7064893915 Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Mon, 27 Oct 2025 15:07:41 -0500 Subject: [PATCH 4/6] (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 | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs index 51d26981f..628141e8a 100644 --- a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs +++ b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs @@ -344,7 +344,7 @@ pub trait SchemaUtilityExtensions { /// None /// ) /// ``` - fn get_keyword_as_object(&self, key: &str) -> Option<&Map>; + fn get_keyword_as_object(&self, key: &str) -> Option<&Object>; /// Checks a JSON Schema for a given keyword and mutably borrows the value of that keyword, /// if it exists, as a [`Map`]. /// @@ -395,7 +395,7 @@ pub trait SchemaUtilityExtensions { /// None /// ) /// ``` - fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map>; + fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Object>; /// Checks a JSON schema for a given keyword and borrows the value of that keyword, if it /// exists, as a [`Number`]. /// @@ -800,7 +800,7 @@ pub trait SchemaUtilityExtensions { /// defs_json.as_object() /// ); /// ``` - fn get_defs(&self) -> Option<&Map>; + fn get_defs(&self) -> Option<&Object>; /// 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`]. @@ -828,9 +828,9 @@ pub trait SchemaUtilityExtensions { /// defs_json.as_object_mut() /// ); /// ``` - 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. + 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. /// /// 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 @@ -1107,7 +1107,7 @@ pub trait SchemaUtilityExtensions { /// Some(&mut new_definition) /// ) /// ``` - fn insert_defs_subschema(&mut self, definition_key: &str, definition_value: &Map) -> Option< Map>; + fn insert_defs_subschema(&mut self, definition_key: &str, definition_value: &Object) -> Option; /// Looks up a subschema in the `$defs` keyword by reference and, if it exists, renames the /// _key_ for the definition. /// @@ -1177,7 +1177,7 @@ pub trait SchemaUtilityExtensions { /// properties_json.as_object() /// ); /// ``` - fn get_properties(&self) -> Option<&Map>; + fn get_properties(&self) -> Option<&Object>; /// 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`]. @@ -1205,7 +1205,7 @@ pub trait SchemaUtilityExtensions { /// properties_json.as_object_mut() /// ); /// ``` - fn get_properties_mut(&mut self) -> Option<&mut Map>; + fn get_properties_mut(&mut self) -> Option<&mut Object>; /// Looks up a property in the `properties` keyword by name and returns the subschema entry as /// a [`Schema`] if it exists. /// @@ -1292,11 +1292,11 @@ impl SchemaUtilityExtensions for Schema { self.get(key) .and_then(Value::as_number) } - fn get_keyword_as_object(&self, key: &str) -> Option<&Map> { + fn get_keyword_as_object(&self, key: &str) -> Option<&Object> { self.get(key) .and_then(Value::as_object) } - fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Map> { + fn get_keyword_as_object_mut(&mut self, key: &str) -> Option<&mut Object> { self.get_mut(key) .and_then(Value::as_object_mut) } @@ -1321,10 +1321,10 @@ impl SchemaUtilityExtensions for Schema { self.get(key) .and_then(Value::as_u64) } - fn get_defs(&self) -> Option<&Map> { + fn get_defs(&self) -> Option<&Object> { self.get_keyword_as_object("$defs") } - fn get_defs_mut(&mut self) -> Option<&mut Map> { + fn get_defs_mut(&mut self) -> Option<&mut Object> { self.get_keyword_as_object_mut("$defs") } fn get_defs_subschema_from_id(&self, id: &str) -> Option<&Schema> { @@ -1468,10 +1468,10 @@ impl SchemaUtilityExtensions for Schema { self.insert("$id".to_string(), Value::String(id_uri.to_string())) .and(old_id) } - fn get_properties(&self) -> Option<&Map> { + fn get_properties(&self) -> Option<&Object> { self.get_keyword_as_object("properties") } - fn get_properties_mut(&mut self) -> Option<&mut Map> { + fn get_properties_mut(&mut self) -> Option<&mut Object> { self.get_keyword_as_object_mut("properties") } fn get_property_subschema(&self, property_name: &str) -> Option<&Schema> { From 6c5884c57a13f7fb1cb33b38748f8f36f433935c Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Wed, 19 Nov 2025 12:44:12 -0600 Subject: [PATCH 5/6] (GH-538) Define VS Code extended vocabulary and dialect This change addresses the need for parsing, validating, and processing the custom JSON Schema keywords that VS Code recognizes for use in the DSC schemas and resource schemas. This change: - Defines every custom keyword that the VS Code language server for JSON recognizes as custom keywords in the `jsonschema` crate so that we can correctly validate schemas that use those keywords programmatically. The implementation in `jsonschema` is primarily intended for custom validation keywords. Every custom keyword that VS Code recognizes is an annotation keyword. They don't affect validation for instances of data. This implementation ensures that we can parse those keywords for our own use and validate that our schema definitions correctly define values for those keywords. - Defines the VS Code vocabulary, which includes the keywords. This enables us to define our own dialects which recognize the VS Code keywords. - Defines a VS Code dialect, which includes the Draft 2020-12 vocabularies and the VS Code vocabulary. - Defines the `VSCodeKeyword` enum for easier selection of keywords, given that you can't ergonomically or safely pass a _type_ in Rust functions. This replaces the previous `VSCODE_KEYWORDS` struct. - Updates the `idiomaticize_externally_tagged_enum` transform function to use the `VSCodeKeyword` enum now that `VSCODE_KEYWORDS` is removed. - Defines extension methods for the `ValidationOptions` struct from the `jsonschema` crate to make adding VS Code keywords, vocabulary, and dialect simpler and more ergonomic, participating in the builder pattern that `jsonschema` uses when creating a validator. - Defines extension methods for the `Schema` struct from the `schemars` crate to simplify working with VS COde keywords in schemas, such as retrieving the value for a keyword or checking whether the schema uses the VS Code dialect. - Adds three new extension methods to the `SchemaUtilityExtensions` trait: - `get_bundled_schema_resource_ids`, which retrieves the value of the `$id` keyword from entries in the `$defs` keyword, each of which represents a bundled schema resource. - `get_references`, which recursively retrieves the value for every `$ref` keyword in the schema. - `replace_references`, which recursively replaces the value of the `$ref` keyword from a given value to a new one specified by the caller. - `reference_is_for_bundled_resource`, which indicates whether a given value for a `$ref` keyword points to a bundled schema resource in the `$defs` keyword for the schema. This change lays the groundwork for incorporating the VS Code keywords into the schemas generated from Rust code and for defining our own vocabulary for DSC as needed. --- Cargo.lock | 2 + Cargo.toml | 2 +- lib/dsc-lib-jsonschema/.clippy.toml | 1 + lib/dsc-lib-jsonschema/Cargo.toml | 2 + lib/dsc-lib-jsonschema/locales/en-us.toml | 53 ++ lib/dsc-lib-jsonschema/locales/vscode.yaml | 471 ++++++++++ .../src/schema_utility_extensions.rs | 350 ++++++- .../src/tests/vscode/dialect.rs | 102 ++ .../tests/vscode/keywords/allow_comments.rs | 40 + .../vscode/keywords/allow_trailing_commas.rs | 40 + .../vscode/keywords/completion_detail.rs | 40 + .../tests/vscode/keywords/default_snippets.rs | 102 ++ .../vscode/keywords/deprecation_message.rs | 40 + .../tests/vscode/keywords/do_not_suggest.rs | 40 + .../vscode/keywords/enum_descriptions.rs | 71 ++ .../src/tests/vscode/keywords/enum_details.rs | 71 ++ .../tests/vscode/keywords/enum_sort_texts.rs | 71 ++ .../tests/vscode/keywords/error_message.rs | 40 + .../vscode/keywords/markdown_description.rs | 40 + .../keywords/markdown_enum_descriptions.rs | 71 ++ .../src/tests/vscode/keywords/mod.rs | 52 + .../vscode/keywords/pattern_error_message.rs | 40 + .../vscode/keywords/suggest_sort_text.rs | 40 + .../src/tests/vscode/mod.rs | 6 + .../src/tests/vscode/schema_extensions.rs | 529 +++++++++++ .../vscode/validation_options_extensions.rs | 889 ++++++++++++++++++ .../src/tests/vscode/vocabulary.rs | 102 ++ .../idiomaticize_externally_tagged_enum.rs | 10 +- lib/dsc-lib-jsonschema/src/vscode/dialect.rs | 329 +++++++ .../src/vscode/keywords/allow_comments.rs | 74 ++ .../vscode/keywords/allow_trailing_commas.rs | 75 ++ .../src/vscode/keywords/completion_detail.rs | 81 ++ .../src/vscode/keywords/default_snippets.rs | 243 +++++ .../vscode/keywords/deprecation_message.rs | 75 ++ .../src/vscode/keywords/do_not_suggest.rs | 75 ++ .../src/vscode/keywords/enum_descriptions.rs | 108 +++ .../src/vscode/keywords/enum_details.rs | 103 ++ .../src/vscode/keywords/enum_sort_texts.rs | 110 +++ .../src/vscode/keywords/error_message.rs | 97 ++ .../vscode/keywords/markdown_description.rs | 85 ++ .../keywords/markdown_enum_descriptions.rs | 104 ++ .../src/vscode/keywords/mod.rs | 45 + .../vscode/keywords/pattern_error_message.rs | 78 ++ .../src/vscode/keywords/suggest_sort_text.rs | 90 ++ .../src/vscode/keywords/vscode_keyword.rs | 243 +++++ .../keywords/vscode_keyword_definition.rs | 86 ++ lib/dsc-lib-jsonschema/src/vscode/mod.rs | 31 +- .../src/vscode/schema_extensions.rs | 220 +++++ .../vscode/validation_options_extensions.rs | 241 +++++ .../src/vscode/vocabulary.rs | 349 +++++++ .../src/vscode/vscode_schema_extensions.rs | 32 - 51 files changed, 6228 insertions(+), 63 deletions(-) create mode 100644 lib/dsc-lib-jsonschema/.clippy.toml create mode 100644 lib/dsc-lib-jsonschema/locales/vscode.yaml create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/dialect.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/allow_comments.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/allow_trailing_commas.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/completion_detail.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/default_snippets.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/deprecation_message.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/do_not_suggest.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_descriptions.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_details.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_sort_texts.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/error_message.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/markdown_description.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/markdown_enum_descriptions.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/mod.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/pattern_error_message.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/keywords/suggest_sort_text.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/schema_extensions.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/validation_options_extensions.rs create mode 100644 lib/dsc-lib-jsonschema/src/tests/vscode/vocabulary.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/dialect.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/allow_comments.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/allow_trailing_commas.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/completion_detail.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/default_snippets.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/deprecation_message.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/do_not_suggest.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/enum_descriptions.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/enum_details.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/enum_sort_texts.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/error_message.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/markdown_description.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/markdown_enum_descriptions.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/mod.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/pattern_error_message.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/suggest_sort_text.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/vscode_keyword.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/keywords/vscode_keyword_definition.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/schema_extensions.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/validation_options_extensions.rs create mode 100644 lib/dsc-lib-jsonschema/src/vscode/vocabulary.rs delete mode 100644 lib/dsc-lib-jsonschema/src/vscode/vscode_schema_extensions.rs diff --git a/Cargo.lock b/Cargo.lock index 30b9174c8..2a4da5788 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,6 +669,7 @@ dependencies = [ name = "dsc-lib-jsonschema" version = "0.0.0" dependencies = [ + "jsonschema", "pretty_assertions", "regex", "rust-i18n", @@ -677,6 +678,7 @@ dependencies = [ "serde_json", "tracing", "url", + "urlencoding", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 510f0b4be..5d339faaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -200,7 +200,7 @@ utfx = { version = "0.1" } uuid = { version = "1.18", features = ["v4"] } # dsc-lib, dsc-lib-jsonschema url = { version = "2.5" } -# dsc-lib +# dsc-lib, dsc-lib-jsonschema urlencoding = { version = "2.1" } # dsc-lib which = { version = "8.0" } diff --git a/lib/dsc-lib-jsonschema/.clippy.toml b/lib/dsc-lib-jsonschema/.clippy.toml new file mode 100644 index 000000000..9f36c4218 --- /dev/null +++ b/lib/dsc-lib-jsonschema/.clippy.toml @@ -0,0 +1 @@ +doc-valid-idents = ["IntelliSense", ".."] diff --git a/lib/dsc-lib-jsonschema/Cargo.toml b/lib/dsc-lib-jsonschema/Cargo.toml index b27abe823..181dfe3dc 100644 --- a/lib/dsc-lib-jsonschema/Cargo.toml +++ b/lib/dsc-lib-jsonschema/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" doctest = false # Disable doc tests by default for compilation speed [dependencies] +jsonschema = { workspace = true } regex = { workspace = true } rust-i18n = { workspace = true } schemars = { workspace = true } @@ -14,6 +15,7 @@ serde = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } url = { workspace = true } +urlencoding = { workspace = true } [dev-dependencies] # Helps review complex comparisons, like schemas diff --git a/lib/dsc-lib-jsonschema/locales/en-us.toml b/lib/dsc-lib-jsonschema/locales/en-us.toml index 00a721fbb..83ec62687 100644 --- a/lib/dsc-lib-jsonschema/locales/en-us.toml +++ b/lib/dsc-lib-jsonschema/locales/en-us.toml @@ -112,3 +112,56 @@ invalid item: %{invalid_item} transforming schema: %{transforming_schema} """ + +[vscode.keywords.allow_comments] +factory_error_invalid_type = "The 'allowComments' VS Code keyword must be a boolean value." + +[vscode.keywords.allow_trailing_commas] +factory_error_invalid_type = "The 'allowTrailingCommas' VS Code keyword must be a boolean value." + +[vscode.keywords.completion_detail] +factory_error_invalid_type = "The 'completionDetail' VS Code keyword must be a string value." + +[vscode.keywords.default_snippets] +factory_error_suffix = "The 'defaultSnippets' VS Code keyword must be an array of objects. Every object must be a valid snippet definition." +factory_error_not_array = "Non-array value is invalid." +factory_error_invalid_item = "Invalid definition for an object in the array." +factory_error_non_object_item = "Array containing non-object items is invalid." + +[vscode.keywords.deprecation_message] +factory_error_invalid_type = "The 'deprecationMessage' VS Code keyword must be a string value." + +[vscode.keywords.do_not_suggest] +factory_error_invalid_type = "The 'doNotSuggest' VS Code keyword must be a boolean value." + +[vscode.keywords.enum_descriptions] +factory_error_suffix = "The 'enumDescriptions' VS Code keyword must be an array of string values. The 'enumDescriptions' keyword should have the same number of items as the 'enum' keyword." +factory_error_not_array = "Non-array value is invalid." +factory_error_non_string_item = "Array containing non-string items is invalid." + +[vscode.keywords.enum_details] +factory_error_suffix = "The 'enumDetails' VS Code keyword must be an array of string values. The 'enumDetails' keyword should have the same number of items as the 'enum' keyword." +factory_error_not_array = "Non-array value is invalid." +factory_error_non_string_item = "Array containing non-string items is invalid." + +[vscode.keywords.enum_sort_texts] +factory_error_suffix = "The 'enumSortTexts' VS Code keyword must be an array of string values. The 'enumSortTexts' keyword should have the same number of items as the 'enum' keyword." +factory_error_not_array = "Non-array value is invalid." +factory_error_non_string_item = "Array containing non-string items is invalid." + +[vscode.keywords.error_message] +factory_error_invalid_type = "The 'errorMessage' VS Code keyword must be a string value." + +[vscode.keywords.markdown_description] +factory_error_invalid_type = "The 'markdownDescription' VS Code keyword must be a string value." + +[vscode.keywords.markdown_enum_descriptions] +factory_error_suffix = "The 'markdownEnumDescriptions' VS Code keyword must be an array of string values. The 'markdownEnumDescriptions' keyword should have the same number of items as the 'enum' keyword." +factory_error_not_array = "Non-array value is invalid." +factory_error_non_string_item = "Array containing non-string items is invalid." + +[vscode.keywords.pattern_error_message] +factory_error_invalid_type = "The 'allowTrailingCommas' VS Code keyword must be a string value." + +[vscode.keywords.suggest_sort_text] +factory_error_invalid_type = "The 'allowTrailingCommas' VS Code keyword must be a string value." diff --git a/lib/dsc-lib-jsonschema/locales/vscode.yaml b/lib/dsc-lib-jsonschema/locales/vscode.yaml new file mode 100644 index 000000000..251858037 --- /dev/null +++ b/lib/dsc-lib-jsonschema/locales/vscode.yaml @@ -0,0 +1,471 @@ +_version: 2 + +vscode: + dialect: + title: + en-US: VS Code dialect + description: + en-US: >- + Defines a meta schema combining Draft 2020-12 and the VS Code extended vocabulary. + markdownDescription: + en-US: |- + Defines a meta schema combining Draft 2020-12 and the VS Code extended vocabulary. + + The VS Code extended vocabulary enables you to define custom keywords that enhance how VS + Code interprets schemas to provide better hover documentation, IntelliSense, validation + errors, and more. + + vocabulary: + title: + en-US: VS Code extended vocabulary + description: + en-US: >- + Defines custom keywords for enhancing the authoring and editing experience for data files + in VS Code. + markdownDescription: + en-US: |- + Defines custom keywords for enhancing the authoring and editing experience for data files + in VS Code. + + These custom keywords don't affect the validation of data. Instead, they provide + annotations that VS Code interprets to provide better hover documentation, IntelliSense, + validation errors, and more. + + keywords: + allowComments: + title: + en-US: Allow comments + description: + en-US: >- + Indicates whether VS Code should allow comments in the JSON file, even when the file + extension isn't '.jsonc'. + markdownDescription: + en-US: |- + Indicates whether VS Code should allow comments in the JSON file, even when the file + extension isn't `.jsonc`. + + By default, JSON comments in `.json` files cause parsing errors. If you define a JSON + Schema with `allowComments` set to `true`, VS Code doesn't raise validation errors for + comments in JSON for that schema. + + allowTrailingCommas: + title: + en-US: Allow trailing commas + description: + en-US: >- + Indicates whether VS Code should allow trailing commas in the JSON file. + markdownDescription: + en-US: |- + Indicates whether VS Code should allow trailing commas in the JSON file. + + By default, a comma after the last item in an array or last key-value pair in an object + causes a parsing error. If you define a JSON Schema with `allowTrailingCommas` set to + `true`, VS Code doesn't raise validation errors for commas after the last item in arrays + or last key-value pair in objects for that Schema. + + completionDetail: + title: + en-US: Completion detail + description: + en-US: >- + Defines additional information for IntelliSense when completing a proposed item, replacing + the `title` keyword as code-formatted text. + markdownDescription: + en-US: |- + Defines additional information for IntelliSense when completing a proposed item, replacing + the `title` keyword as code-formatted text. + + By default, when a user completes a value for a schema or subschema, VS Code displays + additional information in hover text. If the schema defines the `title` keyword, the + hover text includes the title string as the first line of the hover text. + + If you define the `completionDetail` keyword, VS Code displays the string as monospace + code-formatted text instead of the `title` keyword's value. + + If the schema defines the `description` or `markdownDescription` keywords, that text is + displayed in the hover text after the value from the `completionDetail` or `title` + keyword. + + defaultSnippets: + title: + en-US: Default Snippets + description: + en-US: >- + Provides snippets for completion of a schema or subschema value or property. + markdownDescription: + en-US: |- + Provides snippets for completion of a schema or subschema value or property. + + By default, VS Code presents a set of completion options for data with an associated JSON + Schema like suggesting defined property names or enum values. You can use the + `defaultSnippets` keyword to provide an array of snippets with more control over the + presentation, default values, and enable users to quickly fill out the snippet. + + The keyword expects an array of objects that each define a snippet. For more information + about defining snippets, see [Define snippets in JSON Schemas][01]. For more information + about the snippet syntax, see [Snippet syntax][02]. + + [01]: https://code.visualstudio.com/Docs/languages/json#_define-snippets-in-json-schemas + [02]: https://code.visualstudio.com/docs/editing/userdefinedsnippets#_snippet-syntax + + items: + title: + en-US: Snippet definition + description: + en-US: >- + Each item defines a snippet for users that VS Code will surface as part of IntelliSense. + markdownDescription: + en-US: |- + Each item defines a snippet for users that VS Code will surface as part of IntelliSense. + + Every snippet must define either the `body` or `bodyText` property, which VS Code uses + to insert the snippet into the data file. If you specify both `body` and `bodyText`, the + value for `body` supercedes the value for `bodyText`. + + The `description`, and `markdownDescription` properties provide documentation for the + snippet and are displayed in the hover text when a user selects the snippet. If you + specify both `description` and `markdownDescription`, the text for + `markdownDescription` supercedes the text for `description`. + + The `label` property defines a short name for the snippet. If the snippet doesn't define + the `label` property, VS Code shows a stringified representation of the snippet instead. + + Snippets are presented to the user in alphabetical order by the value of their `label` + property (or the stringified representation of the snippet if it has no label). + + properties: + label: + title: + en-US: Snippet label + description: + en-US: >- + Defines a short name for the snippet. + markdownDescription: + en-US: |- + Defines a short name for the snippet instead of using the stringified representation + of the snippet's value. The `label` property also affects the order that VS Code + presents the snippets. VS Code sorts the snippets for completion alphabetically by + label. + description: + title: + en-US: Snippet description + description: + en-US: >- + Defines plain text documentation for the snippet displayed in the completion dialog. + markdownDescription: + en-US: |- + Defines plain text documentation for the snippet displayed in the completion dialog. + + When the snippet doesn't define the `description` or `markdownDescription` property, + the snippet provides no additional context to the user aside from the label until + they select the snippet for completion. Use the `description` property to provide + information to the user about the snippet. If you need to provide rich formatting, + like links or text formatting, use the `markdownDescription` property. + + If you define both the `description` and `markdownDescription` proeprty for a + snippet, the `markdownDescription` text overrides the `description` text. + markdownDescription: + title: + en-US: Snippet markdown description + description: + en-US: >- + Defines formatted documentation for the snippet displayed in the completion dialog. + markdownDescription: + en-US: |- + Defines formatted documentation for the snippet displayed in the completion dialog. + + When the snippet doesn't define the `description` or `markdownDescription` property, + the snippet provides no additional context to the user aside from the label until + they select the snippet for completion. Use the `description` property to provide + information to the user about the snippet. If you need to provide rich formatting, + like links or text formatting, use the `markdownDescription` property. + + If you define both the `description` and `markdownDescription` proeprty for a + snippet, the `markdownDescription` text overrides the `description` text. + body: + title: + en-US: Snippet body + description: + en-US: >- + Defines the data to insert for the snippet. The data can be any type. + markdownDescription: + en-US: |- + Defines the data to insert for the snippet. The data can be any type. When the user + selects the snippet, VS Code inserts the data at the cursor. In string literals for + the `body` you can use [snippet syntax][01] to define tabstops, placeholders, and + variables. + + Alternatively, you can define the `bodyText` property for the snippet, which + specifies the text to insert for the snippet as a string. + + If you define both the `bodyText` and `body` properties for a snippet, the `body` + definition overrides the `bodyText` property. + + [01]: https://code.visualstudio.com/docs/editing/userdefinedsnippets#_snippet-syntax + bodyText: + title: + en-US: Snippet body text + description: + en-US: >- + Defines the data to insert for the snippet as a string literal. + markdownDescription: + en-US: |- + Defines the data to insert for the snippet as a string literal. When the user + selects the snippet, VS Code inserts the text _without_ the enclosing quotation + marks at the cursor. You can use [snippet syntax][01] to define tabstops, + placeholders, and variables in the `bodyText`. + + Alternatively, you can define the `body` property for the snippet, which specifies + the text to insert for the snippet as data. + + If you define both the `bodyText` and `body` properties for a snippet, the `body` + definition overrides the `bodyText` property. + + [01]: https://code.visualstudio.com/docs/editing/userdefinedsnippets#_snippet-syntax + + deprecationMessage: + title: + en-US: Deprecation message + description: + en-US: >- + Defines a message to surface as a warning to users when they specify a deprecated property + in their data. + markdownDescription: + en-US: |- + Defines a message to surface as a warning to users when they specify a deprecated property + in their data. + + This keyword only has an affect when defined in a schema or subschema that also defines + the `deprecated` keyword as `true`. When you define the `deprecationMessage` keyword for + a deprecated schema or subschema, VS Code displays the provided message instead of the + default warning about deprecation. + + doNotSuggest: + title: + en-US: Do not suggest + description: + en-US: >- + Indicates whether VS Code should avoid suggesting the property for IntelliSense. + markdownDescription: + en-US: |- + Indicates whether VS Code should avoid suggesting the property for IntelliSense. + + By default, VS Code will show any defined property in the `properties` keyword as a + completion option with IntelliSense. You can define the `doNotSuggest` keyword in a + property subschema as `true` to indicate that VS Code should not show that property for + IntelliSense. + + enumDescriptions: + title: + en-US: Enum descriptions + description: + en-US: >- + Defines per-value descriptions for schemas that use the enum keyword. + markdownDescription: + en-US: |- + Defines per-value descriptions for schemas that use the `enum` keyword. + + The builtin keywords for JSON Schema includes the `description` keyword, which you can use + to document a given schema or subschema. However, for schemas that use the `enum` keyword + to define an array of valid values, JSON Schema provides no keyword for documenting each + value. + + With the `enumDescriptions` keyword from the VS Code vocabulary, you can document each + item in the `enum` keyword array. VS Code interprets each item in `enumDescriptions` as + documenting the item at the same index in the `enum` keyword. + + This documentation is surfaced in VS Code on hover for an enum value and for IntelliSense + when completing an enum value. + + If you want to use Markdown syntax for the annotation, specify the + `markdownEnumDescriptions` keyword instead. + + enumDetails: + title: + en-US: Enum details + description: + en-US: >- + Defines additional information for IntelliSense when completing a proposed enum value, + shown before the description. + markdownDescription: + en-US: |- + Defines additional information for IntelliSense when completing a proposed enum value, + shown before the description. + + By default, when VS Code suggests a completion for an item defined in the `enum` keyword, + VS Code displays hover text with a description. If the schema defined the `description`, + `enumDescriptions`, or `markdownEnumDescriptions` keywords, VS Code displays that text. + The `markdownEnumDescriptions` keyword overrides the `enumDescriptions` keyword, which + overrides the `description` keyword. + + When you define the `enumDetails` keyword, VS Code displays the string for that enum + value as monospace code-formatted text. The keyword expects an array of strings. VS Code + correlates the items in the `enumDetails` keyword to the items in the `enum` keyword by + their index. The first item in `enumDetails` maps to the first item in `enum` and so on. + + enumSortText: + title: + en-US: Enum sort text + description: + en-US: >- + Defines a alternate strings to use when sorting a suggestion for enum values. + markdownDescription: + en-US: |- + Defines a alternate strings to use when sorting a suggestion for enum values. + + By default, suggestions are sorted alphabetically, not in the order that you define + items in the `enum` keyword array. You can use the `enumSortText` keyword to override + the order the values are displayed, providing a different string for each value. + + The keyword expects an array of strings. VS Code correlates the items in the + `enumSortText` keyword to the items in the `enum` keyword by their index. The first item + in `enumSortText` maps to the first item in `enum` and so on. + + For example, in the following schema, VS Code will suggest the `baz`, then `bar`, then + `foo` values: + + ```json + { + "type": "string", + "enum": ["foo", "bar", "baz"], + "enumSortText": ["c", "b", "a"] + } + ``` + + errorMessage: + title: + en-US: Validation error message + description: + en-US: >- + Defines a friendly error message to raise when a schema or subschema fails validation. + markdownDescription: + en-US: |- + Defines a friendly error message to raise when a schema or subschema fails validation. + + By default, VS Code surfaces a default error message for data that fails schema + validation, like specifying an invalid type. You can use the `errorMessage` keyword to + define a custom message to raise in the editor when the data fails validation for the + following cases: + + - When the data is an invalid type as validated by the `type` keyword. + - When the subschema defined for the `not` keyword is valid. + - When the data is invalid for the defined values in the `enum` keyword. + - When the data is invalid for the defined value in the `const` keyword. + - When a string doesn't match the regular expression defined in the `pattern` keyword. + This message is overridden by the `patternErrorMessage` keyword if it's defined. + - When a string value doesn't match a required format. + - When the data is for an array that is validated by the `minContains` or `maxContains` + keywords and fails those validations. + - When the data includes a property that was defined in the `properties` keyword as + `false`, forbidding the property. + - When the data includes a property that was defined in the `patternProperties` keyword as + `false`, forbidding matching property names. + - When the data includes a property that wasn't defined in the `properties` or + `patternProperties` keyword and the schema defines `additionalProperties` as `false`. + - When the data includes a property that isn't evaluated by any keywords and the schema + defines `unevaluatedProperties` as `false`. + + The value for the `errorMessage` keyword supercedes all default messages for the schema + or subschema where you define the keyword. You can provide per-validation failure + messages by defining the validating keywords in separate entries of the `allOf` keyword + and defining the `errorMessage` keyword for each entry. + + markdownDescription: + title: + en-US: Markdown description + description: + en-US: >- + Defines documentation for the schema or subschema displayed as hover text in VS Code. + markdownDescription: + en-US: |- + Defines documentation for the schema or subschema displayed as hover text in VS Code. + + By default, VS Code displays the text defined in the `description` keyword in the hover + text for properties and values. VS Code interprets the `description` keyword literally, + without converting any apparent markup. + + You can define the `markdownDescription` keyword to provide descriptive text as markdown, + including links and code blocks. When a schema or subschema defines the + `markdownDescription` keyword, that value supercedes any defined text in the `description` + keyword. + + You can also use the `markdownEnumDescriptions` keyword to document the values defined + for the `enum` keyword. + + For more information, see [Use rich formatting in hovers][01]. + + [01]: https://code.visualstudio.com/Docs/languages/json#_use-rich-formatting-in-hovers + + markdownEnumDescriptions: + title: + en-US: Markdown enum descriptions + description: + en-US: >- + Defines documentation for enum values displayed as hover text in VS Code. + markdownDescription: + en-US: |- + Defines documentation for enum values displayed as hover text in VS Code. + + By default, when a user hovers on or selects completion for a value that is validated by + the `enum` keyword, VS Code displays the text from the `description` or + `markdownDescription` keywords for the schema or subschema. You can use the + `markdownEnumDescriptions` keyword to define documentation for each enum value. + + When a schema or subschema defines the `markdownEnumDescriptions` keyword, that value + supercedes any defined text in the `description`, `markdownDescription`, or + `enumDescriptions` keywords. + + The keyword expects an array of strings. VS Code correlates the items in the + `markdownEnumDescriptions` keyword to the items in the `enum` keyword by their index. The + first item in `markdownEnumDescriptions` maps to the first item in `enum` and so on. + + patternErrorMessage: + title: + en-US: Pattern validation error message + description: + en-US: >- + Defines a friendly error message to raise when a schema or subschema fails validation + for the `pattern` keyword. + markdownDescription: + en-US: |- + Defines a friendly error message to raise when a schema or subschema fails validation + for the `pattern` keyword. + + By default, when a value fails validation for the `pattern` keyword, VS Code raises an + error that informs the user that the value is invalid for the given regular expression, + which it displays in the message. + + Reading and parsing regular expressions can be difficult even for experienced users. You + can define the `patternErrorMessage` keyword to provide better information to the user + about the expected pattern for the string value. + + suggestSortText: + title: + en-US: Suggest sort text + description: + en-US: >- + Defines an alternate string to use when sorting a suggestion. + markdownDescription: + en-US: |- + Defines an alternate string to use when sorting a suggestion. + + By default, suggestions are displayed in alphabetical order. You can define the + `suggestSortText` keyword to change how the suggestions are sorted. For example, in + the following schema, VS Code will suggest the `baz`, then `bar`, then `foo` properties: + + ```json + { + "type": "object", + "properties": { + "foo": { + "suggestSortText": "c", + } + "bar": { + "suggestSortText": "b", + } + "baz": { + "suggestSortText": "a", + } + } + } + ``` diff --git a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs index 628141e8a..d639ee711 100644 --- a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs +++ b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs @@ -11,7 +11,8 @@ //! The rest of the utility methods work with specific keywords, like `$id` and `$defs`. use core::{clone::Clone, convert::TryInto, iter::Iterator, option::Option::None}; -use std::string::String; +use std::{collections::HashSet, string::String}; +use std::vec::Vec; use schemars::Schema; use serde_json::{Map, Number, Value}; @@ -327,7 +328,6 @@ pub trait SchemaUtilityExtensions { /// /// ```rust /// use schemars::json_schema; - /// use serde_json::json; /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; /// /// let ref schema = json_schema!({ @@ -340,7 +340,7 @@ pub trait SchemaUtilityExtensions { /// ); /// /// assert_eq!( - /// schema.get_keyword_as_object("enum"), + /// schema.get_keyword_as_object("properties"), /// None /// ) /// ``` @@ -679,6 +679,73 @@ pub trait SchemaUtilityExtensions { fn get_keyword_as_u64(&self, key: &str) -> Option; //************************ $id keyword functions *************************// + /// Retrieves the `$id` values for any entries in the `$defs` keyword. + /// + /// A compound schema resource document, also called a "bundled schema", includes referenced + /// schema resources in the `$defs` keyword. A schema resource always defines the `$id` keyword. + /// + /// This method retrieves those IDs and returns a hashset containing the unique values. + /// + /// Optionally, you can recursively search for schema resource IDs to handle cases where the + /// a bundled schema resource may itself have bundled resources. + /// + /// If the schema doesn't have any bundled schema resources, this method returns an empty + /// hashset. + /// + /// # Examples + /// + /// This example demonstrates the difference between invoking the method for the top-level + /// only and recursively returning nested bundled schema resources. + /// + /// ```rust + /// use std::collections::HashSet; + /// + /// use schemars::json_schema; + /// use serde_json::json; + /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; + /// + /// let ref schema = json_schema!({ + /// "$id": "https://contoso.com/schemas/example.json", + /// "$defs": { + /// "https://contoso.com/schemas/example/string.json": { + /// "$id": "https://contoso.com/schemas/example/string.json", + /// "type": "string" + /// }, + /// "https://contoso.com/schemas/example/object.json": { + /// "$id": "https://contoso.com/schemas/example/object.json", + /// "type": "object", + /// "unevaluatedProperties": { + /// "$ref": "https://contoso.com/schemas/example/unevaluatedProperties.json" + /// }, + /// "$defs": { + /// "https://contoso.com/schemas/example/unevaluatedProperties.json": { + /// "$id": "https://contoso.com/schemas/example/unevaluatedProperties.json" + /// } + /// } + /// } + /// } + /// }); + /// + /// let non_recursive_result: HashSet<&str> = [ + /// "https://contoso.com/schemas/example/string.json", + /// "https://contoso.com/schemas/example/object.json" + /// ].into(); + /// assert_eq!( + /// schema.get_bundled_schema_resource_ids(false), + /// non_recursive_result + /// ); + /// + /// let recursive_result: HashSet<&str> = [ + /// "https://contoso.com/schemas/example/string.json", + /// "https://contoso.com/schemas/example/object.json", + /// "https://contoso.com/schemas/example/unevaluatedProperties.json" + /// ].into(); + /// assert_eq!( + /// schema.get_bundled_schema_resource_ids(true), + /// recursive_result + /// ); + /// ``` + fn get_bundled_schema_resource_ids(&self, recurse: bool) -> HashSet<&str>; /// Retrieves the value of the `$id` keyword as a [`String`]. /// /// If the schema doesn't have the `$id` keyword, this function returns [`None`]. @@ -1261,6 +1328,137 @@ pub trait SchemaUtilityExtensions { /// ); /// ``` fn get_property_subschema_mut(&mut self, property_name: &str) -> Option<&mut Schema>; + + //************************ $ref keyword functions ************************// + /// Retrieves the value for every `$ref` keyword from the [`Schema`] as a [`HashSet`] of + /// unique values. + /// + /// + /// This method recurses through a given schema for the `$ref` keyword and inserts the value + /// into a hashset to return. If the schema doesn't define any references, this method returns + /// an empty hashset. + /// + /// # Examples + /// + /// This example shows how the method returns a unique set of references by recursively + /// searching the schema for the `$ref` keyword. + /// + /// ```rust + /// use std::collections::HashSet; + /// + /// use schemars::json_schema; + /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; + /// + /// let schema = &json_schema!({ + /// "id": "https://contoso.com/schemas/example/object.json", + /// "type": "object", + /// "properties": { + /// "name": { "$ref": "/schemas/example/properties/name.json" }, + /// "age": { "$ref": "/schemas/example/properties/age.json" }, + /// }, + /// }); + /// + /// let references: HashSet<&str> = [ + /// "/schemas/example/properties/name.json", + /// "/schemas/example/properties/age.json" + /// ].into(); + /// + /// assert_eq!( + /// schema.get_references(), + /// references + /// ) + /// ``` + fn get_references(&self) -> HashSet<&str>; + /// Searches the schema for instances of the `$ref` keyword defined as a + /// given value and replaces each instance with a new value. + /// + /// This method simplifies replacing references programmatically, especially + /// for converting references to use the canonical ID of a bundled schema + /// resource. + /// + /// # Examples + /// + /// This example replaces the reference to `#/$defs/name.json` with the + /// canonical ID URI for the referenced schema resource, which is bundled + /// in the schema. + /// + /// ```rust + /// use schemars::json_schema; + /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; + /// + /// let schema = &mut json_schema!({ + /// "id": "https://contoso.com/schemas/example/object.json", + /// "type": "object", + /// "properties": { + /// "name": { "$ref": "#/$defs/name" }, + /// }, + /// "$defs": { + /// "name": { + /// "$id": "https://contoso.com/schemas/example/properties/name.json", + /// "type": "string" + /// } + /// } + /// }); + /// + /// schema.replace_references( + /// "#/$defs/name", + /// "https://contoso.com/schemas/example/properties/name.json" + /// ); + /// + /// assert_eq!( + /// schema.get_references().into_iter().nth(0).unwrap(), + /// "https://contoso.com/schemas/example/properties/name.json" + /// ); + /// ``` + fn replace_references(&mut self, find_value: &str, new_value: &str); + /// Checks whether a given reference maps to a bundled schema resource. + /// + /// This method takes the value of a `$ref` keyword and searches for a matching entry in the + /// `$defs` keyword. The method returns `true` if the reference resolves to an entry in + /// `$defs` and otherwise false. + /// + /// The reference can be any of the following: + /// + /// - A URI identifier fragment, like `#/$defs/foo` + /// - An absolute URL for the referenced schema, like `https://contoso.com/schemas/example.json` + /// - A site-relative URL for the referenced schema, like `/schemas/example.json`. The function + /// can only resolve site-relative URLs when the schema itself defines `$id` with an absolute + /// URL, because it uses the current schema's `$id` as the base URL. + /// + /// # Examples + /// + /// ```rust + /// use schemars::json_schema; + /// use dsc_lib_jsonschema::schema_utility_extensions::SchemaUtilityExtensions; + /// + /// let schema = &json_schema!({ + /// "$id": "https://contoso.com/schemas/example/object.json", + /// "$defs": { + /// "name": { + /// "$id": "https://contoso.com/schemas/example/properties/name.json", + /// "type": "string" + /// } + /// } + /// }); + /// + /// // Resolving reference as pointer + /// assert_eq!(schema.reference_is_for_bundled_resource("#/$defs/name"), true); + /// // Resolving reference as site-relative URI + /// assert_eq!( + /// schema.reference_is_for_bundled_resource("/schemas/example/properties/name.json"), + /// true + /// ); + /// // Resolving reference as absolute URI + /// assert_eq!( + /// schema.reference_is_for_bundled_resource( + /// "https://contoso.com/schemas/example/properties/name.json" + /// ), + /// true + /// ); + /// // Returns false for unresolvable definition + /// assert_eq!(schema.reference_is_for_bundled_resource("#/$defs/invalid"), false); + /// ``` + fn reference_is_for_bundled_resource(&self, reference: &str) -> bool; } impl SchemaUtilityExtensions for Schema { @@ -1443,6 +1641,32 @@ impl SchemaUtilityExtensions for Schema { } }).collect(); } + fn get_bundled_schema_resource_ids(&self, recurse: bool) -> HashSet<&str> { + let mut schema_resource_ids: HashSet<&str> = HashSet::new(); + + let Some(defs) = self.get_defs() else { + return schema_resource_ids + }; + + for (_key, value) in defs { + let Ok(def) = <&Value as TryInto<&Schema>>::try_into(value) else { + continue; + }; + if let Some(id) = def.get_id() { + schema_resource_ids.insert(id); + } + if recurse { + let recursive= def.get_bundled_schema_resource_ids(recurse); + + schema_resource_ids = schema_resource_ids + .union(&recursive) + .copied() + .collect(); + } + } + + schema_resource_ids + } fn get_id(&self) -> Option<&str> { self.get_keyword_as_str("$id") } @@ -1484,4 +1708,124 @@ impl SchemaUtilityExtensions for Schema { .and_then(|properties| properties.get_mut(property_name)) .and_then(|v| <&mut Value as TryInto<&mut Schema>>::try_into(v).ok()) } + fn get_references(&self) -> HashSet<&str> { + let mut references: HashSet<&str> = HashSet::new(); + // First, check the top-level for a reference + if let Some(reference) = self.get_keyword_as_str("$ref") { + references.insert(reference); + } + + // Next, recursively check references in subschemas + for (key, value) in self.as_object().into_iter().flatten() { + // Recursing implementation borrowed from schemars::transform::transform_subschemas - needed + // to recursively transform the schema while passing parameters other than the schema + // itself, which is the only supported option for a function implementing the + // Transform trait. + match key.as_str() { + "not" + | "if" + | "then" + | "else" + | "contains" + | "additionalProperties" + | "propertyNames" + | "additionalItems" => { + if let Ok(subschema) = <&Value as TryInto<&Schema>>::try_into(value) { + references.extend(subschema.get_references()); + } + } + "allOf" | "anyOf" | "oneOf" | "prefixItems" => { + if let Some(array) = value.as_array() { + for value in array { + if let Ok(subschema) = <&Value as TryInto<&Schema>>::try_into(value) { + references.extend(subschema.get_references()); + } + } + } + } + "items" => { + if let Some(array) = value.as_array() { + for value in array { + if let Ok(subschema) = <&Value as TryInto<&Schema>>::try_into(value) { + references.extend(subschema.get_references()); + } + } + } else if let Ok(subschema) = <&Value as TryInto<&Schema>>::try_into(value) { + references.extend(subschema.get_references()); + } + } + "properties" | "patternProperties" | "$defs" | "definitions" => { + if let Some(obj) = value.as_object() { + for value in obj.values() { + if let Ok(subschema) = <&Value as TryInto<&Schema>>::try_into(value) { + references.extend(subschema.get_references()); + } + } + } + } + _ => {} + } + } + + references + } + fn replace_references(&mut self, find_value: &str, new_value: &str) { + if self.get_keyword_as_str("$ref").is_some_and(|r| r == find_value) { + self.insert("$ref".to_string(), Value::String(new_value.to_string())); + } + + for (key, value) in self.as_object_mut().into_iter().flatten() { + // Recursing implementation borrowed from schemars::transform::transform_subschemas - needed + // to recursively transform the schema while passing parameters other than the schema + // itself, which is the only supported option for a function implementing the + // Transform trait. + match key.as_str() { + "not" + | "if" + | "then" + | "else" + | "contains" + | "additionalProperties" + | "propertyNames" + | "additionalItems" => { + if let Ok(subschema) = <&mut Value as TryInto<&mut Schema>>::try_into(value) { + subschema.replace_references(find_value, new_value); + } + } + "allOf" | "anyOf" | "oneOf" | "prefixItems" => { + if let Some(array) = value.as_array_mut() { + for value in array { + if let Ok(subschema) = <&mut Value as TryInto<&mut Schema>>::try_into(value) { + subschema.replace_references(find_value, new_value); + } + } + } + } + "items" => { + if let Some(array) = value.as_array_mut() { + for value in array { + if let Ok(subschema) = <&mut Value as TryInto<&mut Schema>>::try_into(value) { + subschema.replace_references(find_value, new_value); + } + } + } else if let Ok(subschema) = <&mut Value as TryInto<&mut Schema>>::try_into(value) { + subschema.replace_references(find_value, new_value); + } + } + "properties" | "patternProperties" | "$defs" | "definitions" => { + if let Some(obj) = value.as_object_mut() { + for value in obj.values_mut() { + if let Ok(subschema) = <&mut Value as TryInto<&mut Schema>>::try_into(value) { + subschema.replace_references(find_value, new_value); + } + } + } + } + _ => {} + } + } + } + fn reference_is_for_bundled_resource(&self, reference: &str) -> bool { + self.get_defs_subschema_from_reference(reference).is_some() + } } diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/dialect.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/dialect.rs new file mode 100644 index 000000000..c90a9a588 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/dialect.rs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[allow(non_snake_case)] +#[cfg(test)] mod VSCODE_DIALECT_SCHEMA_BUNDLED { + use crate::vscode::dialect::VSCODE_DIALECT_SCHEMA_BUNDLED; + + #[test] fn meta_schema_is_valid() { + let schema = VSCODE_DIALECT_SCHEMA_BUNDLED.clone(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); + } +} + +#[allow(non_snake_case)] +#[cfg(test)] mod VSCODE_DIALECT_SCHEMA_CANONICAL { + use crate::vscode::dialect::VSCODE_DIALECT_SCHEMA_CANONICAL; + + #[test] fn meta_schema_is_valid() { + let schema = VSCODE_DIALECT_SCHEMA_CANONICAL.clone(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); + } +} + +#[allow(non_snake_case)] +#[cfg(test)] mod VSCodeDialect { + #[cfg(test)] mod json_schema_bundled { + use schemars::{SchemaGenerator, generate::SchemaSettings}; + + use crate::vscode::{dialect::VSCodeDialect, vocabulary::VSCodeVocabulary}; + + #[test] fn returns_schema_with_bundled_definitions() { + let schema = VSCodeDialect::json_schema_bundled( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + let has_defs = schema.get("$defs") + .and_then(serde_json::Value::as_object) + .is_some_and(|defs| defs.keys().len() > 0); + + assert!(has_defs); + } + #[test] fn includes_vscode_vocabulary() { + let schema = VSCodeDialect::json_schema_bundled( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + let vocab = schema.get("$vocabulary") + .and_then(serde_json::Value::as_object).unwrap(); + + assert_eq!(vocab.get(VSCodeVocabulary::SPEC_URI).unwrap(), false) + } + } + #[cfg(test)] mod json_schema_canonical { + use pretty_assertions::assert_eq; + use schemars::{SchemaGenerator, generate::SchemaSettings}; + + use crate::vscode::{dialect::VSCodeDialect, vocabulary::VSCodeVocabulary}; + + #[test] fn returns_schema_without_bundled_definitions() { + let schema = VSCodeDialect::json_schema_canonical( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + + assert_eq!(schema.get("$defs"), None); + } + #[test] fn includes_vscode_vocabulary() { + let schema = VSCodeDialect::json_schema_canonical( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + let vocab = schema.get("$vocabulary") + .and_then(serde_json::Value::as_object).unwrap(); + + assert_eq!(vocab.get(VSCodeVocabulary::SPEC_URI).unwrap(), false) + } + } + #[cfg(test)] mod schema_resource_bundled { + use schemars::{SchemaGenerator, generate::SchemaSettings}; + + use crate::vscode::dialect::VSCodeDialect; + + #[test] fn does_not_panic() { + VSCodeDialect::schema_resource_bundled( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + } + } + #[cfg(test)] mod schema_resource_canonical { + use schemars::{SchemaGenerator, generate::SchemaSettings}; + + use crate::vscode::dialect::VSCodeDialect; + + #[test] fn does_not_panic() { + VSCodeDialect::schema_resource_canonical( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + } + } +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/allow_comments.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/allow_comments.rs new file mode 100644 index 000000000..7fb8fb0cd --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/allow_comments.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{AllowCommentsKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = AllowCommentsKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_boolean_value_is_invalid() { + let validation_error = keyword_validator!(AllowCommentsKeyword, &json!({ + "allowComments": "yes" + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/allowComments" + ); + + assert_eq!( + format!("{validation_error}"), + t!("vscode.keywords.allow_comments.factory_error_invalid_type") + ); +} + +#[test] fn boolean_value_is_valid() { + let validator = keyword_validator!(AllowCommentsKeyword, &json!({ + "allowComments": true + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/allow_trailing_commas.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/allow_trailing_commas.rs new file mode 100644 index 000000000..51bf89031 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/allow_trailing_commas.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{AllowTrailingCommasKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = AllowTrailingCommasKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_boolean_value_is_invalid() { + let validation_error = keyword_validator!(AllowTrailingCommasKeyword, &json!({ + "allowTrailingCommas": "yes" + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/allowTrailingCommas" + ); + + assert_eq!( + format!("{validation_error}"), + t!("vscode.keywords.allow_trailing_commas.factory_error_invalid_type") + ); +} + +#[test] fn boolean_value_is_valid() { + let validator = keyword_validator!(AllowTrailingCommasKeyword, &json!({ + "allowTrailingCommas": true + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/completion_detail.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/completion_detail.rs new file mode 100644 index 000000000..c27ad6ae4 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/completion_detail.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{CompletionDetailKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = CompletionDetailKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_string_value_is_invalid() { + let validation_error = keyword_validator!(CompletionDetailKeyword, &json!({ + "completionDetail": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/completionDetail" + ); + + assert_eq!( + format!("{validation_error}"), + t!("vscode.keywords.completion_detail.factory_error_invalid_type") + ); +} + +#[test] fn string_value_is_valid() { + let validator = keyword_validator!(CompletionDetailKeyword, &json!({ + "completionDetail": "string value" + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/default_snippets.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/default_snippets.rs new file mode 100644 index 000000000..a557c815f --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/default_snippets.rs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{DefaultSnippetsKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = DefaultSnippetsKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_array_value_is_invalid() { + let validation_error = keyword_validator!(DefaultSnippetsKeyword, &json!({ + "defaultSnippets": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/defaultSnippets" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.default_snippets.factory_error_not_array"), + t!("vscode.keywords.default_snippets.factory_error_suffix"), + ), + ); +} + +#[test] fn array_with_non_object_is_invalid() { + let validation_error = keyword_validator!(DefaultSnippetsKeyword, &json!({ + "defaultSnippets": [{"label": "valid"}, "invalid"] + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/defaultSnippets" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.default_snippets.factory_error_non_object_item"), + t!("vscode.keywords.default_snippets.factory_error_suffix"), + ), + ); +} +#[test] fn array_with_non_snippet_object_is_invalid() { + let validation_error = keyword_validator!(DefaultSnippetsKeyword, &json!({ + "defaultSnippets": [{"label": "valid"}, {"invalid": true}] + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/defaultSnippets" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.default_snippets.factory_error_invalid_item"), + t!("vscode.keywords.default_snippets.factory_error_suffix"), + ), + ); +} +#[test] fn array_with_invalid_snippet_object_is_invalid() { + let validation_error = keyword_validator!(DefaultSnippetsKeyword, &json!({ + "defaultSnippets": [{"label": "valid"}, {"label": false}] + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/defaultSnippets" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.default_snippets.factory_error_invalid_item"), + t!("vscode.keywords.default_snippets.factory_error_suffix"), + ), + ); +} + +#[test] fn array_of_valid_snippets_is_valid() { + let validator = keyword_validator!(DefaultSnippetsKeyword, &json!({ + "defaultSnippets": [{"label": "valid"}] + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/deprecation_message.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/deprecation_message.rs new file mode 100644 index 000000000..98def487a --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/deprecation_message.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{DeprecationMessageKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = DeprecationMessageKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_string_value_is_invalid() { + let validation_error = keyword_validator!(DeprecationMessageKeyword, &json!({ + "deprecationMessage": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/deprecationMessage" + ); + + assert_eq!( + format!("{validation_error}"), + t!("vscode.keywords.deprecation_message.factory_error_invalid_type") + ); +} + +#[test] fn string_value_is_valid() { + let validator = keyword_validator!(DeprecationMessageKeyword, &json!({ + "deprecationMessage": "string value" + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/do_not_suggest.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/do_not_suggest.rs new file mode 100644 index 000000000..84f94aa62 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/do_not_suggest.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{DoNotSuggestKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = DoNotSuggestKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_boolean_value_is_invalid() { + let validation_error = keyword_validator!(DoNotSuggestKeyword, &json!({ + "doNotSuggest": "yes" + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/doNotSuggest" + ); + + assert_eq!( + format!("{validation_error}"), + t!("vscode.keywords.do_not_suggest.factory_error_invalid_type") + ); +} + +#[test] fn boolean_value_is_valid() { + let validator = keyword_validator!(DoNotSuggestKeyword, &json!({ + "doNotSuggest": true + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_descriptions.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_descriptions.rs new file mode 100644 index 000000000..629d92e9d --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_descriptions.rs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{EnumDescriptionsKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = EnumDescriptionsKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_array_value_is_invalid() { + let validation_error = keyword_validator!(EnumDescriptionsKeyword, &json!({ + "enumDescriptions": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/enumDescriptions" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.enum_descriptions.factory_error_not_array"), + t!("vscode.keywords.enum_descriptions.factory_error_suffix") + ) + ); +} +#[test] fn non_string_item_in_array_value_is_invalid() { + let validation_error = keyword_validator!(EnumDescriptionsKeyword, &json!({ + "enumDescriptions": [ + "a", + 1, + "c" + ] + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/enumDescriptions" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.enum_descriptions.factory_error_non_string_item"), + t!("vscode.keywords.enum_descriptions.factory_error_suffix") + ) + ); +} + +#[test] fn string_array_value_is_valid() { + let validator = keyword_validator!(EnumDescriptionsKeyword, &json!({ + "enumDescriptions": [ + "a", + "b", + "c" + ] + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_details.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_details.rs new file mode 100644 index 000000000..9673bc4bc --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_details.rs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{EnumDetailsKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = EnumDetailsKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_array_value_is_invalid() { + let validation_error = keyword_validator!(EnumDetailsKeyword, &json!({ + "enumDetails": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/enumDetails" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.enum_details.factory_error_not_array"), + t!("vscode.keywords.enum_details.factory_error_suffix") + ) + ); +} +#[test] fn non_string_item_in_array_value_is_invalid() { + let validation_error = keyword_validator!(EnumDetailsKeyword, &json!({ + "enumDetails": [ + "a", + 1, + "c" + ] + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/enumDetails" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.enum_details.factory_error_non_string_item"), + t!("vscode.keywords.enum_details.factory_error_suffix") + ) + ); +} + +#[test] fn string_array_value_is_valid() { + let validator = keyword_validator!(EnumDetailsKeyword, &json!({ + "enumDetails": [ + "a", + "b", + "c" + ] + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_sort_texts.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_sort_texts.rs new file mode 100644 index 000000000..ed9b737c8 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/enum_sort_texts.rs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{EnumSortTextsKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = EnumSortTextsKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_array_value_is_invalid() { + let validation_error = keyword_validator!(EnumSortTextsKeyword, &json!({ + "enumSortTexts": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/enumSortTexts" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.enum_sort_texts.factory_error_not_array"), + t!("vscode.keywords.enum_sort_texts.factory_error_suffix") + ) + ); +} +#[test] fn non_string_item_in_array_value_is_invalid() { + let validation_error = keyword_validator!(EnumSortTextsKeyword, &json!({ + "enumSortTexts": [ + "a", + 1, + "c" + ] + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/enumSortTexts" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.enum_sort_texts.factory_error_non_string_item"), + t!("vscode.keywords.enum_sort_texts.factory_error_suffix") + ) + ); +} + +#[test] fn string_array_value_is_valid() { + let validator = keyword_validator!(EnumSortTextsKeyword, &json!({ + "enumSortTexts": [ + "a", + "b", + "c" + ] + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/error_message.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/error_message.rs new file mode 100644 index 000000000..33cdc7db9 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/error_message.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{ErrorMessageKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = ErrorMessageKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_string_value_is_invalid() { + let validation_error = keyword_validator!(ErrorMessageKeyword, &json!({ + "errorMessage": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/errorMessage" + ); + + assert_eq!( + format!("{validation_error}"), + t!("vscode.keywords.error_message.factory_error_invalid_type") + ); +} + +#[test] fn string_value_is_valid() { + let validator = keyword_validator!(ErrorMessageKeyword, &json!({ + "errorMessage": "string value" + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/markdown_description.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/markdown_description.rs new file mode 100644 index 000000000..75d43a121 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/markdown_description.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{MarkdownDescriptionKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = MarkdownDescriptionKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_string_value_is_invalid() { + let validation_error = keyword_validator!(MarkdownDescriptionKeyword, &json!({ + "markdownDescription": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/markdownDescription" + ); + + assert_eq!( + format!("{validation_error}"), + t!("vscode.keywords.markdown_description.factory_error_invalid_type") + ); +} + +#[test] fn string_value_is_valid() { + let validator = keyword_validator!(MarkdownDescriptionKeyword, &json!({ + "markdownDescription": "string value" + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/markdown_enum_descriptions.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/markdown_enum_descriptions.rs new file mode 100644 index 000000000..35317365a --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/markdown_enum_descriptions.rs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{MarkdownEnumDescriptionsKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = MarkdownEnumDescriptionsKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_array_value_is_invalid() { + let validation_error = keyword_validator!(MarkdownEnumDescriptionsKeyword, &json!({ + "markdownEnumDescriptions": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/markdownEnumDescriptions" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.markdown_enum_descriptions.factory_error_not_array"), + t!("vscode.keywords.markdown_enum_descriptions.factory_error_suffix") + ) + ); +} +#[test] fn non_string_item_in_array_value_is_invalid() { + let validation_error = keyword_validator!(MarkdownEnumDescriptionsKeyword, &json!({ + "markdownEnumDescriptions": [ + "a", + 1, + "c" + ] + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/markdownEnumDescriptions" + ); + + assert_eq!( + format!("{validation_error}"), + format!( + "{} {}", + t!("vscode.keywords.markdown_enum_descriptions.factory_error_non_string_item"), + t!("vscode.keywords.markdown_enum_descriptions.factory_error_suffix") + ) + ); +} + +#[test] fn string_array_value_is_valid() { + let validator = keyword_validator!(MarkdownEnumDescriptionsKeyword, &json!({ + "markdownEnumDescriptions": [ + "a", + "b", + "c" + ] + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/mod.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/mod.rs new file mode 100644 index 000000000..9db22d365 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/mod.rs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/// Creates a [`jsonschema::Validator`] with a specific keyword registered for unit testing +/// individual keywords. +/// +/// The first argument must be the keyword type, like [`AllowCommentsKeyword`]. +/// +/// The second argument must be a reference to a JSON value to use as the testing schema. +/// +/// # Examples +/// +/// This example shows how you can pass a schema you know will fail validation to retrieve +/// the validation error. +/// +/// ```rust +/// use serde_json::json; +/// +/// use crate::vscode::AllowCommentsKeyword; +/// +/// let validation_error = keyword_validator!(AllowCommentsKeyword, &json!({ +/// "allowComments": "yes" +/// })).unwrap_err().to_owned(); +/// +/// assert_eq!( +/// validation_error.instance_path.as_str(), +/// "/allowComments" +/// ); +/// ``` +macro_rules! keyword_validator { + ($keyword:ident, $test_value:expr) => { + jsonschema::options().with_keyword( + $keyword::KEYWORD_NAME, + $keyword::keyword_factory + ).build($test_value) + }; +} + +#[cfg(test)] mod allow_comments; +#[cfg(test)] mod allow_trailing_commas; +#[cfg(test)] mod completion_detail; +#[cfg(test)] mod default_snippets; +#[cfg(test)] mod deprecation_message; +#[cfg(test)] mod do_not_suggest; +#[cfg(test)] mod enum_descriptions; +#[cfg(test)] mod enum_details; +#[cfg(test)] mod enum_sort_texts; +#[cfg(test)] mod error_message; +#[cfg(test)] mod markdown_description; +#[cfg(test)] mod markdown_enum_descriptions; +#[cfg(test)] mod pattern_error_message; +#[cfg(test)] mod suggest_sort_text; \ No newline at end of file diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/pattern_error_message.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/pattern_error_message.rs new file mode 100644 index 000000000..deda9597e --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/pattern_error_message.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{PatternErrorMessageKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = PatternErrorMessageKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_string_value_is_invalid() { + let validation_error = keyword_validator!(PatternErrorMessageKeyword, &json!({ + "patternErrorMessage": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/patternErrorMessage" + ); + + assert_eq!( + format!("{validation_error}"), + t!("vscode.keywords.pattern_error_message.factory_error_invalid_type") + ); +} + +#[test] fn string_value_is_valid() { + let validator = keyword_validator!(PatternErrorMessageKeyword, &json!({ + "patternErrorMessage": "string value" + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/suggest_sort_text.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/suggest_sort_text.rs new file mode 100644 index 000000000..4e89e2e27 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/keywords/suggest_sort_text.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use pretty_assertions::assert_eq; +use rust_i18n::t; +use serde_json::json; + +use crate::vscode::keywords::{SuggestSortTextKeyword, VSCodeKeywordDefinition}; + +#[test] fn meta_schema_is_valid() { + let schema = SuggestSortTextKeyword::default_schema(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); +} + +#[test] fn non_string_value_is_invalid() { + let validation_error = keyword_validator!(SuggestSortTextKeyword, &json!({ + "suggestSortText": true + })).unwrap_err().to_owned(); + + assert_eq!( + validation_error.instance_path.as_str(), + "/suggestSortText" + ); + + assert_eq!( + format!("{validation_error}"), + t!("vscode.keywords.suggest_sort_text.factory_error_invalid_type") + ); +} + +#[test] fn string_value_is_valid() { + let validator = keyword_validator!(SuggestSortTextKeyword, &json!({ + "suggestSortText": "string value" + })); + + assert!(validator.is_ok()); +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/mod.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/mod.rs index 084dc4f58..2c98a0afa 100644 --- a/lib/dsc-lib-jsonschema/src/tests/vscode/mod.rs +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/mod.rs @@ -2,3 +2,9 @@ // Licensed under the MIT License. //! Unit tests for [`dsc-lib-jsonschema::vscode`] + +#[cfg(test)] mod keywords; +#[cfg(test)] mod dialect; +#[cfg(test)] mod schema_extensions; +#[cfg(test)] mod validation_options_extensions; +#[cfg(test)] mod vocabulary; diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/schema_extensions.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/schema_extensions.rs new file mode 100644 index 000000000..c322aa28e --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/schema_extensions.rs @@ -0,0 +1,529 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(test)] mod get_completion_detail { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_completion_detail(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "completionDetail": false + }).get_completion_detail(), + None + ) + } + #[test] fn returns_string_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "completionDetail": "valid" + }).get_completion_detail(), + Some("valid") + ) + } +} +#[cfg(test)] mod get_default_snippets { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::{VSCodeSchemaExtensions, keywords::Snippet}; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_default_snippets(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "defaultSnippets": false + }).get_default_snippets(), + None + ) + } + #[test] fn returns_vec_of_snippets_when_keyword_is_valid() { + let snippets = vec![Snippet{ + label: Some("Example".to_string()), + ..Default::default() + }]; + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "defaultSnippets": snippets + }).get_default_snippets(), + Some(snippets) + ) + } +} +#[cfg(test)] mod get_deprecation_message { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_deprecation_message(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "deprecationMessage": false + }).get_deprecation_message(), + None + ) + } + #[test] fn returns_string_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "deprecationMessage": "valid" + }).get_deprecation_message(), + Some("valid") + ) + } +} +#[cfg(test)] mod get_enum_descriptions { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_enum_descriptions(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "enumDescriptions": false + }).get_enum_descriptions(), + None + ) + } + #[test] fn returns_vec_of_strings_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "enumDescriptions": ["valid"] + }).get_enum_descriptions(), + Some(vec!["valid"]) + ) + } +} +#[cfg(test)] mod get_enum_details { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_enum_details(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "enumDetails": false + }).get_enum_details(), + None + ) + } + #[test] fn returns_vec_of_strings_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "enumDetails": ["valid"] + }).get_enum_details(), + Some(vec!["valid"]) + ) + } +} +#[cfg(test)] mod get_enum_sort_texts { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_enum_sort_texts(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "enumSortTexts": false + }).get_enum_sort_texts(), + None + ) + } + #[test] fn returns_vec_of_strings_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "enumSortTexts": ["valid"] + }).get_enum_sort_texts(), + Some(vec!["valid"]) + ) + } +} +#[cfg(test)] mod get_error_message { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_error_message(), + None, + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "errorMessage": false + }).get_error_message(), + None + ) + } + #[test] fn returns_string_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "errorMessage": "valid" + }).get_error_message(), + Some("valid") + ) + } +} +#[cfg(test)] mod get_markdown_description { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_markdown_description(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "markdownDescription": false + }).get_markdown_description(), + None + ) + } + #[test] fn returns_string_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "markdownDescription": "valid" + }).get_markdown_description(), + Some("valid") + ) + } +} +#[cfg(test)] mod get_markdown_enum_descriptions { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_markdown_enum_descriptions(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "markdownEnumDescriptions": false + }).get_markdown_enum_descriptions(), + None + ) + } + #[test] fn returns_vec_of_strings_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "markdownEnumDescriptions": ["valid"] + }).get_markdown_enum_descriptions(), + Some(vec!["valid"]) + ) + } +} +#[cfg(test)] mod get_pattern_error_message { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_pattern_error_message(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "patternErrorMessage": false + }).get_pattern_error_message(), + None + ) + } + #[test] fn returns_string_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "patternErrorMessage": "valid" + }).get_pattern_error_message(), + Some("valid") + ) + } +} +#[cfg(test)] mod get_suggest_sort_text { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_none_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).get_suggest_sort_text(), + None + ) + } + #[test] fn returns_none_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "suggestSortText": false + }).get_suggest_sort_text(), + None + ) + } + #[test] fn returns_string_when_keyword_is_valid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "suggestSortText": "valid" + }).get_suggest_sort_text(), + Some("valid") + ) + } +} +#[cfg(test)] mod should_allow_comments { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_false_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).should_allow_comments(), + false + ) + } + #[test] fn returns_false_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "allowComments": "invalid" + }).should_allow_comments(), + false + ) + } + #[test] fn returns_false_when_keyword_is_false() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "allowComments": false + }).should_allow_comments(), + false + ) + } + #[test] fn returns_true_when_keyword_is_true() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "allowComments": true + }).should_allow_comments(), + true + ) + } +} +#[cfg(test)] mod should_allow_trailing_commas { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_false_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).should_allow_trailing_commas(), + false + ) + } + #[test] fn returns_false_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "allowTrailingCommas": "invalid" + }).should_allow_trailing_commas(), + false + ) + } + #[test] fn returns_false_when_keyword_is_false() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "allowTrailingCommas": false + }).should_allow_trailing_commas(), + false + ) + } + #[test] fn returns_true_when_keyword_is_true() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "allowTrailingCommas": true + }).should_allow_trailing_commas(), + true + ) + } +} +#[cfg(test)] mod should_not_suggest { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::VSCodeSchemaExtensions; + + #[test] fn returns_false_when_keyword_is_missing() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).should_not_suggest(), + false + ) + } + #[test] fn returns_false_when_keyword_is_invalid() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "doNotSuggest": "invalid" + }).should_not_suggest(), + false + ) + } + #[test] fn returns_false_when_keyword_is_false() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "doNotSuggest": false + }).should_not_suggest(), + false + ) + } + #[test] fn returns_true_when_keyword_is_true() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json", + "doNotSuggest": true + }).should_not_suggest(), + true + ) + } +} +#[cfg(test)] mod uses_vscode_dialect { + use pretty_assertions::assert_eq; + use schemars::json_schema; + + use crate::vscode::{VSCodeSchemaExtensions, dialect::VSCodeDialect}; + + #[test] fn returns_false_when_schema_has_no_explicit_dialect() { + assert_eq!( + json_schema!({ + "$id": "https://contoso.com/schemas/test/example.json" + }).uses_vscode_dialect(), + false + ) + } + #[test] fn returns_false_when_schema_is_not_vscode_dialect() { + assert_eq!( + json_schema!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://contoso.com/schemas/test/example.json" + }).uses_vscode_dialect(), + false + ) + } + #[test] fn returns_true_when_schema_is_vscode_dialect() { + assert_eq!( + json_schema!({ + "$schema": VSCodeDialect::SCHEMA_ID, + "$id": "https://contoso.com/schemas/test/example.json" + }).uses_vscode_dialect(), + true + ) + } +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/validation_options_extensions.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/validation_options_extensions.rs new file mode 100644 index 000000000..8378a8c51 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/validation_options_extensions.rs @@ -0,0 +1,889 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(test)] mod with_vscode_keyword { + use jsonschema::Validator; + use pretty_assertions::assert_eq; + use serde_json::json; + + use crate::vscode::{ + VSCodeValidationOptionsExtensions, + keywords::{AllowCommentsKeyword, VSCodeKeyword, VSCodeKeywordDefinition} + }; + + #[test] fn adds_the_keyword_and_schema() { + let validator = Validator::options() + .with_vscode_keyword(VSCodeKeyword::AllowComments) + .build(&json!({ + "$ref": AllowCommentsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } +} +#[cfg(test)] mod with_vscode_keywords { + use jsonschema::Validator; + use pretty_assertions::assert_eq; + use serde_json::json; + + use crate::vscode::{ + VSCodeValidationOptionsExtensions, + keywords::{ + VSCodeKeywordDefinition, + AllowCommentsKeyword, + AllowTrailingCommasKeyword, + CompletionDetailKeyword, + DefaultSnippetsKeyword, + DeprecationMessageKeyword, + DoNotSuggestKeyword, + EnumDescriptionsKeyword, + EnumDetailsKeyword, + EnumSortTextsKeyword, + ErrorMessageKeyword, + MarkdownDescriptionKeyword, + MarkdownEnumDescriptionsKeyword, + PatternErrorMessageKeyword, + SuggestSortTextKeyword + } + }; + + #[test] fn adds_the_allow_comments_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": AllowCommentsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_allow_trailing_commas_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": AllowTrailingCommasKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_completion_detail_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": CompletionDetailKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_default_snippets_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": DefaultSnippetsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!([{"label": "valid"}]); + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_deprecation_message_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": DeprecationMessageKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_do_not_suggest_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": DoNotSuggestKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_descriptions_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": EnumDescriptionsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_details_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": EnumDetailsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_sort_texts_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": EnumSortTextsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_error_message_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": ErrorMessageKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_markdown_description_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": MarkdownDescriptionKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_markdown_enum_descriptions_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": MarkdownEnumDescriptionsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_pattern_error_message_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": PatternErrorMessageKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_suggest_sort_text_keyword() { + let validator = Validator::options() + .with_vscode_keywords() + .build(&json!({ + "$ref": SuggestSortTextKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } +} + +#[cfg(test)] mod with_vscode_completion_keywords { + use jsonschema::Validator; + use pretty_assertions::assert_eq; + use serde_json::json; + + use crate::vscode::{ + VSCodeValidationOptionsExtensions, + keywords::{ + CompletionDetailKeyword, + DefaultSnippetsKeyword, + DoNotSuggestKeyword, + EnumDetailsKeyword, + EnumSortTextsKeyword, + SuggestSortTextKeyword, + VSCodeKeywordDefinition + } + }; + + #[test] fn adds_the_completion_detail_keyword() { + let validator = Validator::options() + .with_vscode_completion_keywords() + .build(&json!({ + "$ref": CompletionDetailKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_default_snippets_keyword() { + let validator = Validator::options() + .with_vscode_completion_keywords() + .build(&json!({ + "$ref": DefaultSnippetsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!([{"label": "valid"}]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_do_not_suggest_keyword() { + let validator = Validator::options() + .with_vscode_completion_keywords() + .build(&json!({ + "$ref": DoNotSuggestKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_details_keyword() { + let validator = Validator::options() + .with_vscode_completion_keywords() + .build(&json!({ + "$ref": EnumDetailsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_sort_texts_keyword() { + let validator = Validator::options() + .with_vscode_completion_keywords() + .build(&json!({ + "$ref": EnumSortTextsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_suggest_sort_text_keyword() { + let validator = Validator::options() + .with_vscode_completion_keywords() + .build(&json!({ + "$ref": SuggestSortTextKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } +} + +#[cfg(test)] mod with_vscode_documentation_keywords { + use jsonschema::Validator; + use pretty_assertions::assert_eq; + use serde_json::json; + + use crate::vscode::{ + VSCodeValidationOptionsExtensions, + keywords::{ + DeprecationMessageKeyword, + EnumDescriptionsKeyword, + MarkdownDescriptionKeyword, + MarkdownEnumDescriptionsKeyword, + VSCodeKeywordDefinition + } + }; + + #[test] fn adds_the_deprecation_message_keyword() { + let validator = Validator::options() + .with_vscode_documentation_keywords() + .build(&json!({ + "$ref": DeprecationMessageKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_descriptions_keyword() { + let validator = Validator::options() + .with_vscode_documentation_keywords() + .build(&json!({ + "$ref": EnumDescriptionsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_markdown_description_keyword() { + let validator = Validator::options() + .with_vscode_documentation_keywords() + .build(&json!({ + "$ref": MarkdownDescriptionKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_markdown_enum_descriptions_keyword() { + let validator = Validator::options() + .with_vscode_documentation_keywords() + .build(&json!({ + "$ref": MarkdownEnumDescriptionsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } +} + +#[cfg(test)] mod with_vscode_error_keywords { + use jsonschema::Validator; + use pretty_assertions::assert_eq; + use serde_json::json; + + use crate::vscode::{ + VSCodeValidationOptionsExtensions, + keywords::{ + ErrorMessageKeyword, + PatternErrorMessageKeyword, + VSCodeKeywordDefinition + } + }; + + #[test] fn adds_the_error_message_keyword() { + let validator = Validator::options() + .with_vscode_error_keywords() + .build(&json!({ + "$ref": ErrorMessageKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_pattern_error_message_keyword() { + let validator = Validator::options() + .with_vscode_error_keywords() + .build(&json!({ + "$ref": PatternErrorMessageKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } +} + +#[cfg(test)] mod with_vscode_parsing_keywords { + use jsonschema::Validator; + use serde_json::json; + + use crate::vscode::{ + VSCodeValidationOptionsExtensions, + keywords::{ + AllowCommentsKeyword, + AllowTrailingCommasKeyword, + VSCodeKeywordDefinition + } + }; + + #[test] fn adds_the_allow_comments_keyword() { + let validator = Validator::options() + .with_vscode_parsing_keywords() + .build(&json!({ + "$ref": AllowCommentsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_allow_trailing_commas_keyword() { + let validator = Validator::options() + .with_vscode_parsing_keywords() + .build(&json!({ + "$ref": AllowTrailingCommasKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } +} + +#[cfg(test)] mod with_vscode_vocabulary { + use jsonschema::Validator; + use pretty_assertions::assert_eq; + use serde_json::json; + + use crate::vscode::{ + VSCodeValidationOptionsExtensions, + keywords::{ + VSCodeKeywordDefinition, + AllowCommentsKeyword, + AllowTrailingCommasKeyword, + CompletionDetailKeyword, + DefaultSnippetsKeyword, + DeprecationMessageKeyword, + DoNotSuggestKeyword, + EnumDescriptionsKeyword, + EnumDetailsKeyword, + EnumSortTextsKeyword, + ErrorMessageKeyword, + MarkdownDescriptionKeyword, + MarkdownEnumDescriptionsKeyword, + PatternErrorMessageKeyword, + SuggestSortTextKeyword + } + }; + + #[test] fn adds_the_allow_comments_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": AllowCommentsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_allow_trailing_commas_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": AllowTrailingCommasKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_completion_detail_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": CompletionDetailKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_default_snippets_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": DefaultSnippetsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!([{"label": "valid"}]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_deprecation_message_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": DeprecationMessageKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_do_not_suggest_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": DoNotSuggestKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(true); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_descriptions_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": EnumDescriptionsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_details_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": EnumDetailsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_sort_texts_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": EnumSortTextsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_error_message_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": ErrorMessageKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_markdown_description_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": MarkdownDescriptionKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_markdown_enum_descriptions_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": MarkdownEnumDescriptionsKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!("invalid"); + let valid_instance = &json!(["valid"]); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_pattern_error_message_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": PatternErrorMessageKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_suggest_sort_text_keyword() { + let validator = Validator::options() + .with_vscode_vocabulary() + .build(&json!({ + "$ref": SuggestSortTextKeyword::KEYWORD_ID + })).unwrap(); + + let invalid_instance = &json!(false); + let valid_instance = &json!("valid"); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } +} + +#[cfg(test)] mod with_vscode_dialect { + use jsonschema::Validator; + use pretty_assertions::assert_eq; + use serde_json::json; + + use crate::vscode::{ + VSCodeValidationOptionsExtensions, + dialect::VSCodeDialect + }; + + #[test] fn adds_the_allow_comments_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID, + })).unwrap(); + + let invalid_instance = &json!({"allowComments": "invalid"}); + let valid_instance = &json!({"allowComments": true}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_allow_trailing_commas_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID, + })).unwrap(); + + let invalid_instance = &json!({"allowTrailingCommas": "invalid"}); + let valid_instance = &json!({"allowTrailingCommas": true}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_completion_detail_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"completionDetail": false}); + let valid_instance = &json!({"completionDetail": "valid"}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_default_snippets_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"defaultSnippets": "invalid"}); + let valid_instance = &json!({"defaultSnippets": [{"label": "valid"}]}); + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_deprecation_message_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"deprecationMessage": false}); + let valid_instance = &json!({"deprecationMessage": "valid"}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_do_not_suggest_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"doNotSuggest": "invalid"}); + let valid_instance = &json!({"doNotSuggest": true}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_descriptions_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"enumDescriptions": "invalid"}); + let valid_instance = &json!({"enumDescriptions": ["valid"]}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_details_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"enumDetails": "invalid"}); + let valid_instance = &json!({"enumDetails": ["valid"]}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_enum_sort_texts_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"enumSortTexts": "invalid"}); + let valid_instance = &json!({"enumSortTexts": ["valid"]}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_error_message_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"errorMessage": false}); + let valid_instance = &json!({"errorMessage": "valid"}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_markdown_description_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"markdownDescription": false}); + let valid_instance = &json!({"markdownDescription": "valid"}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_markdown_enum_descriptions_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"markdownEnumDescriptions": "invalid"}); + let valid_instance = &json!({"markdownEnumDescriptions": ["valid"]}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_pattern_error_message_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"patternErrorMessage": false}); + let valid_instance = &json!({"patternErrorMessage": "valid"}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } + #[test] fn adds_the_suggest_sort_text_keyword() { + let validator = Validator::options() + .with_vscode_dialect() + .build(&json!({ + "$ref": VSCodeDialect::SCHEMA_ID + })).unwrap(); + + let invalid_instance = &json!({"suggestSortText": false}); + let valid_instance = &json!({"suggestSortText": "valid"}); + + assert_eq!(validator.is_valid(invalid_instance), false); + assert_eq!(validator.is_valid(valid_instance), true); + } +} diff --git a/lib/dsc-lib-jsonschema/src/tests/vscode/vocabulary.rs b/lib/dsc-lib-jsonschema/src/tests/vscode/vocabulary.rs new file mode 100644 index 000000000..c53caf7cf --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/tests/vscode/vocabulary.rs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[allow(non_snake_case)] +#[cfg(test)] mod VSCODE_VOCABULARY_SCHEMA_BUNDLED { + use crate::vscode::vocabulary::VSCODE_VOCABULARY_SCHEMA_BUNDLED; + + #[test] fn meta_schema_is_valid() { + let schema = VSCODE_VOCABULARY_SCHEMA_BUNDLED.clone(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); + } +} + +#[allow(non_snake_case)] +#[cfg(test)] mod VSCODE_VOCABULARY_SCHEMA_CANONICAL { + use crate::vscode::vocabulary::VSCODE_VOCABULARY_SCHEMA_CANONICAL; + + #[test] fn meta_schema_is_valid() { + let schema = VSCODE_VOCABULARY_SCHEMA_CANONICAL.clone(); + let result = jsonschema::meta::validate( + schema.as_value() + ); + assert!(result.is_ok(), "Unexpected error: {}", result.unwrap_err()); + } +} + +#[allow(non_snake_case)] +#[cfg(test)] mod VSCodeVocabulary { + #[cfg(test)] mod json_schema_bundled { + use schemars::{SchemaGenerator, generate::SchemaSettings}; + + use crate::vscode::vocabulary::VSCodeVocabulary; + + #[test] fn returns_schema_with_bundled_definitions() { + let schema = VSCodeVocabulary::json_schema_bundled( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + let has_defs = schema.get("$defs") + .and_then(serde_json::Value::as_object) + .is_some_and(|defs| defs.keys().len() > 0); + + assert!(has_defs); + } + #[test] fn includes_vscode_vocabulary() { + let schema = VSCodeVocabulary::json_schema_bundled( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + let vocab = schema.get("$vocabulary") + .and_then(serde_json::Value::as_object).unwrap(); + + assert_eq!(vocab.get(VSCodeVocabulary::SPEC_URI).unwrap(), false) + } + } + #[cfg(test)] mod json_schema_canonical { + use pretty_assertions::assert_eq; + use schemars::{SchemaGenerator, generate::SchemaSettings}; + + use crate::vscode::vocabulary::VSCodeVocabulary; + + #[test] fn returns_schema_without_bundled_definitions() { + let schema = VSCodeVocabulary::json_schema_canonical( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + + assert_eq!(schema.get("$defs"), None); + } + #[test] fn includes_vscode_vocabulary() { + let schema = VSCodeVocabulary::json_schema_canonical( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + let vocab = schema.get("$vocabulary") + .and_then(serde_json::Value::as_object).unwrap(); + + assert_eq!(vocab.get(VSCodeVocabulary::SPEC_URI).unwrap(), false) + } + } + #[cfg(test)] mod schema_resource_bundled { + use schemars::{SchemaGenerator, generate::SchemaSettings}; + + use crate::vscode::vocabulary::VSCodeVocabulary; + + #[test] fn does_not_panic() { + VSCodeVocabulary::schema_resource_bundled( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + } + } + #[cfg(test)] mod schema_resource_canonical { + use schemars::{SchemaGenerator, generate::SchemaSettings}; + + use crate::vscode::vocabulary::VSCodeVocabulary; + + #[test] fn does_not_panic() { + VSCodeVocabulary::schema_resource_canonical( + &mut SchemaGenerator::new(SchemaSettings::draft2020_12()) + ); + } + } +} 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 index 0189a2d2f..69c84417b 100644 --- a/lib/dsc-lib-jsonschema/src/transforms/idiomaticize_externally_tagged_enum.rs +++ b/lib/dsc-lib-jsonschema/src/transforms/idiomaticize_externally_tagged_enum.rs @@ -1,7 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::cmp::PartialEq; use schemars::Schema; -use serde_json::{Map, Value, json}; +use serde_json::{self, json, Map, Value}; -use crate::vscode::VSCODE_KEYWORDS; +use crate::vscode::keywords::VSCodeKeyword; /// Munges the generated schema for externally tagged enums into an idiomatic object schema. /// @@ -199,7 +203,7 @@ pub fn idiomaticize_externally_tagged_enum(schema: &mut Schema) { if let Some(d) = item_desc && property_data.get("description").is_none() { property_data.insert("description".into(), d.clone()); } - for keyword in VSCODE_KEYWORDS { + for keyword in VSCodeKeyword::ALL { if let Some(keyword_value) = item_data.get(keyword) && property_data.get(keyword).is_none() { property_data.insert(keyword.to_string(), keyword_value.clone()); } diff --git a/lib/dsc-lib-jsonschema/src/vscode/dialect.rs b/lib/dsc-lib-jsonschema/src/vscode/dialect.rs new file mode 100644 index 000000000..f4d792746 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/dialect.rs @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::sync::{Arc, LazyLock}; + +use jsonschema::Resource; +use rust_i18n::t; +use schemars::{JsonSchema, Schema, SchemaGenerator, generate::SchemaSettings, json_schema}; + +use crate::vscode::{ + keywords::{ + AllowCommentsKeyword, + AllowTrailingCommasKeyword, + CompletionDetailKeyword, + DefaultSnippetsKeyword, + DeprecationMessageKeyword, + DoNotSuggestKeyword, + EnumDescriptionsKeyword, + EnumDetailsKeyword, + EnumSortTextsKeyword, + ErrorMessageKeyword, + MarkdownDescriptionKeyword, + MarkdownEnumDescriptionsKeyword, + PatternErrorMessageKeyword, + SuggestSortTextKeyword, + VSCodeKeywordDefinition + }, + vocabulary::VSCodeVocabulary +}; + +/// Defines the meta schema for defining schemas with the VS Code vocabulary. +/// +/// This meta schema is based on the JSON Schema draft 2020-12. It includes an extended set of +/// annotation keywords that VS Code recognizes and uses to enhance the JSON authoring and editing +/// experience. The vocabulary itself is defined in [`VSCodeVocabulary`]. +/// +/// While you don't _need_ to use this meta schema to define the annotation keywords VS Code +/// recognizes, specifying this meta schema for your documents does enable you to validate that +/// the keywords are correctly defined in your own schemas. +/// +/// The meta schema struct defines associated constants and helper methods: +/// +/// - [`SCHEMA_ID`] defines the canonical URI to the meta schema specified in the schema's `$id` +/// keyword. +/// - [`json_schema_bundled()`] retrieves the bundled form of the meta schema with a custom +/// [`SchemaGenerator`]. +/// - [`json_schema_canonical()`] retrieves the canonical form of the meta schema with a custom +/// [`SchemaGenerator`]. +/// - [`schema_resource_bundled()`] retrieves the bundled form of the meta schema with a custom +/// [`SchemaGenerator`] as a [`Resource`]. +/// - [`schema_resource_canonical()`] retrieves the canonical form of the meta schema with a custom +/// [`SchemaGenerator`] as a [`Resource`]. +/// +/// For easier access to the schemas, consider using the following statics if you don't need to use +/// a custom generator: +/// +/// - [`VSCODE_DIALECT_SCHEMA_BUNDLED`] contains the bundled form of the meta schema with the +/// schema resources for the vocabulary and keywords included in the `$defs` keyword. +/// - [`VSCODE_DIALECT_SCHEMA_CANONICAL`] contains the canonical form of the meta schema without +/// the bundled schema resources for a smaller definition. +/// - [`VSCODE_DIALECT_SCHEMA_RESOURCE_BUNDLED`] contains the bundled form of the meta schema as +/// a [`Resource`] to simplify registering the resource with a [`jsonschema::Validator`]. +/// - [`VSCODE_DIALECT_SCHEMA_RESOURCE_CANONICAL`] contains the canonical form of the meta schema +/// as a [`Resource`]. +/// +/// [`SCHEMA_ID`]: Self::SCHEMA_ID +/// [`json_schema_bundled()`]: Self::json_schema_bundled +/// [`json_schema_canonical()`]: Self::json_schema_canonical +/// [`schema_resource_bundled()`]: Self::schema_resource_bundled +/// [`schema_resource_canonical()`]: Self::schema_resource_canonical +pub struct VSCodeDialect; + +impl VSCodeDialect { + /// Defines the canonical URI for the meta schema's `$id` keyword. + pub const SCHEMA_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/meta.json"; + + /// Retrieves the bundled form of the meta schema. + /// + /// The bundled form presents the meta schema as a compound schema document with the VS Code + /// vocabulary and keyword schemas included under the `$defs` keyword. Use this form of the + /// schema when you need every schema resource included in a single document. + /// + /// This function requires you to pass a [`SchemaGenerator`] to retrieve the schema. The + /// definition for the meta schema is static, but you can use custom transforms with your + /// [`SchemaGenerator`] to modify the schema if needed. If you want to use the default + /// representation of the bundled meta schema, use [`VSCODE_DIALECT_SCHEMA_BUNDLED`]. + /// + /// You can also use the [`json_schema_canonical()`] method to retrieve the canonical form of + /// the meta schema without the bundled schema resources or [`VSCODE_DIALECT_SCHEMA_CANONICAL`] + /// to use the default representation of the canonical meta schema. + /// + /// [`json_schema_canonical()`]: Self::json_schema_canonical + pub fn json_schema_bundled(generator: &mut schemars::SchemaGenerator) -> Schema { + Self::json_schema(generator) + } + + /// Retrieves the canonical form of the meta schema. + /// + /// The canonical form presents the meta schema without bundling the VS Code vocabulary or + /// keyword schemas under the `$defs` keyword. Use this form of the schema when you can rely + /// on retrieving the other schemas from network or other methods. + /// + /// This function requires you to pass a [`SchemaGenerator`] to retrieve the schema. The + /// definition for the meta schema is static, but you can use custom transforms with your + /// [`SchemaGenerator`] to modify the schema if needed. If you want to use the default + /// representation of the canonical meta schema, use [`VSCODE_DIALECT_SCHEMA_CANONICAL`]. + /// + /// You can also use the [`json_schema_bundled()`] method to retrieve the bundled + /// form of the meta schema with the schema resources bundled under the `$defs` keyword or + /// [`VSCODE_DIALECT_SCHEMA_BUNDLED`] to use the default representation of the canonical + /// meta schema. + /// + /// [`json_schema_bundled()`]: Self::json_schema_bundled + pub fn json_schema_canonical(generator: &mut schemars::SchemaGenerator) -> Schema { + let mut schema = Self::json_schema(generator); + schema.remove("$defs"); + schema + } + + /// Retrieves the bundled form of the meta schema as a [`Resource`] so you can include + /// it in the registered resources for a [`jsonschema::Validator`] using the [`with_resource()`] + /// or [`with_resources()`] methods on the [`jsonschema::ValidationOptions`] builder. + /// + /// The bundled form presents the meta schema as a compound schema document with the VS Code + /// vocabulary and keyword schemas included under the `$defs` keyword. Use this form of the + /// schema when you need every schema resource included in a single document. + /// + /// This function requires you to pass a [`SchemaGenerator`] to retrieve the schema. The + /// definition for the meta schema is static, but you can use custom transforms with your + /// [`SchemaGenerator`] to modify the schema if needed. If you want to use the default + /// representation of the bundled meta schema, use [`VSCODE_DIALECT_SCHEMA_RESOURCE_BUNDLED`]. + /// + /// You can also use the [`schema_resource_canonical()`] method to retrieve the canonical + /// form of the meta schema without the bundled schema resources or + /// [`VSCODE_DIALECT_SCHEMA_RESOURCE_CANONICAL`] to use the default representation of the + /// canonical meta schema. + /// + /// # Panics + /// + /// This method panics if the schema is malformed and can't be converted into a [`Resource`]. + /// + /// In practice, you should never see a panic from this method because the crate's test suite + /// checks for this failure mode. + /// + /// [`schema_resource_canonical()`]: Self::schema_resource_canonical + /// [`with_resource()`]: jsonschema::ValidationOptions::with_resource + /// [`with_resources()`]: jsonschema::ValidationOptions::with_resources + pub fn schema_resource_bundled(generator: &mut schemars::SchemaGenerator) -> Resource { + Resource::from_contents(Self::json_schema(generator).to_value()).unwrap() + } + + /// Retrieves the bundled form of the meta schema as a [`Resource`] so you can include + /// it in the registered resources for a [`jsonschema::Validator`] using the [`with_resource()`] + /// or [`with_resources()`] methods on the [`jsonschema::ValidationOptions`] builder. + /// + /// The canonical form presents the meta schema without bundling the VS Code vocabulary or + /// keyword schemas under the `$defs` keyword. Use this form of the schema when you can rely + /// on retrieving the other schemas from network or other methods. + /// + /// This function requires you to pass a [`SchemaGenerator`] to retrieve the schema. The + /// definition for the meta schema is static, but you can use custom transforms with your + /// [`SchemaGenerator`] to modify the schema if needed. If you want to use the default + /// representation of the canonical meta schema, use + /// [`VSCODE_DIALECT_SCHEMA_RESOURCE_CANONICAL`]. + /// + /// You can also use the [`schema_resource_bundled()`] method to retrieve the bundled form of + /// the meta schema without the bundled schema resources or + /// [`VSCODE_DIALECT_SCHEMA_RESOURCE_BUNDLED`] to use the default representation of the bundled + /// meta schema. + /// + /// # Panics + /// + /// This method panics if the schema is malformed and can't be converted into a [`Resource`]. + /// + /// In practice, you should never see a panic from this method because the crate's test suite + /// checks for this failure mode. + /// + /// [`schema_resource_bundled()`]: Self::schema_resource_bundled + /// [`with_resource()`]: jsonschema::ValidationOptions::with_resource + /// [`with_resources()`]: jsonschema::ValidationOptions::with_resources + pub fn schema_resource_canonical(generator: &mut schemars::SchemaGenerator) -> Resource { + Resource::from_contents(Self::json_schema_canonical(generator).to_value()).unwrap() + } +} + +impl JsonSchema for VSCodeDialect { + fn json_schema(generator: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": Self::SCHEMA_ID, + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + VSCodeVocabulary::SPEC_URI: false, + }, + "title": t!("vscode.dialect.title"), + "description": t!("vscode.dialect.description"), + "markdownDescription": t!("vscode.dialect.markdownDescription"), + + "$dynamicAnchor": "meta", + "allOf": [ + { "$ref": "https://json-schema.org/draft/2020-12/schema" }, + { "$ref": VSCodeVocabulary::SCHEMA_ID } + ], + "type": ["object", "boolean"], + "$defs": { + VSCodeVocabulary::SCHEMA_ID: VSCodeVocabulary::json_schema_canonical(generator), + AllowCommentsKeyword::KEYWORD_ID: AllowCommentsKeyword::json_schema(generator), + AllowTrailingCommasKeyword::KEYWORD_ID: AllowTrailingCommasKeyword::json_schema(generator), + CompletionDetailKeyword::KEYWORD_ID: CompletionDetailKeyword::json_schema(generator), + DefaultSnippetsKeyword::KEYWORD_ID: DefaultSnippetsKeyword::json_schema(generator), + DeprecationMessageKeyword::KEYWORD_ID: DeprecationMessageKeyword::json_schema(generator), + DoNotSuggestKeyword::KEYWORD_ID: DoNotSuggestKeyword::json_schema(generator), + EnumDescriptionsKeyword::KEYWORD_ID: EnumDescriptionsKeyword::json_schema(generator), + EnumDetailsKeyword::KEYWORD_ID: EnumDetailsKeyword::json_schema(generator), + EnumSortTextsKeyword::KEYWORD_ID: EnumSortTextsKeyword::json_schema(generator), + ErrorMessageKeyword::KEYWORD_ID: ErrorMessageKeyword::json_schema(generator), + MarkdownDescriptionKeyword::KEYWORD_ID: MarkdownDescriptionKeyword::json_schema(generator), + MarkdownEnumDescriptionsKeyword::KEYWORD_ID: MarkdownEnumDescriptionsKeyword::json_schema(generator), + PatternErrorMessageKeyword::KEYWORD_ID: PatternErrorMessageKeyword::json_schema(generator), + SuggestSortTextKeyword::KEYWORD_ID: SuggestSortTextKeyword::json_schema(generator), + } + }) + } + fn schema_name() -> std::borrow::Cow<'static, str> { + Self::SCHEMA_ID.into() + } +} + +/// Contains the bundled form of the VS Code meta schema. +/// +/// The bundled form presents the meta schema as a compound schema document with the VS Code +/// vocabulary and keyword schemas included under the `$defs` keyword. Use this form of the +/// schema when you need every schema resource included in a single document. +/// +/// You can also use [`VSCODE_DIALECT_SCHEMA_CANONICAL`] to retrieve the canonical form of the meta +/// schema without the bundled schema resources. +/// +/// This representation of the schema is generated with the default [`SchemaSettings`] for +/// JSON Schema draft 2020-12. To retrieve the bundled schema with custom generator settings, +/// use the [`json_schema_bundled()`] method. +/// +/// [`json_schema_bundled()`]: VSCodeDialect::json_schema_bundled +pub static VSCODE_DIALECT_SCHEMA_BUNDLED: LazyLock> = LazyLock::new(|| { + let generator = &mut SchemaGenerator::new( + SchemaSettings::draft2020_12() + ); + + Arc::from(VSCodeDialect::json_schema_bundled(generator)) +}); + +/// Contains the canonical form of the VS Code meta schema. +/// +/// The canonical form presents the meta schema without bundling the VS Code vocabulary or +/// keyword schemas under the `$defs` keyword. Use this form of the schema when you can rely +/// on retrieving the other schemas from network or other methods. +/// +/// You can also use [`VSCODE_DIALECT_SCHEMA_BUNDLED`] to retrieve the bundled form of the meta +/// schema with the schema resources bundled under the `$defs` keyword. +/// +/// This representation of the schema is generated with the default [`SchemaSettings`] for +/// JSON Schema draft 2020-12. To retrieve the canonical schema with custom generator settings, +/// use the [`json_schema_canonical()`] method, which takes a [`SchemaGenerator`] as input. +/// +/// [`json_schema_canonical()`]: VSCodeDialect::json_schema_canonical +pub static VSCODE_DIALECT_SCHEMA_CANONICAL: LazyLock> = LazyLock::new(|| { + let generator: &mut SchemaGenerator = &mut SchemaGenerator::new( + SchemaSettings::draft2020_12() + ); + + Arc::from(VSCodeDialect::json_schema_canonical(generator)) +}); + +/// Contains the bundled form of the VS Code meta schema as a [`Resource`] so you can include +/// it in the registered resources for a [`jsonschema::Validator`] using the [`with_resource()`] +/// or [`with_resources()`] methods on the [`jsonschema::ValidationOptions`] builder. +/// +/// The bundled form presents the meta schema as a compound schema document with the VS Code +/// vocabulary and keyword schemas included under the `$defs` keyword. Use this form of the +/// schema when you need every schema resource included in a single document. +/// +/// You can also use [`VSCODE_DIALECT_SCHEMA_RESOURCE_CANONICAL`] to retrieve the canonical form of +/// the meta schema without the bundled schema resources. +/// +/// This representation of the schema is generated with the default [`SchemaSettings`] for +/// JSON Schema draft 2020-12. To retrieve the bundled schema with custom generator settings, +/// use the [`json_schema_bundled()`] method. +/// +/// [`with_resource()`]: jsonschema::ValidationOptions::with_resource +/// [`with_resources()`]: jsonschema::ValidationOptions::with_resources +/// [`json_schema_bundled()`]: VSCodeDialect::json_schema_bundled +pub static VSCODE_DIALECT_SCHEMA_RESOURCE_BUNDLED: LazyLock> = LazyLock::new(|| { + let generator = &mut SchemaGenerator::new( + SchemaSettings::draft2020_12() + ); + + Arc::from(VSCodeDialect::schema_resource_bundled(generator)) +}); + +/// Contains the canonical form of the VS Code meta schema as a [`Resource`] so you can include +/// it in the registered resources for a [`jsonschema::Validator`] using the [`with_resource()`] +/// or [`with_resources()`] methods on the [`jsonschema::ValidationOptions`] builder. +/// +/// The canonical form presents the meta schema without bundling the VS Code vocabulary or +/// keyword schemas under the `$defs` keyword. Use this form of the schema when you can rely +/// on retrieving the other schemas from network or other methods. +/// +/// You can also use [`VSCODE_DIALECT_SCHEMA_RESOURCE_BUNDLED`] to retrieve the bundled form of the +/// meta schema with the schema resources bundled under the `$defs` keyword. +/// +/// This representation of the schema is generated with the default [`SchemaSettings`] for +/// JSON Schema draft 2020-12. To retrieve the bundled schema with custom generator settings, +/// use the [`json_schema_canonical()`] method. +/// +/// [`with_resource()`]: jsonschema::ValidationOptions::with_resource +/// [`with_resources()`]: jsonschema::ValidationOptions::with_resources +/// [`json_schema_canonical()`]: VSCodeDialect::json_schema_canonical +pub static VSCODE_DIALECT_SCHEMA_RESOURCE_CANONICAL: LazyLock> = LazyLock::new(|| { + let generator = &mut SchemaGenerator::new( + SchemaSettings::draft2020_12() + ); + + Arc::from(VSCodeDialect::schema_resource_canonical(generator)) +}); diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/allow_comments.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/allow_comments.rs new file mode 100644 index 000000000..69c69bd4d --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/allow_comments.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `allowComments` keyword for the VS Code vocabulary. +/// +/// This keyword indicates whether VS Code should allow comments in the JSON file, even when the +/// file extension isn't `.jsonc`. +/// +/// By default, JSON comments in `.json` files cause parsing errors. If you define a JSON Schema +/// with `allowComments` set to `true`, VS Code doesn't raise validation errors for comments in +/// JSON for that schema. +#[derive(Default, Serialize, Deserialize)] +pub struct AllowCommentsKeyword(bool); + +impl VSCodeKeywordDefinition for AllowCommentsKeyword { + const KEYWORD_NAME: &str = "allowComments"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/allowComments.json"; + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_bool() { + Ok(Box::new(Self(v))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + t!("vscode.keywords.allow_comments.factory_error_invalid_type"), + )) + } + } +} + +impl JsonSchema for AllowCommentsKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.allowComments.title"), + "description": t!("vscode.keywords.allowComments.description"), + "markdownDescription": t!("vscode.keywords.allowComments.markdownDescription"), + "type": "boolean", + "default": false + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for AllowCommentsKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/allow_trailing_commas.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/allow_trailing_commas.rs new file mode 100644 index 000000000..a0fba1c25 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/allow_trailing_commas.rs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `allowTrailingCommas` keyword for the VS Code vocabulary. +/// +/// This keword indicates whether VS Code should allow trailing commas in the JSON file. +/// +/// By default, a comma after the last item in an array or last key-value pair in an object +/// causes a parsing error. If you define a JSON Schema with `allowTrailingCommas` set to +/// `true`, VS Code doesn't raise validation errors for commas after the last item in arrays +/// or last key-value pair in objects for that Schema. +#[derive(Default, Serialize, Deserialize)] +pub struct AllowTrailingCommasKeyword(bool); + +impl VSCodeKeywordDefinition for AllowTrailingCommasKeyword { + const KEYWORD_NAME: &str = "allowTrailingCommas"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/allowTrailingCommas.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_bool() { + Ok(Box::new(Self(v))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + t!("vscode.keywords.allow_trailing_commas.factory_error_invalid_type"), + )) + } + } +} + +impl JsonSchema for AllowTrailingCommasKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.allowTrailingCommas.title"), + "description": t!("vscode.keywords.allowTrailingCommas.description"), + "markdownDescription": t!("vscode.keywords.allowTrailingCommas.markdownDescription"), + "type": "boolean", + "default": false + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for AllowTrailingCommasKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/completion_detail.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/completion_detail.rs new file mode 100644 index 000000000..d094e390a --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/completion_detail.rs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `completionDetail` keyword for the VS Code vocabulary. +/// +/// This keyword Defines additional information for IntelliSense when completing a proposed item, +/// replacing the `title` keyword as code-formatted text. +/// +/// By default, when a user completes a value for a schema or subschema, VS Code displays +/// additional information in hover text. If the schema defines the `title` keyword, the +/// hover text includes the title string as the first line of the hover text. +/// +/// If you define the `completionDetail` keyword, VS Code displays the string as monospace +/// code-formatted text instead of the `title` keyword's value. +/// +/// If the schema defines the `description` or `markdownDescription` keywords, that text is +/// displayed in the hover text after the value from the `completionDetail` or `title` +/// keyword. +#[derive(Serialize, Deserialize)] +pub struct CompletionDetailKeyword(String); + +impl VSCodeKeywordDefinition for CompletionDetailKeyword { + const KEYWORD_NAME: &str = "completionDetail"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/completionDetail.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: jsonschema::paths::Location, + ) -> Result, jsonschema::ValidationError<'a>> { + if let Some(v) = value.as_str() { + Ok(Box::new(Self(v.to_string()))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + t!("vscode.keywords.completion_detail.factory_error_invalid_type"), + )) + } + } +} + +impl JsonSchema for CompletionDetailKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.completionDetail.title"), + "description": t!("vscode.keywords.completionDetail.title"), + "markdownDescription": t!("vscode.keywords.completionDetail.title"), + "type": "string", + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for CompletionDetailKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/default_snippets.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/default_snippets.rs new file mode 100644 index 000000000..f45c5e91d --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/default_snippets.rs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{json_schema, JsonSchema}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the structure of a snippet in the `defaultSnippets` keyword for the VS Code vocabulary. +/// +/// Every snippet must define either the `body` or `bodyText` property, which VS Code uses +/// to insert the snippet into the data file. If you specify both `body` and `bodyText`, the +/// value for `body` supercedes the value for `bodyText`. +/// +/// The `description`, and `markdownDescription` properties provide documentation for the +/// snippet and are displayed in the hover text when a user selects the snippet. If you +/// specify both `description` and `markdownDescription`, the text for +/// `markdownDescription` supercedes the text for `description`. +/// +/// The `label` property defines a short name for the snippet. If the snippet doesn't define +/// the `label` property, VS Code shows a stringified representation of the snippet instead. +/// +/// Snippets are presented to the user in alphabetical order by the value of their `label` +/// property (or the stringified representation of the snippet if it has no label). +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Snippet { + /// Defines a short name for the snippet instead of using the stringified representation + /// of the snippet's value. The `label` property also affects the order that VS Code + /// presents the snippets. VS Code sorts the snippets for completion alphabetically by + /// label. + pub label: Option, + /// Defines plain text documentation for the snippet displayed in the completion dialog. + /// + /// When the snippet doesn't define the `description` or `markdownDescription` property, + /// the snippet provides no additional context to the user aside from the label until + /// they select the snippet for completion. Use the `description` property to provide + /// information to the user about the snippet. If you need to provide rich formatting, + /// like links or text formatting, use the `markdownDescription` property. + /// + /// If you define both the `description` and `markdownDescription` proeprty for a + /// snippet, the `markdownDescription` text overrides the `description` text. + pub description: Option, + /// Defines formatted documentation for the snippet displayed in the completion dialog. + /// + /// When the snippet doesn't define the `description` or `markdownDescription` property, + /// the snippet provides no additional context to the user aside from the label until + /// they select the snippet for completion. Use the `description` property to provide + /// information to the user about the snippet. If you need to provide rich formatting, + /// like links or text formatting, use the `markdownDescription` property. + /// + /// If you define both the `description` and `markdownDescription` proeprty for a + /// snippet, the `markdownDescription` text overrides the `description` text. + pub markdown_description: Option, + /// Defines the data to insert for the snippet. The data can be any type. When the user + /// selects the snippet, VS Code inserts the data at the cursor. In string literals for + /// the `body` you can use [snippet syntax][01] to define tabstops, placeholders, and + /// variables. + /// + /// Alternatively, you can define the `bodyText` property for the snippet, which + /// specifies the text to insert for the snippet as a string. + /// + /// If you define both the `bodyText` and `body` properties for a snippet, the `body` + /// definition overrides the `bodyText` property. + /// + /// [01]: https://code.visualstudio.com/docs/editing/userdefinedsnippets#_snippet-syntax + pub body: Option, + /// Defines the data to insert for the snippet as a string literal. When the user + /// selects the snippet, VS Code inserts the text _without_ the enclosing quotation + /// marks at the cursor. You can use [snippet syntax][01] to define tabstops, + /// placeholders, and variables in the `bodyText`. + /// + /// Alternatively, you can define the `body` property for the snippet, which specifies + /// the text to insert for the snippet as data. + /// + /// If you define both the `bodyText` and `body` properties for a snippet, the `body` + /// definition overrides the `bodyText` property. + /// + /// [01]: https://code.visualstudio.com/docs/editing/userdefinedsnippets#_snippet-syntax + pub body_text: Option, +} + +/// Defines the `defaultSnippets` keyword for the VS Code vocabulary. +/// +/// This keyword Provides snippets for completion of a schema or subschema value or property. +/// +/// By default, VS Code presents a set of completion options for data with an associated JSON +/// Schema like suggesting defined property names or enum values. You can use the +/// `defaultSnippets` keyword to provide an array of snippets with more control over the +/// presentation, default values, and enable users to quickly fill out the snippet. +/// +/// The keyword expects an array of objects that each define a snippet. For more information +/// about defining snippets, see [Define snippets in JSON Schemas][01]. For more information +/// about the snippet syntax, see [Snippet syntax][02]. +/// +/// [01]: https://code.visualstudio.com/Docs/languages/json#_define-snippets-in-json-schemas +/// [02]: https://code.visualstudio.com/docs/editing/userdefinedsnippets#_snippet-syntax +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DefaultSnippetsKeyword(Vec); + +impl DefaultSnippetsKeyword { + /// Creates a new instance of the keyword from a vector of snippets. + #[must_use] + pub fn new(snippets: Vec) -> Self { + Self(snippets) + } +} + +impl VSCodeKeywordDefinition for DefaultSnippetsKeyword { + const KEYWORD_NAME: &str = "defaultSnippets"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/defaultSnippets.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_array() { + if v.iter().any(|item| item.as_object().is_none()) { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.default_snippets.factory_error_non_object_item"), + t!("vscode.keywords.default_snippets.factory_error_suffix"), + ), + )) + } else if let Ok(snippets) = serde_json::from_value::(value.clone()){ + Ok(Box::new(snippets)) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.default_snippets.factory_error_invalid_item"), + t!("vscode.keywords.default_snippets.factory_error_suffix"), + ) + )) + } + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.default_snippets.factory_error_not_array"), + t!("vscode.keywords.default_snippets.factory_error_suffix"), + ), + )) + } + } +} + +impl JsonSchema for DefaultSnippetsKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.defaultSnippets.title"), + "description": t!("vscode.keywords.defaultSnippets.title"), + "markdownDescription": t!("vscode.keywords.defaultSnippets.title"), + "unevaluatedItems": false, + "type": "array", + "items": { + "title": t!("vscode.keywords.defaultSnippets.items.title"), + "description": t!("vscode.keywords.defaultSnippets.items.description"), + "markdownDescription": t!("vscode.keywords.defaultSnippets.items.markdownDescription"), + "type": "object", + "unevaluatedProperties": false, + "properties": { + "label": { + "title": t!("vscode.keywords.defaultSnippets.properties.label.title"), + "description": t!("vscode.keywords.defaultSnippets.properties.label.title"), + "markdownDescription": t!("vscode.keywords.defaultSnippets.properties.label.title"), + "type": "string" + }, + "description": { + "title": t!("vscode.keywords.defaultSnippets.properties.description.title"), + "description": t!("vscode.keywords.defaultSnippets.properties.description.title"), + "markdownDescription": t!("vscode.keywords.defaultSnippets.properties.description.title"), + "type": "string" + }, + "markdownDescription": { + "title": t!("vscode.keywords.defaultSnippets.properties.markdownDescription.title"), + "description": t!("vscode.keywords.defaultSnippets.properties.markdownDescription.title"), + "markdownDescription": t!("vscode.keywords.defaultSnippets.properties.markdownDescription.title"), + "type": "string" + }, + "body": { + "title": t!("vscode.keywords.defaultSnippets.properties.body.title"), + "description": t!("vscode.keywords.defaultSnippets.properties.body.title"), + "markdownDescription": t!("vscode.keywords.defaultSnippets.properties.body.title"), + }, + "bodyText": { + "title": t!("vscode.keywords.defaultSnippets.properties.bodyText.title"), + "description": t!("vscode.keywords.defaultSnippets.properties.bodyText.title"), + "markdownDescription": t!("vscode.keywords.defaultSnippets.properties.bodyText.title"), + "type": "string" + }, + }, + } + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for DefaultSnippetsKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} + +impl From for Vec { + fn from(value: DefaultSnippetsKeyword) -> Self { + value.0 + } +} + +impl From> for DefaultSnippetsKeyword { + fn from(value: Vec) -> Self { + Self(value) + } +} \ No newline at end of file diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/deprecation_message.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/deprecation_message.rs new file mode 100644 index 000000000..a20ff690a --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/deprecation_message.rs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `deprecationMessage` keyword for the VS Code vocabulary. +/// +/// This keyword defines a message to surface as a warning to users when they specify a deprecated +/// property in their data. +/// +/// This keyword only has an affect when defined in a schema or subschema that also defines +/// the `deprecated` keyword as `true`. When you define the `deprecationMessage` keyword for +/// a deprecated schema or subschema, VS Code displays the provided message instead of the +/// default warning about deprecation. +#[derive(Serialize, Deserialize)] +pub struct DeprecationMessageKeyword(String); + +impl VSCodeKeywordDefinition for DeprecationMessageKeyword { + const KEYWORD_NAME: &str = "deprecationMessage"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/deprecationMessage.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_str() { + Ok(Box::new(Self(v.to_string()))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + t!("vscode.keywords.deprecation_message.factory_error_invalid_type"), + )) + } + } +} + +impl JsonSchema for DeprecationMessageKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.deprecationMessage.title"), + "description": t!("vscode.keywords.deprecationMessage.title"), + "markdownDescription": t!("vscode.keywords.deprecationMessage.title"), + "type": "string", + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for DeprecationMessageKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/do_not_suggest.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/do_not_suggest.rs new file mode 100644 index 000000000..c6323a876 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/do_not_suggest.rs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `doNotSuggest` keyword for the VS Code vocabulary. +/// +/// This keyword indicates whether VS Code should avoid suggesting the property for IntelliSense. +/// +/// By default, VS Code will show any defined property in the `properties` keyword as a +/// completion option with IntelliSense. You can define the `doNotSuggest` keyword in a +/// property subschema as `true` to indicate that VS Code should not show that property for +/// IntelliSense. +#[derive(Serialize, Deserialize)] +pub struct DoNotSuggestKeyword(bool); + +impl VSCodeKeywordDefinition for DoNotSuggestKeyword { + const KEYWORD_NAME: &str = "doNotSuggest"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/doNotSuggest.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_bool() { + Ok(Box::new(Self(v))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + t!("vscode.keywords.do_not_suggest.factory_error_invalid_type"), + )) + } + } +} + +impl JsonSchema for DoNotSuggestKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.doNotSuggest.title"), + "description": t!("vscode.keywords.doNotSuggest.title"), + "markdownDescription": t!("vscode.keywords.doNotSuggest.title"), + "type": "boolean", + "default": "false" + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for DoNotSuggestKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/enum_descriptions.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/enum_descriptions.rs new file mode 100644 index 000000000..15ce2a085 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/enum_descriptions.rs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `enumDescriptions` keyword for the VS Code vocabulary. +/// +/// This keyword Defines per-value descriptions for schemas that use the `enum` keyword. +/// +/// The builtin keywords for JSON Schema includes the `description` keyword, which you can use +/// to document a given schema or subschema. However, for schemas that use the `enum` keyword +/// to define an array of valid values, JSON Schema provides no keyword for documenting each +/// value. +/// +/// With the `enumDescriptions` keyword from the VS Code vocabulary, you can document each +/// item in the `enum` keyword array. VS Code interprets each item in `enumDescriptions` as +/// documenting the item at the same index in the `enum` keyword. +/// +/// This documentation is surfaced in VS Code on hover for an enum value and for IntelliSense +/// when completing an enum value. +/// +/// If you want to use Markdown syntax for the annotation, specify the +/// [`markdownEnumDescriptions` keyword] instead. +/// +/// [`markdownEnumDescriptions` keyword]: crate::vscode::keywords::MarkdownEnumDescriptionsKeyword +#[derive(Serialize, Deserialize)] +pub struct EnumDescriptionsKeyword(Vec); + +impl VSCodeKeywordDefinition for EnumDescriptionsKeyword { + const KEYWORD_NAME: &str = "enumDescriptions"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/enumDescriptions.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_array() { + if v.iter().all(|item| item.as_str().is_some()) { + Ok(Box::new(Self( + v.iter().map(|item| item.as_str().unwrap().to_string()).collect() + ))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.enum_descriptions.factory_error_non_string_item"), + t!("vscode.keywords.enum_descriptions.factory_error_suffix"), + ), + )) + } + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.enum_descriptions.factory_error_not_array"), + t!("vscode.keywords.enum_descriptions.factory_error_suffix"), + ), + )) + } + } +} + +impl JsonSchema for EnumDescriptionsKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.enumDescriptions.title"), + "description": t!("vscode.keywords.enumDescriptions.title"), + "markdownDescription": t!("vscode.keywords.enumDescriptions.title"), + "type": "array", + "items": { + "type": "string" + } + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for EnumDescriptionsKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/enum_details.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/enum_details.rs new file mode 100644 index 000000000..151eff614 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/enum_details.rs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `enumDetails` keyword for the VS Code vocabulary. +/// +/// This keyword defines additional information for IntelliSense when completing a proposed enum +/// value, shown before the description. +/// +/// By default, when VS Code suggests a completion for an item defined in the `enum` keyword, +/// VS Code displays hover text with a description. If the schema defined the `description`, +/// `enumDescriptions`, or `markdownEnumDescriptions` keywords, VS Code displays that text. +/// The `markdownEnumDescriptions` keyword overrides the `enumDescriptions` keyword, which +/// overrides the `description` keyword. +/// +/// When you define the `enumDetails` keyword, VS Code displays the string for that enum +/// value as monospace code-formatted text. The keyword expects an array of strings. VS Code +/// correlates the items in the `enumDetails` keyword to the items in the `enum` keyword by +/// their index. The first item in `enumDetails` maps to the first item in `enum` and so on. +#[derive(Serialize, Deserialize)] +pub struct EnumDetailsKeyword(Vec); + +impl VSCodeKeywordDefinition for EnumDetailsKeyword { + const KEYWORD_NAME: &str = "enumDetails"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/enumDetails.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_array() { + if v.iter().all(|item| item.as_str().is_some()) { + Ok(Box::new(Self( + v.iter().map(|item| item.as_str().unwrap().to_string()).collect() + ))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.enum_details.factory_error_non_string_item"), + t!("vscode.keywords.enum_details.factory_error_suffix"), + ), + )) + } + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.enum_details.factory_error_not_array"), + t!("vscode.keywords.enum_details.factory_error_suffix"), + ), + )) + } + } +} + +impl JsonSchema for EnumDetailsKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.enumDetails.title"), + "description": t!("vscode.keywords.enumDetails.title"), + "markdownDescription": t!("vscode.keywords.enumDetails.title"), + "type": "array", + "items": { + "type": "string" + } + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for EnumDetailsKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/enum_sort_texts.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/enum_sort_texts.rs new file mode 100644 index 000000000..c45fd00ca --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/enum_sort_texts.rs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `enumSortTexts` keyword for the VS Code vocabulary. +/// +/// This keyword defines a alternate strings to use when sorting a suggestion for enum values. +/// +/// By default, suggestions are sorted alphabetically, not in the order that you define +/// items in the `enum` keyword array. You can use the `enumSortText` keyword to override +/// the order the values are displayed, providing a different string for each value. +/// +/// The keyword expects an array of strings. VS Code correlates the items in the +/// `enumSortText` keyword to the items in the `enum` keyword by their index. The first item +/// in `enumSortText` maps to the first item in `enum` and so on. +/// +/// For example, in the following schema, VS Code will suggest the `baz`, then `bar`, then +/// `foo` values: +/// +/// ```json +/// { +/// "type": "string", +/// "enum": ["foo", "bar", "baz"], +/// "enumSortText": ["c", "b", "a"] +/// } +/// ``` +#[derive(Serialize, Deserialize)] +pub struct EnumSortTextsKeyword(Vec); + +impl VSCodeKeywordDefinition for EnumSortTextsKeyword { + const KEYWORD_NAME: &str = "enumSortTexts"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/enumSortTexts.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_array() { + if v.iter().all(|item| item.as_str().is_some()) { + Ok(Box::new(Self( + v.iter().map(|item| item.as_str().unwrap().to_string()).collect() + ))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.enum_sort_texts.factory_error_non_string_item"), + t!("vscode.keywords.enum_sort_texts.factory_error_suffix"), + ), + )) + } + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.enum_sort_texts.factory_error_not_array"), + t!("vscode.keywords.enum_sort_texts.factory_error_suffix"), + ), + )) + } + } +} + +impl JsonSchema for EnumSortTextsKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.enumSortTexts.title"), + "description": t!("vscode.keywords.enumSortTexts.title"), + "markdownDescription": t!("vscode.keywords.enumSortTexts.title"), + "type": "array", + "items": { + "type": "string" + } + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for EnumSortTextsKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/error_message.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/error_message.rs new file mode 100644 index 000000000..4a9424c92 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/error_message.rs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `errorMessage` keyword for the VS Code vocabulary. +/// +/// This keyword Defines a friendly error message to raise when a schema or subschema fails validation. +/// +/// By default, VS Code surfaces a default error message for data that fails schema +/// validation, like specifying an invalid type. You can use the `errorMessage` keyword to +/// define a custom message to raise in the editor when the data fails validation for the +/// following cases: +/// +/// - When the data is an invalid type as validated by the `type` keyword. +/// - When the subschema defined for the `not` keyword is valid. +/// - When the data is invalid for the defined values in the `enum` keyword. +/// - When the data is invalid for the defined value in the `const` keyword. +/// - When a string doesn't match the regular expression defined in the `pattern` keyword. +/// This message is overridden by the `patternErrorMessage` keyword if it's defined. +/// - When a string value doesn't match a required format. +/// - When the data is for an array that is validated by the `minContains` or `maxContains` +/// keywords and fails those validations. +/// - When the data includes a property that was defined in the `properties` keyword as +/// `false`, forbidding the property. +/// - When the data includes a property that was defined in the `patternProperties` keyword as +/// `false`, forbidding matching property names. +/// - When the data includes a property that wasn't defined in the `properties` or +/// `patternProperties` keyword and the schema defines `additionalProperties` as `false`. +/// - When the data includes a property that isn't evaluated by any keywords and the schema +/// defines `unevaluatedProperties` as `false`. +/// +/// The value for the `errorMessage` keyword supercedes all default messages for the schema +/// or subschema where you define the keyword. You can provide per-validation failure +/// messages by defining the validating keywords in separate entries of the `allOf` keyword +/// and defining the `errorMessage` keyword for each entry. +#[derive(Serialize, Deserialize)] +pub struct ErrorMessageKeyword(String); + +impl VSCodeKeywordDefinition for ErrorMessageKeyword { + const KEYWORD_NAME: &str = "errorMessage"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/errorMessage.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_str() { + Ok(Box::new(Self(v.to_string()))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + t!("vscode.keywords.error_message.factory_error_invalid_type"), + )) + } + } +} + +impl JsonSchema for ErrorMessageKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.errorMessage.title"), + "description": t!("vscode.keywords.errorMessage.title"), + "markdownDescription": t!("vscode.keywords.errorMessage.title"), + "type": "string", + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for ErrorMessageKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/markdown_description.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/markdown_description.rs new file mode 100644 index 000000000..e16e71bd2 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/markdown_description.rs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `markdownDescription` keyword for the VS Code vocabulary. +/// +/// This keyword Defines documentation for the schema or subschema displayed as hover text in VS Code. +/// +/// By default, VS Code displays the text defined in the `description` keyword in the hover +/// text for properties and values. VS Code interprets the `description` keyword literally, +/// without converting any apparent markup. +/// +/// You can define the `markdownDescription` keyword to provide descriptive text as markdown, +/// including links and code blocks. When a schema or subschema defines the +/// `markdownDescription` keyword, that value supercedes any defined text in the `description` +/// keyword. +/// +/// You can also use the `markdownEnumDescriptions` keyword to document the values defined +/// for the `enum` keyword. +/// +/// For more information, see [Use rich formatting in hovers][01]. +/// +/// [01]: https://code.visualstudio.com/Docs/languages/json#_use-rich-formatting-in-hovers +#[derive(Serialize, Deserialize)] +pub struct MarkdownDescriptionKeyword(pub String); + +impl VSCodeKeywordDefinition for MarkdownDescriptionKeyword { + const KEYWORD_NAME: &str = "markdownDescription"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/markdownDescription.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_str() { + Ok(Box::new(Self(v.to_string()))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + t!("vscode.keywords.markdown_description.factory_error_invalid_type"), + )) + } + } +} + +impl JsonSchema for MarkdownDescriptionKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.markdownDescription.title"), + "description": t!("vscode.keywords.markdownDescription.title"), + "markdownDescription": t!("vscode.keywords.markdownDescription.title"), + "type": "string", + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for MarkdownDescriptionKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/markdown_enum_descriptions.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/markdown_enum_descriptions.rs new file mode 100644 index 000000000..aa299626b --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/markdown_enum_descriptions.rs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `markdownEnumDescriptions` keyword for the VS Code vocabulary. +/// +/// This keyword defines documentation for enum values displayed as hover text in VS Code. +/// +/// By default, when a user hovers on or selects completion for a value that is validated by +/// the `enum` keyword, VS Code displays the text from the `description` or +/// `markdownDescription` keywords for the schema or subschema. You can use the +/// `markdownEnumDescriptions` keyword to define documentation for each enum value. +/// +/// When a schema or subschema defines the `markdownEnumDescriptions` keyword, that value +/// supercedes any defined text in the `description`, `markdownDescription`, or +/// `enumDescriptions` keywords. +/// +/// The keyword expects an array of strings. VS Code correlates the items in the +/// `markdownEnumDescriptions` keyword to the items in the `enum` keyword by their index. The +/// first item in `markdownEnumDescriptions` maps to the first item in `enum` and so on. +#[derive(Serialize, Deserialize)] +pub struct MarkdownEnumDescriptionsKeyword(Vec); + +impl VSCodeKeywordDefinition for MarkdownEnumDescriptionsKeyword { + const KEYWORD_NAME: &str = "markdownEnumDescriptions"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/markdownEnumDescriptions.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_array() { + if v.iter().all(|item| item.as_str().is_some()) { + Ok(Box::new(Self( + v.iter().map(|item| item.as_str().unwrap().to_string()).collect() + ))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.markdown_enum_descriptions.factory_error_non_string_item"), + t!("vscode.keywords.markdown_enum_descriptions.factory_error_suffix"), + ), + )) + } + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + format!( + "{} {}", + t!("vscode.keywords.markdown_enum_descriptions.factory_error_not_array"), + t!("vscode.keywords.markdown_enum_descriptions.factory_error_suffix"), + ), + )) + } + } +} + +impl JsonSchema for MarkdownEnumDescriptionsKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.markdownEnumDescriptions.title"), + "description": t!("vscode.keywords.markdownEnumDescriptions.title"), + "markdownDescription": t!("vscode.keywords.markdownEnumDescriptions.title"), + "type": "array", + "items": { + "type": "string" + } + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for MarkdownEnumDescriptionsKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} \ No newline at end of file diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/mod.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/mod.rs new file mode 100644 index 000000000..4485d7169 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/mod.rs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(unused_imports)] + +use jsonschema::{Keyword, Resource, ValidationError, ValidationOptions, paths::Location}; +use schemars::{JsonSchema, Schema, json_schema}; +use serde_json::{Map, Value}; + +// Define the trait for implementing keywords and re-export it from `vscode::keywords` +mod vscode_keyword_definition; +pub use vscode_keyword_definition::VSCodeKeywordDefinition; +/// Define the enum for available keywords and re-export it from `vscode::keywords` +mod vscode_keyword; +pub use vscode_keyword::VSCodeKeyword; +// Define the keywords in separate modules and re-export them from `vscode::keywords` +mod allow_comments; +pub use allow_comments::AllowCommentsKeyword; +mod allow_trailing_commas; +pub use allow_trailing_commas::AllowTrailingCommasKeyword; +mod completion_detail; +pub use completion_detail::CompletionDetailKeyword; +mod default_snippets; +pub use default_snippets::{DefaultSnippetsKeyword, Snippet}; +mod deprecation_message; +pub use deprecation_message::DeprecationMessageKeyword; +mod do_not_suggest; +pub use do_not_suggest::DoNotSuggestKeyword; +mod enum_descriptions; +pub use enum_descriptions::EnumDescriptionsKeyword; +mod enum_details; +pub use enum_details::EnumDetailsKeyword; +mod enum_sort_texts; +pub use enum_sort_texts::EnumSortTextsKeyword; +mod error_message; +pub use error_message::ErrorMessageKeyword; +mod markdown_description; +pub use markdown_description::MarkdownDescriptionKeyword; +mod markdown_enum_descriptions; +pub use markdown_enum_descriptions::MarkdownEnumDescriptionsKeyword; +mod pattern_error_message; +pub use pattern_error_message::PatternErrorMessageKeyword; +mod suggest_sort_text; +pub use suggest_sort_text::SuggestSortTextKeyword; + diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/pattern_error_message.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/pattern_error_message.rs new file mode 100644 index 000000000..47dec196e --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/pattern_error_message.rs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `patternErrorMessage` keyword for the VS Code vocabulary. +/// +/// This keyword defines a friendly error message to raise when a schema or subschema fails +/// validation for the `pattern` keyword. +/// +/// By default, when a value fails validation for the `pattern` keyword, VS Code raises an +/// error that informs the user that the value is invalid for the given regular expression, +/// which it displays in the message. +/// +/// Reading and parsing regular expressions can be difficult even for experienced users. You +/// can define the `patternErrorMessage` keyword to provide better information to the user +/// about the expected pattern for the string value. +#[derive(Serialize, Deserialize)] +pub struct PatternErrorMessageKeyword(String); + +impl VSCodeKeywordDefinition for PatternErrorMessageKeyword { + const KEYWORD_NAME: &str = "patternErrorMessage"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/patternErrorMessage.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_str() { + Ok(Box::new(Self(v.to_string()))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + t!("vscode.keywords.pattern_error_message.factory_error_invalid_type"), + )) + } + } +} + +impl JsonSchema for PatternErrorMessageKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.patternErrorMessage.title"), + "description": t!("vscode.keywords.patternErrorMessage.title"), + "markdownDescription": t!("vscode.keywords.patternErrorMessage.title"), + "type": "string", + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for PatternErrorMessageKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/suggest_sort_text.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/suggest_sort_text.rs new file mode 100644 index 000000000..8eda4d25a --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/suggest_sort_text.rs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::borrow::Cow; + +use jsonschema::{Keyword, ValidationError, paths::Location}; +use rust_i18n::t; +use schemars::{Schema, JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; + +use crate::vscode::keywords::VSCodeKeywordDefinition; + +/// Defines the `suggestSortText` keyword for the VS Code vocabulary. +/// +/// This keyword defines an alternate string to use when sorting a suggestion. +/// +/// By default, suggestions are displayed in alphabetical order. You can define the +/// `suggestSortText` keyword to change how the suggestions are sorted. For example, in +/// the following schema, VS Code will suggest the `baz`, then `bar`, then `foo` properties: +/// +/// ```json +/// { +/// "type": "object", +/// "properties": { +/// "foo": { +/// "suggestSortText": "c", +/// } +/// "bar": { +/// "suggestSortText": "b", +/// } +/// "baz": { +/// "suggestSortText": "a", +/// } +/// } +/// } +/// ``` +#[derive(Serialize, Deserialize)] +pub struct SuggestSortTextKeyword(String); + +impl VSCodeKeywordDefinition for SuggestSortTextKeyword { + const KEYWORD_NAME: &str = "suggestSortText"; + const KEYWORD_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/keywords/suggestSortText.json"; + + fn keyword_factory<'a>( + _parent: &'a serde_json::Map, + value: &'a serde_json::Value, + path: Location, + ) -> Result, ValidationError<'a>> { + if let Some(v) = value.as_str() { + Ok(Box::new(Self(v.to_string()))) + } else { + Err(ValidationError::custom( + Location::new(), + path, + value, + t!("vscode.keywords.suggest_sort_text.factory_error_invalid_type"), + )) + } + } +} + +impl JsonSchema for SuggestSortTextKeyword { + fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema { + json_schema!({ + "$schema": Self::META_SCHEMA, + "$id": Self::KEYWORD_ID, + "title": t!("vscode.keywords.suggestSortText.title"), + "description": t!("vscode.keywords.suggestSortText.title"), + "markdownDescription": t!("vscode.keywords.suggestSortText.title"), + "type": "string", + }) + } + + fn schema_name() -> Cow<'static, str> { + Self::KEYWORD_ID.into() + } +} + +impl Keyword for SuggestSortTextKeyword { + fn validate<'i>( + &self, + _: &'i serde_json::Value, + _: &jsonschema::paths::LazyLocation, + ) -> Result<(), jsonschema::ValidationError<'i>> { + Ok(()) + } + fn is_valid(&self, _: &serde_json::Value) -> bool { + true + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/vscode_keyword.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/vscode_keyword.rs new file mode 100644 index 000000000..4c54597a9 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/vscode_keyword.rs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::str::FromStr; + +use jsonschema::ValidationOptions; + +use crate::vscode::keywords::{ + AllowCommentsKeyword, + AllowTrailingCommasKeyword, + CompletionDetailKeyword, + DefaultSnippetsKeyword, + DeprecationMessageKeyword, + DoNotSuggestKeyword, + EnumDescriptionsKeyword, + EnumDetailsKeyword, + EnumSortTextsKeyword, + ErrorMessageKeyword, + MarkdownDescriptionKeyword, + MarkdownEnumDescriptionsKeyword, + PatternErrorMessageKeyword, + SuggestSortTextKeyword, + VSCodeKeywordDefinition +}; + +/// Defines the available keywords for VS Code's extended vocabulary. +/// +/// These keywords are annotation keywords that don't change the validation processing, so any +/// consumer of a schema using these keywords can safely ignore them if it doesn't understand +/// the keywords. +/// +/// The transformers and generators in this library strip the VS Code keywords from canonical +/// schemas, as they are primarily for improving the development experience in a code editor, +/// not machine processing. Removing them from canonical schemas makes canonical schemas smaller +/// and more compatible, as some JSON Schema implementations may error on unrecognized keywords +/// instead of ignoring them. +pub enum VSCodeKeyword { + AllowComments, + AllowTrailingCommas, + CompletionDetail, + DefaultSnippets, + DeprecationMessage, + DoNotSuggest, + EnumDescriptions, + EnumDetails, + EnumSortTexts, + ErrorMessage, + MarkdownDescription, + MarkdownEnumDescriptions, + PatternErrorMessage, + SuggestSortText +} + +impl VSCodeKeyword { + /// Contains the name of every keyword in the VS Code vocabulary. + pub const ALL: [&str; 14] = [ + AllowCommentsKeyword::KEYWORD_NAME, + AllowTrailingCommasKeyword::KEYWORD_NAME, + CompletionDetailKeyword::KEYWORD_NAME, + DefaultSnippetsKeyword::KEYWORD_NAME, + DeprecationMessageKeyword::KEYWORD_NAME, + DoNotSuggestKeyword::KEYWORD_NAME, + EnumDescriptionsKeyword::KEYWORD_NAME, + EnumDetailsKeyword::KEYWORD_NAME, + EnumSortTextsKeyword::KEYWORD_NAME, + ErrorMessageKeyword::KEYWORD_NAME, + MarkdownDescriptionKeyword::KEYWORD_NAME, + MarkdownEnumDescriptionsKeyword::KEYWORD_NAME, + PatternErrorMessageKeyword::KEYWORD_NAME, + SuggestSortTextKeyword::KEYWORD_NAME, + ]; + /// Contains the name of every keyword in the VS Code vocabulary that affects whether and how + /// a schema or subschema is presented for completion with IntelliSense in VS Code. + pub const COMPLETION: [&str; 6] = [ + CompletionDetailKeyword::KEYWORD_NAME, + DefaultSnippetsKeyword::KEYWORD_NAME, + DoNotSuggestKeyword::KEYWORD_NAME, + EnumDetailsKeyword::KEYWORD_NAME, + EnumSortTextsKeyword::KEYWORD_NAME, + SuggestSortTextKeyword::KEYWORD_NAME, + ]; + /// Contains the name of every keyword in the VS Code vocabulary that provides enhanced + /// documentation for a schema or subschema. + pub const DOCUMENTATION: [&str; 4] = [ + DeprecationMessageKeyword::KEYWORD_NAME, + EnumDescriptionsKeyword::KEYWORD_NAME, + MarkdownDescriptionKeyword::KEYWORD_NAME, + MarkdownEnumDescriptionsKeyword::KEYWORD_NAME, + ]; + /// Contains the name of every keyword in the VS Code vocabulary that provides enhanced error + /// messaging for invalid instances. + pub const ERROR: [&str; 2] = [ + ErrorMessageKeyword::KEYWORD_NAME, + PatternErrorMessageKeyword::KEYWORD_NAME, + ]; + /// Contains the name of every keyword in the VS Code vocabulary that affects how VS Code + /// validates the JSON from a parsing perspective as opposed to validating the _data_. + pub const PARSING: [&str; 2] = [ + AllowCommentsKeyword::KEYWORD_NAME, + AllowTrailingCommasKeyword::KEYWORD_NAME, + ]; + + /// Returns the name of a keyword for use in a JSON Schema, like `allowComments` for + /// [`VSCodeKeyword::AllowComments`]. + #[must_use] + pub const fn name(&self) -> &str { + match self { + Self::AllowComments => AllowCommentsKeyword::KEYWORD_NAME, + Self::AllowTrailingCommas => AllowTrailingCommasKeyword::KEYWORD_NAME, + Self::CompletionDetail => CompletionDetailKeyword::KEYWORD_NAME, + Self::DefaultSnippets => DefaultSnippetsKeyword::KEYWORD_NAME, + Self::DeprecationMessage => DeprecationMessageKeyword::KEYWORD_NAME, + Self::DoNotSuggest => DoNotSuggestKeyword::KEYWORD_NAME, + Self::EnumDescriptions => EnumDescriptionsKeyword::KEYWORD_NAME, + Self::EnumDetails => EnumDetailsKeyword::KEYWORD_NAME, + Self::EnumSortTexts => EnumSortTextsKeyword::KEYWORD_NAME, + Self::ErrorMessage => ErrorMessageKeyword::KEYWORD_NAME, + Self::MarkdownDescription => MarkdownDescriptionKeyword::KEYWORD_NAME, + Self::MarkdownEnumDescriptions => MarkdownEnumDescriptionsKeyword::KEYWORD_NAME, + Self::PatternErrorMessage => PatternErrorMessageKeyword::KEYWORD_NAME, + Self::SuggestSortText => SuggestSortTextKeyword::KEYWORD_NAME, + } + } + /// Registers the keyword with an instance of [`ValidationOptions`] from the `jsonschema` + /// crate. + /// + /// This convenience method enables you to quickly add keywords to the validator. However, + /// it doesn't follow the builder pattern typically used with [`ValidationOptions`]. + /// + /// For a more ergonomic way to register keywords, see [`VSCodeValidationOptionsExtensions`]. + /// + /// [`VSCodeValidationOptionsExtensions`]: crate::vscode::VSCodeValidationOptionsExtensions + pub fn register(self, options: ValidationOptions) -> ValidationOptions { + match self { + Self::AllowComments => options.with_keyword( + AllowCommentsKeyword::KEYWORD_NAME, + AllowCommentsKeyword::keyword_factory + ).with_resource( + AllowCommentsKeyword::KEYWORD_ID, + AllowCommentsKeyword::default_schema_resource() + ), + + Self::AllowTrailingCommas => options.with_keyword( + AllowTrailingCommasKeyword::KEYWORD_NAME, + AllowTrailingCommasKeyword::keyword_factory + ).with_resource( + AllowTrailingCommasKeyword::KEYWORD_ID, + AllowTrailingCommasKeyword::default_schema_resource() + ), + + Self::CompletionDetail => options.with_keyword( + CompletionDetailKeyword::KEYWORD_NAME, + CompletionDetailKeyword::keyword_factory + ).with_resource( + CompletionDetailKeyword::KEYWORD_ID, + CompletionDetailKeyword::default_schema_resource() + ), + Self::DefaultSnippets => options.with_keyword( + DefaultSnippetsKeyword::KEYWORD_NAME, + DefaultSnippetsKeyword::keyword_factory + ).with_resource( + DefaultSnippetsKeyword::KEYWORD_ID, + DefaultSnippetsKeyword::default_schema_resource() + ), + Self::DeprecationMessage => options.with_keyword( + DeprecationMessageKeyword::KEYWORD_NAME, + DeprecationMessageKeyword::keyword_factory + ).with_resource( + DeprecationMessageKeyword::KEYWORD_ID, + DeprecationMessageKeyword::default_schema_resource() + ), + Self::DoNotSuggest => options.with_keyword( + DoNotSuggestKeyword::KEYWORD_NAME, + DoNotSuggestKeyword::keyword_factory + ).with_resource( + DoNotSuggestKeyword::KEYWORD_ID, + DoNotSuggestKeyword::default_schema_resource() + ), + Self::EnumDescriptions => options.with_keyword( + EnumDescriptionsKeyword::KEYWORD_NAME, + EnumDescriptionsKeyword::keyword_factory + ).with_resource( + EnumDescriptionsKeyword::KEYWORD_ID, + EnumDescriptionsKeyword::default_schema_resource() + ), + Self::EnumDetails => options.with_keyword( + EnumDetailsKeyword::KEYWORD_NAME, + EnumDetailsKeyword::keyword_factory + ).with_resource( + EnumDetailsKeyword::KEYWORD_ID, + EnumDetailsKeyword::default_schema_resource() + ), + Self::EnumSortTexts => options.with_keyword( + EnumSortTextsKeyword::KEYWORD_NAME, + EnumSortTextsKeyword::keyword_factory + ).with_resource( + EnumSortTextsKeyword::KEYWORD_ID, + EnumSortTextsKeyword::default_schema_resource() + ), + Self::ErrorMessage => options.with_keyword( + ErrorMessageKeyword::KEYWORD_NAME, + ErrorMessageKeyword::keyword_factory + ).with_resource( + ErrorMessageKeyword::KEYWORD_ID, + ErrorMessageKeyword::default_schema_resource() + ), + Self::MarkdownDescription => options.with_keyword( + MarkdownDescriptionKeyword::KEYWORD_NAME, + MarkdownDescriptionKeyword::keyword_factory + ).with_resource( + MarkdownDescriptionKeyword::KEYWORD_ID, + MarkdownDescriptionKeyword::default_schema_resource() + ), + Self::MarkdownEnumDescriptions => options.with_keyword( + MarkdownEnumDescriptionsKeyword::KEYWORD_NAME, + MarkdownEnumDescriptionsKeyword::keyword_factory + ).with_resource( + MarkdownEnumDescriptionsKeyword::KEYWORD_ID, + MarkdownEnumDescriptionsKeyword::default_schema_resource() + ), + Self::PatternErrorMessage => options.with_keyword( + PatternErrorMessageKeyword::KEYWORD_NAME, + PatternErrorMessageKeyword::keyword_factory + ).with_resource( + PatternErrorMessageKeyword::KEYWORD_ID, + PatternErrorMessageKeyword::default_schema_resource() + ), + Self::SuggestSortText => options.with_keyword( + SuggestSortTextKeyword::KEYWORD_NAME, + SuggestSortTextKeyword::keyword_factory + ).with_resource( + SuggestSortTextKeyword::KEYWORD_ID, + SuggestSortTextKeyword::default_schema_resource() + ), + } + } +} + +impl From for String { + fn from(value: VSCodeKeyword) -> Self { + value.name().to_string() + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/keywords/vscode_keyword_definition.rs b/lib/dsc-lib-jsonschema/src/vscode/keywords/vscode_keyword_definition.rs new file mode 100644 index 000000000..62ff2552a --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/keywords/vscode_keyword_definition.rs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use jsonschema::{Keyword, Resource, ValidationError, paths::Location}; +use schemars::{JsonSchema, Schema, SchemaGenerator, generate::SchemaSettings, json_schema}; +use serde_json::{Map, Value}; + +/// Defines a VS Code keyword for the [`jsonschema`] crate. +/// +/// This trait requires the target type to implement both the [`JsonSchema`] and [`Keyword`] traits, +/// as this crate publishes both a vocabulary and meta schema for using the VS Code keywords. +pub trait VSCodeKeywordDefinition : JsonSchema + Keyword { + /// Defines the property name for the keyword, like `markdownDescription`. + const KEYWORD_NAME: &str; + + /// Defines the canonical `$id` URI for the keyword. + const KEYWORD_ID: &str; + + /// Defines the meta schema used to validate the keyword's own schema definition. + const META_SCHEMA: &str = "https://json-schema.org/draft/2020-12/schema"; + + /// Defines the factory function [`jsonschema`] requires for registering a custom keyword. + /// + /// For more information, see [`Keyword`]. + /// + /// # Errors + /// + /// The function returns a [`ValidationError`] when the JSON value is invalid for a given + /// keyword. In practice, none of the VS Code keywords ever return a validation error because + /// they are all annotation keywords. + #[allow(clippy::result_large_err)] + fn keyword_factory<'a>( + _parent: &'a Map, + value: &'a Value, + path: Location, + ) -> Result, ValidationError<'a>>; + + /// Returns the default representation of the JSON Schema for the keyword. + /// + /// The [`JsonSchema`] trait requires the [`json_schema()`] function to accept a mutable + /// reference to a [`SchemaGenerator`] for transforming and otherwise modifying the schema. + /// + /// The VS Code keyword schemas are statically defined with the [`json_schema!()`] macro and + /// don't use the generator. This convenience method passes a dummy default generator to the + /// [`json_schema()`] function so you can retrieve the schema without always needing to + /// instantiate a generator you're not using. In other cases, where you _do_ want to apply + /// transforms to every generated schema, you can still access the [`json_schema()`] trait + /// function directly. + /// + /// [`json_schema()`]: JsonSchema::json_schema + /// [`json_schema!()`]: schemars::json_schema! + #[must_use] + fn default_schema() -> Schema { + let generator = &mut SchemaGenerator::new( + SchemaSettings::draft2020_12() + ); + + Self::json_schema(generator) + } + + /// Returns a [`Schema`] object using the `$ref` keyword to point to the + /// VS Code keyword's canonical `$id` URI. + /// + /// This convenience method enables you to retrieve the reference subschema + /// without needing to construct it manually. + #[must_use] + fn schema_reference() -> Schema { + json_schema!({ + "$ref": Self::KEYWORD_ID + }) + } + + /// Returns the default schema for the keyword as a [`Resource`] to register with a + /// [`jsonschema::Validator`]. + /// + /// # Panics + /// + /// This method panics if the schema is malformed and can't be converted into a [`Resource`]. + /// + /// In practice, you should never see a panic from this method because the crate's test suite + /// checks for this failure mode. + #[must_use] + fn default_schema_resource() -> Resource { + Resource::from_contents(Self::default_schema().to_value()).unwrap() + } +} \ No newline at end of file diff --git a/lib/dsc-lib-jsonschema/src/vscode/mod.rs b/lib/dsc-lib-jsonschema/src/vscode/mod.rs index e5e101d7a..bcfaf1338 100644 --- a/lib/dsc-lib-jsonschema/src/vscode/mod.rs +++ b/lib/dsc-lib-jsonschema/src/vscode/mod.rs @@ -3,27 +3,10 @@ //! Provides helpers for working with JSON Schemas and VS Code. -/// Defines the available keywords for VS Code's extended vocabulary. -/// -/// These keywords are annotation keywords that don't change the validation processing, so any -/// consumer of a schema using these keywords can safely ignore them if it doesn't understand -/// the keywords. -/// -/// The transformers and generators in this library strip the VS Code keywords from canonical -/// schemas, as they are primarily for improving the development experience in a code editor, not -/// machine processing. Removing them from the canonical schemas makes the canonical schemas -/// smaller and more compatible, as some JSON Schema implementations may error on unrecognized -/// keywords instead of ignoring them. -pub const VSCODE_KEYWORDS: [&str; 11] = [ - "defaultSnippets", - "errorMessage", - "patternErrorMessage", - "deprecationMessage", - "enumDescriptions", - "markdownEnumDescriptions", - "markdownDescription", - "doNotSuggest", - "suggestSortText", - "allowComments", - "allowTrailingCommas", -]; +pub mod dialect; +pub mod keywords; +pub mod vocabulary; +mod schema_extensions; +pub use schema_extensions::VSCodeSchemaExtensions; +mod validation_options_extensions; +pub use validation_options_extensions::VSCodeValidationOptionsExtensions; diff --git a/lib/dsc-lib-jsonschema/src/vscode/schema_extensions.rs b/lib/dsc-lib-jsonschema/src/vscode/schema_extensions.rs new file mode 100644 index 000000000..4315cbb12 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/schema_extensions.rs @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::{clone::Clone, iter::Iterator, option::Option::None}; + +use schemars::Schema; + +use crate::{ + schema_utility_extensions::SchemaUtilityExtensions, + vscode::{ + dialect::VSCodeDialect, + keywords::{ + AllowCommentsKeyword, AllowTrailingCommasKeyword, CompletionDetailKeyword, DefaultSnippetsKeyword, DeprecationMessageKeyword, DoNotSuggestKeyword, EnumDescriptionsKeyword, EnumDetailsKeyword, EnumSortTextsKeyword, ErrorMessageKeyword, MarkdownDescriptionKeyword, MarkdownEnumDescriptionsKeyword, PatternErrorMessageKeyword, Snippet, SuggestSortTextKeyword, VSCodeKeywordDefinition + }} +}; + +/// Provides helper functions for working with VS Code's extended JSON Schema keywords with +/// [`schemars::Schema`] instances. +/// +/// The `get_*` functions simplify retrieving the value of a VS Code keyword for a given type. If +/// the schema defines the keyword with the expected type, those functions return a reference to +/// that value as the correct type. If the keyword doesn't exist or has the wrong value type, the +/// functions return [`None`]. +/// +/// The `should_*` function simplify retrieving the effective boolean value for the following +/// keywords, returning the defined value if it exists and otherwise `false`: +/// +/// - [`allowComments`] +/// - [`allowTrailingCommas`] +/// - [`doNotSuggest`] +/// +/// [`allowComments`]: AllowCommentsKeyword +/// [`allowTrailingCommas`]: AllowTrailingCommasKeyword +/// [`doNotSuggest`]: DoNotSuggestKeyword +pub trait VSCodeSchemaExtensions { + /// Retrieves the value for the [`CompletionDetailKeyword`] (`completionDetail`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns the completion detail string. + fn get_completion_detail(&self) -> Option<&str>; + /// Retrieves the value for the [`DefaultSnippetsKeyword`] (`defaultSnippets`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns a vector of snippets. + fn get_default_snippets(&self) -> Option>; + /// Retrieves the value for the [`DeprecationMessageKeyword`] (`deprecationMessage`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns the deprecation message string. + fn get_deprecation_message(&self) -> Option<&str>; + /// Retrieves the value for the [`EnumDescriptionsKeyword`] (`enumDescriptions`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns a vector of strings, where + /// each string describes a value for the `enum` keyword. + /// + /// Each item in the vector this method returns corresponds to an item in the vector for the + /// `enum` keyword by index. + fn get_enum_descriptions(&self) -> Option>; + /// Retrieves the value for the [`EnumDetailsKeyword`] (`enumDetails`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns a vector of strings, where + /// each string provides completion details for a value for the `enum` keyword. + /// + /// Each item in the vector this method returns corresponds to an item in the vector for the + /// `enum` keyword by index. + fn get_enum_details(&self) -> Option>; + /// Retrieves the value for the [`EnumSortTextsKeyword`] (`enumSortTexts`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns a vector of strings, where + /// each string provides an alternate string to sort the values when using IntelliSense. + /// + /// Each item in the vector this method returns corresponds to an item in the vector for the + /// `enum` keyword by index. + fn get_enum_sort_texts(&self) -> Option>; + /// Retrieves the value for the [`ErrorMessageKeyword`] (`errorMessage`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns the error message string. + fn get_error_message(&self) -> Option<&str>; + /// Retrieves the value for the [`MarkdownDescriptionKeyword`] (`markdownDescription`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns the description string. + fn get_markdown_description(&self) -> Option<&str>; + /// Retrieves the value for the [`MarkdownEnumDescriptionsKeyword`] (`markdownEnumDescriptions`) + /// if it's defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns a vector of strings, where + /// each string describes a value for the `enum` keyword. + /// + /// Each item in the vector this method returns corresponds to an item in the vector for the + /// `enum` keyword by index. + fn get_markdown_enum_descriptions(&self) -> Option>; + /// Retrieves the value for the [`PatternErrorMessageKeyword`] (`patternErrorMessage`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns a the error message string. + fn get_pattern_error_message(&self) -> Option<&str>; + /// Retrieves the value for the [`SuggestSortTextKeyword`] (`suggestSortText`) if it's + /// defined in the schema. + /// + /// If the schema doesn't define the keyword, or defines the keyword with an invalid value, + /// this method returns [`None`]. Otherwise, this method returns the alternate string to sort + /// the instance with when using IntelliSense + fn get_suggest_sort_text(&self) -> Option<&str>; + /// Indicates whether the schema defines the [`AllowCommentsKeyword`] (`allowComments`) as + /// `true`. + /// + /// This method returns `true` only when the schema defines the keyword as `true`. If the + /// schema doesn't define the keyword, defines it incorrectly, or defines it as `false`, this + /// method returns `false`. + fn should_allow_comments(&self) -> bool; + /// Indicates whether the schema defines the [`AllowTrailingCommasKeyword`] + /// (`allowTrailingCommas`) as `true`. + /// + /// This method returns `true` only when the schema defines the keyword as `true`. If the + /// schema doesn't define the keyword, defines it incorrectly, or defines it as `false`, this + /// method returns `false`. + fn should_allow_trailing_commas(&self) -> bool; + /// Indicates whether the schema defines the [`DoNotSuggestKeyword`] (`doNotSuggest`) as + /// `true`. + /// + /// This method returns `true` only when the schema defines the keyword as `true`. If the + /// schema doesn't define the keyword, defines it incorrectly, or defines it as `false`, this + /// method returns `false`. + fn should_not_suggest(&self) -> bool; + /// Indicates whether the schema uses the [`VSCodeDialect`] by checking the value of the + /// `$schema` keyword. + /// + /// If the schema doesn't define its dialect or defines a different dialect, this method + /// returns `false`. If the schema defines its dialect as the VS Code dialect, this method + /// returns `true`. + /// + /// Note that any schema may use keywords from the VS Code vocabulary even if the schema uses + /// a different dialect. The keywords are annotation keywords and don't affect validation for + /// data instances, so validators may safely ignore them. + /// + /// [`VSCodeVocabulary`]: crate::vscode::vocabulary::VSCodeVocabulary + fn uses_vscode_dialect(&self) -> bool; +} + +impl VSCodeSchemaExtensions for Schema { + fn get_markdown_description(&self) -> Option<&str> { + self.get_keyword_as_str(MarkdownDescriptionKeyword::KEYWORD_NAME) + } + fn get_markdown_enum_descriptions(&self) -> Option> { + match self.get_keyword_as_array(MarkdownEnumDescriptionsKeyword::KEYWORD_NAME) { + None => None, + Some(list) => list.iter().map(|v| v.as_str()).collect(), + } + } + fn should_allow_comments(&self) -> bool { + self.get_keyword_as_bool(AllowCommentsKeyword::KEYWORD_NAME).unwrap_or_default() + } + fn should_allow_trailing_commas(&self) -> bool { + self.get_keyword_as_bool(AllowTrailingCommasKeyword::KEYWORD_NAME).unwrap_or_default() + } + fn should_not_suggest(&self) -> bool { + self.get_keyword_as_bool(DoNotSuggestKeyword::KEYWORD_NAME).unwrap_or_default() + } + fn get_completion_detail(&self) -> Option<&str> { + self.get_keyword_as_str(CompletionDetailKeyword::KEYWORD_NAME) + } + fn get_deprecation_message(&self) -> Option<&str> { + self.get_keyword_as_str(DeprecationMessageKeyword::KEYWORD_NAME) + } + fn get_enum_descriptions(&self) -> Option> { + match self.get_keyword_as_array(EnumDescriptionsKeyword::KEYWORD_NAME) { + None => None, + Some(list) => list.iter().map(|v| v.as_str()).collect(), + } + } + fn get_enum_details(&self) -> Option> { + match self.get_keyword_as_array(EnumDetailsKeyword::KEYWORD_NAME) { + None => None, + Some(list) => list.iter().map(|v| v.as_str()).collect(), + } + } + fn get_enum_sort_texts(&self) -> Option> { + match self.get_keyword_as_array(EnumSortTextsKeyword::KEYWORD_NAME) { + None => None, + Some(list) => list.iter().map(|v| v.as_str()).collect(), + } + } + fn get_error_message(&self) -> Option<&str> { + self.get_keyword_as_str(ErrorMessageKeyword::KEYWORD_NAME) + } + fn get_pattern_error_message(&self) -> Option<&str> { + self.get_keyword_as_str(PatternErrorMessageKeyword::KEYWORD_NAME) + } + fn get_suggest_sort_text(&self) -> Option<&str> { + self.get_keyword_as_str(SuggestSortTextKeyword::KEYWORD_NAME) + } + fn get_default_snippets(&self) -> Option> { + match self.get(DefaultSnippetsKeyword::KEYWORD_NAME) { + Some(snippets_json) => serde_json::from_value(snippets_json.clone()).ok(), + None => None, + } + } + fn uses_vscode_dialect(&self) -> bool { + if let Some(dialect) = self.get("$schema").and_then(|d| d.as_str()) { + dialect == VSCodeDialect::SCHEMA_ID + } else { + false + } + } +} \ No newline at end of file diff --git a/lib/dsc-lib-jsonschema/src/vscode/validation_options_extensions.rs b/lib/dsc-lib-jsonschema/src/vscode/validation_options_extensions.rs new file mode 100644 index 000000000..b9a81348f --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/validation_options_extensions.rs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use jsonschema::ValidationOptions; + +use crate::vscode::{ + dialect::{VSCodeDialect, VSCODE_DIALECT_SCHEMA_RESOURCE_CANONICAL}, + vocabulary::{VSCodeVocabulary, VSCODE_VOCABULARY_SCHEMA_RESOURCE_CANONICAL}, + keywords::VSCodeKeyword, +}; + +/// Defines extension methods to the [`jsonschema::ValidationOptions`] to simplify registering the +/// VS Code [keywords], [vocabulary], and [dialect meta schema]. +/// +/// [keywords]: VSCodeKeyword +/// [vocabulary]: VSCodeVocabulary +/// [dialect meta schema]: VSCodeDialect +pub trait VSCodeValidationOptionsExtensions { + /// Registers a single VS Code keyword for use with a [`jsonschema::Validator`]. + /// + /// This function registers a specific VS Code keyword and the schema resource that defines it + /// with the [`with_keyword()`] and [`with_resource()`] builder methods. + /// + /// [`with_keyword()`]: ValidationOptions::with_keyword + /// [`with_resource()`]: ValidationOptions::with_resource + /// [`with_vscode_dialect()`]: VSCodeValidationOptionsExtensions::with_vscode_dialect + /// [`with_vscode_vocabulary()`]: VSCodeValidationOptionsExtensions::with_vscode_vocabulary + fn with_vscode_keyword(self, keyword: VSCodeKeyword) -> ValidationOptions; + /// Registers every VS Code keyword for use with a [`jsonschema::Validator`]. + /// + /// This function registers each of the VS Code keywords and the schema resources that define + /// them with the [`with_keyword()`] and [`with_resource()`] builder methods. + /// + /// Use this function when you only want to register the VS Code vocabulary keywords. + /// If you are using the VS Code vocabulary in your own meta schema dialect, use the + /// [`with_vscode_vocabulary()`] method instead. If you are using the VS Code meta schema + /// dialect directly, use the [`with_vscode_dialect()`] method instead. + /// + /// [`with_keyword()`]: ValidationOptions::with_keyword + /// [`with_resource()`]: ValidationOptions::with_resource + /// [`with_vscode_dialect()`]: VSCodeValidationOptionsExtensions::with_vscode_dialect + /// [`with_vscode_vocabulary()`]: VSCodeValidationOptionsExtensions::with_vscode_vocabulary + fn with_vscode_keywords(self) -> ValidationOptions; + /// Registers the VS Code completion keywords for use with a [`jsonschema::Validator`]. + /// + /// This function registers the following VS Code Keywords and the schema resources that + /// define them with the [`with_keyword()`] and [`with_resource()`] builder methods: + /// + /// - [`completionDetail`] + /// - [`defaultSnippets`] + /// - [`doNotSuggest`] + /// - [`enumDetails`] + /// - [`enumSortTexts`] + /// - [`suggestSortText`] + /// + /// Use this function when you only want to register a subset of VS Code vocabulary keywords. + /// If you are using the VS Code vocabulary in your own meta schema dialect, use the + /// [`with_vscode_vocabulary()`] method instead. If you are using the VS Code meta schema + /// dialect directly, use the [`with_vscode_dialect()`] method instead. + /// + /// [`completionDetail`]: super::keywords::CompletionDetailKeyword + /// [`defaultSnippets`]: super::keywords::DefaultSnippetsKeyword + /// [`doNotSuggest`]: super::keywords::DoNotSuggestKeyword + /// [`enumDetails`]: super::keywords::EnumDetailsKeyword + /// [`enumSortTexts`]: super::keywords::EnumSortTextsKeyword + /// [`suggestSortText`]: super::keywords::SuggestSortTextKeyword + /// [`with_keyword()`]: ValidationOptions::with_keyword + /// [`with_resource()`]: ValidationOptions::with_resource + /// [`with_vscode_dialect()`]: VSCodeValidationOptionsExtensions::with_vscode_dialect + /// [`with_vscode_vocabulary()`]: VSCodeValidationOptionsExtensions::with_vscode_vocabulary + fn with_vscode_completion_keywords(self) -> ValidationOptions; + /// Registers the VS Code documentation keywords for use with a [`jsonschema::Validator`]. + /// + /// This function registers the following VS Code Keywords and the schema resources that + /// define them with the [`with_keyword()`] and [`with_resource()`] builder methods: + /// + /// - [`deprecationMessageKeyword`] + /// - [`enumDescriptionsKeyword`] + /// - [`markdownDescriptionKeyword`] + /// - [`markdownEnumDescriptionsKeyword`] + /// + /// Use this function when you only want to register a subset of VS Code vocabulary keywords. + /// If you are using the VS Code vocabulary in your own meta schema dialect, use the + /// [`with_vscode_vocabulary()`] method instead. If you are using the VS Code meta schema + /// dialect directly, use the [`with_vscode_dialect()`] method instead. + /// + /// [`deprecationMessageKeyword`]: super::keywords::DeprecationMessageKeyword + /// [`enumDescriptionsKeyword`]: super::keywords::EnumDescriptionsKeyword + /// [`markdownDescriptionKeyword`]: super::keywords::MarkdownDescriptionKeyword + /// [`markdownEnumDescriptionsKeyword`]: super::keywords::MarkdownEnumDescriptionsKeyword + /// [`enumSortTexts`]: super::keywords::EnumSortTextsKeyword + /// [`suggestSortText`]: super::keywords::SuggestSortTextKeyword + /// [`with_keyword()`]: ValidationOptions::with_keyword + /// [`with_resource()`]: ValidationOptions::with_resource + /// [`with_vscode_dialect()`]: VSCodeValidationOptionsExtensions::with_vscode_dialect + /// [`with_vscode_vocabulary()`]: VSCodeValidationOptionsExtensions::with_vscode_vocabulary + fn with_vscode_documentation_keywords(self) -> ValidationOptions; + /// Registers the VS Code error messaging keywords for use with a [`jsonschema::Validator`]. + /// + /// This function registers the following VS Code Keywords and the schema resources that + /// define them with the [`with_keyword()`] and [`with_resource()`] builder methods: + /// + /// - [`errorMessageKeyword`] + /// - [`patternErrorMessageKeyword`] + /// + /// Use this function when you only want to register a subset of VS Code vocabulary keywords. + /// If you are using the VS Code vocabulary in your own meta schema dialect, use the + /// [`with_vscode_vocabulary()`] method instead. If you are using the VS Code meta schema + /// dialect directly, use the [`with_vscode_dialect()`] method instead. + /// + /// [`errorMessageKeyword`]: super::keywords::ErrorMessageKeyword + /// [`patternErrorMessageKeyword`]: super::keywords::PatternErrorMessageKeyword + /// [`with_keyword()`]: ValidationOptions::with_keyword + /// [`with_resource()`]: ValidationOptions::with_resource + /// [`with_vscode_dialect()`]: VSCodeValidationOptionsExtensions::with_vscode_dialect + /// [`with_vscode_vocabulary()`]: VSCodeValidationOptionsExtensions::with_vscode_vocabulary + fn with_vscode_error_keywords(self) -> ValidationOptions; + /// Registers the VS Code parsing rules keywords for use with a [`jsonschema::Validator`]. + /// + /// This function registers the following VS Code Keywords and the schema resources that + /// define them with the [`with_keyword()`] and [`with_resource()`] builder methods: + /// + /// - [`allowCommentsKeyword`] + /// - [`allowTrailingCommasKeyword`] + /// + /// Use this function when you only want to register a subset of VS Code vocabulary keywords. + /// If you are using the VS Code vocabulary in your own meta schema dialect, use the + /// [`with_vscode_vocabulary()`] method instead. If you are using the VS Code meta schema + /// dialect directly, use the [`with_vscode_dialect()`] method instead. + /// + /// [`allowCommentsKeyword`]: super::keywords::AllowCommentsKeyword + /// [`allowTrailingCommasKeyword`]: super::keywords::AllowTrailingCommasKeyword + /// [`with_keyword()`]: ValidationOptions::with_keyword + /// [`with_resource()`]: ValidationOptions::with_resource + /// [`with_vscode_dialect()`]: VSCodeValidationOptionsExtensions::with_vscode_dialect + /// [`with_vscode_vocabulary()`]: VSCodeValidationOptionsExtensions::with_vscode_vocabulary + fn with_vscode_parsing_keywords(self) -> ValidationOptions; + /// Registers the VS Code vocabulary and keywords for use with a [`jsonschema::Validator`]. + /// + /// This function registers each of the VS Code keywords and the schema resources that define + /// them with the [`with_keyword()`] and [`with_resource()`] builder methods. It also registers + /// the canonical form of the [vocabulary schema] as a schema resource. + /// + /// This is a convenience method for registering the vocabulary and keywords. You don't need to + /// separately add the keywords or schema resources. Use this convenience method when you are + /// defining your own meta schema dialect that uses the VS Code vocabulary. + /// + /// If you are using the VS Code meta schema directly without extending the dialect for your + /// own purposes, use the [`with_vscode_dialect()`] method instead. + /// + /// [`with_keyword()`]: ValidationOptions::with_keyword + /// [`with_resource()`]: ValidationOptions::with_resource + /// [vocabulary schema]: super::vocabulary::VSCODE_VOCABULARY_SCHEMA_CANONICAL + /// [`with_vscode_dialect()`]: VSCodeValidationOptionsExtensions::with_vscode_dialect + fn with_vscode_vocabulary(self) -> ValidationOptions; + /// Registers the VS Code dialect meta schema, vocabulary, and keywords for use with a + /// [`jsonschema::Validator`]. + /// + /// This function registers each of the VS Code keywords and the schema resources that define + /// them with the [`with_keyword()`] and [`with_resource()`] builder methods. It also registers + /// the canonical form of the [vocabulary schema] and [dialect meta schema] as schema resources. + /// + /// This is a convenience method for registering the meta schema, vocabulary, and keywords + /// together. You don't need to separately add the keywords or schema resources. Use this + /// convenience method when you are using the VS Code meta schema dialect and vocabulary. + /// + /// If you're using your own dialect that includes the VS Code vocabulary, use the + /// [`with_vscode_vocabulary()`] method instead. + /// + /// [`with_keyword()`]: ValidationOptions::with_keyword + /// [`with_resource()`]: ValidationOptions::with_resource + /// [vocabulary schema]: super::vocabulary::VSCODE_VOCABULARY_SCHEMA_CANONICAL + /// [dialect meta schema]: super::dialect::VSCODE_DIALECT_SCHEMA_CANONICAL + /// [`with_vscode_vocabulary()`]: VSCodeValidationOptionsExtensions::with_vscode_vocabulary + fn with_vscode_dialect(self) -> ValidationOptions; +} + +impl VSCodeValidationOptionsExtensions for ValidationOptions { + fn with_vscode_keyword(self, keyword: VSCodeKeyword) -> Self { + keyword.register(self) + } + fn with_vscode_keywords(self) -> Self { + self + .with_vscode_keyword(VSCodeKeyword::AllowComments) + .with_vscode_keyword(VSCodeKeyword::AllowTrailingCommas) + .with_vscode_keyword(VSCodeKeyword::CompletionDetail) + .with_vscode_keyword(VSCodeKeyword::DefaultSnippets) + .with_vscode_keyword(VSCodeKeyword::DeprecationMessage) + .with_vscode_keyword(VSCodeKeyword::DoNotSuggest) + .with_vscode_keyword(VSCodeKeyword::EnumDescriptions) + .with_vscode_keyword(VSCodeKeyword::EnumDetails) + .with_vscode_keyword(VSCodeKeyword::EnumSortTexts) + .with_vscode_keyword(VSCodeKeyword::ErrorMessage) + .with_vscode_keyword(VSCodeKeyword::MarkdownDescription) + .with_vscode_keyword(VSCodeKeyword::MarkdownEnumDescriptions) + .with_vscode_keyword(VSCodeKeyword::PatternErrorMessage) + .with_vscode_keyword(VSCodeKeyword::SuggestSortText) + } + fn with_vscode_completion_keywords(self) -> Self { + self + .with_vscode_keyword(VSCodeKeyword::CompletionDetail) + .with_vscode_keyword(VSCodeKeyword::DefaultSnippets) + .with_vscode_keyword(VSCodeKeyword::DoNotSuggest) + .with_vscode_keyword(VSCodeKeyword::EnumDetails) + .with_vscode_keyword(VSCodeKeyword::EnumSortTexts) + .with_vscode_keyword(VSCodeKeyword::SuggestSortText) + } + fn with_vscode_documentation_keywords(self) -> ValidationOptions { + self + .with_vscode_keyword(VSCodeKeyword::DeprecationMessage) + .with_vscode_keyword(VSCodeKeyword::EnumDescriptions) + .with_vscode_keyword(VSCodeKeyword::MarkdownDescription) + .with_vscode_keyword(VSCodeKeyword::MarkdownEnumDescriptions) + } + fn with_vscode_error_keywords(self) -> ValidationOptions { + self + .with_vscode_keyword(VSCodeKeyword::ErrorMessage) + .with_vscode_keyword(VSCodeKeyword::PatternErrorMessage) + } + fn with_vscode_parsing_keywords(self) -> ValidationOptions { + self + .with_vscode_keyword(VSCodeKeyword::AllowComments) + .with_vscode_keyword(VSCodeKeyword::AllowTrailingCommas) + } + fn with_vscode_vocabulary(self) -> ValidationOptions { + self + .with_vscode_keywords() + .with_resource( + VSCodeVocabulary::SCHEMA_ID, + (**VSCODE_VOCABULARY_SCHEMA_RESOURCE_CANONICAL).clone() + ) + } + fn with_vscode_dialect(self) -> ValidationOptions { + self + .with_vscode_vocabulary() + .with_resource( + VSCodeDialect::SCHEMA_ID, + (**VSCODE_DIALECT_SCHEMA_RESOURCE_CANONICAL).clone() + ) + } +} diff --git a/lib/dsc-lib-jsonschema/src/vscode/vocabulary.rs b/lib/dsc-lib-jsonschema/src/vscode/vocabulary.rs new file mode 100644 index 000000000..ecafd8bf2 --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/vscode/vocabulary.rs @@ -0,0 +1,349 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::sync::{Arc, LazyLock}; + +use jsonschema::Resource; +use rust_i18n::t; +use schemars::{JsonSchema, Schema, SchemaGenerator, generate::SchemaSettings, json_schema}; + +use crate::vscode::keywords::{ + AllowCommentsKeyword, + AllowTrailingCommasKeyword, + CompletionDetailKeyword, + DefaultSnippetsKeyword, + DeprecationMessageKeyword, + DoNotSuggestKeyword, + EnumDescriptionsKeyword, + EnumDetailsKeyword, + EnumSortTextsKeyword, + ErrorMessageKeyword, + MarkdownDescriptionKeyword, + MarkdownEnumDescriptionsKeyword, + PatternErrorMessageKeyword, + SuggestSortTextKeyword, VSCodeKeywordDefinition +}; + +/// Defines the vocabulary schema that describes and validates VS Code's JSON Schema keywords. +/// +/// This vocabulary schema is based on the JSON Schema draft 2020-12. It includes an extended set of +/// annotation keywords that VS Code recognizes and uses to enhance the JSON authoring and editing +/// experience. This schema defines the vocabulary. It can't be used as a meta schema on its own. +/// This crate also defines [`VSCodeDialect`], which _does_ function as a general meta schema +/// you can use for authoring your own schemas. +/// +/// You can define your own meta schema dialect by including this vocabulary in your meta schema. +/// +/// The vocabulary schema struct defines associated constants and helper methods: +/// +/// - [`SCHEMA_ID`] defines the canonical URI to the meta schema specified in the schema's `$id` +/// keyword. +/// - [`SPEC_URI`] defines the absolute URI to the documentation for this vocabulary. +/// - [`REQUIRED`] defines whether a processing or validating tool needs to understand this +/// vocabulary to correctly interpret a schema that uses this vocabulary. +/// - [`json_schema_bundled()`] retrieves the bundled form of the meta schema with a custom +/// [`SchemaGenerator`]. +/// - [`json_schema_canonical()`] retrieves the canonical form of the meta schema with a custom +/// [`SchemaGenerator`]. +/// - [`schema_resource_bundled()`] retrieves the bundled form of the meta schema with a custom +/// [`SchemaGenerator`] as a [`Resource`]. +/// - [`schema_resource_canonical()`] retrieves the canonical form of the meta schema with a custom +/// [`SchemaGenerator`] as a [`Resource`]. +/// +/// For easier access to the schemas, consider using the following statics if you don't need to use +/// a custom generator: +/// +/// - [`VSCODE_VOCABULARY_SCHEMA_BUNDLED`] contains the bundled form of the meta schema with the +/// schema resources for the vocabulary and keywords included in the `$defs` keyword. +/// - [`VSCODE_VOCABULARY_SCHEMA_CANONICAL`] contains the canonical form of the meta schema without +/// the bundled schema resources for a smaller definition. +/// - [`VSCODE_VOCABULARY_SCHEMA_RESOURCE_BUNDLED`] contains the bundled form of the meta schema as +/// a [`Resource`] to simplify registering the resource with a [`jsonschema::Validator`]. +/// - [`VSCODE_VOCABULARY_SCHEMA_RESOURCE_CANONICAL`] contains the canonical form of the meta +/// schema as a [`Resource`]. +/// +/// [`SCHEMA_ID`]: Self::SCHEMA_ID +/// [`SPEC_URI`]: Self::SPEC_URI +/// [`REQUIRED`]: Self::REQUIRED +/// [`json_schema_bundled()`]: Self::json_schema_bundled +/// [`json_schema_canonical()`]: Self::json_schema_canonical +/// [`schema_resource_bundled()`]: Self::schema_resource_bundled +/// [`schema_resource_canonical()`]: Self::schema_resource_canonical +/// [`VSCodeDialect`]: super::dialect::VSCodeDialect +pub struct VSCodeVocabulary; + +impl VSCodeVocabulary { + /// Defines the canonical URI for the vocabulary schema's `$id` keyword. + pub const SCHEMA_ID: &str = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/vscode/v0/vocabulary.json"; + /// Defines the URI that points to the human-readable specification for the vocabulary. + pub const SPEC_URI: &str = "https://learn.microsoft.com/powershell/dsc/reference/schemas/vocabulary/vscode"; + /// Defines whether JSON Schema validating and processing tools _must_ understand this + /// vocabulary to correctly interpret a schema that uses this vocabulary. + /// + /// This vocabulary is _not_ required because it contains only annotation keywords, not + /// validation keywords. If a validating or processing tool doesn't understand the vocabulary, + /// it can safely ignore the keywords. + pub const REQUIRED: bool = false; + + /// Retrieves the bundled form of the vocabulary schema. + /// + /// The bundled form presents the vocabulary schema as a compound schema document with the + /// VS Code keyword schemas included under the `$defs` keyword. Use this form of the schema + /// when you need every schema resource included in a single document. + /// + /// This function requires you to pass a [`SchemaGenerator`] to retrieve the schema. The + /// definition for the vocabulary schema is static, but you can use custom transforms with your + /// [`SchemaGenerator`] to modify the schema if needed. If you want to use the default + /// representation of the bundled vocabulary schema, use [`VSCODE_VOCABULARY_SCHEMA_BUNDLED`]. + /// + /// You can also use the [`json_schema_canonical()`] method to retrieve the canonical + /// form of the vocabulary schema without the bundled schema resources or + /// [`VSCODE_VOCABULARY_SCHEMA_CANONICAL`] to use the default representation of the canonical + /// vocabulary schema. + /// + /// [`json_schema_canonical()`]: Self::json_schema_canonical + pub fn json_schema_bundled(generator: &mut schemars::SchemaGenerator) -> Schema { + Self::json_schema(generator) + } + + /// Retrieves the canonical form of the vocabulary schema. + /// + /// The canonical form presents the vocabulary schema without bundling the keyword schemas + /// under the `$defs` keyword. Use this form of the schema when you can rely on retrieving the + /// other schemas from network or other methods. + /// + /// This function requires you to pass a [`SchemaGenerator`] to retrieve the schema. The + /// definition for the vocabulary schema is static, but you can use custom transforms with your + /// [`SchemaGenerator`] to modify the schema if needed. If you want to use the default + /// representation of the canonical vocabulary schema, use + /// [`VSCODE_VOCABULARY_SCHEMA_CANONICAL`]. + /// + /// You can also use the [`json_schema_bundled()`] method to retrieve the bundled form of the + /// vocabulary schema with the schema resources bundled under the `$defs` keyword or + /// [`VSCODE_VOCABULARY_SCHEMA_BUNDLED`] to use the default representation of the bundled + /// vocabulary schema. + /// + /// [`json_schema_bundled()`]: Self::json_schema_bundled + pub fn json_schema_canonical(generator: &mut schemars::SchemaGenerator) -> Schema { + let mut schema = Self::json_schema(generator); + schema.remove("$defs"); + schema + } + + /// Retrieves the bundled form of the vocabulary schema as a [`Resource`] so you can include + /// it in the registered resources for a [`jsonschema::Validator`] using the [`with_resource()`] + /// or [`with_resources()`] methods on the [`jsonschema::ValidationOptions`] builder. + /// + /// The bundled form presents the vocabulary schema as a compound schema document with the + /// VS Code keyword schemas included under the `$defs` keyword. Use this form of the schema + /// when you need every schema resource included in a single document. + /// + /// This function requires you to pass a [`SchemaGenerator`] to retrieve the schema. The + /// definition for the vocabulary schema is static, but you can use custom transforms with your + /// [`SchemaGenerator`] to modify the schema if needed. If you want to use the default + /// representation of the bundled vocabulary schema, use + /// [`VSCODE_VOCABULARY_SCHEMA_RESOURCE_BUNDLED`]. + /// + /// You can also use the [`schema_resource_canonical()`] method to retrieve the canonical + /// form of the vocabulary schema without the bundled schema resources or + /// [`VSCODE_VOCABULARY_SCHEMA_RESOURCE_CANONICAL`] to use the default representation of the + /// canonical vocabulary schema. + /// + /// # Panics + /// + /// This method panics if the schema is malformed and can't be converted into a [`Resource`]. + /// + /// In practice, you should never see a panic from this method because the crate's test suite + /// checks for this failure mode. + /// + /// [`schema_resource_canonical()`]: Self::schema_resource_canonical + /// [`with_resource()`]: jsonschema::ValidationOptions::with_resource + /// [`with_resources()`]: jsonschema::ValidationOptions::with_resources + pub fn schema_resource_bundled(generator: &mut schemars::SchemaGenerator) -> Resource { + Resource::from_contents(Self::json_schema(generator).to_value()).unwrap() + } + + /// Retrieves the bundled form of the vocabulary schema as a [`Resource`] so you can include + /// it in the registered resources for a [`jsonschema::Validator`] using the [`with_resource()`] + /// or [`with_resources()`] methods on the [`jsonschema::ValidationOptions`] builder. + /// + /// The canonical form presents the vocabulary schema without bundling the VS Code keyword + /// schemas under the `$defs` keyword. Use this form of the schema when you can rely on + /// retrieving the other schemas from network or other methods. + /// + /// This function requires you to pass a [`SchemaGenerator`] to retrieve the schema. The + /// definition for the vocabulary schema is static, but you can use custom transforms with your + /// [`SchemaGenerator`] to modify the schema if needed. If you want to use the default + /// representation of the canonical vocabulary schema, use + /// [`VSCODE_VOCABULARY_SCHEMA_RESOURCE_CANONICAL`]. + /// + /// You can also use the [`schema_resource_bundled()`] method to retrieve the bundled form of + /// the vocabulary schema without the bundled schema resources or + /// [`VSCODE_VOCABULARY_SCHEMA_RESOURCE_BUNDLED`] to use the default representation of the + /// bundled vocabulary schema. + /// + /// # Panics + /// + /// This method panics if the schema is malformed and can't be converted into a [`Resource`]. + /// + /// In practice, you should never see a panic from this method because the crate's test suite + /// checks for this failure mode. + /// + /// [`schema_resource_bundled()`]: Self::schema_resource_bundled + /// [`with_resource()`]: jsonschema::ValidationOptions::with_resource + /// [`with_resources()`]: jsonschema::ValidationOptions::with_resources + pub fn schema_resource_canonical(generator: &mut schemars::SchemaGenerator) -> Resource { + Resource::from_contents(Self::json_schema_canonical(generator).to_value()).unwrap() + } +} + +impl JsonSchema for VSCodeVocabulary { + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": Self::SCHEMA_ID, + "$vocabulary": { + Self::SPEC_URI: Self::REQUIRED + }, + "$dynamicAnchor": "meta", + "title": t!("vscode.dialect.title"), + "description": t!("vscode.dialect.description"), + "markdownDescription": t!("vscode.dialect.markdownDescription"), + "type": ["object", "boolean"], + "properties": { + "allowComments": AllowCommentsKeyword::schema_reference(), + "allowTrailingCommas": AllowTrailingCommasKeyword::schema_reference(), + "completionDetail": CompletionDetailKeyword::schema_reference(), + "defaultSnippets": DefaultSnippetsKeyword::schema_reference(), + "deprecationMessage": DeprecationMessageKeyword::schema_reference(), + "doNotSuggest": DoNotSuggestKeyword::schema_reference(), + "enumDescriptions": EnumDescriptionsKeyword::schema_reference(), + "enumDetails": EnumDetailsKeyword::schema_reference(), + "enumSortTexts": EnumSortTextsKeyword::schema_reference(), + "errorMessage": ErrorMessageKeyword::schema_reference(), + "markdownDescription": MarkdownDescriptionKeyword::schema_reference(), + "markdownEnumDescriptions": MarkdownEnumDescriptionsKeyword::schema_reference(), + "patternErrorMessage": PatternErrorMessageKeyword::schema_reference(), + "suggestSortText": SuggestSortTextKeyword::schema_reference() + }, + "$defs": { + AllowCommentsKeyword::KEYWORD_ID: AllowCommentsKeyword::json_schema(generator), + AllowTrailingCommasKeyword::KEYWORD_ID: AllowTrailingCommasKeyword::json_schema(generator), + CompletionDetailKeyword::KEYWORD_ID: CompletionDetailKeyword::json_schema(generator), + DefaultSnippetsKeyword::KEYWORD_ID: DefaultSnippetsKeyword::json_schema(generator), + DeprecationMessageKeyword::KEYWORD_ID: DeprecationMessageKeyword::json_schema(generator), + DoNotSuggestKeyword::KEYWORD_ID: DoNotSuggestKeyword::json_schema(generator), + EnumDescriptionsKeyword::KEYWORD_ID: EnumDescriptionsKeyword::json_schema(generator), + EnumDetailsKeyword::KEYWORD_ID: EnumDetailsKeyword::json_schema(generator), + EnumSortTextsKeyword::KEYWORD_ID: EnumSortTextsKeyword::json_schema(generator), + ErrorMessageKeyword::KEYWORD_ID: ErrorMessageKeyword::json_schema(generator), + MarkdownDescriptionKeyword::KEYWORD_ID: MarkdownDescriptionKeyword::json_schema(generator), + MarkdownEnumDescriptionsKeyword::KEYWORD_ID: MarkdownEnumDescriptionsKeyword::json_schema(generator), + PatternErrorMessageKeyword::KEYWORD_ID: PatternErrorMessageKeyword::json_schema(generator), + SuggestSortTextKeyword::KEYWORD_ID: SuggestSortTextKeyword::json_schema(generator), + } + }) + } + + fn schema_name() -> std::borrow::Cow<'static, str> { + Self::SCHEMA_ID.into() + } +} + +/// Contains the bundled form of the VS Code vocabulary schema. +/// +/// The bundled form presents the vocabulary schema as a compound schema document with the +/// VS Code keyword schemas included under the `$defs` keyword. Use this form of the schema +/// when you need every schema resource included in a single document. +/// +/// You can also use [`VSCODE_VOCABULARY_SCHEMA_CANONICAL`] to retrieve the canonical form of the +/// vocabulary schema without the bundled schema resources. +/// +/// This representation of the schema is generated with the default [`SchemaSettings`] for +/// JSON Schema draft 2020-12. To retrieve the bundled schema with custom generator settings, +/// use the [`json_schema_bundled()`] method. +/// +/// [`json_schema_bundled()`]: VSCodeVocabulary::json_schema_bundled +pub static VSCODE_VOCABULARY_SCHEMA_BUNDLED: LazyLock> = LazyLock::new(|| { + let generator = &mut SchemaGenerator::new( + SchemaSettings::draft2020_12() + ); + + Arc::from(VSCodeVocabulary::json_schema_bundled(generator)) +}); + +/// Contains the canonical form of the VS Code vocabulary schema. +/// +/// The canonical form presents the vocabulary schema without bundling the VS Code keyword +/// schemas under the `$defs` keyword. Use this form of the schema when you can rely on +/// retrieving the other schemas from network or other methods. +/// +/// You can also use [`VSCODE_VOCABULARY_SCHEMA_BUNDLED`] to retrieve the bundled form of the +/// vocabulary schema with the schema resources bundled under the `$defs` keyword. +/// +/// This representation of the schema is generated with the default [`SchemaSettings`] for +/// JSON Schema draft 2020-12. To retrieve the canonical schema with custom generator settings, +/// use the [`json_schema_canonical()`] method, which takes a [`SchemaGenerator`] as input. +/// +/// [`json_schema_canonical()`]: VSCodeVocabulary::json_schema_canonical +pub static VSCODE_VOCABULARY_SCHEMA_CANONICAL: LazyLock> = LazyLock::new(|| { + let generator = &mut SchemaGenerator::new( + SchemaSettings::draft2020_12() + ); + + Arc::from(VSCodeVocabulary::json_schema_canonical(generator)) +}); + +/// Contains the bundled form of the VS Code vocabulary schema as a [`Resource`] so you can +/// include it in the registered resources for a [`jsonschema::Validator`] using the +/// [`with_resource()`] or [`with_resources()`] methods on the +/// [`jsonschema::ValidationOptions`] builder. +/// +/// The bundled form presents the vocabulary schema as a compound schema document with the +/// VS Code keyword schemas included under the `$defs` keyword. Use this form of the schema +/// when you need every schema resource included in a single document. +/// +/// You can also use [`VSCODE_VOCABULARY_SCHEMA_RESOURCE_CANONICAL`] to retrieve the canonical form +/// of the vocabulary schema without the bundled schema resources. +/// +/// This representation of the schema is generated with the default [`SchemaSettings`] for +/// JSON Schema draft 2020-12. To retrieve the bundled schema with custom generator settings, +/// use the [`json_schema_bundled()`] method. +/// +/// [`with_resource()`]: jsonschema::ValidationOptions::with_resource +/// [`with_resources()`]: jsonschema::ValidationOptions::with_resources +/// [`json_schema_bundled()`]: VSCodeVocabulary::json_schema_bundled +pub static VSCODE_VOCABULARY_SCHEMA_RESOURCE_BUNDLED: LazyLock> = LazyLock::new(|| { + let generator = &mut SchemaGenerator::new( + SchemaSettings::draft2020_12() + ); + + Arc::from(VSCodeVocabulary::schema_resource_bundled(generator)) +}); + +/// Contains the canonical form of the VS Code vocabulary schema as a [`Resource`] so you can +/// include it in the registered resources for a [`jsonschema::Validator`] using the +/// [`with_resource()`] or [`with_resources()`] methods on the +/// [`jsonschema::ValidationOptions`] builder. +/// +/// The canonical form presents the meta schema without bundling the VS Code keyword schemas +/// under the `$defs` keyword. Use this form of the schema when you can rely on retrieving the +/// other schemas from network or other methods. +/// +/// You can also use [`VSCODE_VOCABULARY_SCHEMA_RESOURCE_BUNDLED`] to retrieve the bundled form of +/// the vocabulary schema with the schema resources bundled under the `$defs` keyword. +/// +/// This representation of the schema is generated with the default [`SchemaSettings`] for +/// JSON Schema draft 2020-12. To retrieve the bundled schema with custom generator settings, +/// use the [`json_schema_canonical()`] method. +/// +/// [`with_resource()`]: jsonschema::ValidationOptions::with_resource +/// [`with_resources()`]: jsonschema::ValidationOptions::with_resources +/// [`json_schema_canonical()`]: VSCodeVocabulary::json_schema_canonical +pub static VSCODE_VOCABULARY_SCHEMA_RESOURCE_CANONICAL: LazyLock> = LazyLock::new(|| { + let generator = &mut SchemaGenerator::new( + SchemaSettings::draft2020_12() + ); + + Arc::from(VSCodeVocabulary::schema_resource_canonical(generator)) +}); diff --git a/lib/dsc-lib-jsonschema/src/vscode/vscode_schema_extensions.rs b/lib/dsc-lib-jsonschema/src/vscode/vscode_schema_extensions.rs deleted file mode 100644 index bcacd953c..000000000 --- a/lib/dsc-lib-jsonschema/src/vscode/vscode_schema_extensions.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Provides helper functions for working with VS Code's extended JSON Schema keywords with -//! [`schemars::Schema`] instances. -//! -//! The `get_keyword_as_*` functions simplify retrieving the value of a keyword for a given type. -//! If the schema defines the keyword with the expected type, those functions return a reference to -//! that value as the correct type. If the keyword doesn't exist or has the wrong value type, the -//! functions return [`None`]. -//! -//! The rest of the utility methods work with specific keywords, like `$id` and `$defs`. - -use core::{clone::Clone, iter::Iterator, option::Option::None}; -use std::string::String; - -use schemars::Schema; -use serde_json::{Map, Number, Value}; -use url::{Position, Url}; - -type Array = Vec; -type Object = Map; - -pub trait VSCodeSchemaExtensions { - fn has_markdown_description(&self) -> bool; -} - -impl VSCodeSchemaExtensions for Schema { - fn has_markdown_description(&self) -> bool { - self.get("markdownDescription").is_some() - } -} \ No newline at end of file From 187f036cde45b05da98fcd90b33ebd36cb4101af Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Wed, 19 Nov 2025 16:51:56 -0600 Subject: [PATCH 6/6] (MAINT) Fix broken links for rust docs --- lib/dsc-lib/src/schemas/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/dsc-lib/src/schemas/mod.rs b/lib/dsc-lib/src/schemas/mod.rs index 23658308a..85d1d1a88 100644 --- a/lib/dsc-lib/src/schemas/mod.rs +++ b/lib/dsc-lib/src/schemas/mod.rs @@ -70,6 +70,10 @@ impl SchemaForm { /// The extension for [`Bundled`] and [`Canonical`] schemas is `.json` /// /// The extension for [`VSCode`] schemas is `.vscode.json` + /// + /// [`Bundled`]: SchemaForm::Bundled + /// [`Canonical`]: SchemaForm::Canonical + /// [`VSCode`]: SchemaForm::VSCode #[must_use] pub fn to_extension(&self) -> String { match self { @@ -83,6 +87,10 @@ impl SchemaForm { /// The [`Bundled`] and [`VSCode`] schemas are always published in the `bundled` folder /// immediately beneath the version folder. The [`Canonical`] schemas use the folder path /// as defined for that schema. + /// + /// [`Bundled`]: SchemaForm::Bundled + /// [`Canonical`]: SchemaForm::Canonical + /// [`VSCode`]: SchemaForm::VSCode #[must_use] pub fn to_folder_prefix(&self) -> String { match self {