diff --git a/dsc/src/mcp/invoke_dsc_resource.rs b/dsc/src/mcp/invoke_dsc_resource.rs index 9d46d6eab..cf90e9898 100644 --- a/dsc/src/mcp/invoke_dsc_resource.rs +++ b/dsc/src/mcp/invoke_dsc_resource.rs @@ -3,8 +3,7 @@ use crate::mcp::mcp_server::McpServer; use dsc_lib::{ - configure::config_doc::ExecutionKind, - dscresources::{ + DscManager, configure::config_doc::ExecutionKind, dscresources::{ dscresource::Invoke, invoke_result::{ ExportResult, @@ -12,8 +11,7 @@ use dsc_lib::{ SetResult, TestResult, }, - }, - DscManager, + }, types::FullyQualifiedTypeName }; use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; use rust_i18n::t; @@ -51,7 +49,7 @@ pub struct InvokeDscResourceRequest { #[schemars(description = "The operation to perform on the DSC resource")] pub operation: DscOperation, #[schemars(description = "The type name of the DSC resource to invoke")] - pub resource_type: String, + pub resource_type: FullyQualifiedTypeName, #[schemars(description = "The properties to pass to the DSC resource as JSON. Must match the resource JSON schema from `show_dsc_resource` tool.")] pub properties_json: String, } diff --git a/dsc/src/mcp/list_dsc_resources.rs b/dsc/src/mcp/list_dsc_resources.rs index 2f347fb87..c25351c66 100644 --- a/dsc/src/mcp/list_dsc_resources.rs +++ b/dsc/src/mcp/list_dsc_resources.rs @@ -6,7 +6,7 @@ use dsc_lib::{ DscManager, discovery::{ command_discovery::ImportedManifest::Resource, discovery_trait::DiscoveryKind, - }, dscresources::resource_manifest::Kind, progress::ProgressFormat + }, dscresources::resource_manifest::Kind, progress::ProgressFormat, types::FullyQualifiedTypeName }; use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; use rust_i18n::t; @@ -22,11 +22,11 @@ pub struct ResourceListResult { #[derive(Serialize, JsonSchema)] pub struct ResourceSummary { - pub r#type: String, + pub r#type: FullyQualifiedTypeName, pub kind: Kind, pub description: Option, #[serde(rename = "requireAdapter")] - pub require_adapter: Option, + pub require_adapter: Option, } #[derive(Deserialize, JsonSchema)] @@ -70,7 +70,7 @@ impl McpServer { r#type: resource.type_name.clone(), kind: resource.kind.clone(), description: resource.description.clone(), - require_adapter: resource.require_adapter.clone(), + require_adapter: resource.require_adapter, }; resources.insert(resource.type_name.to_lowercase(), summary); } diff --git a/dsc/src/mcp/show_dsc_resource.rs b/dsc/src/mcp/show_dsc_resource.rs index 660fbf312..4a9d11606 100644 --- a/dsc/src/mcp/show_dsc_resource.rs +++ b/dsc/src/mcp/show_dsc_resource.rs @@ -7,7 +7,7 @@ use dsc_lib::{ dscresources::{ dscresource::{Capability, Invoke}, resource_manifest::Kind - }, + }, types::FullyQualifiedTypeName, }; use rmcp::{ErrorData as McpError, Json, tool, tool_router, handler::server::wrapper::Parameters}; use rust_i18n::t; @@ -20,7 +20,7 @@ use tokio::task; pub struct DscResource { /// The namespaced name of the resource. #[serde(rename="type")] - pub type_name: String, + pub type_name: FullyQualifiedTypeName, /// The kind of resource. pub kind: Kind, /// The version of the resource. diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 3132dfedd..800f02767 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -619,7 +619,7 @@ fn list_extensions(dsc: &mut DscManager, extension_name: Option<&String>, format if write_table { table.add_row(vec![ - extension.type_name, + extension.type_name.to_string(), extension.version, capabilities, extension.description.unwrap_or_default() @@ -820,11 +820,11 @@ pub fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adap if write_table { table.add_row(vec![ - resource.type_name, + resource.type_name.to_string(), format!("{:?}", resource.kind), resource.version, capabilities, - resource.require_adapter.unwrap_or_default(), + resource.require_adapter.unwrap_or_default().to_string(), resource.description.unwrap_or_default() ]); } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index d900998e7..55ffb540d 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -709,6 +709,8 @@ forExecutable = "for executable" function = "Function" integerConversion = "Function integer argument conversion" invalidConfiguration = "Invalid configuration" +invalidTypeNamePrefix = "Invalid type name" +invalidTypeNameSuffix = "valid resource type names must match the following pattern" unsupportedManifestVersion = "Unsupported manifest version" mustBe = "Must be" invalidFunctionParameterCount = "Invalid function parameter count for" diff --git a/lib/dsc-lib/locales/schemas.definitions.yaml b/lib/dsc-lib/locales/schemas.definitions.yaml new file mode 100644 index 000000000..2f50f02d6 --- /dev/null +++ b/lib/dsc-lib/locales/schemas.definitions.yaml @@ -0,0 +1,35 @@ +_version: 2 +schemas: + definitions: + resourceType: + title: Fully qualified type name + description: >- + Uniquely identifies a DSC resource or extension. + markdownDescription: |- + The fully qualified type name of a DSC resource or extension uniquely identifies a resource + or extension. + + Fully qualified type names use the following syntax: + + ```yaml + [....]/ + ``` + + Where the type may have zero or more namespace segments for organizing the type. The + `owner`, `namespace`, and `name` segments must consist only of alphanumeric characters and + underscores. + + Conventionally, the first character of each segment is capitalized. When a segment + contains a brand or proper name, use the correct casing for that word, like + `TailspinToys/Settings`, not `Tailspintoys/Settings`. + + Example fully qualified type names include: + + - `Microsoft/OSInfo` + - `Microsoft.SqlServer/Database` + - `Microsoft.Windows.IIS/WebApp` + patternErrorMessage: >- + Invalid type name. Valid resource type names always define an owner and a name separated by + a slash, like `Microsoft/OSInfo`. Type names may optionally include the group, area, and + subarea segments to namespace the resource under the owner, like + `Microsoft.Windows/Registry`. diff --git a/lib/dsc-lib/src/configure/config_doc.rs b/lib/dsc-lib/src/configure/config_doc.rs index 367d01456..98a361c8e 100644 --- a/lib/dsc-lib/src/configure/config_doc.rs +++ b/lib/dsc-lib/src/configure/config_doc.rs @@ -8,10 +8,10 @@ use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; use std::{collections::HashMap, fmt::Display}; -use crate::schemas::{ +use crate::{schemas::{ dsc_repo::DscRepoSchema, transforms::{idiomaticize_externally_tagged_enum, idiomaticize_string_enum} -}; +}, types::FullyQualifiedTypeName}; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(rename_all = "camelCase")] @@ -383,7 +383,7 @@ pub struct Resource { pub condition: Option, /// The fully qualified name of the resource type #[serde(rename = "type")] - pub resource_type: String, + pub resource_type: FullyQualifiedTypeName, #[serde(skip_serializing_if = "Option::is_none", rename = "apiVersion")] pub api_version: Option, /// A friendly name for the resource instance @@ -452,7 +452,7 @@ impl Resource { #[must_use] pub fn new() -> Self { Self { - resource_type: String::new(), + resource_type: FullyQualifiedTypeName::default(), name: String::new(), depends_on: None, kind: None, diff --git a/lib/dsc-lib/src/configure/config_result.rs b/lib/dsc-lib/src/configure/config_result.rs index 31b7c2a84..31fae6e8b 100644 --- a/lib/dsc-lib/src/configure/config_result.rs +++ b/lib/dsc-lib/src/configure/config_result.rs @@ -8,6 +8,7 @@ use serde_json::{Map, Value}; use crate::dscresources::invoke_result::{GetResult, SetResult, TestResult}; use crate::configure::config_doc::{Configuration, Metadata}; use crate::schemas::{dsc_repo::DscRepoSchema, transforms::idiomaticize_string_enum}; +use crate::types::FullyQualifiedTypeName; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase")] @@ -24,7 +25,7 @@ pub enum MessageLevel { pub struct ResourceMessage { pub name: String, #[serde(rename="type")] - pub resource_type: String, + pub resource_type: FullyQualifiedTypeName, pub message: String, pub level: MessageLevel, } @@ -37,7 +38,7 @@ pub struct ResourceGetResult { pub metadata: Option, pub name: String, #[serde(rename="type")] - pub resource_type: String, + pub resource_type: FullyQualifiedTypeName, pub result: GetResult, } @@ -108,7 +109,7 @@ pub struct ResourceSetResult { pub metadata: Option, pub name: String, #[serde(rename="type")] - pub resource_type: String, + pub resource_type: FullyQualifiedTypeName, pub result: SetResult, } @@ -184,7 +185,7 @@ pub struct ResourceTestResult { pub metadata: Option, pub name: String, #[serde(rename="type")] - pub resource_type: String, + pub resource_type: FullyQualifiedTypeName, pub result: TestResult, } diff --git a/lib/dsc-lib/src/configure/depends_on.rs b/lib/dsc-lib/src/configure/depends_on.rs index 2364804f3..dae5fca9d 100644 --- a/lib/dsc-lib/src/configure/depends_on.rs +++ b/lib/dsc-lib/src/configure/depends_on.rs @@ -5,6 +5,7 @@ use crate::configure::config_doc::Resource; use crate::configure::{Configuration, IntOrExpression, ProcessMode, invoke_property_expressions}; use crate::DscError; use crate::parser::Statement; +use crate::types::FullyQualifiedTypeName; use rust_i18n::t; use serde_json::Value; @@ -131,14 +132,15 @@ fn unroll_and_push(order: &mut Vec, resource: &Resource, parser: &mut Ok(()) } -fn get_type_and_name(statement: &str) -> Result<(&str, String), DscError> { +fn get_type_and_name(statement: &str) -> Result<(FullyQualifiedTypeName, String), DscError> { let parts: Vec<&str> = statement.split(':').collect(); if parts.len() != 2 { return Err(DscError::Validation(t!("configure.dependsOn.syntaxIncorrect", dependency = statement).to_string())); } // the name is url encoded so we need to decode it let decoded_name = urlencoding::decode(parts[1]).map_err(|_| DscError::Validation(t!("configure.dependsOn.syntaxIncorrect", dependency = statement).to_string()))?; - Ok((parts[0], decoded_name.into_owned())) + let type_name = parts[0].parse()?; + Ok((type_name, decoded_name.into_owned())) } #[cfg(test)] diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index 04e96a144..8aeb0e42e 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -364,7 +364,7 @@ impl Configurator { continue; } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { - return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); + return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; let filter = add_metadata(dsc_resource, properties, resource.metadata.clone())?; @@ -448,7 +448,7 @@ impl Configurator { continue; } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { - return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); + return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); @@ -616,7 +616,7 @@ impl Configurator { continue; } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { - return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); + return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); @@ -699,7 +699,7 @@ impl Configurator { continue; } let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { - return Err(DscError::ResourceNotFound(resource.resource_type.clone(), resource.api_version.as_deref().unwrap_or("").to_string())); + return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(resource, &dsc_resource.kind)?; let input = add_metadata(dsc_resource, properties, resource.metadata.clone())?; diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 8479b8cb6..328060e24 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -277,7 +277,7 @@ impl ResourceDiscovery for CommandDiscovery { if regex.is_match(&extension.type_name) { trace!("{}", t!("discovery.commandDiscovery.extensionFound", extension = extension.type_name, version = extension.version)); // we only keep newest version of the extension so compare the version and only keep the newest - if let Some(existing_extension) = extensions.get_mut(&extension.type_name) { + if let Some(existing_extension) = extensions.get_mut(extension.type_name.as_ref()) { let Ok(existing_version) = Version::parse(&existing_extension.version) else { return Err(DscError::Operation(t!("discovery.commandDiscovery.extensionInvalidVersion", extension = existing_extension.type_name, version = existing_extension.version).to_string())); }; @@ -285,10 +285,10 @@ impl ResourceDiscovery for CommandDiscovery { return Err(DscError::Operation(t!("discovery.commandDiscovery.extensionInvalidVersion", extension = extension.type_name, version = extension.version).to_string())); }; if new_version > existing_version { - extensions.insert(extension.type_name.clone(), extension.clone()); + extensions.insert(extension.type_name.to_string(), extension.clone()); } } else { - extensions.insert(extension.type_name.clone(), extension.clone()); + extensions.insert(extension.type_name.to_string(), extension.clone()); } } }, @@ -427,7 +427,7 @@ impl ResourceDiscovery for CommandDiscovery { match serde_json::from_str::(line){ Result::Ok(resource) => { if resource.require_adapter.is_none() { - warn!("{}", DscError::MissingRequires(adapter_name.clone(), resource.type_name.clone()).to_string()); + warn!("{}", DscError::MissingRequires(adapter_name.clone(), resource.type_name.to_string()).to_string()); continue; } diff --git a/lib/dsc-lib/src/dscerror.rs b/lib/dsc-lib/src/dscerror.rs index 2e1488c77..4da9b4d13 100644 --- a/lib/dsc-lib/src/dscerror.rs +++ b/lib/dsc-lib/src/dscerror.rs @@ -58,6 +58,9 @@ pub enum DscError { #[error("{t} '{0}': {1}", t = t!("dscerror.invalidRequiredVersion"))] InvalidRequiredVersion(String, String), + #[error("{t} '{0}' - {t2}: '{1}'", t = t!("dscerror.invalidTypeNamePrefix"), t2 = t!("dscerror.InvalidTypeNameSuffix"))] + InvalidTypeName(String, String), + #[error("IO: {0}")] Io(#[from] std::io::Error), diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 8d7001c03..082895ccf 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -7,7 +7,7 @@ use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; use std::{collections::HashMap, env, path::Path, process::Stdio}; -use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, util::canonicalize_which}; +use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, types::FullyQualifiedTypeName, util::canonicalize_which}; use crate::dscerror::DscError; use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; use tracing::{error, warn, info, debug, trace}; @@ -25,7 +25,7 @@ pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; /// # Errors /// /// Error returned if the resource does not successfully get the current state -pub fn invoke_get(resource: &ResourceManifest, cwd: &Path, filter: &str, target_resource: Option<&str>) -> Result { +pub fn invoke_get(resource: &ResourceManifest, cwd: &Path, filter: &str, target_resource: Option) -> Result { debug!("{}", t!("dscresources.commandResource.invokeGet", resource = &resource.resource_type)); let mut command_input = CommandInput { env: None, stdin: None }; let Some(get) = &resource.get else { @@ -33,9 +33,9 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &Path, filter: &str, target_ }; let resource_type = match target_resource { Some(r) => r, - None => &resource.resource_type, + None => resource.resource_type.clone(), }; - let args = process_args(get.args.as_ref(), filter, resource_type); + let args = process_args(get.args.as_ref(), filter, &resource_type); if !filter.is_empty() { verify_json(resource, cwd, filter)?; command_input = get_command_input(get.input.as_ref(), filter)?; @@ -78,7 +78,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &Path, filter: &str, target_ /// /// Error returned if the resource does not successfully set the desired state #[allow(clippy::too_many_lines)] -pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_test: bool, execution_type: &ExecutionKind, target_resource: Option<&str>) -> Result { +pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_test: bool, execution_type: &ExecutionKind, target_resource: Option) -> Result { debug!("{}", t!("dscresources.commandResource.invokeSet", resource = &resource.resource_type)); let operation_type: String; let mut is_synthetic_what_if = false; @@ -105,7 +105,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t // if resource doesn't implement a pre-test, we execute test first to see if a set is needed if !skip_test && set.pre_test != Some(true) { info!("{}", t!("dscresources.commandResource.noPretest", resource = &resource.resource_type)); - let test_result = invoke_test(resource, cwd, desired, target_resource)?; + let test_result = invoke_test(resource, cwd, desired, target_resource.clone())?; if is_synthetic_what_if { return Ok(test_result.into()); } @@ -140,11 +140,11 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t let Some(get) = &resource.get else { return Err(DscError::NotImplemented("get".to_string())); }; - let resource_type = match target_resource { + let resource_type = match target_resource.clone() { Some(r) => r, - None => &resource.resource_type, + None => resource.resource_type.clone(), }; - let args = process_args(get.args.as_ref(), desired, resource_type); + let args = process_args(get.args.as_ref(), desired, &resource_type); let command_input = get_command_input(get.input.as_ref(), desired)?; info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.resource_type, executable = &get.executable)); @@ -159,7 +159,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t serde_json::from_str(&stdout)? } else { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); + return Err(DscError::Command(resource.resource_type.to_string(), exit_code, stderr)); }; let mut pre_state = if pre_state_value.is_object() { let mut pre_state_map: Map = serde_json::from_value(pre_state_value)?; @@ -176,7 +176,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let args = process_args(set.args.as_ref(), desired, resource_type); + let args = process_args(set.args.as_ref(), desired, &resource_type); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); @@ -220,12 +220,12 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t // command should be returning actual state as a JSON line and a list of properties that differ as separate JSON line let mut lines = stdout.lines(); let Some(actual_line) = lines.next() else { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, t!("dscresources.commandResource.setUnexpectedOutput").to_string())); + return Err(DscError::Command(resource.resource_type.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedOutput").to_string())); }; let actual_value: Value = serde_json::from_str(actual_line)?; // TODO: need schema for diff_properties to validate against let Some(diff_line) = lines.next() else { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, t!("dscresources.commandResource.setUnexpectedDiff").to_string())); + return Err(DscError::Command(resource.resource_type.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedDiff").to_string())); }; let diff_properties: Vec = serde_json::from_str(diff_line)?; Ok(SetResult::Resource(ResourceSetResponse { @@ -271,7 +271,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &Path, desired: &str, skip_t /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_test(resource: &ResourceManifest, cwd: &Path, expected: &str, target_resource: Option<&str>) -> Result { +pub fn invoke_test(resource: &ResourceManifest, cwd: &Path, expected: &str, target_resource: Option) -> Result { debug!("{}", t!("dscresources.commandResource.invokeTest", resource = &resource.resource_type)); let Some(test) = &resource.test else { info!("{}", t!("dscresources.commandResource.testSyntheticTest", resource = &resource.resource_type)); @@ -280,11 +280,11 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &Path, expected: &str, targ verify_json(resource, cwd, expected)?; - let resource_type = match target_resource { + let resource_type = match target_resource.clone() { Some(r) => r, - None => &resource.resource_type, + None => resource.resource_type.clone(), }; - let args = process_args(test.args.as_ref(), expected, resource_type); + let args = process_args(test.args.as_ref(), expected, &resource_type); let command_input = get_command_input(test.input.as_ref(), expected)?; info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.resource_type, executable = &test.executable)); @@ -324,11 +324,11 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &Path, expected: &str, targ // command should be returning actual state as a JSON line and a list of properties that differ as separate JSON line let mut lines = stdout.lines(); let Some(actual_value) = lines.next() else { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, t!("dscresources.commandResource.testNoActualState").to_string())); + return Err(DscError::Command(resource.resource_type.to_string(), exit_code, t!("dscresources.commandResource.testNoActualState").to_string())); }; let actual_value: Value = serde_json::from_str(actual_value)?; let Some(diff_properties) = lines.next() else { - return Err(DscError::Command(resource.resource_type.clone(), exit_code, t!("dscresources.commandResource.testNoDiff").to_string())); + return Err(DscError::Command(resource.resource_type.to_string(), exit_code, t!("dscresources.commandResource.testNoDiff").to_string())); }; let diff_properties: Vec = serde_json::from_str(diff_properties)?; expected_value = redact(&expected_value); @@ -380,7 +380,7 @@ fn get_desired_state(actual: &Value) -> Result, DscError> { Ok(in_desired_state) } -fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &Path, expected: &str, target_resource: Option<&str>) -> Result { +fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &Path, expected: &str, target_resource: Option) -> Result { let get_result = invoke_get(resource, cwd, expected, target_resource)?; let actual_state = match get_result { GetResult::Group(results) => { @@ -481,7 +481,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &Path, config: &str, ta /// Error if schema is not available or if there is an error getting the schema pub fn get_schema(resource: &ResourceManifest, cwd: &Path) -> Result { let Some(schema_kind) = resource.schema.as_ref() else { - return Err(DscError::SchemaNotAvailable(resource.resource_type.clone())); + return Err(DscError::SchemaNotAvailable(resource.resource_type.to_string())); }; match schema_kind { @@ -511,7 +511,7 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &Path) -> Result, target_resource: Option<&str>) -> Result { +pub fn invoke_export(resource: &ResourceManifest, cwd: &Path, input: Option<&str>, target_resource: Option) -> Result { let Some(export) = resource.export.as_ref() else { // see if get is supported and use that instead if resource.get.is_some() { @@ -540,7 +540,7 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &Path, input: Option<&str let args: Option>; let resource_type = match target_resource { Some(r) => r, - None => &resource.resource_type, + None => resource.resource_type.clone(), }; if let Some(input) = input { if !input.is_empty() { @@ -549,9 +549,9 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &Path, input: Option<&str command_input = get_command_input(export.input.as_ref(), input)?; } - args = process_args(export.args.as_ref(), input, resource_type); + args = process_args(export.args.as_ref(), input, &resource_type); } else { - args = process_args(export.args.as_ref(), "", resource_type); + args = process_args(export.args.as_ref(), "", &resource_type); } let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index afb2612a8..cf59133a5 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::{AdapterInputKind, Kind}}; +use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::{AdapterInputKind, Kind}, types::FullyQualifiedTypeName}; use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse}; use crate::schemas::transforms::idiomaticize_string_enum; use dscerror::DscError; @@ -33,7 +33,7 @@ use super::{ pub struct DscResource { /// The namespaced name of the resource. #[serde(rename="type")] - pub type_name: String, + pub type_name: FullyQualifiedTypeName, /// The kind of resource. pub kind: Kind, /// The version of the resource. @@ -55,9 +55,9 @@ pub struct DscResource { pub properties: Vec, /// The required resource adapter for the resource. #[serde(rename="requireAdapter")] - pub require_adapter: Option, + pub require_adapter: Option, /// The target resource for the resource adapter. - pub target_resource: Option, + pub target_resource: Option, /// The manifest of the resource. pub manifest: Option, } @@ -98,7 +98,7 @@ impl DscResource { #[must_use] pub fn new() -> Self { Self { - type_name: String::new(), + type_name: FullyQualifiedTypeName::default(), kind: Kind::Resource, version: String::new(), capabilities: Vec::new(), @@ -114,12 +114,12 @@ impl DscResource { } } - fn create_config_for_adapter(self, adapter: &str, input: &str) -> Result { + fn create_config_for_adapter(self, adapter: &FullyQualifiedTypeName, input: &str) -> Result { // create new configuration with adapter and use this as the resource let mut configuration = Configuration::new(); let mut property_map = Map::new(); - property_map.insert("name".to_string(), Value::String(self.type_name.clone())); - property_map.insert("type".to_string(), Value::String(self.type_name.clone())); + property_map.insert("name".to_string(), Value::String(self.type_name.to_string())); + property_map.insert("type".to_string(), Value::String(self.type_name.to_string())); if !input.is_empty() { let resource_properties: Value = serde_json::from_str(input)?; property_map.insert("properties".to_string(), resource_properties); @@ -127,8 +127,8 @@ impl DscResource { let mut resources_map = Map::new(); resources_map.insert("resources".to_string(), Value::Array(vec![Value::Object(property_map)])); let adapter_resource = Resource { - name: self.type_name.clone(), - resource_type: adapter.to_string(), + name: self.type_name.to_string(), + resource_type: adapter.parse()?, properties: Some(resources_map), ..Default::default() }; @@ -140,11 +140,11 @@ impl DscResource { Ok(configurator) } - fn invoke_get_with_adapter(&self, adapter: &str, resource_name: &str, filter: &str) -> Result { + fn invoke_get_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, filter: &str) -> Result { let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { - adapter.target_resource = Some(resource_name.to_string()); + adapter.target_resource = Some(resource_name.clone()); return adapter.get(filter); } @@ -164,11 +164,11 @@ impl DscResource { Ok(get_result) } - fn invoke_set_with_adapter(&self, adapter: &str, resource_name: &str, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result { + fn invoke_set_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result { let mut configurator = self.clone().create_config_for_adapter(adapter, desired)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { - adapter.target_resource = Some(resource_name.to_string()); + adapter.target_resource = Some(resource_name.clone()); return adapter.set(desired, skip_test, execution_type); } @@ -197,11 +197,11 @@ impl DscResource { Ok(set_result) } - fn invoke_test_with_adapter(&self, adapter: &str, resource_name: &str, expected: &str) -> Result { + fn invoke_test_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, expected: &str) -> Result { let mut configurator = self.clone().create_config_for_adapter(adapter, expected)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { - adapter.target_resource = Some(resource_name.to_string()); + adapter.target_resource = Some(resource_name.clone()); return adapter.test(expected); } @@ -231,12 +231,12 @@ impl DscResource { Ok(test_result) } - fn invoke_delete_with_adapter(&self, adapter: &str, resource_name: &str, filter: &str) -> Result<(), DscError> { + fn invoke_delete_with_adapter(&self, adapter: &FullyQualifiedTypeName, resource_name: &FullyQualifiedTypeName, filter: &str) -> Result<(), DscError> { let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { if adapter.capabilities.contains(&Capability::Delete) { - adapter.target_resource = Some(resource_name.to_string()); + adapter.target_resource = Some(resource_name.clone()); return adapter.delete(filter); } return Err(DscError::NotSupported(t!("dscresources.dscresource.adapterDoesNotSupportDelete", adapter = adapter.type_name).to_string())); @@ -246,7 +246,7 @@ impl DscResource { Ok(()) } - fn invoke_export_with_adapter(&self, adapter: &str, input: &str) -> Result { + fn invoke_export_with_adapter(&self, adapter: &FullyQualifiedTypeName, input: &str) -> Result { let mut configurator = self.clone().create_config_for_adapter(adapter, input)?; let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { @@ -276,7 +276,7 @@ impl DscResource { Ok(export_result) } - fn get_adapter_resource(configurator: &mut Configurator, adapter: &str) -> Result { + fn get_adapter_resource(configurator: &mut Configurator, adapter: &FullyQualifiedTypeName) -> Result { if let Some(adapter_resource) = configurator.discovery().find_resource(adapter, None) { return Ok(adapter_resource.clone()); } @@ -391,10 +391,10 @@ impl Invoke for DscResource { }, ImplementedAs::Command => { let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); + return Err(DscError::MissingManifest(self.type_name.to_string())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_get(&resource_manifest, &self.directory, filter, self.target_resource.as_deref()) + command_resource::invoke_get(&resource_manifest, &self.directory, filter, self.target_resource.clone()) }, } } @@ -411,10 +411,10 @@ impl Invoke for DscResource { }, ImplementedAs::Command => { let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); + return Err(DscError::MissingManifest(self.type_name.to_string())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, execution_type, self.target_resource.as_deref()) + command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, execution_type, self.target_resource.clone()) }, } } @@ -431,7 +431,7 @@ impl Invoke for DscResource { }, ImplementedAs::Command => { let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); + return Err(DscError::MissingManifest(self.type_name.to_string())); }; // if test is not directly implemented, then we need to handle it here @@ -462,7 +462,7 @@ impl Invoke for DscResource { Ok(test_result) } else { - command_resource::invoke_test(&resource_manifest, &self.directory, expected, self.target_resource.as_deref()) + command_resource::invoke_test(&resource_manifest, &self.directory, expected, self.target_resource.clone()) } }, } @@ -480,7 +480,7 @@ impl Invoke for DscResource { }, ImplementedAs::Command => { let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); + return Err(DscError::MissingManifest(self.type_name.to_string())); }; let resource_manifest = import_manifest(manifest.clone())?; command_resource::invoke_delete(&resource_manifest, &self.directory, filter, self.target_resource.as_deref()) @@ -500,7 +500,7 @@ impl Invoke for DscResource { }, ImplementedAs::Command => { let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); + return Err(DscError::MissingManifest(self.type_name.to_string())); }; let resource_manifest = import_manifest(manifest.clone())?; command_resource::invoke_validate(&resource_manifest, &self.directory, config, self.target_resource.as_deref()) @@ -520,7 +520,7 @@ impl Invoke for DscResource { }, ImplementedAs::Command => { let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); + return Err(DscError::MissingManifest(self.type_name.to_string())); }; let resource_manifest = import_manifest(manifest.clone())?; command_resource::get_schema(&resource_manifest, &self.directory) @@ -535,10 +535,10 @@ impl Invoke for DscResource { } let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); + return Err(DscError::MissingManifest(self.type_name.to_string())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_export(&resource_manifest, &self.directory, Some(input), self.target_resource.as_deref()) + command_resource::invoke_export(&resource_manifest, &self.directory, Some(input), self.target_resource.clone()) } fn resolve(&self, input: &str) -> Result { @@ -548,7 +548,7 @@ impl Invoke for DscResource { } let Some(manifest) = &self.manifest else { - return Err(DscError::MissingManifest(self.type_name.clone())); + return Err(DscError::MissingManifest(self.type_name.to_string())); }; let resource_manifest = import_manifest(manifest.clone())?; command_resource::invoke_resolve(&resource_manifest, &self.directory, input) diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index 4e9a19925..d0dd3b48e 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; use crate::{ dscerror::DscError, schemas::{dsc_repo::DscRepoSchema, transforms::idiomaticize_string_enum}, + types::FullyQualifiedTypeName, }; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] @@ -44,7 +45,7 @@ pub struct ResourceManifest { pub schema_version: String, /// The namespaced name of the resource. #[serde(rename = "type")] - pub resource_type: String, + pub resource_type: FullyQualifiedTypeName, /// An optional condition for the resource to be active. If the condition evaluates to false, the resource is skipped. #[serde(skip_serializing_if = "Option::is_none")] pub condition: Option, @@ -332,7 +333,7 @@ mod test { let manifest = ResourceManifest{ schema_version: invalid_uri.clone(), - resource_type: "Microsoft.Dsc.Test/InvalidSchemaUri".to_string(), + resource_type: "Microsoft.Dsc.Test/InvalidSchemaUri".parse().unwrap(), version: "0.1.0".to_string(), ..Default::default() }; @@ -353,7 +354,7 @@ mod test { fn test_validate_schema_uri_with_valid_uri() { let manifest = ResourceManifest{ schema_version: ResourceManifest::default_schema_id_uri(), - resource_type: "Microsoft.Dsc.Test/ValidSchemaUri".to_string(), + resource_type: "Microsoft.Dsc.Test/ValidSchemaUri".parse().unwrap(), version: "0.1.0".to_string(), ..Default::default() }; diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index 11128669c..5aff5e121 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -62,13 +62,13 @@ impl DscExtension { let extension = match serde_json::from_value::(self.manifest.clone()) { Ok(manifest) => manifest, Err(err) => { - return Err(DscError::Manifest(self.type_name.clone(), err)); + return Err(DscError::Manifest(self.type_name.to_string(), err)); } }; let Some(discover) = extension.discover else { - return Err(DscError::UnsupportedCapability(self.type_name.clone(), Capability::Discover.to_string())); + return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Discover.to_string())); }; - let args = process_args(discover.args.as_ref(), "", &self.type_name); + let args = process_args(discover.args.as_ref(), "", self.type_name.as_ref()); let (_exit_code, stdout, _stderr) = invoke_command( &discover.executable, args, @@ -103,7 +103,7 @@ impl DscExtension { Ok(resources) } else { Err(DscError::UnsupportedCapability( - self.type_name.clone(), + self.type_name.to_string(), Capability::Discover.to_string() )) } diff --git a/lib/dsc-lib/src/extensions/dscextension.rs b/lib/dsc-lib/src/extensions/dscextension.rs index 379dedcb6..ad57a9762 100644 --- a/lib/dsc-lib/src/extensions/dscextension.rs +++ b/lib/dsc-lib/src/extensions/dscextension.rs @@ -3,6 +3,7 @@ use crate::extensions::import::ImportMethod; use crate::schemas::{dsc_repo::DscRepoSchema, transforms::idiomaticize_string_enum}; +use crate::types::FullyQualifiedTypeName; use serde::{Deserialize, Serialize}; use serde_json::Value; use schemars::JsonSchema; @@ -15,7 +16,7 @@ use std::path::PathBuf; pub struct DscExtension { /// The namespaced name of the resource. #[serde(rename="type")] - pub type_name: String, + pub type_name: FullyQualifiedTypeName, /// The version of the resource. pub version: String, /// The capabilities of the resource. @@ -61,7 +62,7 @@ impl DscExtension { #[must_use] pub fn new() -> Self { Self { - type_name: String::new(), + type_name: FullyQualifiedTypeName::default(), version: String::new(), capabilities: Vec::new(), import: None, diff --git a/lib/dsc-lib/src/extensions/extension_manifest.rs b/lib/dsc-lib/src/extensions/extension_manifest.rs index 59c95e979..c0bad512f 100644 --- a/lib/dsc-lib/src/extensions/extension_manifest.rs +++ b/lib/dsc-lib/src/extensions/extension_manifest.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; use crate::dscerror::DscError; use crate::extensions::{discover::DiscoverMethod, import::ImportMethod, secret::SecretMethod}; use crate::schemas::dsc_repo::DscRepoSchema; +use crate::types::FullyQualifiedTypeName; #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(deny_unknown_fields)] @@ -31,7 +32,7 @@ pub struct ExtensionManifest { pub schema_version: String, /// The namespaced name of the extension. #[serde(rename = "type")] - pub r#type: String, + pub r#type: FullyQualifiedTypeName, /// The version of the extension using semantic versioning. pub version: String, /// An optional condition for the extension to be active. If the condition evaluates to false, the extension is skipped. @@ -106,7 +107,7 @@ mod test { let manifest = ExtensionManifest{ schema_version: invalid_uri.clone(), - r#type: "Microsoft.Dsc.Test/InvalidSchemaUri".to_string(), + r#type: "Microsoft.Dsc.Test/InvalidSchemaUri".parse().unwrap(), version: "0.1.0".to_string(), ..Default::default() }; @@ -127,7 +128,7 @@ mod test { fn test_validate_schema_uri_with_valid_uri() { let manifest = ExtensionManifest{ schema_version: ExtensionManifest::default_schema_id_uri(), - r#type: "Microsoft.Dsc.Test/ValidSchemaUri".to_string(), + r#type: "Microsoft.Dsc.Test/ValidSchemaUri".parse().unwrap(), version: "0.1.0".to_string(), ..Default::default() }; diff --git a/lib/dsc-lib/src/extensions/import.rs b/lib/dsc-lib/src/extensions/import.rs index d456c235a..661987828 100644 --- a/lib/dsc-lib/src/extensions/import.rs +++ b/lib/dsc-lib/src/extensions/import.rs @@ -75,11 +75,11 @@ impl DscExtension { let extension = match serde_json::from_value::(self.manifest.clone()) { Ok(manifest) => manifest, Err(err) => { - return Err(DscError::Manifest(self.type_name.clone(), err)); + return Err(DscError::Manifest(self.type_name.to_string(), err)); } }; let Some(import) = extension.import else { - return Err(DscError::UnsupportedCapability(self.type_name.clone(), Capability::Import.to_string())); + return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Import.to_string())); }; let args = process_import_args(import.args.as_ref(), file)?; let (_exit_code, stdout, _stderr) = invoke_command( @@ -105,7 +105,7 @@ impl DscExtension { } } Err(DscError::UnsupportedCapability( - self.type_name.clone(), + self.type_name.to_string(), Capability::Import.to_string() )) } diff --git a/lib/dsc-lib/src/extensions/secret.rs b/lib/dsc-lib/src/extensions/secret.rs index 819620b07..2805e2588 100644 --- a/lib/dsc-lib/src/extensions/secret.rs +++ b/lib/dsc-lib/src/extensions/secret.rs @@ -70,11 +70,11 @@ impl DscExtension { let extension = match serde_json::from_value::(self.manifest.clone()) { Ok(manifest) => manifest, Err(err) => { - return Err(DscError::Manifest(self.type_name.clone(), err)); + return Err(DscError::Manifest(self.type_name.to_string(), err)); } }; let Some(secret) = extension.secret else { - return Err(DscError::UnsupportedCapability(self.type_name.clone(), Capability::Secret.to_string())); + return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Secret.to_string())); }; let args = process_secret_args(secret.args.as_ref(), name, vault); let (_exit_code, stdout, _stderr) = invoke_command( @@ -101,7 +101,7 @@ impl DscExtension { } } else { Err(DscError::UnsupportedCapability( - self.type_name.clone(), + self.type_name.to_string(), Capability::Secret.to_string() )) } diff --git a/lib/dsc-lib/src/lib.rs b/lib/dsc-lib/src/lib.rs index 32b0cd320..d4be1d74f 100644 --- a/lib/dsc-lib/src/lib.rs +++ b/lib/dsc-lib/src/lib.rs @@ -18,6 +18,7 @@ pub mod extensions; pub mod functions; pub mod parser; pub mod progress; +pub mod types; pub mod util; // Re-export the dependency crate to minimize dependency management. diff --git a/lib/dsc-lib/src/progress.rs b/lib/dsc-lib/src/progress.rs index 41c4ae54d..66857d01a 100644 --- a/lib/dsc-lib/src/progress.rs +++ b/lib/dsc-lib/src/progress.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use crate::DscError; +use crate::types::FullyQualifiedTypeName; use clap::ValueEnum; use indicatif::ProgressStyle; @@ -52,7 +53,7 @@ pub struct Progress { pub resource_name: Option, /// The type of the resource being operated on. #[serde(skip_serializing_if = "Option::is_none")] - pub resource_type: Option, + pub resource_type: Option, /// The result of the operation. #[serde(skip_serializing_if = "Option::is_none")] pub result: Option, @@ -139,9 +140,9 @@ impl ProgressBar { /// * `resource_type` - The type of the resource being operated on /// * `result` - The result of the operation /// - pub fn set_resource(&mut self, name: &str, resource_type: &str) { + pub fn set_resource(&mut self, name: &str, resource_type: &FullyQualifiedTypeName) { self.progress_value.resource_name = Some(name.to_string()); - self.progress_value.resource_type = Some(resource_type.to_string()); + self.progress_value.resource_type = Some(resource_type.clone()); self.progress_value.result = None; self.progress_value.failure = None; } diff --git a/lib/dsc-lib/src/types/fully_qualified_type_name.rs b/lib/dsc-lib/src/types/fully_qualified_type_name.rs new file mode 100644 index 000000000..8c19468e7 --- /dev/null +++ b/lib/dsc-lib/src/types/fully_qualified_type_name.rs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fmt::{Display, Formatter}; +use std::ops::Deref; +use std::str::FromStr; +use std::sync::OnceLock; + +use regex::Regex; +use rust_i18n::t; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::dscerror::DscError; +use crate::schemas::dsc_repo::DscRepoSchema; + +/// Defines the fully qualified type name for a DSC resource or extension. The fully qualified name +/// uniquely identifies each resource and extension. +#[derive( + Clone, + Debug, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + JsonSchema, + DscRepoSchema, +)] +#[serde(try_from = "String")] +#[schemars( + title = t!("schemas.definitions.resourceType.title"), + description = t!("schemas.definitions.resourceType.description"), + extend( + "pattern" = FullyQualifiedTypeName::VALIDATING_PATTERN, + "patternErrorMessage" = t!("schemas.definitions.resourceType.patternErrorMessage"), + "markdownDescription" = t!("schemas.definitions.resourceType.markdownDescription"), + ) +)] +#[dsc_repo_schema(base_name = "resourceType", folder_path = "definitions")] +pub struct FullyQualifiedTypeName(String); + +/// This static lazily defines the validating regex for [`FullyQualifiedTypeName`]. It enables the +/// [`Regex`] instance to be constructed once, the first time it's used, and then reused on all +/// subsequent validation calls. It's kept private, since the API usage is to invoke the +/// [`FullyQualifiedTypeName::validate()`] method for direct validation or to leverage this static +/// from within the constructor for [`FullyQualifiedTypeName`]. +static VALIDATING_REGEX: OnceLock = OnceLock::new(); + +impl FullyQualifiedTypeName { + /// Defines the regular expression for validating a string as a fully qualified type name. + /// + /// The string must begin with one or more alphanumeric characters and underscores that define + /// the `owner` for the type. Following the `owner` segment, the string may include any number + /// of `namespace` segments, which must be separated from the previous segment by a single + /// period (`.`). Finally, the string must include a forward slash (`/`) followed by one or + /// more alphanumeric characters and underscores to define the `name` segment. + pub const VALIDATING_PATTERN: &str = r"^\w+(\.\w+)*\/\w+$"; + + /// Returns the [`Regex`] for [`Self::VALIDATING_PATTERN`]. + /// + /// This private method is used to initialize the [`VALIDATING_REGEX`] private static to reduce + /// the number of times the regular expression is compiled from the pattern string. + fn init_pattern() -> Regex { + Regex::new(Self::VALIDATING_PATTERN).expect("pattern is valid") + } + + /// Validates a given string as a fully qualified name. + /// + /// A string is valid if it matches the [`VALIDATING_PATTERN`]. If the string is invalid, DSC + /// raises the [`DscError::InvalidTypeName`] error. + /// + /// [`VALIDATING_PATTERN`]: Self::VALIDATING_PATTERN + pub fn validate(name: &str) -> Result<(), DscError> { + let pattern = VALIDATING_REGEX.get_or_init(Self::init_pattern); + match pattern.is_match(name) { + true => Ok(()), + false => Err(DscError::InvalidTypeName( + name.to_string(), + pattern.to_string(), + )), + } + } + + /// Creates a new instance of [`FullyQualifiedTypeName`] from a string if the input is valid for the + /// [`VALIDATING_PATTERN`]. If the string is invalid, the method raises the + /// [`DscError::InvalidTypeName`] error. + pub fn new(name: &str) -> Result { + Self::validate(name)?; + Ok(Self(name.to_string())) + } +} + +// While it's technically never valid for a _defined_ FQTN to be empty, we need the default +// implementation for creating empty instances of various structs to then populate/modify. +impl Default for FullyQualifiedTypeName { + fn default() -> Self { + Self(String::new()) + } +} + +// We implement `PartialEq` by hand for various types because FQTNs should be compared +// case insensitively. This obviates the need to `.to_string().to_lowercase()` for comparisons. +impl PartialEq for FullyQualifiedTypeName { + fn eq(&self, other: &Self) -> bool { + self.0.to_lowercase() == other.0.to_lowercase() + } +} + +impl PartialEq for FullyQualifiedTypeName { + fn eq(&self, other: &String) -> bool { + self.0.to_lowercase() == other.to_lowercase() + } +} + +impl PartialEq for FullyQualifiedTypeName { + fn eq(&self, other: &str) -> bool { + self.0.to_lowercase() == other.to_lowercase() + } +} + +impl PartialEq<&str> for FullyQualifiedTypeName { + fn eq(&self, other: &&str) -> bool { + self.0.to_lowercase() == other.to_lowercase() + } +} + +// Enables using the construct `"Owner/Name".parse()` to convert a literal string into an FQTN. +impl FromStr for FullyQualifiedTypeName { + type Err = DscError; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +// Enables converting from a `String` and raising the appropriate error message for an invalid +// FQTN. +impl TryFrom for FullyQualifiedTypeName { + type Error = DscError; + fn try_from(value: String) -> Result { + Self::new(value.as_str()) + } +} + +// Enables converting an FQTN into a string. +impl From for String { + fn from(value: FullyQualifiedTypeName) -> Self { + value.0 + } +} + +// Enables using FQTNs in `format!()` and similar macros. +impl Display for FullyQualifiedTypeName { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +// Enables passing an FQTN as `&str` +impl AsRef for FullyQualifiedTypeName { + fn as_ref(&self) -> &str { + &self.0 + } +} + +// Enables directly accessing string methods on an FQTN, like `.to_lowercase()` or `starts_with()`. +impl Deref for FullyQualifiedTypeName { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/lib/dsc-lib/src/types/mod.rs b/lib/dsc-lib/src/types/mod.rs new file mode 100644 index 000000000..b046d479b --- /dev/null +++ b/lib/dsc-lib/src/types/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod fully_qualified_type_name; +pub use fully_qualified_type_name::FullyQualifiedTypeName; diff --git a/lib/dsc-lib/tests/integration/main.rs b/lib/dsc-lib/tests/integration/main.rs index a203d38a9..28aca9416 100644 --- a/lib/dsc-lib/tests/integration/main.rs +++ b/lib/dsc-lib/tests/integration/main.rs @@ -14,3 +14,4 @@ //! Rust would generate numerous binaries to execute our tests. #[cfg(test)] mod schemas; +#[cfg(test)] mod types; diff --git a/lib/dsc-lib/tests/integration/types/fully_qualified_type_name.rs b/lib/dsc-lib/tests/integration/types/fully_qualified_type_name.rs new file mode 100644 index 000000000..961494d12 --- /dev/null +++ b/lib/dsc-lib/tests/integration/types/fully_qualified_type_name.rs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use jsonschema::Validator; +use schemars::schema_for; +use serde_json::{json, Value}; + +use dsc_lib::{dscerror::DscError, types::FullyQualifiedTypeName}; + +#[test] +fn test_schema_without_segments() { + let schema = Validator::new(schema_for!(FullyQualifiedTypeName).as_value()).unwrap(); + let name = "invalid_type_name"; + + assert!(schema + .validate(&json!(name)) + .unwrap_err() + .to_string() + .starts_with(format!(r#""{name}" does not match"#).as_str())) +} + +#[test] +fn test_schema_with_invalid_character() { + let schema = Validator::new(schema_for!(FullyQualifiedTypeName).as_value()).unwrap(); + let name = "With&Invalid/Character"; + + assert!(schema + .validate(&json!(name)) + .unwrap_err() + .to_string() + .starts_with(format!(r#""{name}" does not match"#).as_str())) +} + +#[test] +fn test_schema_without_namespaces() { + let schema = Validator::new(schema_for!(FullyQualifiedTypeName).as_value()).unwrap(); + let name = "Owner/Name"; + + assert!(schema.validate(&json!(name)).is_ok()) +} + +#[test] +fn test_schema_with_one_namespace() { + let schema = Validator::new(schema_for!(FullyQualifiedTypeName).as_value()).unwrap(); + let name = "Owner.Namespace/Name"; + + assert!(schema.validate(&json!(name)).is_ok()) +} + +#[test] +fn test_schema_with_many_namespaces() { + let schema = Validator::new(schema_for!(FullyQualifiedTypeName).as_value()).unwrap(); + let name = "Owner.A.B.C.D.E.F/Name"; + + assert!(schema.validate(&json!(name)).is_ok()) +} + +#[test] +fn test_deserialize_valid() { + let name = "Owner/Name"; + let deserialized: FullyQualifiedTypeName = serde_json::from_value(json!(name)).unwrap(); + assert_eq!(deserialized.to_string(), name.to_string()) +} + +#[test] +fn test_deserialize_invalid() { + let name = "invalid_name"; + let deserializing_error = serde_json::from_value::(json!(name)) + .unwrap_err() + .to_string(); + let expected_error = DscError::InvalidTypeName( + name.to_string(), + FullyQualifiedTypeName::VALIDATING_PATTERN.to_string(), + ) + .to_string(); + + assert_eq!(deserializing_error, expected_error) +} + +#[test] +fn test_serialize_valid() { + let name = "Owner/Name"; + let instance = FullyQualifiedTypeName::new(name).unwrap(); + let serialized: Value = serde_json::to_value(instance).unwrap(); + assert_eq!(serialized, json!(name)) +} + +#[test] +fn test_display() { + let name = "Owner/Name"; + let instance = FullyQualifiedTypeName::new(name).unwrap(); + assert_eq!(format!("{instance}"), format!("{name}")) +} + +#[test] +fn test_as_ref() { + let name = "Owner/Name"; + let instance = FullyQualifiedTypeName::new(name).unwrap(); + assert_eq!(name, instance.as_ref()) +} + +#[test] +fn test_deref() { + let name = "Owner/Name"; + let instance = FullyQualifiedTypeName::new(name).unwrap(); + assert_eq!(name, &*instance) +} + +#[test] +fn test_default_is_empty() { + let instance = FullyQualifiedTypeName::default(); + assert!(instance.is_empty()) +} diff --git a/lib/dsc-lib/tests/integration/types/mod.rs b/lib/dsc-lib/tests/integration/types/mod.rs new file mode 100644 index 000000000..c45605926 --- /dev/null +++ b/lib/dsc-lib/tests/integration/types/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(test)] +mod fully_qualified_type_name; diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index f3888c408..f685eab94 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -5,8 +5,8 @@ mod args; use args::{Args, SubCommand}; use clap::Parser; -use dsc_lib::dscresources::resource_manifest::{ResourceManifest, GetMethod, Kind}; use dsc_lib::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; +use dsc_lib::dscresources::resource_manifest::{GetMethod, Kind, ResourceManifest}; use dsc_lib::schemas::dsc_repo::DscRepoSchema; use std::path::PathBuf; @@ -15,7 +15,7 @@ fn main() { match args.subcommand { SubCommand::List => { let resource1 = DscResource { - type_name: "Test/TestResource1".to_string(), + type_name: "Test/TestResource1".parse().unwrap(), kind: Kind::Resource, version: "1.0.0".to_string(), capabilities: vec![Capability::Get, Capability::Set], @@ -25,12 +25,12 @@ fn main() { directory: PathBuf::from("test_directory"), author: Some("Microsoft".to_string()), properties: vec!["Property1".to_string(), "Property2".to_string()], - require_adapter: Some("Test/TestGroup".to_string()), + require_adapter: Some("Test/TestGroup".parse().unwrap()), target_resource: None, manifest: Some(serde_json::to_value(ResourceManifest { description: Some("This is a test resource.".to_string()), schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), - resource_type: "Test/TestResource1".to_string(), + resource_type: "Test/TestResource1".parse().unwrap(), kind: Some(Kind::Resource), version: "1.0.0".to_string(), get: Some(GetMethod { @@ -41,7 +41,7 @@ fn main() { }).unwrap()), }; let resource2 = DscResource { - type_name: "Test/TestResource2".to_string(), + type_name: "Test/TestResource2".parse().unwrap(), kind: Kind::Resource, version: "1.0.1".to_string(), capabilities: vec![Capability::Get, Capability::Set], @@ -51,12 +51,12 @@ fn main() { directory: PathBuf::from("test_directory"), author: Some("Microsoft".to_string()), properties: vec!["Property1".to_string(), "Property2".to_string()], - require_adapter: Some("Test/TestGroup".to_string()), + require_adapter: Some("Test/TestGroup".parse().unwrap()), target_resource: None, manifest: Some(serde_json::to_value(ResourceManifest { description: Some("This is a test resource.".to_string()), schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), - resource_type: "Test/TestResource2".to_string(), + resource_type: "Test/TestResource2".parse().unwrap(), kind: Some(Kind::Resource), version: "1.0.1".to_string(), get: Some(GetMethod { @@ -71,7 +71,7 @@ fn main() { }, SubCommand::ListMissingRequires => { let resource1 = DscResource { - type_name: "InvalidResource".to_string(), + type_name: "Test/InvalidResource".parse().unwrap(), kind: Kind::Resource, version: "1.0.0".to_string(), capabilities: vec![Capability::Get],