Skip to main content

uni_common/
value.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Typed value representation for graph properties and query results.
5//!
6//! [`Value`] is the canonical internal representation for all property values,
7//! query parameters, and expression results. Unlike `serde_json::Value`, it
8//! distinguishes integers from floats (`Int(i64)` vs `Float(f64)`) and includes
9//! graph-specific variants (`Node`, `Edge`, `Path`, `Vector`).
10//!
11//! Conversion to/from `serde_json::Value` is provided at the serialization
12//! boundary via `From` implementations.
13
14use crate::api::error::UniError;
15use crate::core::id::{Eid, Vid};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::fmt;
19use std::hash::{Hash, Hasher};
20
21// ============================================================================
22// Temporal Value Types
23// ============================================================================
24
25/// Classification of temporal types for dispatch.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum TemporalType {
28    Date,
29    LocalTime,
30    Time,
31    LocalDateTime,
32    DateTime,
33    Duration,
34    Btic,
35}
36
37/// Typed temporal value representation.
38///
39/// Stores temporal values in their native numeric form for O(1) comparisons
40/// and direct Arrow column construction, with Cypher formatting applied only
41/// at the output boundary via [`std::fmt::Display`].
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub enum TemporalValue {
44    /// Date: days since Unix epoch (1970-01-01). Arrow: Date32.
45    Date { days_since_epoch: i32 },
46    /// Local time (no timezone): nanoseconds since midnight. Arrow: Time64(ns).
47    LocalTime { nanos_since_midnight: i64 },
48    /// Time with timezone offset: nanoseconds since midnight + offset. Arrow: Time64(ns) + metadata.
49    Time {
50        nanos_since_midnight: i64,
51        offset_seconds: i32,
52    },
53    /// Local datetime (no timezone): nanoseconds since Unix epoch. Arrow: Timestamp(ns, None).
54    LocalDateTime { nanos_since_epoch: i64 },
55    /// Datetime with timezone: nanoseconds since Unix epoch (UTC) + offset + optional tz name.
56    /// Arrow: Timestamp(ns, Some("UTC")).
57    DateTime {
58        nanos_since_epoch: i64,
59        offset_seconds: i32,
60        timezone_name: Option<String>,
61    },
62    /// Duration with calendar semantics: months + days + nanoseconds.
63    /// Matches Cypher's duration model which preserves calendar components.
64    Duration { months: i64, days: i64, nanos: i64 },
65    /// Binary Temporal Interval Codec: half-open `[lo, hi)` in milliseconds since epoch,
66    /// with per-bound granularity and certainty packed in a 64-bit meta word.
67    Btic { lo: i64, hi: i64, meta: u64 },
68}
69
70impl Eq for TemporalValue {}
71
72impl Hash for TemporalValue {
73    fn hash<H: Hasher>(&self, state: &mut H) {
74        std::mem::discriminant(self).hash(state);
75        match self {
76            TemporalValue::Date { days_since_epoch } => days_since_epoch.hash(state),
77            TemporalValue::LocalTime {
78                nanos_since_midnight,
79            } => nanos_since_midnight.hash(state),
80            TemporalValue::Time {
81                nanos_since_midnight,
82                offset_seconds,
83            } => {
84                nanos_since_midnight.hash(state);
85                offset_seconds.hash(state);
86            }
87            TemporalValue::LocalDateTime { nanos_since_epoch } => nanos_since_epoch.hash(state),
88            TemporalValue::DateTime {
89                nanos_since_epoch,
90                offset_seconds,
91                timezone_name,
92            } => {
93                nanos_since_epoch.hash(state);
94                offset_seconds.hash(state);
95                timezone_name.hash(state);
96            }
97            TemporalValue::Duration {
98                months,
99                days,
100                nanos,
101            } => {
102                months.hash(state);
103                days.hash(state);
104                nanos.hash(state);
105            }
106            TemporalValue::Btic { lo, hi, meta } => {
107                lo.hash(state);
108                hi.hash(state);
109                meta.hash(state);
110            }
111        }
112    }
113}
114
115impl TemporalValue {
116    /// Returns the temporal type classification.
117    pub fn temporal_type(&self) -> TemporalType {
118        match self {
119            TemporalValue::Date { .. } => TemporalType::Date,
120            TemporalValue::LocalTime { .. } => TemporalType::LocalTime,
121            TemporalValue::Time { .. } => TemporalType::Time,
122            TemporalValue::LocalDateTime { .. } => TemporalType::LocalDateTime,
123            TemporalValue::DateTime { .. } => TemporalType::DateTime,
124            TemporalValue::Duration { .. } => TemporalType::Duration,
125            TemporalValue::Btic { .. } => TemporalType::Btic,
126        }
127    }
128
129    // -----------------------------------------------------------------------
130    // Component accessors
131    // -----------------------------------------------------------------------
132
133    /// Year component, or None for time-only/duration types.
134    pub fn year(&self) -> Option<i64> {
135        self.to_date().map(|d| d.year() as i64)
136    }
137
138    /// Month component (1-12), or None for time-only/duration types.
139    pub fn month(&self) -> Option<i64> {
140        self.to_date().map(|d| d.month() as i64)
141    }
142
143    /// Day-of-month component (1-31), or None for time-only/duration types.
144    pub fn day(&self) -> Option<i64> {
145        self.to_date().map(|d| d.day() as i64)
146    }
147
148    /// Hour component (0-23), or None for date-only types.
149    pub fn hour(&self) -> Option<i64> {
150        self.to_time().map(|t| t.hour() as i64)
151    }
152
153    /// Minute component (0-59), or None for date-only types.
154    pub fn minute(&self) -> Option<i64> {
155        self.to_time().map(|t| t.minute() as i64)
156    }
157
158    /// Second component (0-59), or None for date-only types.
159    pub fn second(&self) -> Option<i64> {
160        self.to_time().map(|t| t.second() as i64)
161    }
162
163    /// Millisecond sub-second component (0-999), or None for date-only types.
164    pub fn millisecond(&self) -> Option<i64> {
165        self.to_time().map(|t| (t.nanosecond() / 1_000_000) as i64)
166    }
167
168    /// Microsecond sub-second component (0-999_999), or None for date-only types.
169    pub fn microsecond(&self) -> Option<i64> {
170        self.to_time().map(|t| (t.nanosecond() / 1_000) as i64)
171    }
172
173    /// Nanosecond sub-second component (0-999_999_999), or None for date-only types.
174    pub fn nanosecond(&self) -> Option<i64> {
175        self.to_time().map(|t| t.nanosecond() as i64)
176    }
177
178    /// Quarter (1-4), or None for time-only/duration types.
179    pub fn quarter(&self) -> Option<i64> {
180        self.to_date().map(|d| ((d.month() - 1) / 3 + 1) as i64)
181    }
182
183    /// ISO week number (1-53), or None for time-only/duration types.
184    pub fn week(&self) -> Option<i64> {
185        self.to_date().map(|d| d.iso_week().week() as i64)
186    }
187
188    /// ISO week year, or None for time-only/duration types.
189    pub fn week_year(&self) -> Option<i64> {
190        self.to_date().map(|d| d.iso_week().year() as i64)
191    }
192
193    /// Ordinal day of year (1-366), or None for time-only/duration types.
194    pub fn ordinal_day(&self) -> Option<i64> {
195        self.to_date().map(|d| d.ordinal() as i64)
196    }
197
198    /// ISO day of week (Monday=1, Sunday=7), or None for time-only/duration types.
199    pub fn day_of_week(&self) -> Option<i64> {
200        self.to_date()
201            .map(|d| (d.weekday().num_days_from_monday() + 1) as i64)
202    }
203
204    /// Day of quarter (1-92), or None for time-only/duration types.
205    pub fn day_of_quarter(&self) -> Option<i64> {
206        self.to_date().map(|d| {
207            let quarter_start_month = ((d.month() - 1) / 3) * 3 + 1;
208            let quarter_start =
209                chrono::NaiveDate::from_ymd_opt(d.year(), quarter_start_month, 1).unwrap();
210            d.signed_duration_since(quarter_start).num_days() + 1
211        })
212    }
213
214    /// Timezone name if available (e.g., "Europe/Stockholm").
215    pub fn timezone(&self) -> Option<&str> {
216        match self {
217            TemporalValue::DateTime {
218                timezone_name: Some(name),
219                ..
220            } => Some(name.as_str()),
221            _ => None,
222        }
223    }
224
225    /// Returns the raw offset in seconds for types that carry a timezone offset.
226    fn raw_offset_seconds(&self) -> Option<i32> {
227        match self {
228            TemporalValue::Time { offset_seconds, .. }
229            | TemporalValue::DateTime { offset_seconds, .. } => Some(*offset_seconds),
230            _ => None,
231        }
232    }
233
234    /// Offset string (e.g., "+01:00", "Z").
235    pub fn offset(&self) -> Option<String> {
236        self.raw_offset_seconds().map(format_offset)
237    }
238
239    /// Offset in minutes.
240    pub fn offset_minutes(&self) -> Option<i64> {
241        self.raw_offset_seconds().map(|s| s as i64 / 60)
242    }
243
244    /// Offset in seconds.
245    pub fn offset_seconds_value(&self) -> Option<i64> {
246        self.raw_offset_seconds().map(|s| s as i64)
247    }
248
249    /// Returns the raw epoch nanos for types that store nanoseconds since epoch.
250    fn raw_epoch_nanos(&self) -> Option<i64> {
251        match self {
252            TemporalValue::DateTime {
253                nanos_since_epoch, ..
254            }
255            | TemporalValue::LocalDateTime {
256                nanos_since_epoch, ..
257            } => Some(*nanos_since_epoch),
258            _ => None,
259        }
260    }
261
262    /// Epoch seconds (for datetime/localdatetime types).
263    pub fn epoch_seconds(&self) -> Option<i64> {
264        self.raw_epoch_nanos().map(|n| n / 1_000_000_000)
265    }
266
267    /// Epoch milliseconds (for datetime/localdatetime types).
268    pub fn epoch_millis(&self) -> Option<i64> {
269        self.raw_epoch_nanos().map(|n| n / 1_000_000)
270    }
271
272    // -----------------------------------------------------------------------
273    // Internal chrono conversion helpers
274    // -----------------------------------------------------------------------
275
276    /// Extract a NaiveDate from types that have a date component.
277    pub fn to_date(&self) -> Option<chrono::NaiveDate> {
278        let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1)?;
279        match self {
280            TemporalValue::Date { days_since_epoch } => {
281                epoch.checked_add_signed(chrono::Duration::days(*days_since_epoch as i64))
282            }
283            TemporalValue::LocalDateTime { nanos_since_epoch } => {
284                let dt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch);
285                Some(dt.date_naive())
286            }
287            TemporalValue::DateTime {
288                nanos_since_epoch,
289                offset_seconds,
290                ..
291            } => {
292                // Convert UTC nanos to local time by adding offset
293                let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
294                let dt = chrono::DateTime::from_timestamp_nanos(local_nanos);
295                Some(dt.date_naive())
296            }
297            _ => None,
298        }
299    }
300
301    /// Extract a NaiveTime from types that have a time component.
302    pub fn to_time(&self) -> Option<chrono::NaiveTime> {
303        match self {
304            TemporalValue::LocalTime {
305                nanos_since_midnight,
306            }
307            | TemporalValue::Time {
308                nanos_since_midnight,
309                ..
310            } => nanos_to_time(*nanos_since_midnight),
311            TemporalValue::LocalDateTime { nanos_since_epoch } => {
312                let dt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch);
313                Some(dt.naive_utc().time())
314            }
315            TemporalValue::DateTime {
316                nanos_since_epoch,
317                offset_seconds,
318                ..
319            } => {
320                let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
321                let dt = chrono::DateTime::from_timestamp_nanos(local_nanos);
322                Some(dt.naive_utc().time())
323            }
324            _ => None,
325        }
326    }
327}
328
329/// Convert nanoseconds since midnight to NaiveTime.
330fn nanos_to_time(nanos: i64) -> Option<chrono::NaiveTime> {
331    let total_secs = nanos / 1_000_000_000;
332    let h = (total_secs / 3600) as u32;
333    let m = ((total_secs % 3600) / 60) as u32;
334    let s = (total_secs % 60) as u32;
335    let ns = (nanos % 1_000_000_000) as u32;
336    chrono::NaiveTime::from_hms_nano_opt(h, m, s, ns)
337}
338
339/// Format an offset in seconds as "+HH:MM" or "Z".
340fn format_offset(offset_seconds: i32) -> String {
341    if offset_seconds == 0 {
342        return "Z".to_string();
343    }
344    format_offset_numeric(offset_seconds)
345}
346
347/// Format offset always as `+HH:MM` or `+HH:MM:SS` (never as `Z`).
348fn format_offset_numeric(offset_seconds: i32) -> String {
349    let sign = if offset_seconds >= 0 { '+' } else { '-' };
350    let abs = offset_seconds.unsigned_abs();
351    let h = abs / 3600;
352    let m = (abs % 3600) / 60;
353    let s = abs % 60;
354    if s != 0 {
355        format!("{}{:02}:{:02}:{:02}", sign, h, m, s)
356    } else {
357        format!("{}{:02}:{:02}", sign, h, m)
358    }
359}
360
361/// Format sub-second fractional part, stripping all trailing zeros.
362fn format_fractional(nanos: u32) -> String {
363    if nanos == 0 {
364        return String::new();
365    }
366    let s = format!("{:09}", nanos);
367    let trimmed = s.trim_end_matches('0');
368    format!(".{}", trimmed)
369}
370
371/// Format time as HH:MM[:SS[.n...]] — omit :SS when seconds and sub-seconds are zero.
372fn format_time_component(hour: u32, minute: u32, second: u32, nanos: u32) -> String {
373    if second == 0 && nanos == 0 {
374        format!("{:02}:{:02}", hour, minute)
375    } else {
376        let frac = format_fractional(nanos);
377        format!("{:02}:{:02}:{:02}{}", hour, minute, second, frac)
378    }
379}
380
381/// Format a NaiveTime as a canonical time string.
382fn format_naive_time(t: &chrono::NaiveTime) -> String {
383    format_time_component(t.hour(), t.minute(), t.second(), t.nanosecond())
384}
385
386/// Convert nanos since midnight to NaiveTime, defaulting to midnight on invalid input.
387fn nanos_to_time_or_midnight(nanos: i64) -> chrono::NaiveTime {
388    nanos_to_time(nanos).unwrap_or_else(|| chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
389}
390
391impl fmt::Display for TemporalValue {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        match self {
394            TemporalValue::Date { days_since_epoch } => {
395                let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
396                let date = epoch + chrono::Duration::days(*days_since_epoch as i64);
397                write!(f, "{}", date.format("%Y-%m-%d"))
398            }
399            TemporalValue::LocalTime {
400                nanos_since_midnight,
401            } => {
402                let time = nanos_to_time_or_midnight(*nanos_since_midnight);
403                write!(f, "{}", format_naive_time(&time))
404            }
405            TemporalValue::Time {
406                nanos_since_midnight,
407                offset_seconds,
408            } => {
409                let time = nanos_to_time_or_midnight(*nanos_since_midnight);
410                write!(
411                    f,
412                    "{}{}",
413                    format_naive_time(&time),
414                    format_offset(*offset_seconds)
415                )
416            }
417            TemporalValue::LocalDateTime { nanos_since_epoch } => {
418                let ndt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch).naive_utc();
419                write!(
420                    f,
421                    "{}T{}",
422                    ndt.date().format("%Y-%m-%d"),
423                    format_naive_time(&ndt.time())
424                )
425            }
426            TemporalValue::DateTime {
427                nanos_since_epoch,
428                offset_seconds,
429                timezone_name,
430            } => {
431                // Display in local time (UTC nanos + offset)
432                let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
433                let ndt = chrono::DateTime::from_timestamp_nanos(local_nanos).naive_utc();
434                let tz = format_offset(*offset_seconds);
435                write!(
436                    f,
437                    "{}T{}{}",
438                    ndt.date().format("%Y-%m-%d"),
439                    format_naive_time(&ndt.time()),
440                    tz
441                )?;
442                if let Some(name) = timezone_name {
443                    write!(f, "[{}]", name)?;
444                }
445                Ok(())
446            }
447            TemporalValue::Duration {
448                months,
449                days,
450                nanos,
451            } => {
452                write!(f, "P")?;
453                let years = months / 12;
454                let rem_months = months % 12;
455                if years != 0 {
456                    write!(f, "{}Y", years)?;
457                }
458                if rem_months != 0 {
459                    write!(f, "{}M", rem_months)?;
460                }
461                if *days != 0 {
462                    write!(f, "{}D", days)?;
463                }
464                // Time part
465                let abs_nanos = nanos.unsigned_abs() as i128;
466                let nanos_sign = if *nanos < 0 { -1i64 } else { 1 };
467                let total_secs = (abs_nanos / 1_000_000_000) as i64;
468                let frac_nanos = (abs_nanos % 1_000_000_000) as u32;
469                let hours = total_secs / 3600;
470                let mins = (total_secs % 3600) / 60;
471                let secs = total_secs % 60;
472
473                if hours != 0 || mins != 0 || secs != 0 || frac_nanos != 0 {
474                    write!(f, "T")?;
475                    if hours != 0 {
476                        write!(f, "{}H", hours * nanos_sign)?;
477                    }
478                    if mins != 0 {
479                        write!(f, "{}M", mins * nanos_sign)?;
480                    }
481                    if secs != 0 || frac_nanos != 0 {
482                        let frac = format_fractional(frac_nanos);
483                        if nanos_sign < 0 && (secs != 0 || frac_nanos != 0) {
484                            write!(f, "-{}{}", secs, frac)?;
485                        } else {
486                            write!(f, "{}{}", secs, frac)?;
487                        }
488                        write!(f, "S")?;
489                    }
490                } else if years == 0 && rem_months == 0 && *days == 0 {
491                    // Zero duration
492                    write!(f, "T0S")?;
493                }
494                Ok(())
495            }
496            TemporalValue::Btic { lo, hi, meta } => match uni_btic::Btic::new(*lo, *hi, *meta) {
497                Ok(btic) => write!(f, "{btic}"),
498                Err(_) => write!(f, "Btic[lo={lo}, hi={hi}, meta={meta:#x}]"),
499            },
500        }
501    }
502}
503
504// Use chrono traits in component accessors - needed by TemporalValue accessors
505use chrono::Datelike as _;
506use chrono::Timelike as _;
507
508/// Dynamic value type for properties, parameters, and results.
509///
510/// Preserves the distinction between integers and floats, and includes
511/// graph-specific variants for nodes, edges, paths, and vectors.
512///
513/// Note: `Eq` and `Hash` are implemented manually to support using `Value` as
514/// HashMap keys. Floats are compared/hashed by their bit representation.
515#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516#[serde(untagged)]
517#[non_exhaustive]
518pub enum Value {
519    /// JSON/Cypher null.
520    Null,
521    /// Boolean value.
522    Bool(bool),
523    /// 64-bit signed integer.
524    Int(i64),
525    /// 64-bit floating-point number.
526    Float(f64),
527    /// UTF-8 string.
528    String(String),
529    /// Raw byte buffer.
530    Bytes(Vec<u8>),
531    /// Ordered list of values.
532    List(Vec<Value>),
533    /// String-keyed map of values.
534    Map(HashMap<String, Value>),
535
536    // Graph-specific
537    /// Graph node with VID, label, and properties.
538    Node(Node),
539    /// Graph edge with EID, type, endpoints, and properties.
540    Edge(Edge),
541    /// Graph path (alternating nodes and edges).
542    Path(Path),
543
544    // Vector
545    /// Dense float vector for similarity search.
546    Vector(Vec<f32>),
547
548    // Temporal
549    /// Typed temporal value (date, time, datetime, duration).
550    Temporal(TemporalValue),
551}
552
553// ---------------------------------------------------------------------------
554// Accessor methods (mirrors serde_json::Value API for migration ease)
555// ---------------------------------------------------------------------------
556
557impl Value {
558    /// Returns `true` if this value is `Null`.
559    pub fn is_null(&self) -> bool {
560        matches!(self, Value::Null)
561    }
562
563    /// Returns the boolean if this is `Bool`, otherwise `None`.
564    pub fn as_bool(&self) -> Option<bool> {
565        match self {
566            Value::Bool(b) => Some(*b),
567            _ => None,
568        }
569    }
570
571    /// Returns the integer if this is `Int`, otherwise `None`.
572    pub fn as_i64(&self) -> Option<i64> {
573        match self {
574            Value::Int(i) => Some(*i),
575            _ => None,
576        }
577    }
578
579    /// Returns the integer as `u64` if this is a non-negative `Int`, otherwise `None`.
580    pub fn as_u64(&self) -> Option<u64> {
581        match self {
582            Value::Int(i) if *i >= 0 => Some(*i as u64),
583            _ => None,
584        }
585    }
586
587    /// Returns a float, coercing `Int` to `f64` if needed.
588    ///
589    /// Returns `None` for non-numeric variants.
590    pub fn as_f64(&self) -> Option<f64> {
591        match self {
592            Value::Float(f) => Some(*f),
593            Value::Int(i) => Some(*i as f64),
594            _ => None,
595        }
596    }
597
598    /// Returns the string slice if this is `String`, otherwise `None`.
599    pub fn as_str(&self) -> Option<&str> {
600        match self {
601            Value::String(s) => Some(s),
602            _ => None,
603        }
604    }
605
606    /// Returns `true` if this is `Int`.
607    pub fn is_i64(&self) -> bool {
608        matches!(self, Value::Int(_))
609    }
610
611    /// Returns `true` if this is `Float` (not `Int`).
612    pub fn is_f64(&self) -> bool {
613        matches!(self, Value::Float(_))
614    }
615
616    /// Returns `true` if this is `String`.
617    pub fn is_string(&self) -> bool {
618        matches!(self, Value::String(_))
619    }
620
621    /// Returns `true` if this is `Int` or `Float`.
622    pub fn is_number(&self) -> bool {
623        matches!(self, Value::Int(_) | Value::Float(_))
624    }
625
626    /// Returns the list if this is `List`, otherwise `None`.
627    pub fn as_array(&self) -> Option<&Vec<Value>> {
628        match self {
629            Value::List(l) => Some(l),
630            _ => None,
631        }
632    }
633
634    /// Returns the map if this is `Map`, otherwise `None`.
635    pub fn as_object(&self) -> Option<&HashMap<String, Value>> {
636        match self {
637            Value::Map(m) => Some(m),
638            _ => None,
639        }
640    }
641
642    /// Returns `true` if this is `Bool`.
643    pub fn is_bool(&self) -> bool {
644        matches!(self, Value::Bool(_))
645    }
646
647    /// Returns `true` if this is `List`.
648    pub fn is_list(&self) -> bool {
649        matches!(self, Value::List(_))
650    }
651
652    /// Returns `true` if this is `Map`.
653    pub fn is_map(&self) -> bool {
654        matches!(self, Value::Map(_))
655    }
656
657    /// Gets a value by key if this is a `Map`.
658    ///
659    /// Returns `None` if not a map or key doesn't exist.
660    pub fn get(&self, key: &str) -> Option<&Value> {
661        match self {
662            Value::Map(m) => m.get(key),
663            _ => None,
664        }
665    }
666
667    /// Returns `true` if this is a `Temporal` value.
668    pub fn is_temporal(&self) -> bool {
669        matches!(self, Value::Temporal(_))
670    }
671
672    /// Returns the temporal value reference if this is `Temporal`, otherwise `None`.
673    pub fn as_temporal(&self) -> Option<&TemporalValue> {
674        match self {
675            Value::Temporal(t) => Some(t),
676            _ => None,
677        }
678    }
679}
680
681impl fmt::Display for Value {
682    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
683        match self {
684            Value::Null => write!(f, "null"),
685            Value::Bool(b) => write!(f, "{b}"),
686            Value::Int(i) => write!(f, "{i}"),
687            Value::Float(v) => {
688                if v.fract() == 0.0 && v.is_finite() {
689                    write!(f, "{v:.1}")
690                } else {
691                    write!(f, "{v}")
692                }
693            }
694            Value::String(s) => write!(f, "{s}"),
695            Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
696            Value::List(l) => {
697                write!(f, "[")?;
698                for (i, item) in l.iter().enumerate() {
699                    if i > 0 {
700                        write!(f, ", ")?;
701                    }
702                    write!(f, "{item}")?;
703                }
704                write!(f, "]")
705            }
706            Value::Map(m) => {
707                write!(f, "{{")?;
708                for (i, (k, v)) in m.iter().enumerate() {
709                    if i > 0 {
710                        write!(f, ", ")?;
711                    }
712                    write!(f, "{k}: {v}")?;
713                }
714                write!(f, "}}")
715            }
716            Value::Node(n) => write!(f, "(:{} {{vid: {}}})", n.labels.join(":"), n.vid),
717            Value::Edge(e) => write!(f, "-[:{}]-", e.edge_type),
718            Value::Path(p) => write!(
719                f,
720                "<path: {} nodes, {} edges>",
721                p.nodes.len(),
722                p.edges.len()
723            ),
724            Value::Vector(v) => write!(f, "<vector: {} dims>", v.len()),
725            Value::Temporal(t) => write!(f, "{t}"),
726        }
727    }
728}
729
730// ---------------------------------------------------------------------------
731// Eq and Hash implementations
732// ---------------------------------------------------------------------------
733
734impl Eq for Value {}
735
736impl Hash for Value {
737    fn hash<H: Hasher>(&self, state: &mut H) {
738        // Discriminant first for type safety
739        std::mem::discriminant(self).hash(state);
740        match self {
741            Value::Null => {}
742            Value::Bool(b) => b.hash(state),
743            Value::Int(i) => i.hash(state),
744            Value::Float(f) => f.to_bits().hash(state),
745            Value::String(s) => s.hash(state),
746            Value::Bytes(b) => b.hash(state),
747            Value::List(l) => l.hash(state),
748            Value::Map(m) => hash_map(m, state),
749            Value::Node(n) => n.hash(state),
750            Value::Edge(e) => e.hash(state),
751            Value::Path(p) => p.hash(state),
752            Value::Vector(v) => {
753                v.len().hash(state);
754                for f in v {
755                    f.to_bits().hash(state);
756                }
757            }
758            Value::Temporal(t) => t.hash(state),
759        }
760    }
761}
762
763// ---------------------------------------------------------------------------
764// Graph entity types
765// ---------------------------------------------------------------------------
766
767/// Helper to hash a HashMap deterministically by sorting keys.
768fn hash_map<H: Hasher>(m: &HashMap<String, Value>, state: &mut H) {
769    let mut pairs: Vec<_> = m.iter().collect();
770    pairs.sort_by_key(|(k, _)| *k);
771    pairs.len().hash(state);
772    for (k, v) in pairs {
773        k.hash(state);
774        v.hash(state);
775    }
776}
777
778/// Graph node with identity, labels, and properties.
779#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
780pub struct Node {
781    /// Internal vertex identifier.
782    pub vid: Vid,
783    /// Node labels (multi-label support).
784    pub labels: Vec<String>,
785    /// Property key-value pairs.
786    pub properties: HashMap<String, Value>,
787}
788
789impl Hash for Node {
790    fn hash<H: Hasher>(&self, state: &mut H) {
791        self.vid.hash(state);
792        let mut sorted_labels = self.labels.clone();
793        sorted_labels.sort();
794        sorted_labels.hash(state);
795        hash_map(&self.properties, state);
796    }
797}
798
799impl Node {
800    /// Gets a typed property by name.
801    ///
802    /// # Errors
803    ///
804    /// Returns `UniError::Query` if the property is missing,
805    /// or `UniError::Type` if it cannot be converted.
806    pub fn get<T: FromValue>(&self, property: &str) -> crate::Result<T> {
807        let val = self
808            .properties
809            .get(property)
810            .ok_or_else(|| UniError::Query {
811                message: format!("Property '{}' not found on node {}", property, self.vid),
812                query: None,
813            })?;
814        T::from_value(val)
815    }
816
817    /// Tries to get a typed property, returning `None` on failure.
818    pub fn try_get<T: FromValue>(&self, property: &str) -> Option<T> {
819        self.properties
820            .get(property)
821            .and_then(|v| T::from_value(v).ok())
822    }
823}
824
825/// Graph edge with identity, type, endpoints, and properties.
826#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
827pub struct Edge {
828    /// Internal edge identifier.
829    pub eid: Eid,
830    /// Relationship type name.
831    pub edge_type: String,
832    /// Source vertex ID.
833    pub src: Vid,
834    /// Destination vertex ID.
835    pub dst: Vid,
836    /// Property key-value pairs.
837    pub properties: HashMap<String, Value>,
838}
839
840impl Hash for Edge {
841    fn hash<H: Hasher>(&self, state: &mut H) {
842        self.eid.hash(state);
843        self.edge_type.hash(state);
844        self.src.hash(state);
845        self.dst.hash(state);
846        hash_map(&self.properties, state);
847    }
848}
849
850impl Edge {
851    /// Gets a typed property by name.
852    ///
853    /// # Errors
854    ///
855    /// Returns `UniError::Query` if the property is missing,
856    /// or `UniError::Type` if it cannot be converted.
857    pub fn get<T: FromValue>(&self, property: &str) -> crate::Result<T> {
858        let val = self
859            .properties
860            .get(property)
861            .ok_or_else(|| UniError::Query {
862                message: format!("Property '{}' not found on edge {}", property, self.eid),
863                query: None,
864            })?;
865        T::from_value(val)
866    }
867}
868
869/// Graph path consisting of alternating nodes and edges.
870#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
871pub struct Path {
872    /// Ordered sequence of nodes along the path.
873    pub nodes: Vec<Node>,
874    /// Ordered sequence of edges connecting the nodes.
875    #[serde(rename = "relationships")]
876    pub edges: Vec<Edge>,
877}
878
879impl Path {
880    /// Returns the nodes in this path.
881    pub fn nodes(&self) -> &[Node] {
882        &self.nodes
883    }
884
885    /// Returns the edges in this path.
886    pub fn edges(&self) -> &[Edge] {
887        &self.edges
888    }
889
890    /// Returns the number of edges (path length).
891    pub fn len(&self) -> usize {
892        self.edges.len()
893    }
894
895    /// Returns `true` if the path has no edges.
896    pub fn is_empty(&self) -> bool {
897        self.edges.is_empty()
898    }
899
900    /// Returns the starting node, or `None` if the path is empty.
901    pub fn start(&self) -> Option<&Node> {
902        self.nodes.first()
903    }
904
905    /// Returns the ending node, or `None` if the path is empty.
906    pub fn end(&self) -> Option<&Node> {
907        self.nodes.last()
908    }
909}
910
911// ---------------------------------------------------------------------------
912// FromValue trait
913// ---------------------------------------------------------------------------
914
915/// Trait for fallible conversion from [`Value`].
916pub trait FromValue: Sized {
917    /// Converts a `Value` reference to `Self`.
918    ///
919    /// # Errors
920    ///
921    /// Returns `UniError::Type` if the value cannot be converted.
922    fn from_value(value: &Value) -> crate::Result<Self>;
923}
924
925/// Blanket implementation: any `T: TryFrom<&Value, Error = UniError>` is `FromValue`.
926impl<T> FromValue for T
927where
928    T: for<'a> TryFrom<&'a Value, Error = UniError>,
929{
930    fn from_value(value: &Value) -> crate::Result<Self> {
931        Self::try_from(value)
932    }
933}
934
935// ---------------------------------------------------------------------------
936// TryFrom<Value> macro for owned values (delegates to &Value)
937// ---------------------------------------------------------------------------
938
939macro_rules! impl_try_from_value_owned {
940    ($($t:ty),+ $(,)?) => {
941        $(
942            impl TryFrom<Value> for $t {
943                type Error = UniError;
944                fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
945                    Self::try_from(&value)
946                }
947            }
948        )+
949    };
950}
951
952impl_try_from_value_owned!(
953    String,
954    i64,
955    i32,
956    f64,
957    bool,
958    Vid,
959    Eid,
960    Vec<f32>,
961    Path,
962    Node,
963    Edge
964);
965
966// ---------------------------------------------------------------------------
967// TryFrom<&Value> implementations for standard types
968// ---------------------------------------------------------------------------
969
970/// Create a type mismatch error.
971fn type_error(expected: &str, value: &Value) -> UniError {
972    UniError::Type {
973        expected: expected.to_string(),
974        actual: format!("{:?}", value),
975    }
976}
977
978impl TryFrom<&Value> for String {
979    type Error = UniError;
980
981    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
982        match value {
983            Value::String(s) => Ok(s.clone()),
984            Value::Int(i) => Ok(i.to_string()),
985            Value::Float(f) => Ok(f.to_string()),
986            Value::Bool(b) => Ok(b.to_string()),
987            Value::Temporal(t) => Ok(t.to_string()),
988            _ => Err(type_error("String", value)),
989        }
990    }
991}
992
993impl TryFrom<&Value> for i64 {
994    type Error = UniError;
995
996    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
997        match value {
998            Value::Int(i) => Ok(*i),
999            Value::Float(f) => Ok(*f as i64),
1000            _ => Err(type_error("Int", value)),
1001        }
1002    }
1003}
1004
1005impl TryFrom<&Value> for i32 {
1006    type Error = UniError;
1007
1008    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1009        match value {
1010            Value::Int(i) => i32::try_from(*i).map_err(|_| UniError::Type {
1011                expected: "i32".to_string(),
1012                actual: format!("Integer {} out of range", i),
1013            }),
1014            Value::Float(f) => {
1015                if *f < i32::MIN as f64 || *f > i32::MAX as f64 {
1016                    return Err(UniError::Type {
1017                        expected: "i32".to_string(),
1018                        actual: format!("Float {} out of range", f),
1019                    });
1020                }
1021                if f.fract() != 0.0 {
1022                    return Err(UniError::Type {
1023                        expected: "i32".to_string(),
1024                        actual: format!("Float {} has fractional part", f),
1025                    });
1026                }
1027                Ok(*f as i32)
1028            }
1029            _ => Err(type_error("Int", value)),
1030        }
1031    }
1032}
1033
1034impl TryFrom<&Value> for f64 {
1035    type Error = UniError;
1036
1037    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1038        match value {
1039            Value::Float(f) => Ok(*f),
1040            Value::Int(i) => Ok(*i as f64),
1041            _ => Err(type_error("Float", value)),
1042        }
1043    }
1044}
1045
1046impl TryFrom<&Value> for bool {
1047    type Error = UniError;
1048
1049    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1050        match value {
1051            Value::Bool(b) => Ok(*b),
1052            _ => Err(type_error("Bool", value)),
1053        }
1054    }
1055}
1056
1057impl TryFrom<&Value> for Vid {
1058    type Error = UniError;
1059
1060    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1061        match value {
1062            Value::Node(n) => Ok(n.vid),
1063            Value::String(s) => {
1064                if let Ok(id) = s.parse::<u64>() {
1065                    return Ok(Vid::new(id));
1066                }
1067                Err(UniError::Type {
1068                    expected: "Vid".into(),
1069                    actual: s.clone(),
1070                })
1071            }
1072            Value::Int(i) => Ok(Vid::new(*i as u64)),
1073            _ => Err(type_error("Vid", value)),
1074        }
1075    }
1076}
1077
1078impl TryFrom<&Value> for Eid {
1079    type Error = UniError;
1080
1081    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1082        match value {
1083            Value::Edge(e) => Ok(e.eid),
1084            Value::String(s) => {
1085                if let Ok(id) = s.parse::<u64>() {
1086                    return Ok(Eid::new(id));
1087                }
1088                Err(UniError::Type {
1089                    expected: "Eid".into(),
1090                    actual: s.clone(),
1091                })
1092            }
1093            Value::Int(i) => Ok(Eid::new(*i as u64)),
1094            _ => Err(type_error("Eid", value)),
1095        }
1096    }
1097}
1098
1099impl TryFrom<&Value> for Vec<f32> {
1100    type Error = UniError;
1101
1102    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1103        match value {
1104            Value::Vector(v) => Ok(v.clone()),
1105            Value::List(l) => {
1106                let mut vec = Vec::with_capacity(l.len());
1107                for item in l {
1108                    match item {
1109                        Value::Float(f) => vec.push(*f as f32),
1110                        Value::Int(i) => vec.push(*i as f32),
1111                        _ => return Err(type_error("Float", item)),
1112                    }
1113                }
1114                Ok(vec)
1115            }
1116            _ => Err(type_error("Vector", value)),
1117        }
1118    }
1119}
1120
1121impl<T> TryFrom<&Value> for Option<T>
1122where
1123    T: for<'a> TryFrom<&'a Value, Error = UniError>,
1124{
1125    type Error = UniError;
1126
1127    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1128        match value {
1129            Value::Null => Ok(None),
1130            _ => T::try_from(value).map(Some),
1131        }
1132    }
1133}
1134
1135impl<T> TryFrom<Value> for Option<T>
1136where
1137    T: TryFrom<Value, Error = UniError>,
1138{
1139    type Error = UniError;
1140    fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
1141        match value {
1142            Value::Null => Ok(None),
1143            _ => T::try_from(value).map(Some),
1144        }
1145    }
1146}
1147
1148impl<T> TryFrom<&Value> for Vec<T>
1149where
1150    T: for<'a> TryFrom<&'a Value, Error = UniError>,
1151{
1152    type Error = UniError;
1153
1154    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1155        match value {
1156            Value::List(l) => {
1157                let mut vec = Vec::with_capacity(l.len());
1158                for item in l {
1159                    vec.push(T::try_from(item)?);
1160                }
1161                Ok(vec)
1162            }
1163            _ => Err(type_error("List", value)),
1164        }
1165    }
1166}
1167
1168impl<T> TryFrom<Value> for Vec<T>
1169where
1170    T: TryFrom<Value, Error = UniError>,
1171{
1172    type Error = UniError;
1173    fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
1174        match value {
1175            Value::List(l) => {
1176                let mut vec = Vec::with_capacity(l.len());
1177                for item in l {
1178                    vec.push(T::try_from(item)?);
1179                }
1180                Ok(vec)
1181            }
1182            other => Err(type_error("List", &other)),
1183        }
1184    }
1185}
1186
1187// ---------------------------------------------------------------------------
1188// TryFrom<&Value> for graph entities (deserialization from Map)
1189// ---------------------------------------------------------------------------
1190
1191/// Gets a value from a map trying alternative keys in order.
1192fn get_with_fallback<'a>(map: &'a HashMap<String, Value>, keys: &[&str]) -> Option<&'a Value> {
1193    keys.iter().find_map(|k| map.get(*k))
1194}
1195
1196/// Extracts a properties map from a value, defaulting to empty.
1197fn extract_properties(value: &Value) -> HashMap<String, Value> {
1198    match value {
1199        Value::Map(m) => m.clone(),
1200        _ => HashMap::new(),
1201    }
1202}
1203
1204impl TryFrom<&Value> for Node {
1205    type Error = UniError;
1206
1207    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1208        match value {
1209            Value::Node(n) => Ok(n.clone()),
1210            Value::Map(m) => {
1211                let vid_val = get_with_fallback(m, &["_vid", "_id", "vid"]);
1212                let props_val = m.get("properties");
1213
1214                let (Some(v), Some(p)) = (vid_val, props_val) else {
1215                    return Err(type_error("Node Map", value));
1216                };
1217
1218                // Extract labels from _labels key (List<String>)
1219                let labels = if let Some(Value::List(label_list)) = m.get("_labels") {
1220                    label_list
1221                        .iter()
1222                        .filter_map(|v| {
1223                            if let Value::String(s) = v {
1224                                Some(s.clone())
1225                            } else {
1226                                None
1227                            }
1228                        })
1229                        .collect()
1230                } else {
1231                    Vec::new()
1232                };
1233
1234                Ok(Node {
1235                    vid: Vid::try_from(v)?,
1236                    labels,
1237                    properties: extract_properties(p),
1238                })
1239            }
1240            _ => Err(type_error("Node", value)),
1241        }
1242    }
1243}
1244
1245impl TryFrom<&Value> for Edge {
1246    type Error = UniError;
1247
1248    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1249        match value {
1250            Value::Edge(e) => Ok(e.clone()),
1251            Value::Map(m) => {
1252                let eid_val = get_with_fallback(m, &["_eid", "_id", "eid"]);
1253                let type_val = get_with_fallback(m, &["_type_name", "_type", "edge_type"]);
1254                let src_val = get_with_fallback(m, &["_src", "src"]);
1255                let dst_val = get_with_fallback(m, &["_dst", "dst"]);
1256                let props_val = m.get("properties");
1257
1258                let (Some(id), Some(t), Some(s), Some(d), Some(p)) =
1259                    (eid_val, type_val, src_val, dst_val, props_val)
1260                else {
1261                    return Err(type_error("Edge Map", value));
1262                };
1263
1264                Ok(Edge {
1265                    eid: Eid::try_from(id)?,
1266                    edge_type: String::try_from(t)?,
1267                    src: Vid::try_from(s)?,
1268                    dst: Vid::try_from(d)?,
1269                    properties: extract_properties(p),
1270                })
1271            }
1272            _ => Err(type_error("Edge", value)),
1273        }
1274    }
1275}
1276
1277impl TryFrom<&Value> for Path {
1278    type Error = UniError;
1279
1280    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1281        match value {
1282            Value::Path(p) => Ok(p.clone()),
1283            Value::Map(m) => {
1284                let (Some(Value::List(nodes_list)), Some(Value::List(rels_list))) =
1285                    (m.get("nodes"), m.get("relationships"))
1286                else {
1287                    return Err(type_error("Path (Map with nodes/relationships)", value));
1288                };
1289
1290                let nodes = nodes_list
1291                    .iter()
1292                    .map(Node::try_from)
1293                    .collect::<std::result::Result<Vec<_>, _>>()?;
1294
1295                let edges = rels_list
1296                    .iter()
1297                    .map(Edge::try_from)
1298                    .collect::<std::result::Result<Vec<_>, _>>()?;
1299
1300                Ok(Path { nodes, edges })
1301            }
1302            _ => Err(type_error("Path", value)),
1303        }
1304    }
1305}
1306
1307// ---------------------------------------------------------------------------
1308// From<T> for Value (primitive constructors)
1309// ---------------------------------------------------------------------------
1310
1311impl From<String> for Value {
1312    fn from(v: String) -> Self {
1313        Value::String(v)
1314    }
1315}
1316
1317impl From<&str> for Value {
1318    fn from(v: &str) -> Self {
1319        Value::String(v.to_string())
1320    }
1321}
1322
1323impl From<i64> for Value {
1324    fn from(v: i64) -> Self {
1325        Value::Int(v)
1326    }
1327}
1328
1329impl From<i32> for Value {
1330    fn from(v: i32) -> Self {
1331        Value::Int(v as i64)
1332    }
1333}
1334
1335impl From<f64> for Value {
1336    fn from(v: f64) -> Self {
1337        Value::Float(v)
1338    }
1339}
1340
1341impl From<bool> for Value {
1342    fn from(v: bool) -> Self {
1343        Value::Bool(v)
1344    }
1345}
1346
1347impl From<Vec<f32>> for Value {
1348    fn from(v: Vec<f32>) -> Self {
1349        Value::Vector(v)
1350    }
1351}
1352
1353// ---------------------------------------------------------------------------
1354// serde_json::Value ↔ Value conversions (JSONB boundary)
1355// ---------------------------------------------------------------------------
1356
1357impl From<serde_json::Value> for Value {
1358    fn from(v: serde_json::Value) -> Self {
1359        match v {
1360            serde_json::Value::Null => Value::Null,
1361            serde_json::Value::Bool(b) => Value::Bool(b),
1362            serde_json::Value::Number(n) => {
1363                if let Some(i) = n.as_i64() {
1364                    Value::Int(i)
1365                } else if let Some(f) = n.as_f64() {
1366                    Value::Float(f)
1367                } else {
1368                    Value::Null
1369                }
1370            }
1371            serde_json::Value::String(s) => Value::String(s),
1372            serde_json::Value::Array(arr) => {
1373                Value::List(arr.into_iter().map(Value::from).collect())
1374            }
1375            serde_json::Value::Object(obj) => {
1376                Value::Map(obj.into_iter().map(|(k, v)| (k, Value::from(v))).collect())
1377            }
1378        }
1379    }
1380}
1381
1382impl From<Value> for serde_json::Value {
1383    fn from(v: Value) -> Self {
1384        match v {
1385            Value::Null => serde_json::Value::Null,
1386            Value::Bool(b) => serde_json::Value::Bool(b),
1387            Value::Int(i) => serde_json::Value::Number(serde_json::Number::from(i)),
1388            Value::Float(f) => serde_json::Number::from_f64(f)
1389                .map(serde_json::Value::Number)
1390                .unwrap_or(serde_json::Value::Null), // NaN/Inf → null
1391            Value::String(s) => serde_json::Value::String(s),
1392            Value::Bytes(b) => {
1393                use base64::Engine;
1394                serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(b))
1395            }
1396            Value::List(l) => {
1397                serde_json::Value::Array(l.into_iter().map(serde_json::Value::from).collect())
1398            }
1399            Value::Map(m) => {
1400                let mut map = serde_json::Map::new();
1401                for (k, v) in m {
1402                    map.insert(k, v.into());
1403                }
1404                serde_json::Value::Object(map)
1405            }
1406            Value::Node(n) => {
1407                let mut map = serde_json::Map::new();
1408                map.insert(
1409                    "_id".to_string(),
1410                    serde_json::Value::String(n.vid.to_string()),
1411                );
1412                map.insert(
1413                    "_labels".to_string(),
1414                    serde_json::Value::Array(
1415                        n.labels
1416                            .into_iter()
1417                            .map(serde_json::Value::String)
1418                            .collect(),
1419                    ),
1420                );
1421                let props: serde_json::Value = Value::Map(n.properties).into();
1422                map.insert("properties".to_string(), props);
1423                serde_json::Value::Object(map)
1424            }
1425            Value::Edge(e) => {
1426                let mut map = serde_json::Map::new();
1427                map.insert(
1428                    "_id".to_string(),
1429                    serde_json::Value::String(e.eid.to_string()),
1430                );
1431                map.insert("_type".to_string(), serde_json::Value::String(e.edge_type));
1432                map.insert(
1433                    "_src".to_string(),
1434                    serde_json::Value::String(e.src.to_string()),
1435                );
1436                map.insert(
1437                    "_dst".to_string(),
1438                    serde_json::Value::String(e.dst.to_string()),
1439                );
1440                let props: serde_json::Value = Value::Map(e.properties).into();
1441                map.insert("properties".to_string(), props);
1442                serde_json::Value::Object(map)
1443            }
1444            Value::Path(p) => {
1445                let mut map = serde_json::Map::new();
1446                map.insert(
1447                    "nodes".to_string(),
1448                    Value::List(p.nodes.into_iter().map(Value::Node).collect()).into(),
1449                );
1450                map.insert(
1451                    "relationships".to_string(),
1452                    Value::List(p.edges.into_iter().map(Value::Edge).collect()).into(),
1453                );
1454                serde_json::Value::Object(map)
1455            }
1456            Value::Vector(v) => serde_json::Value::Array(
1457                v.into_iter()
1458                    .map(|f| {
1459                        serde_json::Number::from_f64(f as f64)
1460                            .map(serde_json::Value::Number)
1461                            .unwrap_or(serde_json::Value::Null)
1462                    })
1463                    .collect(),
1464            ),
1465            Value::Temporal(t) => serde_json::Value::String(t.to_string()),
1466        }
1467    }
1468}
1469
1470// ---------------------------------------------------------------------------
1471// unival! macro
1472// ---------------------------------------------------------------------------
1473
1474/// Constructs a [`Value`] from a literal or expression, similar to `serde_json::json!`.
1475///
1476/// # Examples
1477///
1478/// ```
1479/// use uni_common::unival;
1480/// use uni_common::Value;
1481///
1482/// let null = unival!(null);
1483/// let b = unival!(true);
1484/// let i = unival!(42);
1485/// let f = unival!(3.14);
1486/// let s = unival!("hello");
1487/// let list = unival!([1, 2, "three"]);
1488/// let map = unival!({"key": "val", "num": 42});
1489/// let expr_val = { let x: i64 = 10; unival!(x) };
1490/// ```
1491#[macro_export]
1492macro_rules! unival {
1493    // Null
1494    (null) => {
1495        $crate::Value::Null
1496    };
1497
1498    // Booleans
1499    (true) => {
1500        $crate::Value::Bool(true)
1501    };
1502    (false) => {
1503        $crate::Value::Bool(false)
1504    };
1505
1506    // Array
1507    ([ $($elem:tt),* $(,)? ]) => {
1508        $crate::Value::List(vec![ $( $crate::unival!($elem) ),* ])
1509    };
1510
1511    // Map
1512    ({ $($key:tt : $val:tt),* $(,)? }) => {
1513        $crate::Value::Map({
1514            #[allow(unused_mut)]
1515            let mut map = ::std::collections::HashMap::new();
1516            $( map.insert(($key).to_string(), $crate::unival!($val)); )*
1517            map
1518        })
1519    };
1520
1521    // Fallback: any expression — uses From<T> for Value
1522    ($e:expr) => {
1523        $crate::Value::from($e)
1524    };
1525}
1526
1527// ---------------------------------------------------------------------------
1528// Additional From impls for unival! convenience
1529// ---------------------------------------------------------------------------
1530
1531impl From<usize> for Value {
1532    fn from(v: usize) -> Self {
1533        Value::Int(v as i64)
1534    }
1535}
1536
1537impl From<u64> for Value {
1538    fn from(v: u64) -> Self {
1539        Value::Int(v as i64)
1540    }
1541}
1542
1543impl From<f32> for Value {
1544    fn from(v: f32) -> Self {
1545        Value::Float(v as f64)
1546    }
1547}
1548
1549// ---------------------------------------------------------------------------
1550// Tests
1551// ---------------------------------------------------------------------------
1552
1553#[cfg(test)]
1554mod tests {
1555    use super::*;
1556
1557    #[test]
1558    fn test_accessor_methods() {
1559        assert!(Value::Null.is_null());
1560        assert!(!Value::Int(1).is_null());
1561
1562        assert_eq!(Value::Bool(true).as_bool(), Some(true));
1563        assert_eq!(Value::Int(42).as_bool(), None);
1564
1565        assert_eq!(Value::Int(42).as_i64(), Some(42));
1566        assert_eq!(Value::Float(2.5).as_i64(), None);
1567
1568        // as_f64 coerces Int to Float
1569        assert_eq!(Value::Float(2.5).as_f64(), Some(2.5));
1570        assert_eq!(Value::Int(42).as_f64(), Some(42.0));
1571        assert_eq!(Value::String("x".into()).as_f64(), None);
1572
1573        assert_eq!(Value::String("hello".into()).as_str(), Some("hello"));
1574        assert_eq!(Value::Int(1).as_str(), None);
1575
1576        assert!(Value::Int(1).is_i64());
1577        assert!(!Value::Float(1.0).is_i64());
1578
1579        assert!(Value::Float(1.0).is_f64());
1580        assert!(!Value::Int(1).is_f64());
1581
1582        assert!(Value::Int(1).is_number());
1583        assert!(Value::Float(1.0).is_number());
1584        assert!(!Value::String("x".into()).is_number());
1585    }
1586
1587    #[test]
1588    fn test_serde_json_roundtrip() {
1589        let val = Value::Int(42);
1590        let json: serde_json::Value = val.clone().into();
1591        let back: Value = json.into();
1592        assert_eq!(val, back);
1593
1594        let val = Value::Float(2.5);
1595        let json: serde_json::Value = val.clone().into();
1596        let back: Value = json.into();
1597        assert_eq!(val, back);
1598
1599        let val = Value::String("hello".into());
1600        let json: serde_json::Value = val.clone().into();
1601        let back: Value = json.into();
1602        assert_eq!(val, back);
1603
1604        let val = Value::List(vec![Value::Int(1), Value::Int(2)]);
1605        let json: serde_json::Value = val.clone().into();
1606        let back: Value = json.into();
1607        assert_eq!(val, back);
1608    }
1609
1610    #[test]
1611    fn test_unival_macro() {
1612        assert_eq!(unival!(null), Value::Null);
1613        assert_eq!(unival!(true), Value::Bool(true));
1614        assert_eq!(unival!(false), Value::Bool(false));
1615        assert_eq!(unival!(42_i64), Value::Int(42));
1616        assert_eq!(unival!(2.5_f64), Value::Float(2.5));
1617        assert_eq!(unival!("hello"), Value::String("hello".into()));
1618
1619        // Array
1620        let list = unival!([1_i64, 2_i64]);
1621        assert_eq!(list, Value::List(vec![Value::Int(1), Value::Int(2)]));
1622
1623        // Map
1624        let map = unival!({"key": "val", "num": 42_i64});
1625        if let Value::Map(m) = &map {
1626            assert_eq!(m.get("key"), Some(&Value::String("val".into())));
1627            assert_eq!(m.get("num"), Some(&Value::Int(42)));
1628        } else {
1629            panic!("Expected Map");
1630        }
1631
1632        // Expression fallback
1633        let x: i64 = 99;
1634        assert_eq!(unival!(x), Value::Int(99));
1635    }
1636
1637    #[test]
1638    fn test_int_float_distinction_preserved() {
1639        // This is the key property: Int stays Int, Float stays Float
1640        let int_val = Value::Int(42);
1641        let float_val = Value::Float(42.0);
1642
1643        assert!(int_val.is_i64());
1644        assert!(!int_val.is_f64());
1645
1646        assert!(float_val.is_f64());
1647        assert!(!float_val.is_i64());
1648
1649        // They are NOT equal (different variants)
1650        assert_ne!(int_val, float_val);
1651    }
1652
1653    #[test]
1654    fn test_temporal_display_zero_seconds_omitted() {
1655        // LocalTime: 12:00 (zero seconds omitted)
1656        let lt = TemporalValue::LocalTime {
1657            nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1658        };
1659        assert_eq!(lt.to_string(), "12:00");
1660
1661        // LocalTime: 12:31:14 (non-zero seconds kept)
1662        let lt2 = TemporalValue::LocalTime {
1663            nanos_since_midnight: (12 * 3600 + 31 * 60 + 14) * 1_000_000_000,
1664        };
1665        assert_eq!(lt2.to_string(), "12:31:14");
1666
1667        // LocalTime: 00:00:00.5 (zero seconds but non-zero nanos — keep seconds)
1668        let lt3 = TemporalValue::LocalTime {
1669            nanos_since_midnight: 500_000_000,
1670        };
1671        assert_eq!(lt3.to_string(), "00:00:00.5");
1672
1673        // Time: 12:00Z (zero offset uses Z)
1674        let t = TemporalValue::Time {
1675            nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1676            offset_seconds: 0,
1677        };
1678        assert_eq!(t.to_string(), "12:00Z");
1679
1680        // Time: 12:31:14+01:00 (non-zero offset)
1681        let t2 = TemporalValue::Time {
1682            nanos_since_midnight: (12 * 3600 + 31 * 60 + 14) * 1_000_000_000,
1683            offset_seconds: 3600,
1684        };
1685        assert_eq!(t2.to_string(), "12:31:14+01:00");
1686
1687        // LocalDateTime: 1984-10-11T12:31 (zero seconds omitted)
1688        let epoch_nanos = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1689            .unwrap()
1690            .and_hms_opt(12, 31, 0)
1691            .unwrap()
1692            .and_utc()
1693            .timestamp_nanos_opt()
1694            .unwrap();
1695        let ldt = TemporalValue::LocalDateTime {
1696            nanos_since_epoch: epoch_nanos,
1697        };
1698        assert_eq!(ldt.to_string(), "1984-10-11T12:31");
1699
1700        // DateTime: 1984-10-11T12:31+01:00 (zero seconds, with offset)
1701        let utc_nanos = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1702            .unwrap()
1703            .and_hms_opt(11, 31, 0)
1704            .unwrap()
1705            .and_utc()
1706            .timestamp_nanos_opt()
1707            .unwrap();
1708        let dt = TemporalValue::DateTime {
1709            nanos_since_epoch: utc_nanos,
1710            offset_seconds: 3600,
1711            timezone_name: None,
1712        };
1713        assert_eq!(dt.to_string(), "1984-10-11T12:31+01:00");
1714
1715        // DateTime: 2015-07-21T21:40:32.142+01:00 (non-zero seconds with fractional)
1716        let utc_nanos2 = chrono::NaiveDate::from_ymd_opt(2015, 7, 21)
1717            .unwrap()
1718            .and_hms_nano_opt(20, 40, 32, 142_000_000)
1719            .unwrap()
1720            .and_utc()
1721            .timestamp_nanos_opt()
1722            .unwrap();
1723        let dt2 = TemporalValue::DateTime {
1724            nanos_since_epoch: utc_nanos2,
1725            offset_seconds: 3600,
1726            timezone_name: None,
1727        };
1728        assert_eq!(dt2.to_string(), "2015-07-21T21:40:32.142+01:00");
1729
1730        // DateTime: 1984-10-11T12:31Z (zero offset uses Z)
1731        let utc_nanos3 = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1732            .unwrap()
1733            .and_hms_opt(12, 31, 0)
1734            .unwrap()
1735            .and_utc()
1736            .timestamp_nanos_opt()
1737            .unwrap();
1738        let dt3 = TemporalValue::DateTime {
1739            nanos_since_epoch: utc_nanos3,
1740            offset_seconds: 0,
1741            timezone_name: None,
1742        };
1743        assert_eq!(dt3.to_string(), "1984-10-11T12:31Z");
1744    }
1745
1746    #[test]
1747    fn test_temporal_display_fractional_trailing_zeros_stripped() {
1748        // Full stripping: .9 not .900
1749        let d = TemporalValue::Duration {
1750            months: 0,
1751            days: 0,
1752            nanos: 900_000_000,
1753        };
1754        assert_eq!(d.to_string(), "PT0.9S");
1755
1756        // Full stripping: .4 not .400
1757        let d2 = TemporalValue::Duration {
1758            months: 0,
1759            days: 0,
1760            nanos: 400_000_000,
1761        };
1762        assert_eq!(d2.to_string(), "PT0.4S");
1763
1764        // Millisecond precision preserved: .142
1765        let d3 = TemporalValue::Duration {
1766            months: 0,
1767            days: 0,
1768            nanos: 142_000_000,
1769        };
1770        assert_eq!(d3.to_string(), "PT0.142S");
1771
1772        // Nanosecond precision: .000000001
1773        let d4 = TemporalValue::Duration {
1774            months: 0,
1775            days: 0,
1776            nanos: 1,
1777        };
1778        assert_eq!(d4.to_string(), "PT0.000000001S");
1779    }
1780
1781    #[test]
1782    fn test_temporal_display_offset_second_precision() {
1783        // Offset with seconds: +02:05:59
1784        let t = TemporalValue::Time {
1785            nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1786            offset_seconds: 2 * 3600 + 5 * 60 + 59,
1787        };
1788        assert_eq!(t.to_string(), "12:00+02:05:59");
1789
1790        // Negative offset with seconds: -02:05:07
1791        let t2 = TemporalValue::Time {
1792            nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1793            offset_seconds: -(2 * 3600 + 5 * 60 + 7),
1794        };
1795        assert_eq!(t2.to_string(), "12:00-02:05:07");
1796    }
1797
1798    #[test]
1799    fn test_temporal_display_datetime_with_timezone_name() {
1800        let utc_nanos = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1801            .unwrap()
1802            .and_hms_opt(11, 31, 0)
1803            .unwrap()
1804            .and_utc()
1805            .timestamp_nanos_opt()
1806            .unwrap();
1807        let dt = TemporalValue::DateTime {
1808            nanos_since_epoch: utc_nanos,
1809            offset_seconds: 3600,
1810            timezone_name: Some("Europe/Stockholm".to_string()),
1811        };
1812        assert_eq!(dt.to_string(), "1984-10-11T12:31+01:00[Europe/Stockholm]");
1813    }
1814}