80 lines
2.2 KiB
Rust
80 lines
2.2 KiB
Rust
use std::{fmt, str::FromStr};
|
|
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use thiserror::Error;
|
|
|
|
/// A lowercase SHA-256 digest.
|
|
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, JsonSchema)]
|
|
#[serde(transparent)]
|
|
pub struct Digest(String);
|
|
|
|
impl Digest {
|
|
/// Returns the canonical hexadecimal spelling.
|
|
#[must_use]
|
|
pub fn as_str(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl FromStr for Digest {
|
|
type Err = IdentifierError;
|
|
|
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
|
return Err(IdentifierError::Digest);
|
|
}
|
|
Ok(Self(value.to_ascii_lowercase()))
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Digest {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter.write_str(&self.0)
|
|
}
|
|
}
|
|
|
|
/// A random 128-bit invocation identifier encoded as 32 lowercase hex digits.
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
|
#[serde(transparent)]
|
|
pub struct InvocationId(String);
|
|
|
|
impl InvocationId {
|
|
/// Parses a canonical invocation identifier.
|
|
pub fn parse(value: &str) -> Result<Self, IdentifierError> {
|
|
if value.len() != 32
|
|
|| !value
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
|
{
|
|
return Err(IdentifierError::Invocation);
|
|
}
|
|
Ok(Self(value.to_owned()))
|
|
}
|
|
}
|
|
|
|
/// Identifier validation failure.
|
|
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
|
|
pub enum IdentifierError {
|
|
/// SHA-256 spelling is invalid.
|
|
#[error("digest must contain exactly 64 hexadecimal characters")]
|
|
Digest,
|
|
/// Invocation identifier spelling is invalid.
|
|
#[error("invocation ID must contain exactly 32 lowercase hexadecimal characters")]
|
|
Invocation,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::expect_used)]
|
|
mod tests {
|
|
use std::str::FromStr as _;
|
|
|
|
use super::Digest;
|
|
|
|
#[test]
|
|
fn digest_accepts_hex_and_canonicalizes_case() {
|
|
let digest = Digest::from_str(&"A".repeat(64)).expect("valid digest");
|
|
assert_eq!(digest.as_str(), "a".repeat(64));
|
|
assert!(Digest::from_str(&"a".repeat(63)).is_err());
|
|
}
|
|
}
|