codetics-ocpp-service 0.1.0

Codetics OCPP: Charging Station Management System (CSMS) service for OCPP 1.6J, 2.0.1 and 2.1, with TLS security profiles, smart charging, reservations and tariffs
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)

//! HTTP API error type.

use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};

#[derive(Debug)]
pub enum AppError {
    BadRequest(String),
    NotFound(String),
    Conflict(String),
    Unauthorized,
    /// The station is not connected to this node (it may be connected to `node`).
    NotConnected {
        node: Option<String>,
    },
    Unavailable(String),
    GatewayTimeout(String),
    /// The station answered with a CALLERROR.
    Upstream(ocpp_core::RpcError),
    Internal(String),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{self:?}")
    }
}

impl From<sqlx::Error> for AppError {
    fn from(e: sqlx::Error) -> Self {
        if let sqlx::Error::Database(db) = &e
            && db.is_unique_violation()
        {
            return AppError::Conflict(db.message().to_owned());
        }
        if let sqlx::Error::Database(db) = &e
            && db.is_foreign_key_violation()
        {
            return AppError::Conflict(format!("still referenced: {}", db.message()));
        }
        if let sqlx::Error::Database(db) = &e
            && db.is_check_violation()
        {
            return AppError::BadRequest(db.message().to_owned());
        }
        AppError::Internal(e.to_string())
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, body) = match self {
            AppError::BadRequest(m) => (
                StatusCode::BAD_REQUEST,
                serde_json::json!({ "error": "bad_request", "message": m }),
            ),
            AppError::NotFound(m) => (
                StatusCode::NOT_FOUND,
                serde_json::json!({ "error": "not_found", "message": m }),
            ),
            AppError::Conflict(m) => (
                StatusCode::CONFLICT,
                serde_json::json!({ "error": "conflict", "message": m }),
            ),
            AppError::Unauthorized => (
                StatusCode::UNAUTHORIZED,
                serde_json::json!({ "error": "unauthorized" }),
            ),
            AppError::NotConnected { node } => (
                StatusCode::CONFLICT,
                serde_json::json!({ "error": "station_not_connected", "message": "station is not connected to this node", "connected_node": node }),
            ),
            AppError::Unavailable(m) => (
                StatusCode::SERVICE_UNAVAILABLE,
                serde_json::json!({ "error": "unavailable", "message": m }),
            ),
            AppError::GatewayTimeout(m) => (
                StatusCode::GATEWAY_TIMEOUT,
                serde_json::json!({ "error": "timeout", "message": m }),
            ),
            AppError::Upstream(e) => (
                StatusCode::BAD_GATEWAY,
                serde_json::json!({ "error": "call_error", "error_code": e.code.to_string(), "description": e.description, "details": e.details }),
            ),
            AppError::Internal(m) => {
                tracing::error!(error = %m, "internal error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    serde_json::json!({ "error": "internal" }),
                )
            }
        };
        (status, Json(body)).into_response()
    }
}