use std::{fmt, hash::Hash};
use serde::{Deserialize, Serialize};
macro_rules! string_newtype {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(pub String);
impl $name {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for $name {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl From<String> for $name {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for $name {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl From<$name> for String {
fn from(n: $name) -> String {
n.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 PartialEq<String> for $name {
fn eq(&self, other: &String) -> bool {
self.0 == *other
}
}
impl std::borrow::Borrow<str> for $name {
fn borrow(&self) -> &str {
&self.0
}
}
};
}
string_newtype!(
ThreadName
);
string_newtype!(
MarkerName
);
pub const RESERVED_REF_SEGMENT: &str = "heddle";
pub fn is_reserved_heddle_namespace(name: &str) -> bool {
let mut parts = name.split('/');
match (parts.next(), parts.next()) {
(Some(first), Some(_)) => first.eq_ignore_ascii_case(RESERVED_REF_SEGMENT),
_ => false,
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"ref name '{name}' is reserved: the heddle/ namespace is internal and cannot be a user thread or marker"
)]
pub struct ReservedRefNameError {
pub name: String,
}
impl ThreadName {
pub fn try_new(s: impl Into<String>) -> Result<Self, ReservedRefNameError> {
let name = s.into();
if is_reserved_heddle_namespace(&name) {
return Err(ReservedRefNameError { name });
}
Ok(Self(name))
}
}
impl MarkerName {
pub fn try_new(s: impl Into<String>) -> Result<Self, ReservedRefNameError> {
let name = s.into();
if is_reserved_heddle_namespace(&name) {
return Err(ReservedRefNameError { name });
}
Ok(Self(name))
}
}
string_newtype!(
Scope
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thread_name_display() {
let t = ThreadName::new("main");
assert_eq!(t.0, "main");
assert_eq!(t.0, "main");
assert_eq!(&*t, "main");
}
#[test]
fn serde_transparent_roundtrip() {
let t = ThreadName::new("feature/foo");
let json = serde_json::to_string(&t).unwrap();
assert_eq!(json, "\"feature/foo\"");
let back: ThreadName = serde_json::from_str(&json).unwrap();
assert_eq!(back, t);
}
#[test]
fn marker_name_distinct_from_thread_name() {
let _t: ThreadName = "main".into();
let _m: MarkerName = "v1.0".into();
}
#[test]
#[allow(clippy::cmp_owned)] fn comparison_with_str() {
let t = ThreadName::from("main");
assert!(t == "main");
assert!(t == *"main");
assert!(t == String::from("main"));
}
#[test]
fn borrow_for_hashmap_lookup() {
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert(ThreadName::new("main"), 1);
assert_eq!(map.get("main"), Some(&1));
}
#[test]
fn reserved_namespace_is_heddle_rooted_only() {
assert!(!is_reserved_heddle_namespace("heddle"));
assert!(!is_reserved_heddle_namespace("heddlefoo"));
assert!(!is_reserved_heddle_namespace("my/heddle"));
assert!(!is_reserved_heddle_namespace("main@review"));
assert!(is_reserved_heddle_namespace("heddle/frontier/main/hc-abc"));
assert!(is_reserved_heddle_namespace("Heddle/x"));
}
#[test]
fn try_new_rejects_reserved_thread_and_marker_names() {
assert!(ThreadName::try_new("heddle/frontier/main/hc-1").is_err());
assert!(MarkerName::try_new("heddle/notes").is_err());
assert_eq!(ThreadName::try_new("heddle").unwrap().as_str(), "heddle");
assert_eq!(
ThreadName::try_new("main@hd-abc").unwrap().as_str(),
"main@hd-abc"
);
}
}