1use serde::Deserialize;
18use serde_json::Value;
19use std::fmt;
20
21use crate::ast::{
22 SerializableStackSpec, SerializableStreamSpec, COMPATIBLE_AST_VERSIONS, CURRENT_AST_VERSION,
23};
24
25#[derive(Debug, Clone)]
27pub enum VersionedLoadError {
28 InvalidJson(String),
30 UnsupportedVersion(String),
32 InvalidStructure(String),
34}
35
36impl fmt::Display for VersionedLoadError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 VersionedLoadError::InvalidJson(msg) => {
40 write!(f, "Invalid JSON: {}", msg)
41 }
42 VersionedLoadError::UnsupportedVersion(version) => {
43 write!(
44 f,
45 "Unsupported AST version: {}. Latest supported version: {}. \
46 Older versions are supported via automatic migration.",
47 version, CURRENT_AST_VERSION
48 )
49 }
50 VersionedLoadError::InvalidStructure(msg) => {
51 write!(f, "Invalid AST structure: {}", msg)
52 }
53 }
54 }
55}
56
57impl std::error::Error for VersionedLoadError {}
58
59pub fn load_stack_spec(json: &str) -> Result<SerializableStackSpec, VersionedLoadError> {
81 let raw: Value =
83 serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
84
85 let version = raw
87 .get("ast_version")
88 .and_then(|v| v.as_str())
89 .unwrap_or("0.0.1");
90
91 match version {
95 v if v == CURRENT_AST_VERSION || COMPATIBLE_AST_VERSIONS.contains(&v) => {
96 serde_json::from_value::<SerializableStackSpec>(raw)
97 .map(|mut spec| {
98 spec.ast_version = CURRENT_AST_VERSION.to_string();
100 spec
101 })
102 .map_err(|e| VersionedLoadError::InvalidStructure(e.to_string()))
103 }
104 _ => {
106 Err(VersionedLoadError::UnsupportedVersion(version.to_string()))
108 }
109 }
110}
111
112pub fn load_stream_spec(json: &str) -> Result<SerializableStreamSpec, VersionedLoadError> {
124 let raw: Value =
126 serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
127
128 let version = raw
130 .get("ast_version")
131 .and_then(|v| v.as_str())
132 .unwrap_or("0.0.1");
133
134 match version {
138 v if v == CURRENT_AST_VERSION || COMPATIBLE_AST_VERSIONS.contains(&v) => {
139 serde_json::from_value::<SerializableStreamSpec>(raw)
140 .map(|mut spec| {
141 spec.ast_version = CURRENT_AST_VERSION.to_string();
143 spec
144 })
145 .map_err(|e| VersionedLoadError::InvalidStructure(e.to_string()))
146 }
147 _ => {
149 Err(VersionedLoadError::UnsupportedVersion(version.to_string()))
151 }
152 }
153}
154
155#[derive(Debug, Clone, Deserialize)]
167#[serde(tag = "ast_version")]
168pub enum VersionedStackSpec {
169 #[serde(rename = "0.0.1")]
170 V1(SerializableStackSpec),
171 #[serde(rename = "0.0.2")]
172 V2(SerializableStackSpec),
173 #[serde(rename = "0.0.3")]
174 V3(SerializableStackSpec),
175}
176
177impl VersionedStackSpec {
178 pub fn into_latest(self) -> SerializableStackSpec {
184 match self {
185 VersionedStackSpec::V1(spec)
186 | VersionedStackSpec::V2(spec)
187 | VersionedStackSpec::V3(spec) => spec,
188 }
189 }
190}
191
192#[derive(Debug, Clone, Deserialize)]
204#[serde(tag = "ast_version")]
205pub enum VersionedStreamSpec {
206 #[serde(rename = "0.0.1")]
207 V1(SerializableStreamSpec),
208 #[serde(rename = "0.0.2")]
209 V2(SerializableStreamSpec),
210 #[serde(rename = "0.0.3")]
211 V3(SerializableStreamSpec),
212}
213
214impl VersionedStreamSpec {
215 pub fn into_latest(self) -> SerializableStreamSpec {
221 match self {
222 VersionedStreamSpec::V1(spec)
223 | VersionedStreamSpec::V2(spec)
224 | VersionedStreamSpec::V3(spec) => spec,
225 }
226 }
227}
228
229pub fn detect_ast_version(json: &str) -> Result<String, VersionedLoadError> {
248 let raw: Value =
249 serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
250
251 Ok(raw
252 .get("ast_version")
253 .and_then(|v| v.as_str())
254 .map(|s| s.to_string())
255 .unwrap_or_else(|| "0.0.1".to_string()))
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_load_stack_spec_v1() {
264 let json = r#"
265 {
266 "ast_version": "0.0.1",
267 "stack_name": "TestStack",
268 "program_ids": [],
269 "idls": [],
270 "entities": [],
271 "pdas": {},
272 "instructions": []
273 }
274 "#;
275
276 let result = load_stack_spec(json);
277 assert!(result.is_ok());
278 let spec = result.unwrap();
279 assert_eq!(spec.stack_name, "TestStack");
280 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
281 }
282
283 #[test]
284 fn test_load_stack_spec_v2_without_new_field_metadata() {
285 let json = r#"
286 {
287 "ast_version": "0.0.2",
288 "stack_name": "TestStack",
289 "program_ids": [],
290 "idls": [],
291 "entities": [],
292 "pdas": {},
293 "instructions": []
294 }
295 "#;
296
297 let result = load_stack_spec(json);
298 assert!(result.is_ok());
299 let spec = result.unwrap();
300 assert_eq!(spec.stack_name, "TestStack");
301 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
302 }
303
304 #[test]
305 fn test_load_stack_spec_v3_without_instruction_amount_hints() {
306 let json = r#"
307 {
308 "ast_version": "0.0.3",
309 "stack_name": "TestStack",
310 "program_ids": [],
311 "idls": [],
312 "entities": [],
313 "pdas": {},
314 "instructions": []
315 }
316 "#;
317
318 let result = load_stack_spec(json);
319 assert!(result.is_ok());
320 let spec = result.unwrap();
321 assert_eq!(spec.stack_name, "TestStack");
322 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
323 }
324
325 #[test]
326 fn test_load_stack_spec_no_version_defaults_to_v1() {
327 let json = r#"
329 {
330 "stack_name": "TestStack",
331 "program_ids": [],
332 "idls": [],
333 "entities": [],
334 "pdas": {},
335 "instructions": []
336 }
337 "#;
338
339 let result = load_stack_spec(json);
340 assert!(result.is_ok());
341 let spec = result.unwrap();
342 assert_eq!(spec.stack_name, "TestStack");
343 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
344 }
345
346 #[test]
347 fn test_load_stack_spec_unsupported_version() {
348 let json = r#"
349 {
350 "ast_version": "99.0.0",
351 "stack_name": "TestStack",
352 "program_ids": [],
353 "idls": [],
354 "entities": [],
355 "pdas": {},
356 "instructions": []
357 }
358 "#;
359
360 let result = load_stack_spec(json);
361 assert!(result.is_err());
362 match result.unwrap_err() {
363 VersionedLoadError::UnsupportedVersion(v) => assert_eq!(v, "99.0.0"),
364 _ => panic!("Expected UnsupportedVersion error"),
365 }
366 }
367
368 #[test]
369 fn test_load_stream_spec_v1() {
370 let json = r#"
371 {
372 "ast_version": "0.0.1",
373 "state_name": "TestEntity",
374 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
375 "handlers": [],
376 "sections": [],
377 "field_mappings": {},
378 "resolver_hooks": [],
379 "instruction_hooks": [],
380 "resolver_specs": [],
381 "computed_fields": [],
382 "computed_field_specs": [],
383 "views": []
384 }
385 "#;
386
387 let result = load_stream_spec(json);
388 assert!(result.is_ok());
389 let spec = result.unwrap();
390 assert_eq!(spec.state_name, "TestEntity");
391 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
392 }
393
394 #[test]
395 fn test_load_stream_spec_v2_without_new_field_metadata() {
396 let json = r#"
397 {
398 "ast_version": "0.0.2",
399 "state_name": "TestEntity",
400 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
401 "handlers": [],
402 "sections": [],
403 "field_mappings": {},
404 "resolver_hooks": [],
405 "instruction_hooks": [],
406 "resolver_specs": [],
407 "computed_fields": [],
408 "computed_field_specs": [],
409 "views": []
410 }
411 "#;
412
413 let result = load_stream_spec(json);
414 assert!(result.is_ok());
415 let spec = result.unwrap();
416 assert_eq!(spec.state_name, "TestEntity");
417 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
418 }
419
420 #[test]
421 fn test_load_stream_spec_v3_without_instruction_amount_hints() {
422 let json = r#"
423 {
424 "ast_version": "0.0.3",
425 "state_name": "TestEntity",
426 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
427 "handlers": [],
428 "sections": [],
429 "field_mappings": {},
430 "resolver_hooks": [],
431 "instruction_hooks": [],
432 "resolver_specs": [],
433 "computed_fields": [],
434 "computed_field_specs": [],
435 "views": []
436 }
437 "#;
438
439 let result = load_stream_spec(json);
440 assert!(result.is_ok());
441 let spec = result.unwrap();
442 assert_eq!(spec.state_name, "TestEntity");
443 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
444 }
445
446 #[test]
447 fn test_load_stream_spec_no_version_defaults_to_v1() {
448 let json = r#"
450 {
451 "state_name": "TestEntity",
452 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
453 "handlers": [],
454 "sections": [],
455 "field_mappings": {},
456 "resolver_hooks": [],
457 "instruction_hooks": [],
458 "resolver_specs": [],
459 "computed_fields": [],
460 "computed_field_specs": [],
461 "views": []
462 }
463 "#;
464
465 let result = load_stream_spec(json);
466 assert!(result.is_ok());
467 let spec = result.unwrap();
468 assert_eq!(spec.state_name, "TestEntity");
469 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
470 }
471
472 #[test]
473 fn test_load_stream_spec_unsupported_version() {
474 let json = r#"
475 {
476 "ast_version": "99.0.0",
477 "state_name": "TestEntity",
478 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
479 "handlers": [],
480 "sections": [],
481 "field_mappings": {},
482 "resolver_hooks": [],
483 "instruction_hooks": [],
484 "resolver_specs": [],
485 "computed_fields": [],
486 "computed_field_specs": [],
487 "views": []
488 }
489 "#;
490
491 let result = load_stream_spec(json);
492 assert!(result.is_err());
493 match result.unwrap_err() {
494 VersionedLoadError::UnsupportedVersion(v) => assert_eq!(v, "99.0.0"),
495 _ => panic!("Expected UnsupportedVersion error"),
496 }
497 }
498
499 #[test]
500 fn test_detect_ast_version() {
501 let json = r#"{"ast_version": "0.0.1", "stack_name": "Test"}"#;
502 assert_eq!(detect_ast_version(json).unwrap(), "0.0.1");
503
504 let json_no_version = r#"{"stack_name": "Test"}"#;
505 assert_eq!(detect_ast_version(json_no_version).unwrap(), "0.0.1");
506 }
507
508 #[test]
511 fn test_ast_version_sync_with_macros() {
512 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
514 let macros_types_path = std::path::Path::new(&manifest_dir)
515 .join("..") .join("arete-macros")
517 .join("src")
518 .join("ast")
519 .join("types.rs");
520
521 assert!(
523 macros_types_path.exists(),
524 "Cannot find arete-macros source file at {:?}. \
525 This test requires the source tree to be available.",
526 macros_types_path
527 );
528
529 let content = std::fs::read_to_string(¯os_types_path)
530 .expect("Failed to read arete-macros/src/ast/types.rs");
531
532 let version_line = content
534 .lines()
535 .find(|line| line.contains("pub const CURRENT_AST_VERSION"))
536 .expect("CURRENT_AST_VERSION not found in arete-macros");
537
538 let version_str = version_line
539 .split('=')
540 .nth(1)
541 .and_then(|rhs| rhs.split('"').nth(1))
542 .expect("Failed to parse version string");
543
544 assert_eq!(
545 version_str, CURRENT_AST_VERSION,
546 "AST version mismatch! interpreter has '{}', arete-macros has '{}'. \
547 Both crates must have the same CURRENT_AST_VERSION. \
548 Update both files when bumping the version.",
549 CURRENT_AST_VERSION, version_str
550 );
551 }
552}