use core::fmt;
use core::num::{NonZeroU64, NonZeroU128};
use lgwks_std::hash::{Digest, Hasher};
use lgwks_std::wire::{AlignedVec, WireError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum IdError {
WrongLength {
len: usize,
},
NotHex {
at: usize,
},
Zero,
}
impl fmt::Display for IdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::WrongLength { len } => {
write!(f, "identifier must be 32 hex characters, got {len}")
}
Self::NotHex { at } => {
write!(f, "identifier has a non-lowercase-hex byte at offset {at}")
}
Self::Zero => f.write_str("the all-zero identifier is not a valid identifier"),
}
}
}
impl std::error::Error for IdError {}
#[cfg(feature = "ephemeral")]
#[derive(Debug)]
#[non_exhaustive]
pub enum MintError {
Entropy(lgwks_std::random::EntropyError),
Zero,
}
#[cfg(feature = "ephemeral")]
impl fmt::Display for MintError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Entropy(ref cause) => write!(f, "could not mint an identifier: {cause}"),
Self::Zero => f.write_str("the entropy source returned the all-zero identifier"),
}
}
}
#[cfg(feature = "ephemeral")]
impl std::error::Error for MintError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
Self::Entropy(ref cause) => Some(cause),
Self::Zero => None,
}
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
lgwks_std::wire::Archive,
lgwks_std::wire::Serialize,
lgwks_std::wire::Deserialize,
)]
#[rkyv(crate = lgwks_std::wire::rkyv, compare(PartialEq), derive(Debug))]
pub struct Id128(NonZeroU128);
impl Id128 {
#[must_use]
pub const fn from_nonzero(value: NonZeroU128) -> Self {
Self(value)
}
pub fn from_hex(text: &str) -> Result<Self, IdError> {
if text.len() != 32 {
let refusal = Err(IdError::WrongLength { len: text.len() });
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "from_hex: returning an error to the caller");
return refusal;
}
if let Some(at) = text
.bytes()
.position(|byte| !matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
{
let refusal = Err(IdError::NotHex { at });
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "from_hex: returning an error to the caller");
return refusal;
}
let raw = match u128::from_str_radix(text, 16) {
Ok(raw) => raw,
Err(_not_hex) => {
let refusal = Err(IdError::WrongLength { len: 32 });
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "from_hex: returning an error to the caller");
return refusal;
}
};
match NonZeroU128::new(raw) {
Some(value) => Ok(Self(value)),
None => Err(IdError::Zero),
}
}
#[must_use]
pub const fn get(self) -> NonZeroU128 {
self.0
}
#[must_use]
pub fn to_hex(self) -> String {
lgwks_std::hex::encode(self.0.get().to_be_bytes())
}
#[cfg(feature = "ephemeral")]
pub(crate) fn mint() -> Result<Self, MintError> {
let raw = lgwks_std::random::bytes::<16>().map_err(MintError::Entropy)?;
match NonZeroU128::new(u128::from_be_bytes(raw)) {
Some(value) => Ok(Self(value)),
None => Err(MintError::Zero),
}
}
}
impl fmt::Display for Id128 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
macro_rules! id_role {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(
lgwks_std::wire::Archive,
lgwks_std::wire::Serialize,
lgwks_std::wire::Deserialize,
)]
#[rkyv(crate = lgwks_std::wire::rkyv, compare(PartialEq), derive(Debug))]
pub struct $name(Id128);
impl $name {
#[must_use]
pub const fn new(id: Id128) -> Self {
Self(id)
}
pub fn from_hex(text: &str) -> Result<Self, IdError> {
Ok(Self(Id128::from_hex(text)?))
}
#[must_use]
pub const fn id(self) -> Id128 {
self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
};
}
id_role! {
RunId
}
id_role! {
ActionId
}
id_role! {
EnvironmentId
}
#[cfg(feature = "ephemeral")]
impl RunId {
pub fn mint() -> Result<Self, MintError> {
Ok(Self(Id128::mint()?))
}
}
#[cfg(feature = "ephemeral")]
impl EnvironmentId {
pub fn mint() -> Result<Self, MintError> {
Ok(Self(Id128::mint()?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CounterError {
NotDecimal,
Zero,
Overflow,
}
impl fmt::Display for CounterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
Self::NotDecimal => "counter must be a minimal base-10 unsigned integer",
Self::Zero => "counter must be at least 1",
Self::Overflow => "counter exceeds u64::MAX",
})
}
}
impl std::error::Error for CounterError {}
macro_rules! counter_role {
($(#[$meta:meta])* $name:ident, $what:literal) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(
lgwks_std::wire::Archive,
lgwks_std::wire::Serialize,
lgwks_std::wire::Deserialize,
)]
#[rkyv(crate = lgwks_std::wire::rkyv, compare(PartialEq), derive(Debug))]
pub struct $name(NonZeroU64);
impl $name {
pub const FIRST: Self = Self(NonZeroU64::MIN);
#[must_use]
pub const fn new(value: NonZeroU64) -> Self {
Self(value)
}
pub fn from_decimal(text: &str) -> Result<Self, CounterError> {
if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(CounterError::NotDecimal);
}
let raw: u64 = text.parse().map_err(|_| CounterError::Overflow)?;
match NonZeroU64::new(raw) {
Some(value) => Ok(Self(value)),
None => Err(CounterError::Zero),
}
}
#[must_use]
pub const fn get(self) -> u64 {
self.0.get()
}
#[must_use]
pub const fn checked_next(self) -> Option<Self> {
match self.0.checked_add(1) {
Some(next) => Some(Self(next)),
None => None,
}
}
#[must_use]
pub const fn is_exhausted(self) -> bool {
self.0.get() == u64::MAX
}
#[must_use]
pub const fn role() -> &'static str {
$what
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.get())
}
}
};
}
counter_role! {
AttemptId,
"attempt"
}
counter_role! {
EnvironmentEpoch,
"environment epoch"
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DigestAlgorithm {
Blake3_256,
Sha256,
}
impl DigestAlgorithm {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Blake3_256 => "blake3_256",
Self::Sha256 => "sha256",
}
}
#[must_use]
pub fn from_wire(text: &str) -> Option<Self> {
match text {
"blake3_256" => Some(Self::Blake3_256),
"sha256" => Some(Self::Sha256),
_ => None,
}
}
}
impl fmt::Display for DigestAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DigestError {
UnknownAlgorithm,
UnsupportedAlgorithm {
algorithm: DigestAlgorithm,
},
Malformed(lgwks_std::hash::DigestParseError),
}
impl fmt::Display for DigestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::UnknownAlgorithm => f.write_str("digest algorithm is not one the schema defines"),
Self::UnsupportedAlgorithm { algorithm } => write!(
f,
"digest algorithm {algorithm} is not computable or verifiable here; \
this crate accepts blake3_256"
),
Self::Malformed(ref cause) => write!(f, "{cause}"),
}
}
}
impl std::error::Error for DigestError {}
macro_rules! digest_role {
($(#[$meta:meta])* $name:ident, $what:literal) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(
lgwks_std::wire::Archive,
lgwks_std::wire::Serialize,
lgwks_std::wire::Deserialize,
)]
#[rkyv(crate = lgwks_std::wire::rkyv, compare(PartialEq), derive(Debug))]
pub struct $name(Digest);
impl $name {
#[must_use]
pub const fn new(digest: Digest) -> Self {
Self(digest)
}
#[must_use]
pub const fn digest(self) -> Digest {
self.0
}
#[must_use]
pub const fn algorithm() -> DigestAlgorithm {
DigestAlgorithm::Blake3_256
}
pub fn from_tagged(algorithm: &str, hex: &str) -> Result<Self, DigestError> {
let tag =
DigestAlgorithm::from_wire(algorithm).ok_or(DigestError::UnknownAlgorithm)?;
if tag != DigestAlgorithm::Blake3_256 {
return Err(DigestError::UnsupportedAlgorithm { algorithm: tag });
}
Ok(Self(Digest::from_hex(hex).map_err(DigestError::Malformed)?))
}
#[must_use]
pub fn to_hex(self) -> String {
self.0.to_hex()
}
#[must_use]
pub const fn role() -> &'static str {
$what
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
};
}
digest_role! {
FlowRevision,
"flow revision"
}
digest_role! {
ActionDigest,
"action digest"
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum EffectKeyError {
Id(IdError),
Counter(CounterError),
Digest(DigestError),
}
impl fmt::Display for EffectKeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Id(cause) => write!(f, "{cause}"),
Self::Counter(cause) => write!(f, "{cause}"),
Self::Digest(ref cause) => write!(f, "{cause}"),
}
}
}
impl std::error::Error for EffectKeyError {}
impl From<IdError> for EffectKeyError {
fn from(cause: IdError) -> Self {
Self::Id(cause)
}
}
impl From<CounterError> for EffectKeyError {
fn from(cause: CounterError) -> Self {
Self::Counter(cause)
}
}
impl From<DigestError> for EffectKeyError {
fn from(cause: DigestError) -> Self {
Self::Digest(cause)
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
lgwks_std::wire::Archive,
lgwks_std::wire::Serialize,
lgwks_std::wire::Deserialize,
)]
#[rkyv(crate = lgwks_std::wire::rkyv, compare(PartialEq), derive(Debug))]
pub struct EffectKey {
run: RunId,
action: ActionId,
attempt: AttemptId,
flow: FlowRevision,
digest: ActionDigest,
environment: EnvironmentId,
epoch: EnvironmentEpoch,
}
impl EffectKey {
#[must_use]
pub const fn run(self) -> RunId {
self.run
}
#[must_use]
pub const fn action(self) -> ActionId {
self.action
}
#[must_use]
pub const fn attempt(self) -> AttemptId {
self.attempt
}
#[must_use]
pub const fn flow(self) -> FlowRevision {
self.flow
}
#[must_use]
pub const fn digest(self) -> ActionDigest {
self.digest
}
#[must_use]
pub const fn environment(self) -> EnvironmentId {
self.environment
}
#[must_use]
pub const fn epoch(self) -> EnvironmentEpoch {
self.epoch
}
#[must_use]
pub const fn with_attempt(self, attempt: AttemptId) -> Self {
Self { attempt, ..self }
}
#[must_use]
pub fn same_attempt_earlier_epoch(self, other: Self) -> bool {
self.run == other.run
&& self.action == other.action
&& self.attempt == other.attempt
&& self.flow == other.flow
&& self.digest == other.digest
&& self.environment == other.environment
&& self.epoch.get() < other.epoch.get()
}
pub fn to_bytes(self) -> Result<AlignedVec, WireError> {
lgwks_std::wire::to_bytes::<WireError>(&self)
}
}
impl fmt::Display for EffectKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"run {} action {} attempt {} flow {} digest {} env {} epoch {}",
self.run,
self.action,
self.attempt,
self.flow,
self.digest,
self.environment,
self.epoch
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EffectIdentity {
run: RunId,
environment: EnvironmentId,
flow: FlowRevision,
}
#[cfg(feature = "ephemeral")]
const EPHEMERAL_FLOW: &[u8] = b"lgwks.effect.v1.ephemeral-flow";
impl EffectIdentity {
#[must_use]
pub const fn new(run: RunId, environment: EnvironmentId, flow: FlowRevision) -> Self {
Self {
run,
environment,
flow,
}
}
#[cfg(feature = "ephemeral")]
pub(crate) fn ephemeral() -> Result<Self, MintError> {
Ok(Self {
run: RunId::mint()?,
environment: EnvironmentId::mint()?,
flow: FlowRevision::new(lgwks_std::hash::blake3(EPHEMERAL_FLOW)),
})
}
#[must_use]
pub const fn run(self) -> RunId {
self.run
}
#[must_use]
pub const fn environment(self) -> EnvironmentId {
self.environment
}
#[must_use]
pub const fn flow(self) -> FlowRevision {
self.flow
}
#[must_use]
pub const fn key(
self,
action: ActionId,
attempt: AttemptId,
digest: ActionDigest,
epoch: EnvironmentEpoch,
) -> EffectKey {
EffectKey {
run: self.run,
action,
attempt,
flow: self.flow,
digest,
environment: self.environment,
epoch,
}
}
#[must_use]
pub const fn at_epoch(self, key: EffectKey, epoch: EnvironmentEpoch) -> EffectKey {
EffectIdentity::new(key.run(), self.environment, key.flow()).key(
key.action(),
key.attempt(),
key.digest(),
epoch,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
const RUN: &str = "0102030405060708090a0b0c0d0e0f10";
const ACTION: &str = "1112131415161718191a1b1c1d1e1f20";
const ENV: &str = "2122232425262728292a2b2c2d2e2f30";
const FLOW_HEX: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
const DIGEST_HEX: &str = "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeef";
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn nonzero(value: u64) -> Result<NonZeroU64, CounterError> {
NonZeroU64::new(value).ok_or(CounterError::Zero)
}
#[cfg(feature = "ephemeral")]
#[test]
fn the_all_zero_mint_error_names_the_value_it_refused() {
let rendered = MintError::Zero.to_string();
assert!(
rendered.contains("all-zero"),
"an operator reading this cannot tell what was refused: {rendered}"
);
assert!(
std::error::Error::source(&MintError::Zero).is_none(),
"a refusal with no cause must not claim one"
);
}
fn key(attempt: u64, epoch: u64) -> Result<EffectKey, EffectKeyError> {
let key_run = RunId::from_hex(RUN)?;
let key_action = ActionId::from_hex(ACTION)?;
let key_attempt = AttemptId::new(nonzero(attempt)?);
let key_flow_revision = FlowRevision::from_tagged("blake3_256", FLOW_HEX)?;
let key_digest = ActionDigest::from_tagged("blake3_256", DIGEST_HEX)?;
let key_environment = EnvironmentId::from_hex(ENV)?;
let key_epoch = EnvironmentEpoch::new(nonzero(epoch)?);
Ok(
EffectIdentity::new(key_run, key_environment, key_flow_revision).key(
key_action,
key_attempt,
key_digest,
key_epoch,
),
)
}
#[test]
fn id_round_trips_through_its_hex_form() -> TestResult {
let id = Id128::from_hex(RUN)?;
assert_eq!(id.to_hex(), RUN);
Ok(())
}
#[test]
fn id_round_trips_a_value_with_the_high_bit_set() -> TestResult {
let text = "ffffffffffffffffffffffffffffffff";
let id = Id128::from_hex(text)?;
assert_eq!(id.to_hex(), text);
assert_eq!(id.get().get(), u128::MAX);
Ok(())
}
#[test]
fn id_rejects_the_all_zero_value() {
assert_eq!(
Id128::from_hex("00000000000000000000000000000000"),
Err(IdError::Zero)
);
}
#[test]
fn id_rejects_uppercase_rather_than_folding_it() -> TestResult {
let upper = RUN.to_uppercase();
match Id128::from_hex(&upper) {
Err(IdError::NotHex { at }) => {
assert!(
upper.as_bytes()[at].is_ascii_uppercase(),
"offset {at} does not name the uppercase byte"
);
}
other => return Err(format!("expected a NotHex refusal, got {other:?}").into()),
}
Ok(())
}
#[test]
fn id_reports_the_observed_length_not_a_rebased_offset() {
assert_eq!(
Id128::from_hex("abcd"),
Err(IdError::WrongLength { len: 4 })
);
}
#[test]
fn id_rejects_a_non_ascii_input_without_panicking() {
let text = "é".repeat(16);
assert_eq!(text.len(), 32);
assert_eq!(Id128::from_hex(&text), Err(IdError::NotHex { at: 0 }));
}
#[test]
fn id_accepts_a_nonzero_value_by_construction() {
let id = Id128::from_nonzero(NonZeroU128::MIN);
assert_eq!(id.get().get(), 1);
assert_eq!(id.to_hex(), "00000000000000000000000000000001");
}
#[test]
fn every_id_role_parses_the_same_wire_form() -> TestResult {
let run = RunId::from_hex(RUN)?;
let action = ActionId::from_hex(RUN)?;
let environment = EnvironmentId::from_hex(RUN)?;
assert_eq!(run.id(), action.id());
assert_eq!(action.id(), environment.id());
assert_eq!(run.to_string(), RUN);
Ok(())
}
#[test]
fn attempt_rejects_zero_and_non_decimal() {
assert_eq!(AttemptId::from_decimal("0"), Err(CounterError::Zero));
assert_eq!(AttemptId::from_decimal(""), Err(CounterError::NotDecimal));
assert_eq!(AttemptId::from_decimal("1a"), Err(CounterError::NotDecimal));
assert_eq!(AttemptId::from_decimal("-1"), Err(CounterError::NotDecimal));
assert_eq!(
AttemptId::from_decimal("18446744073709551616"),
Err(CounterError::Overflow)
);
assert_eq!(AttemptId::from_decimal("42").map(AttemptId::get), Ok(42));
}
#[test]
fn attempt_refuses_to_wrap_rather_than_reusing_an_identity() {
let last = AttemptId::new(NonZeroU64::MAX);
assert!(last.is_exhausted());
assert_eq!(last.checked_next(), None);
assert_eq!(AttemptId::FIRST.get(), 1);
assert_eq!(AttemptId::FIRST.checked_next().map(AttemptId::get), Some(2));
}
#[test]
fn the_two_counter_roles_keep_their_own_names() {
assert_eq!(AttemptId::role(), "attempt");
assert_eq!(EnvironmentEpoch::role(), "environment epoch");
assert_eq!(EnvironmentEpoch::FIRST.to_string(), "1");
}
#[test]
fn a_sha256_tag_is_unsupported_rather_than_malformed() {
assert!(matches!(
ActionDigest::from_tagged("sha256", DIGEST_HEX),
Err(DigestError::UnsupportedAlgorithm {
algorithm: DigestAlgorithm::Sha256
})
));
assert!(matches!(
ActionDigest::from_tagged("md5", DIGEST_HEX),
Err(DigestError::UnknownAlgorithm)
));
}
#[test]
fn a_malformed_digest_body_is_distinct_from_an_unsupported_tag() {
assert!(matches!(
ActionDigest::from_tagged("blake3_256", "nothex"),
Err(DigestError::Malformed(
lgwks_std::hash::DigestParseError::WrongLength { len: 6 }
))
));
}
#[test]
fn the_two_digest_roles_carry_the_same_algorithm_tag() {
assert_eq!(FlowRevision::algorithm(), DigestAlgorithm::Blake3_256);
assert_eq!(ActionDigest::algorithm().as_str(), "blake3_256");
assert_eq!(FlowRevision::role(), "flow revision");
assert_eq!(ActionDigest::role(), "action digest");
}
#[test]
fn a_key_round_trips_every_field() -> TestResult {
let bound = key(3, 7)?;
assert_eq!(bound.run().to_string(), RUN);
assert_eq!(bound.action().to_string(), ACTION);
assert_eq!(bound.attempt().get(), 3);
assert_eq!(bound.flow().to_hex(), FLOW_HEX);
assert_eq!(bound.digest().to_hex(), DIGEST_HEX);
assert_eq!(bound.environment().to_string(), ENV);
assert_eq!(bound.epoch().get(), 7);
Ok(())
}
#[test]
fn with_attempt_moves_only_the_attempt() -> TestResult {
let base = key(1, 4)?;
let next = base.with_attempt(AttemptId::new(nonzero(2)?));
assert_ne!(base, next);
assert_eq!(base.run(), next.run());
assert_eq!(base.action(), next.action());
assert_eq!(base.flow(), next.flow());
assert_eq!(base.digest(), next.digest());
assert_eq!(base.environment(), next.environment());
assert_eq!(base.epoch(), next.epoch());
assert_eq!(next.attempt().get(), 2);
Ok(())
}
#[test]
fn an_epoch_bump_under_one_command_is_recognised() -> TestResult {
let prepared = key(1, 4)?;
let after_restart = key(1, 5)?;
assert!(prepared.same_attempt_earlier_epoch(after_restart));
assert!(!after_restart.same_attempt_earlier_epoch(prepared));
Ok(())
}
#[test]
fn a_different_input_digest_is_a_different_key_and_not_an_epoch_move() -> TestResult {
let base = key(1, 4)?;
let moved = EffectIdentity::new(base.run(), base.environment(), base.flow()).key(
base.action(),
base.attempt(),
ActionDigest::from_tagged(
"blake3_256",
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e2f",
)?,
EnvironmentEpoch::new(nonzero(5)?),
);
assert_ne!(base.digest(), moved.digest());
assert!(!base.same_attempt_earlier_epoch(moved));
Ok(())
}
#[test]
fn a_key_renders_every_field_for_a_refusal_message() -> TestResult {
let rendered = key(2, 9)?.to_string();
assert!(rendered.contains(RUN), "{rendered}");
assert!(rendered.contains("attempt 2"), "{rendered}");
assert!(rendered.contains("epoch 9"), "{rendered}");
assert!(rendered.contains("env "), "{rendered}");
Ok(())
}
}
pub trait InputIdentity {
const SCHEMA_ID: &'static [u8];
fn write_identity(&self, hasher: &mut Hasher);
fn names_an_event(&self) -> bool {
false
}
}
macro_rules! input_identity_int {
($($t:ty => $name:literal),* $(,)?) => {$(
impl InputIdentity for $t {
const SCHEMA_ID: &'static [u8] =
concat!("lgwks.bot.schema.v1.int-", $name).as_bytes();
fn write_identity(&self, hasher: &mut Hasher) {
hasher.update(&self.to_le_bytes());
}
}
)*};
}
input_identity_int!(
u8 => "u8",
u16 => "u16",
u32 => "u32",
u64 => "u64",
i8 => "i8",
i16 => "i16",
i32 => "i32",
i64 => "i64",
);
impl InputIdentity for bool {
const SCHEMA_ID: &'static [u8] = b"lgwks.bot.schema.v1.bool";
fn write_identity(&self, hasher: &mut Hasher) {
hasher.update(&[u8::from(*self)]);
}
}
impl InputIdentity for String {
const SCHEMA_ID: &'static [u8] = b"lgwks.bot.schema.v1.string";
fn write_identity(&self, hasher: &mut Hasher) {
hasher.write_framed(self.as_bytes());
}
}
impl InputIdentity for &str {
const SCHEMA_ID: &'static [u8] = b"lgwks.bot.schema.v1.string";
fn write_identity(&self, hasher: &mut Hasher) {
hasher.write_framed(self.as_bytes());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct EventId<T> {
id: u64,
value: T,
}
impl<T> EventId<T> {
#[must_use]
pub const fn new(id: u64, value: T) -> Self {
Self { id, value }
}
#[must_use]
pub const fn id(&self) -> u64 {
self.id
}
#[must_use]
pub const fn value(&self) -> &T {
&self.value
}
}
impl<T: InputIdentity> InputIdentity for EventId<T> {
const SCHEMA_ID: &'static [u8] = b"lgwks.bot.schema.v1.event-id";
fn write_identity(&self, hasher: &mut Hasher) {
hasher.write_framed(T::SCHEMA_ID);
hasher.update(&self.id.to_le_bytes());
self.value.write_identity(hasher);
}
fn names_an_event(&self) -> bool {
true
}
}