use sha2::{Digest, Sha256};
use tracing::{info, warn};
use super::schema::CompiledSchema;
use crate::error::FraiseQLError;
#[must_use]
pub fn canonicalize_json(value: &serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => {
let mut sorted = serde_json::Map::new();
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
sorted.insert(key.clone(), canonicalize_json(&map[key]));
}
serde_json::Value::Object(sorted)
},
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.iter().map(canonicalize_json).collect())
},
other => other.clone(),
}
}
impl CompiledSchema {
pub fn from_json(
json: &str,
strict_integrity: bool,
) -> std::result::Result<Self, FraiseQLError> {
let serde_err = |e: serde_json::Error| FraiseQLError::Parse {
message: format!("Schema JSON parse error: {e}"),
location: String::new(),
};
let mut value: serde_json::Value = serde_json::from_str(json).map_err(serde_err)?;
let obj = value.as_object_mut().ok_or_else(|| FraiseQLError::Validation {
message: "Schema JSON must be an object".to_string(),
path: None,
})?;
let expected_hash = if let Some(hash_val) = obj.remove("_content_hash") {
if let Some(hash_str) = hash_val.as_str() {
Some(hash_str.to_string())
} else {
return Err(FraiseQLError::Validation {
message: "_content_hash must be a string".to_string(),
path: None,
});
}
} else if strict_integrity {
return Err(FraiseQLError::Validation {
message: "Schema integrity check failed: missing _content_hash field. Enable strict_schema_integrity=false for backwards compatibility.".to_string(),
path: None,
});
} else {
warn!(
"Schema integrity check skipped: no _content_hash field present. Consider recompiling with a newer CLI for integrity verification."
);
let mut schema: Self = serde_json::from_str(json).map_err(serde_err)?;
schema.build_indexes();
return Ok(schema);
};
let canonical = canonicalize_json(&value);
let remaining_json = serde_json::to_string_pretty(&canonical).map_err(serde_err)?;
let computed_digest = Sha256::digest(remaining_json.as_bytes());
let computed_hash = hex::encode(&computed_digest[..16]);
if let Some(expected) = expected_hash {
if expected != computed_hash {
if strict_integrity {
return Err(FraiseQLError::Validation {
message: format!(
"Schema integrity check failed: hash mismatch (expected {expected}, got {computed_hash})"
),
path: None,
});
}
warn!(
"Schema integrity check: hash mismatch (expected {expected}, got {computed_hash}). Proceeding because strict_integrity is disabled."
);
} else {
info!("Schema integrity verified: hash matches");
}
}
let mut schema: Self = serde_json::from_str(&remaining_json).map_err(serde_err)?;
schema.build_indexes();
Ok(schema)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
#[must_use]
pub fn content_hash(&self) -> String {
use sha2::{Digest, Sha256};
let json = self.to_json().expect("CompiledSchema always serialises — BUG if this fails");
let digest = Sha256::digest(json.as_bytes());
hex::encode(&digest[..16]) }
}