Skip to main content

helios_subscriptions/notification/
builder.rs

1//! Notification bundle builder.
2//!
3//! Shared logic for constructing notification bundles across FHIR versions.
4
5use chrono::Utc;
6use serde_json::json;
7use uuid::Uuid;
8
9use crate::error::SubscriptionError;
10use crate::manager::{ActiveSubscription, PayloadContent};
11use crate::notification::{NotificationEventData, NotificationType};
12
13/// Builds notification bundles for a subscription.
14pub struct NotificationBundleBuilder<'a> {
15    subscription: &'a ActiveSubscription,
16    notification_type: NotificationType,
17    base_url: &'a str,
18}
19
20impl<'a> NotificationBundleBuilder<'a> {
21    pub fn new(
22        subscription: &'a ActiveSubscription,
23        notification_type: NotificationType,
24        base_url: &'a str,
25    ) -> Self {
26        Self {
27            subscription,
28            notification_type,
29            base_url,
30        }
31    }
32
33    /// Build an R4 backport notification bundle.
34    ///
35    /// - Bundle type: `history`
36    /// - First entry: `Parameters` resource (backport SubscriptionStatus)
37    #[cfg(feature = "R4")]
38    pub fn build_r4(
39        &self,
40        events: &[NotificationEventData],
41        resources: &[serde_json::Value],
42    ) -> Result<serde_json::Value, SubscriptionError> {
43        let mut entries = Vec::new();
44
45        // Build the SubscriptionStatus as a Parameters resource.
46        let status_parameters = self.build_r4_status_parameters(events);
47        entries.push(json!({
48            "fullUrl": format!("urn:uuid:{}", Uuid::new_v4()),
49            "resource": status_parameters,
50            "request": {
51                "method": "GET",
52                "url": format!("Subscription/{}/$status", self.subscription.id)
53            },
54            "response": {
55                "status": "200"
56            }
57        }));
58
59        // Add payload entries.
60        self.add_payload_entries(&mut entries, events, resources);
61
62        Ok(json!({
63            "resourceType": "Bundle",
64            "id": Uuid::new_v4().to_string(),
65            "type": "history",
66            "timestamp": Utc::now().to_rfc3339(),
67            "entry": entries
68        }))
69    }
70
71    /// Build an R4 backport SubscriptionStatus as a Parameters resource.
72    #[cfg(feature = "R4")]
73    fn build_r4_status_parameters(&self, events: &[NotificationEventData]) -> serde_json::Value {
74        let mut parameters = vec![
75            json!({
76                "name": "subscription",
77                "valueReference": {
78                    "reference": format!("Subscription/{}", self.subscription.id)
79                }
80            }),
81            json!({
82                "name": "topic",
83                "valueCanonical": self.subscription.topic_url
84            }),
85            json!({
86                "name": "status",
87                "valueCode": self.subscription.status.as_fhir_str()
88            }),
89            json!({
90                "name": "type",
91                "valueCode": self.notification_type.as_fhir_str()
92            }),
93            json!({
94                "name": "events-since-subscription-start",
95                "valueString": self.subscription.events_since_start.to_string()
96            }),
97        ];
98
99        // Add notification-event parts for event notifications.
100        for event in events {
101            let event_parts = vec![
102                json!({
103                    "name": "event-number",
104                    "valueString": event.event_number.to_string()
105                }),
106                json!({
107                    "name": "timestamp",
108                    "valueInstant": event.timestamp.to_rfc3339()
109                }),
110                json!({
111                    "name": "focus",
112                    "valueReference": {
113                        "reference": &event.focus_reference
114                    }
115                }),
116            ];
117
118            parameters.push(json!({
119                "name": "notification-event",
120                "part": event_parts
121            }));
122        }
123
124        json!({
125            "resourceType": "Parameters",
126            "parameter": parameters
127        })
128    }
129
130    /// Build a native notification bundle (R4B, R5, R6).
131    ///
132    /// - R4B: Bundle type `history`
133    /// - R5/R6: Bundle type `subscription-notification`
134    /// - First entry: native `SubscriptionStatus` resource
135    pub fn build_native(
136        &self,
137        events: &[NotificationEventData],
138        resources: &[serde_json::Value],
139    ) -> Result<serde_json::Value, SubscriptionError> {
140        let mut entries = Vec::new();
141
142        // Build the native SubscriptionStatus resource.
143        let status = self.build_native_status(events);
144        entries.push(json!({
145            "fullUrl": format!("urn:uuid:{}", Uuid::new_v4()),
146            "resource": status,
147            "request": {
148                "method": "GET",
149                "url": format!("Subscription/{}/$status", self.subscription.id)
150            },
151            "response": {
152                "status": "200"
153            }
154        }));
155
156        // Add payload entries.
157        self.add_payload_entries(&mut entries, events, resources);
158
159        let bundle_type = self.bundle_type();
160
161        Ok(json!({
162            "resourceType": "Bundle",
163            "id": Uuid::new_v4().to_string(),
164            "type": bundle_type,
165            "timestamp": Utc::now().to_rfc3339(),
166            "entry": entries
167        }))
168    }
169
170    /// Build a native SubscriptionStatus resource.
171    fn build_native_status(&self, events: &[NotificationEventData]) -> serde_json::Value {
172        let mut notification_events = Vec::new();
173        for event in events {
174            notification_events.push(json!({
175                "eventNumber": event.event_number,
176                "timestamp": event.timestamp.to_rfc3339(),
177                "focus": {
178                    "reference": &event.focus_reference
179                }
180            }));
181        }
182
183        let mut status = json!({
184            "resourceType": "SubscriptionStatus",
185            "status": self.subscription.status.as_fhir_str(),
186            "type": self.notification_type.as_fhir_str(),
187            "eventsSinceSubscriptionStart": self.subscription.events_since_start,
188            "subscription": {
189                "reference": format!("Subscription/{}", self.subscription.id)
190            },
191            "topic": self.subscription.topic_url
192        });
193
194        if !notification_events.is_empty() {
195            status["notificationEvent"] = json!(notification_events);
196        }
197
198        status
199    }
200
201    /// Add payload entries based on the subscription's payload content level.
202    fn add_payload_entries(
203        &self,
204        entries: &mut Vec<serde_json::Value>,
205        events: &[NotificationEventData],
206        resources: &[serde_json::Value],
207    ) {
208        match self.subscription.channel.payload_content {
209            PayloadContent::Empty => {
210                // No additional entries.
211            }
212            PayloadContent::IdOnly => {
213                for event in events {
214                    entries.push(json!({
215                        "fullUrl": format!("{}/{}", self.base_url, event.focus_reference),
216                        "request": {
217                            "method": "GET",
218                            "url": &event.focus_reference
219                        },
220                        "response": {
221                            "status": "200"
222                        }
223                    }));
224                }
225            }
226            PayloadContent::FullResource => {
227                for (i, event) in events.iter().enumerate() {
228                    let mut entry = json!({
229                        "fullUrl": format!("{}/{}", self.base_url, event.focus_reference),
230                        "request": {
231                            "method": "GET",
232                            "url": &event.focus_reference
233                        },
234                        "response": {
235                            "status": "200"
236                        }
237                    });
238
239                    if let Some(resource) = resources.get(i) {
240                        entry["resource"] = resource.clone();
241                    }
242
243                    entries.push(entry);
244                }
245            }
246        }
247    }
248
249    /// Returns the Bundle type based on FHIR version.
250    fn bundle_type(&self) -> &'static str {
251        match self.subscription.fhir_version {
252            #[cfg(feature = "R4")]
253            helios_fhir::FhirVersion::R4 => "history",
254
255            #[cfg(feature = "R4B")]
256            helios_fhir::FhirVersion::R4B => "history",
257
258            // R5, R6 use the new bundle type.
259            #[allow(unreachable_patterns)]
260            _ => "subscription-notification",
261        }
262    }
263}