#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
pub const RAKKOTAN_OFFICIAL_URL: &str = "https://edu-net.co.jp/rakkotan_bak";
pub const RAKKOTAN_BIZ_OFFICIAL_URL: &str = "https://edu-net.co.jp/rakkotan_biz";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AdoptionPrinciple {
PurposeAndOutcome,
AuthorityAndTerms,
DataGovernance,
UserProtection,
OperationalOwnership,
ReversibleAdoption,
}
impl AdoptionPrinciple {
pub const ALL: [Self; 6] = [
Self::PurposeAndOutcome,
Self::AuthorityAndTerms,
Self::DataGovernance,
Self::UserProtection,
Self::OperationalOwnership,
Self::ReversibleAdoption,
];
const fn is_safety_gate(self) -> bool {
matches!(
self,
Self::AuthorityAndTerms | Self::DataGovernance | Self::UserProtection
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Finding {
Satisfied,
NeedsRevision,
Unknown,
Violated,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Assessment {
finding: Finding,
evidence: String,
}
impl Assessment {
pub fn new(finding: Finding, evidence: impl Into<String>) -> Result<Self, AdoptionError> {
let evidence = evidence.into();
if evidence.trim().is_empty() {
return Err(AdoptionError::EmptyEvidence);
}
Ok(Self { finding, evidence })
}
pub const fn finding(&self) -> Finding {
self.finding
}
pub fn evidence(&self) -> &str {
&self.evidence
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdoptionPlan {
organization: String,
assessments: BTreeMap<AdoptionPrinciple, Assessment>,
}
impl AdoptionPlan {
pub fn new(organization: impl Into<String>) -> Result<Self, AdoptionError> {
let organization = organization.into();
if organization.trim().is_empty() {
return Err(AdoptionError::EmptyOrganization);
}
Ok(Self {
organization,
assessments: BTreeMap::new(),
})
}
pub fn assess(
mut self,
principle: AdoptionPrinciple,
finding: Finding,
evidence: impl Into<String>,
) -> Result<Self, AdoptionError> {
self.set_assessment(principle, finding, evidence)?;
Ok(self)
}
pub fn set_assessment(
&mut self,
principle: AdoptionPrinciple,
finding: Finding,
evidence: impl Into<String>,
) -> Result<(), AdoptionError> {
self.assessments
.insert(principle, Assessment::new(finding, evidence)?);
Ok(())
}
pub fn organization(&self) -> &str {
&self.organization
}
pub fn assessment(&self, principle: AdoptionPrinciple) -> Option<&Assessment> {
self.assessments.get(&principle)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdoptionDecision {
Decline,
NeedsEvidence,
Revise,
Ready,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AdoptionPolicy;
impl AdoptionPolicy {
pub fn evaluate(plan: &AdoptionPlan) -> AdoptionReport {
let mut missing = Vec::new();
let mut unresolved = Vec::new();
let mut violated = Vec::new();
for principle in AdoptionPrinciple::ALL {
match plan.assessment(principle).map(Assessment::finding) {
None => missing.push(principle),
Some(Finding::Satisfied) => {}
Some(Finding::NeedsRevision | Finding::Unknown) => unresolved.push(principle),
Some(Finding::Violated) => violated.push(principle),
}
}
let gate_violation = violated.iter().any(|principle| principle.is_safety_gate());
let decision = if gate_violation {
AdoptionDecision::Decline
} else if !missing.is_empty() {
AdoptionDecision::NeedsEvidence
} else if !unresolved.is_empty() || !violated.is_empty() {
AdoptionDecision::Revise
} else {
AdoptionDecision::Ready
};
AdoptionReport {
decision,
missing,
unresolved,
violated,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdoptionReport {
decision: AdoptionDecision,
missing: Vec<AdoptionPrinciple>,
unresolved: Vec<AdoptionPrinciple>,
violated: Vec<AdoptionPrinciple>,
}
impl AdoptionReport {
pub const fn decision(&self) -> AdoptionDecision {
self.decision
}
pub fn missing(&self) -> &[AdoptionPrinciple] {
&self.missing
}
pub fn unresolved(&self) -> &[AdoptionPrinciple] {
&self.unresolved
}
pub fn violated(&self) -> &[AdoptionPrinciple] {
&self.violated
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdoptionError {
EmptyOrganization,
EmptyEvidence,
}
impl fmt::Display for AdoptionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyOrganization => formatter.write_str("organization must not be blank"),
Self::EmptyEvidence => formatter.write_str("assessment evidence must not be blank"),
}
}
}
impl Error for AdoptionError {}
#[cfg(test)]
mod tests {
use super::*;
fn ready_plan() -> AdoptionPlan {
AdoptionPrinciple::ALL
.into_iter()
.try_fold(
AdoptionPlan::new("Example Corp").unwrap(),
|plan, principle| plan.assess(principle, Finding::Satisfied, "reviewed evidence"),
)
.unwrap()
}
#[test]
fn complete_satisfied_plan_is_ready() {
assert_eq!(
AdoptionPolicy::evaluate(&ready_plan()).decision(),
AdoptionDecision::Ready
);
}
#[test]
fn data_violation_is_a_veto_even_with_missing_items() {
let plan = AdoptionPlan::new("Example Corp")
.unwrap()
.assess(
AdoptionPrinciple::DataGovernance,
Finding::Violated,
"retention has no limit",
)
.unwrap();
let report = AdoptionPolicy::evaluate(&plan);
assert_eq!(report.decision(), AdoptionDecision::Decline);
assert_eq!(report.violated(), &[AdoptionPrinciple::DataGovernance]);
}
#[test]
fn incomplete_plan_needs_evidence_in_stable_order() {
let report = AdoptionPolicy::evaluate(&AdoptionPlan::new("Example Corp").unwrap());
assert_eq!(report.decision(), AdoptionDecision::NeedsEvidence);
assert_eq!(report.missing(), &AdoptionPrinciple::ALL);
}
#[test]
fn revision_is_explained() {
let mut plan = ready_plan();
plan.set_assessment(
AdoptionPrinciple::OperationalOwnership,
Finding::NeedsRevision,
"support owner is temporary",
)
.unwrap();
let report = AdoptionPolicy::evaluate(&plan);
assert_eq!(report.decision(), AdoptionDecision::Revise);
assert_eq!(
report.unresolved(),
&[AdoptionPrinciple::OperationalOwnership]
);
}
#[test]
fn blank_inputs_are_rejected() {
assert_eq!(
AdoptionPlan::new(" "),
Err(AdoptionError::EmptyOrganization)
);
assert_eq!(
Assessment::new(Finding::Satisfied, "\t"),
Err(AdoptionError::EmptyEvidence)
);
}
}