Skip to main content

fakecloud_iot/
state.rs

1//! Account-partitioned, serializable state for the AWS IoT Core control plane.
2//!
3//! The registry is deliberately schema-light: every named resource family
4//! (things, policies, certificates, jobs, topic rules, security profiles, ...)
5//! is stored in one uniform two-level map, `resources[resource_type][id]`,
6//! where the stored value is the resource's JSON record (its persisted
7//! attributes plus any minted ARN / id / timestamps). Keeping every key a
8//! plain `String` means the snapshot never depends on the tuple-key serde
9//! adapter and new resource families need no new struct fields.
10//!
11//! Alongside the resource map are:
12//! * `tags` — resource tags keyed by ARN.
13//! * `singletons` — account-scoped singleton configurations (indexing config,
14//!   event configurations, V2 logging options, audit configuration, default
15//!   authorizer, encryption configuration, package configuration, the CA
16//!   registration code, ...), keyed by a stable string.
17//! * `relations` — many-to-many relationship sets keyed by a stable string
18//!   (e.g. `thing-principals:<thing>` -> principal ARNs,
19//!   `principal-policies:<principal>` -> policy names,
20//!   `group-things:<group>` -> thing names). Stored as ordered vectors so
21//!   list operations are deterministic.
22
23use 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/// Per-account IoT Core control-plane state.
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36pub struct IotData {
37    /// `resource_type -> id -> record`. The record is the resource's persisted
38    /// JSON attributes plus any minted ARN / id / timestamps.
39    #[serde(default)]
40    pub resources: BTreeMap<String, BTreeMap<String, Value>>,
41    /// Resource tags keyed by ARN.
42    #[serde(default)]
43    pub tags: BTreeMap<String, BTreeMap<String, String>>,
44    /// Account-scoped singleton configurations keyed by a stable string.
45    #[serde(default)]
46    pub singletons: BTreeMap<String, Value>,
47    /// Many-to-many relationship sets keyed by a stable string.
48    #[serde(default)]
49    pub relations: BTreeMap<String, Vec<String>>,
50    /// Monotonic counter used to mint unique ids within an account.
51    #[serde(default)]
52    pub seq: u64,
53}
54
55impl IotData {
56    /// Fetch a resource record by type + id.
57    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    /// Insert / replace a resource record.
62    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    /// Remove a resource record, returning it if present.
70    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    /// All records of a type, ordered by id.
81    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    /// All records of a type as `(id, record)` pairs, ordered by id. Used by
89    /// list operations whose output element is a plain identifier string (the
90    /// stored key) rather than a projected object.
91    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    /// Next unique sequence value for id minting.
99    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}