1use serde::{Deserialize, Serialize};
24use serde_json::Value;
25
26pub const METHOD_OPEN: &str = "settings.editor.open";
27pub const METHOD_RENDER: &str = "settings.editor.render";
28pub const METHOD_KEY: &str = "settings.editor.key";
29pub const METHOD_COMMIT: &str = "settings.editor.commit";
30pub const METHOD_CLOSE: &str = "settings.editor.close";
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct SettingsEditorOpenParams {
36 pub category: String,
37 pub field: String,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct SettingsEditorKeyParams {
47 pub key: String,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct SettingsEditorCommitParams {
55 pub value: Value,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
61pub struct SettingsEditorCloseParams {
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub reason: Option<String>,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct SettingsEditorRenderParams {
71 pub rows: Vec<SettingsEditorRow>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub cursor: Option<usize>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub footer: Option<String>,
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct SettingsEditorRow {
84 pub label: String,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub marker: Option<String>,
89 #[serde(default = "default_true")]
92 pub selectable: bool,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub data: Option<Value>,
97}
98
99fn default_true() -> bool {
100 true
101}
102
103#[derive(Debug, thiserror::Error)]
107pub enum SettingsEditorParseError {
108 #[error("unknown settings.editor method: {0}")]
109 UnknownMethod(String),
110 #[error("invalid params for {method}: {source}")]
111 InvalidParams {
112 method: &'static str,
113 #[source]
114 source: serde_json::Error,
115 },
116}
117
118#[derive(Debug, Clone, PartialEq)]
123pub enum InboundSettingsEditorFrame {
124 Render(SettingsEditorRenderParams),
125 Commit(SettingsEditorCommitParams),
126 Close(SettingsEditorCloseParams),
127}
128
129pub fn parse_inbound(
131 method: &str,
132 params: Value,
133) -> Result<InboundSettingsEditorFrame, SettingsEditorParseError> {
134 match method {
135 METHOD_RENDER => serde_json::from_value(params)
136 .map(InboundSettingsEditorFrame::Render)
137 .map_err(|source| SettingsEditorParseError::InvalidParams {
138 method: METHOD_RENDER,
139 source,
140 }),
141 METHOD_COMMIT => serde_json::from_value(params)
142 .map(InboundSettingsEditorFrame::Commit)
143 .map_err(|source| SettingsEditorParseError::InvalidParams {
144 method: METHOD_COMMIT,
145 source,
146 }),
147 METHOD_CLOSE => serde_json::from_value(params)
148 .map(InboundSettingsEditorFrame::Close)
149 .map_err(|source| SettingsEditorParseError::InvalidParams {
150 method: METHOD_CLOSE,
151 source,
152 }),
153 other => Err(SettingsEditorParseError::UnknownMethod(other.to_string())),
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use serde_json::json;
161
162 #[test]
163 fn open_params_round_trip() {
164 let p = SettingsEditorOpenParams {
165 category: "capture".to_string(),
166 field: "model_path".to_string(),
167 };
168 let v = serde_json::to_value(&p).unwrap();
169 assert_eq!(v, json!({"category":"capture","field":"model_path"}));
170 let back: SettingsEditorOpenParams = serde_json::from_value(v).unwrap();
171 assert_eq!(back, p);
172 }
173
174 #[test]
175 fn render_params_parse_full_example() {
176 let v = json!({
177 "rows": [
178 { "label": "tiny.en (75 MB)", "marker": "✓", "selectable": true, "data": "/abs/path/.bin" },
179 { "label": "base (142 MB)", "marker": " ", "selectable": true, "data": "download:base" },
180 { "label": "(separator)", "selectable": false }
181 ],
182 "cursor": 2,
183 "footer": "Up/Down Enter to select"
184 });
185 let frame = parse_inbound(METHOD_RENDER, v).unwrap();
186 match frame {
187 InboundSettingsEditorFrame::Render(r) => {
188 assert_eq!(r.rows.len(), 3);
189 assert_eq!(r.rows[0].marker.as_deref(), Some("✓"));
190 assert!(r.rows[0].selectable);
191 assert_eq!(r.rows[0].data.as_ref().unwrap(), &json!("/abs/path/.bin"));
192 assert!(!r.rows[2].selectable);
193 assert_eq!(r.cursor, Some(2));
194 assert_eq!(r.footer.as_deref(), Some("Up/Down Enter to select"));
195 }
196 _ => panic!("expected render frame"),
197 }
198 }
199
200 #[test]
201 fn render_row_selectable_defaults_to_true() {
202 let v = json!({ "rows": [ { "label": "x" } ] });
203 let frame = parse_inbound(METHOD_RENDER, v).unwrap();
204 match frame {
205 InboundSettingsEditorFrame::Render(r) => {
206 assert!(r.rows[0].selectable);
207 assert!(r.rows[0].marker.is_none());
208 assert!(r.rows[0].data.is_none());
209 }
210 _ => panic!(),
211 }
212 }
213
214 #[test]
215 fn commit_params_carry_arbitrary_value() {
216 let v = json!({ "value": { "path": "/x", "id": 7 } });
217 let frame = parse_inbound(METHOD_COMMIT, v).unwrap();
218 match frame {
219 InboundSettingsEditorFrame::Commit(c) => {
220 assert_eq!(c.value["path"], "/x");
221 assert_eq!(c.value["id"], 7);
222 }
223 _ => panic!(),
224 }
225 }
226
227 #[test]
228 fn close_params_optional_reason() {
229 let frame = parse_inbound(METHOD_CLOSE, json!({})).unwrap();
230 match frame {
231 InboundSettingsEditorFrame::Close(c) => assert!(c.reason.is_none()),
232 _ => panic!(),
233 }
234 let frame = parse_inbound(METHOD_CLOSE, json!({"reason":"cancelled"})).unwrap();
235 match frame {
236 InboundSettingsEditorFrame::Close(c) => assert_eq!(c.reason.as_deref(), Some("cancelled")),
237 _ => panic!(),
238 }
239 }
240
241 #[test]
242 fn unknown_method_rejected() {
243 let err = parse_inbound("settings.editor.bogus", json!({})).unwrap_err();
244 assert!(matches!(err, SettingsEditorParseError::UnknownMethod(_)));
245 }
246
247 #[test]
248 fn invalid_render_params_rejected() {
249 let err = parse_inbound(METHOD_RENDER, json!({"rows": "nope"})).unwrap_err();
251 assert!(matches!(
252 err,
253 SettingsEditorParseError::InvalidParams {
254 method: METHOD_RENDER,
255 ..
256 }
257 ));
258 }
259
260 #[test]
261 fn key_params_round_trip() {
262 let p = SettingsEditorKeyParams { key: "Down".into() };
263 let v = serde_json::to_value(&p).unwrap();
264 assert_eq!(v, json!({"key":"Down"}));
265 }
266}