Skip to main content

netgauze_yang_push/model/
telemetry.rs

1// Copyright (C) 2025-present The NetGauze Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12// implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! # Telemetry Message Module
17//!
18//! This module defines the structure and serialization logic for telemetry
19//! messages as specified in:
20//! - [Telemetry Message](https://datatracker.ietf.org/doc/html/draft-netana-nmop-message-broker-telemetry-message).
21//!
22//! Key components include:
23//!
24//! - **TelemetryMessage**: The main structure representing a telemetry message.
25//!
26//! - **TelemetryMessageWrapper**: A wrapper for the telemetry message needed
27//!   for including the module namespace.
28//!
29//! - **Manifest**: Common metadata structure for both network nodes and data
30//!   collection systems, including information such as name, vendor, software
31//!   version, etc.
32//!
33//! - **TelemetryMessageMetadata**: Metadata about the telemetry session between
34//!   collector and network node, including timestamps, protocol information,
35//!   and addressing details.
36//!
37//! - **YangPushSubscriptionMetadata**: YANG-Push specific extension for
38//!   subscription details when the session protocol is YANG Push.
39//!
40//! - **NetworkOperatorMetadata**: Operator-specific metadata implemented as
41//!   key-value labels that can be used to enrich collected data with custom
42//!   information.
43use chrono::{DateTime, Utc};
44
45use crate::cache::storage::SubscriptionInfo;
46use netgauze_netconf_proto::yang_push::identities::{ChangeType, Encoding, Transport};
47use netgauze_netconf_proto::yang_push::subscription::YangPushModuleVersion;
48use netgauze_netconf_proto::yang_push::types::{CentiSeconds, SubscriptionId};
49use netgauze_udp_notif_pkt::notification::Target;
50use serde::{Deserialize, Serialize};
51use serde_json::Value;
52use std::net::IpAddr;
53
54/// Telemetry Message Wrapper
55#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
56pub struct TelemetryMessageWrapper {
57    #[serde(rename = "ietf-telemetry-message:message")]
58    message: TelemetryMessage,
59}
60
61impl TelemetryMessageWrapper {
62    pub const fn new(message: TelemetryMessage) -> Self {
63        Self { message }
64    }
65    pub const fn message(&self) -> &TelemetryMessage {
66        &self.message
67    }
68}
69
70/// Telemetry Message
71#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
72#[serde(rename_all = "kebab-case")]
73pub struct TelemetryMessage {
74    #[serde(skip_serializing_if = "Option::is_none")]
75    network_node_manifest: Option<Manifest>,
76
77    telemetry_message_metadata: TelemetryMessageMetadata,
78
79    #[serde(skip_serializing_if = "Option::is_none")]
80    data_collection_manifest: Option<Manifest>,
81
82    #[serde(skip_serializing_if = "Option::is_none")]
83    network_operator_metadata: Option<NetworkOperatorMetadata>,
84
85    #[serde(skip_serializing_if = "Option::is_none")]
86    payload: Option<Value>,
87}
88
89impl TelemetryMessage {
90    pub const fn new(
91        network_node_manifest: Option<Manifest>,
92        telemetry_message_metadata: TelemetryMessageMetadata,
93        data_collection_manifest: Option<Manifest>,
94        network_operator_metadata: Option<NetworkOperatorMetadata>,
95        payload: Option<Value>,
96    ) -> Self {
97        Self {
98            network_node_manifest,
99            telemetry_message_metadata,
100            data_collection_manifest,
101            network_operator_metadata,
102            payload,
103        }
104    }
105    pub const fn network_node_manifest(&self) -> Option<&Manifest> {
106        self.network_node_manifest.as_ref()
107    }
108    pub const fn data_collection_manifest(&self) -> Option<&Manifest> {
109        self.data_collection_manifest.as_ref()
110    }
111    pub const fn telemetry_message_metadata(&self) -> &TelemetryMessageMetadata {
112        &self.telemetry_message_metadata
113    }
114    pub const fn network_operator_metadata(&self) -> Option<&NetworkOperatorMetadata> {
115        self.network_operator_metadata.as_ref()
116    }
117    pub const fn payload(&self) -> Option<&Value> {
118        self.payload.as_ref()
119    }
120}
121
122/// Telemetry Session Protocol Type
123#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
124pub enum SessionProtocol {
125    #[serde(rename = "yang-push")]
126    YangPush,
127
128    #[serde(rename = "netconf")]
129    Netconf,
130
131    #[serde(rename = "restconf")]
132    Restconf,
133
134    #[default]
135    #[serde(other)]
136    #[serde(rename = "unknown")]
137    Unknown,
138}
139
140/// Telemetry Notification Event Type
141#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
142pub enum EventType {
143    #[serde(rename = "log")]
144    Log,
145
146    #[serde(rename = "update")]
147    Update,
148
149    #[serde(rename = "delete")]
150    Delete,
151}
152
153/// Generic Metadata Manifest
154#[derive(Default, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
155#[serde(rename_all = "kebab-case")]
156pub struct Manifest {
157    #[serde(skip_serializing_if = "Option::is_none")]
158    name: Option<String>,
159
160    #[serde(skip_serializing_if = "Option::is_none")]
161    vendor: Option<String>,
162
163    #[serde(skip_serializing_if = "Option::is_none")]
164    vendor_pen: Option<u32>,
165
166    #[serde(skip_serializing_if = "Option::is_none")]
167    software_version: Option<String>,
168
169    #[serde(skip_serializing_if = "Option::is_none")]
170    software_flavor: Option<String>,
171
172    #[serde(skip_serializing_if = "Option::is_none")]
173    os_version: Option<String>,
174
175    #[serde(skip_serializing_if = "Option::is_none")]
176    os_type: Option<String>,
177}
178
179impl Manifest {
180    pub const fn new(
181        name: Option<String>,
182        vendor: Option<String>,
183        vendor_pen: Option<u32>,
184        software_version: Option<String>,
185        software_flavor: Option<String>,
186        os_version: Option<String>,
187        os_type: Option<String>,
188    ) -> Self {
189        Self {
190            name,
191            vendor,
192            vendor_pen,
193            software_version,
194            software_flavor,
195            os_version,
196            os_type,
197        }
198    }
199}
200
201/// Telemetry Notification Metadata
202#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
203#[serde(rename_all = "kebab-case")]
204pub struct TelemetryMessageMetadata {
205    #[serde(skip_serializing_if = "Option::is_none")]
206    node_export_timestamp: Option<DateTime<Utc>>,
207
208    collection_timestamp: DateTime<Utc>,
209
210    notification_event: EventType,
211
212    #[serde(skip_serializing_if = "Option::is_none")]
213    sequence_number: Option<u32>,
214
215    session_protocol: SessionProtocol,
216
217    export_address: IpAddr,
218
219    #[serde(skip_serializing_if = "Option::is_none")]
220    export_port: Option<u16>,
221
222    #[serde(skip_serializing_if = "Option::is_none")]
223    collection_address: Option<IpAddr>,
224
225    #[serde(skip_serializing_if = "Option::is_none")]
226    collection_port: Option<u16>,
227
228    #[serde(skip_serializing_if = "Option::is_none")]
229    #[serde(rename = "ietf-yang-push-telemetry-message:yang-push-subscription")]
230    yang_push_subscription: Option<YangPushSubscriptionMetadata>,
231}
232
233impl TelemetryMessageMetadata {
234    #[allow(clippy::too_many_arguments)]
235    pub const fn new(
236        node_export_timestamp: Option<DateTime<Utc>>,
237        collection_timestamp: DateTime<Utc>,
238        notification_event: EventType,
239        sequence_number: Option<u32>,
240        session_protocol: SessionProtocol,
241        export_address: IpAddr,
242        export_port: Option<u16>,
243        collection_address: Option<IpAddr>,
244        collection_port: Option<u16>,
245        yang_push_subscription: Option<YangPushSubscriptionMetadata>,
246    ) -> Self {
247        Self {
248            node_export_timestamp,
249            collection_timestamp,
250            notification_event,
251            sequence_number,
252            session_protocol,
253            export_address,
254            export_port,
255            collection_address,
256            collection_port,
257            yang_push_subscription,
258        }
259    }
260    pub const fn node_export_timestamp(&self) -> Option<DateTime<Utc>> {
261        self.node_export_timestamp
262    }
263    pub const fn collection_timestamp(&self) -> DateTime<Utc> {
264        self.collection_timestamp
265    }
266    pub const fn notification_event(&self) -> EventType {
267        self.notification_event
268    }
269    pub const fn sequence_number(&self) -> Option<u32> {
270        self.sequence_number
271    }
272    pub const fn session_protocol(&self) -> &SessionProtocol {
273        &self.session_protocol
274    }
275    pub const fn export_address(&self) -> IpAddr {
276        self.export_address
277    }
278    pub const fn export_port(&self) -> Option<u16> {
279        self.export_port
280    }
281    pub const fn collection_address(&self) -> Option<IpAddr> {
282        self.collection_address
283    }
284    pub const fn collection_port(&self) -> Option<u16> {
285        self.collection_port
286    }
287    pub const fn yang_push_subscription(&self) -> Option<&YangPushSubscriptionMetadata> {
288        self.yang_push_subscription.as_ref()
289    }
290}
291
292#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
293#[serde(rename_all = "kebab-case")]
294pub struct YangPushSubscriptionMetadata {
295    #[serde(skip_serializing_if = "Option::is_none")]
296    id: Option<SubscriptionId>,
297
298    #[serde(flatten)]
299    filter_spec: FilterSpec,
300
301    #[serde(skip_serializing_if = "Option::is_none")]
302    stop_time: Option<DateTime<Utc>>,
303
304    #[serde(skip_serializing_if = "Option::is_none")]
305    transport: Option<Transport>,
306
307    #[serde(skip_serializing_if = "Option::is_none")]
308    encoding: Option<Encoding>,
309
310    #[serde(skip_serializing_if = "Option::is_none")]
311    purpose: Option<String>,
312
313    #[serde(flatten)]
314    update_trigger: Option<UpdateTrigger>,
315
316    module: Vec<YangPushModuleVersion>,
317
318    #[serde(skip_serializing_if = "Option::is_none")]
319    yang_library_content_id: Option<String>,
320}
321
322impl YangPushSubscriptionMetadata {
323    #[allow(clippy::too_many_arguments)]
324    pub const fn new(
325        id: Option<SubscriptionId>,
326        filter_spec: FilterSpec,
327        stop_time: Option<DateTime<Utc>>,
328        transport: Option<Transport>,
329        encoding: Option<Encoding>,
330        purpose: Option<String>,
331        update_trigger: Option<UpdateTrigger>,
332        module: Vec<YangPushModuleVersion>,
333        yang_library_content_id: Option<String>,
334    ) -> Self {
335        Self {
336            id,
337            filter_spec,
338            stop_time,
339            transport,
340            encoding,
341            purpose,
342            update_trigger,
343            module,
344            yang_library_content_id,
345        }
346    }
347    pub const fn id(&self) -> Option<SubscriptionId> {
348        self.id
349    }
350    pub const fn filter_spec(&self) -> &FilterSpec {
351        &self.filter_spec
352    }
353    pub const fn stop_time(&self) -> Option<DateTime<Utc>> {
354        self.stop_time
355    }
356    pub const fn transport(&self) -> Option<&Transport> {
357        self.transport.as_ref()
358    }
359    pub const fn encoding(&self) -> Option<&Encoding> {
360        self.encoding.as_ref()
361    }
362    pub fn purpose(&self) -> Option<&str> {
363        self.purpose.as_deref()
364    }
365    pub const fn update_trigger(&self) -> Option<&UpdateTrigger> {
366        self.update_trigger.as_ref()
367    }
368    pub fn module(&self) -> &[YangPushModuleVersion] {
369        &self.module
370    }
371    pub fn yang_library_content_id(&self) -> Option<&str> {
372        self.yang_library_content_id.as_deref()
373    }
374}
375
376impl From<SubscriptionInfo> for YangPushSubscriptionMetadata {
377    fn from(info: SubscriptionInfo) -> Self {
378        Self {
379            id: Some(info.id),
380            filter_spec: info.target.into(),
381            stop_time: info.stop_time,
382            transport: Some(Transport::UDPNotif),
383            encoding: Some(Encoding::Json),
384            purpose: info.purpose.map(String::from),
385            update_trigger: info.update_trigger.map(UpdateTrigger::from),
386            module: info.models.to_vec(),
387            yang_library_content_id: Some(info.content_id),
388        }
389    }
390}
391
392#[derive(Default, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
393#[serde(rename_all = "kebab-case")]
394pub struct FilterSpec {
395    #[serde(skip_serializing_if = "Option::is_none")]
396    stream: Option<String>,
397
398    #[serde(skip_serializing_if = "Option::is_none")]
399    datastore: Option<String>,
400
401    #[serde(skip_serializing_if = "Option::is_none")]
402    xpath_filter: Option<String>,
403
404    #[serde(skip_serializing_if = "Option::is_none")]
405    subtree_filter: Option<Value>,
406}
407
408impl FilterSpec {
409    pub const fn new(
410        stream: Option<String>,
411        datastore: Option<String>,
412        xpath_filter: Option<String>,
413        subtree_filter: Option<Value>,
414    ) -> Self {
415        Self {
416            stream,
417            datastore,
418            xpath_filter,
419            subtree_filter,
420        }
421    }
422    pub fn stream(&self) -> Option<&str> {
423        self.stream.as_deref()
424    }
425    pub fn datastore(&self) -> Option<&str> {
426        self.datastore.as_deref()
427    }
428    pub fn xpath_filter(&self) -> Option<&str> {
429        self.xpath_filter.as_deref()
430    }
431    pub const fn subtree_filter(&self) -> Option<&Value> {
432        self.subtree_filter.as_ref()
433    }
434}
435
436impl From<Target> for FilterSpec {
437    fn from(target: Target) -> Self {
438        let xpath_filter = if let Some(xpath_filter) = target.stream_xpath_filter {
439            Some(xpath_filter)
440        } else {
441            target.datastore_xpath_filter
442        };
443        let subtree_filter = if let Some(subtree) = target.stream_subtree_filter {
444            Some(subtree)
445        } else {
446            target.datastore_subtree_filter
447        };
448        Self {
449            stream: target.stream.map(|x| x.to_string()),
450            datastore: target.datastore.map(|x| x.to_string()),
451            xpath_filter: xpath_filter.map(|x| x.to_string()),
452            subtree_filter,
453        }
454    }
455}
456
457/// Update Trigger for Yang Push Subscription
458/// (redefined for Telemetry Message)
459#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
460pub enum UpdateTrigger {
461    #[serde(rename = "periodic")]
462    #[serde(rename_all = "kebab-case")]
463    Periodic {
464        #[serde(skip_serializing_if = "Option::is_none")]
465        period: Option<CentiSeconds>,
466
467        #[serde(skip_serializing_if = "Option::is_none")]
468        anchor_time: Option<DateTime<Utc>>,
469    },
470
471    #[serde(rename = "on-change")]
472    #[serde(rename_all = "kebab-case")]
473    OnChange {
474        #[serde(skip_serializing_if = "Option::is_none")]
475        dampening_period: Option<CentiSeconds>,
476
477        #[serde(skip_serializing_if = "Option::is_none")]
478        sync_on_start: Option<bool>,
479
480        #[serde(skip_serializing_if = "Option::is_none")]
481        excluded_change: Option<Vec<ChangeType>>,
482    },
483}
484
485impl From<netgauze_netconf_proto::yang_push::subscription::UpdateTrigger> for UpdateTrigger {
486    fn from(trigger: netgauze_netconf_proto::yang_push::subscription::UpdateTrigger) -> Self {
487        match trigger {
488            netgauze_netconf_proto::yang_push::subscription::UpdateTrigger::Periodic {
489                period,
490                anchor_time,
491            } => UpdateTrigger::Periodic {
492                period,
493                anchor_time,
494            },
495            netgauze_netconf_proto::yang_push::subscription::UpdateTrigger::OnChange {
496                dampening_period,
497                sync_on_start,
498                excluded_change,
499            } => UpdateTrigger::OnChange {
500                dampening_period,
501                sync_on_start,
502                excluded_change: excluded_change.map(|x| x.to_vec()),
503            },
504        }
505    }
506}
507
508#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
509#[serde(rename_all = "kebab-case")]
510pub struct NetworkOperatorMetadata {
511    #[serde(skip_serializing_if = "Vec::is_empty")]
512    labels: Vec<Label>,
513}
514
515impl NetworkOperatorMetadata {
516    pub const fn new(labels: Vec<Label>) -> Self {
517        Self { labels }
518    }
519    pub fn labels(&self) -> &[Label] {
520        &self.labels
521    }
522}
523
524#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
525pub struct Label {
526    name: String,
527
528    #[serde(flatten)]
529    value: LabelValue,
530}
531
532impl Label {
533    pub const fn new(name: String, value: LabelValue) -> Self {
534        Self { name, value }
535    }
536
537    pub const fn name(&self) -> &str {
538        self.name.as_str()
539    }
540
541    pub const fn value(&self) -> &LabelValue {
542        &self.value
543    }
544}
545
546#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
547#[serde(untagged)]
548pub enum LabelValue {
549    /// u64 requires to be serialized as string according to JSON Encoding of
550    /// YANG data specification from [RFC7951](https://datatracker.ietf.org/doc/html/rfc7951#section-6.1).
551    NumberValue {
552        #[serde(rename = "number-value")]
553        #[serde(serialize_with = "serialize_u64_as_string")]
554        #[serde(deserialize_with = "deserialize_u64_from_string")]
555        number_value: u64,
556    },
557    StringValue {
558        #[serde(rename = "string-value")]
559        string_value: String,
560    },
561    AnydataValue {
562        #[serde(rename = "anydata-values")]
563        anydata_values: Value,
564    },
565}
566
567impl LabelValue {
568    pub const fn as_u64(&self) -> Option<u64> {
569        match self {
570            LabelValue::NumberValue { number_value } => Some(*number_value),
571            _ => None,
572        }
573    }
574    pub const fn as_string(&self) -> Option<&str> {
575        match self {
576            LabelValue::StringValue { string_value } => Some(string_value.as_str()),
577            _ => None,
578        }
579    }
580    pub const fn as_anydata(&self) -> Option<&Value> {
581        match self {
582            LabelValue::AnydataValue { anydata_values } => Some(anydata_values),
583            _ => None,
584        }
585    }
586}
587
588fn serialize_u64_as_string<S>(value: &u64, serializer: S) -> Result<S::Ok, S::Error>
589where
590    S: serde::Serializer,
591{
592    serializer.serialize_str(&value.to_string())
593}
594
595fn deserialize_u64_from_string<'de, D>(deserializer: D) -> Result<u64, D::Error>
596where
597    D: serde::Deserializer<'de>,
598{
599    let s: String = Deserialize::deserialize(deserializer)?;
600    s.parse::<u64>().map_err(serde::de::Error::custom)
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606    use chrono::{TimeZone, Utc};
607    use serde_json;
608    use std::str::FromStr;
609    use std::vec;
610
611    #[test]
612    fn test_telemetry_message_serde() {
613        let original_message = TelemetryMessageWrapper {
614            message: TelemetryMessage {
615                network_node_manifest: Some(Manifest {
616                    name: Some("node_id".to_string()),
617                    vendor: Some("FRR".to_string()),
618                    vendor_pen: None,
619                    software_version: None,
620                    software_flavor: None,
621                    os_version: None,
622                    os_type: None,
623                }),
624                telemetry_message_metadata: TelemetryMessageMetadata {
625                    node_export_timestamp: None,
626                    collection_timestamp: Utc.timestamp_millis_opt(0).unwrap(),
627                    notification_event: EventType::Log,
628                    sequence_number: Some(1),
629                    session_protocol: SessionProtocol::YangPush,
630                    export_address: "127.0.0.1".parse().unwrap(),
631                    export_port: Some(8080),
632                    collection_address: None,
633                    collection_port: None,
634                    yang_push_subscription: Some(YangPushSubscriptionMetadata {
635                        id: Some(1),
636                        filter_spec: FilterSpec {
637                            stream: Some("example-stream-subtree-filter-map".to_string()),
638                            datastore: None,
639                            xpath_filter: None,
640                            subtree_filter: Some(serde_json::json!({
641                              "example-map": serde_json::json!({
642                                  "e1": "v1",
643                                  "e2": "v2",
644                              }),
645                            })),
646                        },
647                        stop_time: None,
648                        transport: Some(Transport::UDPNotif),
649                        encoding: Some(Encoding::Json),
650                        purpose: None,
651                        update_trigger: Some(UpdateTrigger::Periodic {
652                            period: Some(CentiSeconds::new(100)),
653                            anchor_time: Some(Utc.timestamp_millis_opt(0).unwrap()),
654                        }),
655                        module: vec![YangPushModuleVersion::new(
656                            "example-module".into(),
657                            Some("2025-01-01".into()),
658                            Some("1.0.0".into()),
659                        )],
660                        yang_library_content_id: Some("random-content-id".into()),
661                    }),
662                },
663                data_collection_manifest: Some(Manifest {
664                    name: Some("dev-collector".to_string()),
665                    vendor: Some("NetGauze".to_string()),
666                    vendor_pen: Some(12345),
667                    software_version: Some("1.0.0".to_string()),
668                    software_flavor: Some("release".to_string()),
669                    os_version: Some("8.10".to_string()),
670                    os_type: Some("Rocky Linux".to_string()),
671                }),
672                network_operator_metadata: Some(NetworkOperatorMetadata {
673                    labels: vec![
674                        Label {
675                            name: "priority_level".to_string(),
676                            value: LabelValue::NumberValue { number_value: 100 },
677                        },
678                        Label {
679                            name: "platform_id".to_string(),
680                            value: LabelValue::StringValue {
681                                string_value: "IETF LAB".to_string(),
682                            },
683                        },
684                        Label {
685                            name: "test_anykey_label".to_string(),
686                            value: LabelValue::AnydataValue {
687                                anydata_values: serde_json::json!({"key": "value"}),
688                            },
689                        },
690                    ],
691                }),
692                payload: None,
693            },
694        };
695
696        // Serialize the TelemetryMessage to JSON
697        let serialized = serde_json::to_string(&original_message).expect("Failed to serialize");
698
699        // Expected JSON string
700        let expected_json = r#"{"ietf-telemetry-message:message":{"network-node-manifest":{"name":"node_id","vendor":"FRR"},"telemetry-message-metadata":{"collection-timestamp":"1970-01-01T00:00:00Z","notification-event":"log","sequence-number":1,"session-protocol":"yang-push","export-address":"127.0.0.1","export-port":8080,"ietf-yang-push-telemetry-message:yang-push-subscription":{"id":1,"stream":"example-stream-subtree-filter-map","subtree-filter":{"example-map":{"e1":"v1","e2":"v2"}},"transport":"ietf-udp-notif-transport:udp-notif","encoding":"ietf-subscribed-notifications:encode-json","periodic":{"period":100,"anchor-time":"1970-01-01T00:00:00Z"},"module":[{"name":"example-module","revision":"2025-01-01","version":"1.0.0"}],"yang-library-content-id":"random-content-id"}},"data-collection-manifest":{"name":"dev-collector","vendor":"NetGauze","vendor-pen":12345,"software-version":"1.0.0","software-flavor":"release","os-version":"8.10","os-type":"Rocky Linux"},"network-operator-metadata":{"labels":[{"name":"priority_level","number-value":"100"},{"name":"platform_id","string-value":"IETF LAB"},{"name":"test_anykey_label","anydata-values":{"key":"value"}}]}}}"#;
701
702        // Assert that the serialized JSON string matches the expected JSON string
703        assert_eq!(
704            serialized, expected_json,
705            "Serialized JSON does not match the expected JSON"
706        );
707
708        // Deserialize the JSON string back to a TelemetryMessage
709        let deserialized: TelemetryMessageWrapper =
710            serde_json::from_str(&serialized).expect("Failed to deserialize");
711
712        // Assert that the original and deserialized messages are equal
713        assert_eq!(original_message, deserialized);
714    }
715
716    #[test]
717    fn test_telemetry_message_minimal() {
718        let original_message = TelemetryMessageWrapper {
719            message: TelemetryMessage {
720                network_node_manifest: None,
721                telemetry_message_metadata: TelemetryMessageMetadata {
722                    node_export_timestamp: None,
723                    collection_timestamp: Utc.timestamp_millis_opt(0).unwrap(),
724                    notification_event: EventType::Log,
725                    sequence_number: None,
726                    session_protocol: SessionProtocol::Unknown,
727                    export_address: "127.0.0.1".parse().unwrap(),
728                    export_port: None,
729                    collection_address: None,
730                    collection_port: None,
731                    yang_push_subscription: None,
732                },
733                data_collection_manifest: None,
734                network_operator_metadata: None,
735                payload: None,
736            },
737        };
738
739        // Serialize the TelemetryMessage to JSON
740        let serialized = serde_json::to_string(&original_message).expect("Failed to serialize");
741
742        // Expected JSON string
743        let expected_json = r#"{"ietf-telemetry-message:message":{"telemetry-message-metadata":{"collection-timestamp":"1970-01-01T00:00:00Z","notification-event":"log","session-protocol":"unknown","export-address":"127.0.0.1"}}}"#;
744
745        // Assert that the serialized JSON string matches the expected JSON string
746        assert_eq!(
747            serialized, expected_json,
748            "Serialized JSON does not match the expected JSON"
749        );
750
751        // Deserialize the JSON string back to a TelemetryMessage
752        let deserialized: TelemetryMessageWrapper =
753            serde_json::from_str(&serialized).expect("Failed to deserialize");
754
755        // Assert that the original and deserialized messages are equal
756        assert_eq!(original_message, deserialized);
757    }
758
759    #[test]
760    fn test_telemetry_message_metadata_new_and_getters() {
761        let metadata = TelemetryMessageMetadata::new(
762            Some(Utc.timestamp_millis_opt(1000).unwrap()),
763            Utc.timestamp_millis_opt(2000).unwrap(),
764            EventType::Update,
765            Some(42),
766            SessionProtocol::Netconf,
767            "192.168.1.1".parse().unwrap(),
768            Some(9090),
769            Some("10.0.0.1".parse().unwrap()),
770            Some(8080),
771            None,
772        );
773        assert_eq!(
774            metadata.node_export_timestamp(),
775            Some(Utc.timestamp_millis_opt(1000).unwrap())
776        );
777        assert_eq!(
778            metadata.collection_timestamp(),
779            Utc.timestamp_millis_opt(2000).unwrap()
780        );
781        assert_eq!(metadata.notification_event(), EventType::Update);
782        assert_eq!(metadata.sequence_number(), Some(42));
783        assert_eq!(metadata.session_protocol(), &SessionProtocol::Netconf);
784        assert_eq!(
785            metadata.export_address(),
786            IpAddr::from_str("192.168.1.1").unwrap()
787        );
788        assert_eq!(metadata.export_port(), Some(9090));
789        assert_eq!(
790            metadata.collection_address(),
791            Some("10.0.0.1".parse().unwrap())
792        );
793        assert_eq!(metadata.collection_port(), Some(8080));
794        assert_eq!(metadata.yang_push_subscription(), None);
795    }
796
797    #[test]
798    fn test_yang_push_subscription_metadata_new_and_getters() {
799        let filter_spec = FilterSpec::new(
800            Some("stream".to_string()),
801            Some("datastore".to_string()),
802            Some("xpath".to_string()),
803            Some(serde_json::json!({"key": "value"})),
804        );
805        let update_trigger = UpdateTrigger::OnChange {
806            dampening_period: Some(CentiSeconds::new(50)),
807            sync_on_start: Some(true),
808            excluded_change: Some(vec![ChangeType::Delete]),
809        };
810        let module = vec![YangPushModuleVersion::new(
811            "mod".into(),
812            Some("rev".into()),
813            Some("ver".into()),
814        )];
815        let metadata = YangPushSubscriptionMetadata::new(
816            Some(1),
817            filter_spec.clone(),
818            Some(Utc.timestamp_millis_opt(3000).unwrap()),
819            Some(Transport::UDPNotif),
820            Some(Encoding::Json),
821            Some("purpose".to_string()),
822            Some(update_trigger.clone()),
823            module.clone(),
824            Some("content_id".to_string()),
825        );
826        assert_eq!(metadata.id(), Some(1));
827        assert_eq!(metadata.filter_spec(), &filter_spec);
828        assert_eq!(
829            metadata.stop_time(),
830            Some(Utc.timestamp_millis_opt(3000).unwrap())
831        );
832        assert_eq!(metadata.transport(), Some(&Transport::UDPNotif));
833        assert_eq!(metadata.encoding(), Some(&Encoding::Json));
834        assert_eq!(metadata.purpose(), Some("purpose"));
835        assert_eq!(metadata.update_trigger(), Some(&update_trigger));
836        assert_eq!(metadata.module(), module.as_slice());
837        assert_eq!(metadata.yang_library_content_id(), Some("content_id"));
838    }
839
840    #[test]
841    fn test_filter_spec_new_and_getters() {
842        let filter_spec = FilterSpec::new(
843            Some("stream".to_string()),
844            Some("datastore".to_string()),
845            Some("xpath".to_string()),
846            Some(serde_json::json!({"key": "value"})),
847        );
848        assert_eq!(filter_spec.stream(), Some("stream"));
849        assert_eq!(filter_spec.datastore(), Some("datastore"));
850        assert_eq!(filter_spec.xpath_filter(), Some("xpath"));
851        assert_eq!(
852            filter_spec.subtree_filter(),
853            Some(&serde_json::json!({"key": "value"}))
854        );
855    }
856}