tlq_fhirpath/
value.rs

1//! Value representation for FHIRPath evaluation
2//!
3//! This module provides efficient, zero-copy value representation using Arc for cheap cloning.
4
5use chrono::{DateTime, NaiveDate, NaiveTime, Utc};
6use rust_decimal::Decimal;
7use serde_json::Value as JsonValue;
8use smallvec::SmallVec;
9use std::collections::HashMap;
10use std::hash::{Hash, Hasher};
11use std::sync::Arc;
12
13/// JSON navigation token for lazy JSON-backed values.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub enum JsonPathToken {
16    Key(Arc<str>),
17    Index(usize),
18}
19
20fn resolve_json_at<'a>(mut current: &'a JsonValue, path: &[JsonPathToken]) -> Option<&'a JsonValue> {
21    for token in path {
22        match token {
23            JsonPathToken::Key(key) => {
24                current = current.as_object()?.get(key.as_ref())?;
25            }
26            JsonPathToken::Index(idx) => {
27                current = current.as_array()?.get(*idx)?;
28            }
29        }
30    }
31    Some(current)
32}
33
34/// Time precision levels according to FHIRPath spec
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum TimePrecision {
37    Hour,        // @T10
38    Minute,      // @T10:30
39    Second,      // @T10:30:00
40    Millisecond, // @T10:30:00.000
41}
42
43impl TimePrecision {
44    /// Compare precision levels (returns true if same, false if different)
45    pub fn is_compatible_with(self, other: TimePrecision) -> bool {
46        self == other
47            || matches!(
48                (self, other),
49                (TimePrecision::Second, TimePrecision::Millisecond)
50                    | (TimePrecision::Millisecond, TimePrecision::Second)
51            )
52    }
53}
54
55/// Date precision levels according to FHIRPath spec
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum DatePrecision {
58    Year,  // @2014
59    Month, // @2014-01
60    Day,   // @2014-01-01
61}
62
63impl DatePrecision {
64    pub fn is_compatible_with(self, other: DatePrecision) -> bool {
65        self == other
66    }
67}
68
69/// DateTime precision levels according to FHIRPath spec
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub enum DateTimePrecision {
72    Year,        // @2015T
73    Month,       // @2015-02T
74    Day,         // @2015-02-04T
75    Hour,        // @2015-02-04T14
76    Minute,      // @2015-02-04T14:30
77    Second,      // @2015-02-04T14:30:00
78    Millisecond, // @2015-02-04T14:30:00.000
79}
80
81impl DateTimePrecision {
82    /// Compare precision levels (returns true if same, false if different)
83    pub fn is_compatible_with(self, other: DateTimePrecision) -> bool {
84        self == other
85            || matches!(
86                (self, other),
87                (DateTimePrecision::Second, DateTimePrecision::Millisecond)
88                    | (DateTimePrecision::Millisecond, DateTimePrecision::Second)
89            )
90    }
91}
92
93/// A FHIRPath value - cheap to clone via Arc
94#[derive(Clone, Debug)]
95pub struct Value(Arc<ValueData>);
96
97impl PartialEq for Value {
98    fn eq(&self, other: &Self) -> bool {
99        if self.ptr_eq(other) {
100            return true;
101        }
102        match (self.data(), other.data()) {
103            (ValueData::Empty, ValueData::Empty) => true,
104            (ValueData::Boolean(l), ValueData::Boolean(r)) => l == r,
105            (ValueData::Integer(l), ValueData::Integer(r)) => l == r,
106            (ValueData::Decimal(l), ValueData::Decimal(r)) => l == r,
107            (ValueData::String(l), ValueData::String(r)) => l == r,
108            (
109                ValueData::Date {
110                    value: lv,
111                    precision: lp,
112                },
113                ValueData::Date {
114                    value: rv,
115                    precision: rp,
116                },
117            ) => lv == rv && lp == rp,
118            (
119                ValueData::DateTime {
120                    value: lv,
121                    precision: lp,
122                    timezone_offset: lt,
123                },
124                ValueData::DateTime {
125                    value: rv,
126                    precision: rp,
127                    timezone_offset: rt,
128                },
129            ) => lv == rv && lp == rp && lt == rt,
130            (
131                ValueData::Time {
132                    value: lv,
133                    precision: lp,
134                },
135                ValueData::Time {
136                    value: rv,
137                    precision: rp,
138                },
139            ) => lv == rv && lp == rp,
140            (
141                ValueData::Quantity { value: lv, unit: lu },
142                ValueData::Quantity { value: rv, unit: ru },
143            ) => lv == rv && lu == ru,
144            (ValueData::Object(l), ValueData::Object(r)) => Arc::ptr_eq(l, r),
145            (
146                ValueData::LazyJson {
147                    root: lr,
148                    path: lp,
149                },
150                ValueData::LazyJson {
151                    root: rr,
152                    path: rp,
153                },
154            ) => Arc::ptr_eq(lr, rr) && lp == rp,
155            _ => false,
156        }
157    }
158}
159
160impl Eq for Value {}
161
162impl Hash for Value {
163    fn hash<H: Hasher>(&self, state: &mut H) {
164        match self.data() {
165            ValueData::Empty => {
166                0u8.hash(state);
167            }
168            ValueData::Boolean(b) => {
169                1u8.hash(state);
170                b.hash(state);
171            }
172            ValueData::Integer(i) => {
173                2u8.hash(state);
174                i.hash(state);
175            }
176            ValueData::Decimal(d) => {
177                3u8.hash(state);
178                // Hash decimal as canonical string to handle precision correctly
179                d.to_string().hash(state);
180            }
181            ValueData::String(s) => {
182                4u8.hash(state);
183                s.hash(state);
184            }
185            ValueData::Date { value, precision } => {
186                5u8.hash(state);
187                value.hash(state);
188                precision.hash(state);
189            }
190            ValueData::DateTime {
191                value,
192                precision,
193                timezone_offset,
194            } => {
195                6u8.hash(state);
196                value.hash(state);
197                precision.hash(state);
198                timezone_offset.hash(state);
199            }
200            ValueData::Time { value, precision } => {
201                7u8.hash(state);
202                value.hash(state);
203                precision.hash(state);
204            }
205            ValueData::Quantity { value, unit } => {
206                8u8.hash(state);
207                // Hash decimal as string for consistency
208                value.to_string().hash(state);
209                unit.hash(state);
210            }
211            ValueData::Object(map) => {
212                9u8.hash(state);
213                // Hash the pointer for Object (same as PartialEq logic)
214                Arc::as_ptr(map).hash(state);
215            }
216            ValueData::LazyJson { root, path } => {
217                10u8.hash(state);
218                // Hash the root pointer and path (same as PartialEq logic)
219                Arc::as_ptr(root).hash(state);
220                path.hash(state);
221            }
222        }
223    }
224}
225
226impl Value {
227    pub fn data(&self) -> &ValueData {
228        &self.0
229    }
230
231    pub fn ptr_eq(&self, other: &Self) -> bool {
232        Arc::ptr_eq(&self.0, &other.0)
233    }
234
235    pub fn from_json(json: JsonValue) -> Self {
236        Self::from_json_root(Arc::new(json))
237    }
238
239    pub fn from_json_root(root: Arc<JsonValue>) -> Self {
240        Self::from_json_node(root.clone(), SmallVec::new(), root.as_ref())
241    }
242
243    pub fn boolean(b: bool) -> Self {
244        Self(Arc::new(ValueData::Boolean(b)))
245    }
246
247    pub fn integer(i: i64) -> Self {
248        Self(Arc::new(ValueData::Integer(i)))
249    }
250
251    pub fn decimal(d: Decimal) -> Self {
252        Self(Arc::new(ValueData::Decimal(d)))
253    }
254
255    pub fn string(s: impl Into<Arc<str>>) -> Self {
256        Self(Arc::new(ValueData::String(s.into())))
257    }
258
259    pub fn empty() -> Self {
260        Self(Arc::new(ValueData::Empty))
261    }
262
263    pub fn date(d: NaiveDate) -> Self {
264        Self(Arc::new(ValueData::Date {
265            value: d,
266            precision: DatePrecision::Day,
267        }))
268    }
269
270    pub fn date_with_precision(d: NaiveDate, precision: DatePrecision) -> Self {
271        Self(Arc::new(ValueData::Date {
272            value: d,
273            precision,
274        }))
275    }
276
277    pub fn datetime(dt: DateTime<Utc>) -> Self {
278        Self(Arc::new(ValueData::DateTime {
279            value: dt,
280            precision: DateTimePrecision::Second,
281            timezone_offset: Some(0), // Default to Z/UTC
282        }))
283    }
284
285    pub fn datetime_with_precision(dt: DateTime<Utc>, precision: DateTimePrecision) -> Self {
286        Self(Arc::new(ValueData::DateTime {
287            value: dt,
288            precision,
289            timezone_offset: Some(0), // Default to Z/UTC
290        }))
291    }
292
293    pub fn datetime_with_precision_and_offset(
294        dt: DateTime<Utc>,
295        precision: DateTimePrecision,
296        offset_seconds: Option<i32>,
297    ) -> Self {
298        Self(Arc::new(ValueData::DateTime {
299            value: dt,
300            precision,
301            timezone_offset: offset_seconds,
302        }))
303    }
304
305    pub fn time(t: NaiveTime) -> Self {
306        Self(Arc::new(ValueData::Time {
307            value: t,
308            precision: TimePrecision::Second,
309        }))
310    }
311
312    pub fn time_with_precision(t: NaiveTime, precision: TimePrecision) -> Self {
313        Self(Arc::new(ValueData::Time {
314            value: t,
315            precision,
316        }))
317    }
318
319    pub fn quantity(value: Decimal, unit: Arc<str>) -> Self {
320        Self(Arc::new(ValueData::Quantity { value, unit }))
321    }
322
323    pub fn object(map: HashMap<Arc<str>, Collection>) -> Self {
324        Self(Arc::new(ValueData::Object(Arc::new(map))))
325    }
326
327    /// Create Value from JSON using eager conversion (for use in from_json_eager)
328    fn from_json_eager_value(json: &JsonValue) -> Self {
329        Self(Arc::new(ValueData::from_json_eager(json)))
330    }
331
332    pub(crate) fn from_json_node(
333        root: Arc<JsonValue>,
334        path: SmallVec<[JsonPathToken; 4]>,
335        node: &JsonValue,
336    ) -> Self {
337        match node {
338            JsonValue::Null => Self::empty(),
339            JsonValue::Bool(b) => Self::boolean(*b),
340            JsonValue::Number(n) => {
341                if let Some(i) = n.as_i64() {
342                    Self::integer(i)
343                } else if let Some(f) = n.as_f64() {
344                    Self::decimal(Decimal::from_f64_retain(f).unwrap_or_default())
345                } else {
346                    Self::empty()
347                }
348            }
349            JsonValue::String(s) => Self::string(Arc::from(s.as_str())),
350            JsonValue::Object(_) => Self(Arc::new(ValueData::LazyJson { root, path })),
351            JsonValue::Array(_) => Self::empty(),
352        }
353    }
354
355    /// Materialize a lazy value into an eagerly-evaluated structure.
356    ///
357    /// This forces conversion of LazyJson to Object. For non-lazy values, returns the same value.
358    /// Use this when you need to ensure a value is fully materialized (e.g., for functions
359    /// that iterate all fields of an object).
360    pub fn materialize(&self) -> Self {
361        match self.data() {
362            ValueData::LazyJson { .. } => Self(Arc::new(self.0.materialize())),
363            _ => self.clone(),
364        }
365    }
366}
367
368/// Internal value data representation
369#[derive(Debug, Clone)]
370pub enum ValueData {
371    // Primitives (inline)
372    Boolean(bool),
373    Integer(i64),
374    Decimal(Decimal),
375
376    // Heap-allocated
377    String(Arc<str>),
378    Date {
379        value: NaiveDate,
380        precision: DatePrecision,
381    },
382    DateTime {
383        value: DateTime<Utc>,
384        precision: DateTimePrecision,
385        /// Timezone offset in seconds east of UTC.
386        /// - `None` means no timezone was specified (local/unknown offset).
387        /// - `Some(0)` means `Z`/UTC.
388        /// - `Some(n)` means a fixed offset `+/-HH:MM`.
389        timezone_offset: Option<i32>,
390    },
391    Time {
392        value: NaiveTime,
393        precision: TimePrecision,
394    },
395    Quantity {
396        value: Decimal,
397        unit: Arc<str>,
398    },
399
400    // Structured (shared)
401    Object(Arc<HashMap<Arc<str>, Collection>>),
402
403    // Lazy JSON - defers conversion until field access (major performance optimization)
404    /// References a node inside a shared JSON tree. Navigation extends `path` without cloning JSON.
405    LazyJson {
406        root: Arc<JsonValue>,
407        path: SmallVec<[JsonPathToken; 4]>,
408    },
409
410    // Special
411    Empty,
412    // TypeInfo will be added later when types module is ready
413}
414
415impl ValueData {
416    pub(crate) fn resolved_json(&self) -> Option<&JsonValue> {
417        match self {
418            ValueData::LazyJson { root, path } => resolve_json_at(root.as_ref(), path.as_slice()),
419            _ => None,
420        }
421    }
422
423    /// Get string value if this is a String
424    pub fn as_string(&self) -> Option<Arc<str>> {
425        match self {
426            ValueData::String(s) => Some(s.clone()),
427            _ => None,
428        }
429    }
430
431    /// Force conversion of lazy JSON to eagerly-evaluated Object structure.
432    ///
433    /// This is used when we need the full object structure (e.g., for functions
434    /// that iterate all keys), but most path navigation avoids this.
435    pub(crate) fn materialize(&self) -> ValueData {
436        match self {
437            ValueData::LazyJson { .. } => self
438                .resolved_json()
439                .map(Self::from_json_eager)
440                .unwrap_or(ValueData::Empty),
441            other => other.clone(),
442        }
443    }
444
445    /// Eagerly convert JSON to Object (old behavior) - only used when materializing
446    fn from_json_eager(json: &JsonValue) -> Self {
447        match json {
448            JsonValue::Null => ValueData::Empty,
449            JsonValue::Bool(b) => ValueData::Boolean(*b),
450            JsonValue::Number(n) => {
451                if let Some(i) = n.as_i64() {
452                    ValueData::Integer(i)
453                } else if let Some(f) = n.as_f64() {
454                    ValueData::Decimal(Decimal::from_f64_retain(f).unwrap_or_default())
455                } else {
456                    ValueData::Empty
457                }
458            }
459            JsonValue::String(s) => ValueData::String(Arc::from(s.as_str())),
460            JsonValue::Array(_arr) => {
461                // Arrays become empty - they're handled at Collection level
462                ValueData::Empty
463            }
464            JsonValue::Object(obj) => {
465                let mut map = HashMap::new();
466                for (k, v) in obj {
467                    if let JsonValue::Array(arr) = v {
468                        let mut coll = Collection::empty();
469                        for item in arr {
470                            coll.push(Value::from_json_eager_value(item));
471                        }
472                        map.insert(Arc::from(k.as_str()), coll);
473                    } else {
474                        let mut coll = Collection::empty();
475                        coll.push(Value::from_json_eager_value(v));
476                        map.insert(Arc::from(k.as_str()), coll);
477                    }
478                }
479                ValueData::Object(Arc::new(map))
480            }
481        }
482    }
483}
484
485/// Threshold for switching from SmallVec to Arc<SmallVec> for cloning optimization.
486/// Collections with more than this many items will use Arc to make cloning O(1).
487const COLLECTION_ARC_THRESHOLD: usize = 4;
488
489/// Collection optimized for singleton case (90% of FHIRPath collections are single-element)
490///
491/// For small collections (≤4 items), uses SmallVec directly.
492/// For large collections (>4 items), wraps the vector in Arc to make cloning O(1).
493#[derive(Clone, Debug)]
494pub struct Collection {
495    inner: CollectionInner,
496}
497
498#[derive(Clone, Debug)]
499enum CollectionInner {
500    /// Small collections stored directly (≤4 items)
501    Small(SmallVec<[Value; 4]>),
502    /// Large collections wrapped in Arc (>4 items)
503    /// Cloning is O(1) - just increments the reference count
504    Large(Arc<SmallVec<[Value; 4]>>),
505}
506
507impl Collection {
508    pub fn empty() -> Self {
509        Self {
510            inner: CollectionInner::Small(SmallVec::new()),
511        }
512    }
513
514    pub fn singleton(value: Value) -> Self {
515        let mut inner = SmallVec::new();
516        inner.push(value);
517        Self {
518            inner: CollectionInner::Small(inner),
519        }
520    }
521
522    pub fn with_capacity(capacity: usize) -> Self {
523        let inner = SmallVec::with_capacity(capacity);
524        if capacity > COLLECTION_ARC_THRESHOLD {
525            Self {
526                inner: CollectionInner::Large(Arc::new(inner)),
527            }
528        } else {
529            Self {
530                inner: CollectionInner::Small(inner),
531            }
532        }
533    }
534
535    /// Get a mutable reference to the underlying SmallVec.
536    /// If the collection is Arc-wrapped, this will clone it to make it mutable.
537    fn get_mut(&mut self) -> &mut SmallVec<[Value; 4]> {
538        // If we have an Arc-wrapped collection, convert it to SmallVec first
539        if let CollectionInner::Large(arc) = &self.inner {
540            let vec = (**arc).clone();
541            self.inner = CollectionInner::Small(vec);
542        }
543
544        // Now we can safely get a mutable reference
545        match &mut self.inner {
546            CollectionInner::Small(vec) => vec,
547            CollectionInner::Large(_) => unreachable!(),
548        }
549    }
550
551    /// Ensure the collection is in the appropriate representation based on its size.
552    /// If it's large and currently Small, convert to Arc. If it's small and currently Arc, convert back.
553    fn ensure_representation(&mut self) {
554        let len = self.len();
555        match &self.inner {
556            CollectionInner::Small(vec) if len > COLLECTION_ARC_THRESHOLD => {
557                // Convert to Arc-wrapped
558                let vec = vec.clone();
559                self.inner = CollectionInner::Large(Arc::new(vec));
560            }
561            CollectionInner::Large(arc) if len <= COLLECTION_ARC_THRESHOLD => {
562                // Convert back to SmallVec (unlikely but possible if items are removed)
563                let vec = (**arc).clone();
564                self.inner = CollectionInner::Small(vec);
565            }
566            _ => {
567                // Already in correct representation
568            }
569        }
570    }
571
572    pub fn push(&mut self, value: Value) {
573        self.get_mut().push(value);
574        self.ensure_representation();
575    }
576
577    pub fn iter(&self) -> impl Iterator<Item = &Value> {
578        // Both SmallVec and Arc<SmallVec> can be converted to slices
579        match &self.inner {
580            CollectionInner::Small(vec) => vec.iter(),
581            CollectionInner::Large(arc) => arc.iter(),
582        }
583    }
584
585    pub fn is_empty(&self) -> bool {
586        match &self.inner {
587            CollectionInner::Small(vec) => vec.is_empty(),
588            CollectionInner::Large(arc) => arc.is_empty(),
589        }
590    }
591
592    pub fn len(&self) -> usize {
593        match &self.inner {
594            CollectionInner::Small(vec) => vec.len(),
595            CollectionInner::Large(arc) => arc.len(),
596        }
597    }
598
599    /// Get a value by index
600    pub fn get(&self, index: usize) -> Option<&Value> {
601        match &self.inner {
602            CollectionInner::Small(vec) => vec.get(index),
603            CollectionInner::Large(arc) => arc.get(index),
604        }
605    }
606
607    pub fn as_boolean(&self) -> Result<bool> {
608        if self.is_empty() {
609            return Ok(false);
610        }
611        if self.len() > 1 {
612            return Err(Error::TypeError("Expected singleton boolean".into()));
613        }
614        match self.get(0).unwrap().data() {
615            ValueData::Boolean(b) => Ok(*b),
616            _ => Err(Error::TypeError("Expected boolean value".into())),
617        }
618    }
619
620    pub fn as_string(&self) -> Result<Arc<str>> {
621        if self.is_empty() {
622            return Err(Error::TypeError("Empty collection".into()));
623        }
624        if self.len() > 1 {
625            return Err(Error::TypeError("Expected singleton string".into()));
626        }
627        match self.get(0).unwrap().data() {
628            ValueData::String(s) => Ok(s.clone()),
629            _ => Err(Error::TypeError("Expected string value".into())),
630        }
631    }
632
633    pub fn as_integer(&self) -> Result<i64> {
634        if self.is_empty() {
635            return Err(Error::TypeError("Empty collection".into()));
636        }
637        if self.len() > 1 {
638            return Err(Error::TypeError("Expected singleton integer".into()));
639        }
640        match self.get(0).unwrap().data() {
641            ValueData::Integer(i) => Ok(*i),
642            _ => Err(Error::TypeError("Expected integer value".into())),
643        }
644    }
645}
646
647use crate::error::{Error, Result};