use crate::client::AxonFlowClient;
use crate::error::AxonFlowError;
use crate::types::agent::null_to_default;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::BTreeMap;
use std::fmt;
pub const TYPED_POLICIES_PATH: &str = "/api/v1/typed-policies";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EditionConstructReport {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(default, deserialize_with = "null_to_default")]
pub obligation_families: Vec<String>,
#[serde(default, deserialize_with = "null_to_default")]
pub attribute_namespaces: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group_scope: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub separation_of_duties: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier_established: Option<bool>,
#[serde(default, deserialize_with = "null_to_default")]
pub reserved: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AuthoringFinding {
pub code: String,
pub severity: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TypedAuthoringDocumentRequest {
pub document: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fixtures: Option<Vec<Value>>,
}
impl TypedAuthoringDocumentRequest {
pub fn new(document: &Value, fixtures: Option<&[Value]>) -> Self {
Self {
document: document.clone(),
fixtures: fixtures.map(<[Value]>::to_vec),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TypedAuthoringEdition {
#[serde(default)]
pub success: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry_version: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog_fixture: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_documents: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub constructs: Option<EditionConstructReport>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub persistence: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signing_key_custody: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TypedPolicyValidation {
#[serde(default)]
pub success: bool,
#[serde(default, deserialize_with = "null_to_default")]
pub findings: Vec<AuthoringFinding>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TypedPolicyPublication {
#[serde(default)]
pub success: bool,
pub digest: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<i64>,
#[serde(default, deserialize_with = "null_to_default")]
pub findings: Vec<AuthoringFinding>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub template_omissions: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub template_omissions_unavailable: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TypedPolicyActivation {
#[serde(default)]
pub success: bool,
#[serde(default, deserialize_with = "null_to_default")]
pub activation: Map<String, Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub template_omissions: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub template_omissions_unavailable: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ActiveTypedPolicy {
pub source: Vec<u8>,
pub document: Value,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TypedPolicySystemControl {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authority: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assurance: Option<String>,
#[serde(default)]
pub mandatory: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, deserialize_with = "null_to_default")]
pub obligations: Vec<Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TypedPolicySystemCorpus {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authority: Option<String>,
#[serde(default, deserialize_with = "null_to_default")]
pub controls: Vec<TypedPolicySystemControl>,
#[serde(default, deserialize_with = "null_to_default")]
pub assurance_counts: BTreeMap<String, i64>,
#[serde(default, deserialize_with = "null_to_default")]
pub document: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct TypedPolicyRefusal {
pub status: u16,
pub reason: Option<String>,
pub code: Option<String>,
pub policy: Option<String>,
pub message: String,
pub findings: Vec<AuthoringFinding>,
pub retry_after: Option<u64>,
}
impl fmt::Display for TypedPolicyRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.reason {
Some(reason) => write!(
f,
"typed policy request refused (HTTP {}, {reason}): {}",
self.status, self.message
),
None => write!(
f,
"typed policy request refused (HTTP {}): {}",
self.status, self.message
),
}
}
}
fn refusal(status: u16, retry_after: Option<u64>, body: &[u8], route: &str) -> AxonFlowError {
let parsed: Value = serde_json::from_slice(body).unwrap_or(Value::Null);
let text = |member: &str| {
parsed
.get(member)
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_string)
};
if status == 401 {
let raw = String::from_utf8_lossy(body).trim().to_string();
let message = text("error")
.or_else(|| Some(raw).filter(|s| !s.is_empty()))
.unwrap_or_else(|| format!("HTTP {status} from {route}"));
return AxonFlowError::ApiError { status, message };
}
let message = text("error").unwrap_or_else(|| format!("HTTP {status} from {route}"));
let findings = parsed
.get("findings")
.and_then(Value::as_array)
.map(|all| {
all.iter()
.filter_map(|f| serde_json::from_value::<AuthoringFinding>(f.clone()).ok())
.collect()
})
.unwrap_or_default();
AxonFlowError::TypedPolicyRefusal(Box::new(TypedPolicyRefusal {
status,
reason: text("reason"),
code: text("code"),
policy: text("policy"),
message,
findings,
retry_after,
}))
}
fn not_an_object(status: u16, route: &str) -> AxonFlowError {
AxonFlowError::ApiError {
status,
message: format!(
"{TYPED_POLICIES_PATH}{route} answered {status} with a body that is not an object"
),
}
}
fn retry_after(response: &reqwest::Response) -> Option<u64> {
response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok())
.filter(|v| !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()))
.and_then(|v| v.parse().ok())
}
#[derive(Clone, Copy)]
pub struct TypedPolicies<'a> {
client: &'a AxonFlowClient,
}
impl AxonFlowClient {
pub fn typed_policies(&self) -> TypedPolicies<'_> {
TypedPolicies { client: self }
}
}
impl TypedPolicies<'_> {
fn url(&self, route: &str) -> String {
format!("{}{TYPED_POLICIES_PATH}{route}", self.client.endpoint())
}
async fn object(
&self,
response: reqwest::Response,
route: &str,
) -> Result<Value, AxonFlowError> {
let status = response.status().as_u16();
let wait = retry_after(&response);
let body = response.bytes().await?;
if !(200..300).contains(&status) {
return Err(refusal(status, wait, &body, route));
}
match serde_json::from_slice::<Value>(&body) {
Ok(value) if value.is_object() => Ok(value),
_ => Err(not_an_object(status, route)),
}
}
async fn get(&self, route: &str) -> Result<Value, AxonFlowError> {
let response = self.client.raw_get_as(&self.url(route), None).await?;
self.object(response, route).await
}
async fn post<T: Serialize>(&self, route: &str, payload: &T) -> Result<Value, AxonFlowError> {
let body = serde_json::to_vec(payload)?;
let response = self
.client
.raw_post_json_bytes(&self.url(route), body, &[])
.await?;
self.object(response, route).await
}
pub async fn edition(&self) -> Result<TypedAuthoringEdition, AxonFlowError> {
Ok(serde_json::from_value(self.get("/edition").await?)?)
}
pub async fn validate(
&self,
document: &Value,
fixtures: Option<&[Value]>,
) -> Result<TypedPolicyValidation, AxonFlowError> {
let request = TypedAuthoringDocumentRequest::new(document, fixtures);
Ok(serde_json::from_value(
self.post("/validate", &request).await?,
)?)
}
pub async fn publish(
&self,
document: &Value,
fixtures: Option<&[Value]>,
) -> Result<TypedPolicyPublication, AxonFlowError> {
let request = TypedAuthoringDocumentRequest::new(document, fixtures);
Ok(serde_json::from_value(
self.post("/publish", &request).await?,
)?)
}
pub async fn activate(
&self,
digest: &str,
reason: Option<&str>,
) -> Result<TypedPolicyActivation, AxonFlowError> {
let mut payload = Map::new();
payload.insert("digest".into(), Value::String(digest.to_string()));
if let Some(reason) = reason.filter(|r| !r.is_empty()) {
payload.insert("reason".into(), Value::String(reason.to_string()));
}
Ok(serde_json::from_value(
self.post("/activate", &payload).await?,
)?)
}
pub async fn active(&self) -> Result<Option<ActiveTypedPolicy>, AxonFlowError> {
let route = "/active";
let response = self.client.raw_get_as(&self.url(route), None).await?;
let status = response.status().as_u16();
let wait = retry_after(&response);
let source = response.bytes().await?.to_vec();
if status == 404
&& serde_json::from_slice::<Value>(&source)
.ok()
.and_then(|body| body.get("reason").cloned())
== Some(Value::String("nothing_active".into()))
{
return Ok(None);
}
if !(200..300).contains(&status) {
return Err(refusal(status, wait, &source, route));
}
match serde_json::from_slice::<Value>(&source) {
Ok(document) if document.is_object() => {
Ok(Some(ActiveTypedPolicy { source, document }))
}
_ => Err(not_an_object(status, route)),
}
}
pub async fn system(&self) -> Result<TypedPolicySystemCorpus, AxonFlowError> {
let body = self.get("/system").await?;
let system = match body.get("system") {
Some(system @ Value::Object(_)) => system.clone(),
_ => Value::Object(Map::new()),
};
Ok(serde_json::from_value(system)?)
}
}