Skip to main content

better_fetch/
schema.rs

1//! Schema registry for endpoint metadata (requires `schema` feature).
2
3use http::Method;
4use schemars::schema::RootSchema;
5use schemars::JsonSchema;
6use std::collections::HashSet;
7
8use crate::error::Error;
9use crate::Result;
10
11/// Metadata describing a single endpoint for documentation and codegen.
12#[derive(Debug, Clone)]
13pub struct EndpointSchema {
14    /// Route path template.
15    pub path: String,
16    /// HTTP method.
17    pub method: Method,
18    /// JSON request body schema.
19    pub request_schema: Option<RootSchema>,
20    /// JSON response schema.
21    pub response_schema: Option<RootSchema>,
22    /// Query string schema.
23    pub query_schema: Option<RootSchema>,
24    /// Path parameter schema.
25    pub params_schema: Option<RootSchema>,
26}
27
28/// Registry of endpoint schemas.
29#[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    /// Creates an empty registry (non-strict by default).
44    pub fn new() -> Self {
45        Self {
46            entries: Vec::new(),
47            strict: false,
48            routes: HashSet::new(),
49        }
50    }
51
52    /// When `true`, [`Self::ensure_route`] rejects unregistered paths at request time.
53    pub fn strict(mut self, strict: bool) -> Self {
54        self.strict = strict;
55        self
56    }
57
58    /// Returns whether strict route validation is enabled.
59    pub fn is_strict(&self) -> bool {
60        self.strict
61    }
62
63    /// Registers path, method, and optional request/response JSON schemas.
64    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        self.routes.insert(route_key(&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    /// Registers a route with request, response, query, and params schemas.
84    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        self.routes.insert(route_key(&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    /// Registers schemas derived from [`Endpoint`](crate::Endpoint) and `JsonSchema` types.
106    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    /// Returns an error if strict mode is enabled and the route is not registered.
125    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::Other(format!(
134                "route not in schema registry: {method} {path}"
135            )))
136        }
137    }
138
139    /// Returns all registered endpoint metadata.
140    pub fn entries(&self) -> &[EndpointSchema] {
141        &self.entries
142    }
143}
144
145fn route_key(path: &str, method: &Method) -> String {
146    format!("{method}:{path}")
147}