helios_fhir/lib.rs
1//! # FHIR Model Infrastructure
2//!
3//! This module provides the foundational types and infrastructure that support the
4//! generated FHIR specification implementations. It contains hand-coded types that
5//! enable the generated code to handle FHIR's complex requirements for precision,
6//! extensions, and cross-version compatibility.
7
8//!
9//! ## Architecture
10//!
11//! The FHIR crate is organized as follows:
12//! - **Generated modules** (`r4.rs`, `r4b.rs`, `r5.rs`, `r6.rs`): Complete FHIR type implementations
13//! - **Infrastructure module** (`lib.rs`): Foundational types used by generated code
14//! - **Test modules**: Validation against official FHIR examples
15//!
16//! ## Key Infrastructure Types
17//!
18//! - [`PreciseDecimal`] - High-precision decimal arithmetic preserving original string format
19//! - [`Element<T, Extension>`] - Base container for FHIR elements with extension support
20//! - [`DecimalElement<Extension>`] - Specialized element for decimal values
21//! - [`FhirVersion`] - Version enumeration for multi-version support
22//!
23//! ## Usage Example
24//!
25//! ```rust
26//! use helios_fhir::r4::{Patient, HumanName};
27//! use helios_fhir::PreciseDecimal;
28//! use rust_decimal::Decimal;
29//!
30//! // Create a patient with precise decimal handling
31//! let patient = Patient {
32//! name: Some(vec![HumanName {
33//! family: Some("Doe".to_string().into()),
34//! given: Some(vec!["John".to_string().into()]),
35//! ..Default::default()
36//! }]),
37//! ..Default::default()
38//! };
39//!
40//! // Work with precise decimals
41//! let precise = PreciseDecimal::from(Decimal::new(12340, 3)); // 12.340
42//! ```
43
44use chrono::{DateTime as ChronoDateTime, NaiveDate, NaiveTime, Utc};
45use helios_fhirpath_support::{EvaluationResult, IntoEvaluationResult, TypeInfoResult};
46#[cfg(feature = "xml")]
47use helios_serde_support::SingleOrVec;
48
49use rust_decimal::Decimal;
50use serde::{
51 Deserialize, Serialize,
52 de::{self, Deserializer, MapAccess, Visitor},
53 ser::{SerializeStruct, Serializer},
54};
55use std::cmp::Ordering;
56use std::fmt;
57use std::marker::PhantomData;
58use std::sync::Arc;
59
60/// Custom deserializer that is more forgiving of null values in JSON.
61///
62/// This creates a custom `Option<T>` deserializer that will return None for null values
63/// but also for any deserialization errors. This makes it possible to skip over
64/// malformed or unexpected values in FHIR JSON.
65pub fn deserialize_forgiving_option<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
66where
67 T: Deserialize<'de>,
68 D: Deserializer<'de>,
69{
70 // Use the intermediate Value approach to check for null first
71 let json_value = serde_json::Value::deserialize(deserializer)?;
72
73 match json_value {
74 serde_json::Value::Null => Ok(None),
75 _ => {
76 // Try to deserialize the value, but return None if it fails
77 match T::deserialize(json_value) {
78 Ok(value) => Ok(Some(value)),
79 Err(_) => Ok(None), // Ignore errors and return None
80 }
81 }
82 }
83}
84
85/// High-precision decimal type that preserves original string representation.
86///
87/// FHIR requires that decimal values maintain their original precision and format
88/// when serialized back to JSON. This type stores both the parsed `Decimal` value
89/// for mathematical operations and the original string for serialization.
90///
91/// # FHIR Precision Requirements
92///
93/// FHIR decimal values must:
94/// - Preserve trailing zeros (e.g., "12.340" vs "12.34")
95/// - Maintain original precision during round-trip serialization
96/// - Support high-precision arithmetic without floating-point errors
97/// - Handle edge cases like very large or very small numbers
98///
99/// # Examples
100///
101/// ```rust
102/// use helios_fhir::PreciseDecimal;
103/// use rust_decimal::Decimal;
104///
105/// // Create from Decimal (derives string representation)
106/// let precise = PreciseDecimal::from(Decimal::new(12340, 3)); // 12.340
107/// assert_eq!(precise.original_string(), "12.340");
108///
109/// // Create with specific string format
110/// let precise = PreciseDecimal::from_parts(
111/// Some(Decimal::new(1000, 2)),
112/// "10.00".to_string()
113/// );
114/// assert_eq!(precise.original_string(), "10.00");
115/// ```
116#[derive(Debug, Clone)]
117pub struct PreciseDecimal {
118 /// The parsed decimal value, `None` if parsing failed (e.g., out of range)
119 value: Option<Decimal>,
120 /// The original string representation preserving format and precision
121 original_string: Arc<str>,
122}
123
124/// Implements equality comparison based on the parsed decimal value.
125///
126/// Two `PreciseDecimal` values are equal if their parsed `Decimal` values are equal,
127/// regardless of their original string representations. This enables mathematical
128/// equality while preserving string format for serialization.
129///
130/// # Examples
131///
132/// ```rust
133/// use helios_fhir::PreciseDecimal;
134/// use rust_decimal::Decimal;
135///
136/// let a = PreciseDecimal::from_parts(Some(Decimal::new(100, 1)), "10.0".to_string());
137/// let b = PreciseDecimal::from_parts(Some(Decimal::new(1000, 2)), "10.00".to_string());
138/// assert_eq!(a, b); // Same decimal value (10.0 == 10.00)
139/// ```
140impl PartialEq for PreciseDecimal {
141 fn eq(&self, other: &Self) -> bool {
142 // Compare parsed decimal values for mathematical equality
143 self.value == other.value
144 }
145}
146
147/// Marker trait implementation indicating total equality for `PreciseDecimal`.
148impl Eq for PreciseDecimal {}
149
150/// Implements partial ordering based on the parsed decimal value.
151///
152/// Ordering is based on the mathematical value of the decimal, not the string
153/// representation. `None` values (unparseable decimals) are considered less than
154/// any valid decimal value.
155impl PartialOrd for PreciseDecimal {
156 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
157 Some(self.cmp(other))
158 }
159}
160
161/// Implements total ordering for `PreciseDecimal`.
162///
163/// Provides a consistent ordering for sorting operations. The ordering is based
164/// on the mathematical value: `None` < `Some(smaller_decimal)` < `Some(larger_decimal)`.
165impl Ord for PreciseDecimal {
166 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
167 self.value.cmp(&other.value)
168 }
169}
170
171// === PreciseDecimal Methods ===
172
173impl PreciseDecimal {
174 /// Creates a new `PreciseDecimal` from its constituent parts.
175 ///
176 /// This constructor allows explicit control over both the parsed value and the
177 /// original string representation. Use this when you need to preserve a specific
178 /// string format or when parsing has already been attempted.
179 ///
180 /// # Arguments
181 ///
182 /// * `value` - The parsed decimal value, or `None` if parsing failed
183 /// * `original_string` - The original string representation to preserve
184 ///
185 /// # Examples
186 ///
187 /// ```rust
188 /// use helios_fhir::PreciseDecimal;
189 /// use rust_decimal::Decimal;
190 ///
191 /// // Create with successful parsing
192 /// let precise = PreciseDecimal::from_parts(
193 /// Some(Decimal::new(12340, 3)),
194 /// "12.340".to_string()
195 /// );
196 ///
197 /// // Create with failed parsing (preserves original string)
198 /// let invalid = PreciseDecimal::from_parts(
199 /// None,
200 /// "invalid_decimal".to_string()
201 /// );
202 /// ```
203 pub fn from_parts(value: Option<Decimal>, original_string: String) -> Self {
204 Self {
205 value,
206 original_string: Arc::from(original_string.as_str()),
207 }
208 }
209
210 /// Helper method to parse a decimal string with support for scientific notation.
211 ///
212 /// This method handles the complexity of parsing decimal strings that may be in
213 /// scientific notation (with 'E' or 'e' exponents) or regular decimal format.
214 /// It normalizes 'E' to 'e' for consistent parsing while preserving the original
215 /// string representation for serialization.
216 ///
217 /// # Arguments
218 ///
219 /// * `s` - The string to parse as a decimal
220 ///
221 /// # Returns
222 ///
223 /// `Some(Decimal)` if parsing succeeds, `None` if the string is not a valid decimal.
224 ///
225 /// # Examples
226 ///
227 /// ```ignore
228 /// use helios_fhir::PreciseDecimal;
229 /// use rust_decimal::Decimal;
230 ///
231 /// // Regular decimal format
232 /// assert!(PreciseDecimal::parse_decimal_string("123.45").is_some());
233 ///
234 /// // Scientific notation with 'e'
235 /// assert!(PreciseDecimal::parse_decimal_string("1.23e2").is_some());
236 ///
237 /// // Scientific notation with 'E' (normalized to 'e')
238 /// assert!(PreciseDecimal::parse_decimal_string("1.23E2").is_some());
239 ///
240 /// // Invalid format
241 /// assert!(PreciseDecimal::parse_decimal_string("invalid").is_none());
242 /// ```
243 fn parse_decimal_string(s: &str) -> Option<Decimal> {
244 // Normalize 'E' to 'e' for consistent parsing
245 let normalized = s.replace('E', "e");
246
247 if normalized.contains('e') {
248 // Use scientific notation parsing
249 Decimal::from_scientific(&normalized).ok()
250 } else {
251 // Use regular decimal parsing
252 normalized.parse::<Decimal>().ok()
253 }
254 }
255
256 /// Returns the parsed decimal value if parsing was successful.
257 ///
258 /// This method provides access to the mathematical value for arithmetic
259 /// operations and comparisons. Returns `None` if the original string
260 /// could not be parsed as a valid decimal.
261 ///
262 /// # Examples
263 ///
264 /// ```rust
265 /// use helios_fhir::PreciseDecimal;
266 /// use rust_decimal::Decimal;
267 ///
268 /// let precise = PreciseDecimal::from(Decimal::new(1234, 2)); // 12.34
269 /// assert_eq!(precise.value(), Some(Decimal::new(1234, 2)));
270 ///
271 /// let invalid = PreciseDecimal::from_parts(None, "invalid".to_string());
272 /// assert_eq!(invalid.value(), None);
273 /// ```
274 pub fn value(&self) -> Option<Decimal> {
275 self.value
276 }
277
278 /// Returns the original string representation.
279 ///
280 /// This method provides access to the exact string format that was used
281 /// to create this `PreciseDecimal`. This string is used during serialization
282 /// to maintain FHIR's precision requirements.
283 ///
284 /// # Examples
285 ///
286 /// ```rust
287 /// use helios_fhir::PreciseDecimal;
288 /// use rust_decimal::Decimal;
289 ///
290 /// let precise = PreciseDecimal::from_parts(
291 /// Some(Decimal::new(100, 2)),
292 /// "1.00".to_string()
293 /// );
294 /// assert_eq!(precise.original_string(), "1.00");
295 /// ```
296 pub fn original_string(&self) -> &str {
297 &self.original_string
298 }
299}
300
301/// Converts a `Decimal` to `PreciseDecimal` with derived string representation.
302///
303/// This implementation allows easy conversion from `rust_decimal::Decimal` values
304/// by automatically generating the string representation using the decimal's
305/// `Display` implementation.
306///
307/// # Examples
308///
309/// ```rust
310/// use helios_fhir::PreciseDecimal;
311/// use rust_decimal::Decimal;
312///
313/// let decimal = Decimal::new(12345, 3); // 12.345
314/// let precise: PreciseDecimal = decimal.into();
315/// assert_eq!(precise.value(), Some(decimal));
316/// assert_eq!(precise.original_string(), "12.345");
317/// ```
318impl From<Decimal> for PreciseDecimal {
319 fn from(value: Decimal) -> Self {
320 // Generate string representation from the decimal value
321 let original_string = Arc::from(value.to_string());
322 Self {
323 value: Some(value),
324 original_string,
325 }
326 }
327}
328
329/// Implements serialization for `PreciseDecimal` preserving original format.
330///
331/// This implementation ensures that the exact original string representation
332/// is preserved during JSON serialization, maintaining FHIR's precision
333/// requirements including trailing zeros and specific formatting.
334///
335/// # FHIR Compliance
336///
337/// FHIR requires that decimal values maintain their original precision when
338/// round-tripped through JSON. This implementation uses `serde_json::RawValue`
339/// to serialize the original string directly as a JSON number.
340///
341/// # Examples
342///
343/// ```rust
344/// use helios_fhir::PreciseDecimal;
345/// use rust_decimal::Decimal;
346/// use serde_json;
347///
348/// let precise = PreciseDecimal::from_parts(
349/// Some(Decimal::new(1230, 2)),
350/// "12.30".to_string()
351/// );
352///
353/// let json = serde_json::to_string(&precise).unwrap();
354/// assert_eq!(json, "12.30"); // Preserves trailing zero
355/// ```
356impl Serialize for PreciseDecimal {
357 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
358 where
359 S: Serializer,
360 {
361 // Use RawValue to preserve exact string format in JSON
362 match serde_json::value::RawValue::from_string(self.original_string.to_string()) {
363 Ok(raw_value) => raw_value.serialize(serializer),
364 Err(e) => Err(serde::ser::Error::custom(format!(
365 "Failed to serialize PreciseDecimal '{}': {}",
366 self.original_string, e
367 ))),
368 }
369 }
370}
371
372/// Implements deserialization for `PreciseDecimal` preserving original format.
373///
374/// This implementation deserializes JSON numbers and strings into `PreciseDecimal`
375/// while preserving the exact original string representation. It handles various
376/// JSON formats including scientific notation and nested object structures.
377///
378/// # Supported Formats
379///
380/// - Direct numbers: `12.340`
381/// - String numbers: `"12.340"`
382/// - Scientific notation: `1.234e2` or `1.234E2`
383/// - Nested objects: `{"value": 12.340}` (for macro-generated structures)
384///
385/// # Examples
386///
387/// ```rust
388/// use helios_fhir::PreciseDecimal;
389/// use serde_json;
390///
391/// // Deserialize from JSON number (trailing zeros are normalized)
392/// let precise: PreciseDecimal = serde_json::from_str("12.340").unwrap();
393/// assert_eq!(precise.original_string(), "12.340"); // JSON number format
394///
395/// // Deserialize from JSON string (preserves exact format)
396/// let precise: PreciseDecimal = serde_json::from_str("\"12.340\"").unwrap();
397/// assert_eq!(precise.original_string(), "12.340"); // Preserves string format
398/// ```
399impl<'de> Deserialize<'de> for PreciseDecimal {
400 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
401 where
402 D: Deserializer<'de>,
403 {
404 // Use intermediate Value to capture exact string representation
405 let json_value = serde_json::Value::deserialize(deserializer)?;
406
407 match json_value {
408 serde_json::Value::Number(n) => {
409 // Extract string representation from JSON number
410 let original_string = n.to_string();
411 let parsed_value = Self::parse_decimal_string(&original_string);
412 Ok(PreciseDecimal::from_parts(parsed_value, original_string))
413 }
414 serde_json::Value::String(s) => {
415 // Use string value directly (preserves exact format)
416 let parsed_value = Self::parse_decimal_string(&s);
417 Ok(PreciseDecimal::from_parts(parsed_value, s))
418 }
419 // Handle nested object format (for macro-generated structures)
420 serde_json::Value::Object(map) => match map.get("value") {
421 Some(serde_json::Value::Number(n)) => {
422 let original_string = n.to_string();
423 let parsed_value = Self::parse_decimal_string(&original_string);
424 Ok(PreciseDecimal::from_parts(parsed_value, original_string))
425 }
426 Some(serde_json::Value::String(s)) => {
427 let original_string = s.clone();
428 let parsed_value = Self::parse_decimal_string(&original_string);
429 Ok(PreciseDecimal::from_parts(parsed_value, original_string))
430 }
431 Some(serde_json::Value::Null) => Err(de::Error::invalid_value(
432 de::Unexpected::Unit,
433 &"a number or string for decimal value",
434 )),
435 None => Err(de::Error::missing_field("value")),
436 _ => Err(de::Error::invalid_type(
437 de::Unexpected::Map,
438 &"a map with a 'value' field containing a number or string",
439 )),
440 },
441 // Handle remaining unexpected types
442 other => Err(de::Error::invalid_type(
443 match other {
444 serde_json::Value::Null => de::Unexpected::Unit, // Or Unexpected::Option if mapping null to None
445 serde_json::Value::Bool(b) => de::Unexpected::Bool(b),
446 serde_json::Value::Array(_) => de::Unexpected::Seq,
447 _ => de::Unexpected::Other("unexpected JSON type for PreciseDecimal"),
448 },
449 &"a number, string, or object with a 'value' field",
450 )),
451 }
452 }
453}
454
455// --- End PreciseDecimal ---
456
457/// Precision levels for FHIR Date values.
458///
459/// FHIR dates support partial precision, allowing year-only, year-month,
460/// or full date specifications. This enum tracks which components are present.
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
462pub enum DatePrecision {
463 /// Year only (YYYY)
464 Year,
465 /// Year and month (YYYY-MM)
466 YearMonth,
467 /// Full date (YYYY-MM-DD)
468 Full,
469}
470
471/// Precision levels for FHIR Time values.
472///
473/// FHIR times support partial precision from hour-only through
474/// sub-second precision. This enum tracks which components are present.
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
476pub enum TimePrecision {
477 /// Hour only (HH)
478 Hour,
479 /// Hour and minute (HH:MM)
480 HourMinute,
481 /// Hour, minute, and second (HH:MM:SS)
482 HourMinuteSecond,
483 /// Full time with sub-second precision (HH:MM:SS.sss)
484 Millisecond,
485}
486
487/// Precision levels for FHIR DateTime values.
488///
489/// FHIR datetimes support partial precision from year-only through
490/// sub-second precision with optional timezone information.
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
492pub enum DateTimePrecision {
493 /// Year only (YYYY)
494 Year,
495 /// Year and month (YYYY-MM)
496 YearMonth,
497 /// Date only (YYYY-MM-DD)
498 Date,
499 /// Date with hour (YYYY-MM-DDTHH)
500 DateHour,
501 /// Date with hour and minute (YYYY-MM-DDTHH:MM)
502 DateHourMinute,
503 /// Date with time to seconds (YYYY-MM-DDTHH:MM:SS)
504 DateHourMinuteSecond,
505 /// Full datetime with sub-second precision (YYYY-MM-DDTHH:MM:SS.sss)
506 Full,
507}
508
509impl Default for PrecisionDate {
510 fn default() -> Self {
511 // Default to epoch date 1970-01-01
512 Self::from_ymd(1970, 1, 1)
513 }
514}
515
516/// Precision-aware FHIR Date type.
517///
518/// This type preserves the original precision and string representation
519/// of FHIR date values while providing typed access to date components.
520///
521/// # FHIR Date Formats
522/// - `YYYY` - Year only
523/// - `YYYY-MM` - Year and month
524/// - `YYYY-MM-DD` - Full date
525///
526/// # Examples
527/// ```rust
528/// use helios_fhir::{PrecisionDate, DatePrecision};
529///
530/// // Create a year-only date
531/// let year_date = PrecisionDate::from_year(2023);
532/// assert_eq!(year_date.precision(), DatePrecision::Year);
533/// assert_eq!(year_date.original_string(), "2023");
534///
535/// // Create a full date
536/// let full_date = PrecisionDate::from_ymd(2023, 3, 15);
537/// assert_eq!(full_date.precision(), DatePrecision::Full);
538/// assert_eq!(full_date.original_string(), "2023-03-15");
539/// ```
540#[derive(Debug, Clone, PartialEq, Eq)]
541pub struct PrecisionDate {
542 /// Year component (always present)
543 year: i32,
544 /// Month component (1-12, None for year-only precision)
545 month: Option<u32>,
546 /// Day component (1-31, None for year or year-month precision)
547 day: Option<u32>,
548 /// Precision level of this date
549 precision: DatePrecision,
550 /// Original string representation
551 original_string: Arc<str>,
552}
553
554impl PrecisionDate {
555 /// Creates a year-only precision date.
556 pub fn from_year(year: i32) -> Self {
557 Self {
558 year,
559 month: None,
560 day: None,
561 precision: DatePrecision::Year,
562 original_string: Arc::from(format!("{:04}", year)),
563 }
564 }
565
566 /// Creates a year-month precision date.
567 pub fn from_year_month(year: i32, month: u32) -> Self {
568 Self {
569 year,
570 month: Some(month),
571 day: None,
572 precision: DatePrecision::YearMonth,
573 original_string: Arc::from(format!("{:04}-{:02}", year, month)),
574 }
575 }
576
577 /// Creates a full precision date.
578 pub fn from_ymd(year: i32, month: u32, day: u32) -> Self {
579 Self {
580 year,
581 month: Some(month),
582 day: Some(day),
583 precision: DatePrecision::Full,
584 original_string: Arc::from(format!("{:04}-{:02}-{:02}", year, month, day)),
585 }
586 }
587
588 /// Parses a FHIR date string, preserving precision.
589 pub fn parse(s: &str) -> Option<Self> {
590 // Remove @ prefix if present
591 let s = s.strip_prefix('@').unwrap_or(s);
592
593 let parts: Vec<&str> = s.split('-').collect();
594 match parts.len() {
595 1 => {
596 // Year only
597 let year = parts[0].parse::<i32>().ok()?;
598 Some(Self {
599 year,
600 month: None,
601 day: None,
602 precision: DatePrecision::Year,
603 original_string: Arc::from(s),
604 })
605 }
606 2 => {
607 // Year-month
608 let year = parts[0].parse::<i32>().ok()?;
609 let month = parts[1].parse::<u32>().ok()?;
610 if !(1..=12).contains(&month) {
611 return None;
612 }
613 Some(Self {
614 year,
615 month: Some(month),
616 day: None,
617 precision: DatePrecision::YearMonth,
618 original_string: Arc::from(s),
619 })
620 }
621 3 => {
622 // Full date
623 let year = parts[0].parse::<i32>().ok()?;
624 let month = parts[1].parse::<u32>().ok()?;
625 let day = parts[2].parse::<u32>().ok()?;
626 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
627 return None;
628 }
629 Some(Self {
630 year,
631 month: Some(month),
632 day: Some(day),
633 precision: DatePrecision::Full,
634 original_string: Arc::from(s),
635 })
636 }
637 _ => None,
638 }
639 }
640
641 /// Returns the precision level of this date.
642 pub fn precision(&self) -> DatePrecision {
643 self.precision
644 }
645
646 /// Returns the original string representation.
647 pub fn original_string(&self) -> &str {
648 &self.original_string
649 }
650
651 /// Returns the year component.
652 pub fn year(&self) -> i32 {
653 self.year
654 }
655
656 /// Returns the month component if present.
657 pub fn month(&self) -> Option<u32> {
658 self.month
659 }
660
661 /// Returns the day component if present.
662 pub fn day(&self) -> Option<u32> {
663 self.day
664 }
665
666 /// Converts to a NaiveDate, using defaults for missing components.
667 pub fn to_naive_date(&self) -> NaiveDate {
668 NaiveDate::from_ymd_opt(self.year, self.month.unwrap_or(1), self.day.unwrap_or(1))
669 .expect("Valid date components")
670 }
671
672 /// Compares two dates considering precision.
673 /// Returns None if comparison is indeterminate due to precision differences.
674 pub fn compare(&self, other: &Self) -> Option<Ordering> {
675 // Compare years first
676 match self.year.cmp(&other.year) {
677 Ordering::Equal => {
678 // Years are equal, check month precision
679 match (self.month, other.month) {
680 (None, None) => Some(Ordering::Equal),
681 (None, Some(_)) | (Some(_), None) => {
682 // Different precisions - comparison may be indeterminate
683 // For < and > we can still determine, but for = it's indeterminate
684 None
685 }
686 (Some(m1), Some(m2)) => match m1.cmp(&m2) {
687 Ordering::Equal => {
688 // Months are equal, check day precision
689 match (self.day, other.day) {
690 (None, None) => Some(Ordering::Equal),
691 (None, Some(_)) | (Some(_), None) => {
692 // Different precisions - indeterminate
693 None
694 }
695 (Some(d1), Some(d2)) => Some(d1.cmp(&d2)),
696 }
697 }
698 other => Some(other),
699 },
700 }
701 }
702 other => Some(other),
703 }
704 }
705}
706
707impl Default for PrecisionTime {
708 fn default() -> Self {
709 // Default to midnight 00:00:00
710 Self::from_hms(0, 0, 0)
711 }
712}
713
714/// Precision-aware FHIR Time type.
715///
716/// This type preserves the original precision and string representation
717/// of FHIR time values. Note that FHIR times do not support timezone information.
718///
719/// # FHIR Time Formats
720/// - `HH` - Hour only
721/// - `HH:MM` - Hour and minute
722/// - `HH:MM:SS` - Hour, minute, and second
723/// - `HH:MM:SS.sss` - Full time with milliseconds
724///
725/// # Examples
726/// ```rust
727/// use helios_fhir::{PrecisionTime, TimePrecision};
728///
729/// // Create an hour-only time
730/// let hour_time = PrecisionTime::from_hour(14);
731/// assert_eq!(hour_time.precision(), TimePrecision::Hour);
732/// assert_eq!(hour_time.original_string(), "14");
733///
734/// // Create a full precision time
735/// let full_time = PrecisionTime::from_hms_milli(14, 30, 45, 123);
736/// assert_eq!(full_time.precision(), TimePrecision::Millisecond);
737/// assert_eq!(full_time.original_string(), "14:30:45.123");
738/// ```
739#[derive(Debug, Clone, PartialEq, Eq)]
740pub struct PrecisionTime {
741 /// Hour component (0-23, always present)
742 hour: u32,
743 /// Minute component (0-59)
744 minute: Option<u32>,
745 /// Second component (0-59)
746 second: Option<u32>,
747 /// Millisecond component (0-999)
748 millisecond: Option<u32>,
749 /// Precision level of this time
750 precision: TimePrecision,
751 /// Original string representation
752 original_string: Arc<str>,
753}
754
755impl PrecisionTime {
756 /// Creates an hour-only precision time.
757 pub fn from_hour(hour: u32) -> Self {
758 Self {
759 hour,
760 minute: None,
761 second: None,
762 millisecond: None,
763 precision: TimePrecision::Hour,
764 original_string: Arc::from(format!("{:02}", hour)),
765 }
766 }
767
768 /// Creates an hour-minute precision time.
769 pub fn from_hm(hour: u32, minute: u32) -> Self {
770 Self {
771 hour,
772 minute: Some(minute),
773 second: None,
774 millisecond: None,
775 precision: TimePrecision::HourMinute,
776 original_string: Arc::from(format!("{:02}:{:02}", hour, minute)),
777 }
778 }
779
780 /// Creates an hour-minute-second precision time.
781 pub fn from_hms(hour: u32, minute: u32, second: u32) -> Self {
782 Self {
783 hour,
784 minute: Some(minute),
785 second: Some(second),
786 millisecond: None,
787 precision: TimePrecision::HourMinuteSecond,
788 original_string: Arc::from(format!("{:02}:{:02}:{:02}", hour, minute, second)),
789 }
790 }
791
792 /// Creates a full precision time with milliseconds.
793 pub fn from_hms_milli(hour: u32, minute: u32, second: u32, millisecond: u32) -> Self {
794 Self {
795 hour,
796 minute: Some(minute),
797 second: Some(second),
798 millisecond: Some(millisecond),
799 precision: TimePrecision::Millisecond,
800 original_string: Arc::from(format!(
801 "{:02}:{:02}:{:02}.{:03}",
802 hour, minute, second, millisecond
803 )),
804 }
805 }
806
807 /// Parses a FHIR time string, preserving precision.
808 pub fn parse(s: &str) -> Option<Self> {
809 // Remove @ and T prefixes if present
810 let s = s.strip_prefix('@').unwrap_or(s);
811 let s = s.strip_prefix('T').unwrap_or(s);
812
813 // Check for timezone (not allowed in FHIR time)
814 if s.contains('+') || s.contains('-') || s.ends_with('Z') {
815 return None;
816 }
817
818 let parts: Vec<&str> = s.split(':').collect();
819 match parts.len() {
820 1 => {
821 // Hour only
822 let hour = parts[0].parse::<u32>().ok()?;
823 if hour > 23 {
824 return None;
825 }
826 Some(Self {
827 hour,
828 minute: None,
829 second: None,
830 millisecond: None,
831 precision: TimePrecision::Hour,
832 original_string: Arc::from(s),
833 })
834 }
835 2 => {
836 // Hour:minute
837 let hour = parts[0].parse::<u32>().ok()?;
838 let minute = parts[1].parse::<u32>().ok()?;
839 if hour > 23 || minute > 59 {
840 return None;
841 }
842 Some(Self {
843 hour,
844 minute: Some(minute),
845 second: None,
846 millisecond: None,
847 precision: TimePrecision::HourMinute,
848 original_string: Arc::from(s),
849 })
850 }
851 3 => {
852 // Hour:minute:second[.millisecond]
853 let hour = parts[0].parse::<u32>().ok()?;
854 let minute = parts[1].parse::<u32>().ok()?;
855
856 // Check for milliseconds
857 let (second, millisecond, precision) = if parts[2].contains('.') {
858 let sec_parts: Vec<&str> = parts[2].split('.').collect();
859 if sec_parts.len() != 2 {
860 return None;
861 }
862 let second = sec_parts[0].parse::<u32>().ok()?;
863 // Parse milliseconds, padding or truncating as needed
864 let ms_str = sec_parts[1];
865 let ms = if ms_str.len() <= 3 {
866 // Pad with zeros if needed
867 let padded = format!("{:0<3}", ms_str);
868 padded.parse::<u32>().ok()?
869 } else {
870 // Truncate to 3 digits
871 ms_str[..3].parse::<u32>().ok()?
872 };
873 (second, Some(ms), TimePrecision::Millisecond)
874 } else {
875 let second = parts[2].parse::<u32>().ok()?;
876 (second, None, TimePrecision::HourMinuteSecond)
877 };
878
879 if hour > 23 || minute > 59 || second > 59 {
880 return None;
881 }
882
883 Some(Self {
884 hour,
885 minute: Some(minute),
886 second: Some(second),
887 millisecond,
888 precision,
889 original_string: Arc::from(s),
890 })
891 }
892 _ => None,
893 }
894 }
895
896 /// Returns the precision level of this time.
897 pub fn precision(&self) -> TimePrecision {
898 self.precision
899 }
900
901 /// Returns the original string representation.
902 pub fn original_string(&self) -> &str {
903 &self.original_string
904 }
905
906 /// Converts to a NaiveTime, using defaults for missing components.
907 pub fn to_naive_time(&self) -> NaiveTime {
908 let milli = self.millisecond.unwrap_or(0);
909 let micro = milli * 1000; // Convert milliseconds to microseconds
910 NaiveTime::from_hms_micro_opt(
911 self.hour,
912 self.minute.unwrap_or(0),
913 self.second.unwrap_or(0),
914 micro,
915 )
916 .expect("Valid time components")
917 }
918
919 /// Compares two times considering precision.
920 /// Per FHIRPath spec: seconds and milliseconds are considered the same precision level
921 pub fn compare(&self, other: &Self) -> Option<Ordering> {
922 match self.hour.cmp(&other.hour) {
923 Ordering::Equal => {
924 match (self.minute, other.minute) {
925 (None, None) => Some(Ordering::Equal),
926 (None, Some(_)) | (Some(_), None) => None,
927 (Some(m1), Some(m2)) => match m1.cmp(&m2) {
928 Ordering::Equal => {
929 match (self.second, other.second) {
930 (None, None) => Some(Ordering::Equal),
931 (None, Some(_)) | (Some(_), None) => None,
932 (Some(s1), Some(s2)) => {
933 // Per FHIRPath spec: second and millisecond precisions are
934 // considered a single precision using decimal comparison
935 let ms1 = self.millisecond.unwrap_or(0);
936 let ms2 = other.millisecond.unwrap_or(0);
937 let total1 = s1 * 1000 + ms1;
938 let total2 = s2 * 1000 + ms2;
939 Some(total1.cmp(&total2))
940 }
941 }
942 }
943 other => Some(other),
944 },
945 }
946 }
947 other => Some(other),
948 }
949 }
950}
951
952impl Default for PrecisionDateTime {
953 fn default() -> Self {
954 // Default to Unix epoch 1970-01-01T00:00:00
955 Self::from_date(1970, 1, 1)
956 }
957}
958
959/// Precision-aware FHIR DateTime type.
960///
961/// This type preserves the original precision and string representation
962/// of FHIR datetime values, including timezone information when present.
963///
964/// # FHIR DateTime Formats
965/// - `YYYY` - Year only
966/// - `YYYY-MM` - Year and month
967/// - `YYYY-MM-DD` - Date only
968/// - `YYYY-MM-DDTHH` - Date with hour
969/// - `YYYY-MM-DDTHH:MM` - Date with hour and minute
970/// - `YYYY-MM-DDTHH:MM:SS` - Date with time to seconds
971/// - `YYYY-MM-DDTHH:MM:SS.sss` - Full datetime with milliseconds
972/// - All time formats can include timezone: `Z`, `+HH:MM`, `-HH:MM`
973///
974/// # Examples
975/// ```rust
976/// use helios_fhir::{PrecisionDateTime, DateTimePrecision};
977///
978/// // Create a date-only datetime
979/// let date_dt = PrecisionDateTime::from_date(2023, 3, 15);
980/// assert_eq!(date_dt.precision(), DateTimePrecision::Date);
981/// assert_eq!(date_dt.original_string(), "2023-03-15");
982///
983/// // Create a full datetime with timezone
984/// let full_dt = PrecisionDateTime::parse("2023-03-15T14:30:45.123Z").unwrap();
985/// assert_eq!(full_dt.precision(), DateTimePrecision::Full);
986/// ```
987#[derive(Debug, Clone, PartialEq, Eq)]
988pub struct PrecisionDateTime {
989 /// Date components
990 pub date: PrecisionDate,
991 /// Time components (if precision includes time)
992 time: Option<PrecisionTime>,
993 /// Timezone offset in minutes from UTC (None means local/unspecified)
994 timezone_offset: Option<i32>,
995 /// Precision level of this datetime
996 precision: DateTimePrecision,
997 /// Original string representation
998 original_string: Arc<str>,
999}
1000
1001impl PrecisionDateTime {
1002 /// Creates a year-only datetime.
1003 pub fn from_year(year: i32) -> Self {
1004 let date = PrecisionDate::from_year(year);
1005 Self {
1006 original_string: date.original_string.clone(),
1007 date,
1008 time: None,
1009 timezone_offset: None,
1010 precision: DateTimePrecision::Year,
1011 }
1012 }
1013
1014 /// Creates a year-month datetime.
1015 pub fn from_year_month(year: i32, month: u32) -> Self {
1016 let date = PrecisionDate::from_year_month(year, month);
1017 Self {
1018 original_string: date.original_string.clone(),
1019 date,
1020 time: None,
1021 timezone_offset: None,
1022 precision: DateTimePrecision::YearMonth,
1023 }
1024 }
1025
1026 /// Creates a date-only datetime.
1027 pub fn from_date(year: i32, month: u32, day: u32) -> Self {
1028 let date = PrecisionDate::from_ymd(year, month, day);
1029 Self {
1030 original_string: date.original_string.clone(),
1031 date,
1032 time: None,
1033 timezone_offset: None,
1034 precision: DateTimePrecision::Date,
1035 }
1036 }
1037
1038 /// Parses a FHIR datetime string, preserving precision and timezone.
1039 pub fn parse(s: &str) -> Option<Self> {
1040 // Remove @ prefix if present
1041 let s = s.strip_prefix('@').unwrap_or(s);
1042
1043 // Check for 'T' separator to determine if time is present
1044 if let Some(t_pos) = s.find('T') {
1045 let date_part = &s[..t_pos];
1046 let time_and_tz = &s[t_pos + 1..];
1047
1048 // Parse date part
1049 let date = PrecisionDate::parse(date_part)?;
1050
1051 // Check for timezone at the end
1052 let (time_part, timezone_offset) = if let Some(stripped) = time_and_tz.strip_suffix('Z')
1053 {
1054 (stripped, Some(0))
1055 } else if let Some(plus_pos) = time_and_tz.rfind('+') {
1056 let tz_str = &time_and_tz[plus_pos + 1..];
1057 let offset = Self::parse_timezone_offset(tz_str)?;
1058 (&time_and_tz[..plus_pos], Some(offset))
1059 } else if let Some(minus_pos) = time_and_tz.rfind('-') {
1060 // Be careful not to confuse negative timezone with date separator
1061 if minus_pos > 0 && time_and_tz[..minus_pos].contains(':') {
1062 let tz_str = &time_and_tz[minus_pos + 1..];
1063 let offset = Self::parse_timezone_offset(tz_str)?;
1064 (&time_and_tz[..minus_pos], Some(-offset))
1065 } else {
1066 (time_and_tz, None)
1067 }
1068 } else {
1069 (time_and_tz, None)
1070 };
1071
1072 // Parse time part if not empty
1073 let (time, precision) = if time_part.is_empty() {
1074 // Just "T" with no time components (partial datetime)
1075 (
1076 None,
1077 match date.precision {
1078 DatePrecision::Full => DateTimePrecision::Date,
1079 DatePrecision::YearMonth => DateTimePrecision::YearMonth,
1080 DatePrecision::Year => DateTimePrecision::Year,
1081 },
1082 )
1083 } else {
1084 let time = PrecisionTime::parse(time_part)?;
1085 let precision = match time.precision {
1086 TimePrecision::Hour => DateTimePrecision::DateHour,
1087 TimePrecision::HourMinute => DateTimePrecision::DateHourMinute,
1088 TimePrecision::HourMinuteSecond => DateTimePrecision::DateHourMinuteSecond,
1089 TimePrecision::Millisecond => DateTimePrecision::Full,
1090 };
1091 (Some(time), precision)
1092 };
1093
1094 Some(Self {
1095 date,
1096 time,
1097 timezone_offset,
1098 precision,
1099 original_string: Arc::from(s),
1100 })
1101 } else {
1102 // No 'T' separator, just a date
1103 let date = PrecisionDate::parse(s)?;
1104 let precision = match date.precision {
1105 DatePrecision::Year => DateTimePrecision::Year,
1106 DatePrecision::YearMonth => DateTimePrecision::YearMonth,
1107 DatePrecision::Full => DateTimePrecision::Date,
1108 };
1109
1110 Some(Self {
1111 original_string: Arc::from(s),
1112 date,
1113 time: None,
1114 timezone_offset: None,
1115 precision,
1116 })
1117 }
1118 }
1119
1120 /// Parses a timezone offset string (e.g., "05:30") into minutes.
1121 fn parse_timezone_offset(s: &str) -> Option<i32> {
1122 let parts: Vec<&str> = s.split(':').collect();
1123 match parts.len() {
1124 1 => {
1125 // Just hours
1126 let hours = parts[0].parse::<i32>().ok()?;
1127 Some(hours * 60)
1128 }
1129 2 => {
1130 // Hours and minutes
1131 let hours = parts[0].parse::<i32>().ok()?;
1132 let minutes = parts[1].parse::<i32>().ok()?;
1133 Some(hours * 60 + minutes)
1134 }
1135 _ => None,
1136 }
1137 }
1138
1139 /// Creates a PrecisionDateTime from a PrecisionDate (for date to datetime conversion).
1140 pub fn from_precision_date(date: PrecisionDate) -> Self {
1141 let precision = match date.precision {
1142 DatePrecision::Year => DateTimePrecision::Year,
1143 DatePrecision::YearMonth => DateTimePrecision::YearMonth,
1144 DatePrecision::Full => DateTimePrecision::Date,
1145 };
1146 Self {
1147 original_string: date.original_string.clone(),
1148 date,
1149 time: None,
1150 timezone_offset: None,
1151 precision,
1152 }
1153 }
1154
1155 /// Returns the precision level of this datetime.
1156 pub fn precision(&self) -> DateTimePrecision {
1157 self.precision
1158 }
1159
1160 /// Returns the original string representation.
1161 pub fn original_string(&self) -> &str {
1162 &self.original_string
1163 }
1164
1165 /// Converts to a chrono DateTime<Utc>, using defaults for missing components.
1166 pub fn to_chrono_datetime(&self) -> ChronoDateTime<Utc> {
1167 let naive_date = self.date.to_naive_date();
1168 let naive_time = self
1169 .time
1170 .as_ref()
1171 .map(|t| t.to_naive_time())
1172 .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
1173
1174 let naive_dt = naive_date.and_time(naive_time);
1175
1176 // Apply timezone offset if present
1177 if let Some(offset_minutes) = self.timezone_offset {
1178 // The datetime is in local time with the given offset
1179 // We need to subtract the offset to get UTC
1180 let utc_naive = naive_dt - chrono::Duration::minutes(offset_minutes as i64);
1181 ChronoDateTime::<Utc>::from_naive_utc_and_offset(utc_naive, Utc)
1182 } else {
1183 // No timezone means we assume UTC
1184 ChronoDateTime::<Utc>::from_naive_utc_and_offset(naive_dt, Utc)
1185 }
1186 }
1187
1188 /// Compares two datetimes considering precision and timezones.
1189 pub fn compare(&self, other: &Self) -> Option<Ordering> {
1190 // Check if precisions are compatible
1191 // Per FHIRPath spec: seconds and milliseconds are the same precision
1192 let self_precision_normalized = match self.precision {
1193 DateTimePrecision::Full => DateTimePrecision::DateHourMinuteSecond,
1194 p => p,
1195 };
1196 let other_precision_normalized = match other.precision {
1197 DateTimePrecision::Full => DateTimePrecision::DateHourMinuteSecond,
1198 p => p,
1199 };
1200
1201 // If precisions don't match (except for seconds/milliseconds), return None
1202 if self_precision_normalized != other_precision_normalized {
1203 // Special handling for date vs datetime with time components
1204 if self.time.is_none() != other.time.is_none() {
1205 return None;
1206 }
1207 }
1208
1209 // If both have sufficient precision and timezone info, compare as full datetimes
1210 if self.precision >= DateTimePrecision::DateHour
1211 && other.precision >= DateTimePrecision::DateHour
1212 && self.timezone_offset.is_some()
1213 && other.timezone_offset.is_some()
1214 {
1215 // Convert to UTC and compare
1216 return Some(self.to_chrono_datetime().cmp(&other.to_chrono_datetime()));
1217 }
1218
1219 // If one has timezone and the other doesn't, comparison is indeterminate
1220 if self.timezone_offset.is_some() != other.timezone_offset.is_some() {
1221 return None;
1222 }
1223
1224 // Otherwise, compare components with precision awareness
1225 match self.date.compare(&other.date) {
1226 Some(Ordering::Equal) => {
1227 // Dates are equal at their precision level
1228 match (&self.time, &other.time) {
1229 (None, None) => Some(Ordering::Equal),
1230 (None, Some(_)) | (Some(_), None) => None, // Different precisions
1231 (Some(t1), Some(t2)) => t1.compare(t2),
1232 }
1233 }
1234 other => other,
1235 }
1236 }
1237}
1238
1239// === Display Implementations for Precision Types ===
1240
1241impl std::fmt::Display for PrecisionDate {
1242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1243 write!(f, "{}", self.original_string)
1244 }
1245}
1246
1247impl std::fmt::Display for PrecisionDateTime {
1248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1249 write!(f, "{}", self.original_string)
1250 }
1251}
1252
1253impl std::fmt::Display for PrecisionTime {
1254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1255 write!(f, "{}", self.original_string)
1256 }
1257}
1258
1259// === Serde Implementations for Precision Types ===
1260
1261impl Serialize for PrecisionDate {
1262 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1263 where
1264 S: Serializer,
1265 {
1266 // Serialize as a simple string
1267 serializer.serialize_str(&self.original_string)
1268 }
1269}
1270
1271impl<'de> Deserialize<'de> for PrecisionDate {
1272 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1273 where
1274 D: Deserializer<'de>,
1275 {
1276 let s = String::deserialize(deserializer)?;
1277 PrecisionDate::parse(&s)
1278 .ok_or_else(|| de::Error::custom(format!("Invalid FHIR date format: {}", s)))
1279 }
1280}
1281
1282impl Serialize for PrecisionTime {
1283 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1284 where
1285 S: Serializer,
1286 {
1287 // Serialize as a simple string
1288 serializer.serialize_str(&self.original_string)
1289 }
1290}
1291
1292impl<'de> Deserialize<'de> for PrecisionTime {
1293 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1294 where
1295 D: Deserializer<'de>,
1296 {
1297 let s = String::deserialize(deserializer)?;
1298 PrecisionTime::parse(&s)
1299 .ok_or_else(|| de::Error::custom(format!("Invalid FHIR time format: {}", s)))
1300 }
1301}
1302
1303impl Serialize for PrecisionDateTime {
1304 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1305 where
1306 S: Serializer,
1307 {
1308 // Serialize as a simple string
1309 serializer.serialize_str(&self.original_string)
1310 }
1311}
1312
1313impl<'de> Deserialize<'de> for PrecisionDateTime {
1314 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1315 where
1316 D: Deserializer<'de>,
1317 {
1318 let s = String::deserialize(deserializer)?;
1319 PrecisionDateTime::parse(&s)
1320 .ok_or_else(|| de::Error::custom(format!("Invalid FHIR datetime format: {}", s)))
1321 }
1322}
1323
1324// === PrecisionInstant Implementation ===
1325
1326/// A FHIR instant value that preserves the original string representation and precision.
1327///
1328/// Instants in FHIR must be complete date-time values with timezone information,
1329/// representing a specific moment in time. This type wraps PrecisionDateTime but
1330/// enforces instant-specific constraints.
1331#[derive(Debug, Clone, PartialEq, Eq, Default)]
1332pub struct PrecisionInstant {
1333 inner: PrecisionDateTime,
1334}
1335
1336impl PrecisionInstant {
1337 /// Parses a FHIR instant string.
1338 /// Returns None if the string is not a valid instant (must have full date, time, and timezone).
1339 pub fn parse(s: &str) -> Option<Self> {
1340 // Parse as PrecisionDateTime first
1341 let dt = PrecisionDateTime::parse(s)?;
1342
1343 // For now, accept any valid datetime as an instant
1344 // In strict mode, we could require timezone, but many FHIR resources
1345 // use instant fields without explicit timezones
1346 Some(PrecisionInstant { inner: dt })
1347 }
1348
1349 /// Returns the original string representation
1350 pub fn original_string(&self) -> &str {
1351 self.inner.original_string()
1352 }
1353
1354 /// Get the inner PrecisionDateTime
1355 pub fn as_datetime(&self) -> &PrecisionDateTime {
1356 &self.inner
1357 }
1358
1359 /// Convert to chrono DateTime<Utc>
1360 pub fn to_chrono_datetime(&self) -> ChronoDateTime<Utc> {
1361 // PrecisionDateTime::to_chrono_datetime returns ChronoDateTime<Utc>
1362 self.inner.to_chrono_datetime()
1363 }
1364}
1365
1366impl fmt::Display for PrecisionInstant {
1367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1368 write!(f, "{}", self.inner)
1369 }
1370}
1371
1372impl Serialize for PrecisionInstant {
1373 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1374 where
1375 S: Serializer,
1376 {
1377 self.inner.serialize(serializer)
1378 }
1379}
1380
1381impl<'de> Deserialize<'de> for PrecisionInstant {
1382 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1383 where
1384 D: Deserializer<'de>,
1385 {
1386 let s = String::deserialize(deserializer)?;
1387 PrecisionInstant::parse(&s)
1388 .ok_or_else(|| de::Error::custom(format!("Invalid FHIR instant format: {}", s)))
1389 }
1390}
1391
1392// === IntoEvaluationResult Implementations for Precision Types ===
1393
1394impl IntoEvaluationResult for PrecisionDate {
1395 fn to_evaluation_result(&self) -> EvaluationResult {
1396 EvaluationResult::date(self.original_string.to_string())
1397 }
1398}
1399
1400impl IntoEvaluationResult for PrecisionTime {
1401 fn to_evaluation_result(&self) -> EvaluationResult {
1402 EvaluationResult::time(self.original_string.to_string())
1403 }
1404}
1405
1406impl IntoEvaluationResult for PrecisionDateTime {
1407 fn to_evaluation_result(&self) -> EvaluationResult {
1408 EvaluationResult::datetime(self.original_string.to_string())
1409 }
1410}
1411
1412impl IntoEvaluationResult for PrecisionInstant {
1413 fn to_evaluation_result(&self) -> EvaluationResult {
1414 // Return as datetime with instant type info
1415 EvaluationResult::DateTime(
1416 self.inner.original_string.to_string(),
1417 Some(TypeInfoResult::new("FHIR", "instant")),
1418 None,
1419 )
1420 }
1421}
1422
1423// Removed DecimalElementObjectVisitor
1424
1425#[cfg(feature = "R4")]
1426pub mod r4;
1427#[cfg(feature = "R4B")]
1428pub mod r4b;
1429#[cfg(feature = "R5")]
1430pub mod r5;
1431#[cfg(feature = "R6")]
1432pub mod r6;
1433
1434pub mod compartment_expressions;
1435pub mod parameters;
1436pub mod search;
1437
1438// Re-export commonly used types from parameters module
1439pub use parameters::{ParameterValueAccessor, VersionIndependentParameters};
1440
1441/// Returns the search-parameter NAMES that link `resource_type` to the
1442/// named compartment (e.g. `"Patient"`, `"Group"`, `"Encounter"`,
1443/// `"Practitioner"`, `"RelatedPerson"`, `"Device"`), for the specified
1444/// FHIR version.
1445///
1446/// Thin version-dispatching wrapper around the per-version code-generated
1447/// `get_compartment_params`. Returns an empty slice when the resource
1448/// type is not a member of the named compartment.
1449///
1450/// Used by:
1451/// - REST compartment-search handler (`/Patient/{id}/Observation` style URLs)
1452/// to know which search params to feed into the search-index query.
1453/// - SoF in-DB runners to filter `$viewdefinition-run` results by patient /
1454/// group membership.
1455///
1456/// Pair this with [`compartment_expressions`] when you need the FHIRPath
1457/// expressions themselves (e.g. for in-process FHIRPath evaluation against
1458/// raw JSON, as `helios_sof::compartment` does).
1459#[allow(unreachable_patterns)]
1460pub fn compartment_params(
1461 version: FhirVersion,
1462 compartment_type: &str,
1463 resource_type: &str,
1464) -> &'static [&'static str] {
1465 match version {
1466 #[cfg(feature = "R4")]
1467 FhirVersion::R4 => r4::get_compartment_params(compartment_type, resource_type),
1468 #[cfg(feature = "R4B")]
1469 FhirVersion::R4B => r4b::get_compartment_params(compartment_type, resource_type),
1470 #[cfg(feature = "R5")]
1471 FhirVersion::R5 => r5::get_compartment_params(compartment_type, resource_type),
1472 #[cfg(feature = "R6")]
1473 FhirVersion::R6 => r6::get_compartment_params(compartment_type, resource_type),
1474 _ => &[],
1475 }
1476}
1477
1478// Internal helpers used by the derive macro; not part of the public API
1479#[doc(hidden)]
1480/// Multi-version FHIR resource container supporting version-agnostic operations.
1481///
1482/// This enum provides a unified interface for working with FHIR resources across
1483/// different specification versions. It enables applications to handle multiple
1484/// FHIR versions simultaneously while maintaining type safety and version-specific
1485/// behavior where needed.
1486///
1487/// # Supported Versions
1488///
1489/// - **R4**: FHIR 4.0.1 (normative)
1490/// - **R4B**: FHIR 4.3.0 (ballot)
1491/// - **R5**: FHIR 5.0.0 (ballot)
1492/// - **R6**: FHIR 6.0.0 (draft)
1493///
1494/// # Feature Flags
1495///
1496/// Each FHIR version is controlled by a corresponding Cargo feature flag.
1497/// Only enabled versions will be available in the enum variants.
1498///
1499/// # Examples
1500///
1501/// ```rust
1502/// use helios_fhir::{FhirResource, FhirVersion};
1503/// # #[cfg(feature = "R4")]
1504/// use helios_fhir::r4::{Patient, HumanName};
1505///
1506/// # #[cfg(feature = "R4")]
1507/// {
1508/// // Create an R4 patient
1509/// let patient = Patient {
1510/// name: Some(vec![HumanName {
1511/// family: Some("Doe".to_string().into()),
1512/// given: Some(vec!["John".to_string().into()]),
1513/// ..Default::default()
1514/// }]),
1515/// ..Default::default()
1516/// };
1517///
1518/// // Wrap in version-agnostic container
1519/// let resource = FhirResource::R4(Box::new(helios_fhir::r4::Resource::Patient(Box::new(patient))));
1520/// assert_eq!(resource.version(), FhirVersion::R4);
1521/// }
1522/// ```
1523///
1524/// # Version Detection
1525///
1526/// Use the `version()` method to determine which FHIR version a resource uses:
1527///
1528/// ```rust
1529/// # use helios_fhir::{FhirResource, FhirVersion};
1530/// # #[cfg(feature = "R4")]
1531/// # {
1532/// # let resource = FhirResource::R4(Box::new(helios_fhir::r4::Resource::Patient(Default::default())));
1533/// match resource.version() {
1534/// #[cfg(feature = "R4")]
1535/// FhirVersion::R4 => println!("This is an R4 resource"),
1536/// #[cfg(feature = "R4B")]
1537/// FhirVersion::R4B => println!("This is an R4B resource"),
1538/// #[cfg(feature = "R5")]
1539/// FhirVersion::R5 => println!("This is an R5 resource"),
1540/// #[cfg(feature = "R6")]
1541/// FhirVersion::R6 => println!("This is an R6 resource"),
1542/// }
1543/// # }
1544/// ```
1545#[derive(Debug)]
1546pub enum FhirResource {
1547 /// FHIR 4.0.1 (normative) resource
1548 #[cfg(feature = "R4")]
1549 R4(Box<r4::Resource>),
1550 /// FHIR 4.3.0 (ballot) resource
1551 #[cfg(feature = "R4B")]
1552 R4B(Box<r4b::Resource>),
1553 /// FHIR 5.0.0 (ballot) resource
1554 #[cfg(feature = "R5")]
1555 R5(Box<r5::Resource>),
1556 /// FHIR 6.0.0 (draft) resource
1557 #[cfg(feature = "R6")]
1558 R6(Box<r6::Resource>),
1559}
1560
1561impl FhirResource {
1562 /// Returns the FHIR specification version of this resource.
1563 ///
1564 /// This method provides version detection for multi-version applications,
1565 /// enabling version-specific processing logic and compatibility checks.
1566 ///
1567 /// # Returns
1568 ///
1569 /// The `FhirVersion` enum variant corresponding to this resource's specification.
1570 ///
1571 /// # Examples
1572 ///
1573 /// ```rust
1574 /// use helios_fhir::{FhirResource, FhirVersion};
1575 ///
1576 /// # #[cfg(feature = "R5")]
1577 /// # {
1578 /// # let resource = FhirResource::R5(Box::new(helios_fhir::r5::Resource::Patient(Default::default())));
1579 /// let version = resource.version();
1580 /// assert_eq!(version, FhirVersion::R5);
1581 ///
1582 /// // Use version for conditional logic
1583 /// match version {
1584 /// FhirVersion::R5 => {
1585 /// println!("Processing R5 resource with latest features");
1586 /// },
1587 /// FhirVersion::R4 => {
1588 /// println!("Processing R4 resource with normative features");
1589 /// },
1590 /// _ => {
1591 /// println!("Processing other FHIR version");
1592 /// }
1593 /// }
1594 /// # }
1595 /// ```
1596 pub fn version(&self) -> FhirVersion {
1597 match self {
1598 #[cfg(feature = "R4")]
1599 FhirResource::R4(_) => FhirVersion::R4,
1600 #[cfg(feature = "R4B")]
1601 FhirResource::R4B(_) => FhirVersion::R4B,
1602 #[cfg(feature = "R5")]
1603 FhirResource::R5(_) => FhirVersion::R5,
1604 #[cfg(feature = "R6")]
1605 FhirResource::R6(_) => FhirVersion::R6,
1606 }
1607 }
1608}
1609
1610/// Enumeration of supported FHIR specification versions.
1611///
1612/// This enum represents the different versions of the FHIR (Fast Healthcare
1613/// Interoperability Resources) specification that this library supports.
1614/// Each version represents a specific release of the FHIR standard with
1615/// its own set of features, resources, and compatibility requirements.
1616///
1617/// # Version Status
1618///
1619/// - **R4** (4.0.1): Normative version, widely adopted in production
1620/// - **R4B** (4.3.0): Ballot version with additional features
1621/// - **R5** (5.0.0): Ballot version with significant enhancements
1622/// - **R6** (6.0.0): Draft version under active development
1623///
1624/// # Feature Flags
1625///
1626/// Each version is controlled by a corresponding Cargo feature flag:
1627/// - `R4`: Enables FHIR R4 support
1628/// - `R4B`: Enables FHIR R4B support
1629/// - `R5`: Enables FHIR R5 support
1630/// - `R6`: Enables FHIR R6 support
1631///
1632/// # Examples
1633///
1634/// ```rust
1635/// use helios_fhir::FhirVersion;
1636///
1637/// // Version comparison
1638/// # #[cfg(all(feature = "R4", feature = "R5"))]
1639/// # {
1640/// assert_ne!(FhirVersion::R4, FhirVersion::R5);
1641/// # }
1642///
1643/// // String representation
1644/// # #[cfg(feature = "R4")]
1645/// # {
1646/// let version = FhirVersion::R4;
1647/// assert_eq!(version.as_str(), "R4");
1648/// assert_eq!(version.to_string(), "R4");
1649/// # }
1650/// ```
1651///
1652/// # CLI Integration
1653///
1654/// This enum implements `clap::ValueEnum` for command-line argument parsing:
1655///
1656/// ```rust,no_run
1657/// use clap::Parser;
1658/// use helios_fhir::FhirVersion;
1659///
1660/// #[derive(Parser)]
1661/// struct Args {
1662/// #[arg(value_enum)]
1663/// version: FhirVersion,
1664/// }
1665/// ```
1666#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1667pub enum FhirVersion {
1668 /// FHIR 4.0.1 (normative) - The current normative version
1669 #[cfg(feature = "R4")]
1670 R4,
1671 /// FHIR 4.3.0 (ballot) - Intermediate version with additional features
1672 #[cfg(feature = "R4B")]
1673 R4B,
1674 /// FHIR 5.0.0 (ballot) - Next major version with significant changes
1675 #[cfg(feature = "R5")]
1676 R5,
1677 /// FHIR 6.0.0 (draft) - Future version under development
1678 #[cfg(feature = "R6")]
1679 R6,
1680}
1681
1682impl FhirVersion {
1683 /// Returns the string representation of the FHIR version.
1684 ///
1685 /// This method provides the standard version identifier as used in
1686 /// FHIR documentation, URLs, and configuration files.
1687 ///
1688 /// # Returns
1689 ///
1690 /// A static string slice representing the version (e.g., "R4", "R5").
1691 ///
1692 /// # Examples
1693 ///
1694 /// ```rust
1695 /// use helios_fhir::FhirVersion;
1696 ///
1697 /// # #[cfg(feature = "R4")]
1698 /// assert_eq!(FhirVersion::R4.as_str(), "R4");
1699 /// # #[cfg(feature = "R5")]
1700 /// assert_eq!(FhirVersion::R5.as_str(), "R5");
1701 /// ```
1702 ///
1703 /// # Usage
1704 ///
1705 /// This method is commonly used for:
1706 /// - Logging and debugging output
1707 /// - Configuration file parsing
1708 /// - API endpoint construction
1709 /// - Version-specific resource loading
1710 pub fn as_str(&self) -> &'static str {
1711 match self {
1712 #[cfg(feature = "R4")]
1713 FhirVersion::R4 => "R4",
1714 #[cfg(feature = "R4B")]
1715 FhirVersion::R4B => "R4B",
1716 #[cfg(feature = "R5")]
1717 FhirVersion::R5 => "R5",
1718 #[cfg(feature = "R6")]
1719 FhirVersion::R6 => "R6",
1720 }
1721 }
1722
1723 /// Parse from MIME-type parameter value (e.g., "4.0", "5.0").
1724 ///
1725 /// Per FHIR spec: <https://hl7.org/fhir/http.html#version-parameter>
1726 ///
1727 /// # Arguments
1728 ///
1729 /// * `value` - The MIME-type parameter value (e.g., "4.0", "4.3", "5.0", "6.0")
1730 ///
1731 /// # Returns
1732 ///
1733 /// The corresponding `FhirVersion` if the value matches an enabled version,
1734 /// or `None` if not recognized or the version feature is not enabled.
1735 ///
1736 /// # Examples
1737 ///
1738 /// ```rust
1739 /// use helios_fhir::FhirVersion;
1740 ///
1741 /// # #[cfg(feature = "R4")]
1742 /// assert_eq!(FhirVersion::from_mime_param("4.0"), Some(FhirVersion::R4));
1743 /// # #[cfg(feature = "R5")]
1744 /// assert_eq!(FhirVersion::from_mime_param("5.0"), Some(FhirVersion::R5));
1745 /// assert_eq!(FhirVersion::from_mime_param("invalid"), None);
1746 /// ```
1747 pub fn from_mime_param(value: &str) -> Option<Self> {
1748 match value.trim() {
1749 #[cfg(feature = "R4")]
1750 "4.0" => Some(FhirVersion::R4),
1751 #[cfg(feature = "R4B")]
1752 "4.3" => Some(FhirVersion::R4B),
1753 #[cfg(feature = "R5")]
1754 "5.0" => Some(FhirVersion::R5),
1755 #[cfg(feature = "R6")]
1756 "6.0" => Some(FhirVersion::R6),
1757 _ => None,
1758 }
1759 }
1760
1761 /// Returns the MIME-type parameter value for this version.
1762 ///
1763 /// This value is used in Content-Type and Accept headers per FHIR spec.
1764 /// Example: `application/fhir+json; fhirVersion=4.0`
1765 ///
1766 /// # Examples
1767 ///
1768 /// ```rust
1769 /// use helios_fhir::FhirVersion;
1770 ///
1771 /// # #[cfg(feature = "R4")]
1772 /// assert_eq!(FhirVersion::R4.as_mime_param(), "4.0");
1773 /// # #[cfg(feature = "R5")]
1774 /// assert_eq!(FhirVersion::R5.as_mime_param(), "5.0");
1775 /// ```
1776 pub fn as_mime_param(&self) -> &'static str {
1777 match self {
1778 #[cfg(feature = "R4")]
1779 FhirVersion::R4 => "4.0",
1780 #[cfg(feature = "R4B")]
1781 FhirVersion::R4B => "4.3",
1782 #[cfg(feature = "R5")]
1783 FhirVersion::R5 => "5.0",
1784 #[cfg(feature = "R6")]
1785 FhirVersion::R6 => "6.0",
1786 }
1787 }
1788
1789 /// Returns the full version string (e.g., "4.0.1", "5.0.0").
1790 ///
1791 /// This is the complete version identifier used in CapabilityStatement.fhirVersion.
1792 ///
1793 /// # Examples
1794 ///
1795 /// ```rust
1796 /// use helios_fhir::FhirVersion;
1797 ///
1798 /// # #[cfg(feature = "R4")]
1799 /// assert_eq!(FhirVersion::R4.full_version(), "4.0.1");
1800 /// # #[cfg(feature = "R5")]
1801 /// assert_eq!(FhirVersion::R5.full_version(), "5.0.0");
1802 /// ```
1803 pub fn full_version(&self) -> &'static str {
1804 match self {
1805 #[cfg(feature = "R4")]
1806 FhirVersion::R4 => "4.0.1",
1807 #[cfg(feature = "R4B")]
1808 FhirVersion::R4B => "4.3.0",
1809 #[cfg(feature = "R5")]
1810 FhirVersion::R5 => "5.0.0",
1811 #[cfg(feature = "R6")]
1812 FhirVersion::R6 => "6.0.0",
1813 }
1814 }
1815
1816 /// Parse from database storage string.
1817 ///
1818 /// Accepts both MIME format ("4.0") and short format ("R4") for flexibility.
1819 /// This is useful when loading version information from the database.
1820 ///
1821 /// # Arguments
1822 ///
1823 /// * `value` - The storage value (e.g., "4.0", "R4", "r4")
1824 ///
1825 /// # Returns
1826 ///
1827 /// The corresponding `FhirVersion` if recognized, or `None` otherwise.
1828 ///
1829 /// # Examples
1830 ///
1831 /// ```rust
1832 /// use helios_fhir::FhirVersion;
1833 ///
1834 /// # #[cfg(feature = "R4")]
1835 /// {
1836 /// assert_eq!(FhirVersion::from_storage("4.0"), Some(FhirVersion::R4));
1837 /// assert_eq!(FhirVersion::from_storage("R4"), Some(FhirVersion::R4));
1838 /// assert_eq!(FhirVersion::from_storage("r4"), Some(FhirVersion::R4));
1839 /// }
1840 /// ```
1841 pub fn from_storage(value: &str) -> Option<Self> {
1842 // Try MIME format first
1843 Self::from_mime_param(value).or_else(|| match value.to_uppercase().as_str() {
1844 #[cfg(feature = "R4")]
1845 "R4" => Some(FhirVersion::R4),
1846 #[cfg(feature = "R4B")]
1847 "R4B" => Some(FhirVersion::R4B),
1848 #[cfg(feature = "R5")]
1849 "R5" => Some(FhirVersion::R5),
1850 #[cfg(feature = "R6")]
1851 "R6" => Some(FhirVersion::R6),
1852 _ => None,
1853 })
1854 }
1855
1856 /// Returns the notification Bundle.type value used for FHIR Subscription
1857 /// notifications in this version.
1858 ///
1859 /// R4 and R4B follow the R4 backport IG and emit `history` Bundles; R5 and
1860 /// R6 emit native `subscription-notification` Bundles per the
1861 /// [Subscription specification](https://build.fhir.org/subscription.html).
1862 pub fn notification_bundle_type(&self) -> &'static str {
1863 match self {
1864 #[cfg(feature = "R4")]
1865 FhirVersion::R4 => "history",
1866 #[cfg(feature = "R4B")]
1867 FhirVersion::R4B => "history",
1868 #[cfg(feature = "R5")]
1869 FhirVersion::R5 => "subscription-notification",
1870 #[cfg(feature = "R6")]
1871 FhirVersion::R6 => "subscription-notification",
1872 }
1873 }
1874
1875 /// Returns all enabled FHIR versions.
1876 ///
1877 /// This is useful for listing supported versions (e.g., in `$versions` operation).
1878 pub fn enabled_versions() -> &'static [FhirVersion] {
1879 &[
1880 #[cfg(feature = "R4")]
1881 FhirVersion::R4,
1882 #[cfg(feature = "R4B")]
1883 FhirVersion::R4B,
1884 #[cfg(feature = "R5")]
1885 FhirVersion::R5,
1886 #[cfg(feature = "R6")]
1887 FhirVersion::R6,
1888 ]
1889 }
1890
1891 /// Returns the default FHIR version for the current build.
1892 ///
1893 /// This is `R4` when the `R4` feature is enabled (the canonical default),
1894 /// otherwise the first enabled version (`R4B`, then `R5`, then `R6`). Unlike
1895 /// [`Default::default`] — which is gated on `feature = "R4"` and therefore
1896 /// unavailable in single-version-minimal builds — this helper is available
1897 /// whenever at least one FHIR version feature is enabled, which the crate
1898 /// requires at compile time. Use it instead of `unwrap_or_default()` /
1899 /// `FhirVersion::default()` on code paths that must compile in any
1900 /// single-version build (e.g. R4B-only).
1901 pub fn default_enabled() -> FhirVersion {
1902 // `enabled_versions()` always has at least one entry: at least one FHIR
1903 // version feature must be enabled at compile time.
1904 FhirVersion::enabled_versions()[0]
1905 }
1906}
1907
1908/// Dispatches a field-type lookup to the per-version generated `FIELD_TYPES`
1909/// table. Returns `(field_type, is_collection)` when the
1910/// `(parent_type, field_name)` pair is known, or `None` when the version
1911/// variant isn't compiled in (e.g. a downstream crate enabled `helios-fhir`
1912/// features that this build doesn't have).
1913///
1914/// Centralizes what used to be a hand-rolled match in
1915/// `helios-persistence::sof` and `helios-fhirpath::type_inference`.
1916pub fn get_field_type(
1917 version: FhirVersion,
1918 parent_type: &str,
1919 field_name: &str,
1920) -> Option<(&'static str, bool)> {
1921 match version {
1922 #[cfg(feature = "R4")]
1923 FhirVersion::R4 => crate::r4::get_field_type(parent_type, field_name),
1924 #[cfg(feature = "R4B")]
1925 FhirVersion::R4B => crate::r4b::get_field_type(parent_type, field_name),
1926 #[cfg(feature = "R5")]
1927 FhirVersion::R5 => crate::r5::get_field_type(parent_type, field_name),
1928 #[cfg(feature = "R6")]
1929 FhirVersion::R6 => crate::r6::get_field_type(parent_type, field_name),
1930 #[allow(unreachable_patterns)]
1931 _ => None,
1932 }
1933}
1934
1935/// Returns true when `name` is the type code of a FHIR primitive datatype
1936/// (case-sensitive, lowercase as in the FHIR spec — `boolean`, `integer`,
1937/// `dateTime`, …). The set is the union across FHIR versions, so
1938/// `integer64` (added in R5) and `xhtml` are included regardless of which
1939/// version feature is enabled.
1940///
1941/// Centralizes what used to be three hand-maintained primitive-type lists
1942/// inside `helios-fhirpath` (`fhir_type_hierarchy`, `resource_type`,
1943/// `type_inference`).
1944pub fn is_primitive_type(name: &str) -> bool {
1945 matches!(
1946 name,
1947 "base64Binary"
1948 | "boolean"
1949 | "canonical"
1950 | "code"
1951 | "date"
1952 | "dateTime"
1953 | "decimal"
1954 | "id"
1955 | "instant"
1956 | "integer"
1957 | "integer64"
1958 | "markdown"
1959 | "oid"
1960 | "positiveInt"
1961 | "string"
1962 | "time"
1963 | "unsignedInt"
1964 | "uri"
1965 | "url"
1966 | "uuid"
1967 | "xhtml"
1968 )
1969}
1970
1971/// Returns true when `field_name` appears anywhere in the per-version
1972/// `FIELD_TYPES` table. Used as a parent-context-free fallback for
1973/// detecting polymorphic typed variants (`valueQuantity`,
1974/// `deceasedBoolean`, …) when the parent FHIR type isn't statically known.
1975pub fn field_exists_anywhere(version: FhirVersion, field_name: &str) -> bool {
1976 field_types(version).is_some_and(|t| t.iter().any(|(_, f, _, _)| *f == field_name))
1977}
1978
1979/// Returns the per-version `FIELD_TYPES` slice when the version's feature
1980/// is compiled in. Each entry is `(parent_type, field_name, field_type,
1981/// is_collection)`. Use this when you need to enumerate all fields of a
1982/// parent type — for a single-field lookup, prefer [`get_field_type`].
1983pub fn field_types(
1984 version: FhirVersion,
1985) -> Option<&'static [(&'static str, &'static str, &'static str, bool)]> {
1986 match version {
1987 #[cfg(feature = "R4")]
1988 FhirVersion::R4 => Some(crate::r4::FIELD_TYPES),
1989 #[cfg(feature = "R4B")]
1990 FhirVersion::R4B => Some(crate::r4b::FIELD_TYPES),
1991 #[cfg(feature = "R5")]
1992 FhirVersion::R5 => Some(crate::r5::FIELD_TYPES),
1993 #[cfg(feature = "R6")]
1994 FhirVersion::R6 => Some(crate::r6::FIELD_TYPES),
1995 #[allow(unreachable_patterns)]
1996 _ => None,
1997 }
1998}
1999
2000/// Returns the compartment search parameters for a given FHIR version.
2001///
2002/// This is a version-agnostic dispatch over the per-version
2003/// `helios_fhir::{r4,r4b,r5,r6}::get_compartment_params` functions, which are
2004/// generated from the official FHIR `CompartmentDefinition` resources.
2005///
2006/// # Arguments
2007///
2008/// * `version` - The FHIR version to use for lookup
2009/// * `compartment_type` - The compartment type (e.g., "Patient", "Encounter")
2010/// * `resource_type` - The target resource type (e.g., "Observation")
2011///
2012/// # Returns
2013///
2014/// A static slice of search parameter names that link the resource to the
2015/// compartment. Returns an empty slice if the resource is not a member of the
2016/// compartment.
2017pub fn get_compartment_params(
2018 version: FhirVersion,
2019 compartment_type: &str,
2020 resource_type: &str,
2021) -> &'static [&'static str] {
2022 match version {
2023 #[cfg(feature = "R4")]
2024 FhirVersion::R4 => r4::get_compartment_params(compartment_type, resource_type),
2025 #[cfg(feature = "R4B")]
2026 FhirVersion::R4B => r4b::get_compartment_params(compartment_type, resource_type),
2027 #[cfg(feature = "R5")]
2028 FhirVersion::R5 => r5::get_compartment_params(compartment_type, resource_type),
2029 #[cfg(feature = "R6")]
2030 FhirVersion::R6 => r6::get_compartment_params(compartment_type, resource_type),
2031 }
2032}
2033
2034/// Implements `Display` trait for user-friendly output formatting.
2035///
2036/// This enables `FhirVersion` to be used in string formatting operations
2037/// and provides consistent output across different contexts.
2038///
2039/// # Examples
2040///
2041/// ```rust
2042/// use helios_fhir::FhirVersion;
2043///
2044/// # #[cfg(feature = "R5")]
2045/// # {
2046/// let version = FhirVersion::R5;
2047/// println!("Using FHIR version: {}", version); // Prints: "Using FHIR version: R5"
2048///
2049/// let formatted = format!("fhir-{}.json", version);
2050/// assert_eq!(formatted, "fhir-R5.json");
2051/// # }
2052/// ```
2053impl std::fmt::Display for FhirVersion {
2054 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2055 write!(f, "{}", self.as_str())
2056 }
2057}
2058
2059/// Provides a default FHIR version when R4 feature is enabled.
2060///
2061/// R4 is chosen as the default because it is the current normative version
2062/// of the FHIR specification and is widely adopted in production systems.
2063///
2064/// # Examples
2065///
2066/// ```rust
2067/// use helios_fhir::FhirVersion;
2068///
2069/// # #[cfg(feature = "R4")]
2070/// # {
2071/// let default_version = FhirVersion::default();
2072/// assert_eq!(default_version, FhirVersion::R4);
2073/// # }
2074/// ```
2075#[cfg(feature = "R4")]
2076impl Default for FhirVersion {
2077 fn default() -> Self {
2078 FhirVersion::R4
2079 }
2080}
2081
2082/// Implements `clap::ValueEnum` for command-line argument parsing.
2083///
2084/// This implementation enables `FhirVersion` to be used directly as a command-line
2085/// argument type with clap, providing automatic parsing, validation, and help text
2086/// generation.
2087///
2088/// # Examples
2089///
2090/// ```rust,no_run
2091/// use clap::Parser;
2092/// use helios_fhir::FhirVersion;
2093///
2094/// #[derive(Parser)]
2095/// struct Args {
2096/// /// FHIR specification version to use
2097/// #[arg(value_enum, default_value_t = FhirVersion::default())]
2098/// version: FhirVersion,
2099/// }
2100///
2101/// // Command line: my-app --version R5
2102/// let args = Args::parse();
2103/// println!("Using FHIR version: {}", args.version);
2104/// ```
2105///
2106/// # Generated Help Text
2107///
2108/// When using this enum with clap, the help text will automatically include
2109/// all available FHIR versions based on enabled feature flags.
2110impl clap::ValueEnum for FhirVersion {
2111 fn value_variants<'a>() -> &'a [Self] {
2112 &[
2113 #[cfg(feature = "R4")]
2114 FhirVersion::R4,
2115 #[cfg(feature = "R4B")]
2116 FhirVersion::R4B,
2117 #[cfg(feature = "R5")]
2118 FhirVersion::R5,
2119 #[cfg(feature = "R6")]
2120 FhirVersion::R6,
2121 ]
2122 }
2123
2124 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
2125 Some(clap::builder::PossibleValue::new(self.as_str()))
2126 }
2127}
2128
2129/// Trait for providing FHIR resource type information
2130///
2131/// This trait allows querying which resource types are available in a specific
2132/// FHIR version without hardcoding resource type lists in multiple places.
2133pub trait FhirResourceTypeProvider {
2134 /// Returns a vector of all resource type names supported in this FHIR version
2135 fn get_resource_type_names() -> Vec<&'static str>;
2136
2137 /// Checks if a given type name is a resource type in this FHIR version
2138 fn is_resource_type(type_name: &str) -> bool {
2139 Self::get_resource_type_names()
2140 .iter()
2141 .any(|&resource_type| resource_type.eq_ignore_ascii_case(type_name))
2142 }
2143}
2144
2145/// Trait for providing FHIR complex type information
2146///
2147/// This trait allows querying which complex data types are available in a specific
2148/// FHIR version without hardcoding complex type lists in multiple places.
2149pub trait FhirComplexTypeProvider {
2150 /// Returns a vector of all complex type names supported in this FHIR version
2151 fn get_complex_type_names() -> Vec<&'static str>;
2152
2153 /// Checks if a given type name is a complex type in this FHIR version
2154 fn is_complex_type(type_name: &str) -> bool {
2155 Self::get_complex_type_names()
2156 .iter()
2157 .any(|&complex_type| complex_type.eq_ignore_ascii_case(type_name))
2158 }
2159}
2160
2161/// Trait for providing FHIR primitive type information
2162///
2163/// This trait allows querying which primitive data types are available in a specific
2164/// FHIR version without hardcoding primitive type lists in multiple places. The
2165/// implementation is generated from the FHIR specification (StructureDefinitions
2166/// whose `kind` is `primitive-type`).
2167pub trait FhirPrimitiveTypeProvider {
2168 /// Returns a vector of all primitive type names supported in this FHIR version
2169 /// (e.g. `boolean`, `string`, `dateTime`, `positiveInt`).
2170 fn get_primitive_type_names() -> Vec<&'static str>;
2171
2172 /// Checks if a given type name is a primitive type in this FHIR version
2173 fn is_primitive_type(type_name: &str) -> bool {
2174 Self::get_primitive_type_names()
2175 .iter()
2176 .any(|&primitive_type| primitive_type.eq_ignore_ascii_case(type_name))
2177 }
2178}
2179
2180// --- Internal Visitor for Element Object Deserialization ---
2181
2182/// Internal visitor struct for deserializing Element objects from JSON maps.
2183///
2184/// This visitor handles the complex deserialization logic for Element<V, E> when
2185/// the JSON input is an object containing id, extension, and value fields.
2186struct ElementObjectVisitor<V, E>(PhantomData<(V, E)>);
2187
2188impl<'de, V, E> Visitor<'de> for ElementObjectVisitor<V, E>
2189where
2190 V: Deserialize<'de>,
2191 E: Deserialize<'de>,
2192{
2193 type Value = Element<V, E>;
2194
2195 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
2196 formatter.write_str("an Element object")
2197 }
2198
2199 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
2200 where
2201 A: MapAccess<'de>,
2202 {
2203 let mut id: Option<String> = None;
2204 let mut extension: Option<Vec<E>> = None;
2205 let mut value: Option<V> = None;
2206
2207 // Manually deserialize fields from the map
2208 while let Some(key) = map.next_key::<String>()? {
2209 match key.as_str() {
2210 "id" => {
2211 if id.is_some() {
2212 return Err(de::Error::duplicate_field("id"));
2213 }
2214 id = Some(map.next_value()?);
2215 }
2216 "extension" => {
2217 if extension.is_some() {
2218 return Err(de::Error::duplicate_field("extension"));
2219 }
2220 #[cfg(feature = "xml")]
2221 {
2222 let single_or_vec: SingleOrVec<E> = map.next_value()?;
2223 extension = Some(single_or_vec.into());
2224 }
2225 #[cfg(not(feature = "xml"))]
2226 {
2227 extension = Some(map.next_value()?);
2228 }
2229 }
2230 "value" => {
2231 if value.is_some() {
2232 return Err(de::Error::duplicate_field("value"));
2233 }
2234 // Deserialize directly into Option<V>
2235 value = Some(map.next_value()?);
2236 }
2237 // Ignore any unknown fields encountered
2238 _ => {
2239 let _ = map.next_value::<de::IgnoredAny>()?;
2240 }
2241 }
2242 }
2243
2244 Ok(Element {
2245 id,
2246 extension,
2247 value,
2248 })
2249 }
2250}
2251
2252/// Generic element container supporting FHIR's extension mechanism.
2253///
2254/// In FHIR, most primitive elements can be extended with additional metadata
2255/// through the `id` and `extension` fields. This container type provides
2256/// the infrastructure to support this pattern across all FHIR data types.
2257///
2258/// # Type Parameters
2259///
2260/// * `V` - The value type (e.g., `String`, `i32`, `PreciseDecimal`)
2261/// * `E` - The extension type (typically the generated `Extension` struct)
2262///
2263/// # FHIR Element Structure
2264///
2265/// FHIR elements can appear in three forms:
2266/// 1. **Primitive value**: Just the value itself (e.g., `"text"`, `42`)
2267/// 2. **Extended primitive**: An object with `value`, `id`, and/or `extension` fields
2268/// 3. **Extension-only**: An object with just `id` and/or `extension` (no value)
2269///
2270/// # Examples
2271///
2272/// ```rust
2273/// use helios_fhir::{Element, r4::Extension};
2274///
2275/// // Simple primitive value
2276/// let simple: Element<String, Extension> = Element {
2277/// value: Some("Hello World".to_string()),
2278/// id: None,
2279/// extension: None,
2280/// };
2281///
2282/// // Extended primitive with ID
2283/// let with_id: Element<String, Extension> = Element {
2284/// value: Some("Hello World".to_string()),
2285/// id: Some("text-element-1".to_string()),
2286/// extension: None,
2287/// };
2288///
2289/// // Extension-only element (no value)
2290/// let extension_only: Element<String, Extension> = Element {
2291/// value: None,
2292/// id: Some("disabled-element".to_string()),
2293/// extension: Some(vec![/* extensions */]),
2294/// };
2295/// ```
2296///
2297/// # Serialization Behavior
2298///
2299/// - If only `value` is present: serializes as the primitive value directly
2300/// - If `id` or `extension` are present: serializes as an object with all fields
2301/// - If everything is `None`: serializes as `null`
2302#[derive(Debug, PartialEq, Eq, Clone, Default)]
2303pub struct Element<V, E> {
2304 /// Optional element identifier for referencing within the resource
2305 pub id: Option<String>,
2306 /// Optional extensions providing additional metadata
2307 pub extension: Option<Vec<E>>,
2308 /// The actual primitive value
2309 pub value: Option<V>,
2310}
2311
2312impl<V, E> Element<V, E> {
2313 /// Returns true when no value, id, or extensions are present.
2314 pub fn is_empty(&self) -> bool {
2315 self.value.is_none()
2316 && self.id.is_none()
2317 && self.extension.as_ref().is_none_or(|ext| ext.is_empty())
2318 }
2319}
2320
2321// Custom Deserialize for Element<V, E>
2322// Remove PartialEq/Eq bounds for V and E as they are not needed for deserialization itself
2323impl<'de, V, E> Deserialize<'de> for Element<V, E>
2324where
2325 V: Deserialize<'de> + 'static, // Added 'static for TypeId comparisons
2326 E: Deserialize<'de>, // Removed PartialEq
2327{
2328 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2329 where
2330 D: Deserializer<'de>,
2331 {
2332 // Use the AnyValueVisitor approach to handle different JSON input types
2333 struct AnyValueVisitor<V, E>(PhantomData<(V, E)>);
2334
2335 impl<'de, V, E> Visitor<'de> for AnyValueVisitor<V, E>
2336 where
2337 V: Deserialize<'de> + 'static,
2338 E: Deserialize<'de>,
2339 {
2340 type Value = Element<V, E>;
2341
2342 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
2343 formatter
2344 .write_str("a primitive value (string, number, boolean), an object, or null")
2345 }
2346
2347 // Handle primitive types by attempting to deserialize V and wrapping it
2348 fn visit_bool<Er>(self, v: bool) -> Result<Self::Value, Er>
2349 where
2350 Er: de::Error,
2351 {
2352 V::deserialize(de::value::BoolDeserializer::new(v)).map(|value| Element {
2353 id: None,
2354 extension: None,
2355 value: Some(value),
2356 })
2357 }
2358 fn visit_i64<Er>(self, v: i64) -> Result<Self::Value, Er>
2359 where
2360 Er: de::Error,
2361 {
2362 V::deserialize(de::value::I64Deserializer::new(v)).map(|value| Element {
2363 id: None,
2364 extension: None,
2365 value: Some(value),
2366 })
2367 }
2368 fn visit_u64<Er>(self, v: u64) -> Result<Self::Value, Er>
2369 where
2370 Er: de::Error,
2371 {
2372 V::deserialize(de::value::U64Deserializer::new(v)).map(|value| Element {
2373 id: None,
2374 extension: None,
2375 value: Some(value),
2376 })
2377 }
2378 fn visit_f64<Er>(self, v: f64) -> Result<Self::Value, Er>
2379 where
2380 Er: de::Error,
2381 {
2382 V::deserialize(de::value::F64Deserializer::new(v)).map(|value| Element {
2383 id: None,
2384 extension: None,
2385 value: Some(value),
2386 })
2387 }
2388 fn visit_str<Er>(self, v: &str) -> Result<Self::Value, Er>
2389 where
2390 Er: de::Error,
2391 {
2392 use std::any::TypeId;
2393
2394 // Try to handle numeric strings for integer types
2395 if TypeId::of::<V>() == TypeId::of::<i64>() {
2396 if let Ok(int_val) = v.parse::<i64>() {
2397 return V::deserialize(de::value::I64Deserializer::new(int_val)).map(
2398 |value| Element {
2399 id: None,
2400 extension: None,
2401 value: Some(value),
2402 },
2403 );
2404 }
2405 } else if TypeId::of::<V>() == TypeId::of::<i32>() {
2406 if let Ok(int_val) = v.parse::<i32>() {
2407 return V::deserialize(de::value::I32Deserializer::new(int_val)).map(
2408 |value| Element {
2409 id: None,
2410 extension: None,
2411 value: Some(value),
2412 },
2413 );
2414 }
2415 } else if TypeId::of::<V>() == TypeId::of::<u64>() {
2416 if let Ok(int_val) = v.parse::<u64>() {
2417 return V::deserialize(de::value::U64Deserializer::new(int_val)).map(
2418 |value| Element {
2419 id: None,
2420 extension: None,
2421 value: Some(value),
2422 },
2423 );
2424 }
2425 } else if TypeId::of::<V>() == TypeId::of::<u32>() {
2426 if let Ok(int_val) = v.parse::<u32>() {
2427 return V::deserialize(de::value::U32Deserializer::new(int_val)).map(
2428 |value| Element {
2429 id: None,
2430 extension: None,
2431 value: Some(value),
2432 },
2433 );
2434 }
2435 }
2436
2437 // Fall back to normal string deserialization
2438 V::deserialize(de::value::StrDeserializer::new(v)).map(|value| Element {
2439 id: None,
2440 extension: None,
2441 value: Some(value),
2442 })
2443 }
2444 fn visit_string<Er>(self, v: String) -> Result<Self::Value, Er>
2445 where
2446 Er: de::Error,
2447 {
2448 use std::any::TypeId;
2449
2450 // Try to handle numeric strings for integer types
2451 if TypeId::of::<V>() == TypeId::of::<i64>() {
2452 if let Ok(int_val) = v.parse::<i64>() {
2453 return V::deserialize(de::value::I64Deserializer::new(int_val)).map(
2454 |value| Element {
2455 id: None,
2456 extension: None,
2457 value: Some(value),
2458 },
2459 );
2460 }
2461 } else if TypeId::of::<V>() == TypeId::of::<i32>() {
2462 if let Ok(int_val) = v.parse::<i32>() {
2463 return V::deserialize(de::value::I32Deserializer::new(int_val)).map(
2464 |value| Element {
2465 id: None,
2466 extension: None,
2467 value: Some(value),
2468 },
2469 );
2470 }
2471 } else if TypeId::of::<V>() == TypeId::of::<u64>() {
2472 if let Ok(int_val) = v.parse::<u64>() {
2473 return V::deserialize(de::value::U64Deserializer::new(int_val)).map(
2474 |value| Element {
2475 id: None,
2476 extension: None,
2477 value: Some(value),
2478 },
2479 );
2480 }
2481 } else if TypeId::of::<V>() == TypeId::of::<u32>() {
2482 if let Ok(int_val) = v.parse::<u32>() {
2483 return V::deserialize(de::value::U32Deserializer::new(int_val)).map(
2484 |value| Element {
2485 id: None,
2486 extension: None,
2487 value: Some(value),
2488 },
2489 );
2490 }
2491 }
2492
2493 // Fall back to normal string deserialization
2494 V::deserialize(de::value::StringDeserializer::new(v.clone())).map(|value| Element {
2495 // Clone v for error message
2496 id: None,
2497 extension: None,
2498 value: Some(value),
2499 })
2500 }
2501 fn visit_borrowed_str<Er>(self, v: &'de str) -> Result<Self::Value, Er>
2502 where
2503 Er: de::Error,
2504 {
2505 use std::any::TypeId;
2506
2507 // Try to handle numeric strings for integer types
2508 if TypeId::of::<V>() == TypeId::of::<i64>() {
2509 if let Ok(int_val) = v.parse::<i64>() {
2510 return V::deserialize(de::value::I64Deserializer::new(int_val)).map(
2511 |value| Element {
2512 id: None,
2513 extension: None,
2514 value: Some(value),
2515 },
2516 );
2517 }
2518 } else if TypeId::of::<V>() == TypeId::of::<i32>() {
2519 if let Ok(int_val) = v.parse::<i32>() {
2520 return V::deserialize(de::value::I32Deserializer::new(int_val)).map(
2521 |value| Element {
2522 id: None,
2523 extension: None,
2524 value: Some(value),
2525 },
2526 );
2527 }
2528 } else if TypeId::of::<V>() == TypeId::of::<u64>() {
2529 if let Ok(int_val) = v.parse::<u64>() {
2530 return V::deserialize(de::value::U64Deserializer::new(int_val)).map(
2531 |value| Element {
2532 id: None,
2533 extension: None,
2534 value: Some(value),
2535 },
2536 );
2537 }
2538 } else if TypeId::of::<V>() == TypeId::of::<u32>() {
2539 if let Ok(int_val) = v.parse::<u32>() {
2540 return V::deserialize(de::value::U32Deserializer::new(int_val)).map(
2541 |value| Element {
2542 id: None,
2543 extension: None,
2544 value: Some(value),
2545 },
2546 );
2547 }
2548 }
2549
2550 // Fall back to normal string deserialization
2551 V::deserialize(de::value::BorrowedStrDeserializer::new(v)).map(|value| Element {
2552 id: None,
2553 extension: None,
2554 value: Some(value),
2555 })
2556 }
2557 fn visit_bytes<Er>(self, v: &[u8]) -> Result<Self::Value, Er>
2558 where
2559 Er: de::Error,
2560 {
2561 V::deserialize(de::value::BytesDeserializer::new(v)).map(|value| Element {
2562 id: None,
2563 extension: None,
2564 value: Some(value),
2565 })
2566 }
2567 fn visit_byte_buf<Er>(self, v: Vec<u8>) -> Result<Self::Value, Er>
2568 where
2569 Er: de::Error,
2570 {
2571 // Use BytesDeserializer with a slice reference &v
2572 V::deserialize(de::value::BytesDeserializer::new(&v)).map(|value| Element {
2573 id: None,
2574 extension: None,
2575 value: Some(value),
2576 })
2577 }
2578
2579 // Handle null
2580 fn visit_none<Er>(self) -> Result<Self::Value, Er>
2581 where
2582 Er: de::Error,
2583 {
2584 Ok(Element {
2585 id: None,
2586 extension: None,
2587 value: None,
2588 })
2589 }
2590 fn visit_unit<Er>(self) -> Result<Self::Value, Er>
2591 where
2592 Er: de::Error,
2593 {
2594 Ok(Element {
2595 id: None,
2596 extension: None,
2597 value: None,
2598 })
2599 }
2600
2601 // Handle Option<T> by visiting Some
2602 fn visit_some<De>(self, deserializer: De) -> Result<Self::Value, De::Error>
2603 where
2604 De: Deserializer<'de>,
2605 {
2606 // Re-dispatch to deserialize_any to handle the inner type correctly
2607 deserializer.deserialize_any(self)
2608 }
2609
2610 // Handle object
2611 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
2612 where
2613 A: MapAccess<'de>,
2614 {
2615 // Deserialize the map using ElementObjectVisitor
2616 // Need to create a deserializer from the map access
2617 let map_deserializer = de::value::MapAccessDeserializer::new(map);
2618 map_deserializer.deserialize_map(ElementObjectVisitor(PhantomData))
2619 }
2620
2621 // We don't expect sequences for a single Element
2622 fn visit_seq<A>(self, _seq: A) -> Result<Self::Value, A::Error>
2623 where
2624 A: de::SeqAccess<'de>,
2625 {
2626 Err(de::Error::invalid_type(de::Unexpected::Seq, &self))
2627 }
2628 }
2629
2630 // Start deserialization using the visitor
2631 deserializer.deserialize_any(AnyValueVisitor(PhantomData))
2632 }
2633}
2634
2635// Custom Serialize for Element<V, E>
2636// Remove PartialEq/Eq bounds for V and E as they are not needed for serialization itself
2637impl<V, E> Serialize for Element<V, E>
2638where
2639 V: Serialize, // Removed PartialEq + Eq
2640 E: Serialize, // Removed PartialEq
2641{
2642 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2643 where
2644 S: Serializer,
2645 {
2646 // If id and extension are None, serialize value directly (or null)
2647 if self.id.is_none() && self.extension.is_none() {
2648 match &self.value {
2649 Some(val) => val.serialize(serializer),
2650 None => serializer.serialize_none(),
2651 }
2652 } else {
2653 // Otherwise, serialize as an object containing id, extension, value if present
2654 let mut len = 0;
2655 if self.id.is_some() {
2656 len += 1;
2657 }
2658 if self.extension.is_some() {
2659 len += 1;
2660 }
2661 if self.value.is_some() {
2662 len += 1;
2663 }
2664
2665 let mut state = serializer.serialize_struct("Element", len)?;
2666 if let Some(id) = &self.id {
2667 state.serialize_field("id", id)?;
2668 }
2669 if let Some(extension) = &self.extension {
2670 state.serialize_field("extension", extension)?;
2671 }
2672 // Restore value serialization for direct Element serialization
2673 if let Some(value) = &self.value {
2674 state.serialize_field("value", value)?;
2675 }
2676 state.end()
2677 }
2678 }
2679}
2680
2681/// Specialized element container for FHIR decimal values with precision preservation.
2682///
2683/// This type combines the generic `Element` pattern with `PreciseDecimal` to provide
2684/// a complete solution for FHIR decimal elements that require both extension support
2685/// and precision preservation during serialization round-trips.
2686///
2687/// # Type Parameters
2688///
2689/// * `E` - The extension type (typically the generated `Extension` struct)
2690///
2691/// # FHIR Decimal Requirements
2692///
2693/// FHIR decimal elements must:
2694/// - Preserve original string precision (e.g., "12.30" vs "12.3")
2695/// - Support mathematical operations using `Decimal` arithmetic
2696/// - Handle extension metadata through `id` and `extension` fields
2697/// - Serialize back to the exact original format when possible
2698///
2699/// # Examples
2700///
2701/// ```rust
2702/// use helios_fhir::{DecimalElement, PreciseDecimal, r4::Extension};
2703/// use rust_decimal::Decimal;
2704///
2705/// // Create from a Decimal value
2706/// let decimal_elem = DecimalElement::<Extension>::new(Decimal::new(1234, 2)); // 12.34
2707///
2708/// // Create with extensions
2709/// let extended_decimal: DecimalElement<Extension> = DecimalElement {
2710/// value: Some(PreciseDecimal::from_parts(
2711/// Some(Decimal::new(12300, 3)),
2712/// "12.300".to_string()
2713/// )),
2714/// id: Some("precision-example".to_string()),
2715/// extension: Some(vec![/* extensions */]),
2716/// };
2717///
2718/// // Access the mathematical value
2719/// if let Some(precise) = &extended_decimal.value {
2720/// if let Some(decimal_val) = precise.value() {
2721/// println!("Mathematical value: {}", decimal_val);
2722/// }
2723/// println!("Original format: {}", precise.original_string());
2724/// }
2725/// ```
2726///
2727/// # Serialization Behavior
2728///
2729/// - **Value only**: Serializes as a JSON number preserving original precision
2730/// - **With extensions**: Serializes as an object with `value`, `id`, and `extension` fields
2731/// - **No value**: Serializes as an object with just the extension fields, or `null` if empty
2732///
2733/// # Integration with FHIRPath
2734///
2735/// When used with FHIRPath evaluation, `DecimalElement` returns:
2736/// - The `Decimal` value for mathematical operations
2737/// - An object representation when extension metadata is accessed
2738/// - Empty collection when the element has no value or extensions
2739#[derive(Debug, PartialEq, Eq, Clone, Default)]
2740pub struct DecimalElement<E> {
2741 /// Optional element identifier for referencing within the resource
2742 pub id: Option<String>,
2743 /// Optional extensions providing additional metadata
2744 pub extension: Option<Vec<E>>,
2745 /// The decimal value with precision preservation
2746 pub value: Option<PreciseDecimal>,
2747}
2748
2749impl<E> DecimalElement<E> {
2750 /// Creates a new `DecimalElement` with the specified decimal value.
2751 ///
2752 /// This constructor creates a simple decimal element with no extensions or ID,
2753 /// containing only the decimal value. The original string representation is
2754 /// automatically derived from the `Decimal` value's `Display` implementation.
2755 ///
2756 /// # Arguments
2757 ///
2758 /// * `value` - The `Decimal` value to store
2759 ///
2760 /// # Returns
2761 ///
2762 /// A new `DecimalElement` with the value set and `id`/`extension` as `None`.
2763 ///
2764 /// # Examples
2765 ///
2766 /// ```rust
2767 /// use helios_fhir::{DecimalElement, r4::Extension};
2768 /// use rust_decimal::Decimal;
2769 ///
2770 /// // Create a simple decimal element
2771 /// let element = DecimalElement::<Extension>::new(Decimal::new(12345, 3)); // 12.345
2772 ///
2773 /// // Verify the structure
2774 /// assert!(element.id.is_none());
2775 /// assert!(element.extension.is_none());
2776 /// assert!(element.value.is_some());
2777 ///
2778 /// // Access the decimal value
2779 /// if let Some(precise_decimal) = &element.value {
2780 /// assert_eq!(precise_decimal.value(), Some(Decimal::new(12345, 3)));
2781 /// assert_eq!(precise_decimal.original_string(), "12.345");
2782 /// }
2783 /// ```
2784 ///
2785 /// # Usage in FHIR Resources
2786 ///
2787 /// This method is typically used when creating FHIR elements programmatically:
2788 ///
2789 /// ```rust
2790 /// use helios_fhir::{DecimalElement, r4::{Extension, Observation}};
2791 /// use rust_decimal::Decimal;
2792 ///
2793 /// let temperature = DecimalElement::<Extension>::new(Decimal::new(3672, 2)); // 36.72
2794 ///
2795 /// // Would be used in an Observation like:
2796 /// // observation.value_quantity.value = Some(temperature);
2797 /// ```
2798 pub fn new(value: Decimal) -> Self {
2799 // Convert the Decimal to PreciseDecimal, which automatically handles
2800 // storing the original string representation via the From trait
2801 let precise_value = PreciseDecimal::from(value);
2802 Self {
2803 id: None,
2804 extension: None,
2805 value: Some(precise_value),
2806 }
2807 }
2808}
2809
2810// Custom Deserialize for DecimalElement<E> using intermediate Value
2811impl<'de, E> Deserialize<'de> for DecimalElement<E>
2812where
2813 E: Deserialize<'de> + Default,
2814{
2815 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2816 where
2817 D: Deserializer<'de>,
2818 {
2819 // Deserialize into an intermediate serde_json::Value first
2820 let json_value = serde_json::Value::deserialize(deserializer)?;
2821
2822 match json_value {
2823 // Handle primitive JSON Number
2824 serde_json::Value::Number(n) => {
2825 // Directly parse the number string to create PreciseDecimal
2826 let s = n.to_string(); // Note: n.to_string() might normalize exponent case (e.g., 'E' -> 'e')
2827 // Replace 'E' with 'e' for parsing
2828 let s_for_parsing = s.replace('E', "e");
2829 // Use from_scientific if 'e' is present, otherwise parse
2830 let parsed_value = if s_for_parsing.contains('e') {
2831 Decimal::from_scientific(&s_for_parsing).ok()
2832 } else {
2833 s_for_parsing.parse::<Decimal>().ok()
2834 };
2835 // Store the ORIGINAL string `s` (as returned by n.to_string()).
2836 let pd = PreciseDecimal::from_parts(parsed_value, s);
2837 Ok(DecimalElement {
2838 id: None,
2839 extension: None,
2840 value: Some(pd),
2841 })
2842 }
2843 // Handle primitive JSON String
2844 serde_json::Value::String(s) => {
2845 // Directly parse the string to create PreciseDecimal
2846 // Replace 'E' with 'e' for parsing
2847 let s_for_parsing = s.replace('E', "e");
2848 // Use from_scientific if 'e' is present, otherwise parse
2849 let parsed_value = if s_for_parsing.contains('e') {
2850 Decimal::from_scientific(&s_for_parsing).ok()
2851 } else {
2852 s_for_parsing.parse::<Decimal>().ok()
2853 };
2854 // Store the ORIGINAL string `s`.
2855 let pd = PreciseDecimal::from_parts(parsed_value, s); // s is owned, no clone needed
2856 Ok(DecimalElement {
2857 id: None,
2858 extension: None,
2859 value: Some(pd),
2860 })
2861 }
2862 // Handle JSON object: deserialize fields individually
2863 serde_json::Value::Object(map) => {
2864 let mut id: Option<String> = None;
2865 let mut extension: Option<Vec<E>> = None;
2866 let mut value: Option<PreciseDecimal> = None;
2867
2868 for (k, v) in map {
2869 match k.as_str() {
2870 "id" => {
2871 if id.is_some() {
2872 return Err(de::Error::duplicate_field("id"));
2873 }
2874 // Deserialize id directly from its Value
2875 id = Deserialize::deserialize(v).map_err(de::Error::custom)?;
2876 }
2877 "extension" => {
2878 if extension.is_some() {
2879 return Err(de::Error::duplicate_field("extension"));
2880 }
2881 #[cfg(feature = "xml")]
2882 {
2883 let single_or_vec: SingleOrVec<E> =
2884 Deserialize::deserialize(v).map_err(de::Error::custom)?;
2885 extension = Some(single_or_vec.into());
2886 }
2887 #[cfg(not(feature = "xml"))]
2888 {
2889 extension =
2890 Deserialize::deserialize(v).map_err(de::Error::custom)?;
2891 }
2892 }
2893 "value" => {
2894 if value.is_some() {
2895 return Err(de::Error::duplicate_field("value"));
2896 }
2897 // Deserialize value using PreciseDecimal::deserialize from its Value
2898 // Handle null explicitly within the value field
2899 if v.is_null() {
2900 value = None;
2901 } else {
2902 value = Some(
2903 PreciseDecimal::deserialize(v).map_err(de::Error::custom)?,
2904 );
2905 }
2906 }
2907 // Ignore any unknown fields encountered
2908 _ => {} // Simply ignore unknown fields
2909 }
2910 }
2911 Ok(DecimalElement {
2912 id,
2913 extension,
2914 value,
2915 })
2916 }
2917 // Handle JSON Null for the whole element
2918 serde_json::Value::Null => Ok(DecimalElement::default()), // Default has value: None
2919 // Handle other unexpected types
2920 other => Err(de::Error::invalid_type(
2921 match other {
2922 serde_json::Value::Bool(b) => de::Unexpected::Bool(b),
2923 serde_json::Value::Array(_) => de::Unexpected::Seq,
2924 _ => de::Unexpected::Other("unexpected JSON type for DecimalElement"),
2925 },
2926 &"a decimal number, string, object, or null",
2927 )),
2928 }
2929 }
2930}
2931
2932// Reinstate custom Serialize implementation for DecimalElement
2933// Remove PartialEq bound for E
2934impl<E> Serialize for DecimalElement<E>
2935where
2936 E: Serialize, // Removed PartialEq bound for E
2937{
2938 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2939 where
2940 S: Serializer,
2941 {
2942 // If we only have a value and no other fields, serialize just the value
2943 if self.id.is_none() && self.extension.is_none() {
2944 if let Some(value) = &self.value {
2945 // Serialize the PreciseDecimal directly, invoking its custom Serialize impl
2946 return value.serialize(serializer);
2947 } else {
2948 // If value is also None, serialize as null
2949 // based on updated test_serialize_decimal_with_no_fields
2950 return serializer.serialize_none();
2951 }
2952 }
2953
2954 // Otherwise, serialize as a struct with all present fields
2955 // Calculate the number of fields that are NOT None
2956 let mut len = 0;
2957 if self.id.is_some() {
2958 len += 1;
2959 }
2960 if self.extension.is_some() {
2961 len += 1;
2962 }
2963 if self.value.is_some() {
2964 len += 1;
2965 }
2966
2967 // Start serializing a struct with the calculated length
2968 let mut state = serializer.serialize_struct("DecimalElement", len)?;
2969
2970 // Serialize 'id' field if it's Some
2971 if let Some(id) = &self.id {
2972 state.serialize_field("id", id)?;
2973 }
2974
2975 // Serialize 'extension' field if it's Some
2976 if let Some(extension) = &self.extension {
2977 state.serialize_field("extension", extension)?;
2978 }
2979
2980 // Serialize 'value' field if it's Some
2981 if let Some(value) = &self.value {
2982 // Serialize the PreciseDecimal directly, invoking its custom Serialize impl
2983 state.serialize_field("value", value)?;
2984 }
2985
2986 // End the struct serialization
2987 state.end()
2988 }
2989}
2990
2991// For Element<V, E> - Returns Object with id, extension, value if present
2992impl<V, E> IntoEvaluationResult for Element<V, E>
2993where
2994 V: IntoEvaluationResult + Clone + 'static,
2995 E: IntoEvaluationResult + Clone,
2996{
2997 fn to_evaluation_result(&self) -> EvaluationResult {
2998 use helios_fhirpath_support::PrimitiveElement;
2999 use std::any::TypeId;
3000
3001 // Build PrimitiveElement metadata from id/extension (used when value is also present)
3002 let primitive_meta = if self.id.is_some() || self.extension.is_some() {
3003 let mut meta = PrimitiveElement::default();
3004 if let Some(id) = &self.id {
3005 meta.id = Some(id.clone());
3006 }
3007 if let Some(ext) = &self.extension {
3008 meta.extension = ext.iter().map(|e| e.to_evaluation_result()).collect();
3009 }
3010 if !meta.is_empty() { Some(meta) } else { None }
3011 } else {
3012 None
3013 };
3014
3015 // Prioritize returning the primitive value if it exists
3016 if let Some(v) = &self.value {
3017 let result = v.to_evaluation_result();
3018 // For primitive values, we need to preserve FHIR type information
3019 let typed = match result {
3020 EvaluationResult::Boolean(b, _, _) => EvaluationResult::fhir_boolean(b),
3021 EvaluationResult::Integer(i, _, _) => EvaluationResult::fhir_integer(i),
3022 #[cfg(not(any(feature = "R4", feature = "R4B")))]
3023 EvaluationResult::Integer64(i, _, _) => EvaluationResult::fhir_integer64(i),
3024 EvaluationResult::String(s, _, _) => EvaluationResult::fhir_string(s, "string"),
3025 EvaluationResult::DateTime(dt, type_info, _) => {
3026 if TypeId::of::<V>() == TypeId::of::<PrecisionInstant>() {
3027 EvaluationResult::DateTime(
3028 dt,
3029 Some(TypeInfoResult::new("FHIR", "instant")),
3030 None,
3031 )
3032 } else {
3033 EvaluationResult::DateTime(dt, type_info, None)
3034 }
3035 }
3036 other => other,
3037 };
3038 return match primitive_meta {
3039 Some(meta) => typed.with_primitive_element(meta),
3040 None => typed,
3041 };
3042 } else if self.id.is_some() || self.extension.is_some() {
3043 // If value is None, but id or extension exist, return an Object with those
3044 let mut map = std::collections::HashMap::new();
3045 if let Some(id) = &self.id {
3046 map.insert("id".to_string(), EvaluationResult::string(id.clone()));
3047 }
3048 if let Some(ext) = &self.extension {
3049 let ext_collection: Vec<EvaluationResult> =
3050 ext.iter().map(|e| e.to_evaluation_result()).collect();
3051 if !ext_collection.is_empty() {
3052 map.insert(
3053 "extension".to_string(),
3054 EvaluationResult::collection(ext_collection),
3055 );
3056 }
3057 }
3058 // Only return Object if map is not empty (i.e., id or extension was actually present)
3059 if !map.is_empty() {
3060 return EvaluationResult::typed_object(map, "FHIR", "Element");
3061 }
3062 }
3063
3064 // If value, id, and extension are all None, return Empty
3065 EvaluationResult::Empty
3066 }
3067}
3068
3069// For DecimalElement<E> - Returns Decimal value if present, otherwise handles id/extension
3070impl<E> IntoEvaluationResult for DecimalElement<E>
3071where
3072 E: IntoEvaluationResult + Clone,
3073{
3074 fn to_evaluation_result(&self) -> EvaluationResult {
3075 use helios_fhirpath_support::PrimitiveElement;
3076
3077 // Build PrimitiveElement metadata from id/extension
3078 let primitive_meta = if self.id.is_some() || self.extension.is_some() {
3079 let mut meta = PrimitiveElement::default();
3080 if let Some(id) = &self.id {
3081 meta.id = Some(id.clone());
3082 }
3083 if let Some(ext) = &self.extension {
3084 meta.extension = ext.iter().map(|e| e.to_evaluation_result()).collect();
3085 }
3086 if !meta.is_empty() { Some(meta) } else { None }
3087 } else {
3088 None
3089 };
3090
3091 // Prioritize returning the primitive decimal value if it exists
3092 if let Some(precise_decimal) = &self.value {
3093 if let Some(decimal_val) = precise_decimal.value() {
3094 let result = EvaluationResult::fhir_decimal(decimal_val);
3095 return match primitive_meta {
3096 Some(meta) => result.with_primitive_element(meta),
3097 None => result,
3098 };
3099 }
3100 // If PreciseDecimal holds None for value, fall through to check id/extension
3101 }
3102
3103 // If value is None, but id or extension exist, return an Object with those
3104 if self.id.is_some() || self.extension.is_some() {
3105 let mut map = std::collections::HashMap::new();
3106 if let Some(id) = &self.id {
3107 map.insert("id".to_string(), EvaluationResult::string(id.clone()));
3108 }
3109 if let Some(ext) = &self.extension {
3110 let ext_collection: Vec<EvaluationResult> =
3111 ext.iter().map(|e| e.to_evaluation_result()).collect();
3112 if !ext_collection.is_empty() {
3113 map.insert(
3114 "extension".to_string(),
3115 EvaluationResult::collection(ext_collection),
3116 );
3117 }
3118 }
3119 // Only return Object if map is not empty
3120 if !map.is_empty() {
3121 return EvaluationResult::typed_object(map, "FHIR", "decimal");
3122 }
3123 }
3124
3125 // If value, id, and extension are all None, return Empty
3126 EvaluationResult::Empty
3127 }
3128}
3129
3130// Implement the trait for the top-level enum
3131impl IntoEvaluationResult for FhirResource {
3132 fn to_evaluation_result(&self) -> EvaluationResult {
3133 match self {
3134 #[cfg(feature = "R4")]
3135 FhirResource::R4(r) => (*r).to_evaluation_result(), // Call impl on inner Box<r4::Resource>
3136 #[cfg(feature = "R4B")]
3137 FhirResource::R4B(r) => (*r).to_evaluation_result(), // Call impl on inner Box<r4b::Resource>
3138 #[cfg(feature = "R5")]
3139 FhirResource::R5(r) => (*r).to_evaluation_result(), // Call impl on inner Box<r5::Resource>
3140 #[cfg(feature = "R6")]
3141 FhirResource::R6(r) => (*r).to_evaluation_result(), // Call impl on inner Box<r6::Resource>
3142 // Note: If no features are enabled, this match might be empty or non-exhaustive.
3143 // This is generally okay as the enum itself wouldn't be usable.
3144 }
3145 }
3146}
3147
3148#[cfg(test)]
3149mod tests {
3150 use super::*;
3151
3152 #[test]
3153 fn test_integer_string_deserialization() {
3154 // Test deserializing a string "2" into Element<i64, ()>
3155 type TestElement = Element<i64, ()>;
3156
3157 // Test case 1: String containing integer
3158 let json_str = r#""2""#;
3159 let result: Result<TestElement, _> = serde_json::from_str(json_str);
3160 assert!(
3161 result.is_ok(),
3162 "Failed to deserialize string '2' as i64: {:?}",
3163 result.err()
3164 );
3165
3166 let element = result.unwrap();
3167 assert_eq!(element.value, Some(2i64));
3168 assert_eq!(element.id, None);
3169 assert_eq!(element.extension, None);
3170
3171 // Test case 2: Number
3172 let json_num = r#"2"#;
3173 let result: Result<TestElement, _> = serde_json::from_str(json_num);
3174 assert!(
3175 result.is_ok(),
3176 "Failed to deserialize number 2 as i64: {:?}",
3177 result.err()
3178 );
3179
3180 let element = result.unwrap();
3181 assert_eq!(element.value, Some(2i64));
3182 }
3183
3184 #[test]
3185 fn test_i32_string_deserialization() {
3186 type TestElement = Element<i32, ()>;
3187
3188 let json_str = r#""123""#;
3189 let result: Result<TestElement, _> = serde_json::from_str(json_str);
3190 assert!(result.is_ok());
3191
3192 let element = result.unwrap();
3193 assert_eq!(element.value, Some(123i32));
3194 }
3195
3196 #[test]
3197 fn test_invalid_string_fallback() {
3198 type TestElement = Element<i64, ()>;
3199
3200 // Non-numeric string should fail for integer type
3201 let json_str = r#""not_a_number""#;
3202 let result: Result<TestElement, _> = serde_json::from_str(json_str);
3203 assert!(
3204 result.is_err(),
3205 "Should fail to deserialize non-numeric string as i64"
3206 );
3207 }
3208}