1use 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#[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#[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 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 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 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 0
156 }
157 };
158
159 self.modules
160 .insert(dispatch::split(base_url), major_version, api)?;
161 Ok(self)
162 }
163
164 pub fn with_version(&mut self, version: Version) -> &mut Self {
191 self.app_version = Some(version);
192 self
193 }
194
195 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 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 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 let mut rng = rand::thread_rng();
275 let index: u64 = rng.gen();
276
277 let path = PathBuf::from(format!("/tmp/tide-disco/{}/public/media", index));
281 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 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 if !state.modules.is_singleton() {
325 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 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 a href=(format!("/{}", module.path())) {(module.path())}
362 @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 (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 #[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 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 if let Some(socket_route) = routes.iter().find(|route| route.method() == Method::Socket)
424 {
425 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 Self::register_metrics(prefix.to_vec(), version, &mut endpoint, metrics_route);
439 }
440
441 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 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 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 endpoint.with(MetricsMiddleware::new(name.clone(), api.clone(), version));
555 }
556
557 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 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 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 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 return Ok(next.run(req).await);
651 };
652 let path = path.collect::<Vec<_>>();
653 let Some(seg1) = path.first() else {
654 return Ok(next.run(req).await);
656 };
657 if seg1.is_empty() {
658 return Ok(next.run(req).await);
660 }
661
662 if let Some(version) = seg1.strip_prefix('v').and_then(|n| n.parse().ok()) {
666 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 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 Ok(next.run(req).await)
691 } else {
692 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 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 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#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
739pub struct AppHealth {
740 pub status: HealthStatus,
745 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#[serde_as]
757#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
758pub struct AppVersion {
759 pub modules: BTreeMap<String, Vec<ApiVersion>>,
763
764 #[serde_as(as = "Option<DisplayFromStr>")]
766 pub app_version: Option<Version>,
767
768 #[serde_as(as = "DisplayFromStr")]
770 pub disco_version: Version,
771}
772
773#[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 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 pub fn register(mut self) -> Result<(), AppError> {
848 self.register_impl()
849 }
850
851 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 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 #[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 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 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 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 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 #[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 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 .with_health_check(|_state| {
1114 async move { HealthStatus::TemporarilyUnavailable }.boxed()
1115 });
1116 }
1117 {
1118 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}