use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::executor::PipelineResult;
use crate::impl_plugin_payload;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ElicitationOp {
Dispatch,
Check,
Validate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ElicitationStatusKind {
Pending,
Resolved,
Expired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ElicitationOutcomeKind {
Approved,
Denied,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitationPayload {
operation: ElicitationOp,
elicitation_id: Option<String>,
kind: String,
from: String,
purpose: Option<String>,
scope: Option<String>,
timeout: Option<String>,
channel: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<ElicitationStatusKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome: Option<ElicitationOutcomeKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approver: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub valid: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
impl ElicitationPayload {
pub fn new(operation: ElicitationOp, kind: impl Into<String>, from: impl Into<String>) -> Self {
Self {
operation,
elicitation_id: None,
kind: kind.into(),
from: from.into(),
purpose: None,
scope: None,
timeout: None,
channel: None,
id: None,
status: None,
outcome: None,
approver: None,
intent_id: None,
expires_at: None,
valid: None,
reason: None,
metadata: HashMap::new(),
}
}
pub fn with_elicitation_id(mut self, id: impl Into<String>) -> Self {
self.elicitation_id = Some(id.into());
self
}
pub fn with_purpose(mut self, purpose: impl Into<String>) -> Self {
self.purpose = Some(purpose.into());
self
}
pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
pub fn with_timeout(mut self, timeout: impl Into<String>) -> Self {
self.timeout = Some(timeout.into());
self
}
pub fn with_channel(mut self, channel: impl Into<String>) -> Self {
self.channel = Some(channel.into());
self
}
pub fn operation(&self) -> ElicitationOp {
self.operation
}
pub fn elicitation_id(&self) -> Option<&str> {
self.elicitation_id.as_deref()
}
pub fn kind(&self) -> &str {
&self.kind
}
pub fn from(&self) -> &str {
&self.from
}
pub fn purpose(&self) -> Option<&str> {
self.purpose.as_deref()
}
pub fn scope(&self) -> Option<&str> {
self.scope.as_deref()
}
pub fn timeout(&self) -> Option<&str> {
self.timeout.as_deref()
}
pub fn channel(&self) -> Option<&str> {
self.channel.as_deref()
}
pub fn from_pipeline_result(result: &PipelineResult) -> Option<Self> {
result
.modified_payload
.as_ref()
.and_then(|p| p.as_any().downcast_ref::<ElicitationPayload>())
.cloned()
}
}
impl_plugin_payload!(ElicitationPayload);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_sets_input_and_leaves_output_empty() {
let p = ElicitationPayload::new(ElicitationOp::Dispatch, "approval", "alice@example.com")
.with_purpose("Approve $25,000 raise")
.with_scope("args.amount <= 25000")
.with_timeout("24h")
.with_channel("ciba");
assert_eq!(p.operation(), ElicitationOp::Dispatch);
assert_eq!(p.kind(), "approval");
assert_eq!(p.from(), "alice@example.com");
assert_eq!(p.purpose(), Some("Approve $25,000 raise"));
assert_eq!(p.scope(), Some("args.amount <= 25000"));
assert_eq!(p.timeout(), Some("24h"));
assert_eq!(p.channel(), Some("ciba"));
assert!(p.elicitation_id().is_none());
assert!(p.id.is_none());
assert!(p.status.is_none());
assert!(p.valid.is_none());
}
#[test]
fn with_elicitation_id_sets_correlation() {
let p = ElicitationPayload::new(ElicitationOp::Check, "approval", "alice@example.com")
.with_elicitation_id("elic-123");
assert_eq!(p.elicitation_id(), Some("elic-123"));
assert_eq!(p.operation(), ElicitationOp::Check);
}
#[test]
fn payload_roundtrips_through_serde() {
let mut p =
ElicitationPayload::new(ElicitationOp::Dispatch, "approval", "alice@example.com");
p.id = Some("elic-1".into());
p.status = Some(ElicitationStatusKind::Pending);
p.intent_id = Some("intent-9".into());
let json = serde_json::to_string(&p).unwrap();
let back: ElicitationPayload = serde_json::from_str(&json).unwrap();
assert_eq!(back.id.as_deref(), Some("elic-1"));
assert_eq!(back.status, Some(ElicitationStatusKind::Pending));
assert_eq!(back.intent_id.as_deref(), Some("intent-9"));
}
}