Skip to main content

drasi_bootstrap_scriptfile/
script_types.rs

1// Copyright 2025 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
15//! Bootstrap script record type definitions
16//!
17//! This module defines the type hierarchy for bootstrap script records used in JSONL script files.
18//! Records are serialized as JSON Lines (one JSON object per line) with a "kind" field that
19//! discriminates between different record types.
20
21use chrono::{DateTime, FixedOffset};
22use serde::{Deserialize, Serialize};
23
24/// Main enum representing all possible bootstrap script record types
25///
26/// Uses tagged serialization with "kind" field to discriminate variants.
27/// Example: `{"kind":"Node","id":"n1","labels":["Person"],"properties":{"name":"Alice"}}`
28#[derive(Clone, Debug, Serialize, Deserialize)]
29#[serde(tag = "kind")]
30pub enum BootstrapScriptRecord {
31    /// Comment record (filtered out during processing)
32    Comment(CommentRecord),
33    /// Header record (required as first record in script)
34    Header(BootstrapHeaderRecord),
35    /// Label/checkpoint record for script navigation
36    Label(LabelRecord),
37    /// Node record representing a graph node/entity
38    Node(NodeRecord),
39    /// Relation record representing a graph edge/relationship
40    Relation(RelationRecord),
41    /// Finish record marking end of script
42    Finish(BootstrapFinishRecord),
43}
44
45/// Header record that must appear first in every bootstrap script
46///
47/// Provides metadata about the script including start time and description.
48#[derive(Clone, Debug, Serialize, Deserialize, Default)]
49pub struct BootstrapHeaderRecord {
50    /// Script start time (used as reference for offset calculations)
51    pub start_time: DateTime<FixedOffset>,
52    /// Human-readable description of the script
53    #[serde(default)]
54    pub description: String,
55}
56
57/// Node record representing a graph node/entity
58///
59/// Contains node identity, labels, and properties as arbitrary JSON.
60#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct NodeRecord {
62    /// Unique identifier for the node
63    pub id: String,
64    /// List of labels/types for the node
65    pub labels: Vec<String>,
66    /// Node properties as arbitrary JSON value
67    #[serde(default)]
68    pub properties: serde_json::Value,
69}
70
71/// Relation record representing a graph edge/relationship
72///
73/// Contains relationship identity, labels, start/end node references, and properties.
74#[derive(Clone, Debug, Serialize, Deserialize)]
75pub struct RelationRecord {
76    /// Unique identifier for the relation
77    pub id: String,
78    /// List of labels/types for the relation
79    pub labels: Vec<String>,
80    /// ID of the start/source node
81    pub start_id: String,
82    /// Optional label of the start node
83    pub start_label: Option<String>,
84    /// ID of the end/target node
85    pub end_id: String,
86    /// Optional label of the end node
87    pub end_label: Option<String>,
88    /// Relation properties as arbitrary JSON value
89    #[serde(default)]
90    pub properties: serde_json::Value,
91}
92
93/// Label/checkpoint record for marking positions in the script
94///
95/// Allows navigation to specific points in the script timeline.
96#[derive(Clone, Debug, Serialize, Deserialize)]
97pub struct LabelRecord {
98    /// Nanoseconds offset from script start time
99    #[serde(default)]
100    pub offset_ns: u64,
101    /// Label name for this checkpoint
102    pub label: String,
103    /// Description of this checkpoint
104    #[serde(default)]
105    pub description: String,
106}
107
108/// Comment record for documentation within scripts
109///
110/// These records are automatically filtered out during processing and never
111/// returned to consumers.
112#[derive(Clone, Debug, Serialize, Deserialize)]
113pub struct CommentRecord {
114    /// Comment text
115    pub comment: String,
116}
117
118/// Finish record marking the end of a bootstrap script
119///
120/// If not present in the script file, one is automatically generated.
121#[derive(Clone, Debug, Serialize, Deserialize)]
122pub struct BootstrapFinishRecord {
123    /// Description of script completion
124    #[serde(default)]
125    pub description: String,
126}
127
128/// Wrapper that adds sequence numbering to bootstrap script records
129///
130/// Used by the reader to track the order in which records are processed,
131/// especially when reading from multiple files.
132#[derive(Clone, Debug, Serialize)]
133pub struct SequencedBootstrapScriptRecord {
134    /// Sequence number (order in which record was read)
135    pub seq: u64,
136    /// The actual record
137    pub record: BootstrapScriptRecord,
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use serde_json::json;
144
145    #[test]
146    fn test_header_record_serialization() {
147        let header = BootstrapHeaderRecord {
148            start_time: DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z").unwrap(),
149            description: "Test Script".to_string(),
150        };
151        let record = BootstrapScriptRecord::Header(header);
152
153        let json = serde_json::to_string(&record).unwrap();
154        assert!(json.contains(r#""kind":"Header"#));
155        assert!(json.contains(r#""description":"Test Script"#));
156
157        // Test deserialization
158        let deserialized: BootstrapScriptRecord = serde_json::from_str(&json).unwrap();
159        if let BootstrapScriptRecord::Header(h) = deserialized {
160            assert_eq!(h.description, "Test Script");
161        } else {
162            panic!("Expected Header record");
163        }
164    }
165
166    #[test]
167    fn test_node_record_serialization() {
168        let node = NodeRecord {
169            id: "n1".to_string(),
170            labels: vec!["Person".to_string()],
171            properties: json!({"name": "Alice", "age": 30}),
172        };
173        let record = BootstrapScriptRecord::Node(node);
174
175        let json = serde_json::to_string(&record).unwrap();
176        assert!(json.contains(r#""kind":"Node"#));
177        assert!(json.contains(r#""id":"n1"#));
178
179        // Test deserialization
180        let deserialized: BootstrapScriptRecord = serde_json::from_str(&json).unwrap();
181        if let BootstrapScriptRecord::Node(n) = deserialized {
182            assert_eq!(n.id, "n1");
183            assert_eq!(n.labels, vec!["Person"]);
184        } else {
185            panic!("Expected Node record");
186        }
187    }
188
189    #[test]
190    fn test_relation_record_serialization() {
191        let relation = RelationRecord {
192            id: "r1".to_string(),
193            labels: vec!["KNOWS".to_string()],
194            start_id: "n1".to_string(),
195            start_label: Some("Person".to_string()),
196            end_id: "n2".to_string(),
197            end_label: Some("Person".to_string()),
198            properties: json!({"since": 2020}),
199        };
200        let record = BootstrapScriptRecord::Relation(relation);
201
202        let json = serde_json::to_string(&record).unwrap();
203        assert!(json.contains(r#""kind":"Relation"#));
204        assert!(json.contains(r#""start_id":"n1"#));
205
206        // Test deserialization
207        let deserialized: BootstrapScriptRecord = serde_json::from_str(&json).unwrap();
208        if let BootstrapScriptRecord::Relation(r) = deserialized {
209            assert_eq!(r.id, "r1");
210            assert_eq!(r.start_id, "n1");
211            assert_eq!(r.end_id, "n2");
212        } else {
213            panic!("Expected Relation record");
214        }
215    }
216
217    #[test]
218    fn test_comment_record_serialization() {
219        let comment = CommentRecord {
220            comment: "This is a comment".to_string(),
221        };
222        let record = BootstrapScriptRecord::Comment(comment);
223
224        let json = serde_json::to_string(&record).unwrap();
225        assert!(json.contains(r#""kind":"Comment"#));
226
227        // Test deserialization
228        let deserialized: BootstrapScriptRecord = serde_json::from_str(&json).unwrap();
229        if let BootstrapScriptRecord::Comment(c) = deserialized {
230            assert_eq!(c.comment, "This is a comment");
231        } else {
232            panic!("Expected Comment record");
233        }
234    }
235
236    #[test]
237    fn test_label_record_serialization() {
238        let label = LabelRecord {
239            offset_ns: 1000,
240            label: "checkpoint_1".to_string(),
241            description: "First checkpoint".to_string(),
242        };
243        let record = BootstrapScriptRecord::Label(label);
244
245        let json = serde_json::to_string(&record).unwrap();
246        assert!(json.contains(r#""kind":"Label"#));
247
248        // Test deserialization
249        let deserialized: BootstrapScriptRecord = serde_json::from_str(&json).unwrap();
250        if let BootstrapScriptRecord::Label(l) = deserialized {
251            assert_eq!(l.label, "checkpoint_1");
252            assert_eq!(l.offset_ns, 1000);
253        } else {
254            panic!("Expected Label record");
255        }
256    }
257
258    #[test]
259    fn test_finish_record_serialization() {
260        let finish = BootstrapFinishRecord {
261            description: "End of script".to_string(),
262        };
263        let record = BootstrapScriptRecord::Finish(finish);
264
265        let json = serde_json::to_string(&record).unwrap();
266        assert!(json.contains(r#""kind":"Finish"#));
267
268        // Test deserialization
269        let deserialized: BootstrapScriptRecord = serde_json::from_str(&json).unwrap();
270        if let BootstrapScriptRecord::Finish(f) = deserialized {
271            assert_eq!(f.description, "End of script");
272        } else {
273            panic!("Expected Finish record");
274        }
275    }
276
277    #[test]
278    fn test_default_header_record() {
279        let header = BootstrapHeaderRecord::default();
280        assert!(header.description.is_empty());
281    }
282
283    #[test]
284    fn test_node_with_empty_properties() {
285        let json = r#"{"kind":"Node","id":"n1","labels":["Test"]}"#;
286        let record: BootstrapScriptRecord = serde_json::from_str(json).unwrap();
287
288        if let BootstrapScriptRecord::Node(n) = record {
289            assert_eq!(n.id, "n1");
290            assert_eq!(n.properties, serde_json::Value::Null);
291        } else {
292            panic!("Expected Node record");
293        }
294    }
295
296    #[test]
297    fn test_relation_without_optional_labels() {
298        let json =
299            r#"{"kind":"Relation","id":"r1","labels":["REL"],"start_id":"n1","end_id":"n2"}"#;
300        let record: BootstrapScriptRecord = serde_json::from_str(json).unwrap();
301
302        if let BootstrapScriptRecord::Relation(r) = record {
303            assert_eq!(r.start_label, None);
304            assert_eq!(r.end_label, None);
305        } else {
306            panic!("Expected Relation record");
307        }
308    }
309}