Skip to main content

drasi_core/models/
element.rs

1// Copyright 2024 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    fmt::{Display, Formatter},
17    sync::Arc,
18};
19
20use serde::{Deserialize, Serialize};
21
22use crate::evaluation::variable_value::VariableValue;
23
24use super::{ElementPropertyMap, ElementValue};
25
26#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
27pub struct ElementReference {
28    pub source_id: Arc<str>,
29    pub element_id: Arc<str>,
30}
31
32impl ElementReference {
33    pub fn new(source_id: &str, element_id: &str) -> Self {
34        ElementReference {
35            source_id: Arc::from(source_id),
36            element_id: Arc::from(element_id),
37        }
38    }
39}
40
41impl Display for ElementReference {
42    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
43        write!(f, "{}:{}", self.source_id, self.element_id)
44    }
45}
46
47/// Timestamp type used for elements, measured in milliseconds since UNIX epoch.
48pub type ElementTimestamp = u64;
49
50/// Maximum plausible `effective_from` value in milliseconds.
51///
52/// Corresponds to approximately year 2286. Any value above this threshold
53/// is almost certainly in the wrong unit (e.g., nanoseconds or microseconds
54/// instead of milliseconds).
55///
56/// A nanosecond-scale timestamp from the current era is ~10^18, which exceeds
57/// this threshold by 5 orders of magnitude—making the check extremely reliable
58/// with zero false positives for any date before year 2286.
59pub const MAX_REASONABLE_MILLIS_TIMESTAMP: u64 = 10_000_000_000_000;
60
61/// Validates that an `ElementTimestamp` value is in the expected millisecond range.
62///
63/// Returns `Ok(())` if the value is zero (used by bootstrap providers for "unknown")
64/// or falls below [`MAX_REASONABLE_MILLIS_TIMESTAMP`].
65///
66/// Returns `Err` with a descriptive message if the value appears to be in the wrong
67/// unit (e.g., nanoseconds instead of milliseconds).
68///
69/// # Examples
70/// ```
71/// use drasi_core::models::validate_effective_from;
72///
73/// // Valid millisecond timestamp (Feb 2026)
74/// assert!(validate_effective_from(1_771_000_000_000).is_ok());
75///
76/// // Zero is allowed (bootstrap "unknown" sentinel)
77/// assert!(validate_effective_from(0).is_ok());
78///
79/// // Nanosecond timestamp is rejected
80/// assert!(validate_effective_from(1_771_000_000_000_000_000).is_err());
81/// ```
82pub fn validate_effective_from(value: ElementTimestamp) -> Result<(), String> {
83    if value == 0 {
84        return Ok(());
85    }
86    if value > MAX_REASONABLE_MILLIS_TIMESTAMP {
87        return Err(format!(
88            "effective_from value {} ({:.2e}) appears to be in nanoseconds or microseconds, \
89             not milliseconds. Expected a value < {} (~year 2286). \
90             Use timestamp_millis() instead of timestamp_nanos_opt().",
91            value, value as f64, MAX_REASONABLE_MILLIS_TIMESTAMP
92        ));
93    }
94    Ok(())
95}
96
97#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
98pub struct ElementMetadata {
99    pub reference: ElementReference,
100    pub labels: Arc<[Arc<str>]>,
101
102    /// The effective time from which this element is valid. Measured in milliseconds since UNIX epoch.
103    pub effective_from: ElementTimestamp,
104}
105
106impl Display for ElementMetadata {
107    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
108        write!(
109            f,
110            "({}, [{}], {})",
111            self.reference,
112            self.labels.join(","),
113            self.effective_from
114        )
115    }
116}
117
118#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
119pub enum Element {
120    // Incoming changes get turned into an Element
121    Node {
122        metadata: ElementMetadata,
123        properties: ElementPropertyMap,
124    },
125    Relation {
126        metadata: ElementMetadata,
127        in_node: ElementReference,
128        out_node: ElementReference,
129        properties: ElementPropertyMap,
130    },
131}
132
133impl Element {
134    pub fn get_reference(&self) -> &ElementReference {
135        match self {
136            Element::Node { metadata, .. } => &metadata.reference,
137            Element::Relation { metadata, .. } => &metadata.reference,
138        }
139    }
140
141    pub fn get_effective_from(&self) -> ElementTimestamp {
142        match self {
143            Element::Node { metadata, .. } => metadata.effective_from,
144            Element::Relation { metadata, .. } => metadata.effective_from,
145        }
146    }
147
148    pub fn get_metadata(&self) -> &ElementMetadata {
149        match self {
150            Element::Node { metadata, .. } => metadata,
151            Element::Relation { metadata, .. } => metadata,
152        }
153    }
154
155    pub fn get_property(&self, name: &str) -> &ElementValue {
156        let props = match self {
157            Element::Node { properties, .. } => properties,
158            Element::Relation { properties, .. } => properties,
159        };
160        &props[name]
161    }
162
163    pub fn get_properties(&self) -> &ElementPropertyMap {
164        match self {
165            Element::Node { properties, .. } => properties,
166            Element::Relation { properties, .. } => properties,
167        }
168    }
169
170    pub fn merge_missing_properties(&mut self, other: &Element) {
171        match (self, other) {
172            (
173                Element::Node {
174                    properties,
175                    metadata,
176                },
177                Element::Node {
178                    properties: other_properties,
179                    metadata: other_metadata,
180                },
181            ) => {
182                assert_eq!(metadata.reference, other_metadata.reference);
183                properties.merge(other_properties);
184            }
185            (
186                Element::Relation {
187                    in_node: _,
188                    out_node: _,
189                    properties,
190                    metadata,
191                },
192                Element::Relation {
193                    in_node: _other_in_node,
194                    out_node: _other_out_node,
195                    properties: other_properties,
196                    metadata: other_metadata,
197                },
198            ) => {
199                assert_eq!(metadata.reference, other_metadata.reference);
200                properties.merge(other_properties);
201            }
202            _ => panic!("Cannot merge different element types"),
203        }
204    }
205
206    pub fn to_expression_variable(&self) -> VariableValue {
207        VariableValue::Element(Arc::new(self.clone()))
208    }
209
210    pub fn update_effective_time(&mut self, timestamp: ElementTimestamp) {
211        match self {
212            Element::Node { metadata, .. } => metadata.effective_from = timestamp,
213            Element::Relation { metadata, .. } => metadata.effective_from = timestamp,
214        }
215    }
216}
217
218impl From<&Element> for serde_json::Value {
219    fn from(val: &Element) -> Self {
220        match val {
221            Element::Node {
222                metadata,
223                properties,
224            } => {
225                let mut properties: serde_json::Map<String, serde_json::Value> = properties.into();
226
227                properties.insert(
228                    "$metadata".to_string(),
229                    serde_json::Value::String(metadata.to_string()),
230                );
231
232                serde_json::Value::Object(properties)
233            }
234            Element::Relation {
235                metadata,
236                in_node,
237                out_node,
238                properties,
239            } => {
240                let mut properties: serde_json::Map<String, serde_json::Value> = properties.into();
241
242                properties.insert(
243                    "$metadata".to_string(),
244                    serde_json::Value::String(metadata.to_string()),
245                );
246
247                properties.insert(
248                    "$in_node".to_string(),
249                    serde_json::Value::String(in_node.to_string()),
250                );
251                properties.insert(
252                    "$out_node".to_string(),
253                    serde_json::Value::String(out_node.to_string()),
254                );
255
256                serde_json::Value::Object(properties)
257            }
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn validate_effective_from_accepts_zero() {
268        assert!(validate_effective_from(0).is_ok());
269    }
270
271    #[test]
272    fn validate_effective_from_accepts_valid_millis() {
273        // Feb 2026 in milliseconds
274        assert!(validate_effective_from(1_771_000_000_000).is_ok());
275        // Jan 2000 in milliseconds
276        assert!(validate_effective_from(946_684_800_000).is_ok());
277        // Year 2100 in milliseconds
278        assert!(validate_effective_from(4_102_444_800_000).is_ok());
279    }
280
281    #[test]
282    fn validate_effective_from_rejects_nanoseconds() {
283        // Feb 2026 in nanoseconds (~1.77 × 10^18)
284        let nanos = 1_771_000_000_000_000_000u64;
285        let result = validate_effective_from(nanos);
286        assert!(result.is_err());
287        assert!(result.unwrap_err().contains("nanoseconds"));
288    }
289
290    #[test]
291    fn validate_effective_from_rejects_microseconds() {
292        // Feb 2026 in microseconds (~1.77 × 10^15)
293        let micros = 1_771_000_000_000_000u64;
294        let result = validate_effective_from(micros);
295        assert!(result.is_err());
296    }
297
298    #[test]
299    fn validate_effective_from_accepts_boundary_value() {
300        // Just under the threshold (year ~2286)
301        assert!(validate_effective_from(MAX_REASONABLE_MILLIS_TIMESTAMP - 1).is_ok());
302        // At the threshold
303        assert!(validate_effective_from(MAX_REASONABLE_MILLIS_TIMESTAMP).is_ok());
304        // Just over the threshold
305        assert!(validate_effective_from(MAX_REASONABLE_MILLIS_TIMESTAMP + 1).is_err());
306    }
307}