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 pub provider_id: String,
58 pub api_id: String,
59 pub major_version: i32,
60}
61
62/// A directory entry together with its JWS signature and the signing certificate.
63///
64/// Received in WebSocket [`DirectoryNotification::modified`] messages.
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct SignedApiRecord {
68 /// The signed directory entry (canonical JSON / RFC 8785 was the payload).
69 pub content: ApiRecord,
70 /// Base64url-encoded JWS Signature value (`X-BDEW-SIGNATURE`).
71 /// Reconstruct the full JWS via [`crate::transport::jws`].
72 pub signature: String,
73 /// Signing certificate encoded per RFC 9440 (`X-BDEW-CERT`).
74 pub signing_cert: String,
75}
76
77// ── Service information ───────────────────────────────────────────────────────
78
79/// Contact details of the directory service technical operator.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct ContactInfo {
83 /// Support e-mail (at least one of `email`/`phone` must be present).
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub email: Option<String>,
86 /// Support phone number.
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub phone: Option<String>,
89}
90
91/// Service-level information about a running directory service instance.
92///
93/// Returned by `GET /info/service/v1` and included in WebSocket notifications.
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct ServiceInfo {
97 /// Fully-qualified version of the implemented interface (e.g. `1.0.0`).
98 pub version: String,
99 /// Contact information for the technical operator.
100 pub contact: ContactInfo,
101 /// Timestamp of the last update to this service-info object.
102 #[serde(with = "time::serde::rfc3339")]
103 pub last_updated: OffsetDateTime,
104 /// Monotonically increasing revision counter; starts at 1.
105 pub revision: i64,
106}
107
108// ── WebSocket subscription protocol ──────────────────────────────────────────
109
110/// Message sent by the **client** to manage its subscriptions.
111///
112/// Sent over the WebSocket channel `/ws/subscriptions/v1`.
113/// The server responds with a [`DirectoryNotification`].
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct SubscriptionRequest {
117 /// Client-chosen correlation ID, echoed back in the response notification.
118 pub id: String,
119 /// Subscriptions to add.
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub requested: Option<Vec<SubscriptionItem>>,
122 /// Subscriptions to cancel.
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub canceled: Option<Vec<ApiRecordRef>>,
125}
126
127/// One item in a subscription request.
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct SubscriptionItem {
131 /// The directory entry to subscribe to.
132 pub record_ref: ApiRecordRef,
133 /// Last-known revision at the client. `0` or absent means no local copy.
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub known_revision: Option<i64>,
136}
137
138/// Message sent by the **server** to notify the client of directory changes.
139///
140/// Received over the WebSocket channel `/ws/subscriptions/v1`.
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct DirectoryNotification {
144 /// Echoed subscription request ID (set when responding to a subscribe/cancel request).
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub subscription_id: Option<String>,
147 /// UTC timestamp when this notification was generated (ISO 8601).
148 #[serde(with = "time::serde::rfc3339")]
149 pub timestamp: OffsetDateTime,
150 /// Current service information (included on first connect or when it changes).
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub service_info: Option<ServiceInfo>,
153 /// Directory entries that were added or updated.
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub modified: Option<Vec<SignedApiRecord>>,
156 /// Redirect configurations for directory entries.
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub redirected: Option<Vec<RedirectInfo>>,
159 /// References to directory entries that were deleted.
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub deleted: Option<Vec<ApiRecordRef>>,
162 /// Subscriptions confirmed as canceled (by client or by server).
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub canceled: Option<Vec<CanceledSubscription>>,
165 /// Error information — mutually exclusive with the change fields above.
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub error: Option<NotificationError>,
168}
169
170/// Redirect target for a directory entry.
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct RedirectInfo {
174 /// The entry for which a redirect is configured (or was removed).
175 pub record_ref: ApiRecordRef,
176 /// Configured redirect target. `None` when the redirect was removed.
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub url: Option<Url>,
179}
180
181/// A subscription that was confirmed as canceled.
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct CanceledSubscription {
185 pub record_ref: ApiRecordRef,
186 /// `true` if the client initiated the cancel; `false` if server-initiated.
187 pub canceled_by_client: bool,
188 /// Human-readable reason (required for server-initiated cancellations).
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub reason: Option<String>,
191}
192
193/// Error payload in a [`DirectoryNotification`].
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct NotificationError {
197 /// HTTP status code describing the error.
198 pub status_code: u32,
199 /// Human-readable description.
200 pub description: String,
201 /// Base64-encoded original [`SubscriptionRequest`] that triggered the error
202 /// (present when the error arose from a subscribe operation and
203 /// `subscription_id` could not be extracted from the request).
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub request: Option<String>,
206}