1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::error::SearchError;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Pagination {
15 pub count: u32,
17
18 pub mode: PaginationMode,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum PaginationMode {
25 Cursor(Option<PageCursor>),
27
28 Offset(u32),
30}
31
32impl Default for Pagination {
33 fn default() -> Self {
34 Self {
35 count: 20,
36 mode: PaginationMode::Cursor(None),
37 }
38 }
39}
40
41impl Pagination {
42 pub fn new(count: u32) -> Self {
44 Self {
45 count,
46 mode: PaginationMode::Cursor(None),
47 }
48 }
49
50 pub fn cursor() -> Self {
52 Self::default()
53 }
54
55 pub fn with_cursor(count: u32, cursor: String) -> Self {
57 match PageCursor::decode(&cursor) {
58 Ok(page_cursor) => Self {
59 count,
60 mode: PaginationMode::Cursor(Some(page_cursor)),
61 },
62 Err(_) => Self {
63 count,
64 mode: PaginationMode::Cursor(None),
65 },
66 }
67 }
68
69 pub fn offset(offset: u32) -> Self {
71 Self {
72 count: 20,
73 mode: PaginationMode::Offset(offset),
74 }
75 }
76
77 pub fn from_cursor(cursor: &str) -> Result<Self, SearchError> {
79 let page_cursor = PageCursor::decode(cursor)?;
80 Ok(Self {
81 count: 20,
82 mode: PaginationMode::Cursor(Some(page_cursor)),
83 })
84 }
85
86 pub fn with_count(mut self, count: u32) -> Self {
88 self.count = count;
89 self
90 }
91
92 pub fn offset_value(&self) -> Option<u32> {
94 match &self.mode {
95 PaginationMode::Offset(offset) => Some(*offset),
96 _ => None,
97 }
98 }
99
100 pub fn cursor_value(&self) -> Option<&PageCursor> {
102 match &self.mode {
103 PaginationMode::Cursor(Some(cursor)) => Some(cursor),
104 _ => None,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct PageCursor {
124 version: u8,
126
127 sort_values: Vec<CursorValue>,
129
130 resource_id: String,
132
133 direction: CursorDirection,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(untagged)]
140pub enum CursorValue {
141 String(String),
143 Number(i64),
145 Decimal(f64),
147 Boolean(bool),
149 Null,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
155pub enum CursorDirection {
156 #[default]
158 Next,
159 Previous,
161}
162
163impl PageCursor {
164 pub fn new(sort_values: Vec<CursorValue>, resource_id: impl Into<String>) -> Self {
166 Self {
167 version: 1,
168 sort_values,
169 resource_id: resource_id.into(),
170 direction: CursorDirection::Next,
171 }
172 }
173
174 pub fn previous(sort_values: Vec<CursorValue>, resource_id: impl Into<String>) -> Self {
176 Self {
177 version: 1,
178 sort_values,
179 resource_id: resource_id.into(),
180 direction: CursorDirection::Previous,
181 }
182 }
183
184 pub fn sort_values(&self) -> &[CursorValue] {
186 &self.sort_values
187 }
188
189 pub fn resource_id(&self) -> &str {
191 &self.resource_id
192 }
193
194 pub fn direction(&self) -> CursorDirection {
196 self.direction
197 }
198
199 pub fn encode(&self) -> String {
201 let json = serde_json::to_vec(self).unwrap_or_default();
202 URL_SAFE_NO_PAD.encode(&json)
203 }
204
205 pub fn decode(s: &str) -> Result<Self, SearchError> {
207 let bytes = URL_SAFE_NO_PAD
208 .decode(s)
209 .map_err(|_| SearchError::InvalidCursor {
210 cursor: s.to_string(),
211 })?;
212
213 serde_json::from_slice(&bytes).map_err(|_| SearchError::InvalidCursor {
214 cursor: s.to_string(),
215 })
216 }
217}
218
219impl From<&str> for CursorValue {
220 fn from(s: &str) -> Self {
221 CursorValue::String(s.to_string())
222 }
223}
224
225impl From<String> for CursorValue {
226 fn from(s: String) -> Self {
227 CursorValue::String(s)
228 }
229}
230
231impl From<i64> for CursorValue {
232 fn from(n: i64) -> Self {
233 CursorValue::Number(n)
234 }
235}
236
237impl From<f64> for CursorValue {
238 fn from(n: f64) -> Self {
239 CursorValue::Decimal(n)
240 }
241}
242
243impl From<bool> for CursorValue {
244 fn from(b: bool) -> Self {
245 CursorValue::Boolean(b)
246 }
247}
248
249impl From<()> for CursorValue {
250 fn from(_: ()) -> Self {
251 CursorValue::Null
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct PageInfo {
258 pub next_cursor: Option<String>,
260
261 pub previous_cursor: Option<String>,
263
264 pub total: Option<u64>,
266
267 pub has_next: bool,
269
270 pub has_previous: bool,
272}
273
274impl PageInfo {
275 pub fn end() -> Self {
277 Self {
278 next_cursor: None,
279 previous_cursor: None,
280 total: None,
281 has_next: false,
282 has_previous: false,
283 }
284 }
285
286 pub fn with_next(cursor: PageCursor) -> Self {
288 Self {
289 next_cursor: Some(cursor.encode()),
290 previous_cursor: None,
291 total: None,
292 has_next: true,
293 has_previous: false,
294 }
295 }
296
297 pub fn with_total(mut self, total: u64) -> Self {
299 self.total = Some(total);
300 self
301 }
302
303 pub fn with_previous(mut self, cursor: PageCursor) -> Self {
305 self.previous_cursor = Some(cursor.encode());
306 self.has_previous = true;
307 self
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct Page<T> {
314 pub items: Vec<T>,
316
317 pub page_info: PageInfo,
319}
320
321impl<T> Page<T> {
322 pub fn new(items: Vec<T>, page_info: PageInfo) -> Self {
324 Self { items, page_info }
325 }
326
327 pub fn empty() -> Self {
329 Self {
330 items: Vec::new(),
331 page_info: PageInfo::end(),
332 }
333 }
334
335 pub fn is_empty(&self) -> bool {
337 self.items.is_empty()
338 }
339
340 pub fn len(&self) -> usize {
342 self.items.len()
343 }
344
345 pub fn map<U, F>(self, f: F) -> Page<U>
347 where
348 F: FnMut(T) -> U,
349 {
350 Page {
351 items: self.items.into_iter().map(f).collect(),
352 page_info: self.page_info,
353 }
354 }
355}
356
357impl<T> Default for Page<T> {
358 fn default() -> Self {
359 Self::empty()
360 }
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct SearchBundle {
366 #[serde(rename = "type")]
368 pub bundle_type: String,
369
370 pub total: Option<u64>,
372
373 pub link: Vec<BundleLink>,
375
376 pub entry: Vec<BundleEntry>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct BundleLink {
383 pub relation: String,
385
386 pub url: String,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct BundleEntry {
393 #[serde(rename = "fullUrl", skip_serializing_if = "Option::is_none")]
395 pub full_url: Option<String>,
396
397 #[serde(skip_serializing_if = "Option::is_none")]
399 pub resource: Option<Value>,
400
401 #[serde(skip_serializing_if = "Option::is_none")]
403 pub search: Option<BundleEntrySearch>,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct BundleEntrySearch {
409 pub mode: SearchEntryMode,
411
412 #[serde(skip_serializing_if = "Option::is_none")]
414 pub score: Option<f64>,
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(rename_all = "lowercase")]
420pub enum SearchEntryMode {
421 Match,
423 Include,
425 Outcome,
427}
428
429impl SearchBundle {
430 pub fn new() -> Self {
432 Self {
433 bundle_type: "searchset".to_string(),
434 total: None,
435 link: Vec::new(),
436 entry: Vec::new(),
437 }
438 }
439
440 pub fn with_total(mut self, total: u64) -> Self {
442 self.total = Some(total);
443 self
444 }
445
446 pub fn with_link(mut self, relation: impl Into<String>, url: impl Into<String>) -> Self {
448 self.link.push(BundleLink {
449 relation: relation.into(),
450 url: url.into(),
451 });
452 self
453 }
454
455 pub fn with_entry(mut self, entry: BundleEntry) -> Self {
457 self.entry.push(entry);
458 self
459 }
460
461 pub fn with_self_link(self, url: impl Into<String>) -> Self {
463 self.with_link("self", url)
464 }
465
466 pub fn with_next_link(self, url: impl Into<String>) -> Self {
468 self.with_link("next", url)
469 }
470
471 pub fn with_previous_link(self, url: impl Into<String>) -> Self {
473 self.with_link("previous", url)
474 }
475}
476
477impl Default for SearchBundle {
478 fn default() -> Self {
479 Self::new()
480 }
481}
482
483impl BundleEntry {
484 pub fn match_entry(full_url: impl Into<String>, resource: Value) -> Self {
486 Self {
487 full_url: Some(full_url.into()),
488 resource: Some(resource),
489 search: Some(BundleEntrySearch {
490 mode: SearchEntryMode::Match,
491 score: None,
492 }),
493 }
494 }
495
496 pub fn include_entry(full_url: impl Into<String>, resource: Value) -> Self {
498 Self {
499 full_url: Some(full_url.into()),
500 resource: Some(resource),
501 search: Some(BundleEntrySearch {
502 mode: SearchEntryMode::Include,
503 score: None,
504 }),
505 }
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn test_pagination_default() {
515 let pagination = Pagination::default();
516 assert_eq!(pagination.count, 20);
517 assert!(matches!(pagination.mode, PaginationMode::Cursor(None)));
518 }
519
520 #[test]
521 fn test_pagination_offset() {
522 let pagination = Pagination::offset(100);
523 assert_eq!(pagination.offset_value(), Some(100));
524 }
525
526 #[test]
527 fn test_cursor_encode_decode() {
528 let cursor = PageCursor::new(
529 vec![CursorValue::String("2024-01-01".to_string())],
530 "patient-123",
531 );
532
533 let encoded = cursor.encode();
534 let decoded = PageCursor::decode(&encoded).unwrap();
535
536 assert_eq!(decoded.resource_id(), "patient-123");
537 assert_eq!(decoded.direction(), CursorDirection::Next);
538 }
539
540 #[test]
541 fn test_cursor_decode_invalid() {
542 let result = PageCursor::decode("not-valid-base64!!!");
543 assert!(result.is_err());
544 }
545
546 #[test]
547 fn test_cursor_previous() {
548 let cursor = PageCursor::previous(vec![CursorValue::Number(100)], "obs-456");
549 assert_eq!(cursor.direction(), CursorDirection::Previous);
550 }
551
552 #[test]
553 fn test_page_info_with_next() {
554 let cursor = PageCursor::new(vec![], "id");
555 let info = PageInfo::with_next(cursor);
556 assert!(info.has_next);
557 assert!(info.next_cursor.is_some());
558 }
559
560 #[test]
561 fn test_page_map() {
562 let page = Page::new(vec![1, 2, 3], PageInfo::end());
563
564 let mapped = page.map(|x| x * 2);
565 assert_eq!(mapped.items, vec![2, 4, 6]);
566 }
567
568 #[test]
569 fn test_search_bundle_builder() {
570 let bundle = SearchBundle::new()
571 .with_total(100)
572 .with_self_link("https://example.com/Patient?name=Smith")
573 .with_next_link("https://example.com/Patient?name=Smith&_cursor=xxx");
574
575 assert_eq!(bundle.total, Some(100));
576 assert_eq!(bundle.link.len(), 2);
577 assert_eq!(bundle.link[0].relation, "self");
578 assert_eq!(bundle.link[1].relation, "next");
579 }
580
581 #[test]
582 fn test_bundle_entry_match() {
583 let entry = BundleEntry::match_entry(
584 "https://example.com/Patient/123",
585 serde_json::json!({"resourceType": "Patient"}),
586 );
587
588 assert_eq!(entry.search.as_ref().unwrap().mode, SearchEntryMode::Match);
589 }
590
591 #[test]
592 fn test_cursor_value_conversions() {
593 let s: CursorValue = "test".into();
594 assert!(matches!(s, CursorValue::String(_)));
595
596 let n: CursorValue = 42i64.into();
597 assert!(matches!(n, CursorValue::Number(_)));
598
599 let b: CursorValue = true.into();
600 assert!(matches!(b, CursorValue::Boolean(_)));
601 }
602}