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