use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use ulid::Ulid;
use crate::frontmatter::Frontmatter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct NoteId(pub Ulid);
impl NoteId {
pub fn new() -> Self {
Self(Ulid::new())
}
pub fn timestamp_ms(&self) -> u64 {
self.0.timestamp_ms()
}
#[must_use]
pub fn derived_from(key: &[u8]) -> NoteId {
let hash = Sha256::digest(key);
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&hash[..16]);
NoteId(Ulid::from_bytes(bytes))
}
}
impl Default for NoteId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for NoteId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ContentHash(pub [u8; 32]);
impl ContentHash {
pub fn compute(frontmatter: &Frontmatter, body: &str) -> Self {
use sha2::{Digest, Sha256};
let canonical = serde_jcs::to_string(frontmatter).expect(
"Frontmatter est toujours sérialisable en JCS : \
pas de f64::NAN/INFINITY possible (TOML RFC 3.3 garantit des floats finis)",
);
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
hasher.update(b"\n---\n");
hasher.update(body.as_bytes());
ContentHash(hasher.finalize().into())
}
pub fn hex(&self) -> String {
self.0.iter().map(|b| format!("{b:02x}")).collect()
}
}
impl std::fmt::Display for ContentHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.hex())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct NoteVersion(pub u32);
impl NoteVersion {
pub fn initial() -> Self {
Self(1)
}
pub fn next(&self) -> Self {
Self(self.0.saturating_add(1))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct IntegritySignature(pub Vec<u8>);
#[cfg(test)]
mod tests_note_version {
use super::*;
#[test]
fn next_increments_by_one() {
let v = NoteVersion(5);
assert_eq!(v.next().0, 6);
}
#[test]
fn next_saturates_at_max() {
let v = NoteVersion(u32::MAX);
assert_eq!(
v.next().0,
u32::MAX,
"next() doit saturer à u32::MAX et non paniquer ou wrapper"
);
}
#[test]
fn next_at_max_minus_one_reaches_max() {
let v = NoteVersion(u32::MAX - 1);
assert_eq!(v.next().0, u32::MAX);
}
}
#[cfg(test)]
mod tests_derived_from {
use super::*;
const SEP: u8 = 0x1f;
fn build_key(parts: &[&str]) -> Vec<u8> {
let mut key = Vec::new();
for (i, part) in parts.iter().enumerate() {
if i > 0 {
key.push(SEP);
}
key.extend_from_slice(part.as_bytes());
}
key
}
#[test]
fn derived_from_deterministic() {
let key = build_key(&["code-gradatum", "src/lib.rs", "fn", "my_function"]);
let id1 = NoteId::derived_from(&key);
let id2 = NoteId::derived_from(&key);
assert_eq!(id1, id2, "même clé doit produire le même NoteId");
}
#[test]
fn derived_from_distinct_keys_produce_distinct_ids() {
let key_a = build_key(&["code-gradatum", "src/lib.rs", "fn", "func_a"]);
let key_b = build_key(&["code-gradatum", "src/lib.rs", "fn", "func_b"]);
let id_a = NoteId::derived_from(&key_a);
let id_b = NoteId::derived_from(&key_b);
assert_ne!(
id_a, id_b,
"clés différentes doivent produire des NoteIds différents"
);
}
#[test]
fn derived_from_different_paths_distinct() {
let key_a = build_key(&["code-gradatum", "src/a.rs", "fn", "helper"]);
let key_b = build_key(&["code-gradatum", "src/b.rs", "fn", "helper"]);
assert_ne!(
NoteId::derived_from(&key_a),
NoteId::derived_from(&key_b),
"même nom dans des fichiers différents = NoteIds distincts"
);
}
#[test]
fn derived_from_produces_valid_ulid_string() {
let key = build_key(&["code-test", "src/main.rs", "struct", "MyStruct"]);
let id = NoteId::derived_from(&key);
let s = id.to_string();
assert_eq!(s.len(), 26, "ULID doit faire 26 caractères, got: '{s}'");
for ch in s.chars() {
assert!(
matches!(ch, '0'..='9' | 'A'..='H' | 'J'..='N' | 'P'..='T' | 'V'..='Z'),
"caractère ULID invalide: '{ch}' dans '{s}'"
);
}
}
#[test]
fn derived_from_stable_value() {
let key = build_key(&[
"code-gradatum",
"crates/gradatum-core/src/lib.rs",
"fn",
"stable_fn",
]);
let id = NoteId::derived_from(&key);
let s = id.to_string();
assert_eq!(
s, "0EQB76CD42PNQN1YPGZK4QNNWW",
"NoteId::derived_from doit être déterministe cross-version (non-régression)"
);
}
}