1use http::Method;
4use schemars::schema::RootSchema;
5use schemars::JsonSchema;
6use std::collections::HashSet;
7
8use crate::error::Error;
9use crate::Result;
10
11#[derive(Debug, Clone)]
13pub struct EndpointSchema {
14 pub path: String,
16 pub method: Method,
18 pub request_schema: Option<RootSchema>,
20 pub response_schema: Option<RootSchema>,
22 pub query_schema: Option<RootSchema>,
24 pub params_schema: Option<RootSchema>,
26}
27
28#[derive(Debug, Clone)]
30pub struct SchemaRegistry {
31 entries: Vec<EndpointSchema>,
32 strict: bool,
33 routes: HashSet<String>,
34}
35
36impl Default for SchemaRegistry {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl SchemaRegistry {
43 pub fn new() -> Self {
45 Self {
46 entries: Vec::new(),
47 strict: false,
48 routes: HashSet::new(),
49 }
50 }
51
52 pub fn strict(mut self, strict: bool) -> Self {
54 self.strict = strict;
55 self
56 }
57
58 pub fn is_strict(&self) -> bool {
60 self.strict
61 }
62
63 pub fn register_endpoint(
65 &mut self,
66 path: impl Into<String>,
67 method: Method,
68 request_schema: Option<RootSchema>,
69 response_schema: Option<RootSchema>,
70 ) {
71 let path = path.into();
72 warn_duplicate_route(&mut self.routes, &path, &method);
73 self.entries.push(EndpointSchema {
74 path,
75 method,
76 request_schema,
77 response_schema,
78 query_schema: None,
79 params_schema: None,
80 });
81 }
82
83 pub fn register_full(
85 &mut self,
86 path: impl Into<String>,
87 method: Method,
88 request_schema: Option<RootSchema>,
89 response_schema: Option<RootSchema>,
90 query_schema: Option<RootSchema>,
91 params_schema: Option<RootSchema>,
92 ) {
93 let path = path.into();
94 warn_duplicate_route(&mut self.routes, &path, &method);
95 self.entries.push(EndpointSchema {
96 path,
97 method,
98 request_schema,
99 response_schema,
100 query_schema,
101 params_schema,
102 });
103 }
104
105 pub fn register_typed<E, Req, Res>(&mut self)
107 where
108 E: crate::Endpoint,
109 Req: JsonSchema + 'static,
110 Res: JsonSchema + 'static,
111 E::Params: JsonSchema,
112 E::Query: JsonSchema,
113 {
114 self.register_full(
115 E::PATH,
116 E::METHOD,
117 Some(schemars::schema_for!(Req)),
118 Some(schemars::schema_for!(Res)),
119 Some(schemars::schema_for!(E::Query)),
120 Some(schemars::schema_for!(E::Params)),
121 );
122 }
123
124 pub fn ensure_route(&self, path: &str, method: &Method) -> Result<()> {
126 if !self.strict {
127 return Ok(());
128 }
129 let key = route_key(path, method);
130 if self.routes.contains(&key) {
131 Ok(())
132 } else {
133 Err(Error::SchemaRoute {
134 method: method.to_string(),
135 path: path.to_string(),
136 })
137 }
138 }
139
140 pub fn entries(&self) -> &[EndpointSchema] {
142 &self.entries
143 }
144
145 pub fn request_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
147 let key = route_key(path, method);
148 self.entries
149 .iter()
150 .find(|e| route_key(&e.path, &e.method) == key)
151 .and_then(|e| e.request_schema.as_ref())
152 }
153
154 pub fn response_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
156 let key = route_key(path, method);
157 self.entries
158 .iter()
159 .find(|e| route_key(&e.path, &e.method) == key)
160 .and_then(|e| e.response_schema.as_ref())
161 }
162
163 pub fn query_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
165 let key = route_key(path, method);
166 self.entries
167 .iter()
168 .find(|e| route_key(&e.path, &e.method) == key)
169 .and_then(|e| e.query_schema.as_ref())
170 }
171
172 pub fn params_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
174 let key = route_key(path, method);
175 self.entries
176 .iter()
177 .find(|e| route_key(&e.path, &e.method) == key)
178 .and_then(|e| e.params_schema.as_ref())
179 }
180}
181
182fn route_key(path: &str, method: &Method) -> String {
183 format!("{method}:{path}")
184}
185
186fn warn_duplicate_route(routes: &mut HashSet<String>, path: &str, method: &Method) {
187 let key = route_key(path, method);
188 if !routes.insert(key) {
189 tracing::warn!(
190 path,
191 method = %method,
192 "duplicate schema registration for route; lookups use the first matching entry"
193 );
194 }
195}