use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const METHOD_OPEN: &str = "settings.editor.open";
pub const METHOD_RENDER: &str = "settings.editor.render";
pub const METHOD_KEY: &str = "settings.editor.key";
pub const METHOD_COMMIT: &str = "settings.editor.commit";
pub const METHOD_CLOSE: &str = "settings.editor.close";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsEditorOpenParams {
pub category: String,
pub field: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettingsEditorKeyParams {
pub key: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SettingsEditorCommitParams {
pub value: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SettingsEditorCloseParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SettingsEditorRenderParams {
pub rows: Vec<SettingsEditorRow>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub footer: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SettingsEditorRow {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub marker: Option<String>,
#[serde(default = "default_true")]
pub selectable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, thiserror::Error)]
pub enum SettingsEditorParseError {
#[error("unknown settings.editor method: {0}")]
UnknownMethod(String),
#[error("invalid params for {method}: {source}")]
InvalidParams {
method: &'static str,
#[source]
source: serde_json::Error,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum InboundSettingsEditorFrame {
Render(SettingsEditorRenderParams),
Commit(SettingsEditorCommitParams),
Close(SettingsEditorCloseParams),
}
pub fn parse_inbound(
method: &str,
params: Value,
) -> Result<InboundSettingsEditorFrame, SettingsEditorParseError> {
match method {
METHOD_RENDER => serde_json::from_value(params)
.map(InboundSettingsEditorFrame::Render)
.map_err(|source| SettingsEditorParseError::InvalidParams {
method: METHOD_RENDER,
source,
}),
METHOD_COMMIT => serde_json::from_value(params)
.map(InboundSettingsEditorFrame::Commit)
.map_err(|source| SettingsEditorParseError::InvalidParams {
method: METHOD_COMMIT,
source,
}),
METHOD_CLOSE => serde_json::from_value(params)
.map(InboundSettingsEditorFrame::Close)
.map_err(|source| SettingsEditorParseError::InvalidParams {
method: METHOD_CLOSE,
source,
}),
other => Err(SettingsEditorParseError::UnknownMethod(other.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn open_params_round_trip() {
let p = SettingsEditorOpenParams {
category: "capture".to_string(),
field: "model_path".to_string(),
};
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v, json!({"category":"capture","field":"model_path"}));
let back: SettingsEditorOpenParams = serde_json::from_value(v).unwrap();
assert_eq!(back, p);
}
#[test]
fn render_params_parse_full_example() {
let v = json!({
"rows": [
{ "label": "tiny.en (75 MB)", "marker": "✓", "selectable": true, "data": "/abs/path/.bin" },
{ "label": "base (142 MB)", "marker": " ", "selectable": true, "data": "download:base" },
{ "label": "(separator)", "selectable": false }
],
"cursor": 2,
"footer": "Up/Down Enter to select"
});
let frame = parse_inbound(METHOD_RENDER, v).unwrap();
match frame {
InboundSettingsEditorFrame::Render(r) => {
assert_eq!(r.rows.len(), 3);
assert_eq!(r.rows[0].marker.as_deref(), Some("✓"));
assert!(r.rows[0].selectable);
assert_eq!(r.rows[0].data.as_ref().unwrap(), &json!("/abs/path/.bin"));
assert!(!r.rows[2].selectable);
assert_eq!(r.cursor, Some(2));
assert_eq!(r.footer.as_deref(), Some("Up/Down Enter to select"));
}
_ => panic!("expected render frame"),
}
}
#[test]
fn render_row_selectable_defaults_to_true() {
let v = json!({ "rows": [ { "label": "x" } ] });
let frame = parse_inbound(METHOD_RENDER, v).unwrap();
match frame {
InboundSettingsEditorFrame::Render(r) => {
assert!(r.rows[0].selectable);
assert!(r.rows[0].marker.is_none());
assert!(r.rows[0].data.is_none());
}
_ => panic!(),
}
}
#[test]
fn commit_params_carry_arbitrary_value() {
let v = json!({ "value": { "path": "/x", "id": 7 } });
let frame = parse_inbound(METHOD_COMMIT, v).unwrap();
match frame {
InboundSettingsEditorFrame::Commit(c) => {
assert_eq!(c.value["path"], "/x");
assert_eq!(c.value["id"], 7);
}
_ => panic!(),
}
}
#[test]
fn close_params_optional_reason() {
let frame = parse_inbound(METHOD_CLOSE, json!({})).unwrap();
match frame {
InboundSettingsEditorFrame::Close(c) => assert!(c.reason.is_none()),
_ => panic!(),
}
let frame = parse_inbound(METHOD_CLOSE, json!({"reason":"cancelled"})).unwrap();
match frame {
InboundSettingsEditorFrame::Close(c) => assert_eq!(c.reason.as_deref(), Some("cancelled")),
_ => panic!(),
}
}
#[test]
fn unknown_method_rejected() {
let err = parse_inbound("settings.editor.bogus", json!({})).unwrap_err();
assert!(matches!(err, SettingsEditorParseError::UnknownMethod(_)));
}
#[test]
fn invalid_render_params_rejected() {
let err = parse_inbound(METHOD_RENDER, json!({"rows": "nope"})).unwrap_err();
assert!(matches!(
err,
SettingsEditorParseError::InvalidParams {
method: METHOD_RENDER,
..
}
));
}
#[test]
fn key_params_round_trip() {
let p = SettingsEditorKeyParams { key: "Down".into() };
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v, json!({"key":"Down"}));
}
}