@@ -8,7 +8,7 @@ use dsc_lib_jsonschema::transforms::{
88} ;
99use rust_i18n:: t;
1010use schemars:: { JsonSchema , json_schema} ;
11- use serde:: { Deserialize , Serialize } ;
11+ use serde:: { Deserialize , Deserializer , Serialize } ;
1212use serde_json:: { Map , Value } ;
1313use 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
349404impl 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