//! 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(&self, serializer: S) -> Result { 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}" ); } }