1use 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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub enum TemporalValue {
44 Date { days_since_epoch: i32 },
46 LocalTime { nanos_since_midnight: i64 },
48 Time {
50 nanos_since_midnight: i64,
51 offset_seconds: i32,
52 },
53 LocalDateTime { nanos_since_epoch: i64 },
55 DateTime {
58 nanos_since_epoch: i64,
59 offset_seconds: i32,
60 timezone_name: Option<String>,
61 },
62 Duration { months: i64, days: i64, nanos: i64 },
65 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 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 pub fn year(&self) -> Option<i64> {
135 self.to_date().map(|d| d.year() as i64)
136 }
137
138 pub fn month(&self) -> Option<i64> {
140 self.to_date().map(|d| d.month() as i64)
141 }
142
143 pub fn day(&self) -> Option<i64> {
145 self.to_date().map(|d| d.day() as i64)
146 }
147
148 pub fn hour(&self) -> Option<i64> {
150 self.to_time().map(|t| t.hour() as i64)
151 }
152
153 pub fn minute(&self) -> Option<i64> {
155 self.to_time().map(|t| t.minute() as i64)
156 }
157
158 pub fn second(&self) -> Option<i64> {
160 self.to_time().map(|t| t.second() as i64)
161 }
162
163 pub fn millisecond(&self) -> Option<i64> {
165 self.to_time().map(|t| (t.nanosecond() / 1_000_000) as i64)
166 }
167
168 pub fn microsecond(&self) -> Option<i64> {
170 self.to_time().map(|t| (t.nanosecond() / 1_000) as i64)
171 }
172
173 pub fn nanosecond(&self) -> Option<i64> {
175 self.to_time().map(|t| t.nanosecond() as i64)
176 }
177
178 pub fn quarter(&self) -> Option<i64> {
180 self.to_date().map(|d| ((d.month() - 1) / 3 + 1) as i64)
181 }
182
183 pub fn week(&self) -> Option<i64> {
185 self.to_date().map(|d| d.iso_week().week() as i64)
186 }
187
188 pub fn week_year(&self) -> Option<i64> {
190 self.to_date().map(|d| d.iso_week().year() as i64)
191 }
192
193 pub fn ordinal_day(&self) -> Option<i64> {
195 self.to_date().map(|d| d.ordinal() as i64)
196 }
197
198 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 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 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 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 pub fn offset(&self) -> Option<String> {
236 self.raw_offset_seconds().map(format_offset)
237 }
238
239 pub fn offset_minutes(&self) -> Option<i64> {
241 self.raw_offset_seconds().map(|s| s as i64 / 60)
242 }
243
244 pub fn offset_seconds_value(&self) -> Option<i64> {
246 self.raw_offset_seconds().map(|s| s as i64)
247 }
248
249 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 pub fn epoch_seconds(&self) -> Option<i64> {
264 self.raw_epoch_nanos().map(|n| n / 1_000_000_000)
265 }
266
267 pub fn epoch_millis(&self) -> Option<i64> {
269 self.raw_epoch_nanos().map(|n| n / 1_000_000)
270 }
271
272 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 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 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
329fn 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
339fn 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
347fn 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
361fn 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
371fn 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
381fn format_naive_time(t: &chrono::NaiveTime) -> String {
383 format_time_component(t.hour(), t.minute(), t.second(), t.nanosecond())
384}
385
386fn 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 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 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 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
504use chrono::Datelike as _;
506use chrono::Timelike as _;
507
508#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516#[serde(untagged)]
517#[non_exhaustive]
518pub enum Value {
519 Null,
521 Bool(bool),
523 Int(i64),
525 Float(f64),
527 String(String),
529 Bytes(Vec<u8>),
531 List(Vec<Value>),
533 Map(HashMap<String, Value>),
535
536 Node(Node),
539 Edge(Edge),
541 Path(Path),
543
544 Vector(Vec<f32>),
547
548 Temporal(TemporalValue),
551}
552
553impl Value {
558 pub fn is_null(&self) -> bool {
560 matches!(self, Value::Null)
561 }
562
563 pub fn as_bool(&self) -> Option<bool> {
565 match self {
566 Value::Bool(b) => Some(*b),
567 _ => None,
568 }
569 }
570
571 pub fn as_i64(&self) -> Option<i64> {
573 match self {
574 Value::Int(i) => Some(*i),
575 _ => None,
576 }
577 }
578
579 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 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 pub fn as_str(&self) -> Option<&str> {
600 match self {
601 Value::String(s) => Some(s),
602 _ => None,
603 }
604 }
605
606 pub fn is_i64(&self) -> bool {
608 matches!(self, Value::Int(_))
609 }
610
611 pub fn is_f64(&self) -> bool {
613 matches!(self, Value::Float(_))
614 }
615
616 pub fn is_string(&self) -> bool {
618 matches!(self, Value::String(_))
619 }
620
621 pub fn is_number(&self) -> bool {
623 matches!(self, Value::Int(_) | Value::Float(_))
624 }
625
626 pub fn as_array(&self) -> Option<&Vec<Value>> {
628 match self {
629 Value::List(l) => Some(l),
630 _ => None,
631 }
632 }
633
634 pub fn as_object(&self) -> Option<&HashMap<String, Value>> {
636 match self {
637 Value::Map(m) => Some(m),
638 _ => None,
639 }
640 }
641
642 pub fn is_bool(&self) -> bool {
644 matches!(self, Value::Bool(_))
645 }
646
647 pub fn is_list(&self) -> bool {
649 matches!(self, Value::List(_))
650 }
651
652 pub fn is_map(&self) -> bool {
654 matches!(self, Value::Map(_))
655 }
656
657 pub fn get(&self, key: &str) -> Option<&Value> {
661 match self {
662 Value::Map(m) => m.get(key),
663 _ => None,
664 }
665 }
666
667 pub fn is_temporal(&self) -> bool {
669 matches!(self, Value::Temporal(_))
670 }
671
672 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
730impl Eq for Value {}
735
736impl Hash for Value {
737 fn hash<H: Hasher>(&self, state: &mut H) {
738 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
763fn 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
780pub struct Node {
781 pub vid: Vid,
783 pub labels: Vec<String>,
785 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
827pub struct Edge {
828 pub eid: Eid,
830 pub edge_type: String,
832 pub src: Vid,
834 pub dst: Vid,
836 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 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
871pub struct Path {
872 pub nodes: Vec<Node>,
874 #[serde(rename = "relationships")]
876 pub edges: Vec<Edge>,
877}
878
879impl Path {
880 pub fn nodes(&self) -> &[Node] {
882 &self.nodes
883 }
884
885 pub fn edges(&self) -> &[Edge] {
887 &self.edges
888 }
889
890 pub fn len(&self) -> usize {
892 self.edges.len()
893 }
894
895 pub fn is_empty(&self) -> bool {
897 self.edges.is_empty()
898 }
899
900 pub fn start(&self) -> Option<&Node> {
902 self.nodes.first()
903 }
904
905 pub fn end(&self) -> Option<&Node> {
907 self.nodes.last()
908 }
909}
910
911pub trait FromValue: Sized {
917 fn from_value(value: &Value) -> crate::Result<Self>;
923}
924
925impl<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
935macro_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
966fn 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
1187fn 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
1196fn 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 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
1307impl 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
1353impl 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), 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#[macro_export]
1492macro_rules! unival {
1493 (null) => {
1495 $crate::Value::Null
1496 };
1497
1498 (true) => {
1500 $crate::Value::Bool(true)
1501 };
1502 (false) => {
1503 $crate::Value::Bool(false)
1504 };
1505
1506 ([ $($elem:tt),* $(,)? ]) => {
1508 $crate::Value::List(vec![ $( $crate::unival!($elem) ),* ])
1509 };
1510
1511 ({ $($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 ($e:expr) => {
1523 $crate::Value::from($e)
1524 };
1525}
1526
1527impl 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#[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 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 let list = unival!([1_i64, 2_i64]);
1621 assert_eq!(list, Value::List(vec![Value::Int(1), Value::Int(2)]));
1622
1623 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 let x: i64 = 99;
1634 assert_eq!(unival!(x), Value::Int(99));
1635 }
1636
1637 #[test]
1638 fn test_int_float_distinction_preserved() {
1639 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 assert_ne!(int_val, float_val);
1651 }
1652
1653 #[test]
1654 fn test_temporal_display_zero_seconds_omitted() {
1655 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 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 let lt3 = TemporalValue::LocalTime {
1669 nanos_since_midnight: 500_000_000,
1670 };
1671 assert_eq!(lt3.to_string(), "00:00:00.5");
1672
1673 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 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 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 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 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 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 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 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 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 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 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 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}