1use crate::metadata::StepMetadata;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6pub const SCHEMA_VERSION: &str = "1.0";
8
9fn default_schema_version() -> String {
10 SCHEMA_VERSION.to_string()
11}
12
13fn parse_schema_version(version: &str) -> Option<(u64, u64)> {
14 let trimmed = version.trim();
15 if trimmed.is_empty() {
16 return None;
17 }
18
19 let mut parts = trimmed.split('.');
20 let major = parts.next()?.parse().ok()?;
21 let minor = parts.next().unwrap_or("0").parse().ok()?;
22 Some((major, minor))
23}
24
25pub fn is_supported_schema_version(version: &str) -> bool {
29 let Some((major, _)) = parse_schema_version(version) else {
30 return false;
31 };
32 let Some((supported_major, _)) = parse_schema_version(SCHEMA_VERSION) else {
33 return false;
34 };
35 major == supported_major
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Schematic {
44 #[serde(default = "default_schema_version")]
46 pub schema_version: String,
47 pub id: String,
49 pub name: String,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub description: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub generated_at: Option<DateTime<Utc>>,
57 pub nodes: Vec<Node>,
59 pub edges: Vec<Edge>,
61}
62
63impl Default for Schematic {
64 fn default() -> Self {
65 Self {
66 schema_version: SCHEMA_VERSION.to_string(),
67 id: Uuid::new_v4().to_string(),
68 name: String::new(),
69 description: None,
70 generated_at: Some(Utc::now()),
71 nodes: Vec::new(),
72 edges: Vec::new(),
73 }
74 }
75}
76
77impl Schematic {
78 pub fn new(name: impl Into<String>) -> Self {
79 Self {
80 name: name.into(),
81 ..Default::default()
82 }
83 }
84
85 pub fn is_supported_schema_version(&self) -> bool {
86 is_supported_schema_version(&self.schema_version)
87 }
88
89 pub fn with_id(name: impl Into<String>, id: impl Into<String>) -> Self {
91 Self {
92 id: id.into(),
93 name: name.into(),
94 ..Default::default()
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct SourceLocation {
102 pub file: String,
104 pub line: u32,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub column: Option<u32>,
109}
110
111impl SourceLocation {
112 pub fn new(file: impl Into<String>, line: u32) -> Self {
113 Self {
114 file: file.into(),
115 line,
116 column: None,
117 }
118 }
119
120 pub fn with_column(file: impl Into<String>, line: u32, column: u32) -> Self {
121 Self {
122 file: file.into(),
123 line,
124 column: Some(column),
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct Node {
131 pub id: String, pub kind: NodeKind,
133 pub label: String,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub description: Option<String>,
136 pub input_type: String,
137 pub output_type: String, pub resource_type: String,
139 pub metadata: StepMetadata,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub bus_capability: Option<BusCapabilitySchema>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub source_location: Option<SourceLocation>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149pub struct BusCapabilitySchema {
150 #[serde(skip_serializing_if = "Vec::is_empty", default)]
151 pub allow: Vec<String>,
152 #[serde(skip_serializing_if = "Vec::is_empty", default)]
153 pub deny: Vec<String>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub enum NodeKind {
158 Ingress, Atom, Synapse, Egress, Subgraph(Box<Schematic>), }
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub enum EdgeType {
167 Linear, Branch(String), Jump, Fault, }
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct Edge {
175 pub from: String,
176 pub to: String,
177 pub kind: EdgeType,
178 pub label: Option<String>,
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_schematic_default_has_version_and_id() {
187 let schematic = Schematic::new("Test Circuit");
188 assert_eq!(schematic.schema_version, SCHEMA_VERSION);
189 assert!(schematic.is_supported_schema_version());
190 assert!(!schematic.id.is_empty());
191 assert!(schematic.generated_at.is_some());
192 }
193
194 #[test]
195 fn test_schematic_serialization_with_new_fields() {
196 let schematic = Schematic::new("Test");
197 let json = serde_json::to_string_pretty(&schematic).unwrap();
198
199 assert!(json.contains("schema_version"));
200 assert!(json.contains("\"1.0\""));
201 assert!(json.contains("generated_at"));
202 }
203
204 #[test]
205 fn test_source_location_optional_in_json() {
206 let schematic = Schematic::new("Test");
207 let json = serde_json::to_string(&schematic).unwrap();
208
209 assert!(!json.contains("description"));
211 }
212
213 #[test]
214 fn test_source_location_creation() {
215 let loc = SourceLocation::new("src/main.rs", 42);
216 assert_eq!(loc.file, "src/main.rs");
217 assert_eq!(loc.line, 42);
218 assert!(loc.column.is_none());
219
220 let loc_with_col = SourceLocation::with_column("src/lib.rs", 10, 5);
221 assert_eq!(loc_with_col.column, Some(5));
222 }
223
224 #[test]
225 fn test_schema_version_defaults_when_missing_in_json() {
226 let json = r#"{
227 "id": "test-id",
228 "name": "Legacy Schematic",
229 "nodes": [],
230 "edges": []
231 }"#;
232 let schematic: Schematic = serde_json::from_str(json).unwrap();
233 assert_eq!(schematic.schema_version, SCHEMA_VERSION);
234 assert!(schematic.is_supported_schema_version());
235 }
236
237 #[test]
238 fn test_supported_schema_version_major_compatibility() {
239 assert!(is_supported_schema_version("1"));
240 assert!(is_supported_schema_version("1.0"));
241 assert!(is_supported_schema_version("1.1"));
242 assert!(is_supported_schema_version("1.0.9"));
243 assert!(!is_supported_schema_version("2.0"));
244 assert!(!is_supported_schema_version(""));
245 assert!(!is_supported_schema_version("invalid"));
246 }
247}