hc_runner/config/
mod.rs

1use std::{
2    io::{self, Write},
3    path::PathBuf,
4};
5
6use crate::{Error, Result};
7use clap::builder::NonEmptyStringValueParser;
8use clap::Parser;
9use reqwest::Url;
10use tracing::Level;
11
12use directories::ProjectDirs;
13
14extern crate config as config_rs;
15use config_rs::{Environment, File};
16use serde::Deserialize;
17
18#[derive(Clone, Debug, Parser)]
19#[command(author, version, about, long_about)]
20struct Cli {
21    #[arg(trailing_var_arg(true), required(true), value_parser=NonEmptyStringValueParser::new())]
22    pub(crate) command: Vec<String>,
23
24    /// Specify a config file in non-default location
25    #[arg(short, long)]
26    pub(crate) config: Option<PathBuf>,
27
28    /// Silence logging / warnings. Does not affect called command's output.
29    #[arg(short, long, conflicts_with("verbose"))]
30    pub quiet: bool,
31
32    /// Set healthchecks slug for this call.
33    #[arg(short, long, value_name = "NAME", value_parser=NonEmptyStringValueParser::new())]
34    pub(crate) slug: String,
35
36    /// Disable calling `/start` and only ping healthchecks if the test was successful.
37    #[arg(long)]
38    pub(crate) success_only: bool,
39
40    /// Set timeout for requests to healthchecks server.
41    #[arg(short, long)]
42    pub(crate) timeout: Option<u64>,
43
44    /// Specify the URL of the healthchecks server for this call.
45    #[arg(short, long)]
46    pub(crate) url: Option<Url>,
47
48    /// Increase logging verbosity. May be repeated. Defaults to `Level::WARN`.
49    #[arg(short, long, action = clap::ArgAction::Count)]
50    pub verbose: u8,
51}
52
53/// Settings that are configurable via config file or environment variables
54/// Order of priority (higher numbers override lower)
55/// 1. Config file
56/// 2. Environment variables
57/// 3. CLI flags
58#[derive(Debug, Deserialize)]
59struct Settings {
60    url: Option<Url>,
61    timeout: Option<u64>,
62}
63
64fn parse_verbosity(n: u8) -> Level {
65    match n {
66        0 => Level::ERROR,
67        1 => Level::WARN,
68        2 => Level::INFO,
69        3 => Level::DEBUG,
70        _ => Level::TRACE,
71    }
72}
73
74#[derive(Debug)]
75pub struct Config {
76    pub(crate) command: Vec<String>,
77    pub(crate) slug: String,
78    pub(crate) success_only: bool,
79    pub(crate) timeout: u64,
80    pub(crate) url: Url,
81    pub verbosity: Level,
82}
83
84impl Config {
85    #[tracing::instrument]
86    pub fn resolve() -> Result<Self> {
87        let cli = Cli::try_parse()?;
88        Self::resolve_with(cli)
89    }
90
91    fn resolve_with(cli: Cli) -> Result<Self> {
92        let mut builder = config_rs::Config::builder();
93
94        let conf_file = cli.config.or_else(|| {
95            ProjectDirs::from("com", "n8henrie", "hc-runner")
96                .map(|pd| pd.config_dir().join("config.toml"))
97        });
98
99        if let Some(conf_file) = conf_file {
100            // tracing not configured until after this method returns, so
101            // this is a non-pretty workaround to help users find where the
102            // config file should be placed
103            if cli.verbose >= 2 {
104                writeln!(
105                    io::stderr(),
106                    "searching for config file at {}",
107                    conf_file.display(),
108                )?;
109            };
110            builder =
111                builder.add_source(File::from(conf_file).required(false));
112        };
113        let settings: Settings = builder
114            .add_source(Environment::with_prefix("HC_RUNNER"))
115            .build()?
116            .try_deserialize()?;
117
118        let url = cli
119            .url
120            .or(settings.url)
121            .ok_or_else(|| Error::Config("Base URL not found".into()))?;
122
123        let timeout: u64 = cli.timeout.or(settings.timeout).unwrap_or(10);
124
125        let verbosity =
126            parse_verbosity(if cli.quiet { 0 } else { cli.verbose });
127        let Cli {
128            command,
129            slug,
130            success_only,
131            ..
132        } = cli;
133
134        Ok(Self {
135            command,
136            slug,
137            success_only,
138            timeout,
139            url,
140            verbosity,
141        })
142    }
143}
144
145#[cfg(test)]
146mod tests;