diff --git a/Cargo.lock b/Cargo.lock index 01b9a9d29..3e69cdeb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -467,6 +467,16 @@ dependencies = [ "darling_macro 0.21.3", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -495,6 +505,19 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" @@ -517,6 +540,17 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -711,6 +745,7 @@ dependencies = [ name = "dsc-lib-jsonschema" version = "0.0.0" dependencies = [ + "dsc-lib-jsonschema-macros", "jsonschema", "pretty_assertions", "regex", @@ -724,6 +759,16 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "dsc-lib-jsonschema-macros" +version = "0.0.0" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dsc-lib-osinfo" version = "1.0.0" @@ -2096,9 +2141,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -2169,9 +2214,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -2860,9 +2905,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d2949b6f9..57c066b96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "dsc", "lib/dsc-lib", "lib/dsc-lib-jsonschema", + "lib/dsc-lib-jsonschema-macros", "resources/dscecho", "lib/dsc-lib-osinfo", "resources/osinfo", @@ -29,6 +30,7 @@ default-members = [ "dsc", "lib/dsc-lib", "lib/dsc-lib-jsonschema", + "lib/dsc-lib-jsonschema-macros", "resources/dscecho", "lib/dsc-lib-osinfo", "resources/osinfo", @@ -54,6 +56,7 @@ Windows = [ "dsc", "lib/dsc-lib", "lib/dsc-lib-jsonschema", + "lib/dsc-lib-jsonschema-macros", "resources/dscecho", "lib/dsc-lib-osinfo", "resources/osinfo", @@ -74,6 +77,7 @@ macOS = [ "dsc", "lib/dsc-lib", "lib/dsc-lib-jsonschema", + "lib/dsc-lib-jsonschema-macros", "resources/dscecho", "lib/dsc-lib-osinfo", "resources/osinfo", @@ -91,6 +95,7 @@ Linux = [ "dsc", "lib/dsc-lib", "lib/dsc-lib-jsonschema", + "lib/dsc-lib-jsonschema-macros", "resources/dscecho", "lib/dsc-lib-osinfo", "resources/osinfo", @@ -130,6 +135,8 @@ clap_complete = { version = "4.5" } crossterm = { version = "0.29" } # dsc ctrlc = { version = "3.5" } +# dsc-lib-jsonschema-macros +darling = { version = "0.23" } # dsc-lib derive_builder = { version = "0.20" } # dsc, dsc-lib @@ -150,6 +157,10 @@ num-traits = { version = "0.2" } os_info = { version = "3.13" } # dsc, dsc-lib path-absolutize = { version = "3.1" } +# dsc-lib-jsonschema-macros +proc-macro2 = { version = "1.0" } +# dsc-lib-jsonschema-macros +quote = { version = "1.0" } # dsc, dsc-lib regex = { version = "1.12" } # registry, dsc-lib-registry @@ -170,6 +181,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } # dsc, dsc-lib, y2j serde_yaml = { version = "0.9" } +# dsc-lib-jsonschema-macros +syn = { version = "2.0" } # dsc, y2j syntect = { version = "5.3", features = ["default-fancy"], default-features = false } # dsc, process @@ -218,6 +231,7 @@ pretty_assertions = { version = "1.4.1" } # Workspace crates as dependencies dsc-lib = { path = "lib/dsc-lib" } dsc-lib-jsonschema = { path = "lib/dsc-lib-jsonschema" } +dsc-lib-jsonschema-macros = { path = "lib/dsc-lib-jsonschema-macros" } dsc-lib-osinfo = { path = "lib/dsc-lib-osinfo" } dsc-lib-security_context = { path = "lib/dsc-lib-security_context" } tree-sitter-dscexpression = { path = "grammars/tree-sitter-dscexpression" } diff --git a/build.data.json b/build.data.json index 91725adb4..a0942b9fc 100644 --- a/build.data.json +++ b/build.data.json @@ -214,6 +214,12 @@ "RelativePath": "lib/dsc-lib-jsonschema", "IsRust": true }, + { + "Name": "dsc-lib-jsonschema-macros", + "Kind": "Library", + "RelativePath": "lib/dsc-lib-jsonschema-macros", + "IsRust": true + }, { "Name": "dsc-lib-osinfo", "Kind": "Library", diff --git a/lib/dsc-lib-jsonschema-macros/.project.data.json b/lib/dsc-lib-jsonschema-macros/.project.data.json new file mode 100644 index 000000000..08285c004 --- /dev/null +++ b/lib/dsc-lib-jsonschema-macros/.project.data.json @@ -0,0 +1,5 @@ +{ + "Name": "dsc-lib-jsonschema-macros", + "Kind": "Library", + "IsRust": true +} diff --git a/lib/dsc-lib-jsonschema-macros/Cargo.toml b/lib/dsc-lib-jsonschema-macros/Cargo.toml new file mode 100644 index 000000000..1423c69a1 --- /dev/null +++ b/lib/dsc-lib-jsonschema-macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dsc-lib-jsonschema-macros" +version = "0.0.0" +edition = "2024" + +[lib] +name = "dsc_lib_jsonschema_macros" +path = "src/lib.rs" +proc-macro = true +doctest = false # Disable doc tests by default for compilation speed + +[dependencies] +darling = { workspace = true } +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } diff --git a/lib/dsc-lib-jsonschema-macros/src/ast/string_or_expr.rs b/lib/dsc-lib-jsonschema-macros/src/ast/string_or_expr.rs new file mode 100644 index 000000000..7adc9c3ab --- /dev/null +++ b/lib/dsc-lib-jsonschema-macros/src/ast/string_or_expr.rs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/// Simplifies passing either a literal string or an expression that evaluates to a string for the +/// annotation fields. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum StringOrExpr { + Expr(Box), + String(String), +} + +impl darling::FromMeta for StringOrExpr { + fn from_expr(expr: &syn::Expr) -> darling::Result { + Ok(Self::Expr(Box::new(expr.clone()))) + } + fn from_string(value: &str) -> darling::Result { + Ok(Self::String(value.to_string())) + } + fn from_value(value: &syn::Lit) -> darling::Result { + match value { + syn::Lit::Str(v) => Ok(Self::String(v.value())), + _ => Err(darling::Error::unexpected_lit_type(value)), + } + } +} + +impl quote::ToTokens for StringOrExpr { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Self::Expr(expr) => expr.to_tokens(tokens), + Self::String(str) => str.to_tokens(tokens), + } + } +} diff --git a/lib/dsc-lib-jsonschema-macros/src/derive/dsc_repo_schema.rs b/lib/dsc-lib-jsonschema-macros/src/derive/dsc_repo_schema.rs new file mode 100644 index 000000000..c46fb6f31 --- /dev/null +++ b/lib/dsc-lib-jsonschema-macros/src/derive/dsc_repo_schema.rs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use darling::{FromDeriveInput, FromMeta}; +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, Ident, Path, parse_macro_input}; + +use crate::ast::StringOrExpr; + +/// Defines the top-level input for the derive macro as `dsc_repo_schema`. +#[derive(Clone, FromDeriveInput)] +#[darling(attributes(dsc_repo_schema))] +struct DscRepoSchemaReceiver { + /// Defines the base name for the schema file, like `exist`. Must be a literal string. + pub base_name: String, + /// Defines the folder path relative to the version folder for the schema file, like + /// `resource/properties`. Must be a literal string. + pub folder_path: String, + /// Defines whether the schema should be bundled. Root schemas should always be bundled, but + /// other schemas may not be. + #[darling(default)] + pub should_bundle: bool, + /// Defines the field for the struct that is used as the `$schema` property. Typically only + /// defined for root schemas. + #[darling(default)] + pub schema_field: Option, +} + +/// Defines the suboptions for the `schema_field` option on the derive macro. +#[derive(FromMeta, Clone)] +#[darling(derive_syn_parse)] +struct DscRepoSchemaField { + /// Defines the field that is used as the `$schema` property. This should be the name of field, + /// like `schema_version`. It's used to generate the validation function. + pub name: Path, + /// Defines the `title` keyword for the `$schema` property. + #[darling(default)] + pub title: Option, + /// Defines the `description` keyword for the `$schema` property. + #[darling(default)] + pub description: Option, + /// Defines the `markdownDescription` keyword for the `$schema` property. + #[darling(default)] + pub markdown_description: Option, +} + +/// Implements the `DscRepoSchema` trait for a type with the derive macro. +pub(crate) fn dsc_repo_schema_impl(input: TokenStream) -> TokenStream { + // Parse input token stream as `DeriveInput` + let original = parse_macro_input!(input as DeriveInput); + + // Destructure the input to get the identity of the type the macro was used on. + let DeriveInput { ident, .. } = original.clone(); + + // Parse the attribute at the top level of the type to retrieve the necessary information. + let args = match DscRepoSchemaReceiver::from_derive_input(&original) { + Ok(v) => v, + Err(e) => { + // Return the error as a token stream for better diagnostics. + return TokenStream::from(e.write_errors()); + } + }; + + let mut output = quote!(); + + if let Some(schema_field) = args.schema_field { + output.extend(generate_with_schema_field( + ident, + args.base_name, + args.folder_path, + args.should_bundle, + schema_field + )); + } else { + output.extend(generate_without_schema_field( + ident, + args.base_name, + args.folder_path, + args.should_bundle + )); + } + + output.into() +} + + +/// Generates the minimal implementation of the `DscRepoSchema` trait when the type doesn't define +/// the `schema_field` option in the macro attribute. +fn generate_without_schema_field( + ident: Ident, + base_name: String, + folder_path: String, + should_bundle: bool +) -> proc_macro2::TokenStream { + quote!( + #[automatically_derived] + impl DscRepoSchema for #ident { + const SCHEMA_FILE_BASE_NAME: &'static str = #base_name; + const SCHEMA_FOLDER_PATH: &'static str = #folder_path; + const SCHEMA_SHOULD_BUNDLE: bool = #should_bundle; + + fn schema_property_metadata() -> schemars::Schema { + schemars::json_schema!({}) + } + } + ) +} + +/// Generates the implementation of the `DscRepoSchema` trait for a type that defines the +/// `schema_field` option in the macro attribute. This is typically used for root schemas, like the +/// configuration document or resource manifest. +/// +/// It generates the trait implementation with the associated constants, the metadata for the field, +/// and the schema URI validation function. +fn generate_with_schema_field( + ident: Ident, + base_name: String, + folder_path: String, + should_bundle: bool, + schema_field: DscRepoSchemaField +) -> proc_macro2::TokenStream { + let schema_property_metadata = generate_schema_property_metadata_fn(&schema_field); + let field = schema_field.name; + quote!( + #[automatically_derived] + impl DscRepoSchema for #ident { + const SCHEMA_FILE_BASE_NAME: &'static str = #base_name; + const SCHEMA_FOLDER_PATH: &'static str = #folder_path; + const SCHEMA_SHOULD_BUNDLE: bool = #should_bundle; + + #schema_property_metadata + + fn validate_schema_uri(&self) -> Result<(), dsc_lib_jsonschema::dsc_repo::UnrecognizedSchemaUri> { + if Self::is_recognized_schema_uri(&self.#field) { + Ok(()) + } else { + Err(dsc_lib_jsonschema::dsc_repo::UnrecognizedSchemaUri( + self.#field.clone(), + Self::recognized_schema_uris(), + )) + } + } + } + ) +} + +/// Generates the implementation for the `schema_property_metadata` trait function, inserting the +/// defined keywords into the schema. +fn generate_schema_property_metadata_fn(schema_field: &DscRepoSchemaField) -> proc_macro2::TokenStream { + let mut schema_body = quote!(); + let fields = schema_field.clone(); + + if let Some(title) = fields.title { + schema_body.extend(quote!{"title": #title,}); + } + if let Some(description) = fields.description { + schema_body.extend(quote!{"description": #description,}); + } + if let Some(markdown_description) = fields.markdown_description { + schema_body.extend(quote!{"markdownDescription": #markdown_description,}); + } + + quote!{ + fn schema_property_metadata() -> schemars::Schema { + schemars::json_schema!({ + #schema_body + }) + } + } +} diff --git a/lib/dsc-lib-jsonschema-macros/src/lib.rs b/lib/dsc-lib-jsonschema-macros/src/lib.rs new file mode 100644 index 000000000..ad01e353e --- /dev/null +++ b/lib/dsc-lib-jsonschema-macros/src/lib.rs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Provides procedural macros for dsc-lib-jsonschema. + +extern crate proc_macro; + +use proc_macro::TokenStream; + +pub(crate) mod derive { + pub(crate) mod dsc_repo_schema; +} + +pub(crate) mod ast { + mod string_or_expr; + pub(crate) use string_or_expr::StringOrExpr; +} + +#[cfg(test)] mod test { + #[cfg(test)] mod ast { + #[cfg(test)] mod string_or_expr; + } +} + +/// Derives the `DscRepoSchema` trait. +/// +/// This is only intended for use in types defined within the `PowerShell/Dsc` repository. It +/// simplifies defining the schemas and extracting them for publishing. +/// +/// You can use this derive macro on structs and enums. +/// +/// # Required Attributes +/// +/// - `base_name`: The base name for the schema file, like `exist`. +/// - `folder_path`: The folder path relative to the version folder, like `resource/properties`. +/// +/// # Optional Attributes +/// +/// - `should_bundle`: Whether the schema should be bundled. A bundled schema includes every +/// referenced schema in the `$defs` keyword to minimize network calls. This typically only +/// applies to root schemas, like for a configuration document. +/// +/// When this attribute isn't specified, the default is `false`. +/// - `schema_field`: Settings for the `$schema` property of a root schema. This option has the +/// following fields: +/// +/// - `name` (required) - Must be the literal name of the struct field that maps to the `$schema` +/// property, like `schema` or `schema_version`. The derive macro uses this to validate the +/// value against the recognized schema URIs for the type. +/// - `title` (optional) - Defines the `title` keyword for the `$schema` property subschema. You +/// can specify the value as a string literal or an expression that resolves to a string. +/// - `description` (optional) - Defines the `description` keyword for the `$schema` property +/// subschema. You can specify the value as a string literal or an expression that resolves to +/// a string. +/// - `markdown_description` (optional) - Defines the `markdownDescription` keyword for the +/// `$schema` property subschema. You can specify the value as a string literal or an +/// expression that resolves to a string. +/// +/// # Examples +/// +/// The following examples show how you can derive the `DscRepoSchema` trait for different types. +/// +/// ## Without schema field or bundling +/// +/// In this example, the `Resource` struct derives the `DscRepoSchema` trait without bundling or a +/// schema field. Given the values for `base_name` and `folder_path`, the canonical URI to this +/// schema for the `v3` version folder is `https://aka.ms/dsc/schemas/v3/config/document.resource.json`. +/// +/// ```ignore +/// #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +/// #[serde(deny_unknown_fields)] +/// #[dsc_repo_schema(base_name = "document.resource", folder_path = "config")] +/// struct Resource { +/// // ... +/// } +/// ``` +/// +/// This is typically all that is required for types that don't define a root schema. +/// +/// ## With schema field and bundling +/// +/// In this example, the `Configuration` struct is a root schema. The `dsc_repo_schema` attribute +/// defines information for the schema field as well as the root document. +/// +/// ```ignore +/// #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +/// #[serde(deny_unknown_fields)] +/// #[dsc_repo_schema( +/// base_name = "document", +/// folder_path = "config", +/// should_bundle = true, +/// schema_field( +/// name = schema, +/// title = t!("schemas.configuration.document.properties.$schema.title"), +/// description = t!("schemas.configuration.document.properties.$schema.description"), +/// markdown_description = t!("schemas.configuration.document.properties.$schema.markdownDescription") +/// ) +/// )] +/// pub struct Configuration { +/// #[serde(rename = "$schema")] +/// #[schemars(schema_with = "Configuration::recognized_schema_uris_subschema")] +/// pub schema: String, +/// // ... +/// } +/// ``` +#[proc_macro_derive(DscRepoSchema, attributes(dsc_repo_schema))] +pub fn derive_into_dsc_repo_schema(item: TokenStream) -> TokenStream { + derive::dsc_repo_schema::dsc_repo_schema_impl(item) +} diff --git a/lib/dsc-lib-jsonschema-macros/src/test/ast/string_or_expr.rs b/lib/dsc-lib-jsonschema-macros/src/test/ast/string_or_expr.rs new file mode 100644 index 000000000..e52ff8033 --- /dev/null +++ b/lib/dsc-lib-jsonschema-macros/src/test/ast/string_or_expr.rs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use darling::FromMeta; + +use crate::ast::StringOrExpr; + +#[test] fn test_string_literal() { + let code = syn::parse_quote!("literal"); + let parsed = StringOrExpr::from_value(&code); + + assert_eq!(parsed.unwrap(), StringOrExpr::String("literal".to_string())); +} + +#[test] fn test_expr_fn() { + let code = syn::parse_quote!(get_value()); + let parsed = StringOrExpr::from_expr(&code); + + assert_eq!(parsed.unwrap(), StringOrExpr::Expr(Box::new(code))); +} + +#[test] fn test_expr_macro() { + let code = syn::parse_quote!(get_value!()); + let parsed = StringOrExpr::from_expr(&code); + + assert_eq!(parsed.unwrap(), StringOrExpr::Expr(Box::new(code))); +} diff --git a/lib/dsc-lib-jsonschema/Cargo.toml b/lib/dsc-lib-jsonschema/Cargo.toml index 374c376c3..a9ae91999 100644 --- a/lib/dsc-lib-jsonschema/Cargo.toml +++ b/lib/dsc-lib-jsonschema/Cargo.toml @@ -17,6 +17,8 @@ thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } urlencoding = { workspace = true } +# workspace dependencies +dsc-lib-jsonschema-macros = { workspace = true } [dev-dependencies] # Helps review complex comparisons, like schemas diff --git a/lib/dsc-lib-jsonschema/src/dsc_repo/dsc_repo_schema.rs b/lib/dsc-lib-jsonschema/src/dsc_repo/dsc_repo_schema.rs index 1ee21e230..a66cee131 100644 --- a/lib/dsc-lib-jsonschema/src/dsc_repo/dsc_repo_schema.rs +++ b/lib/dsc-lib-jsonschema/src/dsc_repo/dsc_repo_schema.rs @@ -192,7 +192,7 @@ pub trait DscRepoSchema : JsonSchema { } /// Defines the error when a user-defined JSON Schema references an unrecognized schema URI. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone, PartialEq)] #[error( "{t}: {0}. {t2}: {1:?}", t = t!("dsc_repo.dsc_repo_schema.unrecognizedSchemaUri"), diff --git a/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs b/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs index 9f9dc1eb4..e4c9b50fb 100644 --- a/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs +++ b/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs @@ -16,6 +16,8 @@ pub use schema_form::SchemaForm; mod schema_uri_prefix; pub use schema_uri_prefix::SchemaUriPrefix; +pub use dsc_lib_jsonschema_macros::DscRepoSchema; + /// Returns the constructed URI for a hosted DSC schema. /// /// This convenience function simplifies constructing the URIs for the various published schemas @@ -114,13 +116,33 @@ pub(crate) fn get_recognized_uris_subschema( |schema_uri| serde_json::Value::String(schema_uri.clone()) ).collect(); - json_schema!({ + let mut subschema = json_schema!({ "type": "string", "format": Some("uri".to_string()), "enum": Some(enums), - "title": metadata.get("title"), - "description": metadata.get("description"), - }) + }); + + let annotation_keywords = [ + "title", + "description", + "markdownDescription", + "enumMarkdownDescriptions", + "enumDescriptions", + "completionDetail", + "defaultSnippets", + "enumDetails", + "enumSortTexts", + "suggestSortText", + "deprecationMessage", + "errorMessage", + ]; + for annotation_keyword in annotation_keywords { + if let Some(value) = metadata.get(annotation_keyword) { + subschema.insert(annotation_keyword.to_string(), value.clone()); + } + } + + subschema } /// Returns the recognized schema URI for the latest major version with the diff --git a/lib/dsc-lib-jsonschema/tests/integration/dsc_repo/derive_dsc_repo_schema.rs b/lib/dsc-lib-jsonschema/tests/integration/dsc_repo/derive_dsc_repo_schema.rs new file mode 100644 index 000000000..d98b0b993 --- /dev/null +++ b/lib/dsc-lib-jsonschema/tests/integration/dsc_repo/derive_dsc_repo_schema.rs @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/// This macro exists to validate that you can pass either a string literal or an expression for a +/// schema field metadata item. +macro_rules! testing_title { + () => { + "Example schema" + }; +} + +#[cfg(test)] mod for_enum { + #[cfg(test)] mod without_bundling { + use pretty_assertions::assert_eq; + use schemars::JsonSchema; + use dsc_lib_jsonschema::dsc_repo::{DscRepoSchema, RecognizedSchemaVersion}; + + #[allow(dead_code)] + #[derive(Clone, Debug, JsonSchema, DscRepoSchema)] + #[dsc_repo_schema(base_name = "valid", folder_path = "example")] + enum Example { + String(String), + Boolean(bool) + } + + #[test] fn test_default_schema_id_uri() { + assert_eq!( + Example::default_schema_id_uri(), + "https://aka.ms/dsc/schemas/v3/example/valid.json".to_string() + ) + } + + #[test] fn test_get_canonical_schema_id_uri() { + assert_eq!( + Example::get_canonical_schema_id_uri(RecognizedSchemaVersion::V3), + "https://aka.ms/dsc/schemas/v3/example/valid.json".to_string() + ) + } + + #[test] fn test_get_bundled_schema_id_uri() { + assert_eq!( + Example::get_bundled_schema_id_uri(RecognizedSchemaVersion::V3), + None + ) + } + #[test] fn test_get_enhanced_schema_id_uri() { + assert_eq!( + Example::get_enhanced_schema_id_uri(RecognizedSchemaVersion::V3), + None + ) + } + } +} + +#[cfg(test)] mod for_struct { + #[cfg(test)] mod without_bundling { + #[cfg(test)] mod without_schema_field { + use pretty_assertions::assert_eq; + use schemars::JsonSchema; + use dsc_lib_jsonschema::dsc_repo::{DscRepoSchema, RecognizedSchemaVersion}; + + #[allow(dead_code)] + #[derive(Clone, Debug, JsonSchema, DscRepoSchema)] + #[dsc_repo_schema(base_name = "valid", folder_path = "example")] + struct Example { + pub foo: String, + pub bar: i32, + pub baz: bool, + } + + #[test] fn test_default_schema_id_uri() { + assert_eq!( + Example::default_schema_id_uri(), + "https://aka.ms/dsc/schemas/v3/example/valid.json".to_string() + ) + } + + #[test] fn test_get_canonical_schema_id_uri() { + assert_eq!( + Example::get_canonical_schema_id_uri(RecognizedSchemaVersion::V3), + "https://aka.ms/dsc/schemas/v3/example/valid.json".to_string() + ) + } + + #[test] fn test_get_bundled_schema_id_uri() { + assert_eq!( + Example::get_bundled_schema_id_uri(RecognizedSchemaVersion::V3), + None + ) + } + #[test] fn test_get_enhanced_schema_id_uri() { + assert_eq!( + Example::get_enhanced_schema_id_uri(RecognizedSchemaVersion::V3), + None + ) + } + } + #[cfg(test)] mod with_schema_field { + use pretty_assertions::assert_eq; + use schemars::JsonSchema; + use dsc_lib_jsonschema::{dsc_repo::{DscRepoSchema, RecognizedSchemaVersion, UnrecognizedSchemaUri}, schema_utility_extensions::SchemaUtilityExtensions}; + + #[allow(dead_code)] + #[derive(Clone, Debug, JsonSchema, DscRepoSchema)] + #[dsc_repo_schema( + base_name = "valid", + folder_path = "example", + schema_field( + name = schema_version, + title = testing_title!(), + description = "An example struct with a schema field.", + ) + )] + struct Example { + pub schema_version: String, + pub foo: String, + pub bar: i32, + pub baz: bool, + } + + #[test] fn test_default_schema_id_uri() { + assert_eq!( + Example::default_schema_id_uri(), + "https://aka.ms/dsc/schemas/v3/example/valid.json".to_string() + ) + } + + #[test] fn test_get_canonical_schema_id_uri() { + assert_eq!( + Example::get_canonical_schema_id_uri(RecognizedSchemaVersion::V3), + "https://aka.ms/dsc/schemas/v3/example/valid.json".to_string() + ) + } + + #[test] fn test_get_bundled_schema_id_uri() { + assert_eq!( + Example::get_bundled_schema_id_uri(RecognizedSchemaVersion::V3), + None + ) + } + #[test] fn test_get_enhanced_schema_id_uri() { + assert_eq!( + Example::get_enhanced_schema_id_uri(RecognizedSchemaVersion::V3), + None + ) + } + + #[test] fn test_recognized_schema_uris_subschema() { + let ref mut generator = schemars::SchemaGenerator::default(); + let subschema = Example::recognized_schema_uris_subschema(generator); + + let enum_subschema = subschema.get_keyword_as_array("enum").unwrap(); + let enum_count = enum_subschema.len(); + let expected_count = RecognizedSchemaVersion::all().len() * 2; + assert_eq!( + enum_count, + expected_count + ); + + assert_eq!( + subschema.get_keyword_as_str("type"), + Some("string") + ); + + assert_eq!( + subschema.get_keyword_as_str("format"), + Some("uri") + ); + + assert_eq!( + subschema.get_keyword_as_str("title"), + Some("Example schema") + ); + + assert_eq!( + subschema.get_keyword_as_str("description"), + Some("An example struct with a schema field.") + ); + assert_eq!( + subschema.get_keyword_as_str("markdownDescription"), + None + ); + } + + #[test] fn test_is_recognized_schema_uri() { + assert_eq!( + Example::is_recognized_schema_uri(&"https://incorrect/uri.json".to_string()), + false + ); + + assert_eq!( + Example::is_recognized_schema_uri(&Example::default_schema_id_uri()), + true + ); + } + + #[test] fn test_validate_schema_uri() { + let valid_instance = Example { + schema_version: Example::default_schema_id_uri(), + foo: String::new(), + bar: 0, + baz: true + }; + + assert_eq!( + valid_instance.validate_schema_uri(), + Ok(()) + ); + + let invalid_uri = "https://incorrect/uri.json".to_string(); + let invalid_instance = Example { + schema_version: invalid_uri.clone(), + foo: String::new(), + bar: 0, + baz: true + }; + + assert_eq!( + invalid_instance.validate_schema_uri(), + Err(UnrecognizedSchemaUri(invalid_uri, Example::recognized_schema_uris())) + ) + } + } + } + + #[cfg(test)] mod with_bundling { + #[cfg(test)] mod without_schema_field { + use pretty_assertions::assert_eq; + use schemars::JsonSchema; + use dsc_lib_jsonschema::dsc_repo::{DscRepoSchema, RecognizedSchemaVersion}; + + #[allow(dead_code)] + #[derive(Clone, Debug, JsonSchema, DscRepoSchema)] + #[dsc_repo_schema(base_name = "valid", folder_path = "example", should_bundle = true)] + struct Example { + pub foo: String, + pub bar: i32, + pub baz: bool, + } + + #[test] fn test_default_schema_id_uri() { + assert_eq!( + Example::default_schema_id_uri(), + "https://aka.ms/dsc/schemas/v3/bundled/example/valid.json".to_string() + ) + } + + #[test] fn test_get_canonical_schema_id_uri() { + assert_eq!( + Example::get_canonical_schema_id_uri(RecognizedSchemaVersion::V3), + "https://aka.ms/dsc/schemas/v3/example/valid.json".to_string() + ) + } + + #[test] fn test_get_bundled_schema_id_uri() { + assert_eq!( + Example::get_bundled_schema_id_uri(RecognizedSchemaVersion::V3), + Some("https://aka.ms/dsc/schemas/v3/bundled/example/valid.json".to_string()) + ) + } + #[test] fn test_get_enhanced_schema_id_uri() { + assert_eq!( + Example::get_enhanced_schema_id_uri(RecognizedSchemaVersion::V3), + Some("https://aka.ms/dsc/schemas/v3/bundled/example/valid.vscode.json".to_string()) + ) + } + } + + #[cfg(test)] mod with_schema_field { + use pretty_assertions::assert_eq; + use schemars::JsonSchema; + use dsc_lib_jsonschema::{dsc_repo::{DscRepoSchema, RecognizedSchemaVersion, UnrecognizedSchemaUri}, schema_utility_extensions::SchemaUtilityExtensions}; + + #[allow(dead_code)] + #[derive(Clone, Debug, JsonSchema, DscRepoSchema)] + #[dsc_repo_schema( + base_name = "valid", + folder_path = "example", + should_bundle = true, + schema_field( + name = schema_version, + title = testing_title!(), + description = "An example struct with a schema field.", + ) + )] + struct Example { + pub schema_version: String, + pub foo: String, + pub bar: i32, + pub baz: bool, + } + + #[test] fn test_default_schema_id_uri() { + assert_eq!( + Example::default_schema_id_uri(), + "https://aka.ms/dsc/schemas/v3/bundled/example/valid.json".to_string() + ) + } + + #[test] fn test_get_canonical_schema_id_uri() { + assert_eq!( + Example::get_canonical_schema_id_uri(RecognizedSchemaVersion::V3), + "https://aka.ms/dsc/schemas/v3/example/valid.json".to_string() + ) + } + + #[test] fn test_get_bundled_schema_id_uri() { + assert_eq!( + Example::get_bundled_schema_id_uri(RecognizedSchemaVersion::V3), + Some("https://aka.ms/dsc/schemas/v3/bundled/example/valid.json".to_string()) + ) + } + #[test] fn test_get_enhanced_schema_id_uri() { + assert_eq!( + Example::get_enhanced_schema_id_uri(RecognizedSchemaVersion::V3), + Some("https://aka.ms/dsc/schemas/v3/bundled/example/valid.vscode.json".to_string()) + ) + } + + #[test] fn test_recognized_schema_uris_subschema() { + let ref mut generator = schemars::SchemaGenerator::default(); + let subschema = Example::recognized_schema_uris_subschema(generator); + + let enum_subschema = subschema.get_keyword_as_array("enum").unwrap(); + let enum_count = enum_subschema.len(); + let expected_count = RecognizedSchemaVersion::all().len() * 6; + assert_eq!( + enum_count, + expected_count + ); + + assert_eq!( + subschema.get_keyword_as_str("type"), + Some("string") + ); + + assert_eq!( + subschema.get_keyword_as_str("format"), + Some("uri") + ); + + assert_eq!( + subschema.get_keyword_as_str("title"), + Some("Example schema") + ); + + assert_eq!( + subschema.get_keyword_as_str("description"), + Some("An example struct with a schema field.") + ); + assert_eq!( + subschema.get_keyword_as_str("markdownDescription"), + None + ); + } + + #[test] fn test_is_recognized_schema_uri() { + assert_eq!( + Example::is_recognized_schema_uri(&"https://incorrect/uri.json".to_string()), + false + ); + + assert_eq!( + Example::is_recognized_schema_uri(&Example::default_schema_id_uri()), + true + ); + } + + #[test] fn test_validate_schema_uri() { + let valid_instance = Example { + schema_version: Example::default_schema_id_uri(), + foo: String::new(), + bar: 0, + baz: true + }; + + assert_eq!( + valid_instance.validate_schema_uri(), + Ok(()) + ); + + let invalid_uri = "https://incorrect/uri.json".to_string(); + let invalid_instance = Example { + schema_version: invalid_uri.clone(), + foo: String::new(), + bar: 0, + baz: true + }; + + assert_eq!( + invalid_instance.validate_schema_uri(), + Err(UnrecognizedSchemaUri(invalid_uri, Example::recognized_schema_uris())) + ) + } + } + } +} diff --git a/lib/dsc-lib-jsonschema/tests/integration/dsc_repo/mod.rs b/lib/dsc-lib-jsonschema/tests/integration/dsc_repo/mod.rs new file mode 100644 index 000000000..9decd672a --- /dev/null +++ b/lib/dsc-lib-jsonschema/tests/integration/dsc_repo/mod.rs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(test)] mod derive_dsc_repo_schema; diff --git a/lib/dsc-lib-jsonschema/tests/integration/main.rs b/lib/dsc-lib-jsonschema/tests/integration/main.rs index c114d89f7..d15748633 100644 --- a/lib/dsc-lib-jsonschema/tests/integration/main.rs +++ b/lib/dsc-lib-jsonschema/tests/integration/main.rs @@ -16,3 +16,4 @@ //! execute our tests. #[cfg(test)] mod transforms; +#[cfg(test)] mod dsc_repo;