1#![forbid(unsafe_code)]
8#![warn(missing_docs)]
9
10use std::collections::BTreeMap;
11use std::error::Error;
12use std::fmt;
13
14pub const RAKKOTAN_OFFICIAL_URL: &str = "https://edu-net.co.jp/rakkotan_bak";
16
17pub const RAKKOTAN_BIZ_OFFICIAL_URL: &str = "https://edu-net.co.jp/rakkotan_biz";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub enum AdoptionPrinciple {
23 PurposeAndOutcome,
25 AuthorityAndTerms,
27 DataGovernance,
29 UserProtection,
31 OperationalOwnership,
33 ReversibleAdoption,
35}
36
37impl AdoptionPrinciple {
38 pub const ALL: [Self; 6] = [
40 Self::PurposeAndOutcome,
41 Self::AuthorityAndTerms,
42 Self::DataGovernance,
43 Self::UserProtection,
44 Self::OperationalOwnership,
45 Self::ReversibleAdoption,
46 ];
47
48 const fn is_safety_gate(self) -> bool {
49 matches!(
50 self,
51 Self::AuthorityAndTerms | Self::DataGovernance | Self::UserProtection
52 )
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Finding {
59 Satisfied,
61 NeedsRevision,
63 Unknown,
65 Violated,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct Assessment {
72 finding: Finding,
73 evidence: String,
74}
75
76impl Assessment {
77 pub fn new(finding: Finding, evidence: impl Into<String>) -> Result<Self, AdoptionError> {
79 let evidence = evidence.into();
80 if evidence.trim().is_empty() {
81 return Err(AdoptionError::EmptyEvidence);
82 }
83 Ok(Self { finding, evidence })
84 }
85
86 pub const fn finding(&self) -> Finding {
88 self.finding
89 }
90
91 pub fn evidence(&self) -> &str {
93 &self.evidence
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct AdoptionPlan {
100 organization: String,
101 assessments: BTreeMap<AdoptionPrinciple, Assessment>,
102}
103
104impl AdoptionPlan {
105 pub fn new(organization: impl Into<String>) -> Result<Self, AdoptionError> {
107 let organization = organization.into();
108 if organization.trim().is_empty() {
109 return Err(AdoptionError::EmptyOrganization);
110 }
111 Ok(Self {
112 organization,
113 assessments: BTreeMap::new(),
114 })
115 }
116
117 pub fn assess(
119 mut self,
120 principle: AdoptionPrinciple,
121 finding: Finding,
122 evidence: impl Into<String>,
123 ) -> Result<Self, AdoptionError> {
124 self.set_assessment(principle, finding, evidence)?;
125 Ok(self)
126 }
127
128 pub fn set_assessment(
130 &mut self,
131 principle: AdoptionPrinciple,
132 finding: Finding,
133 evidence: impl Into<String>,
134 ) -> Result<(), AdoptionError> {
135 self.assessments
136 .insert(principle, Assessment::new(finding, evidence)?);
137 Ok(())
138 }
139
140 pub fn organization(&self) -> &str {
142 &self.organization
143 }
144
145 pub fn assessment(&self, principle: AdoptionPrinciple) -> Option<&Assessment> {
147 self.assessments.get(&principle)
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum AdoptionDecision {
154 Decline,
156 NeedsEvidence,
158 Revise,
160 Ready,
162}
163
164#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
166pub struct AdoptionPolicy;
167
168impl AdoptionPolicy {
169 pub fn evaluate(plan: &AdoptionPlan) -> AdoptionReport {
171 let mut missing = Vec::new();
172 let mut unresolved = Vec::new();
173 let mut violated = Vec::new();
174
175 for principle in AdoptionPrinciple::ALL {
176 match plan.assessment(principle).map(Assessment::finding) {
177 None => missing.push(principle),
178 Some(Finding::Satisfied) => {}
179 Some(Finding::NeedsRevision | Finding::Unknown) => unresolved.push(principle),
180 Some(Finding::Violated) => violated.push(principle),
181 }
182 }
183
184 let gate_violation = violated.iter().any(|principle| principle.is_safety_gate());
185 let decision = if gate_violation {
186 AdoptionDecision::Decline
187 } else if !missing.is_empty() {
188 AdoptionDecision::NeedsEvidence
189 } else if !unresolved.is_empty() || !violated.is_empty() {
190 AdoptionDecision::Revise
191 } else {
192 AdoptionDecision::Ready
193 };
194
195 AdoptionReport {
196 decision,
197 missing,
198 unresolved,
199 violated,
200 }
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct AdoptionReport {
207 decision: AdoptionDecision,
208 missing: Vec<AdoptionPrinciple>,
209 unresolved: Vec<AdoptionPrinciple>,
210 violated: Vec<AdoptionPrinciple>,
211}
212
213impl AdoptionReport {
214 pub const fn decision(&self) -> AdoptionDecision {
216 self.decision
217 }
218
219 pub fn missing(&self) -> &[AdoptionPrinciple] {
221 &self.missing
222 }
223
224 pub fn unresolved(&self) -> &[AdoptionPrinciple] {
226 &self.unresolved
227 }
228
229 pub fn violated(&self) -> &[AdoptionPrinciple] {
231 &self.violated
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum AdoptionError {
238 EmptyOrganization,
240 EmptyEvidence,
242}
243
244impl fmt::Display for AdoptionError {
245 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246 match self {
247 Self::EmptyOrganization => formatter.write_str("organization must not be blank"),
248 Self::EmptyEvidence => formatter.write_str("assessment evidence must not be blank"),
249 }
250 }
251}
252
253impl Error for AdoptionError {}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 fn ready_plan() -> AdoptionPlan {
260 AdoptionPrinciple::ALL
261 .into_iter()
262 .try_fold(
263 AdoptionPlan::new("Example Corp").unwrap(),
264 |plan, principle| plan.assess(principle, Finding::Satisfied, "reviewed evidence"),
265 )
266 .unwrap()
267 }
268
269 #[test]
270 fn complete_satisfied_plan_is_ready() {
271 assert_eq!(
272 AdoptionPolicy::evaluate(&ready_plan()).decision(),
273 AdoptionDecision::Ready
274 );
275 }
276
277 #[test]
278 fn data_violation_is_a_veto_even_with_missing_items() {
279 let plan = AdoptionPlan::new("Example Corp")
280 .unwrap()
281 .assess(
282 AdoptionPrinciple::DataGovernance,
283 Finding::Violated,
284 "retention has no limit",
285 )
286 .unwrap();
287 let report = AdoptionPolicy::evaluate(&plan);
288 assert_eq!(report.decision(), AdoptionDecision::Decline);
289 assert_eq!(report.violated(), &[AdoptionPrinciple::DataGovernance]);
290 }
291
292 #[test]
293 fn incomplete_plan_needs_evidence_in_stable_order() {
294 let report = AdoptionPolicy::evaluate(&AdoptionPlan::new("Example Corp").unwrap());
295 assert_eq!(report.decision(), AdoptionDecision::NeedsEvidence);
296 assert_eq!(report.missing(), &AdoptionPrinciple::ALL);
297 }
298
299 #[test]
300 fn revision_is_explained() {
301 let mut plan = ready_plan();
302 plan.set_assessment(
303 AdoptionPrinciple::OperationalOwnership,
304 Finding::NeedsRevision,
305 "support owner is temporary",
306 )
307 .unwrap();
308 let report = AdoptionPolicy::evaluate(&plan);
309 assert_eq!(report.decision(), AdoptionDecision::Revise);
310 assert_eq!(
311 report.unresolved(),
312 &[AdoptionPrinciple::OperationalOwnership]
313 );
314 }
315
316 #[test]
317 fn blank_inputs_are_rejected() {
318 assert_eq!(
319 AdoptionPlan::new(" "),
320 Err(AdoptionError::EmptyOrganization)
321 );
322 assert_eq!(
323 Assessment::new(Finding::Satisfied, "\t"),
324 Err(AdoptionError::EmptyEvidence)
325 );
326 }
327}