Skip to main content

northroot_proof_engine/
data_shape.rs

1//! Data shape hashing for v0.2
2//!
3//! This module provides functions for computing data shape hashes.
4//! In v0.2, this is a minimal implementation that hashes the schema
5//! and optional sketches.
6
7use std::collections::BTreeMap;
8use thiserror::Error;
9
10use crate::hash::compute_sha256;
11use crate::types::Sha256;
12use crate::validation::validate_sha256_format;
13
14/// Error returned when data shape operations fail
15#[derive(Debug, Error, PartialEq, Eq)]
16pub enum DataShapeError {
17    /// Serialization error
18    #[error("serialization error: {0}")]
19    Serialization(String),
20
21    /// Invalid hash format
22    #[error("invalid hash format: {0}")]
23    InvalidFormat(String),
24}
25
26/// Computes the data shape hash from a schema and optional sketches.
27///
28/// In v0.2, this is a minimal implementation:
29/// - If sketches are provided: `H(JCS({schema, sketches}))`
30/// - Otherwise: `H(JCS({schema}))`
31///
32/// The schema and sketches are canonicalized using BTreeMap for deterministic ordering.
33pub fn compute_data_shape_hash(
34    schema: &serde_json::Value,
35    sketches: Option<&serde_json::Value>,
36) -> Result<Sha256, DataShapeError> {
37    let mut map = BTreeMap::new();
38    map.insert("schema", schema.clone());
39
40    if let Some(sketches_val) = sketches {
41        map.insert("sketches", sketches_val.clone());
42    }
43
44    let canonical = serde_json::to_string(&serde_json::Value::Object(
45        map.into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
46    ))
47    .map_err(|e| DataShapeError::Serialization(e.to_string()))?;
48
49    Ok(compute_sha256(&canonical))
50}
51
52/// Validates that a data shape hash has the correct format.
53///
54/// This is a convenience wrapper around `validate_sha256_format`.
55pub fn validate_data_shape_hash(hash: &str) -> Result<(), DataShapeError> {
56    validate_sha256_format(hash).map_err(|e| DataShapeError::InvalidFormat(e.to_string()))
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_compute_data_shape_hash_schema_only() {
65        let schema = serde_json::json!({
66            "type": "object",
67            "properties": {
68                "name": {"type": "string"},
69                "age": {"type": "number"}
70            }
71        });
72
73        let hash1 = compute_data_shape_hash(&schema, None).unwrap();
74        let hash2 = compute_data_shape_hash(&schema, None).unwrap();
75        assert_eq!(hash1, hash2, "Must be deterministic");
76        assert!(hash1.starts_with("sha256:"));
77    }
78
79    #[test]
80    fn test_compute_data_shape_hash_with_sketches() {
81        let schema = serde_json::json!({
82            "type": "object",
83            "properties": {
84                "name": {"type": "string"}
85            }
86        });
87
88        let sketches = serde_json::json!({
89            "cardinality": 1000,
90            "null_count": 5
91        });
92
93        let hash1 = compute_data_shape_hash(&schema, Some(&sketches)).unwrap();
94        let hash2 = compute_data_shape_hash(&schema, Some(&sketches)).unwrap();
95        assert_eq!(hash1, hash2, "Must be deterministic");
96
97        // Hash with sketches should differ from hash without
98        let hash_no_sketches = compute_data_shape_hash(&schema, None).unwrap();
99        assert_ne!(hash1, hash_no_sketches, "Sketches should affect hash");
100    }
101
102    #[test]
103    fn test_validate_data_shape_hash() {
104        let valid = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
105        assert!(validate_data_shape_hash(valid).is_ok());
106
107        let invalid = "invalid";
108        assert!(validate_data_shape_hash(invalid).is_err());
109    }
110}