use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CurrentKey(String);
impl CurrentKey {
pub const MAX_LENGTH: usize = 256;
pub fn new(key: impl Into<String>) -> Self {
Self(key.into())
}
pub fn new_checked(key: impl Into<String>) -> Option<Self> {
let key = key.into();
if key.is_empty() || key.len() > Self::MAX_LENGTH {
None
} else {
Some(Self(key))
}
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for CurrentKey {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for CurrentKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProposedKey(String);
impl ProposedKey {
pub const MAX_LENGTH: usize = 256;
pub fn new(key: impl Into<String>) -> Self {
Self(key.into())
}
pub fn new_checked(key: impl Into<String>) -> Option<Self> {
let key = key.into();
if key.is_empty() || key.len() > Self::MAX_LENGTH {
None
} else {
Some(Self(key))
}
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for ProposedKey {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ProposedKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ContextPath(String);
impl ContextPath {
pub const MAX_LENGTH: usize = 1024;
pub const MAX_DEPTH: usize = 32;
pub const SEPARATOR: char = '/';
#[must_use]
pub const fn root() -> Self {
Self(String::new())
}
pub fn new(path: impl Into<String>) -> Self {
Self(path.into())
}
pub fn new_checked(path: impl Into<String>) -> Option<Self> {
let path = path.into();
if path.len() > Self::MAX_LENGTH {
return None;
}
let depth = path.matches(Self::SEPARATOR).count();
if depth > Self::MAX_DEPTH {
return None;
}
if path.contains('\0') {
return None;
}
Some(Self(path))
}
#[must_use]
pub fn branch(&self, segment: &str) -> Option<Self> {
if segment.is_empty() || segment.contains(Self::SEPARATOR) || segment.contains('\0') {
return None;
}
let new_path = format!("{}{}{}", self.0, Self::SEPARATOR, segment);
Self::new_checked(new_path)
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
#[must_use]
pub fn depth(&self) -> usize {
if self.0.is_empty() {
0
} else {
self.0.matches(Self::SEPARATOR).count()
}
}
#[inline]
#[must_use]
pub const fn is_root(&self) -> bool {
self.0.is_empty()
}
#[inline]
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl Default for ContextPath {
fn default() -> Self {
Self::root()
}
}
impl AsRef<str> for ContextPath {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ContextPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
write!(f, "/")
} else {
write!(f, "{}", self.0)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_current_key_basic() {
let key = CurrentKey::new("test");
assert_eq!(key.as_str(), "test");
}
#[test]
fn test_current_key_checked() {
assert!(CurrentKey::new_checked("valid").is_some());
assert!(CurrentKey::new_checked("").is_none());
assert!(CurrentKey::new_checked("x".repeat(1000)).is_none());
}
#[test]
fn test_proposed_key_basic() {
let key = ProposedKey::new("message");
assert_eq!(key.as_str(), "message");
}
#[test]
fn test_context_path_root() {
let path = ContextPath::root();
assert!(path.is_root());
assert_eq!(path.depth(), 0);
}
#[test]
fn test_context_path_branch() {
let root = ContextPath::root();
let child = root.branch("child").unwrap();
assert_eq!(child.as_str(), "/child");
assert_eq!(child.depth(), 1);
let grandchild = child.branch("grandchild").unwrap();
assert_eq!(grandchild.as_str(), "/child/grandchild");
assert_eq!(grandchild.depth(), 2);
}
#[test]
fn test_context_path_invalid_branch() {
let root = ContextPath::root();
assert!(root.branch("").is_none());
assert!(root.branch("has/slash").is_none());
assert!(root.branch("has\0null").is_none());
}
#[test]
fn test_context_path_max_depth() {
let mut path = ContextPath::root();
for i in 0..ContextPath::MAX_DEPTH {
path = path.branch(&format!("seg{i}")).unwrap();
}
assert!(path.branch("overflow").is_none());
}
}