Skip to main content

codetics_ocpp_core/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! RPC framework error codes (CALLERROR / CALLRESULTERROR).
5//!
6//! OCPP-J 1.6 and 2.x use slightly different wire strings for the same
7//! concept (1.6 keeps the historic typos `FormationViolation` and
8//! `OccurenceConstraintViolation`, see the 1.6-J errata §5). The semantic
9//! code is shared; the wire spelling is chosen by [`ErrorDialect`].
10
11use std::fmt;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum ErrorCode {
15    FormatViolation,
16    GenericError,
17    InternalError,
18    /// 2.0.1+ only.
19    MessageTypeNotSupported,
20    NotImplemented,
21    NotSupported,
22    OccurrenceConstraintViolation,
23    PropertyConstraintViolation,
24    ProtocolError,
25    /// 2.0.1+ only.
26    RpcFrameworkError,
27    SecurityError,
28    TypeConstraintViolation,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum ErrorDialect {
33    /// OCPP-J 1.6 error code table.
34    Ocpp16,
35    /// OCPP-J 2.0.1 / 2.1 error code table.
36    Ocpp2,
37}
38
39impl ErrorCode {
40    pub fn as_wire(self, dialect: ErrorDialect) -> &'static str {
41        use ErrorCode::*;
42        match (self, dialect) {
43            (FormatViolation, ErrorDialect::Ocpp16) => "FormationViolation",
44            (FormatViolation, ErrorDialect::Ocpp2) => "FormatViolation",
45            (OccurrenceConstraintViolation, ErrorDialect::Ocpp16) => "OccurenceConstraintViolation",
46            (OccurrenceConstraintViolation, ErrorDialect::Ocpp2) => "OccurrenceConstraintViolation",
47            // Codes that do not exist in 1.6 fall back to the closest 1.6 code.
48            (MessageTypeNotSupported, ErrorDialect::Ocpp16) => "NotSupported",
49            (RpcFrameworkError, ErrorDialect::Ocpp16) => "ProtocolError",
50            (MessageTypeNotSupported, _) => "MessageTypeNotSupported",
51            (RpcFrameworkError, _) => "RpcFrameworkError",
52            (GenericError, _) => "GenericError",
53            (InternalError, _) => "InternalError",
54            (NotImplemented, _) => "NotImplemented",
55            (NotSupported, _) => "NotSupported",
56            (PropertyConstraintViolation, _) => "PropertyConstraintViolation",
57            (ProtocolError, _) => "ProtocolError",
58            (SecurityError, _) => "SecurityError",
59            (TypeConstraintViolation, _) => "TypeConstraintViolation",
60        }
61    }
62
63    /// Parses any known spelling (both dialects). Unknown codes map to `GenericError`.
64    pub fn from_wire(s: &str) -> ErrorCode {
65        use ErrorCode::*;
66        match s {
67            "FormatViolation" | "FormationViolation" => FormatViolation,
68            "OccurrenceConstraintViolation" | "OccurenceConstraintViolation" => {
69                OccurrenceConstraintViolation
70            }
71            "InternalError" => InternalError,
72            "MessageTypeNotSupported" => MessageTypeNotSupported,
73            "NotImplemented" => NotImplemented,
74            "NotSupported" => NotSupported,
75            "PropertyConstraintViolation" => PropertyConstraintViolation,
76            "ProtocolError" => ProtocolError,
77            "RpcFrameworkError" => RpcFrameworkError,
78            "SecurityError" => SecurityError,
79            "TypeConstraintViolation" => TypeConstraintViolation,
80            _ => GenericError,
81        }
82    }
83}
84
85impl fmt::Display for ErrorCode {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.write_str(self.as_wire(ErrorDialect::Ocpp2))
88    }
89}
90
91/// An error to be reported to the peer as CALLERROR (or CALLRESULTERROR).
92#[derive(Debug, Clone, PartialEq)]
93pub struct RpcError {
94    pub code: ErrorCode,
95    /// Max 255 characters on the wire (2.0.1 §4.2.3); truncated when encoded.
96    pub description: String,
97    pub details: serde_json::Value,
98}
99
100impl RpcError {
101    pub fn new(code: ErrorCode, description: impl Into<String>) -> Self {
102        RpcError {
103            code,
104            description: description.into(),
105            details: serde_json::Value::Object(Default::default()),
106        }
107    }
108
109    pub fn with_details(mut self, details: serde_json::Value) -> Self {
110        self.details = details;
111        self
112    }
113
114    pub fn not_implemented(action: &str) -> Self {
115        RpcError::new(
116            ErrorCode::NotImplemented,
117            format!("Action '{action}' is not known by this CSMS"),
118        )
119    }
120
121    pub fn not_supported(action: &str) -> Self {
122        RpcError::new(
123            ErrorCode::NotSupported,
124            format!("Action '{action}' is not supported by this CSMS"),
125        )
126    }
127
128    pub fn internal(msg: impl Into<String>) -> Self {
129        RpcError::new(ErrorCode::InternalError, msg)
130    }
131}
132
133impl fmt::Display for RpcError {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(f, "{}: {}", self.code, self.description)
136    }
137}
138
139impl std::error::Error for RpcError {}
140
141impl From<crate::Violation> for RpcError {
142    fn from(v: crate::Violation) -> Self {
143        RpcError::new(v.kind.error_code(), v.to_string())
144    }
145}