use std::fmt;
use std::ops::Deref;
use serde::{Deserialize, Serialize};
use super::{
Abstention, CouncilMember, CouncilSeat, CouncilVote, DecisionCategory,
FinalVotes,
};
use crate::enums::DecisionOutcome;
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct Statement(String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("words for the record may not be blank")]
pub struct BlankStatement;
impl TryFrom<String> for Statement {
type Error = BlankStatement;
fn try_from(text: String) -> Result<Self, Self::Error> {
if text.trim().is_empty() {
Err(BlankStatement)
} else {
Ok(Self(text))
}
}
}
impl TryFrom<&str> for Statement {
type Error = BlankStatement;
fn try_from(text: &str) -> Result<Self, Self::Error> {
text.to_string().try_into()
}
}
impl From<Statement> for String {
fn from(statement: Statement) -> Self {
statement.0
}
}
impl Deref for Statement {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for Statement {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for Statement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Debug for Statement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for Statement {
fn inline_schema() -> bool {
true
}
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("Statement")
}
fn json_schema(
generator: &mut schemars::SchemaGenerator,
) -> schemars::Schema {
String::json_schema(generator)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "vote", rename_all = "snake_case")]
pub enum StewardVote {
Yes,
No,
Concur,
Abstain {
rationale: Statement,
},
Veto {
rationale: Statement,
},
Recused,
}
impl From<&StewardVote> for CouncilVote {
fn from(vote: &StewardVote) -> Self {
match vote {
StewardVote::Yes => Self::Yes,
StewardVote::No => Self::No,
StewardVote::Concur => Self::Concur,
StewardVote::Abstain { .. } => Self::Abstain,
StewardVote::Veto { .. } => Self::Veto,
StewardVote::Recused => Self::Recused,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SeatChoice {
Yes,
No,
Abstain,
Defer,
}
impl From<SeatChoice> for CouncilVote {
fn from(choice: SeatChoice) -> Self {
match choice {
SeatChoice::Yes => Self::Yes,
SeatChoice::No => Self::No,
SeatChoice::Abstain => Self::Abstain,
SeatChoice::Defer => Self::Defer,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("`{0:?}` is not a seat's vote on a voted item")]
pub struct NotASeatChoice(pub CouncilVote);
impl TryFrom<CouncilVote> for SeatChoice {
type Error = NotASeatChoice;
fn try_from(vote: CouncilVote) -> Result<Self, Self::Error> {
match vote {
CouncilVote::Yes => Ok(Self::Yes),
CouncilVote::No => Ok(Self::No),
CouncilVote::Abstain => Ok(Self::Abstain),
CouncilVote::Defer => Ok(Self::Defer),
other @ (CouncilVote::Concur
| CouncilVote::Veto
| CouncilVote::Ranked
| CouncilVote::Recused) => Err(NotASeatChoice(other)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeatVote {
pub choice: SeatChoice,
pub statement: Statement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Vote<'a> {
Seat(&'a SeatVote),
Steward(&'a StewardVote),
}
impl Vote<'_> {
pub fn cast(&self) -> CouncilVote {
match self {
Self::Seat(v) => v.choice.into(),
Self::Steward(v) => (*v).into(),
}
}
pub fn abstention(&self) -> Option<&Statement> {
match self {
Self::Seat(SeatVote {
choice: SeatChoice::Abstain,
statement,
}) => Some(statement),
Self::Steward(StewardVote::Abstain { rationale }) => {
Some(rationale)
}
Self::Seat(_) | Self::Steward(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeatVotes {
pub artist: SeatVote,
pub philosopher: SeatVote,
pub lawyer: SeatVote,
pub engineer: SeatVote,
}
impl SeatVotes {
pub fn seat(&self, seat: CouncilSeat) -> &SeatVote {
match seat {
CouncilSeat::Artist => &self.artist,
CouncilSeat::Philosopher => &self.philosopher,
CouncilSeat::Lawyer => &self.lawyer,
CouncilSeat::Engineer => &self.engineer,
}
}
pub fn iter(&self) -> impl Iterator<Item = (CouncilSeat, &SeatVote)> {
CouncilSeat::ALL
.into_iter()
.map(|seat| (seat, self.seat(seat)))
}
pub fn yes_count(&self) -> usize {
self.iter()
.filter(|(_, v)| v.choice == SeatChoice::Yes)
.count()
}
pub fn no_count(&self) -> usize {
self.iter()
.filter(|(_, v)| v.choice == SeatChoice::No)
.count()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Votes {
#[serde(flatten)]
pub seats: SeatVotes,
pub steward: StewardVote,
}
impl Votes {
pub fn seat(&self, seat: CouncilSeat) -> &SeatVote {
self.seats.seat(seat)
}
pub fn iter(&self) -> impl Iterator<Item = (CouncilMember, Vote<'_>)> {
self.seats
.iter()
.map(|(seat, vote)| (seat.into(), Vote::Seat(vote)))
.chain([(CouncilMember::Steward, Vote::Steward(&self.steward))])
}
pub fn steward_abstention(&self) -> Option<Abstention> {
match &self.steward {
StewardVote::Abstain { rationale } => Some(Abstention {
member: CouncilMember::Steward,
rationale: rationale.to_string().into(),
}),
_ => None,
}
}
pub fn veto_rationale(&self) -> Option<&Statement> {
match &self.steward {
StewardVote::Veto { rationale } => Some(rationale),
_ => None,
}
}
pub fn outcome(&self, category: DecisionCategory) -> DecisionOutcome {
FinalVotes::from(self).outcome(category)
}
pub fn tally(&self) -> String {
FinalVotes::from(self).tally()
}
}
impl From<&Votes> for FinalVotes {
fn from(votes: &Votes) -> Self {
Self {
artist: votes.seats.artist.choice.into(),
philosopher: votes.seats.philosopher.choice.into(),
lawyer: votes.seats.lawyer.choice.into(),
engineer: votes.seats.engineer.choice.into(),
steward: (&votes.steward).into(),
}
}
}
impl FinalVotes {
fn all(&self) -> [CouncilVote; 5] {
[
self.artist,
self.philosopher,
self.lawyer,
self.engineer,
self.steward,
]
}
fn seats(&self) -> [CouncilVote; 4] {
[self.artist, self.philosopher, self.lawyer, self.engineer]
}
pub fn yes_count(&self) -> usize {
self.all()
.iter()
.filter(|v| matches!(v, CouncilVote::Yes | CouncilVote::Concur))
.count()
}
pub fn no_count(&self) -> usize {
self.all()
.iter()
.filter(|v| matches!(v, CouncilVote::No))
.count()
}
pub fn steward_recused(&self) -> bool {
self.steward == CouncilVote::Recused
}
pub fn voting_count(&self) -> usize {
self.all()
.iter()
.filter(|v| {
!matches!(v, CouncilVote::Abstain | CouncilVote::Recused)
})
.count()
}
pub fn tally(&self) -> String {
if self.steward_recused() {
format!(
"{}-{} (Steward recused)",
self.yes_count(),
self.no_count()
)
} else {
format!("{}-{}", self.yes_count(), self.no_count())
}
}
pub fn outcome(&self, category: DecisionCategory) -> DecisionOutcome {
let recused = self.steward_recused();
let passes = |pass: bool| match pass {
true => DecisionOutcome::Approved,
false => DecisionOutcome::Rejected,
};
if self.steward == CouncilVote::Veto {
return DecisionOutcome::Rejected;
}
match category {
DecisionCategory::Routine => passes(self.yes_count() >= 3),
DecisionCategory::Policy if recused => {
passes(self.seats().iter().all(|v| *v == CouncilVote::Yes))
}
DecisionCategory::Policy => passes(
self.yes_count() >= 4
&& matches!(
self.steward,
CouncilVote::Yes | CouncilVote::Concur
),
),
DecisionCategory::Constitutional => {
let voting = self.voting_count();
passes(voting >= 4 && self.yes_count() == voting)
}
DecisionCategory::Emergency => passes(matches!(
self.steward,
CouncilVote::Yes | CouncilVote::Concur
)),
DecisionCategory::Schedule => DecisionOutcome::Deferred,
}
}
}
#[cfg(test)]
mod tests;