//! Defines the [`SettingsValue`] trait for controlling how setting value //! types are serialized to and deserialized from the user-visible TOML settings //! file. //! //! The trait provides a parallel serialization path to serde: types implement //! `to_file_value` / `from_file_value` to produce a human-friendly JSON //! representation that the TOML backend converts to TOML. The default //! implementation delegates to serde, so types without custom file formatting //! need only an empty `impl SettingsValue for T {}`. //! //! Cloud sync and platform-native stores (UserDefaults, registry) continue //! using serde directly — this trait is only consulted when writing to or //! reading from the settings file. use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::path::PathBuf; use chrono::{DateTime, Utc}; use instant::Duration; use serde::{de::DeserializeOwned, Serialize}; // Re-export the derive macro when available. use serde_json::Value; #[cfg(feature = "derive")] pub use settings_value_derive::SettingsValue; /// Defines how a type is represented in the user-visible settings file. /// /// The default implementation delegates to serde (`serde_json::to_value` / /// `serde_json::from_value`), so types that don't need a custom file /// representation can use an empty impl: /// /// ```ignore /// impl SettingsValue for MyType {} /// ``` /// /// Types that want a different representation override the methods. For /// example, `Duration` serializes as an integer (seconds) rather than the /// serde `{ secs, nanos }` object. /// /// # Choosing an implementation strategy /// /// **`#[derive(SettingsValue)]`** — the default choice for most types. /// For enums, the derive converts variant names to snake_case and /// recursively serializes inner data via `SettingsValue`. For structs, /// it recursively calls `to_file_value`/`from_file_value` on each field. /// The derive **bypasses serde entirely** — it does not call /// `serde_json::to_value`. /// /// **Empty `impl SettingsValue for T {}`** (serde passthrough) — use when /// the type has custom `Serialize`/`Deserialize` impls that already /// produce the desired file format (e.g. `StartupShell` serializes as /// `Option`, `SyncId` flattens to a plain string). The /// passthrough delegates to serde, so those custom impls are respected. /// Also use this for types in external crates where you cannot add a /// derive attribute (orphan rule). /// /// **Manual impl with overridden methods** — use when neither the derive /// nor the serde output is suitable. For example, /// `AgentModeCommandExecutionPredicate` serializes as a plain regex /// string in the file, which neither the derive nor serde would produce. pub trait SettingsValue: Serialize + DeserializeOwned { /// Converts this value to a JSON representation for the settings file. fn to_file_value(&self) -> Value { serde_json::to_value(self).expect("serializable type should convert to Value") } /// Reconstructs this value from a JSON representation read from the /// settings file. Returns `None` if the value cannot be parsed. fn from_file_value(value: &Value) -> Option where Self: Sized, { serde_json::from_value(value.clone()).ok() } /// Returns the JSON Schema describing the file representation of this type. /// /// The default delegates to `schemars::JsonSchema`, which is correct for /// passthrough types. Override when `to_file_value` produces a different /// shape than serde (e.g. Duration → integer seconds). fn file_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema where Self: schemars::JsonSchema, { gen.subschema_for::() } } // --------------------------------------------------------------------------- // Convenience macro for external types // --------------------------------------------------------------------------- /// Implements `SettingsValue` as a serde passthrough for types outside this /// crate. The listed types should have appropriate `#[serde(rename_all = …)]` /// attributes on their definitions to ensure the desired serialization format /// in the settings file. #[macro_export] macro_rules! impl_snake_case { ($($ty:ty),* $(,)?) => { $(impl $crate::SettingsValue for $ty {})* }; } // --------------------------------------------------------------------------- // Primitive impls (serde passthrough) // --------------------------------------------------------------------------- macro_rules! impl_default_file_format { ($($ty:ty),* $(,)?) => { $(impl SettingsValue for $ty {})* }; } impl_default_file_format!( bool, u8, u16, u32, u64, usize, i8, i16, i32, i64, f32, f64, String, PathBuf, DateTime, ); // --------------------------------------------------------------------------- // Generic collection impls (recursive) // --------------------------------------------------------------------------- impl SettingsValue for Vec { fn to_file_value(&self) -> Value { Value::Array(self.iter().map(T::to_file_value).collect()) } fn from_file_value(value: &Value) -> Option { value.as_array()?.iter().map(T::from_file_value).collect() } } impl SettingsValue for Option { fn to_file_value(&self) -> Value { match self { Some(v) => v.to_file_value(), None => Value::Null, } } fn from_file_value(value: &Value) -> Option { if value.is_null() { Some(None) } else { Some(Some(T::from_file_value(value)?)) } } } impl SettingsValue for HashSet where T: SettingsValue + Eq + Hash, { fn to_file_value(&self) -> Value { Value::Array(self.iter().map(T::to_file_value).collect()) } fn from_file_value(value: &Value) -> Option { value.as_array()?.iter().map(T::from_file_value).collect() } } impl SettingsValue for HashMap where K: SettingsValue + Eq + Hash, V: SettingsValue, { fn to_file_value(&self) -> Value { let mut obj = serde_json::Map::new(); for (k, v) in self { let key_str = match k.to_file_value() { Value::String(s) => s, other => other.to_string(), }; obj.insert(key_str, v.to_file_value()); } Value::Object(obj) } fn from_file_value(value: &Value) -> Option { let obj = value.as_object()?; let mut map = HashMap::new(); for (key_str, val) in obj { let k = K::from_file_value(&Value::String(key_str.clone()))?; let v = V::from_file_value(val)?; map.insert(k, v); } Some(map) } } // --------------------------------------------------------------------------- // Duration — serialize as integer seconds // --------------------------------------------------------------------------- impl SettingsValue for Duration { fn to_file_value(&self) -> Value { Value::Number(self.as_secs().into()) } fn from_file_value(value: &Value) -> Option { value.as_u64().map(Duration::from_secs) } fn file_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema where Self: schemars::JsonSchema, { gen.subschema_for::() } } #[cfg(test)] mod tests { use super::*; #[test] fn duration_round_trip() { let d = Duration::from_secs(30); let file_val = d.to_file_value(); assert_eq!(file_val, Value::Number(30.into())); let back = Duration::from_file_value(&file_val).unwrap(); assert_eq!(back, d); } #[test] fn vec_recursive() { let v = vec![10u32, 20u32]; let file_val = v.to_file_value(); assert_eq!( file_val, Value::Array(vec![Value::Number(10.into()), Value::Number(20.into())]) ); let back = Vec::::from_file_value(&file_val).unwrap(); assert_eq!(back, v); } #[test] fn option_some() { let v: Option = Some(5); let file_val = v.to_file_value(); assert_eq!(file_val, Value::Number(5.into())); let back = Option::::from_file_value(&file_val).unwrap(); assert_eq!(back, v); } #[test] fn option_none() { let v: Option = None; let file_val = v.to_file_value(); assert_eq!(file_val, Value::Null); let back = Option::::from_file_value(&file_val).unwrap(); assert_eq!(back, v); } #[test] fn bool_passthrough() { assert_eq!(true.to_file_value(), Value::Bool(true)); assert_eq!(bool::from_file_value(&Value::Bool(false)), Some(false)); } #[test] fn string_passthrough() { let s = "hello".to_string(); let file_val = s.to_file_value(); assert_eq!(file_val, Value::String("hello".into())); assert_eq!(String::from_file_value(&file_val), Some(s)); } #[test] fn hashmap_round_trip() { let mut m = HashMap::new(); m.insert("key".to_string(), 42u32); let file_val = m.to_file_value(); let obj = file_val.as_object().unwrap(); assert_eq!(obj.get("key"), Some(&Value::Number(42.into()))); let back = HashMap::::from_file_value(&file_val).unwrap(); assert_eq!(back, m); } }