codetics-ocpp201 0.1.0

Codetics OCPP: OCPP 2.0.1 message types with validation and typed dispatch
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)

//! OCPP 2.0.1 protocol messages (Edition 4).
//!
//! Pure Rust definitions of every OCPP 2.0.1 message. Type names are the
//! specification datatype names without their trailing `Type`
//! (e.g. `ChargingProfileType` → [`ChargingProfile`]).
//!
//! - [`enums`], [`datatypes`], [`messages`]: payload types with serde mappings
//!   and [`ocpp_core::Validate`] impls for length / range / cardinality constraints.
//! - [`actions`]: [`Action`] with message direction, and the typed
//!   [`StationCall`] / [`CsmsCall`] dispatch enums.

pub mod actions;
pub mod datatypes;
pub mod enums;
pub mod messages;

pub use actions::*;
pub use datatypes::*;
pub use enums::*;
pub use messages::*;

/// WebSocket subprotocol of this version.
pub const SUBPROTOCOL: &str = "ocpp2.0.1";

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn decode_spec_boot_notification() {
        let c = StationCall::decode("BootNotification", r#"{"reason": "PowerUp", "chargingStation": {"model": "SingleSocketCharger", "vendorName": "VendorX"}}"#).unwrap();
        let StationCall::BootNotification(b) = c else {
            panic!()
        };
        assert_eq!(b.reason, BootReasonEnum::PowerUp);
        assert_eq!(b.charging_station.vendor_name, "VendorX");
    }

    #[test]
    fn custom_data_is_extensible() {
        let p = r#"{"customData":{"vendorId":"com.mycompany.customheartbeat","mainMeterValue":12345,"sessionsToDate":342}}"#;
        let StationCall::Heartbeat(h) = StationCall::decode("Heartbeat", p).unwrap() else {
            panic!()
        };
        let cd = h.custom_data.unwrap();
        assert_eq!(cd.vendor_id, "com.mycompany.customheartbeat");
        assert_eq!(cd.extra["sessionsToDate"], 342);
        // ...but other objects reject unknown properties.
        assert!(StationCall::decode("Heartbeat", r#"{"foo":1}"#).is_err());
    }

    #[test]
    fn transaction_event_and_min_items() {
        let ok = r#"{"eventType":"Started","timestamp":"2024-05-01T10:00:00Z","triggerReason":"Authorized","seqNo":0,
            "transactionInfo":{"transactionId":"tx-1"},"idToken":{"idToken":"ABC","type":"ISO14443"},"evse":{"id":1,"connectorId":1},
            "meterValue":[{"timestamp":"2024-05-01T10:00:00Z","sampledValue":[{"value":12.5}]}]}"#;
        assert!(StationCall::decode("TransactionEvent", ok).is_ok());
        let empty_samples = ok.replace(r#"[{"value":12.5}]"#, "[]");
        let Err(DecodeError::Invalid(v)) = StationCall::decode("TransactionEvent", &empty_samples)
        else {
            panic!()
        };
        assert_eq!(v.kind, ocpp_core::ViolationKind::Occurrence);
        assert!(
            v.message.contains("$.meterValue[0].sampledValue"),
            "{}",
            v.message
        );
    }

    #[test]
    fn data_transfer_any_data() {
        assert!(
            StationCall::decode("DataTransfer", r#"{"vendorId":"x","data":{"a":[1,2]}}"#).is_ok()
        );
        assert!(StationCall::decode("DataTransfer", r#"{"vendorId":"x","data":"text"}"#).is_ok());
    }
}

/// Compiles and runs the examples of the README.
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
pub struct ReadmeDoctests;