summaryrefslogtreecommitdiff
path: root/src/config.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/config.rs')
-rw-r--r--src/config.rs143
1 files changed, 55 insertions, 88 deletions
diff --git a/src/config.rs b/src/config.rs
index e70043e..bddfb40 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -3,38 +3,52 @@ use std::fs::{self, File};
use std::io;
use std::fmt::{self, Display};
use serde::Deserialize;
+use std::time::Duration;
/// All the application configuration is stored in this structure.
-#[derive(PartialEq, Clone, Debug)]
+#[derive(Deserialize, PartialEq, Clone, Debug)]
pub struct Config {
+ /// Path to the file containing the GitHub secret.
+ pub secret_path: PathBuf,
+
/// The secret string shared with GitHub that is used to verify signed requests.
+ #[serde(skip_deserializing)]
pub secret: String,
/// Event-command pairs. Each element of this array should be matched (and optionally executed)
/// against the commands in gaide.
pub commands: Vec<Command>,
+
+ /// The maximum time the server should spend sitting idle waiting for a connection before
+ /// shutting itself down.
+ ///
+ /// This is pretty relevant as webhook event are relatively rare. Shutting down and waiting for
+ /// socket (re)activation spares a few ressources.
+ #[serde(default)]
+ #[serde(with = "humantime_serde")]
+ pub max_idle_time: Option<Duration>,
}
impl Config {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
- let raw_config = RawConfig::from_path(path)?;
- let secret = fs::read_to_string(raw_config.secret_file)?;
- Ok(Config {
- secret,
- commands: raw_config.commands,
- })
+ let file = File::open(path.as_ref()).map_err(ConfigError::IoReadingConfig)?;
+ let mut config: Config = serde_json::from_reader(file)?;
+
+ config.secret = fs::read_to_string(&config.secret_path)
+ .map_err(ConfigError::IoReadingSecret)?;
+
+ Ok(config)
}
}
-/// This struct reflects the actual JSON on disk. It is further processed before being returned to
-/// the rest of the application.
-#[derive(Deserialize, Clone, Debug, PartialEq)]
-struct RawConfig {
- /// Path to file containing the secret that was shared with GitHub.
- secret_file: PathBuf,
-
- /// Event-command pairs.
- commands: Vec<Command>,
+impl From<serde_json::Error> for ConfigError {
+ fn from(e: serde_json::Error) -> ConfigError {
+ use serde_json::error::Category;
+ match e.classify() {
+ Category::Io => ConfigError::IoReadingConfig(e.into()),
+ _ => ConfigError::SerdeError(e),
+ }
+ }
}
/// Represents an event-command pair. The command is run whenever the given event is received from
@@ -55,86 +69,32 @@ pub struct Command {
pub args: Vec<String>,
}
-/*
-/// Serde helper which disallows empty strings for [`PathBuf`s](std::path::PathBuf). Based on [this
-/// StackOverflow post][so].
-///
-/// [so]: https://stackoverflow.com/a/46755370
-fn string_as_nonempty_pathbuf<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
-where
- D: Deserializer<'de>
-{
- let raw: &str = Deserialize::deserialize(deserializer)?;
- if raw.is_empty() {
- Err(de::Error::custom("path cannot be empty"))
- } else {
- Ok(PathBuf::from(raw))
- }
-}
-*/
-
/// Errors that can occur when reading configuration.
#[derive(Debug)]
pub enum ConfigError {
- /// An IO error occured, such as failing to read the file.
- Io(io::Error),
+ /// An IO error occured while reading the configuration, such as failing to read the file.
+ IoReadingConfig(io::Error),
+ /// An IO error occured while reading the secret file linked via `secret_path`.
+ IoReadingSecret(io::Error),
/// Decoding the file failed, e.g. if JSON is missing comma.
SerdeError(serde_json::Error),
}
-impl From<io::Error> for ConfigError {
- fn from(e: io::Error) -> ConfigError {
- ConfigError::Io(e)
- }
-}
-
-impl From<serde_json::Error> for ConfigError {
- fn from(e: serde_json::Error) -> ConfigError {
- use serde_json::error::Category;
- match e.classify() {
- Category::Io => ConfigError::Io(e.into()),
- _ => ConfigError::SerdeError(e),
- }
- }
-}
-
impl Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
- ConfigError::Io(e) => write!(f, "io error: {}", e),
+ ConfigError::IoReadingConfig(e) => write!(f, "io error while reading configuration file: {}", e),
+ ConfigError::IoReadingSecret(e) => write!(f, "io error while reading secret file: {}", e),
ConfigError::SerdeError(e) => write!(f, "decoding error: {}", e),
}
}
}
-impl RawConfig {
- pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
- let file = File::open(path.as_ref())?;
- let config: Self = serde_json::from_reader(file)?;
- config.validate()?;
- Ok(config)
- }
-
- #[allow(dead_code)] // Useful for tests.
- pub(self) fn from_str(s: &str) -> Result<Self, ConfigError> {
- let config: Self = serde_json::from_str(s)?;
- config.validate()?;
- Ok(config)
- }
-
- fn validate(&self) -> Result<(), ConfigError> {
- if self.secret_file.is_relative() {
- eprintln!("warning: configuration key `.secret_file` is relative path. This will be resolved relative to server's CWD at runtime which is most likely not what you want.");
- // " <- Fix shitty Vim syntax highlighting
- }
- Ok(())
- }
-}
-
#[cfg(test)]
mod tests {
- use super::{Config, Command, RawConfig, ConfigError};
- use std::path::Path;
+ use super::{Config, Command, ConfigError};
+ use std::path::{Path, PathBuf};
+ use std::time::Duration;
macro_rules! assert_matches {
( $e:expr , $pat:pat ) => {
@@ -161,10 +121,12 @@ mod tests {
}
#[test]
- fn load_valid_raw_config() {
+ fn deserialize_valid_config() {
let config_json = r#"
{
- "secret_file": "/path/to/secret.txt",
+ "secret_path": "/path/to/secret.txt",
+
+ "max_idle_time": "10min",
"commands": [
{
@@ -175,9 +137,11 @@ mod tests {
]
}
"#;
- let parsed_config = RawConfig::from_str(config_json).expect("valid config");
- let expected_config = RawConfig {
- secret_file: Path::new("/path/to/secret.txt").to_path_buf(),
+ let parsed_config = serde_json::from_str::<Config>(config_json).expect("valid config");
+ let expected_config = Config {
+ secret_path: Path::new("/path/to/secret.txt").to_path_buf(),
+ secret: "".to_string(), // We didn't ask it to read file
+ max_idle_time: Some(Duration::from_secs(600)),
commands: vec![
Command {
event: "ping".to_string(),
@@ -190,7 +154,7 @@ mod tests {
}
#[test]
- fn args_are_optional() {
+ fn deserialize_command_without_optional_args() {
let command_json = r#"
{
"event": "ping",
@@ -208,15 +172,16 @@ mod tests {
}
#[test]
- fn invalid_json_gives_error() {
+ fn deserialize_invalid_json_gives_error() {
// This JSON has a trailing comma, which isn't allowed.
let config_json = r#"
{
- "secret_file": "blah",
+ "secret_path": "blah",
"commands": [],
}
"#;
- let result = RawConfig::from_str(config_json);
+ // This way we also test the error wrapping code in our implementation of `std::convert::from::From`.
+ let result: Result<Config, ConfigError> = serde_json::from_str::<Config>(config_json).map_err(|e| e.into());
let err = assert_matches!(result, Err(ConfigError::SerdeError(e)) => e);
assert_eq!(err.line(), 5);
assert_eq!(err.column(), 13);
@@ -228,7 +193,9 @@ mod tests {
let parse_result = Config::from_path("examples/config.json");
let parsed_config = assert_matches!(parse_result, Ok(c @ Config { .. }) => c);
let expected_config = Config {
+ secret_path: PathBuf::from("./examples/secret.txt"),
secret: "mysecret".to_string(),
+ max_idle_time: Some(Duration::from_secs(60 * 60)),
commands: vec![
Command {
event: "ping".to_string(),