6.0 KiB
SettingsValue: Custom Settings File Serialization
Problem
Settings values are serialized to the TOML settings file using their serde representations, which aren't always human-friendly. Duration values render as { nanos = 0, secs = 30 } instead of 30, and AgentModeCommandExecutionPredicate values render as opaque regex objects instead of plain strings. The current file_serialize/file_deserialize hooks on the settings macros address this per-setting, but the approach doesn't compose — nested types like Duration inside a struct require the parent to manually handle the transformation.
Changes
New trait: SettingsValue
A trait that setting value types implement to define their TOML file representation:
pub trait SettingsValue: Serialize + DeserializeOwned {
fn to_file_value(&self) -> serde_json::Value { ... } // default: serde passthrough
fn from_file_value(value: &Value) -> Option<Self> { ... } // default: serde passthrough
fn file_schema(gen: &mut SchemaGenerator) -> Schema // default: schemars passthrough
where Self: JsonSchema { ... }
}
to_file_value/from_file_value— parallel serialization path to serde for the settings file. Defaults delegate toserde_json::to_value/from_value.file_schema— returns the JSON Schema describing the file representation. Defaults togen.subschema_for::<Self>(). Override when the file representation differs from serde (e.g. Duration → u64 schema instead of{secs, nanos}object).
SettingsValue is a supertrait bound on Setting::Value.
Derive macro: #[derive(SettingsValue)]
A proc-macro crate (settings_value_derive) generates impls:
- Named-field structs: Recursive serialization — each field calls
to_file_value()/from_file_value()on the field type. Respects#[serde(rename)],#[serde(skip)],#[serde(default)], andOption<T>fields. - Newtype structs (
struct Foo(T)): Delegates to the inner type's impl. - Enums: Serde passthrough (equivalent to empty
impl SettingsValue for T {}). Recursive data-carrying variant support deferred to branch 2.
The derive is gated behind a derive feature on the settings_value crate and re-exported as settings_value::SettingsValue.
Crate structure
Two new crates:
settings_value— trait definition, primitive impls, collection impls, Duration impl. Dependencies:serde,serde_json,instant,schemars. Optionalderivefeature.settings_value_derive— proc-macro crate for#[derive(SettingsValue)]. Dependencies:proc-macro2,quote,syn.
The settings crate depends on settings_value (with derive feature). The app crate depends on it for custom impls. warpui_core gets impls behind an optional settings_value feature flag.
Impls by category
Primitives and standard types — shipped in settings_value with serde passthrough impls:
bool, u8, u16, u32, u64, usize, i8, i16, i32, i64, f32, f64, String, PathBuf.
Generic collections — recursive impls in settings_value:
Vec<T>, Option<T>, HashSet<T> recursively call to_file_value/from_file_value on elements. HashMap<K, V> uses serde passthrough.
Duration — manual impl in settings_value:
to_file_value returns integer seconds, from_file_value parses integer to Duration::from_secs, file_schema returns u64 schema.
AgentModeCommandExecutionPredicate — manual impl in app/:
to_file_value returns the regex as a JSON string, from_file_value parses via new_regex. The Vec<T> impl then makes Vec<AgentModeCommandExecutionPredicate> serialize as an array of strings automatically.
Structs with custom serialization needs (e.g. NotificationsSettings) — use the derive. The struct derive recursively calls trait methods on each field, so Duration fields automatically serialize as integers without manual patching. The #[schemars(with = "u64")] annotation on Duration fields ensures schema correctness.
Most enum and struct types — use #[derive(SettingsValue)] at the type definition site. ~40 types across the app crate.
Exceptions requiring manual passthrough impls:
CycleInfo,AIRequestQuotaInfo— containDateTime<Utc>(foreign type, orphan rule prevents implementingSettingsValuefor it)warpui_coretypes (Weight,ThinStrokes,GraphicsBackend,AccessibilityVerbosity,DisplayIdx) — kept in a centralizedcfg-gated module due to optional feature dependency
Integration with settings macros
The file_serialize, file_deserialize, and file_value_type macro parameters are removed from define_setting!, implement_setting_for_enum!, and define_settings_group!.
The submit_schema_entry! macro uses SettingsValue::file_schema instead of JsonSchema::json_schema for schema generation, ensuring the schema reflects the file representation rather than the serde representation.
Schema validation test
A test in the app crate (app/src/settings/schema_validation_tests.rs) validates that every registered public setting's file_default_value_fn output conforms to its file_schema. This catches mismatches like the Duration case (schema says {secs, nanos} object but file value is integer 30). The test uses jsonschema::draft202012 for validation and covers all ~3000 settings linked via inventory in the app crate.
Behavioral changes to TOML output
Durationvalues change from{ nanos = 0, secs = 30 }to30(integer seconds)AgentModeCommandExecutionPredicatevalues change from opaque regex objects to plain strings- All other types are unchanged (serde-delegating impls produce the same output as before)
Follow-ups
- Snake_case serialization for enum variants in the TOML file (e.g.
ShowAndCollapse→show_and_collapse) via the derive macro's enum arm — branch 2 (daniel/snake-case-enums). - Recursive data-carrying variant support in the enum derive (requires all inner types to impl
SettingsValue) — also branch 2.