Skip to content

Commit

Permalink
new: added command-line option for config file path
Browse files Browse the repository at this point in the history
  • Loading branch information
pamburus committed May 2, 2024
1 parent 0952658 commit 5699746
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 36 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ members = [".", "crate/encstr"]
[workspace.package]
repository = "https://github.com/pamburus/hl"
authors = ["Pavel Ivanov <[email protected]>"]
version = "0.29.0-alpha.4"
version = "0.29.0-alpha.5"
edition = "2021"
license = "MIT"

Expand Down Expand Up @@ -40,7 +40,7 @@ chrono = { version = "0.4", default-features = false, features = [
"std",
] }
chrono-tz = { version = "0", features = ["serde"] }
clap = { version = "4", features = ["wrap_help", "derive", "env"] }
clap = { version = "4", features = ["wrap_help", "derive", "env", "string"] }
clap_complete = "4"
closure = "0"
collection_macros = "0"
Expand Down
76 changes: 75 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use std::path::PathBuf;

// third-party imports
use clap::{value_parser, ArgAction, Parser, ValueEnum};
use clap::{value_parser, ArgAction, Args, Parser, ValueEnum};
use clap_complete::Shell;
use std::num::NonZeroUsize;

Expand All @@ -16,10 +16,76 @@ use crate::{

// ---

#[derive(Args)]
pub struct BootstrapArgs {
/// Configuration file path.
#[arg(long, overrides_with = "config", value_name = "FILE", env = "HL_CONFIG", default_value = default_config_path(), num_args=1)]
pub config: String,
}

/// JSON and logfmt log converter to human readable representation.
#[derive(Parser)]
#[clap(version, disable_help_flag = true)]
pub struct BootstrapOpt {
#[command(flatten)]
pub args: BootstrapArgs,
}

impl BootstrapOpt {
pub fn parse() -> clap::error::Result<Self> {
Self::try_parse_from(Self::args())
}

pub fn args() -> Vec<String> {
let mut args = std::env::args();
let Some(first) = args.next() else {
return vec![];
};

let mut result = vec![first];
let mut follow_up = false;

while let Some(arg) = args.next() {
match (arg.as_bytes(), follow_up) {
(b"--", _) => {
break;
}
([b'-', b'-', b'c', b'o', b'n', b'f', b'i', b'g', b'=', ..], _) => {
result.push(arg);
follow_up = false;
}
(b"--config", _) => {
result.push(arg);
follow_up = true;
}
([b'-'], true) => {
result.push(arg);
follow_up = false;
}
([b'-', ..], true) => {
follow_up = false;
}
(_, true) => {
result.push(arg);
follow_up = false;
}
_ => {}
}
}

result
}
}

// ---

/// JSON and logfmt log converter to human readable representation.
#[derive(Parser)]
#[clap(version, disable_help_flag = true)]
pub struct Opt {
#[command(flatten)]
pub bootstrap: BootstrapArgs,

/// Sort messages chronologically.
#[arg(long, short = 's', overrides_with = "sort")]
pub sort: bool,
Expand Down Expand Up @@ -408,3 +474,11 @@ fn parse_non_zero_size(s: &str) -> std::result::Result<NonZeroUsize, NonZeroSize
Err(NonZeroSizeParseError::ZeroSize)
}
}

fn default_config_path() -> clap::builder::OsStr {
if let Some(dirs) = config::app_dirs() {
dirs.config_dir.join("config.yaml").into_os_string().into()
} else {
"".into()
}
}
38 changes: 27 additions & 11 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
// std imports
use std::sync::Mutex;

// third-party imports
use once_cell::sync::Lazy;
use platform_dirs::AppDirs;

// local imports
use crate::settings::Settings;
use crate::{error::Result, settings::Settings};

// ---

pub const APP_NAME: &str = "hl";

static CONFIG: Lazy<Settings> = Lazy::new(load);
static DEFAULT: Lazy<Settings> = Lazy::new(Settings::default);
static PENDING: Mutex<Option<Settings>> = Mutex::new(None);
static RESOLVED: Lazy<Settings> = Lazy::new(|| PENDING.lock().unwrap().take().unwrap_or_default());

/// Call initialize before any calls to get otherwise it will have no effect.
pub fn initialize(settings: Settings) {
*PENDING.lock().unwrap() = Some(settings);
}

/// Get the resolved settings.
/// If initialized was called before, then a clone of those settings will be returned.
/// Otherwise, the default settings will be returned.
pub fn get() -> &'static Settings {
&CONFIG
&RESOLVED
}

/// Get the default settings.
pub fn default() -> &'static Settings {
&DEFAULT
Default::default()
}

pub fn app_dirs() -> AppDirs {
AppDirs::new(Some(APP_NAME), true).unwrap()
}
/// Load settings from the given file or the default configuration file per platform.
pub fn load(path: String) -> Result<Settings> {
if path.is_empty() {
return Ok(Default::default());
}

// ---
Settings::load(&path)
}

