> {
.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)
@@ -549,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)
@@ -666,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
+ );
}
});
}
@@ -744,7 +772,6 @@ 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,
@@ -794,30 +821,118 @@ 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
///
-/// 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()
- .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)?
+/// 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)],
+) -> Result {
+ let app_config_file = &compose_config_path(org_name, app_name);
+ {
+ 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 {
+ 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");
+ // 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(), "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")
+}
+
+/// 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 58965bad..31f12822 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,33 +1,18 @@
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::env::current_dir;
+use std::process;
use tide_disco::{
- configure_router, get_api_path, get_settings, init_web_server, load_api, AppServerState,
- ConfigKey, HealthStatus::*,
+ app_api_path, compose_settings, configure_router, get_api_path, init_web_server, load_api,
+ AppServerState, 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.
@@ -36,13 +21,25 @@ 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");
+ let api_path_str = api_path.to_str().unwrap();
+
// Combine settings from multiple sources.
- let settings = get_settings::()?;
+ let settings = compose_settings::(
+ "acme",
+ "rocket-sleds",
+ &[(DiscoKey::api_toml.as_ref(), api_path_str)],
+ )?;
// 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.
@@ -52,13 +49,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(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.
+ 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);
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)]