From 44ab18b680c50a9f9638c95c81cd24010dc1b333 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 20 Oct 2025 17:41:47 -0700 Subject: [PATCH 1/4] Add `tryWhich()` function and enable manifests to have `condition` --- dsc/tests/dsc_extension_manifest.tests.ps1 | 41 +++++++++++ dsc/tests/dsc_functions.tests.ps1 | 23 ++++++ dsc/tests/dsc_resource_manifest.tests.ps1 | 51 +++++++++++++ extensions/bicep/bicep.dsc.extension.json | 1 + lib/dsc-lib/locales/en-us.toml | 6 ++ .../src/discovery/command_discovery.rs | 21 +++++- .../src/dscresources/resource_manifest.rs | 3 + lib/dsc-lib/src/extensions/discover.rs | 2 +- .../src/extensions/extension_manifest.rs | 3 + lib/dsc-lib/src/functions/equals.rs | 11 ++- lib/dsc-lib/src/functions/mod.rs | 2 + lib/dsc-lib/src/functions/try_which.rs | 71 +++++++++++++++++++ 12 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 dsc/tests/dsc_extension_manifest.tests.ps1 create mode 100644 dsc/tests/dsc_resource_manifest.tests.ps1 create mode 100644 lib/dsc-lib/src/functions/try_which.rs diff --git a/dsc/tests/dsc_extension_manifest.tests.ps1 b/dsc/tests/dsc_extension_manifest.tests.ps1 new file mode 100644 index 000000000..1533fe96d --- /dev/null +++ b/dsc/tests/dsc_extension_manifest.tests.ps1 @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Extension Manifests' { + It 'Extension manifests with condition: ' -TestCases @( + @{ condition = "[equals(1, 1)]"; shouldBeFound = $true } + @{ condition = "[equals(1, 0)]"; shouldBeFound = $false } + ) { + param($condition, $shouldBeFound) + + $extension_manifest = @" +{ + "`$schema": "https://aka.ms/dsc/schemas/v3/bundled/extension/manifest.json", + "type": "Test/Extension", + "condition": "$condition", + "version": "0.1.0", + "import": { + "fileExtensions": ["foo"], + "executable": "dsc" + } +} +"@ + + try { + $env:DSC_RESOURCE_PATH = $TestDrive + $manifestPath = Join-Path -Path $TestDrive -ChildPath 'Extension.dsc.extension.json' + $extension_manifest | Out-File -FilePath $manifestPath -Encoding utf8 + $extensions = dsc extension list | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 + if ($shouldBeFound) { + $extensions.count | Should -Be 1 + $extensions.type | Should -BeExactly 'Test/Extension' + } + else { + $extensions.count | Should -Be 0 + } + } finally { + $env:DSC_RESOURCE_PATH = $null + } + } +} diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 4123a6865..5cf126a5f 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -1204,4 +1204,27 @@ Describe 'tests for function expressions' { $expected = "Microsoft.DSC.Debug/Echo:$([Uri]::EscapeDataString($name))" $out.results[0].result.actualState.output | Should -BeExactly $expected } + + It 'tryWhich() works for: ' -TestCases @( + @{ expression = "[tryWhich('pwsh')]"; found = $true } + @{ expression = "[tryWhich('nonexistentcommand12345')]"; found = $false } + ) { + param($expression, $found) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + if ($found) { + $out.results[0].result.actualState.output | Should -Not -BeNullOrEmpty + } else { + $out.results[0].result.actualState.output | Should -BeNullOrEmpty + } + } } diff --git a/dsc/tests/dsc_resource_manifest.tests.ps1 b/dsc/tests/dsc_resource_manifest.tests.ps1 new file mode 100644 index 000000000..800054ad9 --- /dev/null +++ b/dsc/tests/dsc_resource_manifest.tests.ps1 @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Resource Manifests' { + It 'Resource manifests with condition: ' -TestCases @( + @{ condition = "[equals(1, 1)]"; shouldBeFound = $true } + @{ condition = "[equals(1, 0)]"; shouldBeFound = $false } + ) { + param($condition, $shouldBeFound) + + $resource_manifest = @" +{ + "`$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/MyEcho", + "version": "1.0.0", + "condition": "$condition", + "get": { + "executable": "dscecho", + "args": [ + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "schema": { + "command": { + "executable": "dscecho" + } + } +} +"@ + + try { + $env:DSC_RESOURCE_PATH = $TestDrive + $manifestPath = Join-Path -Path $TestDrive -ChildPath 'MyEcho.dsc.resource.json' + $resource_manifest | Out-File -FilePath $manifestPath -Encoding utf8 + $resources = dsc resource list | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 + if ($shouldBeFound) { + $resources.count | Should -Be 1 + $resources.type | Should -BeExactly 'Test/MyEcho' + } + else { + $resources.count | Should -Be 0 + } + } finally { + $env:DSC_RESOURCE_PATH = $null + } + } +} diff --git a/extensions/bicep/bicep.dsc.extension.json b/extensions/bicep/bicep.dsc.extension.json index 6677beb07..e2a1f71ad 100644 --- a/extensions/bicep/bicep.dsc.extension.json +++ b/extensions/bicep/bicep.dsc.extension.json @@ -3,6 +3,7 @@ "type": "Microsoft.DSC.Extension/Bicep", "version": "0.1.0", "description": "Enable passing Bicep file directly to DSC, but requires bicep executable to be available.", + "condition": "[not(equals(tryWhich('bicep'), null()))]", "import": { "fileExtensions": ["bicep"], "executable": "bicep", diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index ad410767c..2976257e0 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -118,6 +118,8 @@ foundResourceWithVersion = "Found matching resource '%{resource}' version %{vers foundNonAdapterResources = "Found %{count} non-adapter resources" resourceMissingRequireAdapter = "Resource '%{resource}' is missing 'require_adapter' field." extensionDiscoverFailed = "Extension '%{extension}' failed to discover resources: %{error}" +conditionNotBoolean = "Condition '%{condition}' did not evaluate to a boolean" +conditionNotMet = "Condition '%{condition}' not met, skipping manifest at '%{path}'" [dscresources.commandResource] invokeGet = "Invoking get for '%{resource}'" @@ -542,6 +544,10 @@ invoked = "tryGet function" invalidKeyType = "Invalid key type, must be a string" invalidIndexType = "Invalid index type, must be an integer" +[functions.tryWhich] +description = "Attempts to locate an executable in the system PATH. Null is returned if the executable is not found otherwise the full path to the executable is returned." +invoked = "tryWhich function" + [functions.union] description = "Returns a single array or object with all elements from the parameters" invoked = "union function" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 03941127c..828dae987 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}}; +use crate::{discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, parser::Statement}; use crate::{locked_is_empty, locked_extend, locked_clone, locked_get}; +use crate::configure::context::Context; use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; use crate::dscresources::resource_manifest::{import_manifest, validate_semver, Kind, ResourceManifest, SchemaKind}; use crate::dscresources::command_resource::invoke_command; @@ -640,6 +641,10 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { } } }; + if !evaluate_condition(manifest.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default())); + return Ok(vec![]); + } let resource = load_resource_manifest(path, &manifest)?; return Ok(vec![ImportedManifest::Resource(resource)]); } @@ -659,10 +664,15 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { } } }; + if !evaluate_condition(manifest.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default())); + return Ok(vec![]); + } let extension = load_extension_manifest(path, &manifest)?; return Ok(vec![ImportedManifest::Extension(extension)]); } if DSC_MANIFEST_LIST_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) { + let mut resources: Vec = vec![]; let manifest_list = if extension_is_json { match serde_json::from_str::(&contents) { Ok(manifest) => manifest, @@ -678,15 +688,22 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { } } }; - let mut resources = vec![]; if let Some(resource_manifests) = &manifest_list.resources { for res_manifest in resource_manifests { + if !evaluate_condition(res_manifest.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = res_manifest.condition.unwrap_or_default())); + continue; + } let resource = load_resource_manifest(path, res_manifest)?; resources.push(ImportedManifest::Resource(resource)); } } if let Some(extension_manifests) = &manifest_list.extensions { for ext_manifest in extension_manifests { + if !evaluate_condition(ext_manifest.condition.as_deref())? { + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = ext_manifest.condition.unwrap_or_default())); + continue; + } let extension = load_extension_manifest(path, ext_manifest)?; resources.push(ImportedManifest::Extension(extension)); } diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index fcd1cf10f..bc41eb23e 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -32,6 +32,9 @@ pub struct ResourceManifest { /// The namespaced name of the resource. #[serde(rename = "type")] pub resource_type: String, + /// 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, /// The kind of resource. #[serde(skip_serializing_if = "Option::is_none")] pub kind: Option, diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index 813b6578a..a1bdcb1dc 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -91,7 +91,7 @@ impl DscExtension { let manifest_path = Path::new(&discover_result.manifest_path); // Currently we don't support extensions discovering other extensions for imported_manifest in load_manifest(manifest_path)? { - if let ImportedManifest::Resource(resource) = imported_manifest { + if let Some(ImportedManifest::Resource(resource)) = imported_manifest { resources.push(resource); } } diff --git a/lib/dsc-lib/src/extensions/extension_manifest.rs b/lib/dsc-lib/src/extensions/extension_manifest.rs index 8f338df6c..b745dc9e2 100644 --- a/lib/dsc-lib/src/extensions/extension_manifest.rs +++ b/lib/dsc-lib/src/extensions/extension_manifest.rs @@ -23,6 +23,9 @@ pub struct ExtensionManifest { pub r#type: String, /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, /// The description of the extension. pub description: Option, /// Tags for the extension. diff --git a/lib/dsc-lib/src/functions/equals.rs b/lib/dsc-lib/src/functions/equals.rs index b96c276ed..ce899e439 100644 --- a/lib/dsc-lib/src/functions/equals.rs +++ b/lib/dsc-lib/src/functions/equals.rs @@ -20,8 +20,8 @@ impl Function for Equals { min_args: 2, max_args: 2, accepted_arg_ordered_types: vec![ - vec![FunctionArgKind::Number, FunctionArgKind::String, FunctionArgKind::Array, FunctionArgKind::Object], - vec![FunctionArgKind::Number, FunctionArgKind::String, FunctionArgKind::Array, FunctionArgKind::Object], + vec![FunctionArgKind::Null, FunctionArgKind::Number, FunctionArgKind::String, FunctionArgKind::Array, FunctionArgKind::Object], + vec![FunctionArgKind::Null, FunctionArgKind::Number, FunctionArgKind::String, FunctionArgKind::Array, FunctionArgKind::Object], ], remaining_arg_accepted_types: None, return_types: vec![FunctionArgKind::Boolean], @@ -74,6 +74,13 @@ mod tests { assert_eq!(result, Value::Bool(false)); } + #[test] + fn null_equal() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[equals(null(),null())]", &Context::new()).unwrap(); + assert_eq!(result, Value::Bool(true)); + } + // TODO: Add tests for arrays once `createArray()` is implemented // TODO: Add tests for objects once `createObject()` is implemented } diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index 01133987e..9af07d8fc 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -78,6 +78,7 @@ pub mod uri_component_to_string; pub mod user_function; pub mod utc_now; pub mod variables; +pub mod try_which; /// The kind of argument that a function accepts. #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, JsonSchema)] @@ -205,6 +206,7 @@ impl FunctionDispatcher { Box::new(uri_component_to_string::UriComponentToString{}), Box::new(utc_now::UtcNow{}), Box::new(variables::Variables{}), + Box::new(try_which::TryWhich{}), ]; for function in function_list { functions.insert(function.get_metadata().name.clone(), function); diff --git a/lib/dsc-lib/src/functions/try_which.rs b/lib/dsc-lib/src/functions/try_which.rs new file mode 100644 index 000000000..6644a81a3 --- /dev/null +++ b/lib/dsc-lib/src/functions/try_which.rs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata}; +use rust_i18n::t; +use serde_json::Value; +use tracing::debug; +use which::which; + +#[derive(Debug, Default)] +pub struct TryWhich {} + +impl Function for TryWhich { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "tryWhich".to_string(), + description: t!("functions.tryWhich.description").to_string(), + category: vec![FunctionCategory::System], + min_args: 1, + max_args: 1, + accepted_arg_ordered_types: vec![vec![FunctionArgKind::String]], + remaining_arg_accepted_types: None, + return_types: vec![ + FunctionArgKind::String, + FunctionArgKind::Null, + ], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + debug!("{}", t!("functions.tryWhich.invoked")); + + let exe = args[0].as_str().unwrap(); + match which(exe) { + Ok(found_path) => { + let path_str = found_path.to_string_lossy().to_string(); + Ok(Value::String(path_str)) + }, + Err(_) => { + // In tryWhich, we return null if not found + Ok(Value::Null) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + use serde_json::Value; + + #[test] + fn exe_exists() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[tryWhich('dsc')]", &Context::new()).unwrap(); + #[cfg(windows)] + assert!(result.as_str().unwrap().to_lowercase().ends_with("\\dsc.exe")); + #[cfg(not(windows))] + assert!(result.as_str().unwrap().ends_with("/dsc")); + } + + #[test] + fn invalid_exe() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[tryWhich('does_not_exist')]", &Context::new()).unwrap(); + assert_eq!(result, Value::Null); + } +} From ddf7e6ad8968a18c40f575a78b458f7ddc253409 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 21 Oct 2025 18:20:54 -0700 Subject: [PATCH 2/4] add example for OS detection --- dsc/tests/dsc_extension_manifest.tests.ps1 | 3 +++ dsc/tests/dsc_resource_manifest.tests.ps1 | 3 +++ 2 files changed, 6 insertions(+) diff --git a/dsc/tests/dsc_extension_manifest.tests.ps1 b/dsc/tests/dsc_extension_manifest.tests.ps1 index 1533fe96d..5672fdf50 100644 --- a/dsc/tests/dsc_extension_manifest.tests.ps1 +++ b/dsc/tests/dsc_extension_manifest.tests.ps1 @@ -5,6 +5,9 @@ Describe 'Extension Manifests' { It 'Extension manifests with condition: ' -TestCases @( @{ condition = "[equals(1, 1)]"; shouldBeFound = $true } @{ condition = "[equals(1, 0)]"; shouldBeFound = $false } + @{ condition = "[equals(context().os.family,'macOS')]"; shouldBeFound = $IsMacOS } + @{ condition = "[equals(context().os.family,'Linux')]"; shouldBeFound = $IsLinux } + @{ condition = "[equals(context().os.family,'Windows')]"; shouldBeFound = $IsWindows } ) { param($condition, $shouldBeFound) diff --git a/dsc/tests/dsc_resource_manifest.tests.ps1 b/dsc/tests/dsc_resource_manifest.tests.ps1 index 800054ad9..2703b95b6 100644 --- a/dsc/tests/dsc_resource_manifest.tests.ps1 +++ b/dsc/tests/dsc_resource_manifest.tests.ps1 @@ -5,6 +5,9 @@ Describe 'Resource Manifests' { It 'Resource manifests with condition: ' -TestCases @( @{ condition = "[equals(1, 1)]"; shouldBeFound = $true } @{ condition = "[equals(1, 0)]"; shouldBeFound = $false } + @{ condition = "[equals(context().os.family,'macOS')]"; shouldBeFound = $IsMacOS } + @{ condition = "[equals(context().os.family,'Linux')]"; shouldBeFound = $IsLinux } + @{ condition = "[equals(context().os.family,'Windows')]"; shouldBeFound = $IsWindows } ) { param($condition, $shouldBeFound) From 4bd4c97b1298aed8eefc9bd70a3aa0128f21c8cf Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 24 Oct 2025 14:14:55 -0700 Subject: [PATCH 3/4] fix merge --- lib/dsc-lib/src/discovery/command_discovery.rs | 16 ++++++++++++++-- lib/dsc-lib/src/extensions/discover.rs | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 828dae987..a235b5c1c 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -608,6 +608,18 @@ fn insert_resource(resources: &mut BTreeMap>, resource: } } +fn evaluate_condition(condition: Option<&str>) -> Result { + if let Some(cond) = condition { + let mut statement = Statement::new()?; + let result = statement.parse_and_execute(cond, &Context::new())?; + if let Some(bool_result) = result.as_bool() { + return Ok(bool_result); + } + return Err(DscError::Validation(t!("discovery.commandDiscovery.conditionNotBoolean", condition = cond).to_string())); + } + Ok(true) +} + /// Loads a manifest from the given path and returns a vector of `ImportedManifest`. /// /// # Arguments @@ -691,7 +703,7 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { if let Some(resource_manifests) = &manifest_list.resources { for res_manifest in resource_manifests { if !evaluate_condition(res_manifest.condition.as_deref())? { - debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = res_manifest.condition.unwrap_or_default())); + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = res_manifest.condition.as_ref() : {:?})); continue; } let resource = load_resource_manifest(path, res_manifest)?; @@ -701,7 +713,7 @@ pub fn load_manifest(path: &Path) -> Result, DscError> { if let Some(extension_manifests) = &manifest_list.extensions { for ext_manifest in extension_manifests { if !evaluate_condition(ext_manifest.condition.as_deref())? { - debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = ext_manifest.condition.unwrap_or_default())); + debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = ext_manifest.condition.as_ref() : {:?})); continue; } let extension = load_extension_manifest(path, ext_manifest)?; diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index a1bdcb1dc..813b6578a 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -91,7 +91,7 @@ impl DscExtension { let manifest_path = Path::new(&discover_result.manifest_path); // Currently we don't support extensions discovering other extensions for imported_manifest in load_manifest(manifest_path)? { - if let Some(ImportedManifest::Resource(resource)) = imported_manifest { + if let ImportedManifest::Resource(resource) = imported_manifest { resources.push(resource); } } From 057fe35e74fc50bb2e5b2f846b6695652fa3e667 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 24 Oct 2025 14:17:59 -0700 Subject: [PATCH 4/4] fix clippy --- resources/dscecho/src/main.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/resources/dscecho/src/main.rs b/resources/dscecho/src/main.rs index 90547aa6f..8b1ef5dd4 100644 --- a/resources/dscecho/src/main.rs +++ b/resources/dscecho/src/main.rs @@ -40,11 +40,9 @@ fn main() { } }, Output::Object(ref mut obj) => { - *obj = redact(&Value::Object(obj.clone())) + obj.clone_from(redact(&Value::Object(obj.clone())) .as_object() - .expect("Expected redact() to return a Value::Object") - .clone(); - }, + .expect("Expected redact() to return a Value::Object")); }, _ => {} } }