Skip to main content

energy_api/models/
directory.rs

1//! Wire-format types for the EDI-Energy Directory Service v1.
2//!
3//! Derived from:
4//! - `directoryServiceV1.yaml` (OpenAPI 3.0.1)
5//! - `webSocketV1.yaml` (AsyncAPI 3.0.0)
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10use time::OffsetDateTime;
11use url::Url;
12
13// ── Core record types ─────────────────────────────────────────────────────────
14
15/// Operational status of an API endpoint registered in the directory.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub enum ApiStatus {
18    /// The API is not available and cannot be called.
19    Offline,
20    /// The API accepts requests but performs no real processing (interop testing).
21    Test,
22    /// The API is temporarily unavailable for maintenance.
23    Maintenance,
24    /// The API is available and fully operational.
25    Online,
26}
27
28/// A directory entry describing how to reach one major version of an API service.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct ApiRecord {
32    /// Unique identifier of the responsible API provider (OU from EMT.API cert).
33    pub provider_id: String,
34    /// Unique identifier of the API service (e.g. `controlMeasuresV1`).
35    pub api_id: String,
36    /// Major version of the API service.
37    pub major_version: i32,
38    /// Base URL of the API endpoint.
39    pub url: Url,
40    /// Optional supplementary key-value metadata defined by the service spec.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub additional_metadata: Option<HashMap<String, String>>,
43    /// Timestamp of the last update to this record (RFC 3339 / ISO 8601).
44    #[serde(with = "time::serde::rfc3339")]
45    pub last_updated: OffsetDateTime,
46    /// Monotonically increasing revision counter; starts at 1.
47    pub revision: i64,
48    /// Current operational status of the registered endpoint.
49    pub status: ApiStatus,
50}
51
52/// A lightweight reference to a directory entry, used in subscriptions
53/// and notifications.
54#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct ApiRecordRef {
57    /// 13-digit Marktpartner-ID of the API provider (OU from EMT.API cert).
58    pub provider_id: String,
59    /// API service identifier (e.g. `controlMeasuresV1`).
60    pub api_id: String,
61    /// Major version of the API service.
62    pub major_version: i32,
63}
64
65/// A directory entry together with its JWS signature and the signing certificate.
66///
67/// Received in WebSocket [`DirectoryNotification::modified`] messages.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct SignedApiRecord {
71    /// The signed directory entry (canonical JSON / RFC 8785 was the payload).
72    pub content: ApiRecord,
73    /// Base64url-encoded JWS Signature value (`X-BDEW-SIGNATURE`).
74    /// Reconstruct the full JWS via [`crate::transport::jws`].
75    pub signature: String,
76    /// Signing certificate encoded per RFC 9440 (`X-BDEW-CERT`).
77    pub signing_cert: String,
78}
79
80// ── Service information ───────────────────────────────────────────────────────
81
82/// Contact details of the directory service technical operator.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ContactInfo {
86    /// Support e-mail (at least one of `email`/`phone` must be present).
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub email: Option<String>,
89    /// Support phone number.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub phone: Option<String>,
92}
93
94/// Service-level information about a running directory service instance.
95///
96/// Returned by `GET /info/service/v1` and included in WebSocket notifications.
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ServiceInfo {
100    /// Fully-qualified version of the implemented interface (e.g. `1.0.0`).
101    pub version: String,
102    /// Contact information for the technical operator.
103    pub contact: ContactInfo,
104    /// Timestamp of the last update to this service-info object.
105    #[serde(with = "time::serde::rfc3339")]
106    pub last_updated: OffsetDateTime,
107    /// Monotonically increasing revision counter; starts at 1.
108    pub revision: i64,
109}
110
111// ── WebSocket subscription protocol ──────────────────────────────────────────
112
113/// Message sent by the **client** to manage its subscriptions.
114///
115/// Sent over the WebSocket channel `/ws/subscriptions/v1`.
116/// The server responds with a [`DirectoryNotification`].
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct SubscriptionRequest {
120    /// Client-chosen correlation ID, echoed back in the response notification.
121    pub id: String,
122    /// Subscriptions to add.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub requested: Option<Vec<SubscriptionItem>>,
125    /// Subscriptions to cancel.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub canceled: Option<Vec<ApiRecordRef>>,
128}
129
130/// One item in a subscription request.
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct SubscriptionItem {
134    /// The directory entry to subscribe to.
135    pub record_ref: ApiRecordRef,
136    /// Last-known revision at the client. `0` or absent means no local copy.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub known_revision: Option<i64>,
139}
140
141/// Message sent by the **server** to notify the client of directory changes.
142///
143/// Received over the WebSocket channel `/ws/subscriptions/v1`.
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct DirectoryNotification {
147    /// Echoed subscription request ID (set when responding to a subscribe/cancel request).
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub subscription_id: Option<String>,
150    /// UTC timestamp when this notification was generated (ISO 8601).
151    #[serde(with = "time::serde::rfc3339")]
152    pub timestamp: OffsetDateTime,
153    /// Current service information (included on first connect or when it changes).
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub service_info: Option<ServiceInfo>,
156    /// Directory entries that were added or updated.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub modified: Option<Vec<SignedApiRecord>>,
159    /// Redirect configurations for directory entries.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub redirected: Option<Vec<RedirectInfo>>,
162    /// References to directory entries that were deleted.
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub deleted: Option<Vec<ApiRecordRef>>,
165    /// Subscriptions confirmed as canceled (by client or by server).
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub canceled: Option<Vec<CanceledSubscription>>,
168    /// Error information — mutually exclusive with the change fields above.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub error: Option<NotificationError>,
171}
172
173/// Redirect target for a directory entry.
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct RedirectInfo {
177    /// The entry for which a redirect is configured (or was removed).
178    pub record_ref: ApiRecordRef,
179    /// Configured redirect target. `None` when the redirect was removed.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub url: Option<Url>,
182}
183
184/// A subscription that was confirmed as canceled.
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct CanceledSubscription {
188    /// Reference to the canceled directory entry subscription.
189    pub record_ref: ApiRecordRef,
190    /// `true` if the client initiated the cancel; `false` if server-initiated.
191    pub canceled_by_client: bool,
192    /// Human-readable reason (required for server-initiated cancellations).
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub reason: Option<String>,
195}
196
197/// Error payload in a [`DirectoryNotification`].
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct NotificationError {
201    /// HTTP status code describing the error.
202    pub status_code: u32,
203    /// Human-readable description.
204    pub description: String,
205    /// Base64-encoded original [`SubscriptionRequest`] that triggered the error
206    /// (present when the error arose from a subscribe operation and
207    /// `subscription_id` could not be extracted from the request).
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub request: Option<String>,
210}