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}