#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "pq", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "pq", serde(transparent))]
pub struct Oid(String);
impl Oid {
pub fn new(s: impl Into<String>) -> Self {
Oid(s.into())
}
pub fn hash(bytes: &[u8]) -> Self {
let hex_str = crate::coincidence::canonical_hash(bytes);
Oid(hex_str)
}
pub fn dark() -> Self {
Oid("0".repeat(64))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_dark(&self) -> bool {
self.0 == "0".repeat(64)
}
}
impl std::fmt::Display for Oid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::fmt::Debug for Oid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0.len() >= 12 {
write!(f, "Oid({})", &self.0[..12])
} else {
write!(f, "Oid({})", &self.0)
}
}
}
impl AsRef<str> for Oid {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<&str> for Oid {
fn from(s: &str) -> Self {
Oid(s.to_owned())
}
}
impl From<String> for Oid {
fn from(s: String) -> Self {
Oid(s)
}
}
pub trait Addressable {
fn oid(&self) -> Oid;
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn new_and_as_str() {
let oid = Oid::new("abc123");
assert_eq!(oid.as_str(), "abc123");
}
#[test]
fn display() {
let oid = Oid::new("hello");
assert_eq!(format!("{}", oid), "hello");
}
#[test]
fn equality() {
let a = Oid::new("foo");
let b = Oid::new("foo");
let c = Oid::new("bar");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn ordering() {
let a = Oid::new("apple");
let b = Oid::new("banana");
assert!(a < b);
assert!(b > a);
}
#[test]
fn from_str() {
let oid: Oid = "test".into();
assert_eq!(oid.as_str(), "test");
}
#[test]
fn from_string() {
let oid: Oid = String::from("owned").into();
assert_eq!(oid.as_str(), "owned");
}
#[test]
fn as_ref() {
let oid = Oid::new("reftest");
let s: &str = oid.as_ref();
assert_eq!(s, "reftest");
}
#[test]
fn hash_set_dedup() {
let mut set = HashSet::new();
set.insert(Oid::new("x"));
set.insert(Oid::new("x"));
set.insert(Oid::new("y"));
assert_eq!(set.len(), 2);
}
#[test]
fn clone() {
let a = Oid::new("original");
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn oid_from_bytes() {
let a = Oid::hash(b"hello");
let b = Oid::hash(b"hello");
assert_eq!(a, b); }
#[test]
fn oid_different_content_different_hash() {
let a = Oid::hash(b"hello");
let b = Oid::hash(b"world");
assert_ne!(a, b);
}
#[test]
fn oid_display_is_hex() {
let oid = Oid::hash(b"test");
let s = format!("{}", oid);
assert_eq!(s.len(), 64); assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn oid_dark_is_constant() {
assert_eq!(Oid::dark(), Oid::dark());
}
#[test]
fn oid_dark_differs_from_any_content() {
assert_ne!(Oid::dark(), Oid::hash(b""));
assert_ne!(Oid::dark(), Oid::hash(b"anything"));
}
struct TestValue(u32);
impl Addressable for TestValue {
fn oid(&self) -> Oid {
Oid::hash(&self.0.to_le_bytes())
}
}
#[test]
fn addressable_impl() {
let a = TestValue(42);
let b = TestValue(42);
let c = TestValue(99);
assert_eq!(a.oid(), b.oid());
assert_ne!(a.oid(), c.oid());
}
#[test]
fn oid_hash_is_coincidence_detector() {
let oid = Oid::hash(b"hello");
let s = oid.as_str();
assert_eq!(s.len(), 64, "must be 64 hex chars");
assert!(s.chars().all(|c| c.is_ascii_hexdigit()), "must be hex");
assert_eq!(oid, Oid::hash(b"hello"));
assert_ne!(oid, Oid::hash(b"world"));
}
#[test]
fn oid_hash_empty_bytes_fallback() {
let oid = Oid::hash(b"");
let s = oid.as_str();
assert_eq!(s.len(), 64, "fallback must produce 64 hex chars");
assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(oid, Oid::hash(b""), "fallback must be deterministic");
assert_ne!(oid, Oid::dark(), "fallback must differ from dark");
}
#[test]
fn oid_hash_cross_version_stable() {
let oid = Oid::hash(b"prism");
assert_eq!(
oid.as_str(),
"08f8e91d230c49a5072202e4e82db8306e226d83f77aa6f57d05dc87b56efc1e",
"hash of b\"prism\" must match pinned CoincidenceHash<3> value"
);
}
}