Skip to main content

unb_core/
target_path.rs

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}