/build/source/nativelink-config/src/backcompat.rs
Line | Count | Source |
1 | | use std::collections::HashMap; |
2 | | |
3 | | use serde::{Deserialize, Deserializer, Serialize}; |
4 | | use tracing::warn; |
5 | | |
6 | | use crate::cas_server::WithInstanceName; |
7 | | |
8 | | #[derive(Debug, Deserialize)] |
9 | | #[serde(untagged)] |
10 | | enum WithInstanceNameBackCompat<T> { |
11 | | Map(HashMap<String, T>), |
12 | | Vec(Vec<WithInstanceName<T>>), |
13 | | } |
14 | | |
15 | | /// Use `#[serde(default, deserialize_with = "backcompat::opt_vec_named_config")]` for backwards |
16 | | /// compatibility with map-based access. A deprecation warning will be written to stderr if the |
17 | | /// old format is used. |
18 | 25 | pub(crate) fn opt_vec_with_instance_name<'de, D, T>( |
19 | 25 | deserializer: D, |
20 | 25 | ) -> Result<Option<Vec<WithInstanceName<T>>>, D::Error> |
21 | 25 | where |
22 | 25 | D: Deserializer<'de>, |
23 | 25 | T: Deserialize<'de> + Serialize, |
24 | | { |
25 | 25 | let Some(back_compat) = Option::deserialize(deserializer)?0 else { Branch (25:9): [True: 0, False: 0]
Branch (25:9): [True: 0, False: 0]
Branch (25:9): [True: 6, False: 0]
Branch (25:9): [True: 6, False: 0]
Branch (25:9): [True: 5, False: 0]
Branch (25:9): [True: 6, False: 0]
Branch (25:9): [True: 2, False: 0]
|
26 | 0 | return Ok(None); |
27 | | }; |
28 | | |
29 | 25 | match back_compat { |
30 | 1 | WithInstanceNameBackCompat::Map(map) => { |
31 | | // TODO(palfrey): ideally this would be serde_json5::to_string_pretty but that doesn't exist |
32 | | // JSON is close enough to be workable for now |
33 | 1 | let serde_map = serde_json::to_string_pretty(&map).expect("valid map"); |
34 | 1 | let vec: Vec<WithInstanceName<T>> = map |
35 | 1 | .into_iter() |
36 | 1 | .map(|(instance_name, config)| WithInstanceName { |
37 | 2 | instance_name, |
38 | 2 | config, |
39 | 2 | }) |
40 | 1 | .collect(); |
41 | 1 | warn!( |
42 | 1 | r"WARNING: Using deprecated map format for services. Please migrate to the new array format: |
43 | 1 | // Old: |
44 | 1 | {} |
45 | 1 | // New: |
46 | 1 | {} |
47 | 1 | ", |
48 | | serde_map, |
49 | | // TODO(palfrey): ideally this would be serde_json5::to_string_pretty but that doesn't exist |
50 | | // JSON is close enough to be workable for now |
51 | 1 | serde_json::to_string_pretty(&vec).expect("valid new map") |
52 | | ); |
53 | 1 | Ok(Some(vec)) |
54 | | } |
55 | 24 | WithInstanceNameBackCompat::Vec(vec) => Ok(Some(vec)), |
56 | | } |
57 | 25 | } |
58 | | |
59 | | #[cfg(test)] |
60 | | mod tests { |
61 | | use serde_json::json; |
62 | | use tracing_test::traced_test; |
63 | | |
64 | | use super::*; |
65 | | |
66 | | #[derive(Debug, Deserialize, Serialize, PartialEq)] |
67 | | struct PartialConfig { |
68 | | store: String, |
69 | | } |
70 | | |
71 | | #[derive(Debug, Deserialize, Serialize, PartialEq)] |
72 | | struct FullConfig { |
73 | | #[serde(default, deserialize_with = "opt_vec_with_instance_name")] |
74 | | cas: Option<Vec<WithInstanceName<PartialConfig>>>, |
75 | | } |
76 | | |
77 | | #[test] |
78 | | #[traced_test] |
79 | 1 | fn test_configs_deserialization() { |
80 | 1 | let old_format = json!({ |
81 | 1 | "cas": { |
82 | 1 | "foo": { "store": "foo_store" }, |
83 | 1 | "bar": { "store": "bar_store" } |
84 | | } |
85 | | }); |
86 | | |
87 | 1 | let new_format = json!({ |
88 | 1 | "cas": [ |
89 | | { |
90 | 1 | "instance_name": "foo", |
91 | 1 | "store": "foo_store" |
92 | | }, |
93 | | { |
94 | 1 | "instance_name": "bar", |
95 | 1 | "store": "bar_store" |
96 | | } |
97 | | ] |
98 | | }); |
99 | | |
100 | 1 | let mut old_format: FullConfig = serde_json::from_value(old_format).unwrap(); |
101 | 1 | let mut new_format: FullConfig = serde_json::from_value(new_format).unwrap(); |
102 | | |
103 | | // Ensure deterministic ordering. |
104 | 1 | if let Some(vec) = old_format.cas.as_mut() { Branch (104:16): [True: 1, False: 0]
|
105 | 1 | vec.sort_by(|a, b| a.instance_name.cmp(&b.instance_name)); |
106 | 0 | } |
107 | 1 | if let Some(vec) = new_format.cas.as_mut() { Branch (107:16): [True: 1, False: 0]
|
108 | 1 | vec.sort_by(|a, b| a.instance_name.cmp(&b.instance_name)); |
109 | 0 | } |
110 | | |
111 | 1 | assert_eq!(old_format, new_format); |
112 | | |
113 | 1 | logs_assert(|lines: &[&str]| { |
114 | 1 | if lines.len() != 1 { Branch (114:16): [True: 0, False: 1]
|
115 | 0 | return Err(format!("Expected 1 log line, got: {lines:?}")); |
116 | 1 | } |
117 | 1 | let line = lines[0]; |
118 | | // TODO(palfrey): we should be checking the whole thing, but tracing-test is broken with multi-line items |
119 | | // See https://github.com/dbrgn/tracing-test/issues/48 |
120 | 1 | assert!(line.ends_with("WARNING: Using deprecated map format for services. Please migrate to the new array format:")); |
121 | 1 | Ok(()) |
122 | 1 | }); |
123 | 1 | } |
124 | | |
125 | | #[test] |
126 | 1 | fn test_deserialize_none() { |
127 | 1 | let json = json!({}); |
128 | | |
129 | 1 | let value: FullConfig = serde_json::from_value(json).unwrap(); |
130 | 1 | assert_eq!(value.cas, None); |
131 | 1 | } |
132 | | } |