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    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}