Skip to main content

openleadr_wire/
subscription.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4};
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use serde_with::{serde_as, skip_serializing_none};
9use validator::Validate;
10
11use crate::{
12    ClientId, Event, Identifier, IdentifierError, ObjectType, Program, Report, Ven,
13    program::ProgramId, resource::Resource, resource_group::ResourceGroup,
14};
15
16/// Server provided representation of subscription
17#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
18#[serde(rename_all = "camelCase")]
19pub struct Subscription {
20    /// URL safe VTN assigned object ID.
21    pub id: SubscriptionId,
22    /// datetime in ISO 8601 format
23    #[serde(with = "crate::serde_rfc3339")]
24    pub created_date_time: DateTime<Utc>,
25    /// datetime in ISO 8601 format
26    #[serde(with = "crate::serde_rfc3339")]
27    pub modification_date_time: DateTime<Utc>,
28    pub client_id: ClientId,
29    #[serde(flatten)]
30    #[validate(nested)]
31    pub content: SubscriptionRequest,
32}
33
34/// An object created by a client to receive notification of operations on objects.
35/// Clients may subscribe to be notified when a type of object is created,
36/// updated, or deleted.
37#[skip_serializing_none]
38#[serde_as]
39#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
40#[serde(rename_all = "camelCase")]
41pub struct SubscriptionRequest {
42    /// User generated identifier, may be VEN identifier provisioned out-of-band.
43    #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")]
44    pub client_name: String,
45
46    /// ID attribute of the program object this subscription is associated with.
47    #[serde(rename = "programID")]
48    pub program_id: Option<ProgramId>,
49
50    /// list of objects and operations to subscribe to.
51    #[validate(length(min = 1, max = 15))]
52    pub object_operations: Vec<SubscriptionObjectOperation>,
53    // /// A list of target objects. Used by server to filter notifications.
54    // #[serde(default)]
55    // #[serde_as(deserialize_as = "DefaultOnNull")]
56    // pub targets: Vec<Target>,
57}
58
59#[skip_serializing_none]
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
61#[serde(rename_all = "camelCase")]
62pub struct SubscriptionObjectOperation {
63    /// list of objects to subscribe to.
64    pub objects: Vec<ObjectType>,
65
66    /// list of operations to subscribe to.
67    pub operations: Vec<Operation>,
68
69    /// The transport mechanism used to deliver the notification
70    #[serde(default)]
71    pub mechanism: NotificationMechanism,
72
73    /// User provided webhook URL. Required if `mechanism` is "WEBHOOK"
74    pub callback_url: Option<String>,
75
76    /// User provided token.
77    /// To avoid custom integrations, callback endpoints
78    /// should accept the provided bearer token to authenticate VTN requests.
79    pub bearer_token: Option<String>,
80}
81
82#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
83#[serde(rename_all = "UPPERCASE")]
84pub enum Operation {
85    Create,
86    Update,
87    Delete,
88}
89
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
91#[serde(rename_all = "UPPERCASE")]
92pub enum NotificationMechanism {
93    #[default]
94    Webhook,
95    Websocket,
96}
97
98/// URL safe VTN assigned object ID
99#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq)]
100pub struct SubscriptionId(pub(crate) Identifier);
101
102impl Display for SubscriptionId {
103    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{}", self.0)
105    }
106}
107
108impl SubscriptionId {
109    pub fn as_str(&self) -> &str {
110        self.0.as_str()
111    }
112}
113
114impl FromStr for SubscriptionId {
115    type Err = IdentifierError;
116
117    fn from_str(s: &str) -> Result<Self, Self::Err> {
118        Ok(Self(s.parse()?))
119    }
120}
121
122/// VTN generated object included in request to subscription callbackUrl.
123#[skip_serializing_none]
124#[serde_as]
125#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
126#[serde(rename_all = "camelCase")]
127pub struct Notification {
128    /// A unique ID of the operation that triggered this notification.
129    /// Used to acknowledge receiving a notification over websockets and to allow a VEN to deduplicate notifications.
130    /// A duplication could happend due to multiple subscriptions of (partly) overlying operations, possibly over different
131    /// notification mechanisms, or if a webhook call returns an error code and gets therefore retried, for example.
132    ///
133    /// Note that this an ID of the operation (create, update, ...) that triggerd this notification. This means,
134    /// multiple subscribers can get the same ID, possibly over different notification channels (webhook, websocket, MQTT).
135    ///
136    /// The exact structure of this ID is up to the implementation, but using a counter or UUID is RECOMMENDED.
137    pub id: Identifier,
138
139    /// the operation on on object that triggered the notification.
140    pub operation: Operation,
141
142    /// the object that is the subject of the notification.
143    #[serde(flatten)]
144    pub object: AnyObject,
145    // /// A list of targets.
146    // #[serde(default)]
147    // #[serde_as(deserialize_as = "DefaultOnNull")]
148    // pub targets: Vec<Target>,
149}
150
151#[skip_serializing_none]
152#[serde_as]
153#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
154#[serde(rename_all = "camelCase")]
155pub struct MqttPushNotification {
156    #[serde(rename = "ID")]
157    pub id: Identifier,
158    #[serde(rename = "notificationID")]
159    pub notification_id: Identifier,
160    pub object_type: ObjectType,
161    pub operation: Operation,
162    #[serde(with = "crate::serde_rfc3339")]
163    pub notification_date_time: DateTime<Utc>,
164}
165
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
167#[serde(tag = "objectType", content = "object", rename_all = "UPPERCASE")]
168pub enum AnyObject {
169    Program(Program),
170    Report(Report),
171    Event(Event),
172    Subscription(Subscription),
173    Ven(Ven),
174    Resource(Resource),
175    ResourceGroup(ResourceGroup),
176}
177
178impl AnyObject {
179    pub fn id(&self) -> Identifier {
180        match self {
181            AnyObject::Program(program) => program.id.0.clone(),
182            AnyObject::Report(report) => report.id.0.clone(),
183            AnyObject::Event(event) => event.id.0.clone(),
184            AnyObject::Subscription(subscription) => subscription.id.0.clone(),
185            AnyObject::Ven(ven) => ven.id.0.clone(),
186            AnyObject::Resource(resource) => resource.id.0.clone(),
187            AnyObject::ResourceGroup(resource_group) => resource_group.id.0.clone(),
188        }
189    }
190
191    pub fn kind(&self) -> ObjectType {
192        match self {
193            AnyObject::Program(_) => ObjectType::Program,
194            AnyObject::Report(_) => ObjectType::Report,
195            AnyObject::Event(_) => ObjectType::Event,
196            AnyObject::Subscription(_) => ObjectType::Subscription,
197            AnyObject::Ven(_) => ObjectType::Ven,
198            AnyObject::Resource(_) => ObjectType::Resource,
199            AnyObject::ResourceGroup(_) => ObjectType::ResourceGroup,
200        }
201    }
202}
203
204/// Provides details of each notifier binding supported
205#[skip_serializing_none]
206#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
207#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
208pub struct NotifiersResponse {
209    pub websocket: bool,
210    pub mqtt: Option<MqttNotifierBindingObject>,
211    pub push_mqtt: Option<MqttNotifierBindingObject>,
212}
213
214/// Details of MQTT binding for messaging protocol support
215#[skip_serializing_none]
216#[serde_as]
217#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
218#[serde(rename_all = "camelCase")]
219pub struct MqttNotifierBindingObject {
220    /// URIs for connection to MQTT broker
221    #[serde(rename = "URIS")]
222    pub uris: Vec<String>,
223    /// Currently always JSON, perhaps other formats supported in future
224    pub serialization: SerializationType,
225    pub authentication: MqttNotifierAuthentication,
226}
227
228/// MQTT broker authentication details
229#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
230#[serde(rename_all = "UPPERCASE", rename_all_fields = "camelCase")]
231#[serde(tag = "method")]
232pub enum MqttNotifierAuthentication {
233    /// Specifies anonymous authentication
234    Anonymous,
235    /// Specifies OAuth2 bearer token authentication
236    Oauth2BearerToken {
237        /// Either the distinguished string "{clientID}", or any other literal string
238        username: String,
239    },
240}
241
242#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
243#[serde(rename_all = "UPPERCASE")]
244pub enum SerializationType {
245    Json,
246}
247
248/// Details of MQTT binding for messaging protocol support
249#[skip_serializing_none]
250#[serde_as]
251#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub struct NotifierTopicsResponse {
254    pub topics: NotifierOperationsTopics,
255}
256
257/// MQTT notifier topic names for notifications of subscribable-object operations
258#[skip_serializing_none]
259#[serde_as]
260#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
261#[serde(rename_all = "UPPERCASE")]
262pub struct NotifierOperationsTopics {
263    /// Topic path for CREATE operations,
264    /// not provided for notifications for a specific object ID,
265    /// e.g. until programID foo is created, clients unable to
266    /// request notifications of its creation'
267    pub create: Option<String>,
268    /// Topic path for UPDATE operations
269    pub update: String,
270    /// Topic path for DELETE operations
271    pub delete: String,
272    /// Topic path for ALL operations, if supported by VTN
273    pub all: Option<String>,
274}
275
276#[cfg(test)]
277mod tests {
278    use crate::program::ProgramRequest;
279
280    use super::*;
281
282    #[test]
283    fn parse_subscription_request() {
284        let example = r#"{
285  "clientName": "myClient",
286  "programID": "44",
287  "objectOperations": [
288    {
289      "callbackUrl": "https://myserver.com/event_callbacks",
290      "operations": [
291        "CREATE",
292        "UPDATE"
293      ],
294      "objects": [
295        "EVENT"
296      ]
297    },
298    {
299      "callbackUrl": "https://myserver.com/program_callbacks",
300      "operations": [
301        "CREATE",
302        "UPDATE"
303      ],
304      "objects": [
305        "PROGRAM"
306      ]
307    }
308  ]
309}"#;
310        assert_eq!(
311            serde_json::from_str::<SubscriptionRequest>(example).unwrap(),
312            SubscriptionRequest {
313                client_name: "myClient".to_owned(),
314                program_id: Some("44".parse().unwrap()),
315                object_operations: vec![
316                    SubscriptionObjectOperation {
317                        objects: vec![ObjectType::Event],
318                        operations: vec![Operation::Create, Operation::Update],
319                        mechanism: NotificationMechanism::Webhook,
320                        callback_url: Some("https://myserver.com/event_callbacks".to_owned()),
321                        bearer_token: None,
322                    },
323                    SubscriptionObjectOperation {
324                        objects: vec![ObjectType::Program],
325                        operations: vec![Operation::Create, Operation::Update],
326                        mechanism: NotificationMechanism::Webhook,
327                        callback_url: Some("https://myserver.com/program_callbacks".to_owned()),
328                        bearer_token: None,
329                    }
330                ],
331                // targets: vec![],
332            }
333        );
334    }
335
336    #[test]
337    fn parse_notification() {
338        let example = r#"{
339  "id": "100",
340  "objectType": "PROGRAM",
341  "operation": "UPDATE",
342  "object": {
343    "bindingEvents": false,
344    "createdDateTime": "2023-06-15T15:51:29.000Z",
345    "modificationDateTime": "2023-06-15T15:51:29.000Z",
346    "id": "0",
347    "localPrice": false,
348    "objectType": "PROGRAM",
349    "programName": "myProgram"
350  }
351}"#;
352        assert_eq!(
353            serde_json::from_str::<Notification>(example).unwrap(),
354            Notification {
355                id: "100".parse().unwrap(),
356                operation: Operation::Update,
357                object: AnyObject::Program(Program {
358                    id: "0".parse().unwrap(),
359                    created_date_time: "2023-06-15T15:51:29.000Z".parse().unwrap(),
360                    modification_date_time: "2023-06-15T15:51:29.000Z".parse().unwrap(),
361                    content: ProgramRequest {
362                        program_name: "myProgram".to_owned(),
363                        interval_period: None,
364                        program_descriptions: None,
365                        payload_descriptors: None,
366                        attributes: None,
367                        targets: vec![],
368                    }
369                }),
370                // targets: vec![],
371            }
372        );
373    }
374}