use axum::http::HeaderMap;
use super::error::AdminError;
use crate::desired_state::{
ExpectedRevision, IdempotencyKey, InvalidIdempotencyKey, MutationKind, ResourceScope,
RevisionId,
};
pub const ADMIN_PREFIX: &str = "/admin/v1";
pub const IDEMPOTENCY_KEY_HEADER: &str = "idempotency-key";
pub const EXPECTED_REVISION_HEADER: &str = "x-axond-expected-revision";
pub const DRY_RUN_HEADER: &str = "x-axond-dry-run";
pub const EXPECTED_REVISION_EMPTY: &str = "empty";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteMode {
Apply,
DryRun,
}
impl WriteMode {
pub const fn as_str(self) -> &'static str {
match self {
Self::Apply => "apply",
Self::DryRun => "dry-run",
}
}
pub const fn is_dry_run(self) -> bool {
matches!(self, Self::DryRun)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuditSummary(String);
impl AuditSummary {
pub const MAX_LEN: usize = 300;
pub fn parse(input: &str) -> Result<Self, AdminError> {
let input = input.trim();
if input.is_empty() || input.len() > Self::MAX_LEN {
return Err(AdminError::AuditSummaryInvalid);
}
if !input
.bytes()
.all(|byte| byte.is_ascii_graphic() || byte == b' ')
{
return Err(AdminError::AuditSummaryInvalid);
}
Ok(Self(input.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MutationPreconditions {
pub expected: ExpectedRevision,
pub idempotency_key: IdempotencyKey,
pub mode: WriteMode,
}
impl MutationPreconditions {
pub fn from_headers(headers: &HeaderMap) -> Result<Self, AdminError> {
let idempotency_key = match text(headers, IDEMPOTENCY_KEY_HEADER) {
Header::Absent => return Err(AdminError::IdempotencyKeyRequired),
Header::Unreadable => {
return Err(AdminError::IdempotencyKeyInvalid(
InvalidIdempotencyKey::Unprintable,
));
}
Header::Text(value) => {
IdempotencyKey::parse(value).map_err(AdminError::IdempotencyKeyInvalid)?
}
};
let expected = match text(headers, EXPECTED_REVISION_HEADER) {
Header::Absent => return Err(AdminError::ExpectedRevisionRequired),
Header::Unreadable => return Err(AdminError::ExpectedRevisionInvalid),
Header::Text(value) => parse_expected_revision(value)?,
};
let mode = match text(headers, DRY_RUN_HEADER) {
Header::Absent | Header::Text("false") => WriteMode::Apply,
Header::Text("true") => WriteMode::DryRun,
Header::Unreadable | Header::Text(_) => return Err(AdminError::DryRunInvalid),
};
Ok(Self {
expected,
idempotency_key,
mode,
})
}
}
enum Header<'a> {
Absent,
Unreadable,
Text(&'a str),
}
fn text<'a>(headers: &'a HeaderMap, name: &str) -> Header<'a> {
match headers.get(name) {
None => Header::Absent,
Some(value) => value.to_str().map_or(Header::Unreadable, Header::Text),
}
}
fn parse_expected_revision(value: &str) -> Result<ExpectedRevision, AdminError> {
let value = value.trim();
if value == EXPECTED_REVISION_EMPTY {
return Ok(ExpectedRevision::Empty);
}
RevisionId::parse(value)
.map(ExpectedRevision::Exactly)
.map_err(|_| AdminError::ExpectedRevisionInvalid)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MutationRequest {
pub preconditions: MutationPreconditions,
pub kind: MutationKind,
pub scope: ResourceScope,
pub summary: AuditSummary,
}
impl MutationRequest {
pub const fn mode(&self) -> WriteMode {
self.preconditions.mode
}
}