Add test-config sub-command

This commit is contained in:
Baptiste Careil 2024-12-28 22:48:28 +01:00 committed by ppom
commit 231c9f8a99
No known key found for this signature in database
9 changed files with 156 additions and 47 deletions

View file

@ -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

View file

@ -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
View 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(())
}

View file

@ -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

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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>,

View file

@ -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 {

View file

@ -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(()) => {