printobserver_sdk/error.rs
1//! Every way a call can end other than with the answer it asked for.
2//!
3//! Five of them, and each is a different thing for a caller to do next: the
4//! supervisor was not there, this client would not send the request, the
5//! supervisor said no in words, the policy refused the action — or the
6//! recovery — or the machine itself refused it. The fourth is the one with a
7//! shape rather than a sentence, because a caller that can read the range
8//! allowed can ask again for something inside it.
9
10use crate::contract::{
11 ActionAnswer, PolicyDecision, Range, RecoveryAnswer, RecoveryDecision, RecoveryRejection,
12 RejectionReason, SpoolCommandAnswer,
13};
14
15/// The whole answer a refusal arrived in: an action's, whose record carries
16/// the request and the decision, or a spool command's, carrying the decision
17/// and the events its refusal wrote.
18#[derive(Debug, Clone, PartialEq, serde::Serialize)]
19#[serde(untagged)]
20pub enum RejectedAnswer {
21 /// A print action's answer.
22 Action(Box<ActionAnswer>),
23 /// A spool command's answer.
24 SpoolCommand(Box<SpoolCommandAnswer>),
25}
26
27/// The policy's own refusal of one action, as a caller reads it.
28///
29/// [`Rejection::reason`] is the refusal itself, matched rather than read;
30/// [`Rejection::requested`] and [`Rejection::allowed`] are the value that was
31/// asked for and the range that is allowed, which the policy states when it
32/// ruled on a value and which are absent when it ruled on something else.
33#[derive(Debug, Clone, PartialEq)]
34pub struct Rejection {
35 /// Why the action was refused.
36 pub reason: RejectionReason,
37 /// The value that was asked for, where the policy ruled on one.
38 pub requested: Option<f64>,
39 /// The range that is allowed, where the policy ruled on a value.
40 pub allowed: Option<Range>,
41 /// The whole answer the refusal arrived in.
42 pub answer: RejectedAnswer,
43}
44
45impl Rejection {
46 /// The refusal one answer carries, when the decision on it was a refusal.
47 #[must_use]
48 pub fn of(answer: RejectedAnswer) -> Option<Self> {
49 let decision = match &answer {
50 RejectedAnswer::Action(action) => &action.record.decision,
51 RejectedAnswer::SpoolCommand(command) => &command.decision,
52 };
53 let PolicyDecision::Rejected(reason) = decision else {
54 return None;
55 };
56 let (requested, allowed) = match reason {
57 RejectionReason::OutOfBounds {
58 requested, allowed, ..
59 } => (Some(*requested), Some(allowed.clone())),
60 _ => (None, None),
61 };
62 Some(Self {
63 reason: reason.clone(),
64 requested,
65 allowed,
66 answer,
67 })
68 }
69}
70
71impl core::fmt::Display for Rejection {
72 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
73 write!(
74 formatter,
75 "the supervisor's policy refused this action: {:?}",
76 self.reason
77 )?;
78 if let (Some(requested), Some(allowed)) = (self.requested, self.allowed.as_ref()) {
79 write!(
80 formatter,
81 ". It was asked for {requested}, and what is allowed is {} to {}",
82 allowed.min, allowed.max
83 )?;
84 }
85 Ok(())
86 }
87}
88
89/// A recovery's own refusal, as a caller reads it.
90///
91/// [`RecoveryRejected::rejection`] is the refusal itself, matched rather than
92/// read; [`RecoveryRejected::requested`] and [`RecoveryRejected::allowed`] are
93/// the value that was asked for and the range that is allowed, where a bound
94/// refused it, and absent where something else did.
95#[derive(Debug, Clone, PartialEq)]
96pub struct RecoveryRejected {
97 /// Why the recovery was refused.
98 pub rejection: RecoveryRejection,
99 /// The value that was asked for, where a bound refused it.
100 pub requested: Option<f64>,
101 /// The range that is allowed, where a bound refused it.
102 pub allowed: Option<Range>,
103 /// The whole answer the refusal arrived in.
104 pub answer: RecoveryAnswer,
105}
106
107impl RecoveryRejected {
108 /// The refusal one recovery answer carries, when it was refused.
109 #[must_use]
110 pub fn of(answer: RecoveryAnswer) -> Option<Self> {
111 let RecoveryDecision::Rejected(rejection) = answer.decision.clone() else {
112 return None;
113 };
114 let (requested, allowed) = match &rejection {
115 RecoveryRejection::OutOfBounds {
116 requested, allowed, ..
117 } => (Some(*requested), Some(allowed.clone())),
118 _ => (None, None),
119 };
120 Some(Self {
121 rejection,
122 requested,
123 allowed,
124 answer,
125 })
126 }
127}
128
129impl core::fmt::Display for RecoveryRejected {
130 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
131 write!(
132 formatter,
133 "the supervisor refused this recovery: {}",
134 self.answer.detail
135 )
136 }
137}
138
139/// Why a call did not answer what it asked for.
140#[derive(Debug, Clone, PartialEq)]
141pub enum ClientError {
142 /// Nothing answered at the configured address.
143 Unreachable {
144 /// Where this client looked.
145 address: String,
146 /// What happened there, in the words of whatever failed.
147 detail: String,
148 },
149 /// This client would not send the request, because it carried no reason.
150 ///
151 /// Every mutating operation carries a reason, and a call whose reason is
152 /// empty is refused here rather than at the server: nothing reaches the
153 /// policy, the printer or the record.
154 NoReason,
155 /// A value of the request could not be rendered as JSON.
156 Unsendable {
157 /// What could not be rendered, in the words of whatever refused it.
158 detail: String,
159 },
160 /// The supervisor answered something this client cannot read.
161 Unreadable {
162 /// The status it answered under.
163 status: u16,
164 /// What could not be read, in the words of whatever refused it.
165 detail: String,
166 },
167 /// The supervisor will not do what it was asked, and said why.
168 Refused {
169 /// The status it answered under.
170 status: u16,
171 /// What it said, in its own words.
172 detail: String,
173 },
174 /// The policy accepted the action and the machine refused it.
175 PrinterRefused {
176 /// What the printer said.
177 detail: String,
178 /// The record of the request and the decision taken on it.
179 answer: Box<ActionAnswer>,
180 },
181 /// The policy refused the action, and the refusal is the answer.
182 Rejected(Box<Rejection>),
183 /// The supervisor refused a recovery action, and the refusal is the
184 /// answer.
185 RecoveryRejected(Box<RecoveryRejected>),
186}
187
188impl core::fmt::Display for ClientError {
189 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
190 match self {
191 Self::Unreachable { address, detail } => write!(
192 formatter,
193 "nothing answered at {address}: {detail}. Start the supervisor there, or \
194 point this client at the address it is answering on"
195 ),
196 Self::NoReason => formatter
197 .write_str("every mutating call carries a reason, and this one carries none"),
198 Self::Unsendable { detail } => {
199 write!(
200 formatter,
201 "this request could not be rendered as JSON: {detail}"
202 )
203 }
204 Self::Unreadable { status, detail } => write!(
205 formatter,
206 "the supervisor answered {status} with something this client cannot read: \
207 {detail}"
208 ),
209 Self::Refused { status, detail } => {
210 write!(formatter, "the supervisor answered {status}: {detail}")
211 }
212 Self::PrinterRefused { detail, .. } => write!(
213 formatter,
214 "the policy accepted this action and the machine refused it: {detail}. Look \
215 at the printer, then ask again"
216 ),
217 Self::Rejected(rejection) => write!(formatter, "{rejection}"),
218 Self::RecoveryRejected(rejection) => write!(formatter, "{rejection}"),
219 }
220 }
221}
222
223impl core::error::Error for ClientError {}