fakecloud_iotwireless/
state.rs1use std::collections::BTreeMap;
26use std::sync::Arc;
27
28use parking_lot::RwLock;
29use serde::{Deserialize, Serialize};
30use serde_json::Value;
31
32use fakecloud_core::multi_account::{AccountState, MultiAccountState};
33
34pub const IOTWIRELESS_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38pub struct IotWirelessData {
39 #[serde(default)]
42 pub resources: BTreeMap<String, BTreeMap<String, Value>>,
43 #[serde(default)]
45 pub tags: BTreeMap<String, BTreeMap<String, String>>,
46 #[serde(default)]
48 pub singletons: BTreeMap<String, Value>,
49 #[serde(default)]
52 pub blobs: BTreeMap<String, String>,
53 #[serde(default)]
55 pub relations: BTreeMap<String, Vec<String>>,
56 #[serde(default)]
58 pub seq: u64,
59}
60
61impl IotWirelessData {
62 pub fn get_resource(&self, rtype: &str, id: &str) -> Option<&Value> {
64 self.resources.get(rtype).and_then(|m| m.get(id))
65 }
66
67 pub fn put_resource(&mut self, rtype: &str, id: &str, record: Value) {
69 self.resources
70 .entry(rtype.to_string())
71 .or_default()
72 .insert(id.to_string(), record);
73 }
74
75 pub fn remove_resource(&mut self, rtype: &str, id: &str) -> Option<Value> {
77 let removed = self.resources.get_mut(rtype).and_then(|m| m.remove(id));
78 if let Some(m) = self.resources.get(rtype) {
79 if m.is_empty() {
80 self.resources.remove(rtype);
81 }
82 }
83 removed
84 }
85
86 pub fn list_resources(&self, rtype: &str) -> Vec<Value> {
88 self.resources
89 .get(rtype)
90 .map(|m| m.values().cloned().collect())
91 .unwrap_or_default()
92 }
93
94 pub fn list_resource_entries(&self, rtype: &str) -> Vec<(String, Value)> {
96 self.resources
97 .get(rtype)
98 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
99 .unwrap_or_default()
100 }
101
102 pub fn next_seq(&mut self) -> u64 {
104 self.seq += 1;
105 self.seq
106 }
107
108 pub fn get_singleton(&self, key: &str) -> Option<&Value> {
112 self.singletons.get(key)
113 }
114
115 pub fn put_singleton(&mut self, key: &str, value: Value) {
117 self.singletons.insert(key.to_string(), value);
118 }
119
120 pub fn add_relation(&mut self, key: &str, member: &str) {
125 let set = self.relations.entry(key.to_string()).or_default();
126 if !set.iter().any(|m| m == member) {
127 set.push(member.to_string());
128 }
129 }
130
131 pub fn remove_relation(&mut self, key: &str, member: &str) {
133 if let Some(set) = self.relations.get_mut(key) {
134 set.retain(|m| m != member);
135 if set.is_empty() {
136 self.relations.remove(key);
137 }
138 }
139 }
140
141 pub fn list_relation(&self, key: &str) -> Vec<String> {
143 self.relations.get(key).cloned().unwrap_or_default()
144 }
145}
146
147impl AccountState for IotWirelessData {
148 fn new_for_account(_account_id: &str, _region: &str, _endpoint: &str) -> Self {
149 Self::default()
150 }
151}
152
153pub type SharedIotWirelessState = Arc<RwLock<MultiAccountState<IotWirelessData>>>;
154
155#[derive(Debug, Serialize, Deserialize)]
156pub struct IotWirelessSnapshot {
157 pub schema_version: u32,
158 pub accounts: MultiAccountState<IotWirelessData>,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use serde_json::json;
165
166 #[test]
167 fn new_account_is_empty() {
168 let data = IotWirelessData::new_for_account("000000000000", "us-east-1", "");
169 assert!(data.resources.is_empty());
170 assert!(data.tags.is_empty());
171 assert!(data.singletons.is_empty());
172 }
173
174 #[test]
175 fn resource_round_trips() {
176 let mut d = IotWirelessData::default();
177 d.put_resource("destinations", "dest-1", json!({"Name": "dest-1"}));
178 assert_eq!(
179 d.get_resource("destinations", "dest-1").unwrap()["Name"],
180 "dest-1"
181 );
182 assert_eq!(d.list_resources("destinations").len(), 1);
183 assert!(d.remove_resource("destinations", "dest-1").is_some());
184 assert!(d.get_resource("destinations", "dest-1").is_none());
185 assert!(!d.resources.contains_key("destinations"));
186 }
187
188 #[test]
189 fn singleton_round_trips() {
190 let mut d = IotWirelessData::default();
191 assert!(d.get_singleton("metric-configuration").is_none());
192 d.put_singleton(
193 "metric-configuration",
194 json!({"SummaryMetric": {"Status": "Enabled"}}),
195 );
196 assert_eq!(
197 d.get_singleton("metric-configuration").unwrap()["SummaryMetric"]["Status"],
198 "Enabled"
199 );
200 }
201
202 #[test]
203 fn relation_edges_round_trip() {
204 let mut d = IotWirelessData::default();
205 d.add_relation("fuota-multicast:task-1", "mc-1");
206 d.add_relation("fuota-multicast:task-1", "mc-2");
207 d.add_relation("fuota-multicast:task-1", "mc-1");
209 assert_eq!(
210 d.list_relation("fuota-multicast:task-1"),
211 vec!["mc-1", "mc-2"]
212 );
213 d.remove_relation("fuota-multicast:task-1", "mc-1");
214 assert_eq!(d.list_relation("fuota-multicast:task-1"), vec!["mc-2"]);
215 d.remove_relation("fuota-multicast:task-1", "mc-2");
216 assert!(d.list_relation("fuota-multicast:task-1").is_empty());
217 assert!(!d.relations.contains_key("fuota-multicast:task-1"));
218 }
219}