use serde::{Deserialize, Serialize};
use crate::canonical::{versioned_preimage, CanonicalError};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Idempotency {
Safe,
NotGuaranteed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AutomaticRetries {
Allowed,
Forbidden,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OperationMetadata {
pub mutates_state: bool,
pub idempotency: Idempotency,
pub automatic_retries: AutomaticRetries,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Operation {
pub name: &'static str,
pub schema_pointer: &'static str,
pub mutates_state: bool,
}
impl Operation {
pub const fn metadata(&self) -> OperationMetadata {
let idempotency = if self.mutates_state {
Idempotency::NotGuaranteed
} else {
Idempotency::Safe
};
OperationMetadata {
mutates_state: self.mutates_state,
idempotency,
automatic_retries: match idempotency {
Idempotency::Safe => AutomaticRetries::Allowed,
Idempotency::NotGuaranteed => AutomaticRetries::Forbidden,
},
}
}
}
pub const OPERATIONS: &[Operation] = &[
Operation {
name: "decide",
schema_pointer: "",
mutates_state: false,
},
Operation {
name: "rebalance_to_target",
schema_pointer: "/$defs/Order",
mutates_state: true,
},
];
pub fn operation_contract_preimage() -> Result<Vec<u8>, CanonicalError> {
let table: serde_json::Map<String, serde_json::Value> = OPERATIONS
.iter()
.map(|operation| {
(
operation.name.to_string(),
serde_json::to_value(operation.metadata()).expect("metadata serializes"),
)
})
.collect();
versioned_preimage(&serde_json::Value::Object(table))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_triple_round_trips_under_its_published_spelling() {
for operation in OPERATIONS {
let metadata = operation.metadata();
let json = serde_json::to_string(&metadata).unwrap();
assert_eq!(
serde_json::from_str::<OperationMetadata>(&json).unwrap(),
metadata
);
}
assert_eq!(
serde_json::to_string(&OPERATIONS[0].metadata()).unwrap(),
r#"{"mutates_state":false,"idempotency":"safe","automatic_retries":"allowed"}"#
);
assert_eq!(
serde_json::to_string(&OPERATIONS[1].metadata()).unwrap(),
r#"{"mutates_state":true,"idempotency":"not_guaranteed","automatic_retries":"forbidden"}"#
);
assert!(serde_json::from_str::<OperationMetadata>(
r#"{"mutates_state":true,"idempotency":"maybe","automatic_retries":"allowed"}"#
)
.is_err());
}
#[test]
fn only_a_safe_operation_may_be_retried_automatically() {
for operation in OPERATIONS {
let metadata = operation.metadata();
assert_eq!(
metadata.automatic_retries == AutomaticRetries::Allowed,
metadata.idempotency == Idempotency::Safe,
"{}",
operation.name
);
assert_eq!(
metadata.idempotency == Idempotency::Safe,
!metadata.mutates_state,
"{}",
operation.name
);
}
}
#[test]
fn operation_names_are_unique_and_the_preimage_is_stable() {
let names: std::collections::BTreeSet<&str> =
OPERATIONS.iter().map(|operation| operation.name).collect();
assert_eq!(names.len(), OPERATIONS.len());
assert_eq!(
operation_contract_preimage().unwrap(),
operation_contract_preimage().unwrap()
);
}
#[test]
fn the_preimage_carries_every_declared_operation() {
let preimage = operation_contract_preimage().unwrap();
let text = String::from_utf8_lossy(&preimage);
for operation in OPERATIONS {
assert!(
text.contains(&format!("\"{}\"", operation.name)),
"{} missing from {text}",
operation.name
);
}
}
}