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 #[arg(short, long)]
26 pub(crate) config: Option<PathBuf>,
27
28 #[arg(short, long, conflicts_with("verbose"))]
30 pub quiet: bool,
31
32 #[arg(short, long, value_name = "NAME", value_parser=NonEmptyStringValueParser::new())]
34 pub(crate) slug: String,
35
36 #[arg(long)]
38 pub(crate) success_only: bool,
39
40 #[arg(short, long)]
42 pub(crate) timeout: Option<u64>,
43
44 #[arg(short, long)]
46 pub(crate) url: Option<Url>,
47
48 #[arg(short, long, action = clap::ArgAction::Count)]
50 pub verbose: u8,
51}
52
53#[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 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;