use http::Method;
use schemars::schema::RootSchema;
use schemars::JsonSchema;
use std::collections::HashSet;
use crate::error::Error;
use crate::Result;
#[derive(Debug, Clone)]
pub struct EndpointSchema {
pub path: String,
pub method: Method,
pub request_schema: Option<RootSchema>,
pub response_schema: Option<RootSchema>,
pub query_schema: Option<RootSchema>,
pub params_schema: Option<RootSchema>,
}
#[derive(Debug, Clone)]
pub struct SchemaRegistry {
entries: Vec<EndpointSchema>,
strict: bool,
routes: HashSet<String>,
}
impl Default for SchemaRegistry {
fn default() -> Self {
Self::new()
}
}
impl SchemaRegistry {
pub fn new() -> Self {
Self {
entries: Vec::new(),
strict: false,
routes: HashSet::new(),
}
}
pub fn strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
pub fn is_strict(&self) -> bool {
self.strict
}
pub fn register_endpoint(
&mut self,
path: impl Into<String>,
method: Method,
request_schema: Option<RootSchema>,
response_schema: Option<RootSchema>,
) {
let path = path.into();
warn_duplicate_route(&mut self.routes, &path, &method);
self.entries.push(EndpointSchema {
path,
method,
request_schema,
response_schema,
query_schema: None,
params_schema: None,
});
}
pub fn register_full(
&mut self,
path: impl Into<String>,
method: Method,
request_schema: Option<RootSchema>,
response_schema: Option<RootSchema>,
query_schema: Option<RootSchema>,
params_schema: Option<RootSchema>,
) {
let path = path.into();
warn_duplicate_route(&mut self.routes, &path, &method);
self.entries.push(EndpointSchema {
path,
method,
request_schema,
response_schema,
query_schema,
params_schema,
});
}
pub fn register_typed<E, Req, Res>(&mut self)
where
E: crate::Endpoint,
Req: JsonSchema + 'static,
Res: JsonSchema + 'static,
E::Params: JsonSchema,
E::Query: JsonSchema,
{
self.register_full(
E::PATH,
E::METHOD,
Some(schemars::schema_for!(Req)),
Some(schemars::schema_for!(Res)),
Some(schemars::schema_for!(E::Query)),
Some(schemars::schema_for!(E::Params)),
);
}
pub fn ensure_route(&self, path: &str, method: &Method) -> Result<()> {
if !self.strict {
return Ok(());
}
let key = route_key(path, method);
if self.routes.contains(&key) {
Ok(())
} else {
Err(Error::SchemaRoute {
method: method.to_string(),
path: path.to_string(),
})
}
}
pub fn entries(&self) -> &[EndpointSchema] {
&self.entries
}
pub fn request_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
let key = route_key(path, method);
self.entries
.iter()
.find(|e| route_key(&e.path, &e.method) == key)
.and_then(|e| e.request_schema.as_ref())
}
pub fn response_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
let key = route_key(path, method);
self.entries
.iter()
.find(|e| route_key(&e.path, &e.method) == key)
.and_then(|e| e.response_schema.as_ref())
}
pub fn query_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
let key = route_key(path, method);
self.entries
.iter()
.find(|e| route_key(&e.path, &e.method) == key)
.and_then(|e| e.query_schema.as_ref())
}
pub fn params_schema(&self, path: &str, method: &Method) -> Option<&RootSchema> {
let key = route_key(path, method);
self.entries
.iter()
.find(|e| route_key(&e.path, &e.method) == key)
.and_then(|e| e.params_schema.as_ref())
}
}
fn route_key(path: &str, method: &Method) -> String {
format!("{method}:{path}")
}
fn warn_duplicate_route(routes: &mut HashSet<String>, path: &str, method: &Method) {
let key = route_key(path, method);
if !routes.insert(key) {
tracing::warn!(
path,
method = %method,
"duplicate schema registration for route; lookups use the first matching entry"
);
}
}