1use std::collections::BTreeMap;
24use std::sync::Arc;
25
26use parking_lot::RwLock;
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29
30use fakecloud_core::multi_account::{AccountState, MultiAccountState};
31
32pub const IOT_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
33
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36pub struct IotData {
37 #[serde(default)]
40 pub resources: BTreeMap<String, BTreeMap<String, Value>>,
41 #[serde(default)]
43 pub tags: BTreeMap<String, BTreeMap<String, String>>,
44 #[serde(default)]
46 pub singletons: BTreeMap<String, Value>,
47 #[serde(default)]
49 pub relations: BTreeMap<String, Vec<String>>,
50 #[serde(default)]
52 pub seq: u64,
53}
54
55impl IotData {
56 pub fn get_resource(&self, rtype: &str, id: &str) -> Option<&Value> {
58 self.resources.get(rtype).and_then(|m| m.get(id))
59 }
60
61 pub fn put_resource(&mut self, rtype: &str, id: &str, record: Value) {
63 self.resources
64 .entry(rtype.to_string())
65 .or_default()
66 .insert(id.to_string(), record);
67 }
68
69 pub fn remove_resource(&mut self, rtype: &str, id: &str) -> Option<Value> {
71 let removed = self.resources.get_mut(rtype).and_then(|m| m.remove(id));
72 if let Some(m) = self.resources.get(rtype) {
73 if m.is_empty() {
74 self.resources.remove(rtype);
75 }
76 }
77 removed
78 }
79
80 pub fn list_resources(&self, rtype: &str) -> Vec<Value> {
82 self.resources
83 .get(rtype)
84 .map(|m| m.values().cloned().collect())
85 .unwrap_or_default()
86 }
87
88 pub fn list_resource_entries(&self, rtype: &str) -> Vec<(String, Value)> {
92 self.resources
93 .get(rtype)
94 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
95 .unwrap_or_default()
96 }
97
98 pub fn next_seq(&mut self) -> u64 {
100 self.seq += 1;
101 self.seq
102 }
103}
104
105impl AccountState for IotData {
106 fn new_for_account(_account_id: &str, _region: &str, _endpoint: &str) -> Self {
107 Self::default()
108 }
109}
110
111pub type SharedIotState = Arc<RwLock<MultiAccountState<IotData>>>;
112
113#[derive(Debug, Serialize, Deserialize)]
114pub struct IotSnapshot {
115 pub schema_version: u32,
116 pub accounts: MultiAccountState<IotData>,
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use serde_json::json;
123
124 #[test]
125 fn new_account_is_empty() {
126 let data = IotData::new_for_account("000000000000", "us-east-1", "");
127 assert!(data.resources.is_empty());
128 assert!(data.tags.is_empty());
129 assert!(data.singletons.is_empty());
130 }
131
132 #[test]
133 fn resource_round_trips() {
134 let mut d = IotData::default();
135 d.put_resource("things", "sensor-1", json!({"thingName": "sensor-1"}));
136 assert_eq!(
137 d.get_resource("things", "sensor-1").unwrap()["thingName"],
138 "sensor-1"
139 );
140 assert_eq!(d.list_resources("things").len(), 1);
141 assert!(d.remove_resource("things", "sensor-1").is_some());
142 assert!(d.get_resource("things", "sensor-1").is_none());
143 assert!(!d.resources.contains_key("things"));
144 }
145}