Skip to main content

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