kcode-kweb-db 0.1.0

A convergent signed-DAG store for Kweb nodes and objects
Documentation
use crate::{Error, Result};
use ed25519_dalek::SigningKey;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use sha2::{Digest, Sha256};
use std::{fmt, str::FromStr};

macro_rules! id_type {
    ($name:ident, $length:expr) => {
        #[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
        pub struct $name(pub [u8; $length]);

        impl fmt::Debug for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                fmt::Display::fmt(self, formatter)
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str(&hex::encode(self.0))
            }
        }

        impl FromStr for $name {
            type Err = Error;

            fn from_str(value: &str) -> Result<Self> {
                if value.len() != $length * 2
                    || !value
                        .bytes()
                        .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
                {
                    return Err(Error::invalid_input(concat!(
                        stringify!($name),
                        " must be exact lowercase hex"
                    )));
                }
                let bytes = hex::decode(value)
                    .map_err(|_| Error::invalid_input("identifier contains invalid hex"))?;
                let array = bytes
                    .try_into()
                    .map_err(|_| Error::invalid_input("identifier has the wrong length"))?;
                Ok(Self(array))
            }
        }

        impl Serialize for $name {
            fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
            where
                S: Serializer,
            {
                serializer.serialize_str(&self.to_string())
            }
        }

        impl<'de> Deserialize<'de> for $name {
            fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
            where
                D: Deserializer<'de>,
            {
                let value = String::deserialize(deserializer)?;
                value.parse().map_err(de::Error::custom)
            }
        }
    };
}

id_type!(NodeId, 6);
id_type!(ObjectId, 6);
id_type!(TransactionId, 32);
id_type!(WriterId, 32);

impl NodeId {
    pub(crate) fn random() -> Self {
        Self(rand::random())
    }
}

impl ObjectId {
    pub(crate) fn random() -> Self {
        Self(rand::random())
    }
}

impl TransactionId {
    pub(crate) fn for_signed_bytes(bytes: &[u8]) -> Self {
        let mut hash = Sha256::new();
        hash.update(b"kcode-kweb-db transaction v1\0");
        hash.update(bytes);
        Self(hash.finalize().into())
    }
}

impl WriterId {
    pub fn from_signing_key(signing_key: &[u8; 32]) -> Self {
        let signing_key = SigningKey::from_bytes(signing_key);
        Self(signing_key.verifying_key().to_bytes())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn text_is_exact_lowercase_hex() {
        let id = NodeId([0, 1, 2, 10, 254, 255]);
        assert_eq!(id.to_string(), "0001020afeff");
        assert_eq!(id.to_string().parse::<NodeId>().unwrap(), id);
        assert!("0001020AFEFF".parse::<NodeId>().is_err());
        assert!("0012".parse::<NodeId>().is_err());
    }
}