aiegis_harness/
upstream.rs1use serde::Deserialize;
19use std::time::Duration;
20
21#[derive(Debug, Clone, Deserialize)]
22pub struct UpstreamReceipt {
23 pub rid: String,
24 #[serde(default)]
25 pub receipt: String,
26 #[serde(default)]
27 pub decision: Option<String>,
28 #[serde(default)]
29 pub decision_ms: Option<u64>,
30}
31
32#[derive(Debug)]
33pub enum UpstreamError {
34 Http(String),
35 Status(u16, String),
36 Json(String),
37 NoTag,
38}
39
40impl std::fmt::Display for UpstreamError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 UpstreamError::Http(s) => write!(f, "upstream_http:{s}"),
44 UpstreamError::Status(c, b) => write!(f, "upstream_status:{c}:{b}"),
45 UpstreamError::Json(s) => write!(f, "upstream_json:{s}"),
46 UpstreamError::NoTag => write!(f, "upstream_no_aegis_tag"),
47 }
48 }
49}
50
51impl std::error::Error for UpstreamError {}
52
53#[derive(Clone)]
54pub struct UpstreamClient {
55 client: reqwest::Client,
56 url: String,
57}
58
59impl UpstreamClient {
60 pub fn new(url: String) -> Self {
61 let client = reqwest::Client::builder()
62 .user_agent(format!("aiegis-harness-rs/{}", env!("CARGO_PKG_VERSION")))
63 .timeout(Duration::from_secs(10))
64 .build()
65 .unwrap_or_else(|_| reqwest::Client::new());
66 Self { client, url }
67 }
68
69 pub async fn post(
74 &self,
75 body: &serde_json::Value,
76 aegis_tag: &str,
77 ) -> Result<UpstreamReceipt, UpstreamError> {
78 if aegis_tag.is_empty() {
79 return Err(UpstreamError::NoTag);
80 }
81 let resp = self
82 .client
83 .post(&self.url)
84 .header("X-AEGIS-Tag", aegis_tag)
85 .header("Content-Type", "application/json")
86 .json(body)
87 .send()
88 .await
89 .map_err(|e| UpstreamError::Http(format!("{e}")))?;
90 let status = resp.status();
91 let body_bytes = resp
92 .bytes()
93 .await
94 .map_err(|e| UpstreamError::Http(format!("read:{e}")))?;
95 if !status.is_success() {
96 let body_str = String::from_utf8_lossy(&body_bytes).to_string();
97 return Err(UpstreamError::Status(status.as_u16(), body_str));
98 }
99 serde_json::from_slice::<UpstreamReceipt>(&body_bytes)
100 .map_err(|e| UpstreamError::Json(format!("{e}")))
101 }
102}