From e01cf3d4ae01b0127817a9f0722f3f5f702f363f Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 20 Aug 2025 13:15:39 -0400 Subject: [PATCH 1/6] pass input metadata to resource --- dsc_lib/src/configure/mod.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 2ea60d008..90fabd308 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -170,10 +170,15 @@ fn escape_property_values(properties: &Map) -> Result> ) -> Result { +fn add_metadata(kind: &Kind, mut properties: Option>, resource_metadata: Option ) -> Result { if *kind == Kind::Adapter { // add metadata to the properties so the adapter knows this is a config - let mut metadata = Map::new(); + let mut metadata: Map = Map::new(); + if let Some(resource_metadata) = resource_metadata { + if !resource_metadata.other.is_empty() { + metadata.extend(resource_metadata.other); + } + } let mut dsc_value = Map::new(); dsc_value.insert("context".to_string(), Value::String("configuration".to_string())); metadata.insert("Microsoft.DSC".to_string(), Value::Object(dsc_value)); @@ -185,6 +190,18 @@ fn add_metadata(kind: &Kind, mut properties: Option> ) -> Res return Ok(serde_json::to_string(&properties)?); } + if let Some(resource_metadata) = resource_metadata { + let other_metadata = resource_metadata.other.clone(); + if let Some(mut properties) = properties { + properties.insert("_metadata".to_string(), Value::Object(other_metadata)); + return Ok(serde_json::to_string(&properties)?); + } + let mut props = Map::new(); + props.insert("_metadata".to_string(), Value::Object(other_metadata)); + properties = Some(props); + return Ok(serde_json::to_string(&properties)?); + } + match properties { Some(properties) => { Ok(serde_json::to_string(&properties)?) @@ -330,7 +347,7 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); - let filter = add_metadata(&dsc_resource.kind, properties)?; + let filter = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?; trace!("filter: {filter}"); let start_datetime = chrono::Local::now(); let mut get_result = match dsc_resource.get(&filter) { @@ -425,7 +442,7 @@ impl Configurator { } }; - let desired = add_metadata(&dsc_resource.kind, properties)?; + let desired = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.desired", state = desired)); let start_datetime; @@ -562,7 +579,7 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); - let expected = add_metadata(&dsc_resource.kind, properties)?; + let expected = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.expectedState", state = expected)); let start_datetime = chrono::Local::now(); let mut test_result = match dsc_resource.test(&expected) { @@ -638,7 +655,7 @@ impl Configurator { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); }; let properties = self.get_properties(resource, &dsc_resource.kind)?; - let input = add_metadata(&dsc_resource.kind, properties)?; + let input = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.exportInput", input = input)); let export_result = match add_resource_export_results_to_configuration(dsc_resource, &mut conf, input.as_str()) { Ok(result) => result, From 2a3cbb4b71d85d61ae27711947ec62e3cb1c9ed9 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 20 Aug 2025 14:59:19 -0400 Subject: [PATCH 2/6] add test --- dsc/tests/dsc_metadata.tests.ps1 | 50 ++++++++++++++++++++++++++++ tree-sitter-dscexpression/.gitignore | 20 ++++------- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index f13920273..ada5e983e 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -2,6 +2,56 @@ # Licensed under the MIT License. Describe 'metadata tests' { + It 'resource can provide high-level metadata for ' -TestCases @( + @{ operation = 'get' } + @{ operation = 'set' } + @{ operation = 'test' } + ) { + param($operation) + + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: test + type: Test/Metadata + metadata: + hello: world + myNumber: 42 + properties: +'@ + + $out = dsc config $operation -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results.count | Should -Be 1 + $out.results[0].metadata.hello | Should -BeExactly 'world' + $out.results[0].metadata.myNumber | Should -Be 42 + } + + It 'resource can provide high-level metadata for export' { + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: test + type: Test/Metadata + metadata: + hello: There + myNumber: 16 + properties: +'@ + $out = dsc config export -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.resources.count | Should -Be 3 + $out.resources[0].metadata.hello | Should -BeExactly 'There' + $out.resources[0].metadata.myNumber | Should -Be 16 + $out.resources[0].name | Should -BeExactly 'Metadata example 1' + $out.resources[1].metadata.hello | Should -BeExactly 'There' + $out.resources[1].metadata.myNumber | Should -Be 16 + $out.resources[1].name | Should -BeExactly 'Metadata example 2' + $out.resources[2].metadata.hello | Should -BeExactly 'There' + $out.resources[2].metadata.myNumber | Should -Be 16 + $out.resources[2].name | Should -BeExactly 'Metadata example 3' + } + It 'resource can provide metadata for ' -TestCases @( @{ operation = 'get' } @{ operation = 'set' } diff --git a/tree-sitter-dscexpression/.gitignore b/tree-sitter-dscexpression/.gitignore index 0fcfe6858..17aa2089c 100644 --- a/tree-sitter-dscexpression/.gitignore +++ b/tree-sitter-dscexpression/.gitignore @@ -5,13 +5,14 @@ target/ build/ prebuilds/ node_modules/ -package-lock.json +*.tgz # Swift artifacts .build/ -Package.resolved +Package.swift # Go artifacts +go.sum _obj/ # Python artifacts @@ -19,6 +20,8 @@ _obj/ dist/ *.egg-info *.whl +pyproject.toml +setup.py # C artifacts *.a @@ -27,13 +30,7 @@ dist/ *.dylib *.dll *.pc -*.exp -*.lib - -# Zig artifacts -.zig-cache/ -zig-cache/ -zig-out/ +Makefile # Example dirs /examples/*/ @@ -43,7 +40,4 @@ zig-out/ *.obj *.o -# Archives -*.tar.gz -*.tgz -*.zip +.editorconfig From 362ff1cbd87b355a7e8c6e8d57071fc2f3c9f1a9 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 20 Aug 2025 15:01:36 -0400 Subject: [PATCH 3/6] revert inadvertent tree-sitter .gitignore change --- tree-sitter-dscexpression/.gitignore | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tree-sitter-dscexpression/.gitignore b/tree-sitter-dscexpression/.gitignore index 17aa2089c..0fcfe6858 100644 --- a/tree-sitter-dscexpression/.gitignore +++ b/tree-sitter-dscexpression/.gitignore @@ -5,14 +5,13 @@ target/ build/ prebuilds/ node_modules/ -*.tgz +package-lock.json # Swift artifacts .build/ -Package.swift +Package.resolved # Go artifacts -go.sum _obj/ # Python artifacts @@ -20,8 +19,6 @@ _obj/ dist/ *.egg-info *.whl -pyproject.toml -setup.py # C artifacts *.a @@ -30,7 +27,13 @@ setup.py *.dylib *.dll *.pc -Makefile +*.exp +*.lib + +# Zig artifacts +.zig-cache/ +zig-cache/ +zig-out/ # Example dirs /examples/*/ @@ -40,4 +43,7 @@ Makefile *.obj *.o -.editorconfig +# Archives +*.tar.gz +*.tgz +*.zip From ec92d080ae1fa7992999d4fd8b8c4aca3eb54532 Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 20 Aug 2025 15:40:21 -0400 Subject: [PATCH 4/6] Update dsc_lib/src/configure/mod.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dsc_lib/src/configure/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 90fabd308..934b1c56b 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -191,7 +191,7 @@ fn add_metadata(kind: &Kind, mut properties: Option>, resourc } if let Some(resource_metadata) = resource_metadata { - let other_metadata = resource_metadata.other.clone(); + let other_metadata = resource_metadata.other; if let Some(mut properties) = properties { properties.insert("_metadata".to_string(), Value::Object(other_metadata)); return Ok(serde_json::to_string(&properties)?); From fcbaa32d58cdc01a92d054e30e134b2cfde0cf0a Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Tue, 2 Sep 2025 17:05:35 -0400 Subject: [PATCH 5/6] refactor resource schema validation and use before inserting metadata --- dsc/src/subcommand.rs | 35 ++-------- dsc/src/util.rs | 35 ---------- dsc/tests/dsc_metadata.tests.ps1 | 17 +++++ dsc_lib/locales/en-us.toml | 1 + dsc_lib/src/configure/mod.rs | 34 +++++----- dsc_lib/src/dscresources/dscresource.rs | 90 ++++++++++++++++++++++++- 6 files changed, 128 insertions(+), 84 deletions(-) diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 1c308ad0e..42b41e07a 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -5,7 +5,7 @@ use crate::args::{ConfigSubCommand, SchemaType, ExtensionSubCommand, FunctionSub use crate::resolve::{get_contents, Include}; use crate::resource_command::{get_resource, self}; use crate::tablewriter::Table; -use crate::util::{get_input, get_schema, in_desired_state, set_dscconfigroot, validate_json, write_object, DSC_CONFIG_ROOT, EXIT_DSC_ASSERTION_FAILED, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR}; +use crate::util::{get_input, get_schema, in_desired_state, set_dscconfigroot, write_object, DSC_CONFIG_ROOT, EXIT_DSC_ASSERTION_FAILED, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR}; use dsc_lib::functions::FunctionArgKind; use dsc_lib::{ configure::{ @@ -26,8 +26,8 @@ use dsc_lib::{ TestResult, ValidateResult, }, - dscresources::dscresource::{Capability, ImplementedAs, Invoke}, - dscresources::resource_manifest::{import_manifest, ResourceManifest}, + dscresources::dscresource::{Capability, ImplementedAs, validate_json, validate_properties}, + dscresources::resource_manifest::import_manifest, extensions::dscextension::Capability as ExtensionCapability, functions::FunctionDispatcher, progress::ProgressFormat, @@ -516,34 +516,7 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat) // see if the resource is command based if resource.implemented_as == ImplementedAs::Command { - // if so, see if it implements validate via the resource manifest - if let Some(manifest) = resource.manifest.clone() { - // convert to resource_manifest - let manifest: ResourceManifest = serde_json::from_value(manifest)?; - if manifest.validate.is_some() { - debug!("{}: {type_name} ", t!("subcommand.resourceImplementsValidate")); - // get the resource's part of the config - let resource_config = resource_block["properties"].to_string(); - let result = resource.validate(&resource_config)?; - if !result.valid { - let reason = result.reason.unwrap_or(t!("subcommand.noReason").to_string()); - let type_name = resource.type_name.clone(); - return Err(DscError::Validation(format!("{}: {type_name} {reason}", t!("subcommand.resourceValidationFailed")))); - } - } - else { - // use schema validation - trace!("{}: {type_name}", t!("subcommand.resourceDoesNotImplementValidate")); - let Ok(schema) = resource.schema() else { - return Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noSchemaOrValidate")))); - }; - let schema = serde_json::from_str(&schema)?; - - validate_json(&resource.type_name, &schema, &resource_block["properties"])?; - } - } else { - return Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noManifest")))); - } + validate_properties(resource, &resource_block["properties"])?; } } diff --git a/dsc/src/util.rs b/dsc/src/util.rs index a647b598d..c1a4d726c 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -41,12 +41,10 @@ use dsc_lib::{ parse_input_to_json, }, }; -use jsonschema::Validator; use path_absolutize::Absolutize; use rust_i18n::t; use schemars::{Schema, schema_for}; use serde::Deserialize; -use serde_json::Value; use std::collections::HashMap; use std::env; use std::io::{IsTerminal, Read}; @@ -423,39 +421,6 @@ pub fn enable_tracing(trace_level_arg: Option<&TraceLevel>, trace_format_arg: Op info!("Trace-level is {:?}", tracing_setting.level); } -/// Validate the JSON against the schema. -/// -/// # Arguments -/// -/// * `source` - The source of the JSON -/// * `schema` - The schema to validate against -/// * `json` - The JSON to validate -/// -/// # Returns -/// -/// Nothing on success. -/// -/// # Errors -/// -/// * `DscError` - The JSON is invalid -pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> { - debug!("{}: {source}", t!("util.validatingSchema")); - trace!("JSON: {json}"); - trace!("Schema: {schema}"); - let compiled_schema = match Validator::new(schema) { - Ok(compiled_schema) => compiled_schema, - Err(err) => { - return Err(DscError::Validation(format!("{}: {err}", t!("util.failedToCompileSchema")))); - } - }; - - if let Err(err) = compiled_schema.validate(json) { - return Err(DscError::Validation(format!("{}: '{source}' {err}", t!("util.validationFailed")))); - } - - Ok(()) -} - pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_stdin: bool) -> String { trace!("Input: {input:?}, File: {file:?}"); let value = if let Some(input) = input { diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index ada5e983e..4ecd5b072 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -2,6 +2,23 @@ # Licensed under the MIT License. Describe 'metadata tests' { + It 'metadata not provided if not declared in resource schema' { + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: test + type: Microsoft.DSC.Debug/Echo + metadata: + ignoreKey: true + properties: + output: hello world +'@ + $out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + (Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Will not add '_metadata' to properties because resource schema does not support it*" + $out.results.result.actualState.output | Should -BeExactly 'hello world' + } + It 'resource can provide high-level metadata for ' -TestCases @( @{ operation = 'get' } @{ operation = 'set' } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index b83232c6c..8e7fe4551 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -71,6 +71,7 @@ propertyNotString = "Property '%{name}' with value '%{value}' is not a string" metadataMicrosoftDscIgnored = "Resource returned '_metadata' property 'Microsoft.DSC' which is ignored" metadataNotObject = "Resource returned '_metadata' property which is not an object" metadataRestartRequiredInvalid = "Resource returned '_metadata' property '_restartRequired' which contains invalid value: %{value}" +schemaExcludesMetadata = "Will not add '_metadata' to properties because resource schema does not support it" [discovery.commandDiscovery] couldNotReadSetting = "Could not read 'resourcePath' setting" diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 90fabd308..1d65ff5b9 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -6,7 +6,7 @@ use crate::configure::{config_doc::RestartRequired, parameters::Input}; use crate::dscerror::DscError; use crate::dscresources::invoke_result::ExportResult; use crate::dscresources::{ - {dscresource::{Capability, Invoke, get_diff}, + {dscresource::{Capability, Invoke, get_diff, validate_properties}, invoke_result::{GetResult, SetResult, TestResult, ResourceSetResponse}}, resource_manifest::Kind, }; @@ -170,8 +170,8 @@ fn escape_property_values(properties: &Map) -> Result>, resource_metadata: Option ) -> Result { - if *kind == Kind::Adapter { +fn add_metadata(dsc_resource: &DscResource, mut properties: Option>, resource_metadata: Option ) -> Result { + if dsc_resource.kind == Kind::Adapter { // add metadata to the properties so the adapter knows this is a config let mut metadata: Map = Map::new(); if let Some(resource_metadata) = resource_metadata { @@ -191,15 +191,19 @@ fn add_metadata(kind: &Kind, mut properties: Option>, resourc } if let Some(resource_metadata) = resource_metadata { - let other_metadata = resource_metadata.other.clone(); - if let Some(mut properties) = properties { - properties.insert("_metadata".to_string(), Value::Object(other_metadata)); - return Ok(serde_json::to_string(&properties)?); - } - let mut props = Map::new(); + let other_metadata = resource_metadata.other; + let mut props = if let Some(props) = properties { + props + } else { + Map::new() + }; props.insert("_metadata".to_string(), Value::Object(other_metadata)); - properties = Some(props); - return Ok(serde_json::to_string(&properties)?); + let modified_props = Value::from(props.clone()); + if let Ok(()) = validate_properties(dsc_resource, &modified_props) {} else { + warn!("{}", t!("configure.mod.schemaExcludesMetadata")); + props.remove("_metadata"); + } + return Ok(serde_json::to_string(&props)?); } match properties { @@ -347,7 +351,7 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); - let filter = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?; + let filter = add_metadata(dsc_resource, properties, resource.metadata.clone())?; trace!("filter: {filter}"); let start_datetime = chrono::Local::now(); let mut get_result = match dsc_resource.get(&filter) { @@ -442,7 +446,7 @@ impl Configurator { } }; - let desired = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?; + let desired = add_metadata(dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.desired", state = desired)); let start_datetime; @@ -579,7 +583,7 @@ impl Configurator { }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); - let expected = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?; + let expected = add_metadata(dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.expectedState", state = expected)); let start_datetime = chrono::Local::now(); let mut test_result = match dsc_resource.test(&expected) { @@ -655,7 +659,7 @@ impl Configurator { return Err(DscError::ResourceNotFound(resource.resource_type.clone())); }; let properties = self.get_properties(resource, &dsc_resource.kind)?; - let input = add_metadata(&dsc_resource.kind, properties, resource.metadata.clone())?; + let input = add_metadata(dsc_resource, properties, resource.metadata.clone())?; trace!("{}", t!("configure.mod.exportInput", input = input)); let export_result = match add_resource_export_results_to_configuration(dsc_resource, &mut conf, input.as_str()) { Ok(result) => result, diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index f90a63890..f9029bf6f 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -4,14 +4,24 @@ use crate::{configure::{config_doc::{Configuration, ExecutionKind, Resource}, Configurator}, dscresources::resource_manifest::Kind}; use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse}; use dscerror::DscError; +use jsonschema::Validator; use rust_i18n::t; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::HashMap; -use tracing::{debug, info}; - -use super::{command_resource, dscerror, invoke_result::{ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult}, resource_manifest::import_manifest}; +use tracing::{debug, info, trace}; + +use super::{ + command_resource, + dscerror, + invoke_result::{ + ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult + }, + resource_manifest::{ + import_manifest, ResourceManifest + } +}; #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] @@ -557,6 +567,80 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec { diff_properties } +/// Validates the properties of a resource against its schema. +/// +/// # Arguments +/// +/// * `properties` - The properties of the resource to validate. +/// * `schema` - The schema to validate against. +/// +/// # Returns +/// +/// * `Result<(), DscError>` - Ok if valid, Err with message if invalid. +/// +/// # Errors +/// +/// * `DscError` - Error if the schema is invalid +pub fn validate_properties(resource: &DscResource, properties: &Value) -> Result<(), DscError> { + // if so, see if it implements validate via the resource manifest + let type_name = resource.type_name.clone(); + if let Some(manifest) = resource.manifest.clone() { + // convert to resource_manifest`` + let manifest: ResourceManifest = serde_json::from_value(manifest)?; + if manifest.validate.is_some() { + debug!("{}: {type_name} ", t!("subcommand.resourceImplementsValidate")); + let resource_config = properties.to_string(); + let result = resource.validate(&resource_config)?; + if !result.valid { + let reason = result.reason.unwrap_or(t!("subcommand.noReason").to_string()); + return Err(DscError::Validation(format!("{}: {type_name} {reason}", t!("subcommand.resourceValidationFailed")))); + } + return Ok(()) + } + // use schema validation + trace!("{}: {type_name}", t!("subcommand.resourceDoesNotImplementValidate")); + let Ok(schema) = resource.schema() else { + return Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noSchemaOrValidate")))); + }; + let schema = serde_json::from_str(&schema)?; + return validate_json(&resource.type_name, &schema, properties) + } + Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noManifest")))) +} + +/// Validate the JSON against the schema. +/// +/// # Arguments +/// +/// * `source` - The source of the JSON +/// * `schema` - The schema to validate against +/// * `json` - The JSON to validate +/// +/// # Returns +/// +/// Nothing on success. +/// +/// # Errors +/// +/// * `DscError` - The JSON is invalid +pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> { + debug!("{}: {source}", t!("util.validatingSchema")); + trace!("JSON: {json}"); + trace!("Schema: {schema}"); + let compiled_schema = match Validator::new(schema) { + Ok(compiled_schema) => compiled_schema, + Err(err) => { + return Err(DscError::Validation(format!("{}: {err}", t!("util.failedToCompileSchema")))); + } + }; + + if let Err(err) = compiled_schema.validate(json) { + return Err(DscError::Validation(format!("{}: '{source}' {err}", t!("util.validationFailed")))); + } + + Ok(()) +} + /// Compares two arrays independent of order fn is_same_array(expected: &Vec, actual: &Vec) -> bool { if expected.len() != actual.len() { From bc0e7478a5f17c0d363a74f94fe98e474718d0de Mon Sep 17 00:00:00 2001 From: Tess Gauthier Date: Wed, 3 Sep 2025 10:29:28 -0400 Subject: [PATCH 6/6] rename and move localizations --- dsc/locales/en-us.toml | 9 --------- dsc_lib/locales/en-us.toml | 9 +++++++++ dsc_lib/src/dscresources/dscresource.rs | 18 +++++++++--------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index 4c8a08d6e..060d552cd 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -100,12 +100,6 @@ noResources = "Resources not specified" resourceTypeNotSpecified = "Resource type not specified" validatingResource = "Validating resource named" resourceNotFound = "Resource type not found" -resourceImplementsValidate = "Resource implements validation" -noReason = "No reason provided" -resourceValidationFailed = "Resource failed validation" -resourceDoesNotImplementValidate = "Resource does not implement validation, using schema" -noSchemaOrValidate = "Resource does not have a schema nor supports validation" -noManifest = "Resource does not have a manifest" tableHeader_type = "Type" tableHeader_kind = "Kind" tableHeader_version = "Version" @@ -127,9 +121,6 @@ failedToConvertJsonToString = "Failed to convert JSON to string" failedToReadTracingSetting = "Could not read 'tracing' setting" invalidTraceLevel = "Default to 'warn', invalid DSC_TRACE_LEVEL value" failedToSetTracing = "Unable to set global default tracing subscriber. Tracing is disabled." -validatingSchema = "Validating against schema" -failedToCompileSchema = "JSON Schema Compilation" -validationFailed = "Failed validation" readingInput = "Reading input from command line parameter" inputIsFile = "Document provided is a file path, use '--file' instead" readingInputFromFile = "Reading input from file" diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 2cbf6af2c..466729fe2 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -177,6 +177,15 @@ diffKeyMissing = "diff: key '%{key}' missing" diffKeyNotObject = "diff: key '%{key}' is not an object" diffArraySize = "diff: arrays have different lengths" diffMissingItem = "diff: actual array missing expected item" +failedToCompileSchema = "JSON Schema Compilation" +noManifest = "Resource does not have a manifest" +noReason = "No reason provided" +noSchemaOrValidate = "Resource does not have a schema nor supports validation" +resourceDoesNotImplementValidate = "Resource does not implement validation, using schema" +resourceImplementsValidate = "Resource implements validation" +resourceValidationFailed = "Resource failed validation" +validatingSchema = "Validating against schema" +validationFailed = "Failed validation" [dscresources.resource_manifest] resourceManifestSchemaTitle = "Resource manifest schema URI" diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index f9029bf6f..2c724a1c4 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -588,24 +588,24 @@ pub fn validate_properties(resource: &DscResource, properties: &Value) -> Result // convert to resource_manifest`` let manifest: ResourceManifest = serde_json::from_value(manifest)?; if manifest.validate.is_some() { - debug!("{}: {type_name} ", t!("subcommand.resourceImplementsValidate")); + debug!("{}: {type_name} ", t!("dscresources.dscresource.resourceImplementsValidate")); let resource_config = properties.to_string(); let result = resource.validate(&resource_config)?; if !result.valid { - let reason = result.reason.unwrap_or(t!("subcommand.noReason").to_string()); - return Err(DscError::Validation(format!("{}: {type_name} {reason}", t!("subcommand.resourceValidationFailed")))); + let reason = result.reason.unwrap_or(t!("dscresources.dscresource.noReason").to_string()); + return Err(DscError::Validation(format!("{}: {type_name} {reason}", t!("dscresources.dscresource.resourceValidationFailed")))); } return Ok(()) } // use schema validation - trace!("{}: {type_name}", t!("subcommand.resourceDoesNotImplementValidate")); + trace!("{}: {type_name}", t!("dscresources.dscresource.resourceDoesNotImplementValidate")); let Ok(schema) = resource.schema() else { - return Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noSchemaOrValidate")))); + return Err(DscError::Validation(format!("{}: {type_name}", t!("dscresources.dscresource.noSchemaOrValidate")))); }; let schema = serde_json::from_str(&schema)?; return validate_json(&resource.type_name, &schema, properties) } - Err(DscError::Validation(format!("{}: {type_name}", t!("subcommand.noManifest")))) + Err(DscError::Validation(format!("{}: {type_name}", t!("dscresources.dscresource.noManifest")))) } /// Validate the JSON against the schema. @@ -624,18 +624,18 @@ pub fn validate_properties(resource: &DscResource, properties: &Value) -> Result /// /// * `DscError` - The JSON is invalid pub fn validate_json(source: &str, schema: &Value, json: &Value) -> Result<(), DscError> { - debug!("{}: {source}", t!("util.validatingSchema")); + debug!("{}: {source}", t!("dscresources.dscresource.validatingSchema")); trace!("JSON: {json}"); trace!("Schema: {schema}"); let compiled_schema = match Validator::new(schema) { Ok(compiled_schema) => compiled_schema, Err(err) => { - return Err(DscError::Validation(format!("{}: {err}", t!("util.failedToCompileSchema")))); + return Err(DscError::Validation(format!("{}: {err}", t!("dscresources.dscresource.failedToCompileSchema")))); } }; if let Err(err) = compiled_schema.validate(json) { - return Err(DscError::Validation(format!("{}: '{source}' {err}", t!("util.validationFailed")))); + return Err(DscError::Validation(format!("{}: '{source}' {err}", t!("dscresources.dscresource.validationFailed")))); } Ok(())