use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Basis {
Authenticated,
Asserted,
Connected,
}
impl Basis {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Authenticated => "authenticated",
Self::Asserted => "asserted",
Self::Connected => "connected",
}
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|b| b.as_str() == s)
}
pub const ALL: [Self; 3] = [Self::Authenticated, Self::Asserted, Self::Connected];
#[must_use]
pub fn spellings() -> String {
Self::ALL
.iter()
.map(|b| format!("'{}'", b.as_str()))
.collect::<Vec<_>>()
.join(", ")
}
}
impl std::fmt::Display for Basis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum OperatorError {
#[error("an operator act must name who asked for it")]
Empty,
#[error("an actor name is limited to {max} characters, and this one is {len}")]
TooLong { len: usize, max: usize },
#[error("'{basis}' is not a basis this build knows: one of {known}")]
UnknownBasis { basis: String, known: String },
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "OperatorWire")]
pub struct Operator {
actor: String,
basis: Basis,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct OperatorWire {
actor: String,
basis: Basis,
}
impl TryFrom<OperatorWire> for Operator {
type Error = OperatorError;
fn try_from(w: OperatorWire) -> Result<Self, Self::Error> {
Self::new(w.actor, w.basis)
}
}
impl Operator {
pub const MAX_LEN: usize = 256;
pub fn authenticated(actor: impl Into<String>) -> Result<Self, OperatorError> {
Self::new(actor, Basis::Authenticated)
}
pub fn asserted(actor: impl Into<String>) -> Result<Self, OperatorError> {
Self::new(actor, Basis::Asserted)
}
pub fn connected(actor: impl Into<String>) -> Result<Self, OperatorError> {
Self::new(actor, Basis::Connected)
}
fn new(actor: impl Into<String>, basis: Basis) -> Result<Self, OperatorError> {
let actor = actor.into();
if actor.trim().is_empty() {
return Err(OperatorError::Empty);
}
if actor.len() > Self::MAX_LEN {
return Err(OperatorError::TooLong {
len: actor.len(),
max: Self::MAX_LEN,
});
}
Ok(Self { actor, basis })
}
pub fn from_parts(actor: impl Into<String>, basis: &str) -> Result<Self, OperatorError> {
let basis = Basis::parse(basis).ok_or_else(|| OperatorError::UnknownBasis {
basis: basis.to_owned(),
known: Basis::spellings(),
})?;
Self::new(actor, basis)
}
#[must_use]
pub fn actor(&self) -> &str {
&self.actor
}
#[must_use]
pub const fn basis(&self) -> Basis {
self.basis
}
}
impl std::fmt::Display for Operator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.actor, self.basis)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_act_attributed_to_nobody_is_refused() {
assert_eq!(Operator::authenticated(""), Err(OperatorError::Empty));
assert_eq!(Operator::asserted(" "), Err(OperatorError::Empty));
}
#[test]
fn a_deserialized_operator_takes_the_constructor_door() {
let wire = serde_json::json!({ "actor": "", "basis": "authenticated" });
assert!(
serde_json::from_value::<Operator>(wire).is_err(),
"a blank actor deserialized into a record: the derive reached the field"
);
}
#[test]
fn the_basis_survives_a_round_trip() {
let op = Operator::asserted("alice").expect("a name");
let text = serde_json::to_string(&op).expect("serializes");
assert_eq!(serde_json::from_str::<Operator>(&text).expect("parses"), op);
assert!(text.contains("asserted"), "{text}");
}
}