tide_disco/
app.rs

1// Copyright (c) 2022 Espresso Systems (espressosys.com)
2// This file is part of the tide-disco library.
3
4// You should have received a copy of the MIT License
5// along with the tide-disco library. If not, see <https://mit-license.org/>.
6
7use crate::{
8    api::{Api, ApiError, ApiInner, ApiVersion},
9    dispatch::{self, DispatchError, Trie},
10    healthcheck::{HealthCheck, HealthStatus},
11    http,
12    method::Method,
13    middleware::{request_params, AddErrorBody, MetricsMiddleware},
14    request::RequestParams,
15    route::{health_check_response, respond_with, Handler, Route, RouteError},
16    socket::SocketError,
17    Html, StatusCode,
18};
19use async_std::sync::Arc;
20use derive_more::From;
21use futures::future::{BoxFuture, FutureExt};
22use include_dir::{include_dir, Dir};
23use lazy_static::lazy_static;
24use maud::{html, PreEscaped};
25use rand::Rng;
26use semver::Version;
27use serde::{Deserialize, Serialize};
28use serde_with::{serde_as, DisplayFromStr};
29use snafu::{ResultExt, Snafu};
30use std::{
31    collections::btree_map::BTreeMap,
32    convert::Infallible,
33    env,
34    fmt::Display,
35    fs, io,
36    ops::{Deref, DerefMut},
37    path::PathBuf,
38};
39use tide::{
40    http::headers::HeaderValue,
41    security::{CorsMiddleware, Origin},
42};
43use tide_websockets::WebSocket;
44use vbs::version::StaticVersionType;
45
46pub use tide::listener::{Listener, ToListener};
47
48/// A tide-disco server application.
49///
50/// An [App] is a collection of API modules, plus a global `State`. Modules can be registered by
51/// constructing an [Api] for each module and calling [App::register_module]. Once all of the
52/// desired modules are registered, the app can be converted into an asynchronous server task using
53/// [App::serve].
54///
55/// Note that the [`App`] is bound to a binary serialization version `VER`. This format only applies
56/// to application-level endpoints like `/version` and `/healthcheck`. The binary format version in
57/// use by any given API module may differ, depending on the supported version of the API.
58#[derive(Debug)]
59pub struct App<State, Error> {
60    pub(crate) modules: Trie<ApiInner<State, Error>>,
61    pub(crate) state: Arc<State>,
62    app_version: Option<Version>,
63}
64
65/// An error encountered while building an [App].
66#[derive(Clone, Debug, From, Snafu, PartialEq, Eq)]
67pub enum AppError {
68    Api { source: ApiError },
69    Dispatch { source: DispatchError },
70}
71
72impl<State: Send + Sync + 'static, Error: 'static> App<State, Error> {
73    /// Create a new [App] with a given state.
74    pub fn with_state(state: State) -> Self {
75        Self {
76            modules: Default::default(),
77            state: Arc::new(state),
78            app_version: None,
79        }
80    }
81
82    /// Create and register an API module.
83    ///
84    /// Creates a new [`Api`] with the given `api` specification and returns an RAII guard for this
85    /// API. The guard can be used to access the API module, configure it, and populate its
86    /// handlers. When [`Module::register`] is called on the guard (or the guard is dropped), the
87    /// module will be registered in this [`App`] as if by calling
88    /// [`register_module`](Self::register_module).
89    pub fn module<'a, ModuleError, ModuleVersion>(
90        &'a mut self,
91        base_url: &'a str,
92        api: impl Into<toml::Value>,
93    ) -> Result<Module<'a, State, Error, ModuleError, ModuleVersion>, AppError>
94    where
95        Error: crate::Error + From<ModuleError>,
96        ModuleError: Send + Sync + 'static,
97        ModuleVersion: StaticVersionType + 'static,
98    {
99        Ok(Module {
100            app: self,
101            base_url,
102            api: Some(Api::new(api).context(ApiSnafu)?),
103        })
104    }
105
106    /// Register an API module.
107    ///
108    /// The module `api` will be registered as an implementation of the module hosted under the URL
109    /// prefix `base_url`.
110    ///
111    /// # Versioning
112    ///
113    /// Multiple versions of the same [`Api`] may be registered by calling this function several
114    /// times with the same `base_url`, and passing in different APIs which must have different
115    /// _major_ versions. The API version can be set using [`Api::with_version`].
116    ///
117    /// When multiple versions of the same API are registered, requests for endpoints directly under
118    /// the base URL, like `GET /base_url/endpoint`, will always be dispatched to the latest
119    /// available version of the API. There will in addition be an extension of `base_url` for each
120    /// major version registered, so `GET /base_url/v1/endpoint` will always dispatch to the
121    /// `endpoint` handler in the module with major version 1, if it exists, regardless of what the
122    /// latest version is.
123    ///
124    /// It is an error to register multiple versions of the same module with the same major version.
125    /// It is _not_ an error to register non-sequential versions of a module. For example, you could
126    /// have `/base_url/v2` and `/base_url/v4`, but not `v1` or `v3`. Requests for `v1` or `v3` will
127    /// simply fail.
128    ///
129    /// The intention of this functionality is to allow for non-disruptive breaking updates. Rather
130    /// than deploying a new major version of the API with breaking changes _in place of_ the old
131    /// version, breaking all your clients, you can continue to serve the old version for some
132    /// period of time under a version prefix. Clients can point at this version prefix until they
133    /// update their software to use the new version, on their own time.
134    ///
135    /// Note that non-breaking changes (e.g. new endpoints) can be deployed in place of an existing
136    /// API without even incrementing the major version. The need for serving two versions of an API
137    /// simultaneously only arises when you have breaking changes.
138    pub fn register_module<ModuleError, ModuleVersion>(
139        &mut self,
140        base_url: &str,
141        api: Api<State, ModuleError, ModuleVersion>,
142    ) -> Result<&mut Self, AppError>
143    where
144        Error: crate::Error + From<ModuleError>,
145        ModuleError: Send + Sync + 'static,
146        ModuleVersion: StaticVersionType + 'static,
147    {
148        let mut api = api.map_err(Error::from).into_inner();
149        api.set_name(base_url.to_string());
150
151        let major_version = match api.version().api_version {
152            Some(version) => version.major,
153            None => {
154                // If no version is explicitly specified, default to 0.
155                0
156            }
157        };
158
159        self.modules
160            .insert(dispatch::split(base_url), major_version, api)?;
161        Ok(self)
162    }
163
164    /// Set the application version.
165    ///
166    /// The version information will automatically be included in responses to `GET /version`.
167    ///
168    /// This is the version of the overall application, which may encompass several APIs, each with
169    /// their own version. Changes to the version of any of the APIs which make up this application
170    /// should imply a change to the application version, but the application version may also
171    /// change without changing any of the API versions.
172    ///
173    /// This version is optional, as the `/version` endpoint will automatically include the version
174    /// of each registered API, which is usually enough to uniquely identify the application. Set
175    /// this explicitly if you want to track the version of additional behavior or interfaces which
176    /// are not encompassed by the sub-modules of this application.
177    ///
178    /// If you set an application version, it is a good idea to use the version of the application
179    /// crate found in Cargo.toml. This can be automatically found at build time using the
180    /// environment variable `CARGO_PKG_VERSION` and the [env!] macro. As long as the following code
181    /// is contained in the application crate, it should result in a reasonable version:
182    ///
183    /// ```
184    /// # use vbs::version::StaticVersion;
185    /// # type StaticVer01 = StaticVersion<0, 1>;
186    /// # fn ex(app: &mut tide_disco::App<(), ()>) {
187    /// app.with_version(env!("CARGO_PKG_VERSION").parse().unwrap());
188    /// # }
189    /// ```
190    pub fn with_version(&mut self, version: Version) -> &mut Self {
191        self.app_version = Some(version);
192        self
193    }
194
195    /// Get the version of this application.
196    pub fn version(&self) -> AppVersion {
197        AppVersion {
198            app_version: self.app_version.clone(),
199            disco_version: env!("CARGO_PKG_VERSION").parse().unwrap(),
200            modules: self
201                .modules
202                .iter()
203                .map(|module| {
204                    (
205                        module.path(),
206                        module
207                            .versions
208                            .values()
209                            .rev()
210                            .map(|api| api.version())
211                            .collect(),
212                    )
213                })
214                .collect(),
215        }
216    }
217
218    /// Check the health of each registered module in response to a request.
219    ///
220    /// The response includes a status code for each module, which will be [StatusCode::OK] if the
221    /// module is healthy. Detailed health status from each module is not included in the response
222    /// (due to type erasure) but can be queried using [module_health](Self::module_health) or by
223    /// hitting the endpoint `GET /:module/healthcheck`.
224    pub async fn health(&self, req: RequestParams, state: &State) -> AppHealth {
225        let mut modules_health = BTreeMap::<String, BTreeMap<_, _>>::new();
226        let mut status = HealthStatus::Available;
227        for module in &self.modules {
228            let versions_health = modules_health.entry(module.path()).or_default();
229            for (version, api) in &module.versions {
230                let health = StatusCode::from(api.health(req.clone(), state).await.status());
231                if health != StatusCode::OK {
232                    status = HealthStatus::Unhealthy;
233                }
234                versions_health.insert(*version, health);
235            }
236        }
237        AppHealth {
238            status,
239            modules: modules_health,
240        }
241    }
242
243    /// Check the health of the named module.
244    ///
245    /// The resulting [Response](tide::Response) has a status code which is [StatusCode::OK] if the
246    /// module is healthy. The response body is constructed from the results of the module's
247    /// registered healthcheck handler. If the module does not have an explicit healthcheck
248    /// handler, the response will be a [HealthStatus].
249    ///
250    /// `major_version` can be used to query the health status of a specific version of the desired
251    /// module. If it is not provided, the most recent supported version will be queried.
252    ///
253    /// If there is no module with the given name or version, returns [None].
254    pub async fn module_health(
255        &self,
256        req: RequestParams,
257        state: &State,
258        module: &str,
259        major_version: Option<u64>,
260    ) -> Option<tide::Response> {
261        let module = self.modules.get(dispatch::split(module))?;
262        let api = match major_version {
263            Some(v) => module.versions.get(&v)?,
264            None => module.versions.last_key_value()?.1,
265        };
266        Some(api.health(req, state).await)
267    }
268}
269
270static DEFAULT_PUBLIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/public/media");
271lazy_static! {
272    static ref DEFAULT_PUBLIC_PATH: PathBuf = {
273        // Generate a random number to index into `/tmp` with
274        let mut rng = rand::thread_rng();
275        let index: u64 = rng.gen();
276
277        // The contents of the default public directory are included in the binary. The first time
278        // the default directory is used, if ever, we extract them to a directory on the host file
279        // system and return the path to that directory.
280        let path = PathBuf::from(format!("/tmp/tide-disco/{}/public/media", index));
281        // If the path already exists, move it aside so we can update it.
282        let _ = fs::rename(&path, path.with_extension("old"));
283        DEFAULT_PUBLIC_DIR.extract(&path).unwrap();
284        path
285    };
286}
287
288impl<State, Error> App<State, Error>
289where
290    State: Send + Sync + 'static,
291    Error: 'static + crate::Error,
292{
293    /// Serve the [App] asynchronously.
294    ///
295    /// `VER` controls the binary format version used for responses to top-level endpoints like
296    /// `/version` and `/healthcheck`. All endpoints for specific API modules will use the format
297    /// version of that module (`ModuleVersion` when the module was
298    /// [registered](Self::register_module)).
299    pub async fn serve<L, VER>(self, listener: L, bind_version: VER) -> io::Result<()>
300    where
301        L: ToListener<Arc<Self>>,
302        VER: StaticVersionType + 'static,
303    {
304        let state = Arc::new(self);
305        let mut server = tide::Server::with_state(state.clone());
306        server.with(Self::version_middleware);
307        server.with(AddErrorBody::<Error>::with_version::<VER>());
308        server.with(
309            CorsMiddleware::new()
310                .allow_methods("GET, POST".parse::<HeaderValue>().unwrap())
311                .allow_headers("*".parse::<HeaderValue>().unwrap())
312                .allow_origin(Origin::from("*"))
313                .allow_credentials(true),
314        );
315
316        for module in &state.modules {
317            Self::register_api(&mut server, module.prefix.clone(), &module.versions)?;
318        }
319
320        // Register app-level routes summarizing the status and documentation of all the registered
321        // modules. We skip this step if this is a singleton app with only one module registered at
322        // the root URL, as these app-level endpoints would conflict with the (probably more
323        // specific) API-level status endpoints.
324        if !state.modules.is_singleton() {
325            // Register app-level automatic routes: `healthcheck` and `version`.
326            server
327                .at("healthcheck")
328                .get(move |req: tide::Request<Arc<Self>>| async move {
329                    let state = req.state().clone();
330                    let app_state = &*state.state;
331                    let req = request_params(req, &[]).await?;
332                    let accept = req.accept()?;
333                    let res = state.health(req, app_state).await;
334                    Ok(health_check_response::<_, VER>(&accept, res))
335                });
336            server
337                .at("version")
338                .get(move |req: tide::Request<Arc<Self>>| async move {
339                    let accept = RequestParams::accept_from_headers(&req)?;
340                    respond_with(&accept, req.state().version(), bind_version)
341                        .map_err(|err| Error::from_route_error::<Infallible>(err).into_tide_error())
342                });
343
344            // Serve documentation at the root URL for discoverability
345            server
346                .at("/")
347                .all(move |req: tide::Request<Arc<Self>>| async move {
348                    Ok(tide::Response::from(Self::top_level_docs(req)))
349                });
350        }
351
352        server.listen(listener).await
353    }
354
355    fn list_apis(&self) -> Html {
356        html! {
357            ul {
358                @for module in &self.modules {
359                    li {
360                        // Link to the alias for the latest version as the primary link.
361                        a href=(format!("/{}", module.path())) {(module.path())}
362                        // Add a superscript link (link a footnote) for each specific supported
363                        // version, linking to documentation for that specific version.
364                        @for version in module.versions.keys().rev() {
365                            sup {
366                                a href=(format!("/v{version}/{}", module.path())) {
367                                    (format!("[v{version}]"))
368                                }
369                            }
370                        }
371                        " "
372                        // Take the description of the latest supported version.
373                        (PreEscaped(module.versions.last_key_value().unwrap().1.short_description()))
374                    }
375                }
376            }
377        }
378    }
379
380    fn register_api(
381        server: &mut tide::Server<Arc<Self>>,
382        prefix: Vec<String>,
383        versions: &BTreeMap<u64, ApiInner<State, Error>>,
384    ) -> io::Result<()> {
385        for (version, api) in versions {
386            Self::register_api_version(server, &prefix, *version, api)?;
387        }
388        Ok(())
389    }
390
391    fn register_api_version(
392        server: &mut tide::Server<Arc<Self>>,
393        prefix: &[String],
394        version: u64,
395        api: &ApiInner<State, Error>,
396    ) -> io::Result<()> {
397        // Clippy complains if the only non-trivial operation in an `unwrap_or_else` closure is
398        // a deref, but for `lazy_static` types, deref is an effectful operation that (in this
399        // case) causes a directory to be renamed and another extracted. We only want to execute
400        // this if we need to (if `api.public()` is `None`) so we disable the lint.
401        #[allow(clippy::unnecessary_lazy_evaluations)]
402        server
403            .at("/public")
404            .at(&format!("v{version}"))
405            .at(&prefix.join("/"))
406            .serve_dir(api.public().unwrap_or_else(|| &DEFAULT_PUBLIC_PATH))?;
407
408        // Register routes for this API.
409        let mut version_endpoint = server.at(&format!("/v{version}"));
410        let mut api_endpoint = if prefix.is_empty() {
411            version_endpoint
412        } else {
413            version_endpoint.at(&prefix.join("/"))
414        };
415        api_endpoint.with(AddErrorBody::new(api.error_handler()));
416        for (path, routes) in api.routes_by_path() {
417            let mut endpoint = api_endpoint.at(path);
418            let routes = routes.collect::<Vec<_>>();
419
420            // Register socket and metrics middlewares. These must be registered before any
421            // regular HTTP routes, because Tide only applies middlewares to routes which were
422            // already registered before the route handler.
423            if let Some(socket_route) = routes.iter().find(|route| route.method() == Method::Socket)
424            {
425                // If there is a socket route with this pattern, add the socket middleware to
426                // all endpoints registered under this pattern, so that any request with any
427                // method that has the socket upgrade headers will trigger a WebSockets upgrade.
428                Self::register_socket(prefix.to_vec(), version, &mut endpoint, socket_route);
429            }
430            if let Some(metrics_route) = routes
431                .iter()
432                .find(|route| route.method() == Method::Metrics)
433            {
434                // If there is a metrics route with this pattern, add the metrics middleware to
435                // all endpoints registered under this pattern, so that a request to this path
436                // with the right headers will return metrics instead of going through the
437                // normal method-based dispatching.
438                Self::register_metrics(prefix.to_vec(), version, &mut endpoint, metrics_route);
439            }
440
441            // Register the HTTP routes.
442            for route in routes {
443                if let Method::Http(method) = route.method() {
444                    Self::register_route(prefix.to_vec(), version, &mut endpoint, route, method);
445                }
446            }
447        }
448
449        // Register automatic routes for this API: documentation, `healthcheck` and `version`. Serve
450        // documentation at the root of the API (with or without a trailing slash).
451        for path in ["", "/"] {
452            let prefix = prefix.to_vec();
453            api_endpoint
454                .at(path)
455                .all(move |req: tide::Request<Arc<Self>>| {
456                    let prefix = prefix.clone();
457                    async move {
458                        let api = &req.state().clone().modules[&prefix].versions[&version];
459                        Ok(api.documentation())
460                    }
461                });
462        }
463        {
464            let prefix = prefix.to_vec();
465            api_endpoint
466                .at("*path")
467                .all(move |req: tide::Request<Arc<Self>>| {
468                    let prefix = prefix.clone();
469                    async move {
470                        // The request did not match any route. Serve documentation for the API.
471                        let api = &req.state().clone().modules[&prefix].versions[&version];
472                        let docs = html! {
473                            "No route matches /" (req.param("path")?)
474                            br{}
475                            (api.documentation())
476                        };
477                        Ok(tide::Response::builder(StatusCode::NOT_FOUND)
478                            .body(docs.into_string())
479                            .build())
480                    }
481                });
482        }
483        {
484            let prefix = prefix.to_vec();
485            api_endpoint
486                .at("healthcheck")
487                .get(move |req: tide::Request<Arc<Self>>| {
488                    let prefix = prefix.clone();
489                    async move {
490                        let api = &req.state().clone().modules[&prefix].versions[&version];
491                        let state = req.state().clone();
492                        Ok(api
493                            .health(request_params(req, &[]).await?, &state.state)
494                            .await)
495                    }
496                });
497        }
498        {
499            let prefix = prefix.to_vec();
500            api_endpoint
501                .at("version")
502                .get(move |req: tide::Request<Arc<Self>>| {
503                    let prefix = prefix.clone();
504                    async move {
505                        let api = &req.state().modules[&prefix].versions[&version];
506                        let accept = RequestParams::accept_from_headers(&req)?;
507                        api.version_handler()(&accept, api.version())
508                            .map_err(|err| Error::from_route_error(err).into_tide_error())
509                    }
510                });
511        }
512
513        Ok(())
514    }
515
516    fn register_route(
517        api: Vec<String>,
518        version: u64,
519        endpoint: &mut tide::Route<Arc<Self>>,
520        route: &Route<State, Error>,
521        method: http::Method,
522    ) {
523        let name = route.name();
524        endpoint.method(method, move |req: tide::Request<Arc<Self>>| {
525            let name = name.clone();
526            let api = api.clone();
527            async move {
528                let route = &req.state().clone().modules[&api].versions[&version][&name];
529                let state = &*req.state().clone().state;
530                let req = request_params(req, route.params()).await?;
531                route
532                    .handle(req, state)
533                    .await
534                    .map_err(|err| match err {
535                        RouteError::AppSpecific(err) => err,
536                        _ => Error::from_route_error(err),
537                    })
538                    .map_err(|err| err.into_tide_error())
539            }
540        });
541    }
542
543    fn register_metrics(
544        api: Vec<String>,
545        version: u64,
546        endpoint: &mut tide::Route<Arc<Self>>,
547        route: &Route<State, Error>,
548    ) {
549        let name = route.name();
550        if route.has_handler() {
551            // If there is a metrics handler, add middleware to the endpoint to intercept the
552            // request and respond with metrics, rather than the usual HTTP dispatching, if the
553            // appropriate headers are set.
554            endpoint.with(MetricsMiddleware::new(name.clone(), api.clone(), version));
555        }
556
557        // Register a catch-all HTTP handler for the route, which serves the route documentation as
558        // HTML. This ensures that there is at least one endpoint registered with the Tide
559        // dispatcher, so that the middleware actually fires on requests to this path. In addition,
560        // this handler will trigger for requests that are not otherwise valid, aiding in
561        // discoverability.
562        //
563        // We register the default handler using `all`, which makes it act as a fallback handler.
564        // This means if there are other, non-metrics routes with this same path, we will still
565        // dispatch to them if the path is hit with the appropriate method.
566        Self::register_fallback(api, version, endpoint, route);
567    }
568
569    fn register_socket(
570        api: Vec<String>,
571        version: u64,
572        endpoint: &mut tide::Route<Arc<Self>>,
573        route: &Route<State, Error>,
574    ) {
575        let name = route.name();
576        if route.has_handler() {
577            // If there is a socket handler, add the [WebSocket] middleware to the endpoint, so that
578            // upgrade requests will automatically upgrade to a WebSockets connection.
579            let name = name.clone();
580            let api = api.clone();
581            endpoint.with(WebSocket::new(
582                move |req: tide::Request<Arc<Self>>, conn| {
583                    let name = name.clone();
584                    let api = api.clone();
585                    async move {
586                        let route = &req.state().clone().modules[&api].versions[&version][&name];
587                        let state = &*req.state().clone().state;
588                        let req = request_params(req, route.params()).await?;
589                        route
590                            .handle_socket(req, conn, state)
591                            .await
592                            .map_err(|err| match err {
593                                SocketError::AppSpecific(err) => err,
594                                _ => Error::from_socket_error(err),
595                            })
596                            .map_err(|err| err.into_tide_error())
597                    }
598                },
599            ));
600        }
601
602        // Register a catch-all HTTP handler for the route, which serves the route documentation as
603        // HTML. This ensures that there is at least one endpoint registered with the Tide
604        // dispatcher, so that the middleware actually fires on requests to this path. In addition,
605        // this handler will trigger for requests that are not valid WebSockets handshakes. The
606        // documentation should make clear that this is a WebSockets endpoint, aiding in
607        // discoverability. This will also trigger if there is no socket handler for this route,
608        // which will signal to the developer that they need to implement a socket handler for this
609        // route to work.
610        //
611        // We register the default handler using `all`, which makes it act as a fallback handler.
612        // This means if there are other, non-socket routes with this same path, we will still
613        // dispatch to them if the path is hit with the appropriate method.
614        Self::register_fallback(api, version, endpoint, route);
615    }
616
617    fn register_fallback(
618        api: Vec<String>,
619        version: u64,
620        endpoint: &mut tide::Route<Arc<Self>>,
621        route: &Route<State, Error>,
622    ) {
623        let name = route.name();
624        endpoint.all(move |req: tide::Request<Arc<Self>>| {
625            let name = name.clone();
626            let api = api.clone();
627            async move {
628                let route = &req.state().clone().modules[&api].versions[&version][&name];
629                route
630                    .default_handler()
631                    .map_err(|err| match err {
632                        RouteError::AppSpecific(err) => err,
633                        _ => Error::from_route_error(err),
634                    })
635                    .map_err(|err| err.into_tide_error())
636            }
637        });
638    }
639
640    /// Server middleware which returns redirect responses for requests lacking an explicit version
641    /// prefix.
642    fn version_middleware(
643        req: tide::Request<Arc<Self>>,
644        next: tide::Next<Arc<Self>>,
645    ) -> BoxFuture<tide::Result> {
646        async move {
647            let Some(path) = req.url().path_segments() else {
648                // If we can't parse the path, we can't run this middleware. Do our best by
649                // continuing the request processing lifecycle.
650                return Ok(next.run(req).await);
651            };
652            let path = path.collect::<Vec<_>>();
653            let Some(seg1) = path.first() else {
654                // This is the root URL, with no path segments. Nothing for this middleware to do.
655                return Ok(next.run(req).await);
656            };
657            if seg1.is_empty() {
658                // This is the root URL, with no path segments. Nothing for this middleware to do.
659                return Ok(next.run(req).await);
660            }
661
662            // The first segment is either a version identifier or (part of) an API identifier
663            // (implicitly requesting the latest version of the API). We handle these cases
664            // differently.
665            if let Some(version) = seg1.strip_prefix('v').and_then(|n| n.parse().ok()) {
666                // If the version identifier is present, we probably don't need a redirect. However,
667                // we still check if this is a valid version for the request API. If not, we will
668                // serve documentation listing the available versions.
669                let Some(module) = req.state().modules.search(&path[1..]) else {
670                    let message = format!("No API matches /{}", path[1..].join("/"));
671                    return Ok(Self::top_level_error(req, StatusCode::NOT_FOUND, message));
672                };
673                if !module.versions.contains_key(&version) {
674                    // This version is not supported, list suported versions.
675                    return Ok(html! {
676                        "Unsupported version v" (version) ". Supported versions are:"
677                        ul {
678                            @for v in module.versions.keys().rev() {
679                                li {
680                                    a href=(format!("/v{v}/{}", module.path())) { "v" (v) }
681                                }
682                            }
683                        }
684                    }
685                    .into());
686                }
687
688                // This is a valid request with a specific version. It should be handled
689                // successfully by the route handlers for this API.
690                Ok(next.run(req).await)
691            } else {
692                // If the first path segment is not a version prefix, then the path is either the
693                // name of an API (implicitly requesting the latest version) or one of the magic
694                // top-level endpoints (version, healthcheck). Validate the API and then redirect.
695                if !req.state().modules.is_singleton() && ["version", "healthcheck"].contains(seg1)
696                {
697                    return Ok(next.run(req).await);
698                }
699                let Some(module) = req.state().modules.search(&path) else {
700                    let message = format!("No API matches /{}", path.join("/"));
701                    return Ok(Self::top_level_error(req, StatusCode::NOT_FOUND, message));
702                };
703
704                let latest_version = *module.versions.last_key_value().unwrap().0;
705                let path = path.join("/");
706                Ok(tide::Redirect::temporary(format!("/v{latest_version}/{path}")).into())
707            }
708        }
709        .boxed()
710    }
711
712    /// Top-level documentation about the app.
713    fn top_level_docs(req: tide::Request<Arc<Self>>) -> PreEscaped<String> {
714        html! {
715            br {}
716            "This is a Tide Disco app composed of the following modules:"
717            (req.state().list_apis())
718        }
719    }
720
721    /// Documentation served when there is a routing error at the app level.
722    fn top_level_error(
723        req: tide::Request<Arc<Self>>,
724        status: StatusCode,
725        message: impl Display,
726    ) -> tide::Response {
727        let docs = html! {
728            (message.to_string())
729            (Self::top_level_docs(req))
730        };
731        tide::Response::builder(status)
732            .body(docs.into_string())
733            .build()
734    }
735}
736
737/// The health status of an application.
738#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
739pub struct AppHealth {
740    /// The status of the overall application.
741    ///
742    /// [HealthStatus::Available] if all of the application's modules are healthy, otherwise a
743    /// [HealthStatus] variant with [status](HealthCheck::status) other than 200.
744    pub status: HealthStatus,
745    /// The status of each registered module, indexed by version.
746    pub modules: BTreeMap<String, BTreeMap<u64, StatusCode>>,
747}
748
749impl HealthCheck for AppHealth {
750    fn status(&self) -> StatusCode {
751        self.status.status()
752    }
753}
754
755/// Version information about an application.
756#[serde_as]
757#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
758pub struct AppVersion {
759    /// The supported versions of each module registered with this application.
760    ///
761    /// Versions for each module are ordered from newest to oldest.
762    pub modules: BTreeMap<String, Vec<ApiVersion>>,
763
764    /// The version of this application.
765    #[serde_as(as = "Option<DisplayFromStr>")]
766    pub app_version: Option<Version>,
767
768    /// The version of the Tide Disco server framework.
769    #[serde_as(as = "DisplayFromStr")]
770    pub disco_version: Version,
771}
772
773/// RAII guard to ensure a module is registered after it is configured.
774///
775/// This type allows the owner to configure an [`Api`] module via the [`Deref`] and [`DerefMut`]
776/// traits. Once the API is configured, this object can be dropped, which will automatically
777/// register the module with the [`App`].
778///
779/// # Panics
780///
781/// Note that if anything goes wrong during module registration (for example, there is already an
782/// incompatible module registered with the same name), the drop implementation may panic. To handle
783/// errors without panicking, call [`register`](Self::register) explicitly.
784#[derive(Debug)]
785pub struct Module<'a, State, Error, ModuleError, ModuleVersion>
786where
787    State: Send + Sync + 'static,
788    Error: crate::Error + From<ModuleError> + 'static,
789    ModuleError: Send + Sync + 'static,
790    ModuleVersion: StaticVersionType + 'static,
791{
792    app: &'a mut App<State, Error>,
793    base_url: &'a str,
794    // This is only an [Option] so we can [take] out of it during [drop].
795    api: Option<Api<State, ModuleError, ModuleVersion>>,
796}
797
798impl<State, Error, ModuleError, ModuleVersion> Deref
799    for Module<'_, State, Error, ModuleError, ModuleVersion>
800where
801    State: Send + Sync + 'static,
802    Error: crate::Error + From<ModuleError> + 'static,
803    ModuleError: Send + Sync + 'static,
804    ModuleVersion: StaticVersionType + 'static,
805{
806    type Target = Api<State, ModuleError, ModuleVersion>;
807
808    fn deref(&self) -> &Self::Target {
809        self.api.as_ref().unwrap()
810    }
811}
812
813impl<State, Error, ModuleError, ModuleVersion> DerefMut
814    for Module<'_, State, Error, ModuleError, ModuleVersion>
815where
816    State: Send + Sync + 'static,
817    Error: crate::Error + From<ModuleError> + 'static,
818    ModuleError: Send + Sync + 'static,
819    ModuleVersion: StaticVersionType + 'static,
820{
821    fn deref_mut(&mut self) -> &mut Self::Target {
822        self.api.as_mut().unwrap()
823    }
824}
825
826impl<State, Error, ModuleError, ModuleVersion> Drop
827    for Module<'_, State, Error, ModuleError, ModuleVersion>
828where
829    State: Send + Sync + 'static,
830    Error: crate::Error + From<ModuleError> + 'static,
831    ModuleError: Send + Sync + 'static,
832    ModuleVersion: StaticVersionType + 'static,
833{
834    fn drop(&mut self) {
835        self.register_impl().unwrap();
836    }
837}
838
839impl<State, Error, ModuleError, ModuleVersion> Module<'_, State, Error, ModuleError, ModuleVersion>
840where
841    State: Send + Sync + 'static,
842    Error: crate::Error + From<ModuleError> + 'static,
843    ModuleError: Send + Sync + 'static,
844    ModuleVersion: StaticVersionType + 'static,
845{
846    /// Register this module with the linked app.
847    pub fn register(mut self) -> Result<(), AppError> {
848        self.register_impl()
849    }
850
851    /// Perform the logic of [`Self::register`] without consuming `self`, so this can be called from
852    /// `drop`.
853    fn register_impl(&mut self) -> Result<(), AppError> {
854        if let Some(api) = self.api.take() {
855            self.app.register_module(self.base_url, api)?;
856            Ok(())
857        } else {
858            // Already registered.
859            Ok(())
860        }
861    }
862}
863
864#[cfg(test)]
865mod test {
866    use super::*;
867    use crate::{
868        error::{Error, ServerError},
869        metrics::Metrics,
870        socket::Connection,
871        testing::{setup_test, test_ws_client, Client},
872        Url,
873    };
874    use async_std::{sync::RwLock, task::spawn};
875    use async_tungstenite::tungstenite::Message;
876    use futures::{FutureExt, SinkExt, StreamExt};
877    use portpicker::pick_unused_port;
878    use serde::de::DeserializeOwned;
879    use std::{borrow::Cow, fmt::Debug};
880    use toml::toml;
881    use vbs::{version::StaticVersion, BinarySerializer, Serializer};
882
883    type StaticVer01 = StaticVersion<0, 1>;
884    type SerializerV01 = Serializer<StaticVer01>;
885
886    type StaticVer02 = StaticVersion<0, 2>;
887    type SerializerV02 = Serializer<StaticVer02>;
888
889    type StaticVer03 = StaticVersion<0, 3>;
890    type SerializerV03 = Serializer<StaticVer03>;
891
892    #[derive(Clone, Copy, Debug)]
893    struct FakeMetrics;
894
895    impl Metrics for FakeMetrics {
896        type Error = ServerError;
897
898        fn export(&self) -> Result<String, Self::Error> {
899            Ok("METRICS".into())
900        }
901    }
902
903    /// Test route dispatching for routes with the same path and different methods.
904    #[async_std::test]
905    async fn test_method_dispatch() {
906        setup_test();
907
908        use crate::http::Method::*;
909
910        let mut app = App::<_, ServerError>::with_state(RwLock::new(FakeMetrics));
911        let api_toml = toml! {
912            [meta]
913            FORMAT_VERSION = "0.1.0"
914
915            [route.get_test]
916            PATH = ["/test"]
917            METHOD = "GET"
918
919            [route.post_test]
920            PATH = ["/test"]
921            METHOD = "POST"
922
923            [route.put_test]
924            PATH = ["/test"]
925            METHOD = "PUT"
926
927            [route.delete_test]
928            PATH = ["/test"]
929            METHOD = "DELETE"
930
931            [route.socket_test]
932            PATH = ["/test"]
933            METHOD = "SOCKET"
934
935            [route.metrics_test]
936            PATH = ["/test"]
937            METHOD = "METRICS"
938        };
939        {
940            let mut api = app
941                .module::<ServerError, StaticVer01>("mod", api_toml)
942                .unwrap();
943            api.get("get_test", |_req, _state| {
944                async move { Ok(Get.to_string()) }.boxed()
945            })
946            .unwrap()
947            .post("post_test", |_req, _state| {
948                async move { Ok(Post.to_string()) }.boxed()
949            })
950            .unwrap()
951            .put("put_test", |_req, _state| {
952                async move { Ok(Put.to_string()) }.boxed()
953            })
954            .unwrap()
955            .delete("delete_test", |_req, _state| {
956                async move { Ok(Delete.to_string()) }.boxed()
957            })
958            .unwrap()
959            .socket(
960                "socket_test",
961                |_req, mut conn: Connection<str, (), _, StaticVer01>, _state| {
962                    async move {
963                        conn.send("SOCKET").await.unwrap();
964                        Ok(())
965                    }
966                    .boxed()
967                },
968            )
969            .unwrap()
970            .metrics("metrics_test", |_req, state| {
971                async move { Ok(Cow::Borrowed(state)) }.boxed()
972            })
973            .unwrap();
974        }
975        let port = pick_unused_port().unwrap();
976        let url: Url = format!("http://localhost:{}", port).parse().unwrap();
977        spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
978        let client = Client::new(url.clone()).await;
979
980        // Regular HTTP methods.
981        for method in [Get, Post, Put, Delete] {
982            let res = client
983                .request(method, "mod/test")
984                .header("Accept", "application/json")
985                .send()
986                .await
987                .unwrap();
988            assert_eq!(res.status(), StatusCode::OK);
989            assert_eq!(res.json::<String>().await.unwrap(), method.to_string());
990        }
991
992        // Metrics with Accept header.
993        let res = client
994            .get("mod/test")
995            .header("Accept", "text/plain")
996            .send()
997            .await
998            .unwrap();
999        assert_eq!(res.status(), StatusCode::OK);
1000        assert_eq!(res.text().await.unwrap(), "METRICS");
1001
1002        // Metrics without Accept header.
1003        let res = client.get("mod/test").send().await.unwrap();
1004        assert_eq!(res.status(), StatusCode::OK);
1005        assert_eq!(res.text().await.unwrap(), "METRICS");
1006
1007        // Socket.
1008        let mut conn = test_ws_client(url.join("mod/test").unwrap()).await;
1009        let msg = conn.next().await.unwrap().unwrap();
1010        let body: String = match msg {
1011            Message::Text(m) => serde_json::from_str(&m).unwrap(),
1012            Message::Binary(m) => SerializerV01::deserialize(&m).unwrap(),
1013            m => panic!("expected Text or Binary message, but got {}", m),
1014        };
1015        assert_eq!(body, "SOCKET");
1016    }
1017
1018    /// Test route dispatching for routes with patterns containing different parmaeters
1019    #[async_std::test]
1020    async fn test_param_dispatch() {
1021        setup_test();
1022
1023        let mut app = App::<_, ServerError>::with_state(RwLock::new(()));
1024        let api_toml = toml! {
1025            [meta]
1026            FORMAT_VERSION = "0.1.0"
1027
1028            [route.test]
1029            PATH = ["/test/a/:a", "/test/b/:b"]
1030            ":a" = "Integer"
1031            ":b" = "Boolean"
1032        };
1033        {
1034            let mut api = app
1035                .module::<ServerError, StaticVer01>("mod", api_toml)
1036                .unwrap();
1037            api.get("test", |req, _state| {
1038                async move {
1039                    if let Some(a) = req.opt_integer_param::<_, i32>("a")? {
1040                        Ok(("a", a.to_string()))
1041                    } else {
1042                        Ok(("b", req.boolean_param("b")?.to_string()))
1043                    }
1044                }
1045                .boxed()
1046            })
1047            .unwrap();
1048        }
1049        let port = pick_unused_port().unwrap();
1050        let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1051        spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1052        let client = Client::new(url.clone()).await;
1053
1054        let res = client.get("mod/test/a/42").send().await.unwrap();
1055        assert_eq!(res.status(), StatusCode::OK);
1056        assert_eq!(
1057            res.json::<(String, String)>().await.unwrap(),
1058            ("a".to_string(), "42".to_string())
1059        );
1060
1061        let res = client.get("mod/test/b/true").send().await.unwrap();
1062        assert_eq!(res.status(), StatusCode::OK);
1063        assert_eq!(
1064            res.json::<(String, String)>().await.unwrap(),
1065            ("b".to_string(), "true".to_string())
1066        );
1067    }
1068
1069    #[async_std::test]
1070    async fn test_versions() {
1071        setup_test();
1072
1073        let mut app = App::<_, ServerError>::with_state(RwLock::new(()));
1074
1075        // Create two different, non-consecutive major versions of an API. One method will be
1076        // deleted in version 1, one will be added in version 3, and one will be present in both
1077        // versions (with a different implementation).
1078        let v1_toml = toml! {
1079            [meta]
1080            FORMAT_VERSION = "0.1.0"
1081
1082            [route.deleted]
1083            PATH = ["/deleted"]
1084
1085            [route.unchanged]
1086            PATH = ["/unchanged"]
1087        };
1088        let v3_toml = toml! {
1089            [meta]
1090            FORMAT_VERSION = "0.1.0"
1091
1092            [route.added]
1093            PATH = ["/added"]
1094
1095            [route.unchanged]
1096            PATH = ["/unchanged"]
1097        };
1098
1099        {
1100            let mut v1 = app
1101                .module::<ServerError, StaticVer01>("mod", v1_toml.clone())
1102                .unwrap();
1103            v1.with_version("1.0.0".parse().unwrap())
1104                .get("deleted", |_req, _state| {
1105                    async move { Ok("deleted v1") }.boxed()
1106                })
1107                .unwrap()
1108                .get("unchanged", |_req, _state| {
1109                    async move { Ok("unchanged v1") }.boxed()
1110                })
1111                .unwrap()
1112                // Add a custom healthcheck for the old version so we can check healthcheck routing.
1113                .with_health_check(|_state| {
1114                    async move { HealthStatus::TemporarilyUnavailable }.boxed()
1115                });
1116        }
1117        {
1118            // Registering the same major version twice is an error.
1119            let mut api = app
1120                .module::<ServerError, StaticVer01>("mod", v1_toml)
1121                .unwrap();
1122            api.with_version("1.1.1".parse().unwrap());
1123            assert_eq!(
1124                api.register().unwrap_err(),
1125                DispatchError::ModuleAlreadyExists {
1126                    prefix: "mod".into(),
1127                    version: 1,
1128                }
1129                .into()
1130            );
1131        }
1132        {
1133            let mut v3 = app
1134                .module::<ServerError, StaticVer01>("mod", v3_toml.clone())
1135                .unwrap();
1136            v3.with_version("3.0.0".parse().unwrap())
1137                .get("added", |_req, _state| {
1138                    async move { Ok("added v3") }.boxed()
1139                })
1140                .unwrap()
1141                .get("unchanged", |_req, _state| {
1142                    async move { Ok("unchanged v3") }.boxed()
1143                })
1144                .unwrap();
1145        }
1146
1147        let port = pick_unused_port().unwrap();
1148        let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1149        spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1150        let client = Client::new(url.clone()).await;
1151
1152        // First check that we can call all the expected methods.
1153        assert_eq!(
1154            "deleted v1",
1155            client
1156                .get("v1/mod/deleted")
1157                .send()
1158                .await
1159                .unwrap()
1160                .json::<String>()
1161                .await
1162                .unwrap()
1163        );
1164        assert_eq!(
1165            "unchanged v1",
1166            client
1167                .get("v1/mod/unchanged")
1168                .send()
1169                .await
1170                .unwrap()
1171                .json::<String>()
1172                .await
1173                .unwrap()
1174        );
1175        // For the v3 methods, we can query with or without a version prefix.
1176        for prefix in ["", "/v3"] {
1177            let span = tracing::info_span!("version", prefix);
1178            let _enter = span.enter();
1179
1180            assert_eq!(
1181                "added v3",
1182                client
1183                    .get(&format!("{prefix}/mod/added"))
1184                    .send()
1185                    .await
1186                    .unwrap()
1187                    .json::<String>()
1188                    .await
1189                    .unwrap()
1190            );
1191            assert_eq!(
1192                "unchanged v3",
1193                client
1194                    .get(&format!("{prefix}/mod/unchanged"))
1195                    .send()
1196                    .await
1197                    .unwrap()
1198                    .json::<String>()
1199                    .await
1200                    .unwrap()
1201            );
1202        }
1203
1204        // Test documentation for invalid routes.
1205        let check_docs = |version, route: &'static str| {
1206            let client = &client;
1207            async move {
1208                let span = tracing::info_span!("check_docs", ?version, route);
1209                let _enter = span.enter();
1210                tracing::info!("test invalid route docs");
1211
1212                let prefix = match version {
1213                    Some(v) => format!("/v{v}"),
1214                    None => "".into(),
1215                };
1216
1217                // Invalid route or no route with no version prefix redirects to documentation for
1218                // the latest supported version.
1219                let version = version.unwrap_or(3);
1220
1221                let res = client
1222                    .get(&format!("{prefix}/mod/{route}"))
1223                    .send()
1224                    .await
1225                    .unwrap();
1226                let docs = res.text().await.unwrap();
1227                if !route.is_empty() {
1228                    assert!(
1229                        docs.contains(&format!("No route matches /{route}")),
1230                        "{docs}"
1231                    );
1232                }
1233                assert!(
1234                    docs.contains(&format!("mod API {version}.0.0 Reference")),
1235                    "{docs}"
1236                );
1237            }
1238        };
1239
1240        for route in ["", "deleted"] {
1241            check_docs(None, route).await;
1242        }
1243        for route in ["", "deleted"] {
1244            check_docs(Some(3), route).await;
1245        }
1246        for route in ["", "added"] {
1247            check_docs(Some(1), route).await;
1248        }
1249
1250        // Request with an unsupported version lists the supported versions.
1251        let expected_html = html! {
1252            "Unsupported version v2. Supported versions are:"
1253            ul {
1254                li {
1255                    a href="/v3/mod" {"v3"}
1256                }
1257                li {
1258                    a href="/v1/mod" {"v1"}
1259                }
1260            }
1261        }
1262        .into_string();
1263        for route in ["", "/unchanged"] {
1264            let span = tracing::info_span!("unsupported_version_docs", route);
1265            let _enter = span.enter();
1266            tracing::info!("test unsupported version docs");
1267
1268            let res = client.get(&format!("/v2/mod{route}")).send().await.unwrap();
1269            let docs = res.text().await.unwrap();
1270            assert_eq!(docs, expected_html);
1271        }
1272
1273        // Test version endpoints.
1274        for version in [None, Some(1), Some(3)] {
1275            let span = tracing::info_span!("version_endpoints", version);
1276            let _enter = span.enter();
1277            tracing::info!("test version endpoints");
1278
1279            let prefix = match version {
1280                Some(v) => format!("/v{v}"),
1281                None => "".into(),
1282            };
1283            let res = client
1284                .get(&format!("{prefix}/mod/version"))
1285                .send()
1286                .await
1287                .unwrap();
1288            assert_eq!(
1289                res.json::<ApiVersion>()
1290                    .await
1291                    .unwrap()
1292                    .api_version
1293                    .unwrap()
1294                    .major,
1295                version.unwrap_or(3)
1296            );
1297        }
1298
1299        // Test the application version.
1300        let res = client.get("version").send().await.unwrap();
1301        assert_eq!(
1302            res.json::<AppVersion>().await.unwrap().modules["mod"],
1303            [
1304                ApiVersion {
1305                    api_version: Some("3.0.0".parse().unwrap()),
1306                    spec_version: "0.1.0".parse().unwrap(),
1307                },
1308                ApiVersion {
1309                    api_version: Some("1.0.0".parse().unwrap()),
1310                    spec_version: "0.1.0".parse().unwrap(),
1311                }
1312            ]
1313        );
1314
1315        // Test healthcheck endpoints.
1316        for version in [None, Some(1), Some(3)] {
1317            let span = tracing::info_span!("healthcheck_endpoints", version);
1318            let _enter = span.enter();
1319            tracing::info!("test healthcheck endpoints");
1320
1321            let prefix = match version {
1322                Some(v) => format!("/v{v}"),
1323                None => "".into(),
1324            };
1325            let res = client
1326                .get(&format!("{prefix}/mod/healthcheck"))
1327                .send()
1328                .await
1329                .unwrap();
1330            let status = res.status();
1331            let health: HealthStatus = res.json().await.unwrap();
1332            assert_eq!(health.status(), status);
1333            assert_eq!(
1334                health,
1335                if version == Some(1) {
1336                    HealthStatus::TemporarilyUnavailable
1337                } else {
1338                    HealthStatus::Available
1339                }
1340            );
1341        }
1342
1343        // Test the application health.
1344        let res = client.get("healthcheck").send().await.unwrap();
1345        assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE);
1346        let health: AppHealth = res.json().await.unwrap();
1347        assert_eq!(health.status, HealthStatus::Unhealthy);
1348        assert_eq!(
1349            health.modules["mod"],
1350            [(3, StatusCode::OK), (1, StatusCode::SERVICE_UNAVAILABLE)].into()
1351        );
1352    }
1353
1354    #[async_std::test]
1355    async fn test_api_disco() {
1356        setup_test();
1357
1358        // Test discoverability documentation when a request is for an unknown API.
1359        let mut app = App::<_, ServerError>::with_state(());
1360        app.module::<ServerError, StaticVer01>(
1361            "the-correct-module",
1362            toml! {
1363                route = {}
1364            },
1365        )
1366        .unwrap()
1367        .with_version("1.0.0".parse().unwrap());
1368
1369        let port = pick_unused_port().unwrap();
1370        let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1371        spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1372        let client = Client::new(url.clone()).await;
1373
1374        let expected_list_item = html! {
1375            a href="/the-correct-module" {"the-correct-module"}
1376            sup {
1377                a href="/v1/the-correct-module" {"[v1]"}
1378            }
1379        }
1380        .into_string();
1381
1382        for version_prefix in ["", "/v1"] {
1383            let docs = client
1384                .get(&format!("{version_prefix}/test"))
1385                .send()
1386                .await
1387                .unwrap()
1388                .text()
1389                .await
1390                .unwrap();
1391            assert!(docs.contains("No API matches /test"), "{docs}");
1392            assert!(docs.contains(&expected_list_item), "{docs}");
1393        }
1394
1395        // Top level documentation.
1396        let docs = client.get("").send().await.unwrap().text().await.unwrap();
1397        assert!(!docs.contains("No API matches"), "{docs}");
1398        assert!(docs.contains(&expected_list_item), "{docs}");
1399
1400        let docs = client
1401            .get("/v1")
1402            .send()
1403            .await
1404            .unwrap()
1405            .text()
1406            .await
1407            .unwrap();
1408        assert!(docs.contains("No API matches /"), "{docs}");
1409        assert!(docs.contains(&expected_list_item), "{docs}");
1410    }
1411
1412    #[async_std::test]
1413    async fn test_post_redirect_idempotency() {
1414        setup_test();
1415
1416        let mut app = App::<_, ServerError>::with_state(RwLock::new(0));
1417
1418        let api_toml = toml! {
1419            [meta]
1420            FORMAT_VERSION = "0.1.0"
1421
1422            [route.test]
1423            METHOD = "POST"
1424            PATH = ["/test"]
1425        };
1426        {
1427            let mut api = app
1428                .module::<ServerError, StaticVer01>("mod", api_toml.clone())
1429                .unwrap();
1430            api.post("test", |_req, state| {
1431                async move {
1432                    *state += 1;
1433                    Ok(*state)
1434                }
1435                .boxed()
1436            })
1437            .unwrap();
1438        }
1439
1440        let port = pick_unused_port().unwrap();
1441        let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1442        spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1443        let client = Client::new(url.clone()).await;
1444
1445        for i in 1..3 {
1446            // Request gets redirected to latest version of API and resent, but endpoint handler
1447            // only executes once.
1448            assert_eq!(
1449                client
1450                    .post("mod/test")
1451                    .send()
1452                    .await
1453                    .unwrap()
1454                    .json::<u64>()
1455                    .await
1456                    .unwrap(),
1457                i
1458            );
1459        }
1460    }
1461
1462    #[async_std::test]
1463    async fn test_format_versions() {
1464        setup_test();
1465
1466        // Register two modules with different binary format versions, each in turn different from
1467        // the app-level version. Each module has two endpoints, one which always succeeds and one
1468        // which always fails, so we can test error serialization.
1469        let mut app = App::<_, ServerError>::with_state(());
1470        let api_toml = toml! {
1471            [meta]
1472            FORMAT_VERSION = "0.1.0"
1473
1474            [route.ok]
1475            METHOD = "GET"
1476            PATH = ["/ok"]
1477
1478            [route.err]
1479            METHOD = "GET"
1480            PATH = ["/err"]
1481        };
1482
1483        fn init_api<VER: StaticVersionType + 'static>(api: &mut Api<(), ServerError, VER>) {
1484            api.get("ok", |_req, _state| async move { Ok("ok") }.boxed())
1485                .unwrap()
1486                .get("err", |_req, _state| {
1487                    async move {
1488                        Err::<String, _>(ServerError::catch_all(
1489                            StatusCode::INTERNAL_SERVER_ERROR,
1490                            "err".into(),
1491                        ))
1492                    }
1493                    .boxed()
1494                })
1495                .unwrap();
1496        }
1497
1498        {
1499            let mut api = app
1500                .module::<ServerError, StaticVer02>("mod02", api_toml.clone())
1501                .unwrap();
1502            init_api(&mut api);
1503        }
1504        {
1505            let mut api = app
1506                .module::<ServerError, StaticVer03>("mod03", api_toml.clone())
1507                .unwrap();
1508            init_api(&mut api);
1509        }
1510
1511        let port = pick_unused_port().unwrap();
1512        let url: Url = format!("http://localhost:{}", port).parse().unwrap();
1513        spawn(app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance()));
1514        let client = Client::new(url.clone()).await;
1515
1516        async fn get<S: BinarySerializer, T: DeserializeOwned>(
1517            client: &Client,
1518            endpoint: &str,
1519            expected_status: StatusCode,
1520        ) -> anyhow::Result<T> {
1521            tracing::info!("GET {endpoint} ->");
1522            let res = client
1523                .get(endpoint)
1524                .header("Accept", "application/octet-stream")
1525                .send()
1526                .await
1527                .unwrap();
1528            tracing::info!(?res, "<-");
1529            assert_eq!(res.status(), expected_status);
1530            let bytes = res.bytes().await.unwrap();
1531            anyhow::Context::context(
1532                S::deserialize(&bytes),
1533                format!("failed to deserialize bytes {bytes:?}"),
1534            )
1535        }
1536
1537        #[tracing::instrument(skip(client))]
1538        async fn check_ok<S: BinarySerializer>(
1539            client: &Client,
1540            endpoint: &str,
1541            expected: impl Debug + DeserializeOwned + Eq,
1542        ) {
1543            tracing::info!("checking successful deserialization");
1544            assert_eq!(
1545                expected,
1546                get::<S, _>(client, endpoint, StatusCode::OK).await.unwrap()
1547            );
1548        }
1549
1550        let api_version = ApiVersion {
1551            spec_version: "0.1.0".parse().unwrap(),
1552            api_version: None,
1553        };
1554
1555        check_ok::<SerializerV01>(
1556            &client,
1557            "healthcheck",
1558            AppHealth {
1559                status: HealthStatus::Available,
1560                modules: [
1561                    ("mod02".into(), [(0, StatusCode::OK)].into()),
1562                    ("mod03".into(), [(0, StatusCode::OK)].into()),
1563                ]
1564                .into(),
1565            },
1566        )
1567        .await;
1568        check_ok::<SerializerV01>(
1569            &client,
1570            "version",
1571            AppVersion {
1572                app_version: None,
1573                disco_version: env!("CARGO_PKG_VERSION").parse().unwrap(),
1574                modules: [
1575                    ("mod02".into(), vec![api_version.clone()]),
1576                    ("mod03".into(), vec![api_version.clone()]),
1577                ]
1578                .into(),
1579            },
1580        )
1581        .await;
1582        check_ok::<SerializerV02>(&client, "mod02/ok", "ok".to_string()).await;
1583        check_ok::<SerializerV02>(&client, "mod02/healthcheck", HealthStatus::Available).await;
1584        check_ok::<SerializerV02>(&client, "mod02/version", api_version.clone()).await;
1585        check_ok::<SerializerV03>(&client, "mod03/ok", "ok".to_string()).await;
1586        check_ok::<SerializerV03>(&client, "mod03/healthcheck", HealthStatus::Available).await;
1587        check_ok::<SerializerV03>(&client, "mod03/version", api_version.clone()).await;
1588
1589        #[tracing::instrument(skip(client))]
1590        async fn check_wrong_version<S: BinarySerializer, T: Debug + DeserializeOwned>(
1591            client: &Client,
1592            endpoint: &str,
1593        ) {
1594            tracing::info!("checking deserialization fails with wrong version");
1595            get::<S, T>(client, endpoint, StatusCode::OK)
1596                .await
1597                .unwrap_err();
1598        }
1599
1600        check_wrong_version::<SerializerV02, AppHealth>(&client, "healthcheck").await;
1601        check_wrong_version::<SerializerV02, AppVersion>(&client, "version").await;
1602        check_wrong_version::<SerializerV03, String>(&client, "mod02/ok").await;
1603        check_wrong_version::<SerializerV03, HealthStatus>(&client, "mod02/healthcheck").await;
1604        check_wrong_version::<SerializerV03, ApiVersion>(&client, "mod02/version").await;
1605        check_wrong_version::<SerializerV01, String>(&client, "mod03/ok").await;
1606        check_wrong_version::<SerializerV01, HealthStatus>(&client, "mod03/healthcheck").await;
1607        check_wrong_version::<SerializerV01, ApiVersion>(&client, "mod03/version").await;
1608
1609        #[tracing::instrument(skip(client))]
1610        async fn check_err<S: BinarySerializer>(client: &Client, endpoint: &str) {
1611            tracing::info!("checking error deserialization");
1612            tracing::info!("checking successful deserialization");
1613            assert_eq!(
1614                get::<S, ServerError>(client, endpoint, StatusCode::INTERNAL_SERVER_ERROR)
1615                    .await
1616                    .unwrap(),
1617                ServerError::catch_all(StatusCode::INTERNAL_SERVER_ERROR, "err".into())
1618            );
1619        }
1620
1621        check_err::<SerializerV02>(&client, "mod02/err").await;
1622        check_err::<SerializerV03>(&client, "mod03/err").await;
1623    }
1624
1625    #[async_std::test]
1626    async fn test_api_prefix() {
1627        setup_test();
1628
1629        // It is illegal to register two API modules where one is a prefix (in terms of route
1630        // segments) of another.
1631        for (api1, api2) in [
1632            ("", "api"),
1633            ("api", ""),
1634            ("path", "path/sub"),
1635            ("path/sub", "path"),
1636        ] {
1637            tracing::info!(api1, api2, "test case");
1638            let (prefix, conflict) = if api1.len() < api2.len() {
1639                (api1.to_string(), api2.to_string())
1640            } else {
1641                (api2.to_string(), api1.to_string())
1642            };
1643
1644            let mut app = App::<_, ServerError>::with_state(());
1645            let toml = toml! {
1646                route = {}
1647            };
1648            app.module::<ServerError, StaticVer01>(api1, toml.clone())
1649                .unwrap()
1650                .register()
1651                .unwrap();
1652            assert_eq!(
1653                app.module::<ServerError, StaticVer01>(api2, toml)
1654                    .unwrap()
1655                    .register()
1656                    .unwrap_err(),
1657                DispatchError::ConflictingModules { prefix, conflict }.into()
1658            );
1659        }
1660    }
1661
1662    #[async_std::test]
1663    async fn test_singleton_api() {
1664        setup_test();
1665
1666        // If there is only one API, it should be possible to register it with an empty prefix.
1667        let toml = toml! {
1668            [route.test]
1669            PATH = ["/test"]
1670        };
1671        let mut app = App::<_, ServerError>::with_state(());
1672        let mut api = app.module::<ServerError, StaticVer01>("", toml).unwrap();
1673        api.with_version("0.1.0".parse().unwrap())
1674            .get("test", |_, _| async move { Ok("response") }.boxed())
1675            .unwrap();
1676        api.register().unwrap();
1677
1678        let port = pick_unused_port().unwrap();
1679        spawn(app.serve(format!("0.0.0.0:{port}"), StaticVer01::instance()));
1680        let client = Client::new(format!("http://localhost:{port}").parse().unwrap()).await;
1681
1682        // Test an endpoint.
1683        let res = client.get("/test").send().await.unwrap();
1684        assert_eq!(
1685            res.status(),
1686            StatusCode::OK,
1687            "{}",
1688            res.text().await.unwrap()
1689        );
1690        assert_eq!(res.json::<String>().await.unwrap(), "response");
1691
1692        // Test healthcheck and version endpoints. Since these would ordinarily conflict with the
1693        // app-level healthcheck and version endpoints for an API with no prefix, we only get the
1694        // API-level endpoints, so that a singleton API behaves like a normal API, while app-level
1695        // stuff is reserved for non-trivial applications with more than one API.
1696        let res = client.get("/healthcheck").send().await.unwrap();
1697        assert_eq!(res.status(), StatusCode::OK);
1698        assert_eq!(
1699            res.json::<HealthStatus>().await.unwrap(),
1700            HealthStatus::Available
1701        );
1702
1703        let res = client.get("/version").send().await.unwrap();
1704        assert_eq!(res.status(), StatusCode::OK);
1705        assert_eq!(
1706            res.json::<ApiVersion>().await.unwrap(),
1707            ApiVersion {
1708                api_version: Some("0.1.0".parse().unwrap()),
1709                spec_version: "0.1.0".parse().unwrap(),
1710            },
1711        );
1712    }
1713
1714    #[async_std::test]
1715    async fn test_multi_segment() {
1716        setup_test();
1717
1718        let toml = toml! {
1719            [route.test]
1720            PATH = ["/test"]
1721        };
1722        let mut app = App::<_, ServerError>::with_state(());
1723
1724        for name in ["a", "b"] {
1725            let path = format!("api/{name}");
1726            let mut api = app
1727                .module::<ServerError, StaticVer01>(&path, toml.clone())
1728                .unwrap();
1729            api.with_version("0.1.0".parse().unwrap())
1730                .get("test", move |_, _| async move { Ok(name) }.boxed())
1731                .unwrap();
1732            api.register().unwrap();
1733        }
1734
1735        let port = pick_unused_port().unwrap();
1736        spawn(app.serve(format!("0.0.0.0:{port}"), StaticVer01::instance()));
1737        let client = Client::new(format!("http://localhost:{port}").parse().unwrap()).await;
1738
1739        for api in ["a", "b"] {
1740            tracing::info!(api, "testing api");
1741
1742            // Test an endpoint.
1743            let res = client.get(&format!("api/{api}/test")).send().await.unwrap();
1744            assert_eq!(res.status(), StatusCode::OK);
1745            assert_eq!(res.json::<String>().await.unwrap(), api);
1746
1747            // Test healthcheck.
1748            let res = client
1749                .get(&format!("api/{api}/healthcheck"))
1750                .send()
1751                .await
1752                .unwrap();
1753            assert_eq!(res.status(), StatusCode::OK);
1754            assert_eq!(
1755                res.json::<HealthStatus>().await.unwrap(),
1756                HealthStatus::Available
1757            );
1758
1759            // Test version.
1760            let res = client
1761                .get(&format!("api/{api}/version"))
1762                .send()
1763                .await
1764                .unwrap();
1765            assert_eq!(res.status(), StatusCode::OK);
1766            assert_eq!(
1767                res.json::<ApiVersion>().await.unwrap().api_version.unwrap(),
1768                "0.1.0".parse().unwrap()
1769            );
1770        }
1771
1772        // Test app-level healthcheck.
1773        let res = client.get("healthcheck").send().await.unwrap();
1774        assert_eq!(res.status(), StatusCode::OK);
1775        assert_eq!(
1776            res.json::<AppHealth>().await.unwrap(),
1777            AppHealth {
1778                status: HealthStatus::Available,
1779                modules: [
1780                    ("api/a".into(), [(0, StatusCode::OK)].into()),
1781                    ("api/b".into(), [(0, StatusCode::OK)].into()),
1782                ]
1783                .into()
1784            }
1785        );
1786
1787        // Test app-level version.
1788        let res = client.get("version").send().await.unwrap();
1789        assert_eq!(res.status(), StatusCode::OK);
1790        assert_eq!(
1791            res.json::<AppVersion>().await.unwrap().modules,
1792            [
1793                (
1794                    "api/a".into(),
1795                    vec![ApiVersion {
1796                        api_version: Some("0.1.0".parse().unwrap()),
1797                        spec_version: "0.1.0".parse().unwrap(),
1798                    }]
1799                ),
1800                (
1801                    "api/b".into(),
1802                    vec![ApiVersion {
1803                        api_version: Some("0.1.0".parse().unwrap()),
1804                        spec_version: "0.1.0".parse().unwrap(),
1805                    }]
1806                ),
1807            ]
1808            .into()
1809        );
1810    }
1811}