haematite_cli/serve/
drafts.rs1use haematite::tree::Hash;
15use lys_pass::drafts::{ApprovedDraft, Execution, Executor};
16
17use crate::serve::authorize::subjects;
18use crate::serve::pass::target;
19use crate::serve::protocol::{Operation, Request};
20use crate::serve::service::Service;
21use crate::serve::storage::Failure;
22
23pub struct Drafts<'a> {
25 service: &'a Service,
26 database: String,
27}
28
29impl<'a> Drafts<'a> {
30 #[must_use]
32 pub fn new(service: &'a Service, database: &str) -> Self {
33 Self {
34 service,
35 database: database.to_owned(),
36 }
37 }
38}
39
40impl Executor for Drafts<'_> {
41 type Error = Failure;
42
43 fn receipt(&self, _draft: &str) -> Result<Option<Execution>, Failure> {
47 Ok(None)
48 }
49
50 fn execute_and_record(&mut self, draft: &ApprovedDraft) -> Result<Execution, Failure> {
55 let operation = match self.approved_change(draft) {
56 Ok(operation) => operation,
57 Err(refusal) => {
58 return Ok(Execution::RefusedOnExecution {
59 request_digest: draft.request_digest.clone(),
60 refusal: refusal.code,
61 reason: refusal.message,
62 });
63 }
64 };
65 let receipt = self
66 .service
67 .dispatch(operation)
68 .map_err(|failure| failure.failure().clone())?;
69 let bytes = serde_json::to_vec(&receipt).map_err(Failure::storage)?;
70 Ok(Execution::Executed {
71 request_digest: draft.request_digest.clone(),
72 receipt_digest: Hash::of(&bytes).to_string(),
73 })
74 }
75}
76
77impl Drafts<'_> {
78 fn approved_change(&self, draft: &ApprovedDraft) -> Result<Operation, Failure> {
81 let operation = serde_json::from_str::<Request>(&draft.words)
82 .map_err(|error| Failure::new("draft_words_invalid", error.to_string()))?
83 .operation;
84 if !matches!(operation, Operation::Mutate(_)) {
85 return Err(Failure::new(
86 "draft_not_a_change",
87 "only a change is applied from an approved draft",
88 ));
89 }
90 let differs = || {
91 Failure::new(
92 "draft_target_differs",
93 "the draft's words act on another resource than it was approved for",
94 )
95 };
96 let asked = subjects(&operation)?;
97 let [subject] = asked.as_slice() else {
98 return Err(differs());
99 };
100 let named = target(subject.route, &self.database, subject.name.as_deref())
101 .map_err(|error| Failure::new(error.name(), "the draft's resource is invalid"))?;
102 if named != draft.target {
103 return Err(differs());
104 }
105 Ok(operation)
106 }
107}
108
109#[cfg(test)]
110#[path = "drafts_tests.rs"]
111mod tests;