use serde::{Deserialize, Serialize};
pub const VAULT_ID_MAX_LEN: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct VaultId(String);
impl VaultId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn parse(s: &str) -> Result<Self, crate::error::ValidationError> {
use crate::error::ValidationError;
if s.is_empty() {
return Err(ValidationError::InvalidVaultId("vault_id vide".to_string()));
}
if s.len() > VAULT_ID_MAX_LEN {
return Err(ValidationError::InvalidVaultId(format!(
"vault_id trop long ({} > {VAULT_ID_MAX_LEN} octets)",
s.len()
)));
}
if !s
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(ValidationError::InvalidVaultId(format!(
"charset interdit dans {s:?} (autorisé : a-z 0-9 -)"
)));
}
if s.starts_with('-') || s.ends_with('-') {
return Err(ValidationError::InvalidVaultId(format!(
"tiret en tête ou en queue interdit dans {s:?}"
)));
}
Ok(Self(s.to_string()))
}
}
impl std::fmt::Display for VaultId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for VaultId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for VaultId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl AsRef<str> for VaultId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<&str> for VaultId {
fn eq(&self, other: &&str) -> bool {
self.0.as_str() == *other
}
}
impl PartialEq<str> for VaultId {
fn eq(&self, other: &str) -> bool {
self.0.as_str() == other
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct LocusId(String);
impl LocusId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn parse(s: &str) -> Result<Self, crate::error::ValidationError> {
use crate::error::ValidationError;
if s.is_empty() {
return Err(ValidationError::InvalidLocusId("locus vide".to_string()));
}
if s.len() > LOCUS_MAX_LEN {
return Err(ValidationError::InvalidLocusId(format!(
"locus trop long ({} > {LOCUS_MAX_LEN} octets)",
s.len()
)));
}
if !s
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '/')
{
return Err(ValidationError::InvalidLocusId(format!(
"charset interdit dans {s:?} (autorisé : a-z 0-9 - /)"
)));
}
if s.contains("..") {
return Err(ValidationError::InvalidLocusId(format!(
"remontée de répertoire interdite dans {s:?}"
)));
}
if s.starts_with('/') || s.ends_with('/') || s.contains("//") {
return Err(ValidationError::InvalidLocusId(format!(
"slash en tête/queue ou segment vide interdit dans {s:?}"
)));
}
Ok(Self(s.to_string()))
}
}
pub const LOCUS_MAX_LEN: usize = 128;
impl std::fmt::Display for LocusId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for LocusId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for LocusId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl AsRef<str> for LocusId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BearerId(String);
impl BearerId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for BearerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for BearerId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for BearerId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", content = "id", rename_all = "kebab-case")]
pub enum OverrideScope {
Vault(VaultId),
Locus(LocusId),
Bearer(BearerId),
}
#[cfg(test)]
mod vault_id_parse_tests {
use super::*;
use crate::error::ValidationError;
#[test]
fn vault_id_parse_accepts_valid() {
for ok in ["main", "a", "my-vault", "vault01", "v0-prod", "abc123"] {
assert!(
VaultId::parse(ok).is_ok(),
"{ok:?} doit être accepté par VaultId::parse"
);
}
}
#[test]
fn vault_id_parse_rejects_invalid() {
let bad = [
"", "Main", "my vault", "my_vault", "my/vault", "café", "-main", "main-", ];
for b in bad {
assert!(
VaultId::parse(b).is_err(),
"{b:?} doit être rejeté par VaultId::parse"
);
}
let long = "a".repeat(VAULT_ID_MAX_LEN + 1);
assert!(
VaultId::parse(&long).is_err(),
"vault_id > max doit être rejeté"
);
let at_limit = "a".repeat(VAULT_ID_MAX_LEN);
assert!(
VaultId::parse(&at_limit).is_ok(),
"vault_id == max doit être accepté"
);
}
#[test]
fn vault_id_parse_produces_correct_error_variant() {
let err = VaultId::parse("").unwrap_err();
assert!(
matches!(err, ValidationError::InvalidVaultId(_)),
"attendu InvalidVaultId, obtenu {:?}",
err
);
let err = VaultId::parse("Bad-Vault!").unwrap_err();
assert!(
matches!(err, ValidationError::InvalidVaultId(_)),
"attendu InvalidVaultId, obtenu {:?}",
err
);
}
}
#[cfg(test)]
mod locus_parse_tests {
use super::*;
#[test]
fn locus_parse_accepts_valid() {
for ok in ["knowledge", "knowledge/rust", "a", "a-b/c-d/e9", "x/y/z"] {
assert!(LocusId::parse(ok).is_ok(), "{ok:?} doit être accepté");
}
}
#[test]
fn locus_parse_rejects_invalid() {
let bad = [
"", "Knowledge", "knowledge rust", "know_ledge", "../etc", "a/../b", "/knowledge", "knowledge/", "a//b", "café", ];
for b in bad {
assert!(LocusId::parse(b).is_err(), "{b:?} doit être rejeté");
}
let long = "a".repeat(LOCUS_MAX_LEN + 1);
assert!(
LocusId::parse(&long).is_err(),
"locus > max doit être rejeté"
);
let at_limit = "a".repeat(LOCUS_MAX_LEN);
assert!(
LocusId::parse(&at_limit).is_ok(),
"locus == max doit être accepté"
);
}
}