1use serde::Deserialize;
18use serde_json::Value;
19use std::fmt;
20
21use crate::ast::{SerializableStackSpec, SerializableStreamSpec, CURRENT_AST_VERSION};
22
23#[derive(Debug, Clone)]
25pub enum VersionedLoadError {
26 InvalidJson(String),
28 UnsupportedVersion(String),
30 InvalidStructure(String),
32}
33
34impl fmt::Display for VersionedLoadError {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 VersionedLoadError::InvalidJson(msg) => {
38 write!(f, "Invalid JSON: {}", msg)
39 }
40 VersionedLoadError::UnsupportedVersion(version) => {
41 write!(
42 f,
43 "Unsupported AST version: {}. Latest supported version: {}. \
44 Older versions are supported via automatic migration.",
45 version, CURRENT_AST_VERSION
46 )
47 }
48 VersionedLoadError::InvalidStructure(msg) => {
49 write!(f, "Invalid AST structure: {}", msg)
50 }
51 }
52 }
53}
54
55impl std::error::Error for VersionedLoadError {}
56
57pub fn load_stack_spec(json: &str) -> Result<SerializableStackSpec, VersionedLoadError> {
79 let raw: Value =
81 serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
82
83 let version = raw
85 .get("ast_version")
86 .and_then(|v| v.as_str())
87 .unwrap_or("0.0.1");
88
89 match version {
91 v if v == CURRENT_AST_VERSION => {
92 serde_json::from_value::<SerializableStackSpec>(raw)
94 .map_err(|e| VersionedLoadError::InvalidStructure(e.to_string()))
95 }
96 _ => {
99 Err(VersionedLoadError::UnsupportedVersion(version.to_string()))
101 }
102 }
103}
104
105pub fn load_stream_spec(json: &str) -> Result<SerializableStreamSpec, VersionedLoadError> {
117 let raw: Value =
119 serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
120
121 let version = raw
123 .get("ast_version")
124 .and_then(|v| v.as_str())
125 .unwrap_or("0.0.1");
126
127 match version {
129 v if v == CURRENT_AST_VERSION => {
130 serde_json::from_value::<SerializableStreamSpec>(raw)
132 .map_err(|e| VersionedLoadError::InvalidStructure(e.to_string()))
133 }
134 _ => {
137 Err(VersionedLoadError::UnsupportedVersion(version.to_string()))
139 }
140 }
141}
142
143#[derive(Debug, Clone, Deserialize)]
155#[serde(tag = "ast_version")]
156pub enum VersionedStackSpec {
157 #[serde(rename = "0.0.1")]
158 V1(SerializableStackSpec),
159}
160
161impl VersionedStackSpec {
162 pub fn into_latest(self) -> SerializableStackSpec {
168 match self {
169 VersionedStackSpec::V1(spec) => spec,
170 }
171 }
172}
173
174#[derive(Debug, Clone, Deserialize)]
186#[serde(tag = "ast_version")]
187pub enum VersionedStreamSpec {
188 #[serde(rename = "0.0.1")]
189 V1(SerializableStreamSpec),
190}
191
192impl VersionedStreamSpec {
193 pub fn into_latest(self) -> SerializableStreamSpec {
199 match self {
200 VersionedStreamSpec::V1(spec) => spec,
201 }
202 }
203}
204
205pub fn detect_ast_version(json: &str) -> Result<String, VersionedLoadError> {
224 let raw: Value =
225 serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
226
227 Ok(raw
228 .get("ast_version")
229 .and_then(|v| v.as_str())
230 .map(|s| s.to_string())
231 .unwrap_or_else(|| "0.0.1".to_string()))
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_load_stack_spec_v1() {
240 let json = r#"
241 {
242 "ast_version": "0.0.1",
243 "stack_name": "TestStack",
244 "program_ids": [],
245 "idls": [],
246 "entities": [],
247 "pdas": {},
248 "instructions": []
249 }
250 "#;
251
252 let result = load_stack_spec(json);
253 assert!(result.is_ok());
254 let spec = result.unwrap();
255 assert_eq!(spec.stack_name, "TestStack");
256 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
257 }
258
259 #[test]
260 fn test_load_stack_spec_no_version_defaults_to_v1() {
261 let json = r#"
263 {
264 "stack_name": "TestStack",
265 "program_ids": [],
266 "idls": [],
267 "entities": [],
268 "pdas": {},
269 "instructions": []
270 }
271 "#;
272
273 let result = load_stack_spec(json);
274 assert!(result.is_ok());
275 let spec = result.unwrap();
276 assert_eq!(spec.stack_name, "TestStack");
277 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
278 }
279
280 #[test]
281 fn test_load_stack_spec_unsupported_version() {
282 let json = r#"
283 {
284 "ast_version": "99.0.0",
285 "stack_name": "TestStack",
286 "program_ids": [],
287 "idls": [],
288 "entities": [],
289 "pdas": {},
290 "instructions": []
291 }
292 "#;
293
294 let result = load_stack_spec(json);
295 assert!(result.is_err());
296 match result.unwrap_err() {
297 VersionedLoadError::UnsupportedVersion(v) => assert_eq!(v, "99.0.0"),
298 _ => panic!("Expected UnsupportedVersion error"),
299 }
300 }
301
302 #[test]
303 fn test_load_stream_spec_v1() {
304 let json = r#"
305 {
306 "ast_version": "0.0.1",
307 "state_name": "TestEntity",
308 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
309 "handlers": [],
310 "sections": [],
311 "field_mappings": {},
312 "resolver_hooks": [],
313 "instruction_hooks": [],
314 "resolver_specs": [],
315 "computed_fields": [],
316 "computed_field_specs": [],
317 "views": []
318 }
319 "#;
320
321 let result = load_stream_spec(json);
322 assert!(result.is_ok());
323 let spec = result.unwrap();
324 assert_eq!(spec.state_name, "TestEntity");
325 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
326 }
327
328 #[test]
329 fn test_load_stream_spec_no_version_defaults_to_v1() {
330 let json = r#"
332 {
333 "state_name": "TestEntity",
334 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
335 "handlers": [],
336 "sections": [],
337 "field_mappings": {},
338 "resolver_hooks": [],
339 "instruction_hooks": [],
340 "resolver_specs": [],
341 "computed_fields": [],
342 "computed_field_specs": [],
343 "views": []
344 }
345 "#;
346
347 let result = load_stream_spec(json);
348 assert!(result.is_ok());
349 let spec = result.unwrap();
350 assert_eq!(spec.state_name, "TestEntity");
351 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
352 }
353
354 #[test]
355 fn test_load_stream_spec_unsupported_version() {
356 let json = r#"
357 {
358 "ast_version": "99.0.0",
359 "state_name": "TestEntity",
360 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
361 "handlers": [],
362 "sections": [],
363 "field_mappings": {},
364 "resolver_hooks": [],
365 "instruction_hooks": [],
366 "resolver_specs": [],
367 "computed_fields": [],
368 "computed_field_specs": [],
369 "views": []
370 }
371 "#;
372
373 let result = load_stream_spec(json);
374 assert!(result.is_err());
375 match result.unwrap_err() {
376 VersionedLoadError::UnsupportedVersion(v) => assert_eq!(v, "99.0.0"),
377 _ => panic!("Expected UnsupportedVersion error"),
378 }
379 }
380
381 #[test]
382 fn test_detect_ast_version() {
383 let json = r#"{"ast_version": "0.0.1", "stack_name": "Test"}"#;
384 assert_eq!(detect_ast_version(json).unwrap(), "0.0.1");
385
386 let json_no_version = r#"{"stack_name": "Test"}"#;
387 assert_eq!(detect_ast_version(json_no_version).unwrap(), "0.0.1");
388 }
389
390 #[test]
393 fn test_ast_version_sync_with_macros() {
394 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
396 let macros_types_path = std::path::Path::new(&manifest_dir)
397 .join("..") .join("hyperstack-macros")
399 .join("src")
400 .join("ast")
401 .join("types.rs");
402
403 assert!(
405 macros_types_path.exists(),
406 "Cannot find hyperstack-macros source file at {:?}. \
407 This test requires the source tree to be available.",
408 macros_types_path
409 );
410
411 let content = std::fs::read_to_string(¯os_types_path)
412 .expect("Failed to read hyperstack-macros/src/ast/types.rs");
413
414 let version_line = content
416 .lines()
417 .find(|line| line.contains("pub const CURRENT_AST_VERSION"))
418 .expect("CURRENT_AST_VERSION not found in hyperstack-macros");
419
420 let version_str = version_line
421 .split('=')
422 .nth(1)
423 .and_then(|rhs| rhs.split('"').nth(1))
424 .expect("Failed to parse version string");
425
426 assert_eq!(
427 version_str, CURRENT_AST_VERSION,
428 "AST version mismatch! interpreter has '{}', hyperstack-macros has '{}'. \
429 Both crates must have the same CURRENT_AST_VERSION. \
430 Update both files when bumping the version.",
431 CURRENT_AST_VERSION, version_str
432 );
433 }
434}