Skip to main content

soaprs_http/
identifier.rs

1//! Validated identifiers shared by HTTP declarations and extension packages.
2
3use std::fmt;
4
5use soaprs_core::{SoapError, SoapResult};
6
7macro_rules! identifier {
8    ($name:ident, $description:literal) => {
9        #[doc = $description]
10        #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
11        pub struct $name(String);
12
13        impl $name {
14            /// Validates and wraps a stable logical identifier.
15            pub fn new(value: impl Into<String>) -> SoapResult<Self> {
16                let value = value.into();
17                validate_identifier(stringify!($name), &value)?;
18                Ok(Self(value))
19            }
20
21            /// Returns the identifier as text.
22            pub fn as_str(&self) -> &str {
23                &self.0
24            }
25        }
26
27        impl fmt::Display for $name {
28            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29                formatter.write_str(&self.0)
30            }
31        }
32
33        impl TryFrom<String> for $name {
34            type Error = SoapError;
35
36            fn try_from(value: String) -> Result<Self, Self::Error> {
37                Self::new(value)
38            }
39        }
40
41        impl TryFrom<&str> for $name {
42            type Error = SoapError;
43
44            fn try_from(value: &str) -> Result<Self, Self::Error> {
45                Self::new(value)
46            }
47        }
48    };
49}
50
51identifier!(
52    EndpointId,
53    "Stable endpoint identity used by adapters, documentation, and telemetry."
54);
55identifier!(
56    ContractId,
57    "Logical request or response contract identity resolved by validation and schema adapters."
58);
59fn validate_identifier(kind: &str, value: &str) -> SoapResult<()> {
60    if value.is_empty()
61        || !value.chars().all(|character| {
62            character == '.'
63                || character == '_'
64                || character == '-'
65                || character == ':'
66                || character.is_ascii_alphanumeric()
67        })
68    {
69        return Err(SoapError::validation(format!("invalid {kind} `{value}`")));
70    }
71    Ok(())
72}
73
74#[cfg(test)]
75mod tests {
76    use super::{ContractId, EndpointId};
77
78    #[test]
79    fn identifiers_accept_logical_names_and_reject_transport_fragments() {
80        assert!(EndpointId::new("users.get-by-id").is_ok());
81        assert!(ContractId::new("users:create:request").is_ok());
82        assert!(EndpointId::new("GET /users").is_err());
83        assert!(ContractId::new("").is_err());
84    }
85}