1use std::collections::HashSet;
2
3use serde_json::{Map, Value};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct StoreAccessorConfig {
8 pub store_field: String,
10 pub accessors: Vec<String>,
12}
13
14pub trait Store {
16 fn store_accessors() -> &'static [StoreAccessorConfig] {
18 &[]
19 }
20}
21
22#[must_use]
24pub fn store_accessor(store_field: &str, accessors: &[&str]) -> StoreAccessorConfig {
25 StoreAccessorConfig {
26 store_field: store_field.to_owned(),
27 accessors: accessors
28 .iter()
29 .map(|accessor| (*accessor).to_owned())
30 .collect(),
31 }
32}
33
34#[derive(Debug, Clone, Default, PartialEq)]
36pub struct JsonStore {
37 values: Map<String, Value>,
38}
39
40impl JsonStore {
41 #[must_use]
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 #[must_use]
49 pub fn from_map(values: Map<String, Value>) -> Self {
50 Self { values }
51 }
52
53 #[must_use]
55 pub fn as_map(&self) -> &Map<String, Value> {
56 &self.values
57 }
58
59 #[must_use]
61 pub fn get(&self, key: &str) -> Option<&Value> {
62 self.values.get(key)
63 }
64
65 pub fn set(&mut self, key: &str, value: Value) {
67 self.values.insert(key.to_owned(), value);
68 }
69
70 #[must_use]
72 pub fn get_string(&self, key: &str) -> Option<&str> {
73 self.get(key).and_then(Value::as_str)
74 }
75
76 pub fn set_string(&mut self, key: &str, value: impl Into<String>) {
78 self.set(key, Value::String(value.into()));
79 }
80
81 #[must_use]
83 pub fn get_i64(&self, key: &str) -> Option<i64> {
84 self.get(key).and_then(Value::as_i64)
85 }
86
87 pub fn set_i64(&mut self, key: &str, value: i64) {
89 self.set(key, Value::from(value));
90 }
91
92 #[must_use]
94 pub fn get_bool(&self, key: &str) -> Option<bool> {
95 self.get(key).and_then(Value::as_bool)
96 }
97
98 pub fn set_bool(&mut self, key: &str, value: bool) {
100 self.set(key, Value::from(value));
101 }
102
103 #[must_use]
105 pub fn accessors_are_unique(config: &StoreAccessorConfig) -> bool {
106 let mut unique = HashSet::new();
107 config
108 .accessors
109 .iter()
110 .all(|accessor| unique.insert(accessor))
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use serde_json::{Map, json};
117
118 use super::{JsonStore, Store, StoreAccessorConfig, store_accessor};
119
120 #[derive(Debug)]
121 struct SettingsRecord;
122
123 impl Store for SettingsRecord {
124 fn store_accessors() -> &'static [StoreAccessorConfig] {
125 static ACCESSORS: std::sync::LazyLock<Vec<StoreAccessorConfig>> =
126 std::sync::LazyLock::new(|| {
127 vec![store_accessor(
128 "settings",
129 &["timezone", "dark_mode", "login_count"],
130 )]
131 });
132 ACCESSORS.as_slice()
133 }
134 }
135
136 #[derive(Debug)]
137 struct EmptyStoreRecord;
138
139 impl Store for EmptyStoreRecord {}
140
141 #[test]
142 fn store_accessor_preserves_store_field_and_accessors() {
143 let config = store_accessor("settings", &["timezone", "dark_mode"]);
144 assert_eq!(config.store_field, "settings");
145 assert_eq!(config.accessors, vec!["timezone", "dark_mode"]);
146 }
147
148 #[test]
149 fn json_store_starts_empty() {
150 let store = JsonStore::new();
151 assert!(store.as_map().is_empty());
152 }
153
154 #[test]
155 fn json_store_reads_and_writes_raw_values() {
156 let mut store = JsonStore::new();
157 store.set("timezone", json!("UTC"));
158 assert_eq!(store.get("timezone"), Some(&json!("UTC")));
159 }
160
161 #[test]
162 fn json_store_supports_string_getters_and_setters() {
163 let mut store = JsonStore::new();
164 store.set_string("timezone", "UTC");
165 assert_eq!(store.get_string("timezone"), Some("UTC"));
166 }
167
168 #[test]
169 fn json_store_supports_integer_getters_and_setters() {
170 let mut store = JsonStore::new();
171 store.set_i64("login_count", 5);
172 assert_eq!(store.get_i64("login_count"), Some(5));
173 }
174
175 #[test]
176 fn json_store_supports_boolean_getters_and_setters() {
177 let mut store = JsonStore::new();
178 store.set_bool("dark_mode", true);
179 assert_eq!(store.get_bool("dark_mode"), Some(true));
180 }
181
182 #[test]
183 fn json_store_returns_none_for_wrong_types() {
184 let mut store = JsonStore::new();
185 store.set("timezone", json!(1));
186 assert_eq!(store.get_string("timezone"), None);
187 }
188
189 #[test]
190 fn json_store_can_be_built_from_map() {
191 let map = Map::from_iter([(String::from("timezone"), json!("UTC"))]);
192 let store = JsonStore::from_map(map);
193 assert_eq!(store.get_string("timezone"), Some("UTC"));
194 }
195
196 #[test]
197 fn accessors_are_unique_rejects_duplicates() {
198 let config = store_accessor("settings", &["timezone", "timezone"]);
199 assert!(!JsonStore::accessors_are_unique(&config));
200 }
201
202 #[test]
203 fn accessors_are_unique_accepts_distinct_accessors() {
204 let config = store_accessor("settings", &["timezone", "dark_mode"]);
205 assert!(JsonStore::accessors_are_unique(&config));
206 }
207
208 #[test]
209 fn store_trait_defaults_to_declared_accessors() {
210 assert_eq!(SettingsRecord::store_accessors().len(), 1);
211 assert_eq!(SettingsRecord::store_accessors()[0].store_field, "settings");
212 }
213
214 #[test]
215 fn replacing_a_string_value_preserves_unrelated_keys() {
216 let mut store = JsonStore::from_map(Map::from_iter([
217 (String::from("timezone"), json!("UTC")),
218 (String::from("dark_mode"), json!(true)),
219 ]));
220
221 store.set_string("timezone", "PST");
222
223 assert_eq!(store.get_string("timezone"), Some("PST"));
224 assert_eq!(store.get_bool("dark_mode"), Some(true));
225 }
226
227 #[test]
228 fn replacing_a_value_with_a_new_type_updates_typed_accessors() {
229 let mut store = JsonStore::new();
230 store.set_i64("login_count", 5);
231
232 store.set_string("login_count", "five");
233
234 assert_eq!(store.get_i64("login_count"), None);
235 assert_eq!(store.get_string("login_count"), Some("five"));
236 }
237
238 #[test]
239 fn replacing_one_key_keeps_other_existing_values_intact() {
240 let mut store = JsonStore::from_map(Map::from_iter([
241 (String::from("timezone"), json!("UTC")),
242 (String::from("login_count"), json!(2)),
243 (String::from("dark_mode"), json!(false)),
244 ]));
245
246 store.set_i64("login_count", 3);
247
248 assert_eq!(store.get_string("timezone"), Some("UTC"));
249 assert_eq!(store.get_i64("login_count"), Some(3));
250 assert_eq!(store.get_bool("dark_mode"), Some(false));
251 }
252
253 #[test]
254 fn as_map_exposes_nested_raw_values_without_rewriting_them() {
255 let mut store = JsonStore::new();
256 let profile = json!({"locale": "en", "tags": ["admin", "beta"]});
257
258 store.set("profile", profile.clone());
259
260 assert_eq!(store.as_map().get("profile"), Some(&profile));
261 }
262
263 #[test]
264 fn type_specific_getters_return_none_for_incompatible_values() {
265 let mut store = JsonStore::new();
266 store.set("dark_mode", json!("yes"));
267 store.set("login_count", json!({"count": 3}));
268
269 assert_eq!(store.get_bool("dark_mode"), None);
270 assert_eq!(store.get_i64("login_count"), None);
271 }
272
273 #[test]
274 fn store_trait_defaults_to_empty_accessor_metadata() {
275 assert!(EmptyStoreRecord::store_accessors().is_empty());
276 }
277}