Skip to content

Commit 6dcbb96

Browse files
committed
Merge branch 'gh-57/main/add-tryindexfromend-function' of https://github.com/Gijsreyn/operation-methods into gh-57/main/add-tryindexfromend-function
2 parents cce8fe6 + e9cba87 commit 6dcbb96

File tree

3 files changed

+256
-2
lines changed

3 files changed

+256
-2
lines changed

docs/reference/schemas/config/functions/tryIndexFromEnd.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ appropriate log level or `null` without throwing errors.
200200
Use `tryIndexFromEnd()` with [`coalesce()`][02] to implement fallback logic when
201201
accessing configuration values from arrays that might have different lengths
202202
across regions. This example shows how to safely access regional endpoints with
203-
a default fallback.
203+
a default fallback. This example uses [`createArray()`][05] to build the
204+
regional endpoint arrays.
204205

205206
```yaml
206207
# tryIndexFromEnd.example.4.dsc.config.yaml

dsc/tests/dsc_armv2.tests.ps1

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'ARM Language 2.0 tests' {
5+
It 'config with ARM Language 2.0 format works' {
6+
<#
7+
This JSON config built from the following Bicep code using the
8+
desiredStateConfiguration and moduleExtensionConfigs experimental features:
9+
10+
extension dsc
11+
targetScope = 'desiredStateConfiguration'
12+
13+
resource echoResource 'Microsoft.DSC.Debug/Echo@1.0.0' = {
14+
output: 'Hello World'
15+
}
16+
#>
17+
$configJson = @'
18+
{
19+
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/config/document.json",
20+
"languageVersion": "2.2-experimental",
21+
"contentVersion": "1.0.0.0",
22+
"metadata": {
23+
"_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.",
24+
"_EXPERIMENTAL_FEATURES_ENABLED": [
25+
"Enable defining extension configs for modules"
26+
],
27+
"_generator": {
28+
"name": "bicep",
29+
"version": "0.38.33.27573",
30+
"templateHash": "5233252217641859406"
31+
}
32+
},
33+
"extensions": {
34+
"dsc": {
35+
"name": "DesiredStateConfiguration",
36+
"version": "0.1.0"
37+
}
38+
},
39+
"resources": {
40+
"echoResource": {
41+
"extension": "dsc",
42+
"type": "Microsoft.DSC.Debug/Echo",
43+
"apiVersion": "1.0.0",
44+
"properties": {
45+
"output": "Hello World"
46+
}
47+
}
48+
}
49+
}
50+
'@
51+
$out = dsc config get -i $configJson | ConvertFrom-Json -Depth 10
52+
$LASTEXITCODE | Should -Be 0
53+
$out.results | Should -HaveCount 1
54+
$out.results[0].type | Should -Be 'Microsoft.DSC.Debug/Echo'
55+
$out.results[0].result.actualState.output | Should -Be 'Hello World'
56+
}
57+
}

lib/dsc-lib/src/configure/config_doc.rs

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use dsc_lib_jsonschema::transforms::{
88
};
99
use rust_i18n::t;
1010
use schemars::{JsonSchema, json_schema};
11-
use serde::{Deserialize, Serialize};
11+
use serde::{Deserialize, Deserializer, Serialize};
1212
use serde_json::{Map, Value};
1313
use std::{collections::HashMap, fmt::Display};
1414

@@ -159,6 +159,11 @@ pub struct Configuration {
159159
#[serde(rename = "$schema")]
160160
#[schemars(schema_with = "Configuration::recognized_schema_uris_subschema")]
161161
pub schema: String,
162+
/// Irrelevant Bicep metadata from using the extension
163+
/// TODO: Potentially check this as a feature flag.
164+
#[serde(skip_serializing_if = "Option::is_none")]
165+
#[serde(rename = "languageVersion")]
166+
pub language_version: Option<String>,
162167
#[serde(rename = "contentVersion")]
163168
pub content_version: Option<String>,
164169
#[serde(skip_serializing_if = "Option::is_none")]
@@ -169,9 +174,53 @@ pub struct Configuration {
169174
pub outputs: Option<HashMap<String, Output>>,
170175
#[serde(skip_serializing_if = "Option::is_none")]
171176
pub parameters: Option<HashMap<String, Parameter>>,
177+
#[serde(deserialize_with = "deserialize_resources")]
172178
pub resources: Vec<Resource>,
173179
#[serde(skip_serializing_if = "Option::is_none")]
174180
pub variables: Option<Map<String, Value>>,
181+
/// Irrelevant Bicep metadata from using the extension
182+
#[serde(skip_serializing_if = "Option::is_none")]
183+
pub imports: Option<Map<String, Value>>,
184+
#[serde(skip_serializing_if = "Option::is_none")]
185+
pub extensions: Option<Map<String, Value>>,
186+
}
187+
188+
/// Simplest implementation of a custom deserializer that will map a JSON object
189+
/// of resources (where the keys are symbolic names) as found in ARMv2 back to a
190+
/// vector, so the rest of this codebase can remain untouched.
191+
fn deserialize_resources<'de, D>(deserializer: D) -> Result<Vec<Resource>, D::Error>
192+
where
193+
D: Deserializer<'de>,
194+
{
195+
let value = Value::deserialize(deserializer)?;
196+
197+
match value {
198+
Value::Array(resources) => {
199+
resources.into_iter()
200+
.map(|resource| serde_json::from_value::<Resource>(resource).map_err(serde::de::Error::custom))
201+
.collect()
202+
}
203+
Value::Object(resources) => {
204+
resources.into_iter()
205+
.map(|(name, resource)| {
206+
let mut resource = serde_json::from_value::<Resource>(resource).map_err(serde::de::Error::custom)?;
207+
// Note that this is setting the symbolic name as the
208+
// resource's name property only if that isn't already set.
209+
// In the general use case from Bicep, it won't be, but
210+
// we're unsure of the implications in other use cases.
211+
//
212+
// TODO: We will need to update the 'dependsOn' logic to
213+
// accept both the symbolic name as mapped here in addition
214+
// to `resourceId()`, or possibly track both.
215+
if resource.name.is_empty() {
216+
resource.name = name;
217+
}
218+
Ok(resource)
219+
})
220+
.collect()
221+
}
222+
other => Err(serde::de::Error::custom(format!("Expected resources to be either an array or an object, but was {:?}", other))),
223+
}
175224
}
176225

177226
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
@@ -316,6 +365,7 @@ pub struct Resource {
316365
#[serde(skip_serializing_if = "Option::is_none", rename = "apiVersion")]
317366
pub api_version: Option<String>,
318367
/// A friendly name for the resource instance
368+
#[serde(default)]
319369
pub name: String, // friendly unique instance name
320370
#[serde(skip_serializing_if = "Option::is_none")]
321371
pub comments: Option<String>,
@@ -344,6 +394,11 @@ pub struct Resource {
344394
pub resources: Option<Vec<Resource>>,
345395
#[serde(skip_serializing_if = "Option::is_none")]
346396
pub metadata: Option<Metadata>,
397+
/// Irrelevant Bicep metadata from using the extension
398+
#[serde(skip_serializing_if = "Option::is_none")]
399+
pub import: Option<String>,
400+
#[serde(skip_serializing_if = "Option::is_none")]
401+
pub extension: Option<String>,
347402
}
348403

349404
impl Default for Configuration {
@@ -381,13 +436,16 @@ impl Configuration {
381436
pub fn new() -> Self {
382437
Self {
383438
schema: Self::default_schema_id_uri(),
439+
language_version: None,
384440
content_version: Some("1.0.0".to_string()),
385441
metadata: None,
386442
parameters: None,
387443
resources: Vec::new(),
388444
functions: None,
389445
variables: None,
390446
outputs: None,
447+
imports: None,
448+
extensions: None,
391449
}
392450
}
393451
}
@@ -413,6 +471,8 @@ impl Resource {
413471
location: None,
414472
tags: None,
415473
api_version: None,
474+
import: None,
475+
extension: None,
416476
}
417477
}
418478
}
@@ -466,4 +526,140 @@ mod test {
466526

467527
assert!(result.is_ok());
468528
}
529+
530+
#[test]
531+
fn test_invalid_resource_field_in_array() {
532+
let config_json = r#"{
533+
"resources": [
534+
{
535+
"invalidField": "someValue"
536+
}
537+
]
538+
}"#;
539+
540+
let result: Result<Configuration, _> = serde_json::from_str(config_json);
541+
assert!(result.is_err());
542+
let err = result.unwrap_err().to_string();
543+
assert!(err.starts_with("unknown field `invalidField`, expected one of `condition`, `type`,"));
544+
}
545+
546+
#[test]
547+
fn test_invalid_resource_field_in_object() {
548+
let config_json = r#"{
549+
"resources": {
550+
"someResource": {
551+
"invalidField": "someValue"
552+
}
553+
}
554+
}"#;
555+
556+
let result: Result<Configuration, _> = serde_json::from_str(config_json);
557+
assert!(result.is_err());
558+
let err = result.unwrap_err().to_string();
559+
assert!(err.starts_with("unknown field `invalidField`, expected one of `condition`, `type`,"));
560+
}
561+
562+
#[test]
563+
fn test_invalid_resource_type_in_array() {
564+
let config_json = r#"{
565+
"resources": [
566+
"invalidType"
567+
]
568+
}"#;
569+
570+
let result: Result<Configuration, _> = serde_json::from_str(config_json);
571+
assert!(result.is_err());
572+
let err = result.unwrap_err().to_string();
573+
assert!(err.contains("expected struct Resource"));
574+
}
575+
576+
#[test]
577+
fn test_invalid_resource_type_in_object() {
578+
let config_json = r#"{
579+
"resources": {
580+
"someResource": "invalidType"
581+
}
582+
}"#;
583+
584+
let result: Result<Configuration, _> = serde_json::from_str(config_json);
585+
assert!(result.is_err());
586+
let err = result.unwrap_err().to_string();
587+
assert!(err.contains("expected struct Resource"));
588+
}
589+
590+
#[test]
591+
fn test_resources_as_array() {
592+
let config_json = r#"{
593+
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/config/document.json",
594+
"resources": [
595+
{
596+
"type": "Microsoft.DSC.Debug/Echo",
597+
"name": "echoResource",
598+
"apiVersion": "1.0.0"
599+
},
600+
{
601+
"type": "Microsoft/Process",
602+
"name": "processResource",
603+
"apiVersion": "0.1.0"
604+
}
605+
]
606+
}"#;
607+
608+
let config: Configuration = serde_json::from_str(config_json).unwrap();
609+
610+
assert_eq!(config.resources.len(), 2);
611+
assert_eq!(config.resources[0].name, "echoResource");
612+
assert_eq!(config.resources[0].resource_type, "Microsoft.DSC.Debug/Echo");
613+
assert_eq!(config.resources[0].api_version.as_deref(), Some("1.0.0"));
614+
615+
assert_eq!(config.resources[1].name, "processResource");
616+
assert_eq!(config.resources[1].resource_type, "Microsoft/Process");
617+
assert_eq!(config.resources[1].api_version.as_deref(), Some("0.1.0"));
618+
}
619+
620+
#[test]
621+
fn test_resources_with_symbolic_names() {
622+
let config_json = r#"{
623+
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/config/document.json",
624+
"languageVersion": "2.2-experimental",
625+
"extensions": {
626+
"dsc": {
627+
"name": "DesiredStateConfiguration",
628+
"version": "0.1.0"
629+
}
630+
},
631+
"resources": {
632+
"echoResource": {
633+
"extension": "dsc",
634+
"type": "Microsoft.DSC.Debug/Echo",
635+
"apiVersion": "1.0.0",
636+
"properties": {
637+
"output": "Hello World"
638+
}
639+
},
640+
"processResource": {
641+
"extension": "dsc",
642+
"type": "Microsoft/Process",
643+
"apiVersion": "0.1.0",
644+
"properties": {
645+
"name": "pwsh",
646+
"pid": 1234
647+
}
648+
}
649+
}
650+
}"#;
651+
652+
let config: Configuration = serde_json::from_str(config_json).unwrap();
653+
assert_eq!(config.resources.len(), 2);
654+
655+
// Find resources by name (order may vary in HashMap)
656+
let echo_resource = config.resources.iter().find(|r| r.name == "echoResource").unwrap();
657+
let process_resource = config.resources.iter().find(|r| r.name == "processResource").unwrap();
658+
659+
assert_eq!(echo_resource.resource_type, "Microsoft.DSC.Debug/Echo");
660+
assert_eq!(echo_resource.api_version.as_deref(), Some("1.0.0"));
661+
662+
assert_eq!(process_resource.resource_type, "Microsoft/Process");
663+
assert_eq!(process_resource.api_version.as_deref(), Some("0.1.0"));
664+
}
469665
}

0 commit comments

Comments
 (0)