Skip to content

Commit 2d179d4

Browse files
committed
Implement deserializer for resources to handle Array and Object
Which is necessary to support ARMv2's new "symbolic names" structure. Many alternatives were tried, including updating the type to be an `enum` and then either implementing the used `Vec`-like interfaces or pattern matching on the type everywhere `resources` used. Neither of these were great and resulted in a lot of unnecessary code churn. The `serde_with` library was also briefly tried with the `PickAs` trait, but the only thing that gained was eliminating the need to define the (very easy) deserialization of `Array`, at the cost of multiple additional libraries. This seems to be the best way forward as it results in the least possible potential problems and eliminates changes throughout the codebase. What it lacks it the ability serialize to ARMv2, but that's not (yet) desired. It also has not fixed `dependsOn` but that would need to be done regardless.
1 parent 1c9baa8 commit 2d179d4

File tree

1 file changed

+117
-1
lines changed

1 file changed

+117
-1
lines changed

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

Lines changed: 117 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

@@ -174,6 +174,7 @@ pub struct Configuration {
174174
pub outputs: Option<HashMap<String, Output>>,
175175
#[serde(skip_serializing_if = "Option::is_none")]
176176
pub parameters: Option<HashMap<String, Parameter>>,
177+
#[serde(deserialize_with = "deserialize_resources")]
177178
pub resources: Vec<Resource>,
178179
#[serde(skip_serializing_if = "Option::is_none")]
179180
pub variables: Option<Map<String, Value>>,
@@ -184,6 +185,44 @@ pub struct Configuration {
184185
pub extensions: Option<Map<String, Value>>,
185186
}
186187

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+
}
224+
}
225+
187226
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
188227
#[serde(deny_unknown_fields)]
189228
pub struct Parameter {
@@ -326,6 +365,7 @@ pub struct Resource {
326365
#[serde(skip_serializing_if = "Option::is_none", rename = "apiVersion")]
327366
pub api_version: Option<String>,
328367
/// A friendly name for the resource instance
368+
#[serde(default)]
329369
pub name: String, // friendly unique instance name
330370
#[serde(skip_serializing_if = "Option::is_none")]
331371
pub comments: Option<String>,
@@ -486,4 +526,80 @@ mod test {
486526

487527
assert!(result.is_ok());
488528
}
529+
530+
#[test]
531+
fn test_resources_as_array() {
532+
let config_json = r#"{
533+
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/config/document.json",
534+
"resources": [
535+
{
536+
"type": "Microsoft.DSC.Debug/Echo",
537+
"name": "echoResource",
538+
"apiVersion": "1.0.0"
539+
},
540+
{
541+
"type": "Microsoft/Process",
542+
"name": "processResource",
543+
"apiVersion": "0.1.0"
544+
}
545+
]
546+
}"#;
547+
548+
let config: Configuration = serde_json::from_str(config_json).unwrap();
549+
550+
assert_eq!(config.resources.len(), 2);
551+
assert_eq!(config.resources[0].name, "echoResource");
552+
assert_eq!(config.resources[0].resource_type, "Microsoft.DSC.Debug/Echo");
553+
assert_eq!(config.resources[0].api_version.as_deref(), Some("1.0.0"));
554+
555+
assert_eq!(config.resources[1].name, "processResource");
556+
assert_eq!(config.resources[1].resource_type, "Microsoft/Process");
557+
assert_eq!(config.resources[1].api_version.as_deref(), Some("0.1.0"));
558+
}
559+
560+
#[test]
561+
fn test_resources_with_symbolic_names() {
562+
let config_json = r#"{
563+
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/config/document.json",
564+
"languageVersion": "2.2-experimental",
565+
"extensions": {
566+
"dsc": {
567+
"name": "DesiredStateConfiguration",
568+
"version": "0.1.0"
569+
}
570+
},
571+
"resources": {
572+
"echoResource": {
573+
"extension": "dsc",
574+
"type": "Microsoft.DSC.Debug/Echo",
575+
"apiVersion": "1.0.0",
576+
"properties": {
577+
"output": "Hello World"
578+
}
579+
},
580+
"processResource": {
581+
"extension": "dsc",
582+
"type": "Microsoft/Process",
583+
"apiVersion": "0.1.0",
584+
"properties": {
585+
"name": "pwsh",
586+
"pid": 1234
587+
}
588+
}
589+
}
590+
}"#;
591+
592+
let config: Configuration = serde_json::from_str(config_json).unwrap();
593+
assert_eq!(config.resources.len(), 2);
594+
595+
// Find resources by name (order may vary in HashMap)
596+
let echo_resource = config.resources.iter().find(|r| r.name == "echoResource").unwrap();
597+
let process_resource = config.resources.iter().find(|r| r.name == "processResource").unwrap();
598+
599+
assert_eq!(echo_resource.resource_type, "Microsoft.DSC.Debug/Echo");
600+
assert_eq!(echo_resource.api_version.as_deref(), Some("1.0.0"));
601+
602+
assert_eq!(process_resource.resource_type, "Microsoft/Process");
603+
assert_eq!(process_resource.api_version.as_deref(), Some("0.1.0"));
604+
}
489605
}

0 commit comments

Comments
 (0)