mirror of
https://framagit.org/ppom/reaction
synced 2026-03-14 12:45:47 +01:00
Add test-config sub-command
This commit is contained in:
parent
cf96fa30f1
commit
231c9f8a99
9 changed files with 156 additions and 47 deletions
17
src/cli.rs
17
src/cli.rs
|
|
@ -25,7 +25,7 @@ pub struct Cli {
|
|||
pub enum SubCommand {
|
||||
/// Start reaction daemon
|
||||
Start {
|
||||
/// configuration file in json, jsonnet or yaml format. required.
|
||||
/// configuration file in json, jsonnet or yaml format, or directory containing those files. required.
|
||||
#[clap(short = 'c', long)]
|
||||
config: PathBuf,
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ Then prints the flushed matches and actions."
|
|||
Giving a configuration file permits to use its patterns in REGEX."
|
||||
)]
|
||||
TestRegex {
|
||||
/// configuration file in json, jsonnet or yaml format. required.
|
||||
/// configuration file in json, jsonnet or yaml format, or directory containing those files. required.
|
||||
#[clap(short = 'c', long)]
|
||||
config: PathBuf,
|
||||
|
||||
|
|
@ -102,6 +102,19 @@ Giving a configuration file permits to use its patterns in REGEX."
|
|||
#[clap(value_name = "LINE")]
|
||||
line: Option<String>,
|
||||
},
|
||||
TestConfig {
|
||||
/// configuration file in json, jsonnet or yaml format, or directory containing those files. required.
|
||||
#[clap(short = 'c', long)]
|
||||
config: PathBuf,
|
||||
|
||||
/// how to format output: json or yaml.
|
||||
#[clap(short = 'f', long, default_value_t = Format::YAML)]
|
||||
format: Format,
|
||||
|
||||
/// whether to output additional information
|
||||
#[clap(short = 'v', long, default_value_t = false)]
|
||||
verbose: bool,
|
||||
},
|
||||
}
|
||||
|
||||
// Enums
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
mod show_flush;
|
||||
mod test_config;
|
||||
mod test_regex;
|
||||
|
||||
pub use show_flush::request;
|
||||
pub use test_config::test_config;
|
||||
pub use test_regex::test_regex;
|
||||
|
|
|
|||
42
src/client/test_config.rs
Normal file
42
src/client/test_config.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use std::{error::Error, path::PathBuf};
|
||||
|
||||
use crate::{cli::Format, concepts::Config};
|
||||
|
||||
pub fn test_config(
|
||||
config_path: PathBuf,
|
||||
format: Format,
|
||||
verbose: bool,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let (mut cfg, cfg_files) = Config::from_path_raw(&config_path)?;
|
||||
if verbose {
|
||||
if config_path.is_dir() {
|
||||
println!(
|
||||
"Loaded the configuration from the following files in the directory {} in this order:",
|
||||
config_path.display()
|
||||
);
|
||||
println!("{}\n", cfg_files.join("\n"));
|
||||
} else {
|
||||
println!(
|
||||
"Loaded the configuration from the file {}",
|
||||
config_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// first serialize the raw config (before regexes are transformed and their original version
|
||||
// discarded)
|
||||
let cfg_str = match format {
|
||||
Format::JSON => serde_json::to_string_pretty(&cfg).map_err(|e| e.to_string()),
|
||||
Format::YAML => serde_yaml::to_string(&cfg).map_err(|e| e.to_string()),
|
||||
}
|
||||
.map_err(|e| format!("Error serializing back the configuration: {e}"))?;
|
||||
|
||||
// then try to finalize the configuration: will raise an error on an invalid config
|
||||
cfg.setup()
|
||||
.map_err(|e| format!("Configuration file {}: {}", config_path.display(), e))?;
|
||||
|
||||
// only print the serialized config if everyting went well
|
||||
println!("{cfg_str}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -2,23 +2,28 @@ use std::{cmp::Ordering, collections::BTreeSet, fmt::Display, sync::Arc};
|
|||
|
||||
use chrono::TimeDelta;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::process::Command;
|
||||
|
||||
use super::parse_duration;
|
||||
use super::parse_duration::*;
|
||||
use super::{Match, Pattern};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Deserialize , Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Action {
|
||||
cmd: Vec<String>,
|
||||
|
||||
// TODO one shot time deserialization
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
after: Option<String>,
|
||||
#[serde(skip)]
|
||||
after_duration: Option<TimeDelta>,
|
||||
|
||||
#[serde(rename = "onexit", default = "set_false")]
|
||||
#[serde(
|
||||
rename = "onexit",
|
||||
default = "set_false",
|
||||
skip_serializing_if = "is_false"
|
||||
)]
|
||||
on_exit: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
|
|
@ -35,6 +40,10 @@ fn set_false() -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!*b
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ use super::{Filter, Pattern, Stream};
|
|||
|
||||
pub type Patterns = BTreeMap<String, Arc<Pattern>>;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(test, derive(Default))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
|
|
@ -28,12 +28,12 @@ pub struct Config {
|
|||
#[serde(default = "num_cpus::get")]
|
||||
concurrency: usize,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
start: Vec<Vec<String>>,
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
stop: Vec<Vec<String>>,
|
||||
|
||||
#[serde(default = "dot")]
|
||||
#[serde(default = "dot", skip_serializing_if = "String::is_empty")]
|
||||
state_directory: String,
|
||||
|
||||
// This field only serve the purpose of having a top-level place for saving YAML variables
|
||||
|
|
@ -146,20 +146,22 @@ impl Config {
|
|||
run_commands(&self.stop, "stop")
|
||||
}
|
||||
|
||||
pub fn from_path(path: &Path) -> Result<(Self, Vec<String>), String> {
|
||||
pub fn from_path_raw(path: &Path) -> Result<(Self, Vec<String>), String> {
|
||||
match std::fs::metadata(path) {
|
||||
Err(e) => Err(format!("Error accessing {}: {e}", path.to_string_lossy())),
|
||||
Ok(m) => {
|
||||
if m.is_file() {
|
||||
Self::from_file(path).map(|cfg| {
|
||||
let fname = path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or("".to_string());
|
||||
(cfg, vec![fname])
|
||||
})
|
||||
Self::_from_file_raw(path)
|
||||
.map(|cfg| {
|
||||
let fname = path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or("".to_string());
|
||||
(cfg, vec![fname])
|
||||
})
|
||||
.map_err(|e| format!("Configuration file {}: {}", path.display(), e))
|
||||
} else if m.is_dir() {
|
||||
Self::from_dir(path)
|
||||
Self::_from_dir_raw(path)
|
||||
} else {
|
||||
Err(format!(
|
||||
"Invalid file type for {}: not a file nor a directory",
|
||||
|
|
@ -170,7 +172,27 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_dir(path: &Path) -> Result<(Self, Vec<String>), String> {
|
||||
pub fn from_path(path: &Path) -> Result<(Self, Vec<String>), String> {
|
||||
match Self::from_path_raw(path) {
|
||||
Ok((mut cfg, files)) => {
|
||||
cfg.setup().map_err(ConfigError::BadConfig).map_err(|e| {
|
||||
if path.is_dir() {
|
||||
format!(
|
||||
"{e}\nWhile reading config from {}. List of files read, in that order:\n{}",
|
||||
path.display(),
|
||||
files.join("\n"),
|
||||
)
|
||||
} else {
|
||||
format!("{e}\nWhile reading config from {}.", path.display())
|
||||
}
|
||||
})?;
|
||||
Ok((cfg, files))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn _from_dir_raw(path: &Path) -> Result<(Self, Vec<String>), String> {
|
||||
let dir = std::fs::read_dir(path)
|
||||
.map_err(|e| format!("Error accessing directory {}: {e}", path.display()))?;
|
||||
let mut cfg: Option<Self> = None;
|
||||
|
|
@ -246,14 +268,7 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(mut cfg) = cfg {
|
||||
cfg.setup().map_err(ConfigError::BadConfig).map_err(|e| {
|
||||
format!(
|
||||
"{e}\nWhile reading files from {}. List of files read, in that order:\n{}",
|
||||
path.display(),
|
||||
read_cfg_fname.join("\n"),
|
||||
)
|
||||
})?;
|
||||
if let Some(cfg) = cfg {
|
||||
Ok((cfg, read_cfg_fname))
|
||||
} else {
|
||||
Err(format!(
|
||||
|
|
@ -264,8 +279,11 @@ impl Config {
|
|||
}
|
||||
|
||||
pub fn from_file(path: &Path) -> Result<Self, String> {
|
||||
Config::_from_file(path)
|
||||
.map_err(|err| format!("Configuration file {}: {}", path.display(), err))
|
||||
match Self::_from_file_raw(path) {
|
||||
Ok(mut cfg) => cfg.setup().map(|_| cfg).map_err(ConfigError::BadConfig),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
.map_err(|err| format!("Configuration file {}: {}", path.display(), err))
|
||||
}
|
||||
|
||||
fn _extension_to_format(extension: &str) -> Result<Format, ConfigError> {
|
||||
|
|
@ -289,7 +307,7 @@ impl Config {
|
|||
Ok(cfg)
|
||||
}
|
||||
|
||||
fn _from_file(path: &Path) -> Result<Self, ConfigError> {
|
||||
fn _from_file_raw(path: &Path) -> Result<Self, ConfigError> {
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|ex| ex.to_str())
|
||||
|
|
@ -297,11 +315,7 @@ impl Config {
|
|||
|
||||
let format = Self::_extension_to_format(extension)?;
|
||||
|
||||
let mut config: Config = Self::_load_file(path, format)?;
|
||||
|
||||
config.setup().map_err(ConfigError::BadConfig)?;
|
||||
|
||||
Ok(config)
|
||||
Self::_load_file(path, format)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use std::{
|
|||
|
||||
use chrono::TimeDelta;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
use super::parse_duration;
|
||||
|
|
@ -16,7 +16,7 @@ use super::{Action, Match, Pattern, Patterns};
|
|||
|
||||
// Only names are serialized
|
||||
// Only computed fields are not deserialized
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Filter {
|
||||
actions: BTreeMap<String, Action>,
|
||||
|
|
@ -31,8 +31,9 @@ pub struct Filter {
|
|||
#[serde(skip)]
|
||||
patterns: Arc<BTreeSet<Arc<Pattern>>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
retry: Option<u32>,
|
||||
#[serde(rename = "retryperiod")]
|
||||
#[serde(rename = "retryperiod", skip_serializing_if = "Option::is_none")]
|
||||
retry_period: Option<String>,
|
||||
#[serde(skip)]
|
||||
retry_duration: Option<TimeDelta>,
|
||||
|
|
@ -44,6 +45,29 @@ pub struct Filter {
|
|||
}
|
||||
|
||||
impl Filter {
|
||||
#[cfg(test)]
|
||||
pub fn from_name(stream_name: &str, filter_name: &str) -> Filter {
|
||||
Filter {
|
||||
stream_name: stream_name.into(),
|
||||
name: filter_name.into(),
|
||||
..Filter::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn from_name_and_patterns(
|
||||
stream_name: &str,
|
||||
filter_name: &str,
|
||||
patterns: Vec<Pattern>,
|
||||
) -> Filter {
|
||||
Filter {
|
||||
stream_name: stream_name.into(),
|
||||
name: filter_name.into(),
|
||||
patterns: Arc::new(patterns.into_iter().map(|p| Arc::new(p)).collect()),
|
||||
..Filter::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(test, derive(Default))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Pattern {
|
||||
pub regex: String,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
ignore: Vec<String>,
|
||||
|
||||
#[serde(default, rename = "ignoreregex")]
|
||||
#[serde(default, rename = "ignoreregex", skip_serializing_if = "Vec::is_empty")]
|
||||
ignore_regex: Vec<String>,
|
||||
#[serde(skip)]
|
||||
compiled_ignore_regex: Vec<Regex>,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use std::{cmp::Ordering, collections::BTreeMap, hash::Hash};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Filter, Patterns};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(test, derive(Default))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Stream {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::{io::IsTerminal, process::exit};
|
|||
use clap::Parser;
|
||||
use reaction::{
|
||||
cli::{Cli, SubCommand},
|
||||
client::{request, test_regex},
|
||||
client::{request, test_config, test_regex},
|
||||
daemon::daemon,
|
||||
protocol::Order,
|
||||
};
|
||||
|
|
@ -72,6 +72,11 @@ async fn main() {
|
|||
regex,
|
||||
line,
|
||||
} => test_regex(config, regex, line),
|
||||
SubCommand::TestConfig {
|
||||
config,
|
||||
format,
|
||||
verbose,
|
||||
} => test_config(config, format, verbose),
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue