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 loaded =
82 arete_artifacts::load_legacy_stack_value(json.as_bytes()).map_err(|error| match error {
83 arete_artifacts::ArtifactError::UnsupportedVersion { version, .. } => {
84 VersionedLoadError::UnsupportedVersion(version)
85 }
86 arete_artifacts::ArtifactError::InvalidJson(message) => {
87 VersionedLoadError::InvalidJson(message)
88 }
89 other => VersionedLoadError::InvalidStructure(other.to_string()),
90 })?;
91 serde_json::from_value::<SerializableStackSpec>(loaded.artifact)
92 .map_err(|error| VersionedLoadError::InvalidStructure(error.to_string()))
93}
94
95pub fn load_stream_spec(json: &str) -> Result<SerializableStreamSpec, VersionedLoadError> {
107 let raw: Value =
109 serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
110
111 let version = raw
113 .get("ast_version")
114 .and_then(|v| v.as_str())
115 .unwrap_or("0.0.1");
116
117 match version {
121 v if v == CURRENT_AST_VERSION || COMPATIBLE_AST_VERSIONS.contains(&v) => {
122 serde_json::from_value::<SerializableStreamSpec>(raw)
123 .map(|mut spec| {
124 spec.ast_version = CURRENT_AST_VERSION.to_string();
126 spec
127 })
128 .map_err(|e| VersionedLoadError::InvalidStructure(e.to_string()))
129 }
130 _ => {
132 Err(VersionedLoadError::UnsupportedVersion(version.to_string()))
134 }
135 }
136}
137
138#[derive(Debug, Clone, Deserialize)]
150#[serde(tag = "ast_version")]
151pub enum VersionedStackSpec {
152 #[serde(rename = "0.0.1")]
153 V1(SerializableStackSpec),
154 #[serde(rename = "0.0.2")]
155 V2(SerializableStackSpec),
156 #[serde(rename = "0.0.3")]
157 V3(SerializableStackSpec),
158 #[serde(rename = "0.0.4")]
159 V4(SerializableStackSpec),
160 #[serde(rename = "0.0.5")]
161 V5(SerializableStackSpec),
162}
163
164impl VersionedStackSpec {
165 pub fn into_latest(self) -> SerializableStackSpec {
171 match self {
172 VersionedStackSpec::V1(spec)
173 | VersionedStackSpec::V2(spec)
174 | VersionedStackSpec::V3(spec)
175 | VersionedStackSpec::V4(spec)
176 | VersionedStackSpec::V5(spec) => spec,
177 }
178 }
179}
180
181#[derive(Debug, Clone, Deserialize)]
193#[serde(tag = "ast_version")]
194pub enum VersionedStreamSpec {
195 #[serde(rename = "0.0.1")]
196 V1(SerializableStreamSpec),
197 #[serde(rename = "0.0.2")]
198 V2(SerializableStreamSpec),
199 #[serde(rename = "0.0.3")]
200 V3(SerializableStreamSpec),
201 #[serde(rename = "0.0.4")]
202 V4(SerializableStreamSpec),
203 #[serde(rename = "0.0.5")]
204 V5(SerializableStreamSpec),
205}
206
207impl VersionedStreamSpec {
208 pub fn into_latest(self) -> SerializableStreamSpec {
214 match self {
215 VersionedStreamSpec::V1(spec)
216 | VersionedStreamSpec::V2(spec)
217 | VersionedStreamSpec::V3(spec)
218 | VersionedStreamSpec::V4(spec)
219 | VersionedStreamSpec::V5(spec) => spec,
220 }
221 }
222}
223
224pub fn detect_ast_version(json: &str) -> Result<String, VersionedLoadError> {
243 let raw: Value =
244 serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
245
246 Ok(raw
247 .get("ast_version")
248 .and_then(|v| v.as_str())
249 .map(|s| s.to_string())
250 .unwrap_or_else(|| "0.0.1".to_string()))
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_load_stack_spec_v1() {
259 let json = r#"
260 {
261 "ast_version": "0.0.1",
262 "stack_name": "TestStack",
263 "program_ids": [],
264 "idls": [],
265 "entities": [],
266 "pdas": {},
267 "instructions": []
268 }
269 "#;
270
271 let result = load_stack_spec(json);
272 assert!(result.is_ok());
273 let spec = result.unwrap();
274 assert_eq!(spec.stack_name, "TestStack");
275 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
276 }
277
278 #[test]
279 fn test_load_stack_spec_v2_without_new_field_metadata() {
280 let json = r#"
281 {
282 "ast_version": "0.0.2",
283 "stack_name": "TestStack",
284 "program_ids": [],
285 "idls": [],
286 "entities": [],
287 "pdas": {},
288 "instructions": []
289 }
290 "#;
291
292 let result = load_stack_spec(json);
293 assert!(result.is_ok());
294 let spec = result.unwrap();
295 assert_eq!(spec.stack_name, "TestStack");
296 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
297 }
298
299 #[test]
300 fn test_load_stack_spec_v3_without_instruction_amount_hints() {
301 let json = r#"
302 {
303 "ast_version": "0.0.3",
304 "stack_name": "TestStack",
305 "program_ids": [],
306 "idls": [],
307 "entities": [],
308 "pdas": {},
309 "instructions": []
310 }
311 "#;
312
313 let result = load_stack_spec(json);
314 assert!(result.is_ok());
315 let spec = result.unwrap();
316 assert_eq!(spec.stack_name, "TestStack");
317 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
318 }
319
320 #[test]
321 fn test_load_stack_spec_no_version_defaults_to_v1() {
322 let json = r#"
324 {
325 "stack_name": "TestStack",
326 "program_ids": [],
327 "idls": [],
328 "entities": [],
329 "pdas": {},
330 "instructions": []
331 }
332 "#;
333
334 let result = load_stack_spec(json);
335 assert!(result.is_ok());
336 let spec = result.unwrap();
337 assert_eq!(spec.stack_name, "TestStack");
338 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
339 }
340
341 #[test]
342 fn test_load_stack_spec_unsupported_version() {
343 let json = r#"
344 {
345 "ast_version": "99.0.0",
346 "stack_name": "TestStack",
347 "program_ids": [],
348 "idls": [],
349 "entities": [],
350 "pdas": {},
351 "instructions": []
352 }
353 "#;
354
355 let result = load_stack_spec(json);
356 assert!(result.is_err());
357 match result.unwrap_err() {
358 VersionedLoadError::UnsupportedVersion(v) => assert_eq!(v, "99.0.0"),
359 _ => panic!("Expected UnsupportedVersion error"),
360 }
361 }
362
363 #[test]
364 fn test_load_stream_spec_v1() {
365 let json = r#"
366 {
367 "ast_version": "0.0.1",
368 "state_name": "TestEntity",
369 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
370 "handlers": [],
371 "sections": [],
372 "field_mappings": {},
373 "resolver_hooks": [],
374 "instruction_hooks": [],
375 "resolver_specs": [],
376 "computed_fields": [],
377 "computed_field_specs": [],
378 "views": []
379 }
380 "#;
381
382 let result = load_stream_spec(json);
383 assert!(result.is_ok());
384 let spec = result.unwrap();
385 assert_eq!(spec.state_name, "TestEntity");
386 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
387 }
388
389 #[test]
390 fn test_load_stream_spec_v2_without_new_field_metadata() {
391 let json = r#"
392 {
393 "ast_version": "0.0.2",
394 "state_name": "TestEntity",
395 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
396 "handlers": [],
397 "sections": [],
398 "field_mappings": {},
399 "resolver_hooks": [],
400 "instruction_hooks": [],
401 "resolver_specs": [],
402 "computed_fields": [],
403 "computed_field_specs": [],
404 "views": []
405 }
406 "#;
407
408 let result = load_stream_spec(json);
409 assert!(result.is_ok());
410 let spec = result.unwrap();
411 assert_eq!(spec.state_name, "TestEntity");
412 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
413 }
414
415 #[test]
416 fn test_load_stream_spec_v3_without_instruction_amount_hints() {
417 let json = r#"
418 {
419 "ast_version": "0.0.3",
420 "state_name": "TestEntity",
421 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
422 "handlers": [],
423 "sections": [],
424 "field_mappings": {},
425 "resolver_hooks": [],
426 "instruction_hooks": [],
427 "resolver_specs": [],
428 "computed_fields": [],
429 "computed_field_specs": [],
430 "views": []
431 }
432 "#;
433
434 let result = load_stream_spec(json);
435 assert!(result.is_ok());
436 let spec = result.unwrap();
437 assert_eq!(spec.state_name, "TestEntity");
438 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
439 }
440
441 #[test]
442 fn test_load_stream_spec_no_version_defaults_to_v1() {
443 let json = r#"
445 {
446 "state_name": "TestEntity",
447 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
448 "handlers": [],
449 "sections": [],
450 "field_mappings": {},
451 "resolver_hooks": [],
452 "instruction_hooks": [],
453 "resolver_specs": [],
454 "computed_fields": [],
455 "computed_field_specs": [],
456 "views": []
457 }
458 "#;
459
460 let result = load_stream_spec(json);
461 assert!(result.is_ok());
462 let spec = result.unwrap();
463 assert_eq!(spec.state_name, "TestEntity");
464 assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
465 }
466
467 #[test]
468 fn test_load_stream_spec_unsupported_version() {
469 let json = r#"
470 {
471 "ast_version": "99.0.0",
472 "state_name": "TestEntity",
473 "identity": {"primary_keys": ["id"], "lookup_indexes": []},
474 "handlers": [],
475 "sections": [],
476 "field_mappings": {},
477 "resolver_hooks": [],
478 "instruction_hooks": [],
479 "resolver_specs": [],
480 "computed_fields": [],
481 "computed_field_specs": [],
482 "views": []
483 }
484 "#;
485
486 let result = load_stream_spec(json);
487 assert!(result.is_err());
488 match result.unwrap_err() {
489 VersionedLoadError::UnsupportedVersion(v) => assert_eq!(v, "99.0.0"),
490 _ => panic!("Expected UnsupportedVersion error"),
491 }
492 }
493
494 #[test]
495 fn test_detect_ast_version() {
496 let json = r#"{"ast_version": "0.0.1", "stack_name": "Test"}"#;
497 assert_eq!(detect_ast_version(json).unwrap(), "0.0.1");
498
499 let json_no_version = r#"{"stack_name": "Test"}"#;
500 assert_eq!(detect_ast_version(json_no_version).unwrap(), "0.0.1");
501 }
502
503 #[test]
506 fn test_ast_version_sync_with_macros() {
507 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
509 let macros_types_path = std::path::Path::new(&manifest_dir)
510 .join("..") .join("arete-macros")
512 .join("src")
513 .join("ast")
514 .join("types.rs");
515
516 assert!(
518 macros_types_path.exists(),
519 "Cannot find arete-macros source file at {:?}. \
520 This test requires the source tree to be available.",
521 macros_types_path
522 );
523
524 let content = std::fs::read_to_string(¯os_types_path)
525 .expect("Failed to read arete-macros/src/ast/types.rs");
526
527 let version_line = content
529 .lines()
530 .find(|line| line.contains("pub const CURRENT_AST_VERSION"))
531 .expect("CURRENT_AST_VERSION not found in arete-macros");
532
533 let version_str = version_line
534 .split('=')
535 .nth(1)
536 .and_then(|rhs| rhs.split('"').nth(1))
537 .expect("Failed to parse version string");
538
539 assert_eq!(
540 version_str, CURRENT_AST_VERSION,
541 "AST version mismatch! interpreter has '{}', arete-macros has '{}'. \
542 Both crates must have the same CURRENT_AST_VERSION. \
543 Update both files when bumping the version.",
544 CURRENT_AST_VERSION, version_str
545 );
546 }
547}