use std::collections::HashSet;
use serde_json::{Map, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoreAccessorConfig {
pub store_field: String,
pub accessors: Vec<String>,
}
pub trait Store {
fn store_accessors() -> &'static [StoreAccessorConfig] {
&[]
}
}
#[must_use]
pub fn store_accessor(store_field: &str, accessors: &[&str]) -> StoreAccessorConfig {
StoreAccessorConfig {
store_field: store_field.to_owned(),
accessors: accessors
.iter()
.map(|accessor| (*accessor).to_owned())
.collect(),
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct JsonStore {
values: Map<String, Value>,
}
impl JsonStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_map(values: Map<String, Value>) -> Self {
Self { values }
}
#[must_use]
pub fn as_map(&self) -> &Map<String, Value> {
&self.values
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&Value> {
self.values.get(key)
}
pub fn set(&mut self, key: &str, value: Value) {
self.values.insert(key.to_owned(), value);
}
#[must_use]
pub fn get_string(&self, key: &str) -> Option<&str> {
self.get(key).and_then(Value::as_str)
}
pub fn set_string(&mut self, key: &str, value: impl Into<String>) {
self.set(key, Value::String(value.into()));
}
#[must_use]
pub fn get_i64(&self, key: &str) -> Option<i64> {
self.get(key).and_then(Value::as_i64)
}
pub fn set_i64(&mut self, key: &str, value: i64) {
self.set(key, Value::from(value));
}
#[must_use]
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.get(key).and_then(Value::as_bool)
}
pub fn set_bool(&mut self, key: &str, value: bool) {
self.set(key, Value::from(value));
}
#[must_use]
pub fn accessors_are_unique(config: &StoreAccessorConfig) -> bool {
let mut unique = HashSet::new();
config
.accessors
.iter()
.all(|accessor| unique.insert(accessor))
}
}
#[cfg(test)]
mod tests {
use serde_json::{Map, json};
use super::{JsonStore, Store, StoreAccessorConfig, store_accessor};
#[derive(Debug)]
struct SettingsRecord;
impl Store for SettingsRecord {
fn store_accessors() -> &'static [StoreAccessorConfig] {
static ACCESSORS: std::sync::LazyLock<Vec<StoreAccessorConfig>> =
std::sync::LazyLock::new(|| {
vec![store_accessor(
"settings",
&["timezone", "dark_mode", "login_count"],
)]
});
ACCESSORS.as_slice()
}
}
#[derive(Debug)]
struct EmptyStoreRecord;
impl Store for EmptyStoreRecord {}
#[test]
fn store_accessor_preserves_store_field_and_accessors() {
let config = store_accessor("settings", &["timezone", "dark_mode"]);
assert_eq!(config.store_field, "settings");
assert_eq!(config.accessors, vec!["timezone", "dark_mode"]);
}
#[test]
fn json_store_starts_empty() {
let store = JsonStore::new();
assert!(store.as_map().is_empty());
}
#[test]
fn json_store_reads_and_writes_raw_values() {
let mut store = JsonStore::new();
store.set("timezone", json!("UTC"));
assert_eq!(store.get("timezone"), Some(&json!("UTC")));
}
#[test]
fn json_store_supports_string_getters_and_setters() {
let mut store = JsonStore::new();
store.set_string("timezone", "UTC");
assert_eq!(store.get_string("timezone"), Some("UTC"));
}
#[test]
fn json_store_supports_integer_getters_and_setters() {
let mut store = JsonStore::new();
store.set_i64("login_count", 5);
assert_eq!(store.get_i64("login_count"), Some(5));
}
#[test]
fn json_store_supports_boolean_getters_and_setters() {
let mut store = JsonStore::new();
store.set_bool("dark_mode", true);
assert_eq!(store.get_bool("dark_mode"), Some(true));
}
#[test]
fn json_store_returns_none_for_wrong_types() {
let mut store = JsonStore::new();
store.set("timezone", json!(1));
assert_eq!(store.get_string("timezone"), None);
}
#[test]
fn json_store_can_be_built_from_map() {
let map = Map::from_iter([(String::from("timezone"), json!("UTC"))]);
let store = JsonStore::from_map(map);
assert_eq!(store.get_string("timezone"), Some("UTC"));
}
#[test]
fn accessors_are_unique_rejects_duplicates() {
let config = store_accessor("settings", &["timezone", "timezone"]);
assert!(!JsonStore::accessors_are_unique(&config));
}
#[test]
fn accessors_are_unique_accepts_distinct_accessors() {
let config = store_accessor("settings", &["timezone", "dark_mode"]);
assert!(JsonStore::accessors_are_unique(&config));
}
#[test]
fn store_trait_defaults_to_declared_accessors() {
assert_eq!(SettingsRecord::store_accessors().len(), 1);
assert_eq!(SettingsRecord::store_accessors()[0].store_field, "settings");
}
#[test]
fn replacing_a_string_value_preserves_unrelated_keys() {
let mut store = JsonStore::from_map(Map::from_iter([
(String::from("timezone"), json!("UTC")),
(String::from("dark_mode"), json!(true)),
]));
store.set_string("timezone", "PST");
assert_eq!(store.get_string("timezone"), Some("PST"));
assert_eq!(store.get_bool("dark_mode"), Some(true));
}
#[test]
fn replacing_a_value_with_a_new_type_updates_typed_accessors() {
let mut store = JsonStore::new();
store.set_i64("login_count", 5);
store.set_string("login_count", "five");
assert_eq!(store.get_i64("login_count"), None);
assert_eq!(store.get_string("login_count"), Some("five"));
}
#[test]
fn replacing_one_key_keeps_other_existing_values_intact() {
let mut store = JsonStore::from_map(Map::from_iter([
(String::from("timezone"), json!("UTC")),
(String::from("login_count"), json!(2)),
(String::from("dark_mode"), json!(false)),
]));
store.set_i64("login_count", 3);
assert_eq!(store.get_string("timezone"), Some("UTC"));
assert_eq!(store.get_i64("login_count"), Some(3));
assert_eq!(store.get_bool("dark_mode"), Some(false));
}
#[test]
fn as_map_exposes_nested_raw_values_without_rewriting_them() {
let mut store = JsonStore::new();
let profile = json!({"locale": "en", "tags": ["admin", "beta"]});
store.set("profile", profile.clone());
assert_eq!(store.as_map().get("profile"), Some(&profile));
}
#[test]
fn type_specific_getters_return_none_for_incompatible_values() {
let mut store = JsonStore::new();
store.set("dark_mode", json!("yes"));
store.set("login_count", json!({"count": 3}));
assert_eq!(store.get_bool("dark_mode"), None);
assert_eq!(store.get_i64("login_count"), None);
}
#[test]
fn store_trait_defaults_to_empty_accessor_metadata() {
assert!(EmptyStoreRecord::store_accessors().is_empty());
}
}