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