use super::{
AgendaRanking, CouncilDecisionRecord, CouncilRound, CouncilSeat,
CouncilVote, DecisionCategory, FinalVotes, Redactable, SeatChoice,
SeatVote, SeatVotes, Statement, Votes,
};
use crate::enums::{DecisionOutcome, RoundType};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decided {
Voted(Votes),
Ranked(AgendaRanking),
Tabled,
RecusedDeferral { deferring: u32 },
}
impl Decided {
pub fn final_votes(&self) -> FinalVotes {
match self {
Self::Voted(votes) => votes.into(),
Self::Ranked(ranking) => {
let ranked = |seat: CouncilSeat| match ranking
.ballots
.iter()
.any(|b| b.seat == seat)
{
true => CouncilVote::Ranked,
false => CouncilVote::Abstain,
};
FinalVotes {
artist: ranked(CouncilSeat::Artist),
philosopher: ranked(CouncilSeat::Philosopher),
lawyer: ranked(CouncilSeat::Lawyer),
engineer: ranked(CouncilSeat::Engineer),
steward: CouncilVote::Abstain,
}
}
Self::Tabled => FinalVotes {
artist: CouncilVote::Defer,
philosopher: CouncilVote::Defer,
lawyer: CouncilVote::Defer,
engineer: CouncilVote::Defer,
steward: CouncilVote::Defer,
},
Self::RecusedDeferral { .. } => FinalVotes {
artist: CouncilVote::Defer,
philosopher: CouncilVote::Defer,
lawyer: CouncilVote::Defer,
engineer: CouncilVote::Defer,
steward: CouncilVote::Recused,
},
}
}
pub fn outcome(&self, category: DecisionCategory) -> DecisionOutcome {
match self {
Self::Voted(votes) => votes.outcome(category),
Self::Ranked(_) => DecisionOutcome::Approved,
Self::Tabled | Self::RecusedDeferral { .. } => {
DecisionOutcome::Deferred
}
}
}
pub fn tally(&self) -> String {
match self {
Self::Voted(votes) => votes.tally(),
Self::Ranked(ranking) => format!(
"ranked {}/{}; the Steward executes the order and does not vote",
ranking.ballots.len(),
CouncilSeat::ALL.len()
),
Self::Tabled => "Deferred".to_string(),
Self::RecusedDeferral { deferring } => format!(
"Deferred: {deferring} seat(s) still deferring at the round cap, \
Steward recused; it returns to a later sitting and may not be \
deferred again"
),
}
}
}
impl CouncilDecisionRecord {
pub fn decided(mut self, decided: Decided) -> Self {
self.final_votes = decided.final_votes();
self.outcome = decided.outcome(self.category);
self.vote_tally = decided.tally().into();
(self.veto_rationale, self.abstentions, self.agenda_ranking) =
match decided {
Decided::Voted(votes) => (
votes.veto_rationale().map(|r| r.to_string().into()),
votes.steward_abstention().into_iter().collect(),
None,
),
Decided::Ranked(ranking) => (None, Vec::new(), Some(ranking)),
Decided::Tabled | Decided::RecusedDeferral { .. } => {
(None, Vec::new(), None)
}
};
self
}
pub fn check(&self) -> Result<(), RecordRuleError> {
veto_rule(Some(self.final_votes.steward), self.veto_rationale.as_ref())
}
pub fn check_data(data: &serde_json::Value) -> Result<(), RecordRuleError> {
#[derive(serde::Deserialize)]
struct Steward {
steward: CouncilVote,
}
#[derive(serde::Deserialize)]
struct Part {
final_votes: Option<Steward>,
veto_rationale: Option<Redactable<String>>,
}
let part: Part = serde_json::from_value(data.clone())
.map_err(|_| RecordRuleError::Unreadable)?;
veto_rule(
part.final_votes.map(|v| v.steward),
part.veto_rationale.as_ref(),
)
}
}
fn veto_rule(
steward: Option<CouncilVote>,
rationale: Option<&Redactable<String>>,
) -> Result<(), RecordRuleError> {
let vetoed = steward == Some(CouncilVote::Veto);
let rationale = rationale.map(|r| match r {
Redactable::Value(text) => !text.trim().is_empty(),
Redactable::Redacted(_) => true,
});
match (vetoed, rationale) {
(true, Some(true)) | (false, None) => Ok(()),
(true, None | Some(false)) => {
Err(RecordRuleError::VetoWithoutRationale)
}
(false, Some(_)) => Err(RecordRuleError::RationaleWithoutVeto),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum RecordRuleError {
#[error(
"the Steward vetoed with no rationale; a veto is recorded with its \
rationale (Constitution Art. IV § 5)"
)]
VetoWithoutRationale,
#[error("the record carries a veto rationale but the Steward did not veto")]
RationaleWithoutVeto,
#[error("the record's `final_votes` or `veto_rationale` does not read")]
Unreadable,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FinalRoundError {
#[error("round {0} is not a final vote")]
NotFinal(u32),
#[error("the {} has no response in the final round", .0.name())]
Missing(CouncilSeat),
#[error("the {} answered more than once in the final round", .0.name())]
Twice(CouncilSeat),
#[error("the {} cast no vote in the final round", .0.name())]
NoVote(CouncilSeat),
#[error("the {name}'s final vote, `{vote:?}`, is not a seat's vote", name = .0.name(), vote = .1)]
NotASeatChoice(CouncilSeat, CouncilVote),
#[error(
"the {}'s final-vote statement is blank or withheld; it is the \
record's words for the vote, and an abstention's rationale (Art. \
IV § 3)",
.0.name()
)]
NoStatement(CouncilSeat),
}
impl SeatVotes {
pub fn from_final_round(
round: &CouncilRound,
) -> Result<Self, FinalRoundError> {
if round.round_type != RoundType::FinalVote {
return Err(FinalRoundError::NotFinal(round.number));
}
let seat = |seat: CouncilSeat| -> Result<SeatVote, FinalRoundError> {
let mut responses =
round.responses.iter().filter(|r| r.role == seat);
let response =
responses.next().ok_or(FinalRoundError::Missing(seat))?;
if responses.next().is_some() {
return Err(FinalRoundError::Twice(seat));
}
let vote = response.vote.ok_or(FinalRoundError::NoVote(seat))?;
let choice = SeatChoice::try_from(vote)
.map_err(|_| FinalRoundError::NotASeatChoice(seat, vote))?;
let statement = response
.position
.value()
.and_then(|p| Statement::try_from(p.as_str()).ok())
.ok_or(FinalRoundError::NoStatement(seat))?;
Ok(SeatVote { choice, statement })
};
Ok(Self {
artist: seat(CouncilSeat::Artist)?,
philosopher: seat(CouncilSeat::Philosopher)?,
lawyer: seat(CouncilSeat::Lawyer)?,
engineer: seat(CouncilSeat::Engineer)?,
})
}
}
#[cfg(test)]
mod tests;