1use std::time::Duration;
4
5use http::Method;
6use soaprs_core::{SoapError, SoapResult};
7
8use crate::{AuthorizationPolicy, RateLimitPolicy, RoutePath};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct EndpointMetadata {
13 pub name: String,
15 pub method: Method,
17 pub path: RoutePath,
19 pub authorization: AuthorizationPolicy,
21 pub rate_limit: Option<RateLimitPolicy>,
23 pub timeout: Option<Duration>,
25 pub tags: Vec<String>,
27}
28
29impl EndpointMetadata {
30 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 pub fn authorize(mut self, policy: AuthorizationPolicy) -> Self {
55 self.authorization = policy;
56 self
57 }
58
59 pub fn rate_limit(mut self, policy: RateLimitPolicy) -> Self {
61 self.rate_limit = Some(policy);
62 self
63 }
64
65 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 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}