Some checks failed
CI / Check (macos-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Clippy (strict) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
124 lines
3.7 KiB
Rust
124 lines
3.7 KiB
Rust
//! Structured output for selected items. Wraps the original
|
|
//! item value with action context and original index so
|
|
//! hooks and downstream tools know what happened.
|
|
|
|
use serde::ser::SerializeMap;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
|
|
/// What the user did to produce this output.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum OutputAction {
|
|
Select,
|
|
Quicklist,
|
|
Cancel,
|
|
}
|
|
|
|
/// A selected item wrapped with output context. For object
|
|
/// values, the original fields are merged at the top level
|
|
/// alongside `action` and `index`. For non-object values
|
|
/// (plain strings, numbers), the value appears as a `value`
|
|
/// field.
|
|
#[derive(Debug, Clone)]
|
|
pub struct OutputItem {
|
|
pub value: Value,
|
|
pub action: OutputAction,
|
|
pub index: usize,
|
|
}
|
|
|
|
impl Serialize for OutputItem {
|
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
match &self.value {
|
|
Value::Object(map) => {
|
|
// Flatten: merge object fields with action/index
|
|
let mut s = serializer.serialize_map(Some(map.len() + 2))?;
|
|
for (k, v) in map {
|
|
s.serialize_entry(k, v)?;
|
|
}
|
|
s.serialize_entry("action", &self.action)?;
|
|
s.serialize_entry("index", &self.index)?;
|
|
s.end()
|
|
}
|
|
_ => {
|
|
// Non-object: put value in a "value" field
|
|
let mut s = serializer.serialize_map(Some(3))?;
|
|
s.serialize_entry("value", &self.value)?;
|
|
s.serialize_entry("action", &self.action)?;
|
|
s.serialize_entry("index", &self.index)?;
|
|
s.end()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn output_item_flattens_object() {
|
|
let item = OutputItem {
|
|
value: json!({"label": "Firefox", "url": "https://firefox.com"}),
|
|
action: OutputAction::Select,
|
|
index: 3,
|
|
};
|
|
let json = serde_json::to_value(&item).unwrap_or_default();
|
|
assert_eq!(json["label"], "Firefox");
|
|
assert_eq!(json["url"], "https://firefox.com");
|
|
assert_eq!(json["action"], "select");
|
|
assert_eq!(json["index"], 3);
|
|
}
|
|
|
|
#[test]
|
|
fn output_item_string_value() {
|
|
let item = OutputItem {
|
|
value: json!("hello"),
|
|
action: OutputAction::Select,
|
|
index: 0,
|
|
};
|
|
let json = serde_json::to_value(&item).unwrap_or_default();
|
|
assert_eq!(json["value"], "hello");
|
|
assert_eq!(json["action"], "select");
|
|
assert_eq!(json["index"], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn output_item_cancel_action() {
|
|
let item = OutputItem {
|
|
value: json!(null),
|
|
action: OutputAction::Cancel,
|
|
index: 0,
|
|
};
|
|
let json = serde_json::to_value(&item).unwrap_or_default();
|
|
assert_eq!(json["action"], "cancel");
|
|
}
|
|
|
|
#[test]
|
|
fn output_item_quicklist_action() {
|
|
let item = OutputItem {
|
|
value: json!("test"),
|
|
action: OutputAction::Quicklist,
|
|
index: 2,
|
|
};
|
|
let json = serde_json::to_value(&item).unwrap_or_default();
|
|
assert_eq!(json["action"], "quicklist");
|
|
assert_eq!(json["index"], 2);
|
|
}
|
|
|
|
#[test]
|
|
fn output_item_string_contains_value_text() {
|
|
let item = OutputItem {
|
|
value: json!("alpha"),
|
|
action: OutputAction::Select,
|
|
index: 0,
|
|
};
|
|
let serialized = serde_json::to_string(&item).unwrap_or_default();
|
|
assert!(
|
|
serialized.contains("alpha"),
|
|
"output should contain the value text: {serialized}"
|
|
);
|
|
}
|
|
}
|