Skip to main content

soaprs_http/
endpoint.rs

1//! Endpoint metadata without framework handler types.
2
3use std::time::Duration;
4
5use http::Method;
6use soaprs_core::{SoapError, SoapResult};
7
8use crate::{AuthorizationPolicy, RateLimitPolicy, RoutePath};
9
10/// Portable endpoint metadata consumed by framework adapters and documentation.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct EndpointMetadata {
13    /// Stable endpoint name used by diagnostics and API documentation.
14    pub name: String,
15    /// HTTP method.
16    pub method: Method,
17    /// Portable route path.
18    pub path: RoutePath,
19    /// Authorization requirement.
20    pub authorization: AuthorizationPolicy,
21    /// Optional rate-limit policy.
22    pub rate_limit: Option<RateLimitPolicy>,
23    /// Optional request timeout.
24    pub timeout: Option<Duration>,
25    /// Documentation and grouping tags.
26    pub tags: Vec<String>,
27}
28
29impl EndpointMetadata {
30    /// Creates endpoint metadata with public access and no optional policies.
31    pub fn new(name: impl Into<String>, method: Method, path: RoutePath) -> SoapResult<Self> {
32        let name = name.into();
33        if name.is_empty()
34            || !name.chars().all(|character| {
35                character == '.' || character == '_' || character.is_ascii_alphanumeric()
36            })
37        {
38            return Err(SoapError::validation(format!(
39                "invalid endpoint name `{name}`"
40            )));
41        }
42        Ok(Self {
43            name,
44            method,
45            path,
46            authorization: AuthorizationPolicy::Public,
47            rate_limit: None,
48            timeout: None,
49            tags: Vec::new(),
50        })
51    }
52
53    /// Sets the authorization policy.
54    pub fn authorize(mut self, policy: AuthorizationPolicy) -> Self {
55        self.authorization = policy;
56        self
57    }
58
59    /// Sets the rate-limit policy.
60    pub fn rate_limit(mut self, policy: RateLimitPolicy) -> Self {
61        self.rate_limit = Some(policy);
62        self
63    }
64
65    /// Sets a non-zero request timeout.
66    pub fn timeout(mut self, timeout: Duration) -> SoapResult<Self> {
67        if timeout.is_zero() {
68            return Err(SoapError::validation(
69                "endpoint timeout must be greater than zero",
70            ));
71        }
72        self.timeout = Some(timeout);
73        Ok(self)
74    }
75
76    /// Adds a non-empty documentation tag once.
77    pub fn tag(mut self, tag: impl Into<String>) -> SoapResult<Self> {
78        let tag = tag.into();
79        if tag.trim().is_empty() {
80            return Err(SoapError::validation("endpoint tag cannot be empty"));
81        }
82        if !self.tags.contains(&tag) {
83            self.tags.push(tag);
84        }
85        Ok(self)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use std::time::Duration;
92
93    use http::Method;
94
95    use crate::{AuthorizationPolicy, EndpointMetadata, RoutePath};
96
97    #[test]
98    fn builds_metadata_without_a_framework_handler() {
99        let result = EndpointMetadata::new(
100            "users.get",
101            Method::GET,
102            match RoutePath::new("/users/{id}") {
103                Ok(path) => path,
104                Err(error) => panic!("valid path failed: {error}"),
105            },
106        )
107        .map(|metadata| metadata.authorize(AuthorizationPolicy::Authenticated))
108        .and_then(|metadata| metadata.timeout(Duration::from_secs(5)))
109        .and_then(|metadata| metadata.tag("users"));
110
111        assert!(result.is_ok());
112    }
113}