use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::fmt;
use std::ops::Deref;
macro_rules! string_newtype {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Serialize,
Deserialize,
)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Deref for $name {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Borrow<str> for $name {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for $name {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&$name> for $name {
fn from(value: &$name) -> Self {
value.clone()
}
}
impl From<$name> for String {
fn from(value: $name) -> Self {
value.0
}
}
impl From<&$name> for String {
fn from(value: &$name) -> Self {
value.as_str().to_string()
}
}
impl PartialEq<&str> for $name {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<str> for $name {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<$name> for &str {
fn eq(&self, other: &$name) -> bool {
*self == other.as_str()
}
}
impl PartialEq<$name> for str {
fn eq(&self, other: &$name) -> bool {
self == other.as_str()
}
}
impl PartialEq<String> for $name {
fn eq(&self, other: &String) -> bool {
self.as_str() == other.as_str()
}
}
impl PartialEq<$name> for String {
fn eq(&self, other: &$name) -> bool {
self.as_str() == other.as_str()
}
}
impl PartialEq<&$name> for $name {
fn eq(&self, other: &&$name) -> bool {
self == *other
}
}
};
}
string_newtype!(
FactId
);
string_newtype!(
ProposalId
);
string_newtype!(
ObservationId
);
string_newtype!(
ApprovalId
);
string_newtype!(
ArtifactId
);
string_newtype!(
GateId
);
string_newtype!(
ActorId
);
string_newtype!(
ValidationCheckId
);
string_newtype!(
TraceId
);
string_newtype!(
SpanId
);
string_newtype!(
TraceSystemId
);
string_newtype!(
TraceReference
);
string_newtype!(
PrincipalId
);
string_newtype!(
EventId
);
string_newtype!(
TenantId
);
string_newtype!(
CorrelationId
);
string_newtype!(
ChainId
);
string_newtype!(
TraceLinkId
);
string_newtype!(
BackendId
);
string_newtype!(
PackId
);
string_newtype!(
TruthId
);
string_newtype!(
PolicyId
);
string_newtype!(
ApprovalPointId
);
string_newtype!(
CriterionId
);
string_newtype!(
ConstraintName
);
string_newtype!(
ConstraintValue
);
string_newtype!(
DomainId
);
string_newtype!(
PolicyVersionId
);
string_newtype!(
ResourceId
);
string_newtype!(
ResourceKind
);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ContentHash(#[serde(with = "hex_bytes")] [u8; 32]);
impl ContentHash {
#[must_use]
pub fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub fn from_hex(hex: &str) -> Self {
let mut bytes = [0u8; 32];
hex::decode_to_slice(hex, &mut bytes).expect("invalid hex string");
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
#[must_use]
pub fn zero() -> Self {
Self([0u8; 32])
}
}
impl Default for ContentHash {
fn default() -> Self {
Self::zero()
}
}
impl fmt::Display for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
mod hex_bytes {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex::encode(bytes))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
let mut bytes = [0u8; 32];
hex::decode_to_slice(raw, &mut bytes).map_err(serde::de::Error::custom)?;
Ok(bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Timestamp(String);
impl Timestamp {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn epoch() -> Self {
Self::new("1970-01-01T00:00:00Z")
}
#[must_use]
pub fn now() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
Self(format!("{}Z", duration.as_secs()))
}
}
impl Deref for Timestamp {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl AsRef<str> for Timestamp {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<&str> for Timestamp {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for Timestamp {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<Timestamp> for String {
fn from(value: Timestamp) -> Self {
value.0
}
}
impl From<&Timestamp> for String {
fn from(value: &Timestamp) -> Self {
value.as_str().to_string()
}
}
impl PartialEq<&str> for Timestamp {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<str> for Timestamp {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<Timestamp> for &str {
fn eq(&self, other: &Timestamp) -> bool {
*self == other.as_str()
}
}
impl PartialEq<Timestamp> for str {
fn eq(&self, other: &Timestamp) -> bool {
self == other.as_str()
}
}
impl PartialEq<String> for Timestamp {
fn eq(&self, other: &String) -> bool {
self.as_str() == other.as_str()
}
}
impl PartialEq<Timestamp> for String {
fn eq(&self, other: &Timestamp) -> bool {
self.as_str() == other.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_newtypes_compare_like_strings_without_erasing_type_identity() {
let fact_id = FactId::new("fact-1");
let proposal_id = ProposalId::new("fact-1");
assert_eq!(fact_id, "fact-1");
assert_eq!("fact-1", fact_id);
assert_ne!(fact_id.to_string(), "");
assert_ne!(fact_id.as_str(), "");
assert_eq!(proposal_id.as_str(), "fact-1");
}
#[test]
fn content_hash_hex_roundtrip() {
let hash = ContentHash::from_hex(
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
);
assert_eq!(
hash.to_hex(),
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
);
}
#[test]
fn timestamp_is_transparent() {
let timestamp = Timestamp::epoch();
let json = serde_json::to_string(×tamp).expect("timestamp should serialize");
assert_eq!(json, r#""1970-01-01T00:00:00Z""#);
}
}