use crate::version::SemVer;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CapabilitySlot {
Deployer,
Secrets,
Telemetry,
Sessions,
State,
Revocation,
Messaging,
Extension,
}
impl CapabilitySlot {
pub const ALL: &'static [CapabilitySlot] = &[
CapabilitySlot::Deployer,
CapabilitySlot::Secrets,
CapabilitySlot::Telemetry,
CapabilitySlot::Sessions,
CapabilitySlot::State,
CapabilitySlot::Revocation,
CapabilitySlot::Messaging,
CapabilitySlot::Extension,
];
pub fn as_str(self) -> &'static str {
match self {
CapabilitySlot::Deployer => "deployer",
CapabilitySlot::Secrets => "secrets",
CapabilitySlot::Telemetry => "telemetry",
CapabilitySlot::Sessions => "sessions",
CapabilitySlot::State => "state",
CapabilitySlot::Revocation => "revocation",
CapabilitySlot::Messaging => "messaging",
CapabilitySlot::Extension => "extension",
}
}
pub fn binds_in_packs(self) -> bool {
!matches!(self, CapabilitySlot::Messaging | CapabilitySlot::Extension)
}
}
impl fmt::Display for CapabilitySlot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct PackDescriptor {
raw: String,
path: String,
version: SemVer,
}
impl PackDescriptor {
pub fn try_new(raw: impl Into<String>) -> Result<Self, PackDescriptorParseError> {
let raw = raw.into();
Self::parse(&raw).map(|(path, version)| Self { raw, path, version })
}
fn parse(s: &str) -> Result<(String, SemVer), PackDescriptorParseError> {
let mut parts = s.splitn(2, '@');
let path = parts.next().unwrap_or("");
let version = parts
.next()
.ok_or(PackDescriptorParseError::MissingVersion)?;
if parts.next().is_some() {
return Err(PackDescriptorParseError::MultipleAt);
}
if path.is_empty() {
return Err(PackDescriptorParseError::EmptyPath);
}
if !path.contains('.') {
return Err(PackDescriptorParseError::PathMissingDot);
}
for ch in path.chars() {
if !descriptor_path_char_ok(ch) {
return Err(PackDescriptorParseError::InvalidPathChar(ch));
}
}
let version = version
.parse::<SemVer>()
.map_err(|err| PackDescriptorParseError::InvalidSemver(err.to_string()))?;
Ok((path.to_string(), version))
}
pub fn as_str(&self) -> &str {
&self.raw
}
pub fn path(&self) -> &str {
&self.path
}
pub fn version(&self) -> &SemVer {
&self.version
}
}
pub(crate) fn descriptor_path_char_ok(ch: char) -> bool {
ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '.'
}
impl fmt::Display for PackDescriptor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.raw)
}
}
impl FromStr for PackDescriptor {
type Err = PackDescriptorParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_new(s)
}
}
impl TryFrom<String> for PackDescriptor {
type Error = PackDescriptorParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_new(value)
}
}
impl From<PackDescriptor> for String {
fn from(value: PackDescriptor) -> Self {
value.raw
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PackDescriptorParseError {
#[error("pack descriptor missing `@<semver>` suffix")]
MissingVersion,
#[error("pack descriptor contains more than one `@` separator")]
MultipleAt,
#[error("pack descriptor path is empty")]
EmptyPath,
#[error("pack descriptor path must contain at least one `.`")]
PathMissingDot,
#[error("pack descriptor path contains invalid character `{0}`")]
InvalidPathChar(char),
#[error("pack descriptor version is not valid SemVer: {0}")]
InvalidSemver(String),
}
#[cfg(test)]
mod capability_slot_tests {
use super::*;
#[test]
fn as_str_round_trips_for_every_variant() {
for slot in CapabilitySlot::ALL {
let s = slot.as_str();
let back: CapabilitySlot =
serde_json::from_value(serde_json::Value::String(s.to_string()))
.expect("lowercase as_str deserialises back to the variant");
assert_eq!(back, *slot, "round-trip failed for {s}");
}
}
#[test]
fn extension_is_an_n_per_env_slot() {
assert!(!CapabilitySlot::Extension.binds_in_packs());
assert!(!CapabilitySlot::Messaging.binds_in_packs());
for slot in CapabilitySlot::ALL {
let expect_in_packs =
!matches!(slot, CapabilitySlot::Messaging | CapabilitySlot::Extension);
assert_eq!(slot.binds_in_packs(), expect_in_packs, "{slot}");
}
}
#[test]
fn all_contains_extension_exactly_once() {
assert_eq!(
CapabilitySlot::ALL
.iter()
.filter(|s| **s == CapabilitySlot::Extension)
.count(),
1
);
}
}