Skip to content

Commit be5b5b2

Browse files
committed
update json structure
1 parent 572804b commit be5b5b2

File tree

2 files changed

+249
-53
lines changed

2 files changed

+249
-53
lines changed

datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,6 @@ impl UniversalFlagConfig {
8282
impl From<UniversalFlagConfigWire> for CompiledFlagsConfig {
8383
fn from(config: UniversalFlagConfigWire) -> Self {
8484
let flags = config
85-
.data
86-
.attributes
8785
.flags
8886
.into_iter()
8987
.map(|(key, flag)| {
@@ -99,8 +97,8 @@ impl From<UniversalFlagConfigWire> for CompiledFlagsConfig {
9997
.collect();
10098

10199
CompiledFlagsConfig {
102-
created_at: config.data.attributes.created_at.into(),
103-
environment: config.data.attributes.environment,
100+
created_at: config.created_at.into(),
101+
environment: config.environment,
104102
flags,
105103
}
106104
}

datadog-ffe/src/rules_based/ufc/models.rs

Lines changed: 247 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -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")]
3537
pub(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")]
71123
pub 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)]
467519
pub(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

Comments
 (0)