unb-core 2.0.3

Core unb protocol types: envelope, session, routing, taxonomy
Documentation
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(())
    }
}