1use http::Method;
4use schemars::schema::RootSchema;
5use schemars::JsonSchema;
6use std::collections::HashMap;
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 index: HashMap<String, usize>,
35}
36
37impl Default for SchemaRegistry {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl SchemaRegistry {
44 pub fn new() -> Self {
46 Self {
47 entries: Vec::new(),
48 strict: false,
49 index: HashMap::new(),
50 }
51 }
52
53 pub fn strict(mut self, strict: bool) -> Self {
55 self.strict = strict;
56 self
57 }
58
59 pub fn is_strict(&self) -> bool {
61 self.strict
62 }
63
64 pub fn register_endpoint(
66 &mut self,
67 path: impl Into<String>,
68 method: Method,
69 request_schema: Option<RootSchema>,
70 response_schema: Option<RootSchema>,
71 ) {
72 self.push_entry(EndpointSchema {
73 path: path.into(),
74 method,
75 request_schema,
76 response_schema,
77 query_schema: None,
78 params_schema: None,
79 });
80 }
81
82 pub fn register_full(
84 &mut self,
85 path: impl Into<String>,
86 method: Method,
87 request_schema: Option<RootSchema>,
88 response_schema: Option<RootSchema>,
89 query_schema: Option<RootSchema>,
90 params_schema: Option<RootSchema>,
91 ) {
92 self.push_entry(EndpointSchema {
93 path: path.into(),
94 method,
95 request_schema,
96 response_schema,
97 query_schema,
98 params_schema,
99 });
100 }
101
102 fn push_entry(&mut self, entry: EndpointSchema) {
104 let key = route_key(&entry.path, &entry.method);
105 if self.index.contains_key(&key) {
106 tracing::warn!(
107 path = %entry.path,
108 method = %entry.method,
109 "duplicate schema registration for route; lookups use the first matching entry"
110 );
111 }
112 let idx = self.entries.len();
113 self.index.entry(key).or_insert(idx);
114 self.entries.push(entry);
115 }
116
117 pub fn register_typed<E, Req, Res>(&mut self)
119 where
120 E: crate::Endpoint,
121 Req: JsonSchema + 'static,
122 Res: JsonSchema + 'static,
123 E::Params: JsonSchema,
124 E::Query: JsonSchema,
125 {
126 self.register_full(
127 E::PATH,
128 E::METHOD,
129 Some(schemars::schema_for!(Req)),
130 Some(schemars::schema_for!(Res)),
131 Some(schemars::schema_for!(E::Query)),
132 Some(schemars::schema_for!(E::Params)),
133 );
134 }
135
136 pub fn ensure_route(&self, path: &str, method: &Method) -> Result<()> {
138 if !self.strict {
139 return Ok(());
140 }
141 if self.index.contains_key(&route_key(path, method)) {
142 Ok(())
143 } else {
144 Err(Error::SchemaRoute {
145 method: method.to_string(),
146 path: path.to_string(),
147 })
148 }
149 }
150
151 pub fn entries(&self) -> &[EndpointSchema] {
153 &self.entries
154 }
155
156 fn find(&self, path: &str, method: &Method) -> Option<&EndpointSchema> {
158 self.index
159 .get(&route_key(path, method))
160 .map(|&i| &self.entries[i])
161 }
162
163 pub fn request_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
165 self.find(path, method)
166 .and_then(|e| e.request_schema.as_ref())
167 }
168
169 pub fn response_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
171 self.find(path, method)
172 .and_then(|e| e.response_schema.as_ref())
173 }
174
175 pub fn query_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
177 self.find(path, method)
178 .and_then(|e| e.query_schema.as_ref())
179 }
180
181 pub fn params_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
183 self.find(path, method)
184 .and_then(|e| e.params_schema.as_ref())
185 }
186}
187
188fn route_key(path: &str, method: &Method) -> String {
189 format!("{method}:{path}")
190}