1use omgbase_store::Store;
6use rusqlite::{OptionalExtension, params};
7use serde_json::{Map, Value};
8
9use crate::error::Result;
10
11pub type Settings = Map<String, Value>;
13
14fn parse(text: Option<String>) -> Settings {
16 match text.and_then(|t| serde_json::from_str::<Value>(&t).ok()) {
17 Some(Value::Object(m)) => m,
18 _ => Settings::new(),
19 }
20}
21
22pub fn workspace_settings(store: &Store) -> Result<Settings> {
24 let text: Option<String> = store
25 .conn()
26 .query_row(
27 "SELECT settings FROM workspace_settings WHERE id = 0",
28 [],
29 |r| r.get(0),
30 )
31 .optional()?;
32 Ok(parse(text))
33}
34
35pub fn repo_own_settings(store: &Store, repo_id: &str) -> Result<Settings> {
37 let text: Option<String> = store
38 .conn()
39 .query_row(
40 "SELECT settings FROM repos WHERE repo_id = ?1",
41 params![repo_id],
42 |r| r.get(0),
43 )
44 .optional()?;
45 Ok(parse(text))
46}
47
48pub fn write_workspace_settings(store: &Store, settings: &Settings) -> Result<()> {
50 store.conn().execute(
51 "INSERT INTO workspace_settings (id, settings) VALUES (0, ?1)
52 ON CONFLICT(id) DO UPDATE SET settings = excluded.settings",
53 params![Value::Object(settings.clone()).to_string()],
54 )?;
55 Ok(())
56}
57
58pub fn write_repo_settings(store: &Store, repo_id: &str, settings: &Settings) -> Result<()> {
60 store.conn().execute(
61 "UPDATE repos SET settings = ?1 WHERE repo_id = ?2",
62 params![Value::Object(settings.clone()).to_string(), repo_id],
63 )?;
64 Ok(())
65}
66
67#[must_use]
72pub fn deep_merge(base: &Settings, over: &Settings) -> Settings {
73 let mut out = base.clone();
74 for (k, v) in over {
75 let merged = match (out.get(k), v) {
76 (Some(Value::Object(b)), Value::Object(o)) => Value::Object(deep_merge(b, o)),
77 _ => v.clone(),
78 };
79 out.insert(k.clone(), merged);
80 }
81 out
82}
83
84pub fn resolve_settings(store: &Store, repo_id: Option<&str>) -> Result<Settings> {
86 let defaults = workspace_settings(store)?;
87 match repo_id {
88 None => Ok(defaults),
89 Some(id) => Ok(deep_merge(&defaults, &repo_own_settings(store, id)?)),
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use serde_json::json;
97
98 fn obj(v: Value) -> Settings {
99 v.as_object().cloned().unwrap()
100 }
101
102 #[test]
103 fn deep_merge_rules() {
104 let base = obj(json!({"a": 1, "e": {"m": "x", "d": 768}, "arr": [1, 2], "keep": true}));
105 let over = obj(json!({"e": {"d": 384, "new": 1}, "arr": [3], "a": null, "z": {"q": 1}}));
106 let merged = deep_merge(&base, &over);
107 assert_eq!(
108 Value::Object(merged.clone()),
109 json!({"a": null, "e": {"m": "x", "d": 384, "new": 1}, "arr": [3], "keep": true, "z": {"q": 1}})
110 );
111 assert_eq!(
112 merged.keys().collect::<Vec<_>>(),
113 ["a", "e", "arr", "keep", "z"],
114 "base order kept, new keys appended"
115 );
116 assert_eq!(
118 Value::Object(deep_merge(
119 &obj(json!({"x": {"a": 1}})),
120 &obj(json!({"x": 2}))
121 )),
122 json!({"x": 2})
123 );
124 assert_eq!(
125 Value::Object(deep_merge(
126 &obj(json!({"x": 2})),
127 &obj(json!({"x": {"a": 1}}))
128 )),
129 json!({"x": {"a": 1}})
130 );
131 assert_eq!(
132 deep_merge(&Settings::new(), &Settings::new()),
133 Settings::new()
134 );
135 }
136
137 #[test]
138 fn layers_round_trip_and_bad_text_reads_as_empty() {
139 let mut s = Store::open_in_memory().unwrap();
140 let repo = s.create_repo("r").unwrap();
141 assert!(workspace_settings(&s).unwrap().is_empty());
142 assert!(repo_own_settings(&s, &repo).unwrap().is_empty());
143 write_workspace_settings(&s, &obj(json!({"embedding": {"model": "a", "dim": 1}}))).unwrap();
144 write_repo_settings(&s, &repo, &obj(json!({"embedding": {"dim": 2}}))).unwrap();
145 assert_eq!(
146 Value::Object(resolve_settings(&s, Some(&repo)).unwrap()),
147 json!({"embedding": {"model": "a", "dim": 2}})
148 );
149 assert_eq!(
150 Value::Object(resolve_settings(&s, None).unwrap()),
151 json!({"embedding": {"model": "a", "dim": 1}})
152 );
153 assert!(
154 resolve_settings(&s, Some("rp_nope"))
155 .unwrap()
156 .contains_key("embedding")
157 );
158 s.conn()
159 .execute("UPDATE workspace_settings SET settings = 'not json'", [])
160 .unwrap();
161 assert!(workspace_settings(&s).unwrap().is_empty());
162 s.conn()
163 .execute("UPDATE workspace_settings SET settings = '[1]'", [])
164 .unwrap();
165 assert!(workspace_settings(&s).unwrap().is_empty());
166 write_workspace_settings(&s, &obj(json!({"k": 1}))).unwrap();
167 assert_eq!(workspace_settings(&s).unwrap(), obj(json!({"k": 1})));
168 }
169}