use serde::Deserialize;
use serde_json::Value;
use std::fmt;
use crate::ast::{SerializableStackSpec, SerializableStreamSpec, CURRENT_AST_VERSION};
#[derive(Debug, Clone)]
pub enum VersionedLoadError {
InvalidJson(String),
UnsupportedVersion(String),
InvalidStructure(String),
}
impl fmt::Display for VersionedLoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VersionedLoadError::InvalidJson(msg) => {
write!(f, "Invalid JSON: {}", msg)
}
VersionedLoadError::UnsupportedVersion(version) => {
write!(
f,
"Unsupported AST version: {}. Latest supported version: {}. \
Older versions are supported via automatic migration.",
version, CURRENT_AST_VERSION
)
}
VersionedLoadError::InvalidStructure(msg) => {
write!(f, "Invalid AST structure: {}", msg)
}
}
}
}
impl std::error::Error for VersionedLoadError {}
pub fn load_stack_spec(json: &str) -> Result<SerializableStackSpec, VersionedLoadError> {
let raw: Value =
serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
let version = raw
.get("ast_version")
.and_then(|v| v.as_str())
.unwrap_or("0.0.1");
match version {
v if v == CURRENT_AST_VERSION => {
serde_json::from_value::<SerializableStackSpec>(raw)
.map_err(|e| VersionedLoadError::InvalidStructure(e.to_string()))
}
_ => {
Err(VersionedLoadError::UnsupportedVersion(version.to_string()))
}
}
}
pub fn load_stream_spec(json: &str) -> Result<SerializableStreamSpec, VersionedLoadError> {
let raw: Value =
serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
let version = raw
.get("ast_version")
.and_then(|v| v.as_str())
.unwrap_or("0.0.1");
match version {
v if v == CURRENT_AST_VERSION => {
serde_json::from_value::<SerializableStreamSpec>(raw)
.map_err(|e| VersionedLoadError::InvalidStructure(e.to_string()))
}
_ => {
Err(VersionedLoadError::UnsupportedVersion(version.to_string()))
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "ast_version")]
pub enum VersionedStackSpec {
#[serde(rename = "0.0.1")]
V1(SerializableStackSpec),
}
impl VersionedStackSpec {
pub fn into_latest(self) -> SerializableStackSpec {
match self {
VersionedStackSpec::V1(spec) => spec,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "ast_version")]
pub enum VersionedStreamSpec {
#[serde(rename = "0.0.1")]
V1(SerializableStreamSpec),
}
impl VersionedStreamSpec {
pub fn into_latest(self) -> SerializableStreamSpec {
match self {
VersionedStreamSpec::V1(spec) => spec,
}
}
}
pub fn detect_ast_version(json: &str) -> Result<String, VersionedLoadError> {
let raw: Value =
serde_json::from_str(json).map_err(|e| VersionedLoadError::InvalidJson(e.to_string()))?;
Ok(raw
.get("ast_version")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "0.0.1".to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_stack_spec_v1() {
let json = r#"
{
"ast_version": "0.0.1",
"stack_name": "TestStack",
"program_ids": [],
"idls": [],
"entities": [],
"pdas": {},
"instructions": []
}
"#;
let result = load_stack_spec(json);
assert!(result.is_ok());
let spec = result.unwrap();
assert_eq!(spec.stack_name, "TestStack");
assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
}
#[test]
fn test_load_stack_spec_no_version_defaults_to_v1() {
let json = r#"
{
"stack_name": "TestStack",
"program_ids": [],
"idls": [],
"entities": [],
"pdas": {},
"instructions": []
}
"#;
let result = load_stack_spec(json);
assert!(result.is_ok());
let spec = result.unwrap();
assert_eq!(spec.stack_name, "TestStack");
assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
}
#[test]
fn test_load_stack_spec_unsupported_version() {
let json = r#"
{
"ast_version": "99.0.0",
"stack_name": "TestStack",
"program_ids": [],
"idls": [],
"entities": [],
"pdas": {},
"instructions": []
}
"#;
let result = load_stack_spec(json);
assert!(result.is_err());
match result.unwrap_err() {
VersionedLoadError::UnsupportedVersion(v) => assert_eq!(v, "99.0.0"),
_ => panic!("Expected UnsupportedVersion error"),
}
}
#[test]
fn test_load_stream_spec_v1() {
let json = r#"
{
"ast_version": "0.0.1",
"state_name": "TestEntity",
"identity": {"primary_keys": ["id"], "lookup_indexes": []},
"handlers": [],
"sections": [],
"field_mappings": {},
"resolver_hooks": [],
"instruction_hooks": [],
"resolver_specs": [],
"computed_fields": [],
"computed_field_specs": [],
"views": []
}
"#;
let result = load_stream_spec(json);
assert!(result.is_ok());
let spec = result.unwrap();
assert_eq!(spec.state_name, "TestEntity");
assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
}
#[test]
fn test_load_stream_spec_no_version_defaults_to_v1() {
let json = r#"
{
"state_name": "TestEntity",
"identity": {"primary_keys": ["id"], "lookup_indexes": []},
"handlers": [],
"sections": [],
"field_mappings": {},
"resolver_hooks": [],
"instruction_hooks": [],
"resolver_specs": [],
"computed_fields": [],
"computed_field_specs": [],
"views": []
}
"#;
let result = load_stream_spec(json);
assert!(result.is_ok());
let spec = result.unwrap();
assert_eq!(spec.state_name, "TestEntity");
assert_eq!(spec.ast_version, CURRENT_AST_VERSION);
}
#[test]
fn test_load_stream_spec_unsupported_version() {
let json = r#"
{
"ast_version": "99.0.0",
"state_name": "TestEntity",
"identity": {"primary_keys": ["id"], "lookup_indexes": []},
"handlers": [],
"sections": [],
"field_mappings": {},
"resolver_hooks": [],
"instruction_hooks": [],
"resolver_specs": [],
"computed_fields": [],
"computed_field_specs": [],
"views": []
}
"#;
let result = load_stream_spec(json);
assert!(result.is_err());
match result.unwrap_err() {
VersionedLoadError::UnsupportedVersion(v) => assert_eq!(v, "99.0.0"),
_ => panic!("Expected UnsupportedVersion error"),
}
}
#[test]
fn test_detect_ast_version() {
let json = r#"{"ast_version": "0.0.1", "stack_name": "Test"}"#;
assert_eq!(detect_ast_version(json).unwrap(), "0.0.1");
let json_no_version = r#"{"stack_name": "Test"}"#;
assert_eq!(detect_ast_version(json_no_version).unwrap(), "0.0.1");
}
#[test]
fn test_ast_version_sync_with_macros() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let macros_types_path = std::path::Path::new(&manifest_dir)
.join("..") .join("hyperstack-macros")
.join("src")
.join("ast")
.join("types.rs");
assert!(
macros_types_path.exists(),
"Cannot find hyperstack-macros source file at {:?}. \
This test requires the source tree to be available.",
macros_types_path
);
let content = std::fs::read_to_string(¯os_types_path)
.expect("Failed to read hyperstack-macros/src/ast/types.rs");
let version_line = content
.lines()
.find(|line| line.contains("pub const CURRENT_AST_VERSION"))
.expect("CURRENT_AST_VERSION not found in hyperstack-macros");
let version_str = version_line
.split('=')
.nth(1)
.and_then(|rhs| rhs.split('"').nth(1))
.expect("Failed to parse version string");
assert_eq!(
version_str, CURRENT_AST_VERSION,
"AST version mismatch! interpreter has '{}', hyperstack-macros has '{}'. \
Both crates must have the same CURRENT_AST_VERSION. \
Update both files when bumping the version.",
CURRENT_AST_VERSION, version_str
);
}
}