Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 34 additions & 20 deletions dsc/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -163,37 +164,37 @@ pub fn add_fields_to_json(json: &str, fields_to_add: &HashMap<String, String>) -
pub fn get_schema(schema: SchemaType) -> Schema {
match schema {
SchemaType::AdaptedDscResourceManifest => {
schema_for!(AdaptedDscResourceManifest)
repo_schema::<AdaptedDscResourceManifest>()
},
SchemaType::Configuration => {
schema_for!(Configuration)
repo_schema::<Configuration>()
},
SchemaType::ConfigurationExportResult => {
schema_for!(ConfigurationExportResult)
repo_schema::<ConfigurationExportResult>()
},
SchemaType::ConfigurationGetResult => {
schema_for!(ConfigurationGetResult)
repo_schema::<ConfigurationGetResult>()
},
SchemaType::ConfigurationSetResult => {
schema_for!(ConfigurationSetResult)
repo_schema::<ConfigurationSetResult>()
},
SchemaType::ConfigurationTestResult => {
schema_for!(ConfigurationTestResult)
repo_schema::<ConfigurationTestResult>()
},
SchemaType::DscResource => {
schema_for!(DscResource)
repo_schema::<DscResource>()
},
SchemaType::ExtensionDiscoverResult => {
schema_for!(DiscoverResult)
repo_schema::<DiscoverResult>()
},
SchemaType::ExtensionManifest => {
schema_for!(ExtensionManifest)
repo_schema::<ExtensionManifest>()
},
SchemaType::FunctionDefinition => {
schema_for!(FunctionDefinition)
repo_schema::<FunctionDefinition>()
},
SchemaType::GetResult => {
schema_for!(GetResult)
repo_schema::<GetResult>()
},
SchemaType::Include => {
schema_for!(Include)
Expand All @@ -202,35 +203,48 @@ pub fn get_schema(schema: SchemaType) -> Schema {
schema_for!(ManifestList)
},
SchemaType::ResolveResult => {
schema_for!(ResolveResult)
repo_schema::<ResolveResult>()
},
SchemaType::Resource => {
schema_for!(Resource)
repo_schema::<Resource>()
},
SchemaType::ResourceGetResult => {
schema_for!(ResourceGetResult)
repo_schema::<ResourceGetResult>()
},
SchemaType::ResourceSetResult => {
schema_for!(ResourceSetResult)
repo_schema::<ResourceSetResult>()
},
SchemaType::ResourceTestResult => {
schema_for!(ResourceTestResult)
repo_schema::<ResourceTestResult>()
},
SchemaType::ResourceManifest => {
schema_for!(ResourceManifest)
repo_schema::<ResourceManifest>()
},
SchemaType::RestartRequired => {
schema_for!(RestartRequired)
repo_schema::<RestartRequired>()
},
SchemaType::SetResult => {
schema_for!(SetResult)
repo_schema::<SetResult>()
},
SchemaType::TestResult => {
schema_for!(TestResult)
repo_schema::<TestResult>()
},
}
}

fn repo_schema<T: DscRepoSchema>() -> 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
Expand Down
4 changes: 4 additions & 0 deletions lib/dsc-lib-jsonschema/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
1 change: 1 addition & 0 deletions lib/dsc-lib-jsonschema/src/dsc_repo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
52 changes: 52 additions & 0 deletions lib/dsc-lib-jsonschema/src/dsc_repo/recognized_schema_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>);

impl std::str::FromStr for RecognizedSchemaVersion {
type Err = UnrecognizedSchemaVersion;

fn from_str(s: &str) -> Result<Self, Self::Err> {
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::<RecognizedSchemaVersion>().is_err());
assert!("not-a-version".parse::<RecognizedSchemaVersion>().is_err());
}
}
17 changes: 15 additions & 2 deletions lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "resource/adapted",
should_bundle = true,
schema_field(
name = schema_version,
Expand All @@ -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")]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for using this special associated function instead of recognized_schema_uris_subschema() on this type (I see that it was previously using the same subschema as the ResourceManifest, which was incorrect)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mikey Lombardi (He/Him) (@michaeltlombardi) - I looked a bit back in the history, and this function exists to widen the set of $schema URIs the adapted-manifest schema accepts (as far as my understanding goes). It takes the type's own subschema and replaces its enum with the union of the adapted manifest's URI and the ResourceManifest URIs. Using the plain earlier one would restrict the enum to only the new resource/adapted/manifest.json URIs. And I think that would have invalidated every adapted resource manifest that already exists, because until this branch those documents could only declare a resource manifest schema URI.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this for a bit, I'm torn - the prior versions were incorrectly defined and when you defined $schema in a given document the validator uses that URI to retrieve and validate the data.

I don't think we should merge the lists, not least because this schema isn't valid for many of the early resource schema definitions.

pub schema_version: String,
/// The namespaced name of the resource.
#[serde(rename="type")]
Expand Down Expand Up @@ -64,3 +64,16 @@ pub struct AdaptedDscResourceManifest {
/// The JSON Schema of the resource.
pub schema: Map<String, Value>,
}

impl AdaptedDscResourceManifest {
fn recognized_schema_uris_union_subschema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let mut subschema = <Self as DscRepoSchema>::recognized_schema_uris_subschema(generator);
let uris: Vec<Value> = Self::recognized_schema_uris()
.into_iter()
.chain(ResourceManifest::recognized_schema_uris())
.map(Value::String)
.collect();
subschema.insert("enum".to_string(), Value::Array(uris));
subschema
}
}
2 changes: 1 addition & 1 deletion lib/dsc-lib/src/dscresources/invoke_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
7 changes: 7 additions & 0 deletions xtask/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
10 changes: 9 additions & 1 deletion xtask/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<RecognizedSchemaVersion>,
/// 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<String>,
}
}
87 changes: 83 additions & 4 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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");
Expand All @@ -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<RecognizedSchemaVersion>,
release: Option<&str>
) -> Result<Vec<RecognizedSchemaVersion>, 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::<RecognizedSchemaVersion>()
.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(_))
));
}
}
Loading
Loading