use std::fmt;
use crate::{validate_node_identifier, CoreError, MAX_SUBJECT_LEN};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TargetPath {
target: String,
subject: String,
}
impl TargetPath {
pub fn parse_application(path: &str) -> Result<Self, CoreError> {
let path = Self::origin_path(path)?;
let mut segments = path.split('/');
let target = segments.next().unwrap_or_default();
let subject_segments = segments.collect::<Vec<_>>();
let subject = subject_segments.join(".");
if subject_segments.iter().any(|segment| segment.is_empty()) {
return Err(CoreError::Malformed(
"target paths contain only non-empty path-safe segments".into(),
));
}
Self::application(target, subject)
}
pub fn parse_discovery(path: &str) -> Result<Self, CoreError> {
let target = Self::origin_path(path)?;
if target.is_empty() || target.contains('/') {
return Err(CoreError::Malformed(
"discovery target paths contain exactly one target node".into(),
));
}
Self::discovery(target)
}
pub fn application(
target: impl Into<String>,
subject: impl Into<String>,
) -> Result<Self, CoreError> {
let target = target.into();
let subject = subject.into();
Self::validate_target(&target)?;
if subject.is_empty()
|| subject.len() > MAX_SUBJECT_LEN
|| subject.contains(['/', '?', '#'])
|| subject.split('.').any(|segment| segment.is_empty())
|| subject.bytes().any(|byte| byte.is_ascii_control())
{
return Err(CoreError::Malformed(
"local subject requires non-empty path-safe dot-separated segments within its length limit"
.into(),
));
}
Ok(Self { target, subject })
}
pub fn discovery(target: impl Into<String>) -> Result<Self, CoreError> {
let target = target.into();
Self::validate_target(&target)?;
Ok(Self {
target,
subject: String::new(),
})
}
pub fn target(&self) -> &str {
&self.target
}
pub fn subject(&self) -> &str {
&self.subject
}
fn origin_path(path: &str) -> Result<&str, CoreError> {
if path.contains(['?', '#']) || path.bytes().any(|byte| byte.is_ascii_control()) {
return Err(CoreError::Malformed(
"target paths must not contain uri delimiters or control characters".into(),
));
}
path.strip_prefix('/')
.ok_or_else(|| CoreError::Malformed("target paths start with a slash".into()))
}
fn validate_target(target: &str) -> Result<(), CoreError> {
validate_node_identifier(target).map_err(|_| {
CoreError::Malformed(
"target node is empty, reserved, not an ASCII URI-segment identifier, or exceeds its length limit"
.into(),
)
})
}
}
impl fmt::Display for TargetPath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "/{}", self.target)?;
if !self.subject.is_empty() {
write!(formatter, "/{}", self.subject.replace('.', "/"))?;
}
Ok(())
}
}