Skip to main content

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 {}