codetics-ocpp-core 0.1.0

Codetics OCPP: version-independent OCPP-J building blocks (RPC framing, error codes, RFC 3339 date-time, validation, subprotocol negotiation, message ids)
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)

//! RPC framework error codes (CALLERROR / CALLRESULTERROR).
//!
//! OCPP-J 1.6 and 2.x use slightly different wire strings for the same
//! concept (1.6 keeps the historic typos `FormationViolation` and
//! `OccurenceConstraintViolation`, see the 1.6-J errata §5). The semantic
//! code is shared; the wire spelling is chosen by [`ErrorDialect`].

use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCode {
    FormatViolation,
    GenericError,
    InternalError,
    /// 2.0.1+ only.
    MessageTypeNotSupported,
    NotImplemented,
    NotSupported,
    OccurrenceConstraintViolation,
    PropertyConstraintViolation,
    ProtocolError,
    /// 2.0.1+ only.
    RpcFrameworkError,
    SecurityError,
    TypeConstraintViolation,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorDialect {
    /// OCPP-J 1.6 error code table.
    Ocpp16,
    /// OCPP-J 2.0.1 / 2.1 error code table.
    Ocpp2,
}

impl ErrorCode {
    pub fn as_wire(self, dialect: ErrorDialect) -> &'static str {
        use ErrorCode::*;
        match (self, dialect) {
            (FormatViolation, ErrorDialect::Ocpp16) => "FormationViolation",
            (FormatViolation, ErrorDialect::Ocpp2) => "FormatViolation",
            (OccurrenceConstraintViolation, ErrorDialect::Ocpp16) => "OccurenceConstraintViolation",
            (OccurrenceConstraintViolation, ErrorDialect::Ocpp2) => "OccurrenceConstraintViolation",
            // Codes that do not exist in 1.6 fall back to the closest 1.6 code.
            (MessageTypeNotSupported, ErrorDialect::Ocpp16) => "NotSupported",
            (RpcFrameworkError, ErrorDialect::Ocpp16) => "ProtocolError",
            (MessageTypeNotSupported, _) => "MessageTypeNotSupported",
            (RpcFrameworkError, _) => "RpcFrameworkError",
            (GenericError, _) => "GenericError",
            (InternalError, _) => "InternalError",
            (NotImplemented, _) => "NotImplemented",
            (NotSupported, _) => "NotSupported",
            (PropertyConstraintViolation, _) => "PropertyConstraintViolation",
            (ProtocolError, _) => "ProtocolError",
            (SecurityError, _) => "SecurityError",
            (TypeConstraintViolation, _) => "TypeConstraintViolation",
        }
    }

    /// Parses any known spelling (both dialects). Unknown codes map to `GenericError`.
    pub fn from_wire(s: &str) -> ErrorCode {
        use ErrorCode::*;
        match s {
            "FormatViolation" | "FormationViolation" => FormatViolation,
            "OccurrenceConstraintViolation" | "OccurenceConstraintViolation" => {
                OccurrenceConstraintViolation
            }
            "InternalError" => InternalError,
            "MessageTypeNotSupported" => MessageTypeNotSupported,
            "NotImplemented" => NotImplemented,
            "NotSupported" => NotSupported,
            "PropertyConstraintViolation" => PropertyConstraintViolation,
            "ProtocolError" => ProtocolError,
            "RpcFrameworkError" => RpcFrameworkError,
            "SecurityError" => SecurityError,
            "TypeConstraintViolation" => TypeConstraintViolation,
            _ => GenericError,
        }
    }
}

impl fmt::Display for ErrorCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_wire(ErrorDialect::Ocpp2))
    }
}

/// An error to be reported to the peer as CALLERROR (or CALLRESULTERROR).
#[derive(Debug, Clone, PartialEq)]
pub struct RpcError {
    pub code: ErrorCode,
    /// Max 255 characters on the wire (2.0.1 §4.2.3); truncated when encoded.
    pub description: String,
    pub details: serde_json::Value,
}

impl RpcError {
    pub fn new(code: ErrorCode, description: impl Into<String>) -> Self {
        RpcError {
            code,
            description: description.into(),
            details: serde_json::Value::Object(Default::default()),
        }
    }

    pub fn with_details(mut self, details: serde_json::Value) -> Self {
        self.details = details;
        self
    }

    pub fn not_implemented(action: &str) -> Self {
        RpcError::new(
            ErrorCode::NotImplemented,
            format!("Action '{action}' is not known by this CSMS"),
        )
    }

    pub fn not_supported(action: &str) -> Self {
        RpcError::new(
            ErrorCode::NotSupported,
            format!("Action '{action}' is not supported by this CSMS"),
        )
    }

    pub fn internal(msg: impl Into<String>) -> Self {
        RpcError::new(ErrorCode::InternalError, msg)
    }
}

impl fmt::Display for RpcError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.code, self.description)
    }
}

impl std::error::Error for RpcError {}

impl From<crate::Violation> for RpcError {
    fn from(v: crate::Violation) -> Self {
        RpcError::new(v.kind.error_code(), v.to_string())
    }
}