Skip to main content

ocpi_kit/transport/
status.rs

1//! OCPI status codes: the four-digit codes that live inside a 200 OK.
2//!
3//! > *The transport layer ends after a message is correctly parsed into a (semantically
4//! > unvalidated) JSON structure. … If a request is syntactically valid JSON and addresses an
5//! > existing resource, and comes from a sender that is successfully authenticated and
6//! > authorized, this request is supposed to have reached the OCPI layer. To such a request, an
7//! > HTTP error status code MUST NOT be returned.*
8//!
9//! That sentence is the single most misunderstood rule in OCPI. A server that answers a
10//! semantically invalid Location with `HTTP 422` is not doing OCPI. [`StatusCode`] plus
11//! [`OcpiError::http_status`](super::OcpiError::http_status) encode the rule so it is hard to
12//! get wrong.
13//!
14//! Spec: 2.3.0 §status_codes_status_codes
15
16use core::fmt;
17
18use serde::{Deserialize, Serialize};
19
20/// A four-digit OCPI status code.
21///
22/// Modelled as a newtype over `u16` rather than an enum because the spec reserves custom ranges
23/// (`19xx`, `29xx`, `39xx`, `49xx`) that a party may define values in, and because a peer may
24/// send a code from a newer version.
25///
26/// ```
27/// use ocpi_kit::transport::{StatusCode, StatusClass};
28///
29/// assert_eq!(StatusCode::SUCCESS.class(), StatusClass::Success);
30/// assert_eq!(StatusCode::INVALID_PARAMETERS.get(), 2001);
31/// assert!(StatusCode::new(2901).is_custom());
32/// ```
33///
34/// Spec: 2.3.0 §status_codes_status_codes
35#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
36#[serde(transparent)]
37#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
38pub struct StatusCode(u16);
39
40impl StatusCode {
41    // --- 1xxx: Success -----------------------------------------------------------------------
42    /// `1000` — Generic success code.
43    pub const SUCCESS: Self = Self(1000);
44
45    // --- 2xxx: Client errors -----------------------------------------------------------------
46    /// `2000` — Generic client error.
47    pub const CLIENT_ERROR: Self = Self(2000);
48    /// `2001` — Invalid or missing parameters, e.g. a missing `last_updated` in a PATCH.
49    pub const INVALID_PARAMETERS: Self = Self(2001);
50    /// `2002` — Not enough information, e.g. an authorization request with too little information.
51    pub const NOT_ENOUGH_INFORMATION: Self = Self(2002);
52    /// `2003` — Unknown Location, e.g. a `START_SESSION` with an unknown location.
53    pub const UNKNOWN_LOCATION: Self = Self(2003);
54    /// `2004` — Unknown Token, e.g. a real-time authorization of an unknown Token.
55    pub const UNKNOWN_TOKEN: Self = Self(2004);
56
57    // --- 3xxx: Server errors -----------------------------------------------------------------
58    /// `3000` — Generic server error.
59    pub const SERVER_ERROR: Self = Self(3000);
60    /// `3001` — Unable to use the client's API, e.g. a failed call-back during registration.
61    pub const UNABLE_TO_USE_CLIENT_API: Self = Self(3001);
62    /// `3002` — Unsupported version.
63    pub const UNSUPPORTED_VERSION: Self = Self(3002);
64    /// `3003` — No matching endpoints or expected endpoints missing between parties.
65    pub const NO_MATCHING_ENDPOINTS: Self = Self(3003);
66
67    // --- 4xxx: Hub errors --------------------------------------------------------------------
68    /// `4000` — Generic hub error.
69    pub const HUB_ERROR: Self = Self(4000);
70    /// `4001` — Unknown receiver: the `OCPI-to-*` address is unknown.
71    pub const UNKNOWN_RECEIVER: Self = Self(4001);
72    /// `4002` — Timeout on a forwarded request.
73    pub const TIMEOUT_ON_FORWARDED_REQUEST: Self = Self(4002);
74    /// `4003` — Connection problem: the receiving party is not connected.
75    pub const CONNECTION_PROBLEM: Self = Self(4003);
76
77    /// Wraps a raw code.
78    #[must_use]
79    pub const fn new(code: u16) -> Self {
80        Self(code)
81    }
82
83    /// The raw code.
84    #[must_use]
85    pub const fn get(self) -> u16 {
86        self.0
87    }
88
89    /// Which of the four ranges this code falls in.
90    #[must_use]
91    pub const fn class(self) -> StatusClass {
92        match self.0 {
93            1000..=1999 => StatusClass::Success,
94            2000..=2999 => StatusClass::ClientError,
95            3000..=3999 => StatusClass::ServerError,
96            4000..=4999 => StatusClass::HubError,
97            _ => StatusClass::Unknown,
98        }
99    }
100
101    /// Whether this is a success code, so `data` carries the documented payload.
102    ///
103    /// > *When the status code is in the success range (1xxx), the `data` field in the response
104    /// > message SHOULD contain the information as specified in the protocol. Otherwise the
105    /// > `data` field is unspecified.*
106    #[must_use]
107    pub const fn is_success(self) -> bool {
108        matches!(self.class(), StatusClass::Success)
109    }
110
111    /// Whether the code falls in one of the reserved custom sub-ranges (`x900`–`x999`).
112    ///
113    /// > *Custom status code range values SHALL NOT be used by standard OCPI module … When custom
114    /// > status codes are used, keep in mind that different custom modules could use the same
115    /// > values with a different meaning, as they are not standardized.*
116    #[must_use]
117    pub const fn is_custom(self) -> bool {
118        !matches!(self.class(), StatusClass::Unknown) && self.0 % 1000 >= 900
119    }
120
121    /// The description the specification gives for this code, when it defines one.
122    #[must_use]
123    pub const fn description(self) -> Option<&'static str> {
124        Some(match self.0 {
125            1000 => "Generic success code",
126            2000 => "Generic client error",
127            2001 => "Invalid or missing parameters",
128            2002 => "Not enough information",
129            2003 => "Unknown Location",
130            2004 => "Unknown Token",
131            3000 => "Generic server error",
132            3001 => "Unable to use the client's API",
133            3002 => "Unsupported version",
134            3003 => "No matching endpoints or expected endpoints missing between parties",
135            4000 => "Generic error",
136            4001 => "Unknown receiver (TO address is unknown)",
137            4002 => "Timeout on forwarded request",
138            4003 => "Connection problem (receiving party is not connected)",
139            _ => return None,
140        })
141    }
142}
143
144impl fmt::Display for StatusCode {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        match self.description() {
147            Some(d) => write!(f, "{} ({d})", self.0),
148            None => write!(f, "{}", self.0),
149        }
150    }
151}
152
153impl fmt::Debug for StatusCode {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "StatusCode({self})")
156    }
157}
158
159impl From<u16> for StatusCode {
160    fn from(value: u16) -> Self {
161        Self(value)
162    }
163}
164impl From<StatusCode> for u16 {
165    fn from(value: StatusCode) -> Self {
166        value.0
167    }
168}
169
170/// The four status code ranges the specification defines.
171///
172/// Spec: 2.3.0 §status_codes_status_codes
173#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
174#[non_exhaustive]
175pub enum StatusClass {
176    /// `1xxx` — the request was handled as documented.
177    Success,
178    /// `2xxx` — the data sent by the client cannot be processed by the server.
179    ClientError,
180    /// `3xxx` — the server encountered an internal error.
181    ServerError,
182    /// `4xxx` — a hub failed to route the message.
183    HubError,
184    /// A code outside `1000`–`4999`, which the specification does not define.
185    Unknown,
186}
187
188impl StatusClass {
189    /// Whether this class means the request succeeded.
190    #[must_use]
191    pub const fn is_success(self) -> bool {
192        matches!(self, Self::Success)
193    }
194
195    /// Whether the fault lies with the party that sent the request.
196    #[must_use]
197    pub const fn is_client_fault(self) -> bool {
198        matches!(self, Self::ClientError)
199    }
200}
201
202impl fmt::Display for StatusClass {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        f.write_str(match self {
205            Self::Success => "success",
206            Self::ClientError => "client error",
207            Self::ServerError => "server error",
208            Self::HubError => "hub error",
209            Self::Unknown => "unknown status class",
210        })
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn classes_follow_the_ranges() {
220        assert_eq!(StatusCode::new(1000).class(), StatusClass::Success);
221        assert_eq!(StatusCode::new(1999).class(), StatusClass::Success);
222        assert_eq!(StatusCode::new(2001).class(), StatusClass::ClientError);
223        assert_eq!(StatusCode::new(3002).class(), StatusClass::ServerError);
224        assert_eq!(StatusCode::new(4003).class(), StatusClass::HubError);
225        assert_eq!(StatusCode::new(5000).class(), StatusClass::Unknown);
226        assert_eq!(StatusCode::new(999).class(), StatusClass::Unknown);
227    }
228
229    #[test]
230    fn custom_ranges_are_recognised() {
231        for code in [1900, 1999, 2900, 3950, 4999] {
232            assert!(StatusCode::new(code).is_custom(), "{code} is in a reserved custom range");
233        }
234        for code in [1000, 2001, 3003, 4000, 2899] {
235            assert!(!StatusCode::new(code).is_custom(), "{code} is a standard code");
236        }
237        assert!(!StatusCode::new(5900).is_custom(), "outside every defined class");
238    }
239
240    #[test]
241    fn serialises_as_a_bare_number() {
242        assert_eq!(serde_json::to_string(&StatusCode::SUCCESS).unwrap(), "1000");
243        let parsed: StatusCode = serde_json::from_str("2001").unwrap();
244        assert_eq!(parsed, StatusCode::INVALID_PARAMETERS);
245    }
246
247    #[test]
248    fn display_includes_the_spec_description() {
249        assert_eq!(StatusCode::INVALID_PARAMETERS.to_string(), "2001 (Invalid or missing parameters)");
250        assert_eq!(StatusCode::new(2901).to_string(), "2901");
251    }
252}