@@ -31,28 +31,12 @@ impl From<WireTimestamp> for Timestamp {
3131}
3232
3333/// JSON API wrapper for Universal Flag Configuration.
34- #[ derive( Debug , Serialize , Deserialize , Clone ) ]
34+ /// Supports both the new flat format and the legacy nested format for backward compatibility.
35+ #[ derive( Debug , Serialize , Clone ) ]
36+ #[ serde( rename_all = "camelCase" ) ]
3537pub ( crate ) struct UniversalFlagConfigWire {
36- /// JSON API data envelope.
37- pub data : UniversalFlagConfigData ,
38- }
39-
40- /// JSON API data structure for Universal Flag Configuration.
41- #[ derive( Debug , Serialize , Deserialize , Clone ) ]
42- pub ( crate ) struct UniversalFlagConfigData {
43- /// JSON API type field.
44- #[ serde( rename = "type" ) ]
45- pub data_type : String ,
46- /// JSON API id field.
38+ /// Configuration id field.
4739 pub id : String ,
48- /// JSON API attributes containing the actual UFC data.
49- pub attributes : UniversalFlagConfigAttributes ,
50- }
51-
52- /// Universal Flag Configuration attributes. This contains the actual flag configuration data.
53- #[ derive( Debug , Serialize , Deserialize , Clone ) ]
54- #[ serde( rename_all = "camelCase" ) ]
55- pub ( crate ) struct UniversalFlagConfigAttributes {
5640 /// When configuration was last updated.
5741 pub created_at : WireTimestamp ,
5842 #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
@@ -66,6 +50,74 @@ pub(crate) struct UniversalFlagConfigAttributes {
6650 pub flags : HashMap < Str , TryParse < FlagWire > > ,
6751}
6852
53+ // Support both flat and nested formats during deserialization
54+ impl < ' de > Deserialize < ' de > for UniversalFlagConfigWire {
55+ fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
56+ where
57+ D : serde:: Deserializer < ' de > ,
58+ {
59+ #[ derive( Deserialize ) ]
60+ #[ serde( untagged) ]
61+ enum UniversalFlagConfigWireHelper {
62+ // New flat format (preferred)
63+ Flat {
64+ id : String ,
65+ #[ serde( rename = "createdAt" ) ]
66+ created_at : WireTimestamp ,
67+ #[ serde( default ) ]
68+ format : Option < ConfigurationFormat > ,
69+ environment : Environment ,
70+ flags : HashMap < Str , TryParse < FlagWire > > ,
71+ } ,
72+ // Legacy nested format (for backward compatibility)
73+ Nested {
74+ data : UniversalFlagConfigDataLegacy ,
75+ } ,
76+ }
77+
78+ #[ derive( Deserialize ) ]
79+ struct UniversalFlagConfigDataLegacy {
80+ #[ serde( rename = "type" ) ]
81+ _data_type : String ,
82+ id : String ,
83+ attributes : UniversalFlagConfigAttributesLegacy ,
84+ }
85+
86+ #[ derive( Deserialize ) ]
87+ #[ serde( rename_all = "camelCase" ) ]
88+ struct UniversalFlagConfigAttributesLegacy {
89+ created_at : WireTimestamp ,
90+ #[ serde( default ) ]
91+ format : Option < ConfigurationFormat > ,
92+ environment : Environment ,
93+ flags : HashMap < Str , TryParse < FlagWire > > ,
94+ }
95+
96+ let helper = UniversalFlagConfigWireHelper :: deserialize ( deserializer) ?;
97+
98+ match helper {
99+ UniversalFlagConfigWireHelper :: Flat { id, created_at, format, environment, flags } => {
100+ Ok ( UniversalFlagConfigWire {
101+ id,
102+ created_at,
103+ format,
104+ environment,
105+ flags,
106+ } )
107+ }
108+ UniversalFlagConfigWireHelper :: Nested { data } => {
109+ Ok ( UniversalFlagConfigWire {
110+ id : data. id ,
111+ created_at : data. attributes . created_at ,
112+ format : data. attributes . format ,
113+ environment : data. attributes . environment ,
114+ flags : data. attributes . flags ,
115+ } )
116+ }
117+ }
118+ }
119+ }
120+
69121#[ derive( Debug , Serialize , Deserialize , Clone ) ]
70122#[ serde( rename_all = "SCREAMING_SNAKE_CASE" ) ]
71123pub enum ConfigurationFormat {
@@ -461,16 +513,78 @@ impl From<Vec<String>> for ConditionValue {
461513 }
462514}
463515
464- #[ derive( Debug , Serialize , Deserialize , Clone ) ]
516+ #[ derive( Debug , Serialize , Clone ) ]
465517#[ serde( rename_all = "camelCase" ) ]
466518#[ allow( missing_docs) ]
467519pub ( crate ) struct SplitWire {
468520 pub shards : Vec < ShardWire > ,
469521 pub variation_key : Str ,
470- #[ serde( default ) ]
522+ #[ serde(
523+ default ,
524+ deserialize_with = "deserialize_extra_logging" ,
525+ skip_serializing_if = "Option::is_none"
526+ ) ]
471527 pub extra_logging : Option < Arc < HashMap < String , String > > > ,
472528}
473529
530+ fn deserialize_extra_logging < ' de , D > (
531+ deserializer : D ,
532+ ) -> Result < Option < Arc < HashMap < String , String > > > , D :: Error >
533+ where
534+ D : serde:: Deserializer < ' de > ,
535+ {
536+ use serde:: de:: Error ;
537+
538+ let value: Option < serde_json:: Value > = Option :: deserialize ( deserializer) ?;
539+ match value {
540+ None => Ok ( None ) ,
541+ Some ( serde_json:: Value :: Null ) => Ok ( None ) ,
542+ Some ( serde_json:: Value :: String ( s) ) if s == "None" => Ok ( None ) ,
543+ Some ( serde_json:: Value :: Object ( map) ) => {
544+ let mut result = HashMap :: new ( ) ;
545+ for ( k, v) in map {
546+ if let serde_json:: Value :: String ( s) = v {
547+ result. insert ( k, s) ;
548+ } else {
549+ return Err ( D :: Error :: custom ( format ! (
550+ "extraLogging values must be strings, got: {:?}" ,
551+ v
552+ ) ) ) ;
553+ }
554+ }
555+ Ok ( Some ( Arc :: new ( result) ) )
556+ }
557+ Some ( other) => Err ( D :: Error :: custom ( format ! (
558+ "extraLogging must be null, \" None\" , or an object, got: {:?}" ,
559+ other
560+ ) ) ) ,
561+ }
562+ }
563+
564+ // Manual Deserialize implementation for SplitWire
565+ impl < ' de > serde:: Deserialize < ' de > for SplitWire {
566+ fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
567+ where
568+ D : serde:: Deserializer < ' de > ,
569+ {
570+ #[ derive( Deserialize ) ]
571+ #[ serde( rename_all = "camelCase" ) ]
572+ struct SplitWireHelper {
573+ shards : Vec < ShardWire > ,
574+ variation_key : Str ,
575+ #[ serde( default , deserialize_with = "deserialize_extra_logging" ) ]
576+ extra_logging : Option < Arc < HashMap < String , String > > > ,
577+ }
578+
579+ let helper = SplitWireHelper :: deserialize ( deserializer) ?;
580+ Ok ( SplitWire {
581+ shards : helper. shards ,
582+ variation_key : helper. variation_key ,
583+ extra_logging : helper. extra_logging ,
584+ } )
585+ }
586+ }
587+
474588#[ derive( Debug , Clone , PartialEq , Serialize , Deserialize ) ]
475589#[ serde( rename_all = "camelCase" ) ]
476590#[ allow( missing_docs) ]
@@ -515,29 +629,24 @@ mod tests {
515629 let ufc: UniversalFlagConfigWire = serde_json:: from_str (
516630 r#"
517631 {
518- "data": {
519- "type": "universal-flag-configuration",
520- "id": "1",
521- "attributes": {
522- "createdAt": "2024-07-18T00:00:00Z",
523- "format": "SERVER",
524- "environment": {"name": "test"},
525- "flags": {
526- "success": {
527- "key": "success",
528- "enabled": true,
529- "variationType": "BOOLEAN",
530- "variations": {},
531- "allocations": []
532- },
533- "fail_parsing": {
534- "key": "fail_parsing",
535- "enabled": true,
536- "variationType": "NEW_TYPE",
537- "variations": {},
538- "allocations": []
539- }
540- }
632+ "id": "1",
633+ "createdAt": "2024-07-18T00:00:00Z",
634+ "format": "SERVER",
635+ "environment": {"name": "test"},
636+ "flags": {
637+ "success": {
638+ "key": "success",
639+ "enabled": true,
640+ "variationType": "BOOLEAN",
641+ "variations": {},
642+ "allocations": []
643+ },
644+ "fail_parsing": {
645+ "key": "fail_parsing",
646+ "enabled": true,
647+ "variationType": "NEW_TYPE",
648+ "variations": {},
649+ "allocations": []
541650 }
542651 }
543652 }
@@ -546,19 +655,108 @@ mod tests {
546655 . unwrap ( ) ;
547656 assert ! (
548657 matches!(
549- ufc. data . attributes . flags. get( "success" ) . unwrap( ) ,
658+ ufc. flags. get( "success" ) . unwrap( ) ,
550659 TryParse :: Parsed ( _)
551660 ) ,
552661 "{:?} should match TryParse::Parsed(_)" ,
553- ufc. data . attributes . flags. get( "success" ) . unwrap( )
662+ ufc. flags. get( "success" ) . unwrap( )
554663 ) ;
555664 assert ! (
556665 matches!(
557- ufc. data . attributes . flags. get( "fail_parsing" ) . unwrap( ) ,
666+ ufc. flags. get( "fail_parsing" ) . unwrap( ) ,
558667 TryParse :: ParseFailed ( _)
559668 ) ,
560669 "{:?} should match TryParse::ParseFailed(_)" ,
561- ufc. data . attributes . flags. get( "fail_parsing" ) . unwrap( )
670+ ufc. flags. get( "fail_parsing" ) . unwrap( )
562671 ) ;
563672 }
673+
674+ #[ test]
675+ fn parse_data_json ( ) {
676+ // Test parsing the actual data.json file with the new flat structure
677+ let json_content = {
678+ let path = if std:: path:: Path :: new ( "tests/data.json" ) . exists ( ) {
679+ "tests/data.json"
680+ } else if std:: path:: Path :: new ( "datadog-ffe/tests/data.json" ) . exists ( ) {
681+ "datadog-ffe/tests/data.json"
682+ } else {
683+ return ; // Skip test if file not found
684+ } ;
685+ std:: fs:: read_to_string ( path) . unwrap ( )
686+ } ;
687+ let ufc: UniversalFlagConfigWire = serde_json:: from_str ( & json_content)
688+ . expect ( "Failed to parse data.json" ) ;
689+
690+ // Verify basic structure
691+ assert_eq ! ( ufc. id, "1" ) ;
692+ assert_eq ! ( & ufc. environment. name as & str , "staging" ) ;
693+ assert ! ( ufc. flags. len( ) > 0 , "Should have at least one flag" ) ;
694+
695+ // Verify a specific flag exists and is parsed correctly
696+ let flag = match ufc. flags . get ( "alberto-flag" ) . unwrap ( ) {
697+ TryParse :: Parsed ( f) => f,
698+ TryParse :: ParseFailed ( v) => panic ! ( "Failed to parse alberto-flag: {:?}" , v) ,
699+ } ;
700+ assert_eq ! ( & flag. key as & str , "alberto-flag" ) ;
701+ assert_eq ! ( flag. enabled, true ) ;
702+ }
703+
704+ #[ test]
705+ fn parse_extra_logging_as_string_none ( ) {
706+ let ufc: UniversalFlagConfigWire = serde_json:: from_str (
707+ r#"
708+ {
709+ "id": "1",
710+ "createdAt": "2024-07-18T00:00:00Z",
711+ "format": "SERVER",
712+ "environment": {"name": "test"},
713+ "flags": {
714+ "aaron-s-hand-modified-cool-flag-with-emoji-in-name-great": {
715+ "key": "aaron-s-hand-modified-cool-flag-with-emoji-in-name-great",
716+ "enabled": true,
717+ "variationType": "BOOLEAN",
718+ "variations": {
719+ "false": {
720+ "key": "false",
721+ "value": false
722+ },
723+ "true": {
724+ "key": "true",
725+ "value": true
726+ }
727+ },
728+ "allocations": [
729+ {
730+ "key": "allocation-default",
731+ "rules": [],
732+ "splits": [
733+ {
734+ "shards": [],
735+ "variationKey": "true",
736+ "extraLogging": "None"
737+ }
738+ ],
739+ "doLog": true
740+ }
741+ ]
742+ }
743+ }
744+ }
745+ "# ,
746+ )
747+ . unwrap ( ) ;
748+
749+ let flag = match ufc. flags . get ( "aaron-s-hand-modified-cool-flag-with-emoji-in-name-great" ) . unwrap ( ) {
750+ TryParse :: Parsed ( f) => f,
751+ TryParse :: ParseFailed ( _) => panic ! ( "Failed to parse flag" ) ,
752+ } ;
753+
754+ assert_eq ! ( & flag. key as & str , "aaron-s-hand-modified-cool-flag-with-emoji-in-name-great" ) ;
755+ assert_eq ! ( flag. enabled, true ) ;
756+ assert_eq ! ( flag. allocations. len( ) , 1 ) ;
757+ assert_eq ! ( flag. allocations[ 0 ] . splits. len( ) , 1 ) ;
758+
759+ // extraLogging should be None when the JSON contains "None" as a string
760+ assert ! ( flag. allocations[ 0 ] . splits[ 0 ] . extra_logging. is_none( ) ) ;
761+ }
564762}
0 commit comments