use std::fmt::Display;
#[repr(u16)]
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NodeKind {
User = 0,
SocialAuth = 1,
Namespace = 2,
Package = 3,
PackageVersion = 4,
PackageCollaborator = 5,
PackageCollaboratorInvite = 6,
NativeExecutable = 7,
PackageVersionNPMBinding = 8,
PackageVersionPythonBinding = 9,
PackageTransferRequest = 10,
Interface = 11,
InterfaceVersion = 12,
PublicKey = 13,
UserNotification = 14,
ActivityEvent = 15,
NamespaceCollaborator = 16,
NamespaceCollaboratorInvite = 17,
BindingsGenerator = 18,
DeployConfigVersion = 19,
DeployConfigInfo = 20,
DeployApp = 21,
DeployAppVersion = 22,
Waitlist = 23,
WaitlistMember = 24,
CardPaymentMethod = 25,
PaymentIntent = 26,
AppAlias = 27,
Nonce = 28,
TermsOfService = 29,
}
impl NodeKind {
pub fn from_num(x: u64) -> Option<Self> {
match x {
0 => Some(Self::User),
1 => Some(Self::SocialAuth),
2 => Some(Self::Namespace),
3 => Some(Self::Package),
4 => Some(Self::PackageVersion),
5 => Some(Self::PackageCollaborator),
6 => Some(Self::PackageCollaboratorInvite),
7 => Some(Self::NativeExecutable),
8 => Some(Self::PackageVersionNPMBinding),
9 => Some(Self::PackageVersionPythonBinding),
10 => Some(Self::PackageTransferRequest),
11 => Some(Self::Interface),
12 => Some(Self::InterfaceVersion),
13 => Some(Self::PublicKey),
14 => Some(Self::UserNotification),
15 => Some(Self::ActivityEvent),
16 => Some(Self::NamespaceCollaborator),
17 => Some(Self::NamespaceCollaboratorInvite),
18 => Some(Self::BindingsGenerator),
19 => Some(Self::DeployConfigVersion),
20 => Some(Self::DeployConfigInfo),
21 => Some(Self::DeployApp),
22 => Some(Self::DeployAppVersion),
23 => Some(Self::Waitlist),
24 => Some(Self::WaitlistMember),
25 => Some(Self::CardPaymentMethod),
26 => Some(Self::PaymentIntent),
27 => Some(Self::AppAlias),
28 => Some(Self::Nonce),
29 => Some(Self::TermsOfService),
_ => None,
}
}
pub fn parse_prefix(s: &str) -> Option<NodeKind> {
match s {
"u" => Some(Self::User),
"su" => Some(Self::SocialAuth),
"ns" => Some(Self::Namespace),
"pk" => Some(Self::Package),
"pkv" => Some(Self::PackageVersion),
"pc" => Some(Self::PackageCollaborator),
"pci" => Some(Self::PackageCollaboratorInvite),
"ne" => Some(Self::NativeExecutable),
"pkvbjs" => Some(Self::PackageVersionNPMBinding),
"pkvbpy" => Some(Self::PackageVersionPythonBinding),
"pt" => Some(Self::PackageTransferRequest),
"in" => Some(Self::Interface),
"inv" => Some(Self::InterfaceVersion),
"pub" => Some(Self::PublicKey),
"nt" => Some(Self::UserNotification),
"ae" => Some(Self::ActivityEvent),
"nsc" => Some(Self::NamespaceCollaborator),
"nsci" => Some(Self::NamespaceCollaboratorInvite),
"bg" => Some(Self::BindingsGenerator),
"dcv" => Some(Self::DeployConfigVersion),
"dci" => Some(Self::DeployConfigInfo),
"da" => Some(Self::DeployApp),
"dav" => Some(Self::DeployAppVersion),
"wl" => Some(Self::Waitlist),
"wlm" => Some(Self::WaitlistMember),
"cpm" => Some(Self::CardPaymentMethod),
"pi" => Some(Self::PaymentIntent),
"daa" => Some(Self::AppAlias),
"nnc" => Some(Self::Nonce),
"tos" => Some(Self::TermsOfService),
_ => None,
}
}
fn as_prefix(&self) -> &'static str {
match self {
Self::User => "u",
Self::SocialAuth => "su",
Self::Namespace => "ns",
Self::Package => "pk",
Self::PackageVersion => "pkv",
Self::PackageCollaborator => "pc",
Self::PackageCollaboratorInvite => "pci",
Self::NativeExecutable => "ne",
Self::PackageVersionNPMBinding => "pkvbjs",
Self::PackageVersionPythonBinding => "pkvbpy",
Self::PackageTransferRequest => "pt",
Self::Interface => "in",
Self::InterfaceVersion => "inv",
Self::PublicKey => "pub",
Self::UserNotification => "nt",
Self::ActivityEvent => "ae",
Self::NamespaceCollaborator => "nsc",
Self::NamespaceCollaboratorInvite => "nsci",
Self::BindingsGenerator => "bg",
Self::DeployConfigVersion => "dcv",
Self::DeployConfigInfo => "dci",
Self::DeployApp => "da",
Self::DeployAppVersion => "dav",
Self::Waitlist => "wl",
Self::WaitlistMember => "wlm",
Self::CardPaymentMethod => "cpm",
Self::PaymentIntent => "pi",
Self::AppAlias => "daa",
Self::Nonce => "nnc",
Self::TermsOfService => "tos",
}
}
}
impl Display for NodeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::User => "User",
Self::SocialAuth => "SocialAuth",
Self::Namespace => "Namespace",
Self::Package => "Package",
Self::PackageVersion => "PackageVersion",
Self::PackageCollaborator => "PackageCollaborator",
Self::PackageCollaboratorInvite => "PackageCollaboratorInvite",
Self::NativeExecutable => "NativeExecutable",
Self::PackageVersionNPMBinding => "PackageVersionNPMBinding",
Self::PackageVersionPythonBinding => "PackageVersionPythonBinding",
Self::PackageTransferRequest => "PackageTransferRequest",
Self::Interface => "Interface",
Self::InterfaceVersion => "InterfaceVersion",
Self::PublicKey => "PublicKey",
Self::UserNotification => "UserNotification",
Self::ActivityEvent => "ActivityEvent",
Self::NamespaceCollaborator => "NamespaceCollaborator",
Self::NamespaceCollaboratorInvite => "NamespaceCollaboratorInvite",
Self::BindingsGenerator => "BindingsGenerator",
Self::DeployConfigVersion => "DeployConfigVersion",
Self::DeployConfigInfo => "DeployConfigInfo",
Self::DeployApp => "DeployApp",
Self::DeployAppVersion => "DeployAppVersion",
Self::Waitlist => "Waitlist",
Self::WaitlistMember => "WaitlistMember",
Self::CardPaymentMethod => "CardPaymentMethod",
Self::PaymentIntent => "PaymentIntent",
Self::AppAlias => "AppAlias",
Self::Nonce => "Nonce",
Self::TermsOfService => "TermsOfService",
};
write!(f, "{name}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GlobalId {
kind: NodeKind,
database_id: u64,
}
impl GlobalId {
const SALT: &'static str = "wasmer salt hashid";
const MIN_LENGTH: usize = 12;
const ALPHABET_PREFIXED: &'static str =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
const ALPHABET_URL: &'static str = "abcdefghijklmnopqrstuvwxyz0123456789";
pub fn new(kind: NodeKind, database_id: u64) -> Self {
Self { kind, database_id }
}
fn build_harsh(alphabet: &str, salt: &[u8]) -> harsh::Harsh {
harsh::HarshBuilder::new()
.alphabet(alphabet.as_bytes())
.salt(salt)
.length(GlobalId::MIN_LENGTH)
.build()
.unwrap()
}
fn build_harsh_prefixed() -> harsh::Harsh {
Self::build_harsh(Self::ALPHABET_PREFIXED, Self::SALT.as_bytes())
}
fn build_harsh_url() -> harsh::Harsh {
Self::build_harsh(Self::ALPHABET_URL, Self::SALT.as_bytes())
}
pub fn kind(&self) -> NodeKind {
self.kind
}
pub fn database_id(&self) -> u64 {
self.database_id
}
pub fn encode_prefixed(&self) -> String {
let hash = Self::build_harsh_prefixed().encode(&[
1,
2,
self.kind as u64,
self.database_id,
]);
format!("{}_{}", self.kind.as_prefix(), hash)
}
fn parse_values(values: &[u64]) -> Result<Self, ErrorKind> {
let scope = values.first().cloned().ok_or(ErrorKind::MissingScope)?;
if scope != 1 {
return Err(ErrorKind::UnknownScope(scope));
}
let version = values.get(1).cloned().ok_or(ErrorKind::MissingVersion)?;
if version != 2 {
return Err(ErrorKind::UnknownVersion(version));
}
let ty_raw = values.get(2).cloned().ok_or(ErrorKind::MissingNodeType)?;
let ty_parsed = NodeKind::from_num(ty_raw).ok_or(ErrorKind::UnknownNodeType(ty_raw))?;
let db_id = values.get(3).cloned().ok_or(ErrorKind::MissingDatabaseId)?;
Ok(Self {
kind: ty_parsed,
database_id: db_id,
})
}
pub fn parse_prefixed(hash: &str) -> Result<Self, GlobalIdParseError> {
let (prefix, value) = hash
.split_once('_')
.ok_or_else(|| GlobalIdParseError::new(hash, ErrorKind::MissingPrefix))?;
if prefix.is_empty() {
return Err(GlobalIdParseError::new(hash, ErrorKind::MissingPrefix));
}
let ty_prefix = NodeKind::parse_prefix(prefix).ok_or_else(|| {
GlobalIdParseError::new(hash, ErrorKind::UnknownPrefix(prefix.to_string()))
})?;
let values = Self::build_harsh_prefixed()
.decode(value)
.map_err(|err| GlobalIdParseError::new(hash, ErrorKind::Decode(err.to_string())))?;
let s = Self::parse_values(&values).map_err(|kind| GlobalIdParseError::new(hash, kind))?;
if ty_prefix != s.kind {
return Err(GlobalIdParseError::new(hash, ErrorKind::PrefixTypeMismatch));
}
Ok(s)
}
pub fn encode_url(&self) -> String {
Self::build_harsh_url().encode(&[
1,
2,
self.kind as u64,
self.database_id,
])
}
pub fn parse_url(hash: &str) -> Result<Self, GlobalIdParseError> {
let values = Self::build_harsh_url()
.decode(hash)
.map_err(|err| GlobalIdParseError::new(hash, ErrorKind::Decode(err.to_string())))?;
Self::parse_values(&values).map_err(|kind| GlobalIdParseError::new(hash, kind))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobalIdParseError {
id: String,
kind: ErrorKind,
}
impl GlobalIdParseError {
fn new(id: impl Into<String>, kind: ErrorKind) -> Self {
Self {
id: id.into(),
kind,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
enum ErrorKind {
MissingPrefix,
UnknownPrefix(String),
PrefixTypeMismatch,
MissingScope,
UnknownScope(u64),
MissingVersion,
UnknownVersion(u64),
MissingNodeType,
UnknownNodeType(u64),
MissingDatabaseId,
Decode(String),
}
impl std::fmt::Display for GlobalIdParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "could not parse global id '{}': ", self.id)?;
match &self.kind {
ErrorKind::UnknownPrefix(p) => {
write!(f, "unknown type prefix '{}'", p)
}
ErrorKind::Decode(s) => {
write!(f, "decode error: {}", s)
}
ErrorKind::MissingScope => {
write!(f, "missing scope value")
}
ErrorKind::UnknownScope(x) => {
write!(f, "unknown scope value {}", x)
}
ErrorKind::MissingVersion => {
write!(f, "missing version value")
}
ErrorKind::UnknownVersion(v) => {
write!(f, "unknown version value {}", v)
}
ErrorKind::UnknownNodeType(t) => {
write!(f, "unknown node type '{}'", t)
}
ErrorKind::MissingPrefix => write!(f, "missing prefix"),
ErrorKind::PrefixTypeMismatch => write!(f, "prefix type mismatch"),
ErrorKind::MissingNodeType => write!(f, "missing node type"),
ErrorKind::MissingDatabaseId => write!(f, "missing database id"),
}
}
}
impl std::error::Error for GlobalIdParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_global_id() {
let x1 = GlobalId {
kind: NodeKind::DeployApp,
database_id: 123,
};
assert_eq!(Ok(x1), GlobalId::parse_prefixed(&x1.encode_prefixed()),);
assert_eq!(Ok(x1), GlobalId::parse_url(&x1.encode_url()));
assert_eq!(
GlobalId::parse_prefixed("da_MRrWI0t5U582"),
Ok(GlobalId {
kind: NodeKind::DeployApp,
database_id: 273,
})
);
assert_eq!(
GlobalId::parse_prefixed("oOtQIDI7q").err().unwrap().kind,
ErrorKind::MissingPrefix,
);
assert_eq!(
GlobalId::parse_prefixed("oOtQIDI7q").err().unwrap().kind,
ErrorKind::MissingPrefix,
);
assert_eq!(
GlobalId::parse_prefixed("_oOtQIDI7q").err().unwrap().kind,
ErrorKind::MissingPrefix,
);
assert_eq!(
GlobalId::parse_prefixed("lala_oOtQIDI7q")
.err()
.unwrap()
.kind,
ErrorKind::UnknownPrefix("lala".to_string()),
);
let kind = GlobalId::parse_prefixed("da_xxx").err().unwrap().kind;
assert!(matches!(kind, ErrorKind::Decode(_)));
}
#[test]
fn test_global_id_parse_values() {
assert_eq!(GlobalId::parse_values(&[]), Err(ErrorKind::MissingScope),);
assert_eq!(
GlobalId::parse_values(&[2]),
Err(ErrorKind::UnknownScope(2)),
);
assert_eq!(GlobalId::parse_values(&[1]), Err(ErrorKind::MissingVersion),);
assert_eq!(
GlobalId::parse_values(&[1, 999]),
Err(ErrorKind::UnknownVersion(999)),
);
assert_eq!(
GlobalId::parse_values(&[1, 2]),
Err(ErrorKind::MissingNodeType),
);
assert_eq!(
GlobalId::parse_values(&[1, 2, 99999]),
Err(ErrorKind::UnknownNodeType(99999)),
);
assert_eq!(
GlobalId::parse_values(&[1, 2, 1]),
Err(ErrorKind::MissingDatabaseId),
);
assert_eq!(
GlobalId::parse_values(&[1, 2, 1, 1]),
Ok(GlobalId {
kind: NodeKind::SocialAuth,
database_id: 1,
}),
);
}
}