faucet_cli/serve/
idempotency.rs1use serde_json::Value;
8use sha2::{Digest, Sha256};
9
10pub fn fingerprint(merged: &Value, name: Option<&str>) -> String {
12 let mut canon = String::new();
13 write_canonical(merged, &mut canon);
14 let mut hasher = Sha256::new();
15 hasher.update(name.unwrap_or("").as_bytes());
16 hasher.update([0u8]); hasher.update(canon.as_bytes());
18 format!("{:x}", hasher.finalize())
19}
20
21pub fn request_fingerprint(
34 config_fingerprint: &str,
35 clock: Option<&str>,
36 timeout_secs: Option<u64>,
37 labels: &std::collections::BTreeMap<String, String>,
38) -> String {
39 let mut hasher = Sha256::new();
40 hasher.update(config_fingerprint.as_bytes());
41 hasher.update([0u8]);
42 hasher.update(clock.unwrap_or("").as_bytes());
43 hasher.update([0u8]);
44 hasher.update(
45 timeout_secs
46 .map(|t| t.to_string())
47 .unwrap_or_default()
48 .as_bytes(),
49 );
50 hasher.update([0u8]);
51 for (k, v) in labels {
52 hasher.update(k.as_bytes());
53 hasher.update([0u8]);
54 hasher.update(v.as_bytes());
55 hasher.update([0u8]);
56 }
57 format!("{:x}", hasher.finalize())
58}
59
60fn write_canonical(v: &Value, out: &mut String) {
62 match v {
63 Value::Object(map) => {
64 let mut keys: Vec<&String> = map.keys().collect();
65 keys.sort_unstable();
66 out.push('{');
67 for (i, k) in keys.iter().enumerate() {
68 if i > 0 {
69 out.push(',');
70 }
71 out.push_str(&serde_json::to_string(k).expect("string key serializes"));
72 out.push(':');
73 write_canonical(&map[*k], out);
74 }
75 out.push('}');
76 }
77 Value::Array(items) => {
78 out.push('[');
79 for (i, item) in items.iter().enumerate() {
80 if i > 0 {
81 out.push(',');
82 }
83 write_canonical(item, out);
84 }
85 out.push(']');
86 }
87 other => out.push_str(&serde_json::to_string(other).expect("scalar serializes")),
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use serde_json::json;
95
96 #[test]
97 fn stable_and_order_independent() {
98 let a = json!({ "b": 1, "a": [1, 2], "c": { "y": true, "x": null } });
99 let b = json!({ "c": { "x": null, "y": true }, "a": [1, 2], "b": 1 });
100 assert_eq!(fingerprint(&a, Some("p")), fingerprint(&b, Some("p")));
101 }
102
103 #[test]
104 fn differs_on_value_change() {
105 let a = json!({ "a": 1 });
106 let b = json!({ "a": 2 });
107 assert_ne!(fingerprint(&a, Some("p")), fingerprint(&b, Some("p")));
108 }
109
110 #[test]
111 fn differs_on_name() {
112 let v = json!({ "a": 1 });
113 assert_ne!(fingerprint(&v, Some("p1")), fingerprint(&v, Some("p2")));
114 }
115
116 #[test]
117 fn array_order_is_significant() {
118 assert_ne!(
119 fingerprint(&json!([1, 2]), None),
120 fingerprint(&json!([2, 1]), None)
121 );
122 }
123
124 #[test]
125 fn request_fingerprint_includes_run_affecting_fields() {
126 use std::collections::BTreeMap;
127 let cfg_fp = fingerprint(&json!({ "a": 1 }), Some("p"));
128 let empty = BTreeMap::new();
129 let base = request_fingerprint(&cfg_fp, None, None, &empty);
130
131 assert_eq!(base, request_fingerprint(&cfg_fp, None, None, &empty));
133 assert_ne!(
135 base,
136 request_fingerprint(&cfg_fp, Some("2026-01-01T00:00:00Z"), None, &empty)
137 );
138 assert_ne!(base, request_fingerprint(&cfg_fp, None, Some(30), &empty));
140 let mut labels = BTreeMap::new();
142 labels.insert("env".to_string(), "prod".to_string());
143 assert_ne!(base, request_fingerprint(&cfg_fp, None, None, &labels));
144 let other_cfg = fingerprint(&json!({ "a": 2 }), Some("p"));
146 assert_ne!(base, request_fingerprint(&other_cfg, None, None, &empty));
147 }
148
149 #[test]
150 fn request_fingerprint_label_value_is_significant() {
151 use std::collections::BTreeMap;
152 let cfg_fp = fingerprint(&json!({}), None);
153 let mut a = BTreeMap::new();
154 a.insert("k".to_string(), "v1".to_string());
155 let mut b = BTreeMap::new();
156 b.insert("k".to_string(), "v2".to_string());
157 assert_ne!(
158 request_fingerprint(&cfg_fp, None, None, &a),
159 request_fingerprint(&cfg_fp, None, None, &b)
160 );
161 }
162}