Skip to main content

ocpp_client/
error.rs

1use serde_json::Value;
2
3/// Implemented once per OCPP version by that version's error enum (`OCPP1_6Error`,
4/// `OCPP2_0_1Error`, ...), so the generic [`crate::Client`] engine can build and read
5/// CALLERROR payloads without knowing which version it's carrying.
6pub trait ProtocolError: core::fmt::Debug + Send + Sync + Sized + 'static {
7    fn code(&self) -> &str;
8    fn description(&self) -> &str;
9    fn details(&self) -> &Value;
10    fn not_implemented(action: &str) -> Self;
11    fn from_wire(code: &str, description: &str, details: Value) -> Self;
12}
13
14/// Everything that can go wrong sending or receiving a single OCPP action, flattened into
15/// one type instead of the `Result<Result<Response, ProtocolError>, Box<dyn Error>>` shape.
16#[derive(Debug)]
17pub enum ClientError<E> {
18    /// The other side answered with a CALLERROR.
19    Protocol(E),
20    /// No CALLRESULT/CALLERROR arrived before the client's timeout elapsed.
21    Timeout,
22    /// The payload didn't match the expected request/response type.
23    Decode(serde_json::Error),
24    /// The transport failed to send or receive a frame.
25    Transport(crate::transport::TransportError),
26    /// The connection was closed before a response arrived.
27    Closed,
28}
29
30impl<E: ProtocolError> core::fmt::Display for ClientError<E> {
31    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
32        match self {
33            ClientError::Protocol(e) => {
34                write!(f, "protocol error: {} ({})", e.code(), e.description())
35            }
36            ClientError::Timeout => write!(f, "request timed out"),
37            ClientError::Decode(e) => write!(f, "failed to decode payload: {e}"),
38            ClientError::Transport(e) => write!(f, "transport error: {e}"),
39            ClientError::Closed => write!(f, "connection closed"),
40        }
41    }
42}
43
44impl<E: ProtocolError> core::error::Error for ClientError<E> {}
45
46/// The version-independent half of each version's `From<ValidationError>` impl: the CALLERROR
47/// `description` and `errorDetails` that answer a schema violation.
48///
49/// Only the wire *code* differs between versions - 1.6J's `OccurenceConstraintViolation` against
50/// 2.x's `OccurrenceConstraintViolation` - so the three impls pick that themselves from
51/// [`ocpp_types::validate::ConstraintClass`] and share everything else through here.
52///
53/// `errorDetails` has no shape in the specification, so it carries the JSON path on its own,
54/// separately from the sentence in `description`: a peer can match on `details["path"]` without
55/// parsing prose. The path renders the same way `ValidationError`'s `Display` writes it
56/// (`id[0]`, and `<payload>` for a violation at the root), because it *is* that rendering minus
57/// the trailing reason.
58///
59/// The version features are part of the gate, not just `validate`: every caller lives in a
60/// per-version module, so `validate` on its own - which is a legitimate thing for a consumer to
61/// select - would leave this dead and fail the `-D warnings` clippy. `--all-features` never
62/// reaches that combination.
63#[cfg(all(
64    feature = "validate",
65    any(feature = "ocpp_1_6", feature = "ocpp_2_0_1", feature = "ocpp_2_1")
66))]
67pub(crate) fn validation_error_parts(
68    error: &ocpp_types::validate::ValidationError,
69) -> (alloc::string::String, Value) {
70    use alloc::string::ToString;
71    use core::fmt::Write;
72    use ocpp_types::validate::PathSegment;
73
74    let mut path = alloc::string::String::new();
75    if error.path_truncated() {
76        path.push_str("...");
77    }
78    if error.path().is_empty() && !error.path_truncated() {
79        path.push_str("<payload>");
80    }
81    for (position, segment) in error.path().iter().enumerate() {
82        match segment {
83            PathSegment::Field(name) => {
84                if position > 0 || error.path_truncated() {
85                    path.push('.');
86                }
87                path.push_str(name);
88            }
89            // Writing into a String cannot fail; the Result is core::fmt's, not io's.
90            PathSegment::Index(index) => {
91                let _ = write!(path, "[{index}]");
92            }
93        }
94    }
95
96    (error.to_string(), serde_json::json!({ "path": path }))
97}