1use 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#[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#[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#[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#[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#[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#[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#[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 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 let serialized = serde_json::to_string(&original_message).expect("Failed to serialize");
698
699 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_eq!(
704 serialized, expected_json,
705 "Serialized JSON does not match the expected JSON"
706 );
707
708 let deserialized: TelemetryMessageWrapper =
710 serde_json::from_str(&serialized).expect("Failed to deserialize");
711
712 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 let serialized = serde_json::to_string(&original_message).expect("Failed to serialize");
741
742 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_eq!(
747 serialized, expected_json,
748 "Serialized JSON does not match the expected JSON"
749 );
750
751 let deserialized: TelemetryMessageWrapper =
753 serde_json::from_str(&serialized).expect("Failed to deserialize");
754
755 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}