jacquardSnapshot

← snapshot

7249 bytes
//! Identifier newtypes for people, agents, organisations, and registry events.
//!
//! Every identifier is a `repr(transparent)` wrapper over [`NonZeroU64`]. The
//! niche means `Option<HumanId>` occupies eight bytes with no discriminant.
//!
//! Identifiers are typed rather than interchangeable `u64`s: passing an
//! [`AgentId`] where a [`HumanId`] is expected is a compile error, which is
//! not a convenience here — the attestation invariant (only a human can
//! attest) leans on exactly this distinction.
//!
//! Objects in the version DAG are *not* identified by these sequential ids;
//! they are identified by content digests. See [`crate::oid`].

use core::fmt;
use core::num::NonZeroU64;
use core::str::FromStr;

use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::error::CoreError;

/// Maximum rendered length of an identifier: prefix, hyphen, 16 hex digits.
const ID_BUF: usize = 24;

macro_rules! define_id {
    ($(#[$meta:meta])* $name:ident, $prefix:literal) => {
        $(#[$meta])*
        #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
        #[repr(transparent)]
        pub struct $name(NonZeroU64);

        impl $name {
            /// Textual prefix used when rendering this identifier.
            pub const PREFIX: &'static str = $prefix;

            /// Smallest representable identifier.
            pub const MIN: Self = Self(NonZeroU64::MIN);

            /// Constructs an identifier, rejecting zero.
            ///
            /// # Errors
            ///
            /// Returns [`CoreError::ZeroIdentifier`] if `raw` is zero.
            pub const fn new(raw: u64) -> Result<Self, CoreError> {
                match NonZeroU64::new(raw) {
                    Some(v) => Ok(Self(v)),
                    None => Err(CoreError::ZeroIdentifier),
                }
            }

            /// Constructs an identifier from a value known to be non-zero.
            #[must_use]
            pub const fn from_non_zero(raw: NonZeroU64) -> Self {
                Self(raw)
            }

            /// Returns the underlying value.
            #[must_use]
            pub const fn get(self) -> u64 {
                self.0.get()
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "{}-{:016x}", $prefix, self.0.get())
            }
        }

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

        impl FromStr for $name {
            type Err = CoreError;

            fn from_str(s: &str) -> Result<Self, Self::Err> {
                let body = s
                    .strip_prefix(concat!($prefix, "-"))
                    .ok_or(CoreError::IdentifierPrefix { expected: $prefix })?;
                let raw =
                    u64::from_str_radix(body, 16).map_err(|_| CoreError::IdentifierBody)?;
                Self::new(raw)
            }
        }

        impl Serialize for $name {
            fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
                if s.is_human_readable() {
                    // Rendered into a stack buffer: registry records serialise
                    // in bulk and this path must not allocate.
                    let mut buf = [0u8; ID_BUF];
                    let n = render(&mut buf, $prefix, self.0.get());
                    s.serialize_str(core::str::from_utf8(&buf[..n]).unwrap_or($prefix))
                } else {
                    s.serialize_u64(self.0.get())
                }
            }
        }

        impl<'de> Deserialize<'de> for $name {
            fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
                struct V;
                impl Visitor<'_> for V {
                    type Value = $name;

                    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                        write!(f, "a `{}-` prefixed identifier or a non-zero u64", $prefix)
                    }

                    fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<$name, E> {
                        <$name>::from_str(v).map_err(E::custom)
                    }

                    fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<$name, E> {
                        <$name>::new(v).map_err(E::custom)
                    }
                }
                if d.is_human_readable() { d.deserialize_str(V) } else { d.deserialize_u64(V) }
            }
        }
    };
}

/// Writes `prefix-<16 hex digits>` into `buf`, returning the number of bytes
/// written.
fn render(buf: &mut [u8; ID_BUF], prefix: &str, value: u64) -> usize {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let p = prefix.as_bytes();
    let mut n = 0;
    for &b in p {
        buf[n] = b;
        n += 1;
    }
    buf[n] = b'-';
    n += 1;
    for shift in (0..16).rev() {
        let nibble = (value >> (shift * 4)) & 0x0f;
        buf[n] = HEX[nibble as usize];
        n += 1;
    }
    n
}

define_id!(
    /// Identifies a person.
    ///
    /// The only identity an attestation can be minted from (see the
    /// `jac-decision` crate). Deliberately a different type from [`AgentId`]:
    /// the distinction carries the attestation invariant.
    HumanId,
    "hum"
);

define_id!(
    /// Identifies a coding agent or model.
    ///
    /// Agents may author snapshots and propose decisions. No code path turns
    /// an `AgentId` into an attestation.
    AgentId,
    "agt"
);

define_id!(
    /// Identifies an organisation participating in the rendezvous registry.
    OrgId,
    "org"
);

define_id!(
    /// Identifies one published sketch in the rendezvous registry.
    PublicationId,
    "pub"
);

define_id!(
    /// Identifies a brokered introduction between two organisations.
    ///
    /// The token both parties receive. The registry never carries content
    /// between them; introduction happens out of band.
    IntroductionId,
    "intro"
);

#[cfg(test)]
#[expect(
    clippy::unwrap_used,
    reason = "test assertions read better with unwrap"
)]
mod tests {
    use super::*;

    #[test]
    fn option_is_niche_optimised() {
        assert_eq!(size_of::<HumanId>(), 8);
        assert_eq!(size_of::<Option<HumanId>>(), 8);
        assert_eq!(size_of::<Option<AgentId>>(), 8);
    }

    #[test]
    fn zero_is_rejected() {
        assert_eq!(HumanId::new(0), Err(CoreError::ZeroIdentifier));
    }

    #[test]
    fn display_roundtrips() {
        let id = HumanId::new(42).unwrap();
        assert_eq!(id.to_string(), "hum-000000000000002a");
        assert_eq!(HumanId::from_str("hum-000000000000002a").unwrap(), id);
    }

    #[test]
    fn prefixes_are_not_interchangeable() {
        assert!(AgentId::from_str("hum-000000000000002a").is_err());
    }

    #[test]
    fn json_uses_the_prefixed_form() {
        let id = OrgId::new(255).unwrap();
        let json = serde_json::to_string(&id).unwrap();
        assert_eq!(json, "\"org-00000000000000ff\"");
        assert_eq!(serde_json::from_str::<OrgId>(&json).unwrap(), id);
    }
}