Skip to main content

poly_kv/
digest_compat.rs

1//! Digest type abstraction that compiles with or without `stack-ids`.
2//!
3//! When `typed-ids` is active, `Digest` is `stack_ids::ContentDigest`.
4//! When inactive, `Digest` is a local newtype wrapping `String`.
5//!
6//! Both paths expose the same method signatures so callers don't need
7//! cfg-gates at every call site.
8
9#[cfg(feature = "typed-ids")]
10pub use stack_ids::ContentDigest as Digest;
11
12#[cfg(feature = "typed-ids")]
13impl From<stack_ids::DigestError> for crate::error::PolyKvError {
14    fn from(e: stack_ids::DigestError) -> Self {
15        crate::error::PolyKvError::Internal(format!("digest error: {:?}", e))
16    }
17}
18
19/// Unified wrapper around `ContentDigest::compute_json` that returns
20/// `Result<Digest, PolyKvError>` regardless of feature configuration.
21/// Call this instead of `Digest::compute_json` directly to avoid
22/// `DigestError` → `PolyKvError` conversion issues.
23#[cfg(feature = "typed-ids")]
24pub fn compute_json<T: serde::Serialize>(value: &T) -> crate::error::Result<Digest> {
25    Digest::compute_json(value).map_err(crate::error::PolyKvError::from)
26}
27
28/// Unified wrapper around `ContentDigest::compute` (raw bytes).
29#[cfg(feature = "typed-ids")]
30pub fn compute(data: &[u8]) -> Digest {
31    Digest::compute(data)
32}
33
34/// Unified wrapper around `ContentDigest::compute_str`.
35#[cfg(feature = "typed-ids")]
36pub fn compute_str(data: &str) -> Digest {
37    Digest::compute_str(data)
38}
39
40// ── Fallback (no typed-ids) ────────────────────────────────────────
41
42#[cfg(not(feature = "typed-ids"))]
43#[derive(
44    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
45)]
46#[serde(transparent)]
47pub struct Digest(pub String);
48
49#[cfg(not(feature = "typed-ids"))]
50impl Digest {
51    pub fn compute(data: &[u8]) -> Self {
52        Self(blake3::hash(data).to_hex().to_string())
53    }
54
55    pub fn compute_str(data: &str) -> Self {
56        Self::compute(data.as_bytes())
57    }
58
59    pub fn compute_json<T: serde::Serialize>(value: &T) -> crate::error::Result<Self> {
60        internal_compute_json(value)
61    }
62
63    pub fn hex(&self) -> &str {
64        &self.0
65    }
66
67    pub fn from_hex_unchecked(hex: impl Into<String>) -> Self {
68        Self(hex.into())
69    }
70}
71
72/// Unified wrapper (same signature as the typed-ids path).
73#[cfg(not(feature = "typed-ids"))]
74pub fn compute_json<T: serde::Serialize>(value: &T) -> crate::error::Result<Digest> {
75    internal_compute_json(value)
76}
77
78#[cfg(not(feature = "typed-ids"))]
79pub fn compute(data: &[u8]) -> Digest {
80    Digest::compute(data)
81}
82
83#[cfg(not(feature = "typed-ids"))]
84pub fn compute_str(data: &str) -> Digest {
85    Digest::compute_str(data)
86}
87
88#[cfg(not(feature = "typed-ids"))]
89fn internal_compute_json<T: serde::Serialize>(value: &T) -> crate::error::Result<Digest> {
90    let json = serde_json::to_string(value)
91        .map_err(|e| crate::error::PolyKvError::Internal(format!("digest json: {}", e)))?;
92    // Canonicalize JSON key ordering for determinism
93    let val: serde_json::Value = serde_json::from_str(&json)
94        .map_err(|e| crate::error::PolyKvError::Internal(format!("digest parse: {}", e)))?;
95    let canonical = canonicalize_json_value(val);
96    let canonical_str = serde_json::to_string(&canonical)
97        .map_err(|e| crate::error::PolyKvError::Internal(format!("digest canonicalize: {}", e)))?;
98    Ok(Digest(
99        blake3::hash(canonical_str.as_bytes()).to_hex().to_string(),
100    ))
101}
102
103#[cfg(not(feature = "typed-ids"))]
104fn canonicalize_json_value(value: serde_json::Value) -> serde_json::Value {
105    match value {
106        serde_json::Value::Object(map) => {
107            let mut entries: Vec<(String, serde_json::Value)> = map
108                .into_iter()
109                .map(|(k, v)| (k, canonicalize_json_value(v)))
110                .collect();
111            entries.sort_by(|a, b| a.0.cmp(&b.0));
112            let mut ordered = serde_json::Map::new();
113            for (k, v) in entries {
114                ordered.insert(k, v);
115            }
116            serde_json::Value::Object(ordered)
117        }
118        serde_json::Value::Array(items) => {
119            serde_json::Value::Array(items.into_iter().map(canonicalize_json_value).collect())
120        }
121        other => other,
122    }
123}