1use crate::client::AxonFlowClient;
49use crate::error::AxonFlowError;
50use crate::types::agent::null_to_default;
54use serde::{Deserialize, Serialize};
55use serde_json::{Map, Value};
56use std::collections::BTreeMap;
57use std::fmt;
58
59pub const TYPED_POLICIES_PATH: &str = "/api/v1/typed-policies";
61
62#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
68#[non_exhaustive]
69pub struct EditionConstructReport {
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub edition: Option<String>,
73 #[serde(default, deserialize_with = "null_to_default")]
74 pub obligation_families: Vec<String>,
75 #[serde(default, deserialize_with = "null_to_default")]
76 pub attribute_namespaces: Vec<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub group_scope: Option<bool>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub separation_of_duties: Option<bool>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub tier_established: Option<bool>,
83 #[serde(default, deserialize_with = "null_to_default")]
85 pub reserved: Vec<String>,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[non_exhaustive]
92pub struct AuthoringFinding {
93 pub code: String,
95 pub severity: String,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub policy_id: Option<String>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub summary: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub detail: Option<String>,
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111#[non_exhaustive]
112pub struct TypedAuthoringDocumentRequest {
113 pub document: Value,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub fixtures: Option<Vec<Value>>,
121}
122
123impl TypedAuthoringDocumentRequest {
124 pub fn new(document: &Value, fixtures: Option<&[Value]>) -> Self {
127 Self {
128 document: document.clone(),
129 fixtures: fixtures.map(<[Value]>::to_vec),
130 }
131 }
132}
133
134#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
136#[non_exhaustive]
137pub struct TypedAuthoringEdition {
138 #[serde(default)]
139 pub success: bool,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub catalog: Option<String>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub catalog_digest: Option<String>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub registry_version: Option<i64>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub catalog_fixture: Option<bool>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub root: Option<String>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub max_documents: Option<i64>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub constructs: Option<EditionConstructReport>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub persistence: Option<String>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub signing_key_custody: Option<String>,
169}
170
171#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
174#[non_exhaustive]
175pub struct TypedPolicyValidation {
176 #[serde(default)]
177 pub success: bool,
178 #[serde(default, deserialize_with = "null_to_default")]
179 pub findings: Vec<AuthoringFinding>,
180}
181
182#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
184#[non_exhaustive]
185pub struct TypedPolicyPublication {
186 #[serde(default)]
187 pub success: bool,
188 pub digest: String,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub version: Option<i64>,
191 #[serde(default, deserialize_with = "null_to_default")]
192 pub findings: Vec<AuthoringFinding>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub template_omissions: Option<Value>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub template_omissions_unavailable: Option<String>,
200}
201
202#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
204#[non_exhaustive]
205pub struct TypedPolicyActivation {
206 #[serde(default)]
207 pub success: bool,
208 #[serde(default, deserialize_with = "null_to_default")]
209 pub activation: Map<String, Value>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub template_omissions: Option<Value>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub template_omissions_unavailable: Option<String>,
217}
218
219#[derive(Debug, Clone, PartialEq)]
224#[non_exhaustive]
225pub struct ActiveTypedPolicy {
226 pub source: Vec<u8>,
227 pub document: Value,
228}
229
230#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
232#[non_exhaustive]
233pub struct TypedPolicySystemControl {
234 pub id: String,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub name: Option<String>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub authority: Option<String>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub assurance: Option<String>,
242 #[serde(default)]
245 pub mandatory: bool,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub description: Option<String>,
248 #[serde(default, deserialize_with = "null_to_default")]
249 pub obligations: Vec<Value>,
250}
251
252#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
255#[non_exhaustive]
256pub struct TypedPolicySystemCorpus {
257 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub root: Option<String>,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub version: Option<i64>,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub digest: Option<String>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub authority: Option<String>,
266 #[serde(default, deserialize_with = "null_to_default")]
267 pub controls: Vec<TypedPolicySystemControl>,
268 #[serde(default, deserialize_with = "null_to_default")]
269 pub assurance_counts: BTreeMap<String, i64>,
270 #[serde(default, deserialize_with = "null_to_default")]
271 pub document: Map<String, Value>,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
287#[non_exhaustive]
288pub struct TypedPolicyRefusal {
289 pub status: u16,
290 pub reason: Option<String>,
291 pub code: Option<String>,
292 pub policy: Option<String>,
294 pub message: String,
295 pub findings: Vec<AuthoringFinding>,
296 pub retry_after: Option<u64>,
297}
298
299impl fmt::Display for TypedPolicyRefusal {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 match &self.reason {
302 Some(reason) => write!(
303 f,
304 "typed policy request refused (HTTP {}, {reason}): {}",
305 self.status, self.message
306 ),
307 None => write!(
308 f,
309 "typed policy request refused (HTTP {}): {}",
310 self.status, self.message
311 ),
312 }
313 }
314}
315
316fn refusal(status: u16, retry_after: Option<u64>, body: &[u8], route: &str) -> AxonFlowError {
322 let parsed: Value = serde_json::from_slice(body).unwrap_or(Value::Null);
323 let text = |member: &str| {
324 parsed
325 .get(member)
326 .and_then(Value::as_str)
327 .filter(|s| !s.is_empty())
328 .map(str::to_string)
329 };
330 if status == 401 {
331 let raw = String::from_utf8_lossy(body).trim().to_string();
335 let message = text("error")
336 .or_else(|| Some(raw).filter(|s| !s.is_empty()))
337 .unwrap_or_else(|| format!("HTTP {status} from {route}"));
338 return AxonFlowError::ApiError { status, message };
339 }
340 let message = text("error").unwrap_or_else(|| format!("HTTP {status} from {route}"));
341 let findings = parsed
342 .get("findings")
343 .and_then(Value::as_array)
344 .map(|all| {
345 all.iter()
346 .filter_map(|f| serde_json::from_value::<AuthoringFinding>(f.clone()).ok())
347 .collect()
348 })
349 .unwrap_or_default();
350 AxonFlowError::TypedPolicyRefusal(Box::new(TypedPolicyRefusal {
351 status,
352 reason: text("reason"),
353 code: text("code"),
354 policy: text("policy"),
355 message,
356 findings,
357 retry_after,
358 }))
359}
360
361fn not_an_object(status: u16, route: &str) -> AxonFlowError {
363 AxonFlowError::ApiError {
364 status,
365 message: format!(
366 "{TYPED_POLICIES_PATH}{route} answered {status} with a body that is not an object"
367 ),
368 }
369}
370
371fn retry_after(response: &reqwest::Response) -> Option<u64> {
373 response
374 .headers()
375 .get(reqwest::header::RETRY_AFTER)
376 .and_then(|v| v.to_str().ok())
377 .filter(|v| !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()))
378 .and_then(|v| v.parse().ok())
379}
380
381#[derive(Clone, Copy)]
389pub struct TypedPolicies<'a> {
390 client: &'a AxonFlowClient,
391}
392
393impl AxonFlowClient {
394 pub fn typed_policies(&self) -> TypedPolicies<'_> {
397 TypedPolicies { client: self }
398 }
399}
400
401impl TypedPolicies<'_> {
402 fn url(&self, route: &str) -> String {
403 format!("{}{TYPED_POLICIES_PATH}{route}", self.client.endpoint())
404 }
405
406 async fn object(
408 &self,
409 response: reqwest::Response,
410 route: &str,
411 ) -> Result<Value, AxonFlowError> {
412 let status = response.status().as_u16();
413 let wait = retry_after(&response);
414 let body = response.bytes().await?;
415 if !(200..300).contains(&status) {
416 return Err(refusal(status, wait, &body, route));
417 }
418 match serde_json::from_slice::<Value>(&body) {
419 Ok(value) if value.is_object() => Ok(value),
420 _ => Err(not_an_object(status, route)),
421 }
422 }
423
424 async fn get(&self, route: &str) -> Result<Value, AxonFlowError> {
425 let response = self.client.raw_get_as(&self.url(route), None).await?;
426 self.object(response, route).await
427 }
428
429 async fn post<T: Serialize>(&self, route: &str, payload: &T) -> Result<Value, AxonFlowError> {
430 let body = serde_json::to_vec(payload)?;
431 let response = self
432 .client
433 .raw_post_json_bytes(&self.url(route), body, &[])
434 .await?;
435 self.object(response, route).await
436 }
437
438 pub async fn edition(&self) -> Result<TypedAuthoringEdition, AxonFlowError> {
441 Ok(serde_json::from_value(self.get("/edition").await?)?)
442 }
443
444 pub async fn validate(
449 &self,
450 document: &Value,
451 fixtures: Option<&[Value]>,
452 ) -> Result<TypedPolicyValidation, AxonFlowError> {
453 let request = TypedAuthoringDocumentRequest::new(document, fixtures);
454 Ok(serde_json::from_value(
455 self.post("/validate", &request).await?,
456 )?)
457 }
458
459 pub async fn publish(
473 &self,
474 document: &Value,
475 fixtures: Option<&[Value]>,
476 ) -> Result<TypedPolicyPublication, AxonFlowError> {
477 let request = TypedAuthoringDocumentRequest::new(document, fixtures);
478 Ok(serde_json::from_value(
479 self.post("/publish", &request).await?,
480 )?)
481 }
482
483 pub async fn activate(
492 &self,
493 digest: &str,
494 reason: Option<&str>,
495 ) -> Result<TypedPolicyActivation, AxonFlowError> {
496 let mut payload = Map::new();
497 payload.insert("digest".into(), Value::String(digest.to_string()));
498 if let Some(reason) = reason.filter(|r| !r.is_empty()) {
499 payload.insert("reason".into(), Value::String(reason.to_string()));
500 }
501 Ok(serde_json::from_value(
502 self.post("/activate", &payload).await?,
503 )?)
504 }
505
506 pub async fn active(&self) -> Result<Option<ActiveTypedPolicy>, AxonFlowError> {
520 let route = "/active";
521 let response = self.client.raw_get_as(&self.url(route), None).await?;
522 let status = response.status().as_u16();
523 let wait = retry_after(&response);
524 let source = response.bytes().await?.to_vec();
525 if status == 404
526 && serde_json::from_slice::<Value>(&source)
527 .ok()
528 .and_then(|body| body.get("reason").cloned())
529 == Some(Value::String("nothing_active".into()))
530 {
531 return Ok(None);
532 }
533 if !(200..300).contains(&status) {
534 return Err(refusal(status, wait, &source, route));
535 }
536 match serde_json::from_slice::<Value>(&source) {
537 Ok(document) if document.is_object() => {
538 Ok(Some(ActiveTypedPolicy { source, document }))
539 }
540 _ => Err(not_an_object(status, route)),
541 }
542 }
543
544 pub async fn system(&self) -> Result<TypedPolicySystemCorpus, AxonFlowError> {
546 let body = self.get("/system").await?;
547 let system = match body.get("system") {
548 Some(system @ Value::Object(_)) => system.clone(),
549 _ => Value::Object(Map::new()),
550 };
551 Ok(serde_json::from_value(system)?)
552 }
553}