use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FlowAction {
Propose,
Validate,
Promote,
Commit,
AdvancePhase,
}
impl FlowAction {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Propose => "propose",
Self::Validate => "validate",
Self::Promote => "promote",
Self::Commit => "commit",
Self::AdvancePhase => "advance_phase",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowGatePrincipal {
pub id: String,
pub authority: String,
pub domains: Vec<String>,
pub policy_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowGateResource {
pub id: String,
pub kind: String,
pub phase: String,
pub gates_passed: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FlowGateContext {
pub commitment_type: Option<String>,
pub amount: Option<i64>,
pub human_approval_present: Option<bool>,
pub required_gates_met: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowGateInput {
pub principal: FlowGatePrincipal,
pub resource: FlowGateResource,
pub action: FlowAction,
pub context: FlowGateContext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FlowGateOutcome {
Promote,
Reject,
Escalate,
}
impl FlowGateOutcome {
#[must_use]
pub const fn is_allowed(self) -> bool {
matches!(self, Self::Promote)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowGateDecision {
pub outcome: FlowGateOutcome,
pub reason: Option<String>,
pub source: Option<String>,
}
impl FlowGateDecision {
#[must_use]
pub fn promote(reason: Option<String>, source: Option<String>) -> Self {
Self {
outcome: FlowGateOutcome::Promote,
reason,
source,
}
}
#[must_use]
pub fn reject(reason: Option<String>, source: Option<String>) -> Self {
Self {
outcome: FlowGateOutcome::Reject,
reason,
source,
}
}
#[must_use]
pub fn escalate(reason: Option<String>, source: Option<String>) -> Self {
Self {
outcome: FlowGateOutcome::Escalate,
reason,
source,
}
}
}
#[derive(Debug, Error)]
pub enum FlowGateError {
#[error("authorizer failed: {0}")]
Authorizer(String),
#[error("invalid flow gate input: {0}")]
InvalidInput(String),
}
pub trait FlowGateAuthorizer: Send + Sync {
fn decide(&self, input: &FlowGateInput) -> Result<FlowGateDecision, FlowGateError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AllowAllFlowGateAuthorizer;
impl FlowGateAuthorizer for AllowAllFlowGateAuthorizer {
fn decide(&self, _input: &FlowGateInput) -> Result<FlowGateDecision, FlowGateError> {
Ok(FlowGateDecision::promote(
Some("allow_all test authorizer".into()),
Some("allow_all".into()),
))
}
}
#[derive(Debug, Default, Clone)]
pub struct RejectAllFlowGateAuthorizer {
reason: Option<String>,
}
impl RejectAllFlowGateAuthorizer {
#[must_use]
pub fn with_reason(reason: impl Into<String>) -> Self {
Self {
reason: Some(reason.into()),
}
}
}
impl FlowGateAuthorizer for RejectAllFlowGateAuthorizer {
fn decide(&self, _input: &FlowGateInput) -> Result<FlowGateDecision, FlowGateError> {
Ok(FlowGateDecision::reject(
self.reason
.clone()
.or_else(|| Some("reject_all test authorizer".into())),
Some("reject_all".into()),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_input() -> FlowGateInput {
FlowGateInput {
principal: FlowGatePrincipal {
id: "agent:test".into(),
authority: "supervisory".into(),
domains: vec!["finance".into()],
policy_version: Some("v1".into()),
},
resource: FlowGateResource {
id: "expense:1".into(),
kind: "expense".into(),
phase: "commitment".into(),
gates_passed: vec!["receipt".into()],
},
action: FlowAction::Validate,
context: FlowGateContext {
commitment_type: Some("expense".into()),
amount: Some(100),
human_approval_present: Some(false),
required_gates_met: Some(true),
},
}
}
#[test]
fn allow_all_authorizer_promotes() {
let decision = AllowAllFlowGateAuthorizer
.decide(&sample_input())
.expect("allow_all should succeed");
assert_eq!(decision.outcome, FlowGateOutcome::Promote);
}
#[test]
fn reject_all_authorizer_rejects() {
let decision = RejectAllFlowGateAuthorizer::with_reason("blocked")
.decide(&sample_input())
.expect("reject_all should succeed");
assert_eq!(decision.outcome, FlowGateOutcome::Reject);
assert_eq!(decision.reason.as_deref(), Some("blocked"));
}
}