Coverage Report

Created: 2025-05-08 18:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-config/src/backcompat.rs
Line
Count
Source
1
use std::collections::HashMap;
2
3
use serde::{Deserialize, Deserializer};
4
5
use crate::cas_server::WithInstanceName;
6
7
#[derive(Debug, Deserialize)]
8
#[serde(untagged)]
9
enum WithInstanceNameBackCompat<T> {
10
    Map(HashMap<String, T>),
11
    Vec(Vec<WithInstanceName<T>>),
12
}
13
14
const DEPRECATION_MESSAGE: &str = r#"
15
WARNING: Using deprecated map format for services. Please migrate to the new array format:
16
    // Old:
17
    "cas": {
18
        "main": {
19
            "cas_store": "STORE_NAME"
20
        }
21
    }
22
    // New:
23
    "cas": [
24
        {
25
            "instance_name": "main",
26
            "cas_store": "STORE_NAME"
27
        }
28
    ]
29
"#;
30
31
/// Use `#[serde(default, deserialize_with = "backcompat::opt_vec_named_config")]` for backwards
32
/// compatibility with map-based access. A deprecation warning will be written to stderr if the
33
/// old format is used.
34
2
pub(crate) fn opt_vec_with_instance_name<'de, D, T>(
35
2
    deserializer: D,
36
2
) -> Result<Option<Vec<WithInstanceName<T>>>, D::Error>
37
2
where
38
2
    D: Deserializer<'de>,
39
2
    T: Deserialize<'de>,
40
2
{
41
2
    let Some(back_compat) = Option::deserialize(deserializer)
?0
else {
  Branch (41:9): [Folded - Ignored]
  Branch (41:9): [True: 2, False: 0]
42
0
        return Ok(None);
43
    };
44
45
2
    match back_compat {
46
1
        WithInstanceNameBackCompat::Map(map) => {
47
1
            eprintln!("{DEPRECATION_MESSAGE}");
48
1
            let vec = map
49
1
                .into_iter()
50
2
                .map(|(instance_name, config)| WithInstanceName {
51
2
                    instance_name,
52
2
                    config,
53
2
                })
54
1
                .collect();
55
1
            Ok(Some(vec))
56
        }
57
1
        WithInstanceNameBackCompat::Vec(vec) => Ok(Some(vec)),
58
    }
59
2
}
60
61
#[cfg(test)]
62
mod tests {
63
    use serde_json::json;
64
65
    use super::*;
66
67
    #[derive(Debug, Deserialize, PartialEq)]
68
    struct PartialConfig {
69
        store: String,
70
    }
71
72
0
    #[derive(Debug, Deserialize, PartialEq)]
73
    struct FullConfig {
74
        #[serde(default, deserialize_with = "opt_vec_with_instance_name")]
75
        cas: Option<Vec<WithInstanceName<PartialConfig>>>,
76
    }
77
78
    #[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
1
            }
85
1
        });
86
1
87
1
        let new_format = json!({
88
1
            "cas": [
89
1
                {
90
1
                    "instance_name": "foo",
91
1
                    "store": "foo_store"
92
1
                },
93
1
                {
94
1
                    "instance_name": "bar",
95
1
                    "store": "bar_store"
96
1
                }
97
1
            ]
98
1
        });
99
1
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
1
    }
113
114
    #[test]
115
1
    fn test_deserialize_none() {
116
1
        let json = json!({});
117
1
118
1
        let value: FullConfig = serde_json::from_value(json).unwrap();
119
1
        assert_eq!(value.cas, None);
120
1
    }
121
}