meshdb_core/property.rs
1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// A Cypher duration — months, days, seconds, nanoseconds. Matches
5/// the Bolt 4.4 Duration struct (tag `0x45`) field-for-field so no
6/// conversion is needed at the wire boundary. Stored as four
7/// separate components because the calendar units (`months`, `days`)
8/// can't be reduced to seconds without knowing a reference date,
9/// while the exact units (`seconds`, `nanos`) can.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct Duration {
12 pub months: i64,
13 pub days: i64,
14 pub seconds: i64,
15 pub nanos: i32,
16}
17
18/// A Cypher spatial point. Stored as raw coordinates plus an EPSG
19/// SRID tag — the SRID determines the coordinate reference system
20/// (CRS) and whether `z` is present:
21///
22/// | SRID | CRS name | Dims | Axes |
23/// |------|-----------------|------|-----------------------------|
24/// | 7203 | `cartesian` | 2 | x, y |
25/// | 9157 | `cartesian-3d` | 3 | x, y, z |
26/// | 4326 | `wgs-84` | 2 | longitude (x), latitude (y) |
27/// | 4979 | `wgs-84-3d` | 3 | longitude, latitude, height |
28///
29/// The 2D/3D distinction is carried by `z.is_some()` — the SRID is
30/// required to agree with that (a 2D SRID with `Some(z)`, or a 3D
31/// SRID with `None`, is an invariant violation caller-side).
32///
33/// Matches the Bolt 4.4 Point2D (tag `0x58`) / Point3D (tag `0x59`)
34/// struct shape so the wire conversion is a straight field copy.
35#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
36pub struct Point {
37 pub srid: i32,
38 pub x: f64,
39 pub y: f64,
40 pub z: Option<f64>,
41}
42
43/// EPSG SRID for the Cartesian 2D coordinate system.
44pub const SRID_CARTESIAN_2D: i32 = 7203;
45/// EPSG SRID for the Cartesian 3D coordinate system.
46pub const SRID_CARTESIAN_3D: i32 = 9157;
47/// EPSG SRID for WGS-84 (geographic 2D).
48pub const SRID_WGS84_2D: i32 = 4326;
49/// EPSG SRID for WGS-84 3D (geographic + height).
50pub const SRID_WGS84_3D: i32 = 4979;
51
52impl Point {
53 /// The CRS name for this point's SRID, matching Neo4j's output of
54 /// `point.crs` — e.g. `"cartesian"`, `"wgs-84-3d"`. Returns
55 /// `"unknown"` for any SRID outside the four recognised values.
56 pub fn crs_name(&self) -> &'static str {
57 match self.srid {
58 SRID_CARTESIAN_2D => "cartesian",
59 SRID_CARTESIAN_3D => "cartesian-3d",
60 SRID_WGS84_2D => "wgs-84",
61 SRID_WGS84_3D => "wgs-84-3d",
62 _ => "unknown",
63 }
64 }
65
66 /// True when the SRID is a WGS-84 (geographic) CRS.
67 pub fn is_geographic(&self) -> bool {
68 matches!(self.srid, SRID_WGS84_2D | SRID_WGS84_3D)
69 }
70
71 /// True when the point has a z coordinate.
72 pub fn is_3d(&self) -> bool {
73 self.z.is_some()
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78#[serde(tag = "type", content = "value")]
79pub enum Property {
80 Null,
81 String(String),
82 Int64(i64),
83 Float64(f64),
84 Bool(bool),
85 List(Vec<Property>),
86 Map(HashMap<String, Property>),
87 /// UTC epoch **nanoseconds** with an optional timezone offset
88 /// and optional IANA region name. The nanos value is always UTC;
89 /// `tz_offset_secs` is the offset at this instant (used for the
90 /// `+HH:MM` / `Z` suffix), and `tz_name` — when set — is the
91 /// zone identifier (e.g. `"Europe/Stockholm"`) that produced
92 /// that offset, rendered as a `[Region/City]` suffix.
93 DateTime {
94 nanos: i128,
95 tz_offset_secs: Option<i32>,
96 tz_name: Option<String>,
97 },
98 /// Local (naive) datetime as epoch nanoseconds.
99 /// Formatters omit any timezone suffix.
100 LocalDateTime(i128),
101 /// Days since the UNIX epoch (1970-01-01, UTC). `i64` gives
102 /// roughly ±25 billion years — wide enough to round-trip the
103 /// ±999999999 year range the TCK exercises at its extremes.
104 /// Maps to Bolt `Date` (struct tag `0x44`), which is also i64.
105 Date(i64),
106 /// A Cypher duration value — see [`Duration`].
107 Duration(Duration),
108 /// Time of day as nanoseconds since midnight, with an optional
109 /// timezone offset in seconds. Distinct from `DateTime` so the
110 /// formatter can produce `'12:31:14.645876123'` instead of a
111 /// full date-time string.
112 Time {
113 nanos: i64,
114 tz_offset_secs: Option<i32>,
115 },
116 /// A spatial point — see [`Point`]. Encoded on the Bolt wire as
117 /// either a `Point2D` (tag `0x58`) or `Point3D` (tag `0x59`)
118 /// struct depending on whether `z` is set.
119 Point(Point),
120}
121
122impl Property {
123 pub fn type_name(&self) -> &'static str {
124 match self {
125 Property::Null => "Null",
126 Property::String(_) => "String",
127 Property::Int64(_) => "Int64",
128 Property::Float64(_) => "Float64",
129 Property::Bool(_) => "Bool",
130 Property::List(_) => "List",
131 Property::Map(_) => "Map",
132 Property::DateTime { .. } => "DateTime",
133 Property::LocalDateTime(_) => "LocalDateTime",
134 Property::Date(_) => "Date",
135 Property::Duration(_) => "Duration",
136 Property::Time { .. } => "Time",
137 Property::Point(_) => "Point",
138 }
139 }
140}
141
142impl From<String> for Property {
143 fn from(v: String) -> Self {
144 Property::String(v)
145 }
146}
147
148impl From<&str> for Property {
149 fn from(v: &str) -> Self {
150 Property::String(v.to_string())
151 }
152}
153
154impl From<i64> for Property {
155 fn from(v: i64) -> Self {
156 Property::Int64(v)
157 }
158}
159
160impl From<i32> for Property {
161 fn from(v: i32) -> Self {
162 Property::Int64(v as i64)
163 }
164}
165
166impl From<f64> for Property {
167 fn from(v: f64) -> Self {
168 Property::Float64(v)
169 }
170}
171
172impl From<bool> for Property {
173 fn from(v: bool) -> Self {
174 Property::Bool(v)
175 }
176}
177
178impl<T: Into<Property>> From<Vec<T>> for Property {
179 fn from(v: Vec<T>) -> Self {
180 Property::List(v.into_iter().map(Into::into).collect())
181 }
182}