From 435088f2d9bf27d3266a7b77bd4b253e3d08f0d8 Mon Sep 17 00:00:00 2001 From: "John D. Corbett" Date: Sat, 2 Jul 2022 07:44:20 -0700 Subject: [PATCH 1/7] Added fn body_json() --- src/request.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/request.rs b/src/request.rs index 20ee8863..24e6d6c8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -28,6 +28,9 @@ pub enum RequestError { name: String, expected: String, }, + + #[snafu(display("Unable to compose JSON"))] + JsonSnafu, } /// Parameters passed to a route handler. @@ -232,6 +235,13 @@ impl RequestParams { pub fn body_bytes(&self) -> Vec { self.post_data.clone() } + + pub fn body_json(&self) -> Result + where + T: serde::de::DeserializeOwned, + { + serde_json::from_slice(&self.post_data.clone()).map_err(|_| RequestError::JsonSnafu {}) + } } #[derive(Clone, Debug)] From 52bf5235c41dae077fe207f35873ebf671cf6084 Mon Sep 17 00:00:00 2001 From: "John D. Corbett" Date: Tue, 5 Jul 2022 19:10:05 -0700 Subject: [PATCH 2/7] Renamed Args to DiscoArgs and made it public. Renamed ConfigKey to DiscoKey. The idea is to make it easier for applications to incorporate tide-disco parameters and configuration values. --- src/lib.rs | 29 ++++++++++++++++++++++------- src/main.rs | 26 +++++++------------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 58fb6a55..71f37d9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,7 +142,7 @@ use crate::ApiKey::*; use async_std::sync::{Arc, RwLock}; use async_std::task::spawn; use async_std::task::JoinHandle; -use clap::CommandFactory; +use clap::{CommandFactory, Parser}; use config::{Config, ConfigError}; use routefinder::Router; use serde::Deserialize; @@ -179,9 +179,24 @@ pub use error::Error; pub use request::{RequestError, RequestParam, RequestParamType, RequestParamValue, RequestParams}; pub use tide::http::{self, StatusCode}; +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct DiscoArgs { + #[clap(long)] + /// Server address + pub base_url: Option, + #[clap(long)] + /// HTTP routes + pub api_toml: Option, + /// If true, log in color. Otherwise, no color. + #[clap(long)] + pub ansi_color: Option, +} + +// TODO Rename this DiscoKey or something suggestive of Tide Disco #[derive(AsRefStr, Debug)] #[allow(non_camel_case_types)] -pub enum ConfigKey { +pub enum DiscoKey { base_url, disco_toml, brand_toml, @@ -714,11 +729,11 @@ pub fn get_settings() -> Result { // file keys lower case. This is a config-rs bug. See // https://github.com/mehcode/config-rs/issues/340 Config::builder() - .set_default(ConfigKey::base_url.as_ref(), "http://localhost:65535")? - .set_default(ConfigKey::disco_toml.as_ref(), "api/disco.toml")? - .set_default(ConfigKey::brand_toml.as_ref(), "api/brand.toml")? - .set_default(ConfigKey::api_toml.as_ref(), "api/api.toml")? - .set_default(ConfigKey::ansi_color.as_ref(), false)? + .set_default(DiscoKey::base_url.as_ref(), "http://localhost:65535")? + .set_default(DiscoKey::disco_toml.as_ref(), "api/disco.toml")? + .set_default(DiscoKey::brand_toml.as_ref(), "api/brand.toml")? + .set_default(DiscoKey::api_toml.as_ref(), "api/api.toml")? + .set_default(DiscoKey::ansi_color.as_ref(), false)? .add_source(config::File::with_name("config/default.toml")) .add_source(config::File::with_name("config/org.toml")) .add_source(config::File::with_name("config/app.toml")) diff --git a/src/main.rs b/src/main.rs index 58965bad..525cd51a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,27 +7,13 @@ use signal_hook::consts::{SIGINT, SIGTERM, SIGUSR1}; use std::{path::PathBuf, process}; use tide_disco::{ configure_router, get_api_path, get_settings, init_web_server, load_api, AppServerState, - ConfigKey, HealthStatus::*, + DiscoArgs, DiscoKey, HealthStatus::*, }; use tracing::info; use url::Url; mod signal; -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -struct Args { - #[clap(long)] - /// Server address - base_url: Option, - #[clap(long)] - /// HTTP routes - api_toml: Option, - /// If true, log in color. Otherwise, no color. - #[clap(long)] - ansi_color: Option, -} - impl Interrupt for InterruptHandle { fn signal_action(signal: i32) { // TOOD modify web_state based on the signal. @@ -39,10 +25,12 @@ impl Interrupt for InterruptHandle { #[async_std::main] async fn main() -> Result<(), ConfigError> { // Combine settings from multiple sources. - let settings = get_settings::()?; + let settings = get_settings::()?; // Colorful logs upon request. - let want_color = settings.get_bool("ansi_color").unwrap_or(false); + let want_color = settings + .get_bool(DiscoKey::ansi_color.as_ref()) + .unwrap_or(false); // Configure logs with timestamps, no color, and settings from // the RUST_LOG environment variable. @@ -55,8 +43,8 @@ async fn main() -> Result<(), ConfigError> { info!("{:?}", settings); // Fetch the configuration values before any slow operations. - let api_toml = &settings.get_string(ConfigKey::api_toml.as_ref())?; - let base_url = &settings.get_string(ConfigKey::base_url.as_ref())?; + let api_toml = &settings.get_string(DiscoKey::api_toml.as_ref())?; + let base_url = &settings.get_string(DiscoKey::base_url.as_ref())?; // Load a TOML file and display something from it. let api = load_api(&get_api_path(api_toml)); From 27d3bd3ab66ccabc3f29d0ecd67152266ea5154f Mon Sep 17 00:00:00 2001 From: "John D. Corbett" Date: Fri, 8 Jul 2022 10:03:55 -0700 Subject: [PATCH 3/7] Improved config division of labor. Enabled app to supply default values. --- Cargo.toml | 1 + src/lib.rs | 119 +++++++++++++++++++++++++++++++++++++++++----------- src/main.rs | 9 ++-- 3 files changed, 99 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 13534f7b..801d515b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ bincode = "1.3.3" clap = { version = "3.2.5", features = ["derive"] } config = "0.13.1" derive_more = "0.99" +dirs = "4.0.0" edit-distance = "2.1.0" futures = "0.3.21" futures-util = "0.3.8" diff --git a/src/lib.rs b/src/lib.rs index 71f37d9d..1d2b9605 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,7 +146,8 @@ use clap::{CommandFactory, Parser}; use config::{Config, ConfigError}; use routefinder::Router; use serde::Deserialize; -use std::fs::read_to_string; +use std::fs::{read_to_string, OpenOptions}; +use std::io::Write; use std::str::FromStr; use std::{ collections::HashMap, @@ -162,7 +163,7 @@ use tide::{ Request, Response, }; use toml::value::Value; -use tracing::error; +use tracing::{error, info}; use url::Url; pub mod api; @@ -193,15 +194,18 @@ pub struct DiscoArgs { pub ansi_color: Option, } -// TODO Rename this DiscoKey or something suggestive of Tide Disco +/// Configuration keys for Tide Disco settings +/// +/// The application is expected to define additional keys. Note, string literals could be used +/// directly, but defining an enum allows the compiler to catch typos. #[derive(AsRefStr, Debug)] #[allow(non_camel_case_types)] pub enum DiscoKey { + ansi_color, + api_toml, + app_toml, base_url, disco_toml, - brand_toml, - api_toml, - ansi_color, } #[derive(AsRefStr, Clone, Debug, Deserialize, strum_macros::Display)] @@ -664,13 +668,14 @@ pub async fn disco_web_handler(req: Request) -> tide::Result { } } -// TODO The routes should come from api.toml. pub async fn init_web_server( base_url: &str, state: AppServerState, ) -> std::io::Result>> { + info!("a"); let base_url = Url::parse(base_url).unwrap(); let mut web_server = tide::with_state(state); + info!("9"); web_server.with( CorsMiddleware::new() .allow_methods("GET, POST".parse::().unwrap()) @@ -678,16 +683,19 @@ pub async fn init_web_server( .allow_origin(Origin::from("*")) .allow_credentials(true), ); + info!("9"); // TODO Replace these hardcoded routes with api.toml routes web_server.at("/help").get(compose_reference_documentation); web_server.at("/help/").get(compose_reference_documentation); web_server.at("/healthcheck").get(healthcheck); web_server.at("/healthcheck/").get(healthcheck); + info!("10"); web_server.at("/").all(disco_web_handler); web_server.at("/*").all(disco_web_handler); web_server.at("/public").serve_dir("public/media/")?; + info!("11"); Ok(spawn(web_server.listen(base_url.to_string()))) } @@ -716,28 +724,89 @@ fn get_cmd_line_map() -> config::Environment { /// Get the application configuration /// -/// Gets the configuration from -/// - Defaults in the source -/// - A configuration file config/app.toml +/// Build the configuration from +/// - Defaults in the tide-disco source +/// - Defaults passed from the app +/// - A configuration file from the app /// - Command line arguments /// - Environment variables -/// Last one wins. Additional file sources can be added. -pub fn get_settings() -> Result { - // In the config-rs crate, environment variable names are - // converted to lower case, but keys in files are not, so if we - // want environment variables to override file value, we must make - // file keys lower case. This is a config-rs bug. See - // https://github.com/mehcode/config-rs/issues/340 - Config::builder() +/// Last one wins. +/// +/// Environment variables have a prefix of the given app_name in upper case with hyphens converted +/// to underscores. Hyphens are illegal in environment variables in bash, et.al.. +pub fn compose_settings( + org_name: &str, + app_name: &str, + app_defaults: &[(&str, &str)], + app_config_file: &PathBuf, +) -> Result { + { + let app_config = OpenOptions::new() + .write(true) + .create_new(true) + .open(app_config_file); + if let Ok(mut app_config) = app_config { + write!( + app_config, + "# {app_name} configuration\n\n\ + # Note: keys must be lower case.\n\n" + ) + .map_err(|e| ConfigError::Foreign(e.into()))?; + for (k, v) in app_defaults { + write!(app_config, "{k} = \"{v}\"\n") + .map_err(|e| ConfigError::Foreign(e.into()))?; + } + } + // app_config file handle gets closed exiting this scope so + // Config can read it. + } + let env_var_prefix = &app_name.replace("-", "_"); + let org_config_file = org_data_path(&org_name).join("org.toml"); + // In the config-rs crate, environment variable names are converted to lower case, but keys in + // files are not, so if we want environment variables to override file value, we must make file + // keys lower case. This is a config-rs bug. See https://github.com/mehcode/config-rs/issues/340 + let mut builder = Config::builder() .set_default(DiscoKey::base_url.as_ref(), "http://localhost:65535")? - .set_default(DiscoKey::disco_toml.as_ref(), "api/disco.toml")? - .set_default(DiscoKey::brand_toml.as_ref(), "api/brand.toml")? - .set_default(DiscoKey::api_toml.as_ref(), "api/api.toml")? + .set_default(DiscoKey::disco_toml.as_ref(), "disco.toml")? // TODO path to share config + .set_default( + DiscoKey::app_toml.as_ref(), + app_api_path(&org_name, &app_name) + .to_str() + .expect("Invalid api path"), + )? .set_default(DiscoKey::ansi_color.as_ref(), false)? .add_source(config::File::with_name("config/default.toml")) - .add_source(config::File::with_name("config/org.toml")) - .add_source(config::File::with_name("config/app.toml")) + .add_source(config::File::with_name( + org_config_file + .to_str() + .expect("Invalid organization configuration file path"), + )) + .add_source(config::File::with_name( + app_config_file + .to_str() + .expect("Invalid application configuration file path"), + )) .add_source(get_cmd_line_map::()) - .add_source(config::Environment::with_prefix("APP")) - .build() + .add_source(config::Environment::with_prefix(env_var_prefix)); // No hyphens allowed + for (k, v) in app_defaults { + builder = builder.set_default(*k, *v).expect("Failed to set default"); + } + builder.build() +} + +pub fn init_logging(want_color: bool) { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_ansi(want_color) + .init(); +} + +pub fn org_data_path(org_name: &str) -> PathBuf { + dirs::data_local_dir() + .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from("./"))) + .join(org_name) +} + +pub fn app_api_path(org_name: &str, app_name: &str) -> PathBuf { + org_data_path(org_name).join(app_name).join("api.toml") } diff --git a/src/main.rs b/src/main.rs index 525cd51a..7de35da8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,15 @@ use crate::signal::Interrupt; use async_std::sync::{Arc, RwLock}; -use clap::Parser; use config::ConfigError; use signal::InterruptHandle; use signal_hook::consts::{SIGINT, SIGTERM, SIGUSR1}; -use std::{path::PathBuf, process}; +use std::path::PathBuf; +use std::process; use tide_disco::{ - configure_router, get_api_path, get_settings, init_web_server, load_api, AppServerState, + compose_settings, configure_router, get_api_path, init_web_server, load_api, AppServerState, DiscoArgs, DiscoKey, HealthStatus::*, }; use tracing::info; -use url::Url; mod signal; @@ -25,7 +24,7 @@ impl Interrupt for InterruptHandle { #[async_std::main] async fn main() -> Result<(), ConfigError> { // Combine settings from multiple sources. - let settings = get_settings::()?; + let settings = compose_settings::("acme", "rocket-sleds", &[], &PathBuf::from("."))?; // Colorful logs upon request. let want_color = settings From 7c77d48ee013f970c6f5e12c4ef88bfd78090b56 Mon Sep 17 00:00:00 2001 From: "John D. Corbett" Date: Fri, 8 Jul 2022 18:28:25 -0700 Subject: [PATCH 4/7] Minor cleanup. --- src/lib.rs | 3 ++- src/main.rs | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1d2b9605..197df26e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,7 +163,7 @@ use tide::{ Request, Response, }; use toml::value::Value; -use tracing::{error, info}; +use tracing::{error, info, trace}; use url::Url; pub mod api; @@ -340,6 +340,7 @@ pub fn configure_router(api: &toml::Value) -> Arc> { .as_array() .expect("Expecting TOML array."); for path in paths { + trace!("adding path: {:?}", path); index += 1; router .add(path.as_str().expect("Expecting a path string."), index) diff --git a/src/main.rs b/src/main.rs index 7de35da8..f129dc67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,12 @@ use async_std::sync::{Arc, RwLock}; use config::ConfigError; use signal::InterruptHandle; use signal_hook::consts::{SIGINT, SIGTERM, SIGUSR1}; +use std::env::current_dir; use std::path::PathBuf; use std::process; use tide_disco::{ - compose_settings, configure_router, get_api_path, init_web_server, load_api, AppServerState, - DiscoArgs, DiscoKey, HealthStatus::*, + app_api_path, compose_settings, configure_router, get_api_path, init_web_server, load_api, + AppServerState, DiscoArgs, DiscoKey, HealthStatus::*, }; use tracing::info; @@ -23,8 +24,16 @@ impl Interrupt for InterruptHandle { #[async_std::main] async fn main() -> Result<(), ConfigError> { + let api_path = current_dir().unwrap().join("api").join("api.toml"); + let api_path_str = api_path.to_str().unwrap(); + // Combine settings from multiple sources. - let settings = compose_settings::("acme", "rocket-sleds", &[], &PathBuf::from("."))?; + let settings = compose_settings::( + "acme", + "rocket-sleds", + &[(DiscoKey::api_toml.as_ref(), api_path_str)], + &app_api_path("acme", "rocket-sleds"), + )?; // Colorful logs upon request. let want_color = settings @@ -39,13 +48,18 @@ async fn main() -> Result<(), ConfigError> { .try_init() .unwrap(); - info!("{:?}", settings); + info!("Settings: {:?}", settings); + info!("api_path: {:?}", api_path_str); + info!("app_api_path: {:?}", app_api_path("acme", "rocket-sleds")); // Fetch the configuration values before any slow operations. let api_toml = &settings.get_string(DiscoKey::api_toml.as_ref())?; let base_url = &settings.get_string(DiscoKey::base_url.as_ref())?; // Load a TOML file and display something from it. + info!("api_toml: {:?}", api_toml); + info!("base_url: {:?}", base_url); + info!("get_api_path: {:?}", &get_api_path(api_toml)); let api = load_api(&get_api_path(api_toml)); let router = configure_router(&api); From 67a19bdf3b31c3af1e4bc163e193832db467b66e Mon Sep 17 00:00:00 2001 From: "John D. Corbett" Date: Fri, 8 Jul 2022 21:19:37 -0700 Subject: [PATCH 5/7] Moved a few functions here from address-book. --- src/lib.rs | 34 +++++++++++++++++++++++++++++++++- src/main.rs | 2 -- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 197df26e..fc73784e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,6 +140,7 @@ use crate::ApiKey::*; use async_std::sync::{Arc, RwLock}; +use async_std::task::sleep; use async_std::task::spawn; use async_std::task::JoinHandle; use clap::{CommandFactory, Parser}; @@ -149,6 +150,7 @@ use serde::Deserialize; use std::fs::{read_to_string, OpenOptions}; use std::io::Write; use std::str::FromStr; +use std::time::Duration; use std::{ collections::HashMap, env, @@ -180,6 +182,9 @@ pub use error::Error; pub use request::{RequestError, RequestParam, RequestParamType, RequestParamValue, RequestParams}; pub use tide::http::{self, StatusCode}; +/// Number of times to poll before failing +const STARTUP_RETRIES: u32 = 255; + #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] pub struct DiscoArgs { @@ -723,6 +728,14 @@ fn get_cmd_line_map() -> config::Environment { })) } +/// Compose the path to the application's configuration file +pub fn compose_config_path(org_dir_name: &str, app_name: &str) -> PathBuf { + let mut app_config_path = org_data_path(org_dir_name); + app_config_path = app_config_path.join(app_name).join(app_name); + app_config_path.set_extension("toml"); + app_config_path +} + /// Get the application configuration /// /// Build the configuration from @@ -739,8 +752,8 @@ pub fn compose_settings( org_name: &str, app_name: &str, app_defaults: &[(&str, &str)], - app_config_file: &PathBuf, ) -> Result { + let app_config_file = &compose_config_path(org_name, app_name); { let app_config = OpenOptions::new() .write(true) @@ -811,3 +824,22 @@ pub fn org_data_path(org_name: &str) -> PathBuf { pub fn app_api_path(org_name: &str, app_name: &str) -> PathBuf { org_data_path(org_name).join(app_name).join("api.toml") } + +/// Wait for the server to respond to a connection request +/// +/// This is useful for tests for which it doesn't make sense to send requests until the server has +/// started. +pub async fn wait_for_server(base_url: &str) { + // Wait for the server to come up and start serving. + let pause_ms = Duration::from_millis(100); + for _ in 0..STARTUP_RETRIES { + if surf::connect(base_url).send().await.is_ok() { + return; + } + sleep(pause_ms).await; + } + panic!( + "Address Book did not start in {:?} milliseconds", + pause_ms * STARTUP_RETRIES + ); +} diff --git a/src/main.rs b/src/main.rs index f129dc67..1276e8f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ use config::ConfigError; use signal::InterruptHandle; use signal_hook::consts::{SIGINT, SIGTERM, SIGUSR1}; use std::env::current_dir; -use std::path::PathBuf; use std::process; use tide_disco::{ app_api_path, compose_settings, configure_router, get_api_path, init_web_server, load_api, @@ -32,7 +31,6 @@ async fn main() -> Result<(), ConfigError> { "acme", "rocket-sleds", &[(DiscoKey::api_toml.as_ref(), api_path_str)], - &app_api_path("acme", "rocket-sleds"), )?; // Colorful logs upon request. From 8eb80de29a5bf0e97c2840b05873a767dae2a03d Mon Sep 17 00:00:00 2001 From: "John D. Corbett" Date: Sat, 9 Jul 2022 18:38:02 -0700 Subject: [PATCH 6/7] Improved the documentation. Removed debugging code. Added starting implementations for catch-all route handling. --- src/api.rs | 13 +++++++++++++ src/app.rs | 36 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 38 +++++++++++++++++++++++++++++--------- src/main.rs | 3 +++ 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/api.rs b/src/api.rs index 1c04e040..b64fc22a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,3 +1,16 @@ +// COPYRIGHT100 (c) 2022 Espresso Systems (espressosys.com) +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with this program. If +// not, see . + use crate::{ healthcheck::{HealthCheck, HealthStatus}, method::{method_is_mutable, ReadState, WriteState}, diff --git a/src/app.rs b/src/app.rs index 7910c5ad..fffed195 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,16 @@ +// COPYRIGHT100 (c) 2022 Espresso Systems (espressosys.com) +// +// This program is free software: you can redistribute it and/or modify it under the terms of the +// GNU General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with this program. If +// not, see . + use crate::{ api::{Api, ApiVersion}, healthcheck::{HealthCheck, HealthStatus}, @@ -248,6 +261,29 @@ impl App(err).into_tide_error()) }); + // Register catch-all routes for discoverability + { + server + .at("/") + .get(move |req: tide::Request>| async move { + // TODO invoke disco_web_handler with the URL, etc. + Ok(format!("help /\n{:?}", req.url())) + }); + } + { + server + .at("/*") + .get(move |req: tide::Request>| async move { + // TODO invoke disco_web_handler with the URL, etc. + Ok(format!("help /*\n{:?}", req.url())) + }); + } + // TODO add a call to serve_dir + { + // TODO This path is not found for address-book + //server.at("/public").serve_dir("public/media/")?; + } + server.listen(listener).await } } diff --git a/src/lib.rs b/src/lib.rs index fc73784e..7f86ea00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,32 @@ -//! Web server framework with built-in discoverability. +//! _Tide Disco is a web server framework with built-in discoverability support for +//! [Tide](https://github.com/http-rs/tide)_ //! -//! # Overview -//! TODO +//! We say a system is _discoverable_ if guesses and mistakes regarding usage are rewarded with +//! relevant documentation and assistance at producing correct requests. To offer this capability in +//! a practical way, it is helpful to specify the web API in data files, rather than code, so that +//! all relevant text can be edited in one concise readable specification. +//! +//! Tide Disco leverages TOML to specify +//! - Routes with typed parameters +//! - Route documentation +//! - Route error messages +//! - General documentation +//! +//! ## Goals +//! +//! - Context-sensitive help +//! - Spelling suggestions +//! - Reference documentation assembled from route documentation +//! - Forms and other user interfaces to aid in the construction of correct inputs +//! - Localization +//! - Novice and expert help +//! - Flexible route parsing, e.g. named parameters rather than positional parameters +//! - API fuzz testing automation based on parameter types +//! +//! ## Future +//! +//! - WebSocket support +//! - Runtime control over logging //! //! # Boxed futures //! @@ -165,7 +190,7 @@ use tide::{ Request, Response, }; use toml::value::Value; -use tracing::{error, info, trace}; +use tracing::{error, trace}; use url::Url; pub mod api; @@ -678,10 +703,8 @@ pub async fn init_web_server( base_url: &str, state: AppServerState, ) -> std::io::Result>> { - info!("a"); let base_url = Url::parse(base_url).unwrap(); let mut web_server = tide::with_state(state); - info!("9"); web_server.with( CorsMiddleware::new() .allow_methods("GET, POST".parse::().unwrap()) @@ -689,19 +712,16 @@ pub async fn init_web_server( .allow_origin(Origin::from("*")) .allow_credentials(true), ); - info!("9"); // TODO Replace these hardcoded routes with api.toml routes web_server.at("/help").get(compose_reference_documentation); web_server.at("/help/").get(compose_reference_documentation); web_server.at("/healthcheck").get(healthcheck); web_server.at("/healthcheck/").get(healthcheck); - info!("10"); web_server.at("/").all(disco_web_handler); web_server.at("/*").all(disco_web_handler); web_server.at("/public").serve_dir("public/media/")?; - info!("11"); Ok(spawn(web_server.listen(base_url.to_string()))) } diff --git a/src/main.rs b/src/main.rs index 1276e8f7..31f12822 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,9 @@ impl Interrupt for InterruptHandle { } } +// This demonstrates the older way of configuring the web server. What's valuable here is that it +// shows the bare bones of discoverability from a TOML file. +// TODO integrate discoverability into the new method of wrapping Tide. #[async_std::main] async fn main() -> Result<(), ConfigError> { let api_path = current_dir().unwrap().join("api").join("api.toml"); From dd2fa908e890c3f448efd85b6a337a9ffdbe6c97 Mon Sep 17 00:00:00 2001 From: "John D. Corbett" Date: Mon, 11 Jul 2022 17:54:54 -0700 Subject: [PATCH 7/7] "Cliiiippy!" in the voice of an angry Captain Kirk. --- src/app.rs | 6 +++--- src/lib.rs | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/app.rs b/src/app.rs index fffed195..b0f3c194 100644 --- a/src/app.rs +++ b/src/app.rs @@ -340,10 +340,10 @@ pub struct AppVersion { /// body of the response. /// /// If the response does not contain an error, it is passed through unchanged. -fn add_error_body<'a, T: Clone + Send + Sync + 'static, E: crate::Error>( +fn add_error_body( req: tide::Request, - next: tide::Next<'a, T>, -) -> BoxFuture<'a, tide::Result> { + next: tide::Next, +) -> BoxFuture { Box::pin(async { let mut accept = Accept::from_headers(&req)?; let mut res = next.run(req).await; diff --git a/src/lib.rs b/src/lib.rs index 1e760378..49cb7e75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -575,7 +575,7 @@ pub async fn compose_reference_documentation( help += &document_route(meta, entry); }); } - help += &format!("{}\n", &vk(meta, HTML_BOTTOM.as_ref())); + help = format!("{}{}\n", help, &vk(meta, HTML_BOTTOM.as_ref())); Ok(tide::Response::builder(200) .content_type(tide::http::mime::HTML) .body(help) @@ -692,8 +692,10 @@ pub fn check_literals(url: &Url, api: &Value, first_segment: &str) -> String { url.path_segments().unwrap().for_each(|useg| { let d = edit_distance::edit_distance(pseg, useg); if 0 < d && d <= pseg.len() / 2 { - typos += - &format!("

