soaprs_http/
identifier.rs1use 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 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 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}