use std::borrow::Borrow;
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::errors::NameError;
use crate::model::release_version::HostSeries;
const MAX_PROJECT_NAME_LEN: usize = 64;
const MAX_SERIES_NAME_LEN: usize = 64;
const MAX_TAG_NAME_LEN: usize = 128;
const MAX_TAG_GLOB_LEN: usize = 128;
const MAX_MODULE_NAME_LEN: usize = 256;
const MAX_FUNCTION_NAME_LEN: usize = 256;
const MAX_TYPE_NAME_LEN: usize = 256;
const MAX_RECORD_NAME_LEN: usize = 256;
const MAX_FIELD_NAME_LEN: usize = 256;
const MAX_APPLICATION_NAME_LEN: usize = 128;
const MAX_BEHAVIOUR_NAME_LEN: usize = 256;
const MAX_MACRO_NAME_LEN: usize = 256;
const MAX_DEPENDENCY_NAME_LEN: usize = 128;
const MAX_DEPENDENCY_VERSION_LEN: usize = 128;
const APPLICATION_PATTERN: &str = "[a-z][a-z0-9_]{0,127}";
const PROJECT_PATTERN: &str = "[a-z][a-z0-9_-]{0,63}";
const SERIES_PATTERN: &str = "[a-z][a-z0-9_.-]{0,63}";
fn is_project_name_char(c: char) -> bool {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-'
}
fn is_series_name_char(c: char) -> bool {
is_project_name_char(c) || c == '.'
}
fn is_application_name_char(c: char) -> bool {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'
}
fn validate_simple_name(
kind: &'static str,
value: &str,
max_len: usize,
first_char_ok: fn(char) -> bool,
rest_char_ok: fn(char) -> bool,
pattern: &'static str,
) -> Result<(), NameError> {
if value.is_empty() {
return Err(NameError::Empty { kind });
}
if value.len() > max_len {
return Err(NameError::TooLong {
kind,
len: value.len(),
max: max_len,
});
}
let mut chars = value.chars();
let first = chars.next().expect("non-empty");
if !first_char_ok(first) {
return Err(NameError::PatternMismatch {
kind,
value: value.to_owned(),
pattern,
});
}
for ch in chars {
if !rest_char_ok(ch) {
return Err(NameError::InvalidCharacter {
kind,
ch,
value: value.to_owned(),
});
}
}
Ok(())
}
fn validate_tag_glob(value: &str) -> Result<(), NameError> {
if value.is_empty() {
return Err(NameError::Empty { kind: "tag glob" });
}
if value.len() > MAX_TAG_GLOB_LEN {
return Err(NameError::TooLong {
kind: "tag glob",
len: value.len(),
max: MAX_TAG_GLOB_LEN,
});
}
for ch in value.chars() {
if ch.is_ascii_control()
|| ch.is_whitespace()
|| ch == '/'
|| ch == '\\'
|| ch == '\0'
|| ch == ':'
|| ch == '~'
|| ch == '^'
|| ch == '['
{
return Err(NameError::InvalidCharacter {
kind: "tag glob",
ch,
value: value.to_owned(),
});
}
}
if value == "." || value == ".." || value.starts_with('.') || value.ends_with('.') {
return Err(NameError::PatternMismatch {
kind: "tag glob",
value: value.to_owned(),
pattern: "non-empty, no path separators or git ref-magic",
});
}
Ok(())
}
fn validate_tag_name(value: &str) -> Result<(), NameError> {
if value.is_empty() {
return Err(NameError::Empty { kind: "tag" });
}
if value.len() > MAX_TAG_NAME_LEN {
return Err(NameError::TooLong {
kind: "tag",
len: value.len(),
max: MAX_TAG_NAME_LEN,
});
}
for ch in value.chars() {
if ch.is_ascii_control()
|| ch.is_whitespace()
|| ch == '/'
|| ch == '\\'
|| ch == '\0'
|| ch == ':'
|| ch == '~'
|| ch == '^'
|| ch == '?'
|| ch == '*'
|| ch == '['
{
return Err(NameError::InvalidCharacter {
kind: "tag",
ch,
value: value.to_owned(),
});
}
}
if value == "." || value == ".." || value.starts_with('.') || value.ends_with('.') {
return Err(NameError::PatternMismatch {
kind: "tag",
value: value.to_owned(),
pattern: "non-empty, no path separators or git ref-magic",
});
}
Ok(())
}
fn validate_erlang_name(kind: &'static str, value: &str, max_len: usize) -> Result<(), NameError> {
if value.is_empty() {
return Err(NameError::Empty { kind });
}
if value.len() > max_len {
return Err(NameError::TooLong {
kind,
len: value.len(),
max: max_len,
});
}
if value.starts_with('\'') && value.ends_with('\'') && value.len() >= 2 {
return Ok(());
}
let first = value.chars().next().expect("non-empty");
if !(first.is_ascii_lowercase() || first == '_') {
return Err(NameError::PatternMismatch {
kind,
value: value.to_owned(),
pattern: "lowercase-led atom or single-quoted",
});
}
let last = value.chars().last().expect("non-empty");
let drop_trailing_punct = if matches!(last, '?' | '!') {
value.len() - last.len_utf8()
} else {
value.len()
};
for ch in value[..drop_trailing_punct].chars() {
if !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '@') {
return Err(NameError::InvalidCharacter {
kind,
ch,
value: value.to_owned(),
});
}
}
Ok(())
}
fn validate_module_name(kind: &'static str, value: &str, max_len: usize) -> Result<(), NameError> {
if value.is_empty() {
return Err(NameError::Empty { kind });
}
if value.len() > max_len {
return Err(NameError::TooLong {
kind,
len: value.len(),
max: max_len,
});
}
if value.starts_with('\'') && value.ends_with('\'') && value.len() >= 2 {
return Ok(());
}
let first = value.chars().next().expect("non-empty");
if !(first.is_ascii_alphabetic() || first == '_') {
return Err(NameError::PatternMismatch {
kind,
value: value.to_owned(),
pattern: "Erlang atom or Elixir module name",
});
}
for ch in value.chars() {
if !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '@' || ch == '.') {
return Err(NameError::InvalidCharacter {
kind,
ch,
value: value.to_owned(),
});
}
}
Ok(())
}
macro_rules! string_newtype {
($name:ident, $validate:expr) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct $name(Arc<str>);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, NameError> {
let v = value.into();
$validate(&v)?;
Ok(Self(Arc::from(v)))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0.to_string()
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for $name {
fn eq(&self, other: &str) -> bool {
&*self.0 == other
}
}
impl PartialEq<&str> for $name {
fn eq(&self, other: &&str) -> bool {
&*self.0 == *other
}
}
impl Borrow<str> for $name {
fn borrow(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for $name {
type Err = NameError;
fn from_str(s: &str) -> Result<Self, NameError> {
Self::new(s.to_owned())
}
}
};
}
macro_rules! vocabulary {
(
$(#[$enum_meta:meta])*
$vis:vis enum $name:ident : $noun:literal {
$($(#[$variant_meta:meta])* $variant:ident => $label:literal),+ $(,)?
}
) => {
$(#[$enum_meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
$vis enum $name {
$($(#[$variant_meta])* #[serde(rename = $label)] $variant),+
}
impl $name {
pub const ALL: &'static [Self] = &[$(Self::$variant),+];
#[must_use]
pub const fn label(self) -> &'static str {
match self {
$(Self::$variant => $label),+
}
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
impl std::str::FromStr for $name {
type Err = crate::errors::UnknownLabel;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
$($label => Ok(Self::$variant),)+
other => Err(crate::errors::UnknownLabel::new(
$noun,
other,
Self::ALL.iter().map(|v| v.label()),
)),
}
}
}
};
}
pub(crate) use vocabulary;
string_newtype!(ProjectName, |v: &str| {
validate_simple_name(
"project name",
v,
MAX_PROJECT_NAME_LEN,
|c| c.is_ascii_lowercase(),
is_project_name_char,
PROJECT_PATTERN,
)
});
string_newtype!(SeriesName, |v: &str| {
validate_simple_name(
"series name",
v,
MAX_SERIES_NAME_LEN,
|c| c.is_ascii_lowercase(),
is_series_name_char,
SERIES_PATTERN,
)
});
string_newtype!(TagName, validate_tag_name);
const MAX_GIT_REF_LEN: usize = 256;
string_newtype!(GitRef, validate_git_ref);
fn validate_git_ref(value: &str) -> Result<(), NameError> {
if value.is_empty() {
return Err(NameError::Empty { kind: "git ref" });
}
if value.len() > MAX_GIT_REF_LEN {
return Err(NameError::TooLong {
kind: "git ref",
len: value.len(),
max: MAX_GIT_REF_LEN,
});
}
for ch in value.chars() {
if ch.is_ascii_control() || ch.is_whitespace() || ch == '\0' || ch == ':' {
return Err(NameError::InvalidCharacter {
kind: "git ref",
ch,
value: value.to_owned(),
});
}
}
Ok(())
}
string_newtype!(TagGlob, validate_tag_glob);
impl TagGlob {
pub fn matches(&self, tag: &TagName) -> bool {
glob_match(self.as_str(), tag.as_str())
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let pattern: Vec<char> = pattern.chars().collect();
let text: Vec<char> = text.chars().collect();
let mut pi = 0usize;
let mut ti = 0usize;
let mut star_p: Option<usize> = None;
let mut star_t: Option<usize> = None;
while ti < text.len() {
if pi < pattern.len() && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
pi += 1;
ti += 1;
} else if pi < pattern.len() && pattern[pi] == '*' {
star_p = Some(pi);
star_t = Some(ti);
pi += 1;
} else if let (Some(sp), Some(st)) = (star_p, star_t) {
pi = sp + 1;
star_t = Some(st + 1);
ti = st + 1;
} else {
return false;
}
}
while pi < pattern.len() && pattern[pi] == '*' {
pi += 1;
}
pi == pattern.len()
}
string_newtype!(ModuleName, |v: &str| {
validate_module_name("module name", v, MAX_MODULE_NAME_LEN)
});
string_newtype!(FunctionName, |v: &str| {
validate_erlang_name("function name", v, MAX_FUNCTION_NAME_LEN)
});
string_newtype!(TypeName, |v: &str| {
validate_erlang_name("type name", v, MAX_TYPE_NAME_LEN)
});
string_newtype!(RecordName, |v: &str| {
validate_erlang_name("record name", v, MAX_RECORD_NAME_LEN)
});
string_newtype!(FieldName, |v: &str| {
validate_erlang_name("field name", v, MAX_FIELD_NAME_LEN)
});
string_newtype!(ApplicationName, |v: &str| {
validate_simple_name(
"application name",
v,
MAX_APPLICATION_NAME_LEN,
|c| c.is_ascii_lowercase(),
is_application_name_char,
APPLICATION_PATTERN,
)
});
string_newtype!(BehaviourName, |v: &str| {
validate_module_name("behaviour name", v, MAX_BEHAVIOUR_NAME_LEN)
});
string_newtype!(MacroName, |v: &str| {
validate_macro_name("macro name", v, MAX_MACRO_NAME_LEN)
});
string_newtype!(DependencyName, |v: &str| {
validate_dependency_name("dependency name", v, MAX_DEPENDENCY_NAME_LEN)
});
string_newtype!(DependencyVersion, |v: &str| {
validate_dependency_version("dependency version", v, MAX_DEPENDENCY_VERSION_LEN)
});
const MAX_MAP_KEY_LEN: usize = 256;
const MAX_SCHEMA_FEATURE_LEN: usize = 128;
string_newtype!(MapKey, |v: &str| {
validate_erlang_name("map key", v, MAX_MAP_KEY_LEN)
});
string_newtype!(SchemaFeature, |v: &str| {
validate_simple_name(
"schema feature",
v,
MAX_SCHEMA_FEATURE_LEN,
|c| c.is_ascii_lowercase(),
is_application_name_char,
"lowercase letter, then lowercase digits underscores",
)
});
fn validate_macro_name(kind: &'static str, value: &str, max_len: usize) -> Result<(), NameError> {
validate_simple_name(
kind,
value,
max_len,
|c| c.is_ascii_alphabetic() || c == '_',
|c| c.is_ascii_alphanumeric() || c == '_',
"letter or underscore, then letters digits underscores",
)
}
const MAX_CONF_KEY_LEN: usize = 256;
const MAX_VALIDATOR_NAME_LEN: usize = 128;
const MAX_PARTIAL_NAME_LEN: usize = 128;
const MAX_MAKE_VARIABLE_NAME_LEN: usize = 128;
string_newtype!(ConfKey, |v: &str| {
validate_simple_name(
"configuration key",
v,
MAX_CONF_KEY_LEN,
|c| c.is_ascii_alphanumeric() || c == '_' || c == '$',
|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '$' | '.' | '-' | '@' | '*'),
"letters digits and _ $ . - @ *, not starting with a separator",
)
});
string_newtype!(ValidatorName, |v: &str| {
validate_simple_name(
"validator name",
v,
MAX_VALIDATOR_NAME_LEN,
|c| c.is_ascii_alphanumeric() || c == '_',
|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-' | '@'),
"letters digits and _ . - @, not starting with a separator",
)
});
string_newtype!(PartialName, |v: &str| {
validate_simple_name(
"partial name",
v,
MAX_PARTIAL_NAME_LEN,
|c| c.is_ascii_alphanumeric() || c == '_',
|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'),
"letters digits _ -, without '/' or '.'",
)
});
string_newtype!(MakeVariableName, |v: &str| {
validate_simple_name(
"make variable name",
v,
MAX_MAKE_VARIABLE_NAME_LEN,
|c| c.is_ascii_alphabetic() || c == '_',
|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-'),
"letter or underscore, then letters digits _ . -",
)
});
string_newtype!(HostFactsDigest, |v: &str| {
validate_lower_hex(
"host facts digest",
v,
BLAKE3_HEX_LEN,
"expected 64 lowercase hex characters",
)
});
string_newtype!(ExtractionSettingsDigest, |v: &str| {
validate_lower_hex(
"extraction settings digest",
v,
BLAKE3_HEX_LEN,
"expected 64 lowercase hex characters",
)
});
string_newtype!(StoreDigest, |v: &str| {
validate_lower_hex(
"store digest",
v,
BLAKE3_HEX_LEN,
"expected 64 lowercase hex characters",
)
});
string_newtype!(SpecText, |v: &str| {
if v.trim().is_empty() {
return Err(NameError::Empty { kind: "spec text" });
}
Ok(())
});
const BLAKE3_HEX_LEN: usize = 64;
fn validate_lower_hex(
kind: &'static str,
value: &str,
len: usize,
reason: &'static str,
) -> Result<(), NameError> {
if value.len() != len || !value.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')) {
return Err(NameError::Invalid {
kind,
value: value.to_owned(),
reason,
});
}
Ok(())
}
impl HostFactsDigest {
#[must_use]
pub fn from_digest(digest: [u8; 32]) -> Self {
Self(Arc::from(lower_hex(&digest)))
}
}
impl ExtractionSettingsDigest {
#[must_use]
pub fn from_digest(digest: [u8; 32]) -> Self {
Self(Arc::from(lower_hex(&digest)))
}
}
impl StoreDigest {
#[must_use]
pub fn from_digest(digest: [u8; 32]) -> Self {
Self(Arc::from(lower_hex(&digest)))
}
}
fn lower_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut hex = String::with_capacity(bytes.len() * 2);
for byte in bytes {
hex.push(HEX[usize::from(byte >> 4)] as char);
hex.push(HEX[usize::from(byte & 0x0f)] as char);
}
hex
}
string_newtype!(HostName, |v: &str| {
validate_simple_name(
"host name",
v,
MAX_PROJECT_NAME_LEN,
|c| c.is_ascii_lowercase(),
is_project_name_char,
PROJECT_PATTERN,
)
});
const MAX_EXTRACTOR_VERSION_LEN: usize = 16;
string_newtype!(ExtractorVersion, |v: &str| {
if v.is_empty() {
return Err(NameError::Empty {
kind: "extractor version",
});
}
if v.len() > MAX_EXTRACTOR_VERSION_LEN {
return Err(NameError::TooLong {
kind: "extractor version",
len: v.len(),
max: MAX_EXTRACTOR_VERSION_LEN,
});
}
match v
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_')))
{
Some(ch) => Err(NameError::InvalidCharacter {
kind: "extractor version",
ch,
value: v.to_owned(),
}),
None => Ok(()),
}
});
const SERIES_PLACEHOLDER: &str = "{series}";
string_newtype!(SeriesTagPattern, validate_series_tag_pattern);
fn validate_series_tag_pattern(value: &str) -> Result<(), NameError> {
let invalid = |reason: &'static str| NameError::Invalid {
kind: "series tag pattern",
value: value.to_owned(),
reason,
};
if value.matches(SERIES_PLACEHOLDER).count() != 1 {
return Err(invalid("expected exactly one {series} placeholder"));
}
let without = value.replacen(SERIES_PLACEHOLDER, "", 1);
if without.contains(['{', '}']) {
return Err(invalid(
"braces are only allowed in the {series} placeholder",
));
}
validate_tag_glob(&value.replacen(SERIES_PLACEHOLDER, "0.0", 1))
}
impl SeriesTagPattern {
#[must_use]
pub fn for_series(&self, series: HostSeries) -> TagGlob {
let glob = self.0.replacen(SERIES_PLACEHOLDER, &series.to_string(), 1);
TagGlob(Arc::from(glob))
}
}
const PLAN_CACHE_KEY_PREFIX: &str = "jigsaw-";
const PLAN_DIGEST_BYTES: usize = 8;
const PLAN_DIGEST_HEX_LEN: usize = PLAN_DIGEST_BYTES * 2;
const MAX_PLAN_CACHE_KEY_LEN: usize = 256;
string_newtype!(PlanCacheKey, validate_plan_cache_key);
fn validate_plan_cache_key(value: &str) -> Result<(), NameError> {
let invalid = |reason: &'static str| NameError::Invalid {
kind: "plan cache key",
value: value.to_owned(),
reason,
};
if value.len() > MAX_PLAN_CACHE_KEY_LEN {
return Err(NameError::TooLong {
kind: "plan cache key",
len: value.len(),
max: MAX_PLAN_CACHE_KEY_LEN,
});
}
if !value.starts_with(PLAN_CACHE_KEY_PREFIX) {
return Err(invalid("expected the jigsaw- prefix"));
}
if !value
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '-' | '.' | '_'))
{
return Err(invalid("expected lowercase letters, digits and - . _"));
}
let Some(split) = value.len().checked_sub(PLAN_DIGEST_HEX_LEN) else {
return Err(invalid("expected a 16 character digest suffix"));
};
let (head, digest) = value.split_at(split);
if !head.ends_with('-') || !digest.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')) {
return Err(invalid(
"expected a dash and 16 lowercase hex characters at the end",
));
}
Ok(())
}
impl PlanCacheKey {
#[must_use]
pub fn compose(
host: &HostName,
series: HostSeries,
format_version: u32,
extractor: &ExtractorVersion,
digest: [u8; PLAN_DIGEST_BYTES],
) -> Self {
let key = format!(
"{PLAN_CACHE_KEY_PREFIX}{host}-{series}-f{format_version}-e{extractor}-{}",
lower_hex(&digest)
);
Self(Arc::from(key))
}
#[must_use]
pub fn restore_prefix(&self) -> &str {
&self.0[..self.0.len() - PLAN_DIGEST_HEX_LEN]
}
}
fn validate_dependency_name(
kind: &'static str,
value: &str,
max_len: usize,
) -> Result<(), NameError> {
validate_simple_name(
kind,
value,
max_len,
|c| c.is_ascii_lowercase(),
is_application_name_char,
"lowercase letter, then lowercase digits underscores",
)
}
fn validate_dependency_version(
kind: &'static str,
value: &str,
max_len: usize,
) -> Result<(), NameError> {
if value.is_empty() {
return Err(NameError::Empty { kind });
}
if value.len() > max_len {
return Err(NameError::TooLong {
kind,
len: value.len(),
max: max_len,
});
}
for ch in value.chars() {
if ch.is_ascii_control() || ch == '\0' || ch.is_whitespace() {
return Err(NameError::InvalidCharacter {
kind,
ch,
value: value.to_owned(),
});
}
}
Ok(())
}
const MAX_RELATIVE_PATH_LEN: usize = 4096;
string_newtype!(RelativePath, validate_relative_path);
fn validate_relative_path(value: &str) -> Result<(), NameError> {
if value.is_empty() {
return Err(NameError::Empty {
kind: "relative path",
});
}
if value.len() > MAX_RELATIVE_PATH_LEN {
return Err(NameError::TooLong {
kind: "relative path",
len: value.len(),
max: MAX_RELATIVE_PATH_LEN,
});
}
if value.starts_with('/') {
return Err(NameError::Invalid {
kind: "relative path",
value: value.to_owned(),
reason: "absolute paths are not allowed",
});
}
if value.split('/').any(|seg| seg == "..") {
return Err(NameError::Invalid {
kind: "relative path",
value: value.to_owned(),
reason: "parent-directory segments are not allowed",
});
}
if value.contains('\0') {
return Err(NameError::Invalid {
kind: "relative path",
value: value.to_owned(),
reason: "NUL bytes are not allowed",
});
}
Ok(())
}
impl RelativePath {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, NameError> {
let s = std::str::from_utf8(bytes).map_err(|_| NameError::Invalid {
kind: "relative path",
value: String::from_utf8_lossy(bytes).into_owned(),
reason: "path is not valid UTF-8",
})?;
Self::new(s.to_owned())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct Arity(u8);
impl Arity {
pub const fn new(value: u8) -> Self {
Self(value)
}
pub const fn get(self) -> u8 {
self.0
}
}
impl fmt::Display for Arity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for Arity {
type Err = NameError;
fn from_str(s: &str) -> Result<Self, NameError> {
let parsed: i64 = s
.parse()
.map_err(|_| NameError::InvalidArityParse { raw: s.to_owned() })?;
if !(0..=255).contains(&parsed) {
return Err(NameError::InvalidArity { value: parsed });
}
Ok(Self(parsed as u8))
}
}
impl TryFrom<usize> for Arity {
type Error = NameError;
fn try_from(value: usize) -> Result<Self, NameError> {
u8::try_from(value)
.map(Self)
.map_err(|_| NameError::InvalidArity {
value: i64::try_from(value).unwrap_or(i64::MAX),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct CommitSha(Arc<str>);
impl CommitSha {
pub fn new(value: impl Into<String>) -> Result<Self, NameError> {
let v = value.into();
if v.len() != 40
|| !v
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
{
return Err(NameError::InvalidCommitSha { value: v });
}
Ok(Self(Arc::from(v)))
}
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn abbreviated(&self) -> &str {
&self.0[..12]
}
}
impl fmt::Display for CommitSha {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for CommitSha {
type Err = NameError;
fn from_str(s: &str) -> Result<Self, NameError> {
Self::new(s.to_owned())
}
}
impl AsRef<str> for CommitSha {
fn as_ref(&self) -> &str {
&self.0
}
}
const MIN_COMMIT_SHA_PREFIX_LEN: usize = 7;
const MAX_COMMIT_SHA_PREFIX_LEN: usize = 40;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(try_from = "String", into = "String")]
pub struct CommitShaPrefix(String);
impl TryFrom<String> for CommitShaPrefix {
type Error = NameError;
fn try_from(value: String) -> Result<Self, NameError> {
Self::new(value)
}
}
impl From<CommitShaPrefix> for String {
fn from(p: CommitShaPrefix) -> Self {
p.0
}
}
impl CommitShaPrefix {
pub fn new(value: impl Into<String>) -> Result<Self, NameError> {
let raw = value.into();
if raw.is_empty() {
return Err(NameError::EmptyCommitShaPrefix);
}
let normalised = raw.to_ascii_lowercase();
if let Some((position, ch)) = normalised
.chars()
.enumerate()
.find(|(_, c)| !c.is_ascii_hexdigit())
{
return Err(NameError::CommitShaPrefixNonHex {
value: normalised,
ch,
position,
});
}
let len = normalised.len();
if len < MIN_COMMIT_SHA_PREFIX_LEN {
return Err(NameError::CommitShaPrefixTooShort {
value: normalised,
len,
});
}
if len > MAX_COMMIT_SHA_PREFIX_LEN {
return Err(NameError::CommitShaPrefixTooLong {
value: normalised,
len,
});
}
Ok(Self(normalised))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn is_full(&self) -> bool {
self.0.len() == MAX_COMMIT_SHA_PREFIX_LEN
}
#[must_use]
pub fn as_full_sha(&self) -> Option<CommitSha> {
if self.is_full() {
CommitSha::new(self.0.clone()).ok()
} else {
None
}
}
}
impl fmt::Display for CommitShaPrefix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for CommitShaPrefix {
type Err = NameError;
fn from_str(s: &str) -> Result<Self, NameError> {
Self::new(s.to_owned())
}
}
impl AsRef<str> for CommitShaPrefix {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Mfa {
pub module: ModuleName,
pub function: FunctionName,
pub arity: Arity,
}
impl Mfa {
pub fn new(module: ModuleName, function: FunctionName, arity: Arity) -> Self {
Self {
module,
function,
arity,
}
}
}
impl fmt::Display for Mfa {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}/{}", self.module, self.function, self.arity)
}
}
impl FromStr for Mfa {
type Err = NameError;
fn from_str(s: &str) -> Result<Self, NameError> {
let (head, arity_part) = s.rsplit_once('/').ok_or_else(|| NameError::InvalidMfa {
value: s.to_owned(),
})?;
let (module_part, function_part) =
head.split_once(':').ok_or_else(|| NameError::InvalidMfa {
value: s.to_owned(),
})?;
let arity = arity_part
.parse::<Arity>()
.map_err(|_| NameError::InvalidMfa {
value: s.to_owned(),
})?;
Ok(Self {
module: module_part.parse()?,
function: function_part.parse()?,
arity,
})
}
}