fn load() -> Settings {
Settings::load(&app_dirs()).unwrap()
/// Get the application platform-specific directories.
pub fn app_dirs() -> Option<AppDirs> {
AppDirs::new(Some(APP_NAME), true)
}
17 changes: 17 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ pub enum Error {
ParseFloatError(#[from] ParseFloatError),
#[error(transparent)]
ParseIntError(#[from] ParseIntError),
#[error("failed to detect application directories")]
AppDirs,
}

/// SizeParseError is an error which may occur when parsing size.
Expand Down Expand Up @@ -134,3 +136,18 @@ pub struct InvalidLevelError {
pub type Result<T> = std::result::Result<T, Error>;

pub const HILITE: Color = Color::Yellow;

pub fn log(err: &Error) {
eprintln!("{} {}", Color::LightRed.bold().paint("error:"), err);
}

#[cfg(test)]

Check warning on line 144 in src/error.rs

View check run for this annotation

Codecov / codecov/patch

src/error.rs#L144

Added line #L144 was not covered by tests
mod tests {
use super::*;

#[test]
fn test_log() {
let err = Error::Io(std::io::Error::new(std::io::ErrorKind::Other, "test"));
log(&err);
}
}
20 changes: 15 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ use std::{
use chrono::Utc;
use clap::{CommandFactory, Parser};
use itertools::Itertools;
use nu_ansi_term::Color;

// local imports
use hl::{
Expand All @@ -32,14 +31,25 @@ use hl::{
// ---

fn run() -> Result<()> {
let app_dirs = config::app_dirs();
let settings = config::get();
let opt = cli::Opt::parse();
let bootstrap = match cli::BootstrapOpt::parse() {
Ok(bootstrap) => bootstrap,
Err(err) => err.exit(),
};
let settings = config::load(bootstrap.args.config)?;
config::initialize(settings.clone());

let opt = cli::Opt::parse();
if opt.help {
return cli::Opt::command().print_help().map_err(Error::Io);
}

let app_dirs = match config::app_dirs() {
Some(app_dirs) => app_dirs,
None => {
return Err(Error::AppDirs);
}
};

if let Some(shell) = opt.shell_completions {
let mut cmd = cli::Opt::command();
let name = cmd.get_name().to_string();
Expand Down Expand Up @@ -310,7 +320,7 @@ fn run() -> Result<()> {

fn main() {
if let Err(err) = run() {
eprintln!("{}: {}", Color::Red.paint("error"), err);
hl::error::log(&err);
process::exit(1);
}
}
72 changes: 57 additions & 15 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::include_str;
use chrono_tz::Tz;
use config::{Config, File, FileFormat};
use derive_deref::Deref;
use platform_dirs::AppDirs;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize, Serializer};
use strum::IntoEnumIterator;

Expand All @@ -16,11 +16,12 @@ use crate::level::Level;

// ---

static DEFAULT_SETTINGS: &str = include_str!("../etc/defaults/config.yaml");
static DEFAULT_SETTINGS_RAW: &str = include_str!("../etc/defaults/config.yaml");
static DEFAULT_SETTINGS: Lazy<Settings> = Lazy::new(|| Settings::load_from_str("", FileFormat::Yaml));

// ---

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Settings {
pub fields: Fields,
Expand All @@ -32,29 +33,37 @@ pub struct Settings {
}

impl Settings {
pub fn load(app_dirs: &AppDirs) -> Result<Self, Error> {
let filename = std::env::var("HL_CONFIG")
.unwrap_or_else(|_| app_dirs.config_dir.join("config.yaml").to_string_lossy().to_string());

pub fn load(filename: &str) -> Result<Self, Error> {
Ok(Config::builder()
.add_source(File::from_str(DEFAULT_SETTINGS, FileFormat::Yaml))
.add_source(File::with_name(&filename).required(false))
.add_source(File::from_str(DEFAULT_SETTINGS_RAW, FileFormat::Yaml))
.add_source(File::with_name(filename))
.build()?
.try_deserialize()?)
}
}

impl Default for Settings {
fn default() -> Self {
pub fn load_from_str(value: &str, format: FileFormat) -> Self {
Config::builder()
.add_source(File::from_str(DEFAULT_SETTINGS, FileFormat::Yaml))
.add_source(File::from_str(DEFAULT_SETTINGS_RAW, FileFormat::Yaml))
.add_source(File::from_str(value, format))
.build()
.unwrap()
.try_deserialize()
.unwrap()
}
}

impl Default for Settings {
fn default() -> Self {
DEFAULT_SETTINGS.clone()
}
}

impl Default for &'static Settings {
fn default() -> Self {
&DEFAULT_SETTINGS
}
}

// ---

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
Expand All @@ -80,7 +89,7 @@ pub struct PredefinedFields {

// ---

#[derive(Debug, Serialize, Deserialize, Deref, Clone)]
#[derive(Debug, Serialize, Deserialize, Deref, Clone, PartialEq, Eq)]
pub struct TimeField(pub Field);

impl Default for TimeField {
Expand Down Expand Up @@ -189,7 +198,7 @@ impl Default for CallerLineField {

// ---

#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct Field {
pub names: Vec<String>,
}
Expand Down Expand Up @@ -292,3 +301,36 @@ where
let ordered: BTreeMap<_, _> = value.iter().collect();
ordered.serialize(serializer)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_default_settings() {
let test = |settings: &Settings| {
assert_eq!(settings.concurrency, None);
assert_eq!(settings.time_format, "%b %d %T.%3N");
assert_eq!(settings.time_zone, chrono_tz::UTC);
assert_eq!(settings.theme, "universal");
};

let settings: &'static Settings = Default::default();
test(settings);
test(&Settings::default());
}

#[test]
fn test_load_settings_k8s() {
let settings = Settings::load("etc/defaults/config-k8s.yaml").unwrap();
assert_eq!(
settings.fields.predefined.time,
TimeField(Field {
names: vec!["ts".into()]
})
);
assert_eq!(settings.time_format, "%b %d %T.%3N");
assert_eq!(settings.time_zone, chrono_tz::UTC);
assert_eq!(settings.theme, "universal");
}
}

0 comments on commit 5699746

Please sign in to comment.