From 59b5cb2005fe50440037b341aa53799d71313519 Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:39:45 +0200 Subject: [PATCH 1/6] (GH-538) Enhance schema export functionality and add error handling for duplicate paths --- dsc/src/util.rs | 54 +++++++----- lib/dsc-lib-jsonschema/locales/en-us.toml | 4 + lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs | 1 + .../src/dsc_repo/recognized_schema_version.rs | 52 +++++++++++ .../dscresources/adapted_resource_manifest.rs | 17 +++- lib/dsc-lib/src/dscresources/invoke_result.rs | 2 +- xtask/locales/en-us.toml | 7 ++ xtask/src/args.rs | 10 ++- xtask/src/main.rs | 87 ++++++++++++++++++- xtask/src/schemas/export.rs | 33 ++++++- 10 files changed, 235 insertions(+), 32 deletions(-) diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 51eb4596a..7050d53bd 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -41,6 +41,7 @@ use dsc_lib::{ extension_manifest::ExtensionManifest, }, functions::FunctionDefinition, + schemas::dsc_repo::{DscRepoSchema, RecognizedSchemaVersion, SchemaForm, SchemaUriPrefix}, util::{ get_setting, parse_input_to_json, @@ -164,37 +165,37 @@ pub fn add_fields_to_json(json: &str, fields_to_add: &HashMap) - pub fn get_schema(schema: SchemaType) -> Schema { match schema { SchemaType::AdaptedDscResourceManifest => { - schema_for!(AdaptedDscResourceManifest) + repo_schema::() }, SchemaType::Configuration => { - schema_for!(Configuration) + repo_schema::() }, SchemaType::ConfigurationExportResult => { - schema_for!(ConfigurationExportResult) + repo_schema::() }, SchemaType::ConfigurationGetResult => { - schema_for!(ConfigurationGetResult) + repo_schema::() }, SchemaType::ConfigurationSetResult => { - schema_for!(ConfigurationSetResult) + repo_schema::() }, SchemaType::ConfigurationTestResult => { - schema_for!(ConfigurationTestResult) + repo_schema::() }, SchemaType::DscResource => { - schema_for!(DscResource) + repo_schema::() }, SchemaType::ExtensionDiscoverResult => { - schema_for!(DiscoverResult) + repo_schema::() }, SchemaType::ExtensionManifest => { - schema_for!(ExtensionManifest) + repo_schema::() }, SchemaType::FunctionDefinition => { - schema_for!(FunctionDefinition) + repo_schema::() }, SchemaType::GetResult => { - schema_for!(GetResult) + repo_schema::() }, SchemaType::Include => { schema_for!(Include) @@ -203,35 +204,48 @@ pub fn get_schema(schema: SchemaType) -> Schema { schema_for!(ManifestList) }, SchemaType::ResolveResult => { - schema_for!(ResolveResult) + repo_schema::() }, SchemaType::Resource => { - schema_for!(Resource) + repo_schema::() }, SchemaType::ResourceGetResult => { - schema_for!(ResourceGetResult) + repo_schema::() }, SchemaType::ResourceSetResult => { - schema_for!(ResourceSetResult) + repo_schema::() }, SchemaType::ResourceTestResult => { - schema_for!(ResourceTestResult) + repo_schema::() }, SchemaType::ResourceManifest => { - schema_for!(ResourceManifest) + repo_schema::() }, SchemaType::RestartRequired => { - schema_for!(RestartRequired) + repo_schema::() }, SchemaType::SetResult => { - schema_for!(SetResult) + repo_schema::() }, SchemaType::TestResult => { - schema_for!(TestResult) + repo_schema::() }, } } +fn repo_schema() -> Schema { + let schema_form = if T::SCHEMA_SHOULD_BUNDLE { + SchemaForm::Bundled + } else { + SchemaForm::Canonical + }; + T::generate_schema( + RecognizedSchemaVersion::default(), + schema_form, + SchemaUriPrefix::AkaDotMs + ) +} + /// Write the JSON object to the console /// /// # Arguments diff --git a/lib/dsc-lib-jsonschema/locales/en-us.toml b/lib/dsc-lib-jsonschema/locales/en-us.toml index d0a0b263b..1c28d2d4b 100644 --- a/lib/dsc-lib-jsonschema/locales/en-us.toml +++ b/lib/dsc-lib-jsonschema/locales/en-us.toml @@ -5,6 +5,10 @@ unrecognizedSchemaUri = "Unrecognized $schema URI" validSchemaUrisAre = "Valid schema URIs are" missingTranslation = "unable to retrieve translation for undefined key '#{key}'" +[dsc_repo.recognized_schema_version] +unrecognizedVersion = "Unrecognized schema version folder" +validVersionsAre = "Valid schema version folders are" + [transforms.idiomaticize_externally_tagged_enum] applies_to = "invalid application of idiomaticize_externally_tagged_enum; missing 'oneOf' keyword in transforming schema: %{transforming_schema}" oneOf_array = "invalid application of idiomaticize_externally_tagged_enum; 'oneOf' isn't an array in transforming schema: %{transforming_schema}" diff --git a/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs b/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs index 40a99f2e0..e149350e5 100644 --- a/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs +++ b/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs @@ -14,6 +14,7 @@ pub use crate::schema_i18n; mod recognized_schema_version; pub use recognized_schema_version::RecognizedSchemaVersion; +pub use recognized_schema_version::UnrecognizedSchemaVersion; mod schema_form; pub use schema_form::SchemaForm; diff --git a/lib/dsc-lib-jsonschema/src/dsc_repo/recognized_schema_version.rs b/lib/dsc-lib-jsonschema/src/dsc_repo/recognized_schema_version.rs index 38ac3161d..dcb25f86f 100644 --- a/lib/dsc-lib-jsonschema/src/dsc_repo/recognized_schema_version.rs +++ b/lib/dsc-lib-jsonschema/src/dsc_repo/recognized_schema_version.rs @@ -6,4 +6,56 @@ //! checks the git tags for non-prerelease versions of DSC to generate the enum type with all of the //! correct values. The enum can be used transparently throughout the rest of the libraries. +use rust_i18n::t; +use thiserror::Error; + include!(concat!(env!("OUT_DIR"), "/recognized_schema_version.rs")); + +/// Defines the error when parsing a string that isn't a recognized schema version folder. +#[derive(Error, Debug, Clone, PartialEq)] +#[error( + "{t}: {0}. {t2}: {1:?}", + t = t!("dsc_repo.recognized_schema_version.unrecognizedVersion"), + t2 = t!("dsc_repo.recognized_schema_version.validVersionsAre") +)] +pub struct UnrecognizedSchemaVersion(pub String, pub Vec); + +impl std::str::FromStr for RecognizedSchemaVersion { + type Err = UnrecognizedSchemaVersion; + + fn from_str(s: &str) -> Result { + let candidate = s.trim(); + Self::all() + .into_iter() + .find(|version| version.to_string().eq_ignore_ascii_case(candidate)) + .ok_or_else(|| UnrecognizedSchemaVersion( + candidate.to_string(), + Self::all().iter().map(ToString::to_string).collect() + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_str_round_trips_every_recognized_version() { + for version in RecognizedSchemaVersion::all() { + let parsed: RecognizedSchemaVersion = version.to_string().parse().unwrap(); + assert_eq!(parsed, version); + } + } + + #[test] + fn from_str_is_case_insensitive_and_trims() { + let parsed: RecognizedSchemaVersion = " VNEXT ".parse().unwrap(); + assert_eq!(parsed, RecognizedSchemaVersion::VNext); + } + + #[test] + fn from_str_rejects_unrecognized_versions() { + assert!("v99.0.0".parse::().is_err()); + assert!("not-a-version".parse::().is_err()); + } +} diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index e13d38db7..d825d5738 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -25,7 +25,7 @@ pub enum AdaptedPathOrContent { #[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema( base_name = "manifest", - folder_path = "resource", + folder_path = "adaptedresource", should_bundle = true, schema_field( name = schema_version, @@ -36,7 +36,7 @@ pub enum AdaptedPathOrContent { pub struct AdaptedDscResourceManifest { /// The version of the resource manifest schema. #[serde(rename = "$schema")] - #[schemars(schema_with = "ResourceManifest::recognized_schema_uris_subschema")] + #[schemars(schema_with = "AdaptedDscResourceManifest::recognized_schema_uris_union_subschema")] pub schema_version: String, /// The namespaced name of the resource. #[serde(rename="type")] @@ -64,3 +64,16 @@ pub struct AdaptedDscResourceManifest { /// The JSON Schema of the resource. pub schema: Map, } + +impl AdaptedDscResourceManifest { + fn recognized_schema_uris_union_subschema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + let mut subschema = ::recognized_schema_uris_subschema(generator); + let uris: Vec = Self::recognized_schema_uris() + .into_iter() + .chain(ResourceManifest::recognized_schema_uris()) + .map(Value::String) + .collect(); + subschema.insert("enum".to_string(), Value::Array(uris)); + subschema + } +} diff --git a/lib/dsc-lib/src/dscresources/invoke_result.rs b/lib/dsc-lib/src/dscresources/invoke_result.rs index 1658f56a0..9115d2896 100644 --- a/lib/dsc-lib/src/dscresources/invoke_result.rs +++ b/lib/dsc-lib/src/dscresources/invoke_result.rs @@ -193,7 +193,7 @@ pub struct DeleteResult { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[dsc_repo_schema(base_name = "delete", folder_path = "outputs/resource")] +#[dsc_repo_schema(base_name = "delete.whatIf", folder_path = "outputs/resource")] #[serde(deny_unknown_fields)] pub struct DeleteWhatIfResult { #[serde(rename = "whatIf", skip_serializing_if = "Option::is_none")] diff --git a/xtask/locales/en-us.toml b/xtask/locales/en-us.toml index 38f87e629..dba331f7a 100644 --- a/xtask/locales/en-us.toml +++ b/xtask/locales/en-us.toml @@ -2,7 +2,14 @@ about = "xtask provides build helpers for the DSC rust project." schemaAbout = "Commands for managing DSC repository schemas." schemaExportAbout = "Export DSC schemas to disk." +schemaExportVersionHelp = "The schema version folder to export, like 'v3.2' or 'vNext'. May be specified multiple times. Defaults to 'vNext'." +schemaExportReleaseHelp = "A release version like '3.3.0'. Exports the patch, minor, and major version folders for the release, like 'v3.3.0', 'v3.3', and 'v3'." + +[main] +invalidReleaseVersion = "Invalid release version; expected a full version like '3.3.0'" +unrecognizedReleaseFolder = "Schema version folder isn't recognized; if the release was just tagged, refresh 'lib/dsc-lib-jsonschema/.versions.json' by running '.versions.ps1' and rebuild" [schemas.export] serializationFailure = "Failed to serialize JSON Schema as string" ioError = "Failed to export JSON Schema, IO error" +duplicatePath = "Multiple schemas export to the same path; check the `dsc_repo_schema` attributes for a `base_name`/`folder_path` collision" diff --git a/xtask/src/args.rs b/xtask/src/args.rs index 03e8001ba..eb54330b4 100644 --- a/xtask/src/args.rs +++ b/xtask/src/args.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use clap::{Parser, Subcommand}; +use dsc_lib::schemas::dsc_repo::RecognizedSchemaVersion; use rust_i18n::t; #[derive(Debug, Parser)] @@ -24,5 +25,12 @@ pub enum SubCommand { #[derive(Debug, PartialEq, Eq, Subcommand)] pub enum SchemaSubCommand { #[clap(name = "export", about = t!("args.schemaExportAbout").to_string())] - Export + Export { + /// The schema version folder(s) to export. Repeatable. Defaults to `vNext`. + #[clap(long = "schema-version", help = t!("args.schemaExportVersionHelp").to_string())] + schema_versions: Vec, + /// A release version that expands to its patch, minor, and major version folders. + #[clap(long = "release", conflicts_with = "schema_versions", help = t!("args.schemaExportReleaseHelp").to_string())] + release: Option, + } } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 35ef085d2..1f60e95e1 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -3,7 +3,7 @@ use clap::Parser; use dsc_lib::schemas::dsc_repo::RecognizedSchemaVersion; -use rust_i18n::i18n; +use rust_i18n::{i18n, t}; use thiserror::Error; use crate::{ @@ -19,7 +19,11 @@ pub(crate) mod schemas { #[derive(Debug, Error)] pub(crate) enum XTaskError { #[error(transparent)] - SchemaExport(#[from] SchemaExportError) + SchemaExport(#[from] SchemaExportError), + #[error("{t}: {0}", t = t!("main.invalidReleaseVersion"))] + InvalidReleaseVersion(String), + #[error("{t}: {0}", t = t!("main.unrecognizedReleaseFolder"))] + UnrecognizedReleaseFolder(String), } i18n!("locales", fallback = "en-us"); @@ -29,10 +33,85 @@ fn main() -> Result<(), XTaskError> { match args.subcommand { SubCommand::Schema { sub_command } => match sub_command { - SchemaSubCommand::Export => { - export_schemas(RecognizedSchemaVersion::VNext)?; + SchemaSubCommand::Export { schema_versions, release } => { + for schema_version in resolve_export_versions(schema_versions, release.as_deref())? { + export_schemas(schema_version)?; + } Ok(()) }, }, } } + +fn resolve_export_versions( + schema_versions: Vec, + release: Option<&str> +) -> Result, XTaskError> { + let Some(release) = release else { + return Ok(if schema_versions.is_empty() { + vec![RecognizedSchemaVersion::VNext] + } else { + schema_versions + }); + }; + + let version = release.trim().trim_start_matches('v'); + let segments: Vec<&str> = version.split('.').collect(); + let is_numeric = |segment: &&str| !segment.is_empty() && segment.chars().all(|c| c.is_ascii_digit()); + if segments.len() != 3 || !segments.iter().all(is_numeric) { + return Err(XTaskError::InvalidReleaseVersion(release.to_string())); + } + + let folders = [ + format!("v{}.{}.{}", segments[0], segments[1], segments[2]), + format!("v{}.{}", segments[0], segments[1]), + format!("v{}", segments[0]), + ]; + folders.iter().map(|folder| { + folder.parse::() + .map_err(|_| XTaskError::UnrecognizedReleaseFolder(folder.clone())) + }).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_defaults_to_vnext() { + let versions = resolve_export_versions(Vec::new(), None).unwrap(); + assert_eq!(versions, vec![RecognizedSchemaVersion::VNext]); + } + + #[test] + fn resolve_passes_through_explicit_versions() { + let requested = vec![RecognizedSchemaVersion::VNext, RecognizedSchemaVersion::default()]; + let versions = resolve_export_versions(requested.clone(), None).unwrap(); + assert_eq!(versions, requested); + } + + #[test] + fn resolve_release_expands_to_patch_minor_and_major_folders() { + let latest = RecognizedSchemaVersion::latest().to_string(); + let release = latest.trim_start_matches('v').to_string(); + let versions = resolve_export_versions(Vec::new(), Some(&release)).unwrap(); + assert_eq!(versions.len(), 3); + assert_eq!(versions[0].to_string(), latest); + } + + #[test] + fn resolve_release_rejects_partial_versions() { + assert!(matches!( + resolve_export_versions(Vec::new(), Some("3.2")), + Err(XTaskError::InvalidReleaseVersion(_)) + )); + } + + #[test] + fn resolve_release_rejects_unrecognized_versions() { + assert!(matches!( + resolve_export_versions(Vec::new(), Some("99.0.0")), + Err(XTaskError::UnrecognizedReleaseFolder(_)) + )); + } +} diff --git a/xtask/src/schemas/export.rs b/xtask/src/schemas/export.rs index 936ec8cd2..fdfb4a8b8 100644 --- a/xtask/src/schemas/export.rs +++ b/xtask/src/schemas/export.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::{fs, ops::Add, path::PathBuf, sync::LazyLock}; +use std::{collections::HashSet, fs, ops::Add, path::PathBuf, sync::LazyLock}; use dsc_lib::schemas::{ dsc_repo::{DscRepoSchema, RecognizedSchemaVersion}, @@ -20,6 +20,10 @@ pub(crate) enum SchemaExportError { /// Raised when an IO error prevents exporting a schema to the file system. #[error("{t}: {0}", t = t!("schemas.export.ioError"))] IOError(#[from] std::io::Error), + /// Raised when two exported types resolve to the same file path, which would silently + /// overwrite one schema with another. + #[error("{t}: {0}", t = t!("schemas.export.duplicatePath"))] + DuplicatePath(String), } /// Helper static to retrieve the root folder once and use repeatedly when exporting schemas to the @@ -30,8 +34,18 @@ static PROJECT_DIR: LazyLock = LazyLock::new(|| { }); /// Writes the given JSON Schema to the filesystem relative to the project folder. -pub(crate) fn write_schema(relative_path: PathBuf, schema: Schema) -> Result<(), SchemaExportError> { - +/// +/// The `written_paths` set records every path exported during the run so that two types +/// resolving to the same path fail the export instead of silently overwriting each other. +pub(crate) fn write_schema( + written_paths: &mut HashSet, + relative_path: PathBuf, + schema: Schema +) -> Result<(), SchemaExportError> { + if !written_paths.insert(relative_path.clone()) { + return Err(SchemaExportError::DuplicatePath(relative_path.display().to_string())); + } + let json_schema = serde_json::to_string_pretty(&schema.to_value_with_stable_order())?.add("\n"); let path = PROJECT_DIR.clone().join("schemas").join(relative_path); let folder = path.parent().unwrap(); @@ -46,11 +60,12 @@ pub(crate) fn write_schema(relative_path: PathBuf, schema: Schema) -> Result<(), } macro_rules! export_type_schemas { - ($schema_version:expr => $($type_to_export:ty),+) => { + ($written_paths:expr, $schema_version:expr => $($type_to_export:ty),+) => { { $( for schema_form in <$type_to_export>::get_valid_schema_forms() { write_schema( + $written_paths, <$type_to_export>::get_schema_relative_path($schema_version, schema_form).into(), <$type_to_export>::generate_exportable_schema($schema_version, schema_form) )?; @@ -63,16 +78,21 @@ macro_rules! export_type_schemas { pub(crate) fn export_schemas( schema_version: RecognizedSchemaVersion ) -> Result<(), SchemaExportError> { + let mut written_paths: HashSet = HashSet::new(); export_type_schemas!( + &mut written_paths, schema_version => + dsc_lib::configure::config_doc::ConfigDirective, dsc_lib::configure::config_doc::Configuration, dsc_lib::configure::config_doc::DataType, + dsc_lib::configure::config_doc::ExecutionInformation, dsc_lib::configure::config_doc::ExecutionKind, dsc_lib::configure::config_doc::Metadata, dsc_lib::configure::config_doc::Operation, dsc_lib::configure::config_doc::Output, dsc_lib::configure::config_doc::Parameter, dsc_lib::configure::config_doc::Resource, + dsc_lib::configure::config_doc::ResourceDirective, dsc_lib::configure::config_doc::ResourceDiscoveryMode, dsc_lib::configure::config_doc::RestartRequired, dsc_lib::configure::config_doc::SecurityContextKind, @@ -87,6 +107,7 @@ pub(crate) fn export_schemas( dsc_lib::configure::config_result::ResourceGetResult, dsc_lib::configure::config_result::ResourceMessage, dsc_lib::configure::config_result::ResourceSetResult, + dsc_lib::configure::config_result::ResourceTestResult, dsc_lib::dscresources::adapted_resource_manifest::AdaptedDscResourceManifest, dsc_lib::dscresources::dscresource::Capability, dsc_lib::dscresources::dscresource::DscResource, @@ -104,6 +125,7 @@ pub(crate) fn export_schemas( dsc_lib::dscresources::resource_manifest::Adapter, dsc_lib::dscresources::resource_manifest::DeleteMethod, dsc_lib::dscresources::resource_manifest::ExportMethod, + dsc_lib::dscresources::resource_manifest::ExportSchemaKind, dsc_lib::dscresources::resource_manifest::GetArgKind, dsc_lib::dscresources::resource_manifest::GetMethod, dsc_lib::dscresources::resource_manifest::InputKind, @@ -111,6 +133,7 @@ pub(crate) fn export_schemas( dsc_lib::dscresources::resource_manifest::ResolveMethod, dsc_lib::dscresources::resource_manifest::ResourceManifest, dsc_lib::dscresources::resource_manifest::ReturnKind, + dsc_lib::dscresources::resource_manifest::SchemaArgKind, dsc_lib::dscresources::resource_manifest::SchemaKind, dsc_lib::dscresources::resource_manifest::SetDeleteArgKind, dsc_lib::dscresources::resource_manifest::SetMethod, @@ -126,12 +149,14 @@ pub(crate) fn export_schemas( dsc_lib::functions::FunctionArgKind, dsc_lib::functions::FunctionCategory, dsc_lib::functions::FunctionDefinition, + dsc_lib::types::DateVersion, dsc_lib::types::ExitCodesMap, dsc_lib::types::FullyQualifiedTypeName, dsc_lib::types::ResourceVersion, dsc_lib::types::ResourceVersionReq, dsc_lib::types::SemanticVersion, dsc_lib::types::SemanticVersionReq, + dsc_lib::types::Tag, dsc_lib::types::TagList ); From c716d0407b3127a8ae774625d6b79117a51efb86 Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:39:45 +0200 Subject: [PATCH 2/6] (GH-538) Enhance schema export functionality and add error handling for duplicate paths --- dsc/src/util.rs | 54 +++++++----- lib/dsc-lib-jsonschema/locales/en-us.toml | 4 + lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs | 1 + .../src/dsc_repo/recognized_schema_version.rs | 52 +++++++++++ .../dscresources/adapted_resource_manifest.rs | 17 +++- lib/dsc-lib/src/dscresources/invoke_result.rs | 2 +- xtask/locales/en-us.toml | 7 ++ xtask/src/args.rs | 10 ++- xtask/src/main.rs | 87 ++++++++++++++++++- xtask/src/schemas/export.rs | 33 ++++++- 10 files changed, 235 insertions(+), 32 deletions(-) diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 4b18fac6c..deedef391 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -41,6 +41,7 @@ use dsc_lib::{ extension_manifest::ExtensionManifest, }, functions::FunctionDefinition, + schemas::dsc_repo::{DscRepoSchema, RecognizedSchemaVersion, SchemaForm, SchemaUriPrefix}, util::{ get_setting, parse_input_to_json, @@ -163,37 +164,37 @@ pub fn add_fields_to_json(json: &str, fields_to_add: &HashMap) - pub fn get_schema(schema: SchemaType) -> Schema { match schema { SchemaType::AdaptedDscResourceManifest => { - schema_for!(AdaptedDscResourceManifest) + repo_schema::() }, SchemaType::Configuration => { - schema_for!(Configuration) + repo_schema::() }, SchemaType::ConfigurationExportResult => { - schema_for!(ConfigurationExportResult) + repo_schema::() }, SchemaType::ConfigurationGetResult => { - schema_for!(ConfigurationGetResult) + repo_schema::() }, SchemaType::ConfigurationSetResult => { - schema_for!(ConfigurationSetResult) + repo_schema::() }, SchemaType::ConfigurationTestResult => { - schema_for!(ConfigurationTestResult) + repo_schema::() }, SchemaType::DscResource => { - schema_for!(DscResource) + repo_schema::() }, SchemaType::ExtensionDiscoverResult => { - schema_for!(DiscoverResult) + repo_schema::() }, SchemaType::ExtensionManifest => { - schema_for!(ExtensionManifest) + repo_schema::() }, SchemaType::FunctionDefinition => { - schema_for!(FunctionDefinition) + repo_schema::() }, SchemaType::GetResult => { - schema_for!(GetResult) + repo_schema::() }, SchemaType::Include => { schema_for!(Include) @@ -202,35 +203,48 @@ pub fn get_schema(schema: SchemaType) -> Schema { schema_for!(ManifestList) }, SchemaType::ResolveResult => { - schema_for!(ResolveResult) + repo_schema::() }, SchemaType::Resource => { - schema_for!(Resource) + repo_schema::() }, SchemaType::ResourceGetResult => { - schema_for!(ResourceGetResult) + repo_schema::() }, SchemaType::ResourceSetResult => { - schema_for!(ResourceSetResult) + repo_schema::() }, SchemaType::ResourceTestResult => { - schema_for!(ResourceTestResult) + repo_schema::() }, SchemaType::ResourceManifest => { - schema_for!(ResourceManifest) + repo_schema::() }, SchemaType::RestartRequired => { - schema_for!(RestartRequired) + repo_schema::() }, SchemaType::SetResult => { - schema_for!(SetResult) + repo_schema::() }, SchemaType::TestResult => { - schema_for!(TestResult) + repo_schema::() }, } } +fn repo_schema() -> Schema { + let schema_form = if T::SCHEMA_SHOULD_BUNDLE { + SchemaForm::Bundled + } else { + SchemaForm::Canonical + }; + T::generate_schema( + RecognizedSchemaVersion::default(), + schema_form, + SchemaUriPrefix::AkaDotMs + ) +} + /// Write the JSON object to the console /// /// # Arguments diff --git a/lib/dsc-lib-jsonschema/locales/en-us.toml b/lib/dsc-lib-jsonschema/locales/en-us.toml index d0a0b263b..1c28d2d4b 100644 --- a/lib/dsc-lib-jsonschema/locales/en-us.toml +++ b/lib/dsc-lib-jsonschema/locales/en-us.toml @@ -5,6 +5,10 @@ unrecognizedSchemaUri = "Unrecognized $schema URI" validSchemaUrisAre = "Valid schema URIs are" missingTranslation = "unable to retrieve translation for undefined key '#{key}'" +[dsc_repo.recognized_schema_version] +unrecognizedVersion = "Unrecognized schema version folder" +validVersionsAre = "Valid schema version folders are" + [transforms.idiomaticize_externally_tagged_enum] applies_to = "invalid application of idiomaticize_externally_tagged_enum; missing 'oneOf' keyword in transforming schema: %{transforming_schema}" oneOf_array = "invalid application of idiomaticize_externally_tagged_enum; 'oneOf' isn't an array in transforming schema: %{transforming_schema}" diff --git a/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs b/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs index 40a99f2e0..e149350e5 100644 --- a/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs +++ b/lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs @@ -14,6 +14,7 @@ pub use crate::schema_i18n; mod recognized_schema_version; pub use recognized_schema_version::RecognizedSchemaVersion; +pub use recognized_schema_version::UnrecognizedSchemaVersion; mod schema_form; pub use schema_form::SchemaForm; diff --git a/lib/dsc-lib-jsonschema/src/dsc_repo/recognized_schema_version.rs b/lib/dsc-lib-jsonschema/src/dsc_repo/recognized_schema_version.rs index 38ac3161d..dcb25f86f 100644 --- a/lib/dsc-lib-jsonschema/src/dsc_repo/recognized_schema_version.rs +++ b/lib/dsc-lib-jsonschema/src/dsc_repo/recognized_schema_version.rs @@ -6,4 +6,56 @@ //! checks the git tags for non-prerelease versions of DSC to generate the enum type with all of the //! correct values. The enum can be used transparently throughout the rest of the libraries. +use rust_i18n::t; +use thiserror::Error; + include!(concat!(env!("OUT_DIR"), "/recognized_schema_version.rs")); + +/// Defines the error when parsing a string that isn't a recognized schema version folder. +#[derive(Error, Debug, Clone, PartialEq)] +#[error( + "{t}: {0}. {t2}: {1:?}", + t = t!("dsc_repo.recognized_schema_version.unrecognizedVersion"), + t2 = t!("dsc_repo.recognized_schema_version.validVersionsAre") +)] +pub struct UnrecognizedSchemaVersion(pub String, pub Vec); + +impl std::str::FromStr for RecognizedSchemaVersion { + type Err = UnrecognizedSchemaVersion; + + fn from_str(s: &str) -> Result { + let candidate = s.trim(); + Self::all() + .into_iter() + .find(|version| version.to_string().eq_ignore_ascii_case(candidate)) + .ok_or_else(|| UnrecognizedSchemaVersion( + candidate.to_string(), + Self::all().iter().map(ToString::to_string).collect() + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_str_round_trips_every_recognized_version() { + for version in RecognizedSchemaVersion::all() { + let parsed: RecognizedSchemaVersion = version.to_string().parse().unwrap(); + assert_eq!(parsed, version); + } + } + + #[test] + fn from_str_is_case_insensitive_and_trims() { + let parsed: RecognizedSchemaVersion = " VNEXT ".parse().unwrap(); + assert_eq!(parsed, RecognizedSchemaVersion::VNext); + } + + #[test] + fn from_str_rejects_unrecognized_versions() { + assert!("v99.0.0".parse::().is_err()); + assert!("not-a-version".parse::().is_err()); + } +} diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index e13d38db7..d825d5738 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -25,7 +25,7 @@ pub enum AdaptedPathOrContent { #[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema( base_name = "manifest", - folder_path = "resource", + folder_path = "adaptedresource", should_bundle = true, schema_field( name = schema_version, @@ -36,7 +36,7 @@ pub enum AdaptedPathOrContent { pub struct AdaptedDscResourceManifest { /// The version of the resource manifest schema. #[serde(rename = "$schema")] - #[schemars(schema_with = "ResourceManifest::recognized_schema_uris_subschema")] + #[schemars(schema_with = "AdaptedDscResourceManifest::recognized_schema_uris_union_subschema")] pub schema_version: String, /// The namespaced name of the resource. #[serde(rename="type")] @@ -64,3 +64,16 @@ pub struct AdaptedDscResourceManifest { /// The JSON Schema of the resource. pub schema: Map, } + +impl AdaptedDscResourceManifest { + fn recognized_schema_uris_union_subschema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + let mut subschema = ::recognized_schema_uris_subschema(generator); + let uris: Vec = Self::recognized_schema_uris() + .into_iter() + .chain(ResourceManifest::recognized_schema_uris()) + .map(Value::String) + .collect(); + subschema.insert("enum".to_string(), Value::Array(uris)); + subschema + } +} diff --git a/lib/dsc-lib/src/dscresources/invoke_result.rs b/lib/dsc-lib/src/dscresources/invoke_result.rs index 1658f56a0..9115d2896 100644 --- a/lib/dsc-lib/src/dscresources/invoke_result.rs +++ b/lib/dsc-lib/src/dscresources/invoke_result.rs @@ -193,7 +193,7 @@ pub struct DeleteResult { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[dsc_repo_schema(base_name = "delete", folder_path = "outputs/resource")] +#[dsc_repo_schema(base_name = "delete.whatIf", folder_path = "outputs/resource")] #[serde(deny_unknown_fields)] pub struct DeleteWhatIfResult { #[serde(rename = "whatIf", skip_serializing_if = "Option::is_none")] diff --git a/xtask/locales/en-us.toml b/xtask/locales/en-us.toml index 38f87e629..dba331f7a 100644 --- a/xtask/locales/en-us.toml +++ b/xtask/locales/en-us.toml @@ -2,7 +2,14 @@ about = "xtask provides build helpers for the DSC rust project." schemaAbout = "Commands for managing DSC repository schemas." schemaExportAbout = "Export DSC schemas to disk." +schemaExportVersionHelp = "The schema version folder to export, like 'v3.2' or 'vNext'. May be specified multiple times. Defaults to 'vNext'." +schemaExportReleaseHelp = "A release version like '3.3.0'. Exports the patch, minor, and major version folders for the release, like 'v3.3.0', 'v3.3', and 'v3'." + +[main] +invalidReleaseVersion = "Invalid release version; expected a full version like '3.3.0'" +unrecognizedReleaseFolder = "Schema version folder isn't recognized; if the release was just tagged, refresh 'lib/dsc-lib-jsonschema/.versions.json' by running '.versions.ps1' and rebuild" [schemas.export] serializationFailure = "Failed to serialize JSON Schema as string" ioError = "Failed to export JSON Schema, IO error" +duplicatePath = "Multiple schemas export to the same path; check the `dsc_repo_schema` attributes for a `base_name`/`folder_path` collision" diff --git a/xtask/src/args.rs b/xtask/src/args.rs index 03e8001ba..eb54330b4 100644 --- a/xtask/src/args.rs +++ b/xtask/src/args.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use clap::{Parser, Subcommand}; +use dsc_lib::schemas::dsc_repo::RecognizedSchemaVersion; use rust_i18n::t; #[derive(Debug, Parser)] @@ -24,5 +25,12 @@ pub enum SubCommand { #[derive(Debug, PartialEq, Eq, Subcommand)] pub enum SchemaSubCommand { #[clap(name = "export", about = t!("args.schemaExportAbout").to_string())] - Export + Export { + /// The schema version folder(s) to export. Repeatable. Defaults to `vNext`. + #[clap(long = "schema-version", help = t!("args.schemaExportVersionHelp").to_string())] + schema_versions: Vec, + /// A release version that expands to its patch, minor, and major version folders. + #[clap(long = "release", conflicts_with = "schema_versions", help = t!("args.schemaExportReleaseHelp").to_string())] + release: Option, + } } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 35ef085d2..1f60e95e1 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -3,7 +3,7 @@ use clap::Parser; use dsc_lib::schemas::dsc_repo::RecognizedSchemaVersion; -use rust_i18n::i18n; +use rust_i18n::{i18n, t}; use thiserror::Error; use crate::{ @@ -19,7 +19,11 @@ pub(crate) mod schemas { #[derive(Debug, Error)] pub(crate) enum XTaskError { #[error(transparent)] - SchemaExport(#[from] SchemaExportError) + SchemaExport(#[from] SchemaExportError), + #[error("{t}: {0}", t = t!("main.invalidReleaseVersion"))] + InvalidReleaseVersion(String), + #[error("{t}: {0}", t = t!("main.unrecognizedReleaseFolder"))] + UnrecognizedReleaseFolder(String), } i18n!("locales", fallback = "en-us"); @@ -29,10 +33,85 @@ fn main() -> Result<(), XTaskError> { match args.subcommand { SubCommand::Schema { sub_command } => match sub_command { - SchemaSubCommand::Export => { - export_schemas(RecognizedSchemaVersion::VNext)?; + SchemaSubCommand::Export { schema_versions, release } => { + for schema_version in resolve_export_versions(schema_versions, release.as_deref())? { + export_schemas(schema_version)?; + } Ok(()) }, }, } } + +fn resolve_export_versions( + schema_versions: Vec, + release: Option<&str> +) -> Result, XTaskError> { + let Some(release) = release else { + return Ok(if schema_versions.is_empty() { + vec![RecognizedSchemaVersion::VNext] + } else { + schema_versions + }); + }; + + let version = release.trim().trim_start_matches('v'); + let segments: Vec<&str> = version.split('.').collect(); + let is_numeric = |segment: &&str| !segment.is_empty() && segment.chars().all(|c| c.is_ascii_digit()); + if segments.len() != 3 || !segments.iter().all(is_numeric) { + return Err(XTaskError::InvalidReleaseVersion(release.to_string())); + } + + let folders = [ + format!("v{}.{}.{}", segments[0], segments[1], segments[2]), + format!("v{}.{}", segments[0], segments[1]), + format!("v{}", segments[0]), + ]; + folders.iter().map(|folder| { + folder.parse::() + .map_err(|_| XTaskError::UnrecognizedReleaseFolder(folder.clone())) + }).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_defaults_to_vnext() { + let versions = resolve_export_versions(Vec::new(), None).unwrap(); + assert_eq!(versions, vec![RecognizedSchemaVersion::VNext]); + } + + #[test] + fn resolve_passes_through_explicit_versions() { + let requested = vec![RecognizedSchemaVersion::VNext, RecognizedSchemaVersion::default()]; + let versions = resolve_export_versions(requested.clone(), None).unwrap(); + assert_eq!(versions, requested); + } + + #[test] + fn resolve_release_expands_to_patch_minor_and_major_folders() { + let latest = RecognizedSchemaVersion::latest().to_string(); + let release = latest.trim_start_matches('v').to_string(); + let versions = resolve_export_versions(Vec::new(), Some(&release)).unwrap(); + assert_eq!(versions.len(), 3); + assert_eq!(versions[0].to_string(), latest); + } + + #[test] + fn resolve_release_rejects_partial_versions() { + assert!(matches!( + resolve_export_versions(Vec::new(), Some("3.2")), + Err(XTaskError::InvalidReleaseVersion(_)) + )); + } + + #[test] + fn resolve_release_rejects_unrecognized_versions() { + assert!(matches!( + resolve_export_versions(Vec::new(), Some("99.0.0")), + Err(XTaskError::UnrecognizedReleaseFolder(_)) + )); + } +} diff --git a/xtask/src/schemas/export.rs b/xtask/src/schemas/export.rs index 936ec8cd2..fdfb4a8b8 100644 --- a/xtask/src/schemas/export.rs +++ b/xtask/src/schemas/export.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::{fs, ops::Add, path::PathBuf, sync::LazyLock}; +use std::{collections::HashSet, fs, ops::Add, path::PathBuf, sync::LazyLock}; use dsc_lib::schemas::{ dsc_repo::{DscRepoSchema, RecognizedSchemaVersion}, @@ -20,6 +20,10 @@ pub(crate) enum SchemaExportError { /// Raised when an IO error prevents exporting a schema to the file system. #[error("{t}: {0}", t = t!("schemas.export.ioError"))] IOError(#[from] std::io::Error), + /// Raised when two exported types resolve to the same file path, which would silently + /// overwrite one schema with another. + #[error("{t}: {0}", t = t!("schemas.export.duplicatePath"))] + DuplicatePath(String), } /// Helper static to retrieve the root folder once and use repeatedly when exporting schemas to the @@ -30,8 +34,18 @@ static PROJECT_DIR: LazyLock = LazyLock::new(|| { }); /// Writes the given JSON Schema to the filesystem relative to the project folder. -pub(crate) fn write_schema(relative_path: PathBuf, schema: Schema) -> Result<(), SchemaExportError> { - +/// +/// The `written_paths` set records every path exported during the run so that two types +/// resolving to the same path fail the export instead of silently overwriting each other. +pub(crate) fn write_schema( + written_paths: &mut HashSet, + relative_path: PathBuf, + schema: Schema +) -> Result<(), SchemaExportError> { + if !written_paths.insert(relative_path.clone()) { + return Err(SchemaExportError::DuplicatePath(relative_path.display().to_string())); + } + let json_schema = serde_json::to_string_pretty(&schema.to_value_with_stable_order())?.add("\n"); let path = PROJECT_DIR.clone().join("schemas").join(relative_path); let folder = path.parent().unwrap(); @@ -46,11 +60,12 @@ pub(crate) fn write_schema(relative_path: PathBuf, schema: Schema) -> Result<(), } macro_rules! export_type_schemas { - ($schema_version:expr => $($type_to_export:ty),+) => { + ($written_paths:expr, $schema_version:expr => $($type_to_export:ty),+) => { { $( for schema_form in <$type_to_export>::get_valid_schema_forms() { write_schema( + $written_paths, <$type_to_export>::get_schema_relative_path($schema_version, schema_form).into(), <$type_to_export>::generate_exportable_schema($schema_version, schema_form) )?; @@ -63,16 +78,21 @@ macro_rules! export_type_schemas { pub(crate) fn export_schemas( schema_version: RecognizedSchemaVersion ) -> Result<(), SchemaExportError> { + let mut written_paths: HashSet = HashSet::new(); export_type_schemas!( + &mut written_paths, schema_version => + dsc_lib::configure::config_doc::ConfigDirective, dsc_lib::configure::config_doc::Configuration, dsc_lib::configure::config_doc::DataType, + dsc_lib::configure::config_doc::ExecutionInformation, dsc_lib::configure::config_doc::ExecutionKind, dsc_lib::configure::config_doc::Metadata, dsc_lib::configure::config_doc::Operation, dsc_lib::configure::config_doc::Output, dsc_lib::configure::config_doc::Parameter, dsc_lib::configure::config_doc::Resource, + dsc_lib::configure::config_doc::ResourceDirective, dsc_lib::configure::config_doc::ResourceDiscoveryMode, dsc_lib::configure::config_doc::RestartRequired, dsc_lib::configure::config_doc::SecurityContextKind, @@ -87,6 +107,7 @@ pub(crate) fn export_schemas( dsc_lib::configure::config_result::ResourceGetResult, dsc_lib::configure::config_result::ResourceMessage, dsc_lib::configure::config_result::ResourceSetResult, + dsc_lib::configure::config_result::ResourceTestResult, dsc_lib::dscresources::adapted_resource_manifest::AdaptedDscResourceManifest, dsc_lib::dscresources::dscresource::Capability, dsc_lib::dscresources::dscresource::DscResource, @@ -104,6 +125,7 @@ pub(crate) fn export_schemas( dsc_lib::dscresources::resource_manifest::Adapter, dsc_lib::dscresources::resource_manifest::DeleteMethod, dsc_lib::dscresources::resource_manifest::ExportMethod, + dsc_lib::dscresources::resource_manifest::ExportSchemaKind, dsc_lib::dscresources::resource_manifest::GetArgKind, dsc_lib::dscresources::resource_manifest::GetMethod, dsc_lib::dscresources::resource_manifest::InputKind, @@ -111,6 +133,7 @@ pub(crate) fn export_schemas( dsc_lib::dscresources::resource_manifest::ResolveMethod, dsc_lib::dscresources::resource_manifest::ResourceManifest, dsc_lib::dscresources::resource_manifest::ReturnKind, + dsc_lib::dscresources::resource_manifest::SchemaArgKind, dsc_lib::dscresources::resource_manifest::SchemaKind, dsc_lib::dscresources::resource_manifest::SetDeleteArgKind, dsc_lib::dscresources::resource_manifest::SetMethod, @@ -126,12 +149,14 @@ pub(crate) fn export_schemas( dsc_lib::functions::FunctionArgKind, dsc_lib::functions::FunctionCategory, dsc_lib::functions::FunctionDefinition, + dsc_lib::types::DateVersion, dsc_lib::types::ExitCodesMap, dsc_lib::types::FullyQualifiedTypeName, dsc_lib::types::ResourceVersion, dsc_lib::types::ResourceVersionReq, dsc_lib::types::SemanticVersion, dsc_lib::types::SemanticVersionReq, + dsc_lib::types::Tag, dsc_lib::types::TagList ); From a43c5c84b7e218ade797d537b06db03b78ea5b08 Mon Sep 17 00:00:00 2001 From: Gijs Reijn <26114636+Gijsreyn@users.noreply.github.com> Date: Thu, 13 Aug 2026 06:24:01 +0200 Subject: [PATCH 3/6] Update lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs Co-authored-by: Mikey Lombardi (He/Him) --- lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index d825d5738..fcb8b1c6e 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -25,7 +25,7 @@ pub enum AdaptedPathOrContent { #[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema( base_name = "manifest", - folder_path = "adaptedresource", + folder_path = "resource/adapted", should_bundle = true, schema_field( name = schema_version, From ab3c4edaa37b5aef2e7d27d6db7b05a46a715d51 Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:42:07 +0200 Subject: [PATCH 4/6] Update schema URIs in adapted resource manifests to use the new resource schema --- ...TestClassResource.dsc.adaptedResource.json | 2 +- .../powershell/powershell.discover.tests.ps1 | 2 +- extensions/test/discover/discover.ps1 | 2 +- lib/dsc-lib/locales/en-us.toml | 2 ++ .../src/discovery/command_discovery.rs | 10 ++++++ .../dscresources/adapted_resource_manifest.rs | 32 ++++++++++++++++--- ...s_personalization.dsc.adaptedResource.yaml | 2 +- .../adaptedTest.dsc.adaptedResource.json | 2 +- .../deprecated/deprecated.dsc.manifests.json | 2 +- tools/dsctest/dsctest.dsc.manifests.json | 4 +-- 10 files changed, 47 insertions(+), 13 deletions(-) diff --git a/adapters/powershell/Tests/PSAdaptedTestClassResource.dsc.adaptedResource.json b/adapters/powershell/Tests/PSAdaptedTestClassResource.dsc.adaptedResource.json index 1e12f92a9..0a582046e 100644 --- a/adapters/powershell/Tests/PSAdaptedTestClassResource.dsc.adaptedResource.json +++ b/adapters/powershell/Tests/PSAdaptedTestClassResource.dsc.adaptedResource.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json", "type": "PSAdaptedTestClassResource/PSAdaptedTestClass", "kind": "resource", "version": "0.1.0", diff --git a/extensions/powershell/powershell.discover.tests.ps1 b/extensions/powershell/powershell.discover.tests.ps1 index 783df14c8..9c0abadfb 100644 --- a/extensions/powershell/powershell.discover.tests.ps1 +++ b/extensions/powershell/powershell.discover.tests.ps1 @@ -22,7 +22,7 @@ BeforeAll { $fakeManifest | ConvertTo-Json -Depth 10 | Set-Content -Path $manifestPath $fakeAdaptedManifest = @{ - '$schema' = "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json" + '$schema' = "https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json" type = "Test/FakeAdaptedResource" kind = "resource" version = "0.1.0" diff --git a/extensions/test/discover/discover.ps1 b/extensions/test/discover/discover.ps1 index 8bf249154..4ed693dbf 100644 --- a/extensions/test/discover/discover.ps1 +++ b/extensions/test/discover/discover.ps1 @@ -14,7 +14,7 @@ if ($Extensions) { foreach ($extension in $Extensions.Split(',')) { $resource = [pscustomobject]@{ manifestContent = @{ - '$schema' = "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json" + '$schema' = "https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json" type = "TestDiscover/$count" kind = "resource" version = "1.0.0" diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index ac656d528..cfde157b0 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -125,6 +125,7 @@ discoverResources = "Discovering '%{kind}' using filter: %{filter}" progressSearching = "Searching for resources" extensionSearching = "Searching for extensions" foundManifest = "Found manifest: %{path}" +adaptedResourceDeprecatedSchemaUri = "Adapted resource manifest '%{path}' uses the deprecated schema URI '%{uri}'. Update the `$schema` value to '%{default_uri}'" extensionFound = "Extension '%{extension}' version %{version} found" adapterFound = "Resource adapter '%{adapter}' version %{version} found" resourceFound = "Resource '%{resource}' version %{version} found" @@ -255,6 +256,7 @@ resourceManifestSchemaTitle = "Resource manifest schema URI" resourceManifestSchemaDescription = "Defines the JSON Schema the resource manifest adheres to." adaptedResourceManifestSchemaTitle = "Adapted resource manifest schema URI" adaptedResourceManifestSchemaDescription = "Defines the JSON Schema the adapted resource manifest adheres to." +adaptedResourceManifestDeprecatedSchemaUri = "This URI is deprecated for adapted resource manifests and only recognized for backwards compatibility. Update the `$schema` value to an adapted resource manifest schema URI, like the default: %{uri}" [extensions.dscextension] discoverNoResults = "No results returned for discovery extension '%{extension}'" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 04a18779e..9796bc4aa 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -12,6 +12,7 @@ use crate::dscerror::DscError; use crate::extensions::dscextension::{self, DscExtension, Capability as ExtensionCapability}; use crate::extensions::extension_manifest::ExtensionManifest; use crate::progress::{ProgressBar, ProgressFormat}; +use crate::schemas::dsc_repo::DscRepoSchema; use crate::schemas::transforms::idiomaticize_externally_tagged_enum; use rust_i18n::t; use schemars::JsonSchema; @@ -866,6 +867,15 @@ pub fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResource )); } + if ResourceManifest::is_recognized_schema_uri(&manifest.schema_version) { + warn!("{}", t!( + "discovery.commandDiscovery.adaptedResourceDeprecatedSchemaUri", + path = path.to_string_lossy(), + uri = manifest.schema_version, + default_uri = AdaptedDscResourceManifest::default_schema_id_uri() + )); + } + let mut resource = DscResource::new(); let directory = path.parent().unwrap(); match &manifest.path_or_content { diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index fcb8b1c6e..1cc4852ac 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -36,7 +36,7 @@ pub enum AdaptedPathOrContent { pub struct AdaptedDscResourceManifest { /// The version of the resource manifest schema. #[serde(rename = "$schema")] - #[schemars(schema_with = "AdaptedDscResourceManifest::recognized_schema_uris_union_subschema")] + #[schemars(schema_with = "AdaptedDscResourceManifest::recognized_schema_uris_with_deprecated_subschema")] pub schema_version: String, /// The namespaced name of the resource. #[serde(rename="type")] @@ -66,14 +66,36 @@ pub struct AdaptedDscResourceManifest { } impl AdaptedDscResourceManifest { - fn recognized_schema_uris_union_subschema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + /// Generates the subschema validating the `$schema` property for this type. + /// + /// Before the adapted resource manifest schema had its own URIs, instances declared the + /// resource manifest schema URIs instead. Those URIs remain accepted so existing manifests + /// stay valid, but are marked as deprecated so tooling can prompt authors to update to this + /// type's own URIs. + fn recognized_schema_uris_with_deprecated_subschema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { let mut subschema = ::recognized_schema_uris_subschema(generator); - let uris: Vec = Self::recognized_schema_uris() + subschema.remove("enum"); + let recognized_uris: Vec = Self::recognized_schema_uris() .into_iter() - .chain(ResourceManifest::recognized_schema_uris()) .map(Value::String) .collect(); - subschema.insert("enum".to_string(), Value::Array(uris)); + let deprecated_uris: Vec = ResourceManifest::recognized_schema_uris() + .into_iter() + .map(Value::String) + .collect(); + subschema.insert("oneOf".to_string(), serde_json::json!([ + { + "enum": recognized_uris, + }, + { + "enum": deprecated_uris, + "deprecated": true, + "deprecationMessage": t!( + "dscresources.resource_manifest.adaptedResourceManifestDeprecatedSchemaUri", + uri = Self::default_schema_id_uri() + ), + }, + ])); subschema } } diff --git a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml index 1628ab6ba..3b51ec906 100644 --- a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml +++ b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml @@ -1,7 +1,7 @@ # Adapted Registry resource for personalization settings defined in: # https://learn.microsoft.com/en-us/windows/apps/develop/settings/settings-common#personalization---colors -$schema: https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json +$schema: https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json type: Microsoft.Windows/Personalization kind: resource version: 1.0.0 diff --git a/tools/dsctest/adaptedTest.dsc.adaptedResource.json b/tools/dsctest/adaptedTest.dsc.adaptedResource.json index f674d5f2d..4d9e52d06 100644 --- a/tools/dsctest/adaptedTest.dsc.adaptedResource.json +++ b/tools/dsctest/adaptedTest.dsc.adaptedResource.json @@ -1,5 +1,5 @@ { - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json", "type": "Adapted/Three", "kind": "resource", "version": "1.0.0", diff --git a/tools/dsctest/deprecated/deprecated.dsc.manifests.json b/tools/dsctest/deprecated/deprecated.dsc.manifests.json index cb0c96f45..ceb5e1ee6 100644 --- a/tools/dsctest/deprecated/deprecated.dsc.manifests.json +++ b/tools/dsctest/deprecated/deprecated.dsc.manifests.json @@ -1,7 +1,7 @@ { "adaptedResources": [ { - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json", "type": "Adapted/Deprecated", "deprecationMessage": "This adapted resource is deprecated", "kind": "resource", diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index e5bcc8a3c..ab490de9d 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -1,7 +1,7 @@ { "adaptedResources": [ { - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json", "type": "Adapted/Two", "kind": "resource", "version": "2.0.0", @@ -38,7 +38,7 @@ } }, { - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json", "type": "Adapted/Four", "kind": "resource", "version": "1.0.0", From 21e5a78ff05cf2a99e8256454caf8536a3acb766 Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:47:49 +0200 Subject: [PATCH 5/6] Remove comment --- lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index 1cc4852ac..c6381a40e 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -66,12 +66,6 @@ pub struct AdaptedDscResourceManifest { } impl AdaptedDscResourceManifest { - /// Generates the subschema validating the `$schema` property for this type. - /// - /// Before the adapted resource manifest schema had its own URIs, instances declared the - /// resource manifest schema URIs instead. Those URIs remain accepted so existing manifests - /// stay valid, but are marked as deprecated so tooling can prompt authors to update to this - /// type's own URIs. fn recognized_schema_uris_with_deprecated_subschema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { let mut subschema = ::recognized_schema_uris_subschema(generator); subschema.remove("enum"); From 98ddc314338d658ae9c6e2d29d68832a7c3c06f9 Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Sat, 15 Aug 2026 05:43:00 +0200 Subject: [PATCH 6/6] Add deprecated endpoint shipped --- lib/dsc-lib/src/discovery/command_discovery.rs | 2 +- lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 9796bc4aa..9a7538e09 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -867,7 +867,7 @@ pub fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResource )); } - if ResourceManifest::is_recognized_schema_uri(&manifest.schema_version) { + if AdaptedDscResourceManifest::is_deprecated_schema_uri(&manifest.schema_version) { warn!("{}", t!( "discovery.commandDiscovery.adaptedResourceDeprecatedSchemaUri", path = path.to_string_lossy(), diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index c6381a40e..0e2dac392 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -66,6 +66,13 @@ pub struct AdaptedDscResourceManifest { } impl AdaptedDscResourceManifest { + pub const LEGACY_SHIPPED_SCHEMA_URI: &'static str = "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json"; + + #[must_use] + pub fn is_deprecated_schema_uri(uri: &String) -> bool { + uri.as_str() == Self::LEGACY_SHIPPED_SCHEMA_URI || ResourceManifest::is_recognized_schema_uri(uri) + } + fn recognized_schema_uris_with_deprecated_subschema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { let mut subschema = ::recognized_schema_uris_subschema(generator); subschema.remove("enum"); @@ -75,6 +82,7 @@ impl AdaptedDscResourceManifest { .collect(); let deprecated_uris: Vec = ResourceManifest::recognized_schema_uris() .into_iter() + .chain([Self::LEGACY_SHIPPED_SCHEMA_URI.to_string()]) .map(Value::String) .collect(); subschema.insert("oneOf".to_string(), serde_json::json!([