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 550358f03..d639ee711 100644 --- a/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs +++ b/lib/dsc-lib-jsonschema/src/schema_utility_extensions.rs @@ -10,16 +10,14 @@ //! //! 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 core::{clone::Clone, convert::TryInto, iter::Iterator, option::Option::None}; +use std::{collections::HashSet, string::String}; +use std::vec::Vec; 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 +66,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 +113,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`]. /// @@ -330,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!({ @@ -343,7 +340,7 @@ pub trait SchemaUtilityExtensions { /// ); /// /// assert_eq!( - /// schema.get_keyword_as_object("enum"), + /// schema.get_keyword_as_object("properties"), /// None /// ) /// ``` @@ -537,6 +534,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 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 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`]. /// @@ -585,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`]. @@ -749,10 +910,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" /// }); @@ -764,7 +924,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"), @@ -773,9 +933,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 @@ -788,10 +948,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" /// }); @@ -803,7 +962,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"), @@ -812,9 +971,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: /// @@ -834,10 +993,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" /// }); @@ -850,17 +1008,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) /// ); /// ``` /// @@ -869,10 +1027,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" /// }); @@ -884,12 +1041,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!( @@ -897,9 +1054,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: /// @@ -925,10 +1082,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" /// }); @@ -940,13 +1096,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 /// ); /// ``` /// @@ -955,10 +1111,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" /// }); @@ -970,7 +1125,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!( @@ -982,7 +1137,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. /// @@ -999,20 +1154,20 @@ 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"), @@ -1119,9 +1274,9 @@ pub trait SchemaUtilityExtensions { /// ``` 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 - /// 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 /// @@ -1130,7 +1285,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" /// }); @@ -1142,10 +1297,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. /// @@ -1155,10 +1310,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" /// }); @@ -1170,18 +1324,149 @@ 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>; + + //************************ $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 { - 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) } @@ -1222,6 +1507,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) @@ -1232,7 +1525,7 @@ impl SchemaUtilityExtensions for Schema { 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<&Object> { + fn get_defs_subschema_from_id(&self, id: &str) -> Option<&Schema> { let defs = self.get_defs()?; for def in defs.values() { @@ -1240,14 +1533,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() { @@ -1255,17 +1548,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(); @@ -1282,10 +1577,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(); @@ -1305,8 +1602,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) @@ -1316,7 +1613,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())); @@ -1326,7 +1623,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. @@ -1344,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") } @@ -1375,14 +1698,134 @@ impl SchemaUtilityExtensions for Schema { 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<&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()) + } + 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/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 96% 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 index b08aa723e..8c845da27 100644 --- a/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions/mod.rs +++ b/lib/dsc-lib-jsonschema/src/tests/schema_utility_extensions.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 { @@ -335,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; @@ -382,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) ); } } @@ -397,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; @@ -444,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; @@ -518,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() { @@ -539,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() { @@ -560,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; @@ -634,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() { @@ -656,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() { @@ -677,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 ); } } @@ -883,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; @@ -916,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!({ @@ -926,7 +927,7 @@ test_cases_for_get_keyword_as_mut!( }); assert_eq!( schema.get_property_subschema("foo").unwrap(), - property.as_object().unwrap() + property ) } } @@ -936,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; @@ -969,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!({ @@ -979,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 ) } } 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/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 new file mode 100644 index 000000000..69c84417b --- /dev/null +++ b/lib/dsc-lib-jsonschema/src/transforms/idiomaticize_externally_tagged_enum.rs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::cmp::PartialEq; +use schemars::Schema; +use serde_json::{self, json, Map, Value}; + +use crate::vscode::keywords::VSCodeKeyword; + +/// 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 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()); + } + } + // 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/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 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; 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 {