1use std::fmt;
2
3use crate::{validate_node_identifier, CoreError, MAX_SUBJECT_LEN};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
6pub struct TargetPath {
7 target: String,
8 subject: String,
9}
10
11impl TargetPath {
12 pub fn parse_application(path: &str) -> Result<Self, CoreError> {
13 let path = Self::origin_path(path)?;
14 let mut segments = path.split('/');
15 let target = segments.next().unwrap_or_default();
16 let subject_segments = segments.collect::<Vec<_>>();
17 let subject = subject_segments.join(".");
18 if subject_segments.iter().any(|segment| segment.is_empty()) {
19 return Err(CoreError::Malformed(
20 "target paths contain only non-empty path-safe segments".into(),
21 ));
22 }
23 Self::application(target, subject)
24 }
25
26 pub fn parse_discovery(path: &str) -> Result<Self, CoreError> {
27 let target = Self::origin_path(path)?;
28 if target.is_empty() || target.contains('/') {
29 return Err(CoreError::Malformed(
30 "discovery target paths contain exactly one target node".into(),
31 ));
32 }
33 Self::discovery(target)
34 }
35
36 pub fn application(
37 target: impl Into<String>,
38 subject: impl Into<String>,
39 ) -> Result<Self, CoreError> {
40 let target = target.into();
41 let subject = subject.into();
42 Self::validate_target(&target)?;
43 if subject.is_empty()
44 || subject.len() > MAX_SUBJECT_LEN
45 || subject.contains(['/', '?', '#'])
46 || subject.split('.').any(|segment| segment.is_empty())
47 || subject.bytes().any(|byte| byte.is_ascii_control())
48 {
49 return Err(CoreError::Malformed(
50 "local subject requires non-empty path-safe dot-separated segments within its length limit"
51 .into(),
52 ));
53 }
54 Ok(Self { target, subject })
55 }
56
57 pub fn discovery(target: impl Into<String>) -> Result<Self, CoreError> {
58 let target = target.into();
59 Self::validate_target(&target)?;
60 Ok(Self {
61 target,
62 subject: String::new(),
63 })
64 }
65
66 pub fn target(&self) -> &str {
67 &self.target
68 }
69
70 pub fn subject(&self) -> &str {
71 &self.subject
72 }
73
74 fn origin_path(path: &str) -> Result<&str, CoreError> {
75 if path.contains(['?', '#']) || path.bytes().any(|byte| byte.is_ascii_control()) {
76 return Err(CoreError::Malformed(
77 "target paths must not contain uri delimiters or control characters".into(),
78 ));
79 }
80 path.strip_prefix('/')
81 .ok_or_else(|| CoreError::Malformed("target paths start with a slash".into()))
82 }
83
84 fn validate_target(target: &str) -> Result<(), CoreError> {
85 validate_node_identifier(target).map_err(|_| {
86 CoreError::Malformed(
87 "target node is empty, reserved, not an ASCII URI-segment identifier, or exceeds its length limit"
88 .into(),
89 )
90 })
91 }
92}
93
94impl fmt::Display for TargetPath {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 write!(formatter, "/{}", self.target)?;
97 if !self.subject.is_empty() {
98 write!(formatter, "/{}", self.subject.replace('.', "/"))?;
99 }
100 Ok(())
101 }
102}