// Copyright (c) 2022 Espresso Systems (espressosys.com)
// This file is part of the tide-disco library.
// You should have received a copy of the MIT License
// along with the tide-disco library. If not, see .
use async_std::sync::RwLock;
use futures::FutureExt;
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use std::io;
use tide_disco::{Api, App, Error, RequestError, StatusCode};
use tracing::info;
use vbs::version::{StaticVersion, StaticVersionType};
type StaticVer01 = StaticVersion<0, 1>;
#[derive(Clone, Debug, Deserialize, Serialize, Snafu)]
enum HelloError {
Goodbye {
status: StatusCode,
farewell: String,
},
}
impl tide_disco::Error for HelloError {
fn catch_all(status: StatusCode, farewell: String) -> Self {
Self::Goodbye { status, farewell }
}
fn status(&self) -> StatusCode {
match self {
Self::Goodbye { status, .. } => *status,
}
}
}
impl From for HelloError {
fn from(err: RequestError) -> Self {
Self::catch_all(StatusCode::BAD_REQUEST, err.to_string())
}
}
async fn serve(port: u16) -> io::Result<()> {
let mut app = App::<_, HelloError>::with_state(RwLock::new("Hello".to_string()));
app.with_version(env!("CARGO_PKG_VERSION").parse().unwrap());
let mut api =
Api::, HelloError, StaticVer01>::from_file("examples/hello-world/api.toml")
.unwrap();
api.with_version(env!("CARGO_PKG_VERSION").parse().unwrap());
// Can invoke by browsing
// `http://0.0.0.0:8080/hello/greeting/dude`
// Note: "greeting" is the route name in `api.toml`. `[route.greeting]` is
// unrelated to the route PATH list.
api.get("greeting", |req, greeting| {
async move {
let name = req.string_param("name")?;
info!("called /greeting with :name = {}", name);
Ok(format!("{}, {}", greeting, name,))
}
.boxed()
})
.unwrap();
// Can invoke with
// `curl -i -X POST http://0.0.0.0:8080/hello/greeting/yo`
api.post("setgreeting", |req, greeting| {
async move {
let new_greeting = req.string_param("greeting")?;
info!("called /setgreeting with :greeting = {}", new_greeting);
*greeting = new_greeting.to_string();
Ok(())
}
.boxed()
})
.unwrap();
app.register_module("hello", api).unwrap();
app.serve(format!("0.0.0.0:{}", port), StaticVer01::instance())
.await
}
#[async_std::main]
async fn main() -> io::Result<()> {
// Configure logs with timestamps and settings from the RUST_LOG
// environment variable.
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.try_init()
.unwrap();
serve(8080).await
}
#[cfg(test)]
mod test {
use super::*;
use async_std::task::spawn;
use portpicker::pick_unused_port;
use tide_disco::{
api::ApiVersion,
app::{AppHealth, AppVersion},
healthcheck::HealthStatus,
testing::{setup_test, Client},
Url,
};
#[async_std::test]
async fn test_get_set_greeting() {
setup_test();
let port = pick_unused_port().unwrap();
spawn(serve(port));
let url = Url::parse(&format!("http://localhost:{}/hello/", port)).unwrap();
let client = Client::new(url).await;
let res = client.get("greeting/tester").send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.json::().await.unwrap(), "Hello, tester");
let res = client.post("greeting/Sup").send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let res = client.get("greeting/tester").send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.json::().await.unwrap(), "Sup, tester");
}
#[async_std::test]
async fn test_version() {
setup_test();
let port = pick_unused_port().unwrap();
spawn(serve(port));
let url = Url::parse(&format!("http://localhost:{}/", port)).unwrap();
let client = Client::new(url).await;
// Check the API version.
let res = client.get("hello/version").send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let api_version = ApiVersion {
api_version: Some(env!("CARGO_PKG_VERSION").parse().unwrap()),
spec_version: "0.1.0".parse().unwrap(),
};
assert_eq!(res.json::().await.unwrap(), api_version);
// Check the overall version.
let res = client.get("version").send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.json::().await.unwrap(),
AppVersion {
app_version: Some(env!("CARGO_PKG_VERSION").parse().unwrap()),
disco_version: env!("CARGO_PKG_VERSION").parse().unwrap(),
modules: [("hello".to_string(), vec![api_version])].into()
}
)
}
#[async_std::test]
async fn test_healthcheck() {
setup_test();
let port = pick_unused_port().unwrap();
spawn(serve(port));
let url = Url::parse(&format!("http://localhost:{}/", port)).unwrap();
let client = Client::new(url).await;
// Check the API health.
let res = client.get("hello/healthcheck").send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
// The example API does not have a custom healthcheck, so we just get the default response.
assert_eq!(
res.json::().await.unwrap(),
HealthStatus::Available
);
// Check the overall health.
let res = client.get("healthcheck").send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.json::().await.unwrap(),
AppHealth {
status: HealthStatus::Available,
modules: [("hello".to_string(), [(0, StatusCode::OK)].into())].into(),
}
)
}
}