Skip to main content

lino_rest_api/
app.rs

1//! The Links Notation REST API application.
2//!
3//! The application is a framework-independent request handler: it takes a
4//! [`RawRequest`] — a method, a path, a query string, headers and body bytes —
5//! and answers with [`ResponseParts`]. Everything the specification requires
6//! happens in between: content negotiation, Links Notation bodies, problem
7//! details, automatic `HEAD`, automatic `OPTIONS`, `405 Method Not Allowed` with
8//! `Allow`, CORS and a machine-readable service description.
9//!
10//! [`mod@crate::serve`] runs it over HTTP; anything else that can build a
11//! [`RawRequest`] can run it too.
12
13use std::collections::HashMap;
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::Arc;
17
18use lino_objects_codec::LinoValue;
19
20use crate::codec::to_json_pretty;
21use crate::cors::{CorsPolicy, cors_headers};
22use crate::description::{ServiceInfo, openapi_document, service_description};
23use crate::headers::Headers;
24use crate::media_type::{JSON_CONTENT_TYPE, LINO_CONTENT_TYPE};
25use crate::middleware::{
26    DEFAULT_MAX_BODY_BYTES, EtagPolicy, ResponseParts, append_vary, build_problem_response,
27    build_response, decode_request_body, negotiate_request,
28};
29use crate::problem::LinoHttpError;
30use crate::request::{LinoRequest, QueryDefaults};
31use crate::resource::{ResourceOptions, register_resource};
32use crate::response::{Body, LinoResult, raw_response};
33use crate::router::{RouteMeta, RouteTable};
34use crate::store::Store;
35
36/// Methods a handler can be registered for.
37pub const HTTP_METHODS: [&str; 7] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
38
39/// Where the native service description lives (specification section 9).
40pub const DESCRIPTION_PATH: &str = "/.well-known/lino-api";
41
42/// Where the generated OpenAPI 3.1 document lives (specification section 9).
43pub const OPENAPI_PATH: &str = "/.well-known/openapi.json";
44
45/// What a handler returns: a future resolving to a result or a problem.
46pub type HandlerFuture = Pin<Box<dyn Future<Output = Result<LinoResult, LinoHttpError>> + Send>>;
47
48/// A registered route handler.
49pub type Handler = Arc<dyn Fn(LinoRequest) -> HandlerFuture + Send + Sync>;
50
51/// One HTTP request as it arrives from a server, before it is decoded.
52#[derive(Debug, Clone, Default)]
53pub struct RawRequest {
54    /// HTTP method.
55    pub method: String,
56    /// Request path, without the query string.
57    pub path: String,
58    /// Query string, without its leading `?`.
59    pub query_string: String,
60    /// Request headers.
61    pub headers: Headers,
62    /// Raw request body.
63    pub body: Vec<u8>,
64}
65
66impl RawRequest {
67    /// A request with a method and a path, and nothing else set.
68    pub fn new<M: Into<String>, P: Into<String>>(method: M, path: P) -> Self {
69        Self {
70            method: method.into().to_uppercase(),
71            path: path.into(),
72            ..Self::default()
73        }
74    }
75
76    /// The same request with a query string, without its leading `?`.
77    pub fn with_query<S: Into<String>>(mut self, query_string: S) -> Self {
78        self.query_string = query_string.into();
79        self
80    }
81
82    /// The same request with one more header.
83    pub fn with_header<N: Into<String>, V: Into<String>>(mut self, name: N, value: V) -> Self {
84        self.headers.insert(name, value);
85        self
86    }
87
88    /// The same request carrying a body of a media type.
89    pub fn with_body<B: Into<Vec<u8>>>(mut self, body: B, content_type: &str) -> Self {
90        self.body = body.into();
91        self.headers.insert("Content-Type", content_type);
92        self
93    }
94}
95
96/// An application that speaks Links Notation.
97pub struct LinoApp {
98    /// Title, version and prose description of the service.
99    pub info: ServiceInfo,
100    /// CORS policy, when cross-origin requests are allowed.
101    pub cors: Option<CorsPolicy>,
102    /// Representations the server may produce; [`None`] means all of them.
103    pub supported: Option<Vec<String>>,
104    /// Largest accepted request body.
105    pub max_body_bytes: usize,
106    /// Page-size defaults for collection queries.
107    pub query_defaults: QueryDefaults,
108    routes: RouteTable,
109    handlers: HashMap<(String, String), Handler>,
110    describe_routes: bool,
111}
112
113impl Default for LinoApp {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl LinoApp {
120    /// A new application serving its own description.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use lino_rest_api::app::{LinoApp, RawRequest};
126    /// use lino_rest_api::value::{object, string};
127    ///
128    /// # tokio_test::block_on(async {
129    /// let mut app = LinoApp::new();
130    /// app.get_fn("/health", "Health", |_request| {
131    ///     Ok(object([("status", string("ok"))]))
132    /// });
133    ///
134    /// let response = app.respond(RawRequest::new("GET", "/health")).await;
135    /// assert_eq!(response.status, 200);
136    /// assert_eq!(response.body, "(\n  status \"ok\"\n)");
137    /// # });
138    /// ```
139    pub fn new() -> Self {
140        let mut app = Self {
141            info: ServiceInfo::default(),
142            cors: None,
143            supported: None,
144            max_body_bytes: DEFAULT_MAX_BODY_BYTES,
145            query_defaults: QueryDefaults::default(),
146            routes: RouteTable::new(),
147            handlers: HashMap::new(),
148            describe_routes: true,
149        };
150        app.register_description();
151        app
152    }
153
154    /// The same application with another title and version.
155    pub fn with_info(mut self, info: ServiceInfo) -> Self {
156        self.info = info;
157        self
158    }
159
160    /// The same application with a CORS policy.
161    pub fn with_cors(mut self, cors: CorsPolicy) -> Self {
162        self.cors = Some(cors);
163        self
164    }
165
166    /// The same application producing only these representations.
167    pub fn with_supported<I: IntoIterator<Item = String>>(mut self, supported: I) -> Self {
168        self.supported = Some(supported.into_iter().collect());
169        self
170    }
171
172    /// The same application refusing request bodies larger than this.
173    pub fn with_max_body_bytes(mut self, max_body_bytes: usize) -> Self {
174        self.max_body_bytes = max_body_bytes;
175        self
176    }
177
178    /// The same application with other collection page-size defaults.
179    pub fn with_query_defaults(mut self, query_defaults: QueryDefaults) -> Self {
180        self.query_defaults = query_defaults;
181        self
182    }
183
184    /// The same application without the description routes.
185    pub fn without_description(mut self) -> Self {
186        self.describe_routes = false;
187        self.routes.remove("GET", DESCRIPTION_PATH);
188        self.routes.remove("GET", OPENAPI_PATH);
189        self
190    }
191
192    // Registration -----------------------------------------------------------
193
194    /// Register an asynchronous handler for a method and a path.
195    ///
196    /// # Panics
197    ///
198    /// When the method is not one of [`HTTP_METHODS`], which is a mistake in the
199    /// program rather than in a request.
200    pub fn route<F, Fut, R>(
201        &mut self,
202        method: &str,
203        path: &str,
204        summary: &str,
205        handler: F,
206    ) -> &mut Self
207    where
208        F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
209        Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
210        R: Into<LinoResult> + Send + 'static,
211    {
212        let normalized = method.to_uppercase();
213        assert!(
214            HTTP_METHODS.contains(&normalized.as_str()),
215            "Unsupported HTTP method: {method}"
216        );
217
218        self.routes
219            .register(&normalized, path, RouteMeta::summary(summary));
220        let handler: Handler = Arc::new(move |request| {
221            let future = handler(request);
222            Box::pin(async move { future.await.map(Into::into) })
223        });
224        self.handlers
225            .insert((normalized, path.to_string()), handler);
226        self
227    }
228
229    /// Register a synchronous handler for a method and a path.
230    pub fn route_fn<F, R>(
231        &mut self,
232        method: &str,
233        path: &str,
234        summary: &str,
235        handler: F,
236    ) -> &mut Self
237    where
238        F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
239        R: Into<LinoResult> + Send + 'static,
240    {
241        self.route(method, path, summary, move |request| {
242            std::future::ready(handler(request))
243        })
244    }
245
246    /// Register an asynchronous `GET` handler.
247    pub fn get<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
248    where
249        F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
250        Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
251        R: Into<LinoResult> + Send + 'static,
252    {
253        self.route("GET", path, summary, handler)
254    }
255
256    /// Register an asynchronous `POST` handler.
257    pub fn post<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
258    where
259        F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
260        Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
261        R: Into<LinoResult> + Send + 'static,
262    {
263        self.route("POST", path, summary, handler)
264    }
265
266    /// Register an asynchronous `PUT` handler.
267    pub fn put<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
268    where
269        F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
270        Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
271        R: Into<LinoResult> + Send + 'static,
272    {
273        self.route("PUT", path, summary, handler)
274    }
275
276    /// Register an asynchronous `PATCH` handler.
277    pub fn patch<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
278    where
279        F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
280        Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
281        R: Into<LinoResult> + Send + 'static,
282    {
283        self.route("PATCH", path, summary, handler)
284    }
285
286    /// Register an asynchronous `DELETE` handler.
287    pub fn delete<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
288    where
289        F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
290        Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
291        R: Into<LinoResult> + Send + 'static,
292    {
293        self.route("DELETE", path, summary, handler)
294    }
295
296    /// Register a synchronous `GET` handler.
297    pub fn get_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
298    where
299        F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
300        R: Into<LinoResult> + Send + 'static,
301    {
302        self.route_fn("GET", path, summary, handler)
303    }
304
305    /// Register a synchronous `POST` handler.
306    pub fn post_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
307    where
308        F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
309        R: Into<LinoResult> + Send + 'static,
310    {
311        self.route_fn("POST", path, summary, handler)
312    }
313
314    /// Register a synchronous `PUT` handler.
315    pub fn put_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
316    where
317        F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
318        R: Into<LinoResult> + Send + 'static,
319    {
320        self.route_fn("PUT", path, summary, handler)
321    }
322
323    /// Register a synchronous `PATCH` handler.
324    pub fn patch_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
325    where
326        F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
327        R: Into<LinoResult> + Send + 'static,
328    {
329        self.route_fn("PATCH", path, summary, handler)
330    }
331
332    /// Register a synchronous `DELETE` handler.
333    pub fn delete_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
334    where
335        F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
336        R: Into<LinoResult> + Send + 'static,
337    {
338        self.route_fn("DELETE", path, summary, handler)
339    }
340
341    /// Register the six CRUD routes of a collection backed by a store.
342    ///
343    /// See [`crate::resource::register_resource`] for the options.
344    pub fn resource<S: Store + 'static>(
345        &mut self,
346        path: &str,
347        store: Arc<S>,
348        options: ResourceOptions,
349    ) -> &mut Self {
350        register_resource(self, path, store, options);
351        self
352    }
353
354    /// Register the service description routes of specification section 9.
355    ///
356    /// The documents are rendered when they are asked for rather than here,
357    /// because they describe every route, including the ones registered after
358    /// this call.
359    fn register_description(&mut self) {
360        self.routes.register(
361            "GET",
362            DESCRIPTION_PATH,
363            RouteMeta::summary("Service description"),
364        );
365        self.routes.register(
366            "GET",
367            OPENAPI_PATH,
368            RouteMeta::summary("OpenAPI 3.1 description"),
369        );
370    }
371
372    // Description ------------------------------------------------------------
373
374    /// The native service description of specification section 9.
375    pub fn describe(&self) -> LinoValue {
376        service_description(
377            &self.info,
378            &self.routes.describe(),
379            self.supported.as_deref(),
380        )
381    }
382
383    /// The OpenAPI 3.1 rendering of the service description.
384    pub fn openapi(&self) -> LinoValue {
385        openapi_document(
386            &self.info,
387            &self.routes.describe(),
388            self.supported.as_deref(),
389        )
390    }
391
392    /// The route table, for inspection.
393    pub fn routes(&self) -> &RouteTable {
394        &self.routes
395    }
396
397    // Request handling -------------------------------------------------------
398
399    /// Run one decoded request through routing and its handler.
400    ///
401    /// # Errors
402    ///
403    /// A `404` when no route matches, a `405` when the route does not serve the
404    /// method, and anything the handler reports.
405    pub async fn handle(&self, mut request: LinoRequest) -> Result<ResponseParts, LinoHttpError> {
406        let Some((entry, params)) = self.routes.match_path(&request.path) else {
407            return Err(LinoHttpError::new(
408                404,
409                Some(&format!("No resource at {}", request.path)),
410            ));
411        };
412        request.params = params;
413        let pattern = entry.pattern.clone();
414        let allowed = self.routes.allowed_methods_of(entry).join(", ");
415
416        let method = request.method.clone();
417        // HEAD is served by the GET handler with the body dropped, and OPTIONS is
418        // answered from the route table unless a handler claims it.
419        let lookup = if method == "HEAD" && entry.has_method("GET") {
420            "GET".to_string()
421        } else {
422            method.clone()
423        };
424
425        if method == "OPTIONS" && !entry.has_method("OPTIONS") {
426            let mut headers = Headers::new();
427            headers.insert("Allow", allowed);
428            return Ok(ResponseParts::new(204, headers, ""));
429        }
430
431        if !entry.has_method(&lookup) {
432            return Err(LinoHttpError::new(
433                405,
434                Some(&format!("{method} is not allowed on {}", request.path)),
435            )
436            .with_header("Allow", allowed));
437        }
438
439        let result = match self.handlers.get(&(lookup.clone(), pattern.clone())) {
440            Some(handler) => handler(request.clone()).await?,
441            None => self.describe_route(&pattern)?,
442        };
443
444        build_response(
445            &result.body,
446            result.status,
447            result.media_type.as_deref().unwrap_or(&request.media_type),
448            &result.headers,
449            &request.headers,
450            &method,
451            EtagPolicy {
452                etag: result.etag,
453                preconditions: result.preconditions,
454                require_precondition: result.require_precondition,
455            },
456        )
457    }
458
459    /// Serve one of the description routes registered by the constructor.
460    ///
461    /// # Errors
462    ///
463    /// A `500` when the path has no handler and is not a description route,
464    /// which can only happen if the route table and the handlers disagree.
465    fn describe_route(&self, pattern: &str) -> Result<LinoResult, LinoHttpError> {
466        match pattern {
467            DESCRIPTION_PATH if self.describe_routes => Ok(self.describe().into()),
468            OPENAPI_PATH if self.describe_routes => Ok(raw_response(
469                &format!("{}\n", to_json_pretty(&self.openapi())),
470                JSON_CONTENT_TYPE,
471                200,
472            )),
473            other => Err(LinoHttpError::new(
474                500,
475                Some(&format!("No handler registered for {other}")),
476            )),
477        }
478    }
479
480    /// Answer one raw request, decoding it and rendering any error as problem
481    /// details.
482    ///
483    /// This is the whole application in one call: negotiation, body decoding,
484    /// routing, CORS and problem rendering. No error escapes it.
485    pub async fn respond(&self, raw: RawRequest) -> ResponseParts {
486        let method = raw.method.to_uppercase();
487        let extra_headers = match &self.cors {
488            Some(policy) => cors_headers(policy, raw.headers.get("origin")),
489            None => Headers::new(),
490        };
491        let preflight =
492            method == "OPTIONS" && raw.headers.contains("access-control-request-method");
493
494        let mut media_type = LINO_CONTENT_TYPE.to_string();
495        let instance = if raw.query_string.is_empty() {
496            raw.path.clone()
497        } else {
498            format!("{}?{}", raw.path, raw.query_string)
499        };
500
501        let mut parts = if preflight && !extra_headers.is_empty() {
502            ResponseParts::new(204, Headers::new(), "")
503        } else {
504            match self.decode_and_handle(&raw, &method, &mut media_type).await {
505                Ok(parts) => parts,
506                Err(error) => build_problem_response(&error, &media_type, Some(&instance)),
507            }
508        };
509
510        parts.headers.extend_missing(&extra_headers);
511        if extra_headers
512            .get("Access-Control-Allow-Origin")
513            .is_some_and(|origin| origin != "*")
514        {
515            append_vary(&mut parts.headers, "Origin");
516        }
517        if method == "HEAD" {
518            parts.body = String::new();
519        }
520        parts
521    }
522
523    /// Negotiate, decode and route one raw request.
524    ///
525    /// # Errors
526    ///
527    /// A `406`, a `413`, a `415`, a `400`, or anything routing and the handler
528    /// report; [`LinoApp::respond`] turns them into problem details.
529    async fn decode_and_handle(
530        &self,
531        raw: &RawRequest,
532        method: &str,
533        media_type: &mut String,
534    ) -> Result<ResponseParts, LinoHttpError> {
535        *media_type = negotiate_request(&raw.headers, self.supported.as_deref())?;
536        let decoded = decode_request_body(
537            &raw.body,
538            raw.headers.get("content-type"),
539            self.max_body_bytes,
540        )?;
541
542        let mut request = LinoRequest::new(method, &raw.path);
543        request.headers = raw.headers.clone();
544        request.query_string = raw.query_string.clone();
545        request.media_type = media_type.clone();
546        request.body = decoded.value;
547        request.request_media_type = decoded.media_type;
548        request.query_defaults = self.query_defaults;
549        self.handle(request).await
550    }
551}
552
553/// Create a new application, for symmetry with the other packages.
554pub fn create_lino_app() -> LinoApp {
555    LinoApp::new()
556}
557
558/// A body that carries no value, for handlers that answer with a status alone.
559pub const EMPTY: Body = Body::Empty;