Found '{}'. Did you mean '{}'?

\n", useg, pseg); + typos = format!( + "{}

Found '{}'. Did you mean '{}'?

\n", + typos, useg, pseg + ); } }); } @@ -858,15 +860,15 @@ pub fn compose_settings( ) .map_err(|e| ConfigError::Foreign(e.into()))?; for (k, v) in app_defaults { - write!(app_config, "{k} = \"{v}\"\n") + writeln!(app_config, "{k} = \"{v}\"") .map_err(|e| ConfigError::Foreign(e.into()))?; } } // app_config file handle gets closed exiting this scope so // Config can read it. } - let env_var_prefix = &app_name.replace("-", "_"); - let org_config_file = org_data_path(&org_name).join("org.toml"); + let env_var_prefix = &app_name.replace('-', "_"); + let org_config_file = org_data_path(org_name).join("org.toml"); // In the config-rs crate, environment variable names are converted to lower case, but keys in // files are not, so if we want environment variables to override file value, we must make file // keys lower case. This is a config-rs bug. See https://github.com/mehcode/config-rs/issues/340 @@ -875,7 +877,7 @@ pub fn compose_settings( .set_default(DiscoKey::disco_toml.as_ref(), "disco.toml")? // TODO path to share config .set_default( DiscoKey::app_toml.as_ref(), - app_api_path(&org_name, &app_name) + app_api_path(org_name, app_name) .to_str() .expect("Invalid api path"), )?