use crate::theme::Theme;
use serde::Deserialize;
use std::path::PathBuf;
pub mod output;
pub mod source;
pub mod validate;
pub use output::{Format, OutputConfig};
pub use source::{ConfigSource, NetworkSource, OutputSource, ServerSource, TestSource};
pub use validate::{ValidationResult, get_config_path_internal, load_config_file, validate_config};
#[derive(Debug, Clone, Default)]
pub struct TestSelection {
pub no_download: bool,
pub no_upload: bool,
pub single: bool,
}
impl TestSelection {
#[must_use]
pub(crate) fn from_source(
source: &TestSource,
file_config: &File,
merge_bool: impl Fn(Option<bool>, Option<bool>) -> bool,
) -> Self {
Self {
no_download: merge_bool(source.no_download, file_config.no_download),
no_upload: merge_bool(source.no_upload, file_config.no_upload),
single: merge_bool(source.single, file_config.single),
}
}
}
#[derive(Debug, Clone)]
pub struct NetworkConfig {
pub source: Option<String>,
pub timeout: u64,
pub ca_cert: Option<String>,
pub tls_version: Option<String>,
pub pin_certs: bool,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
source: None,
timeout: 10,
ca_cert: None,
tls_version: None,
pin_certs: false,
}
}
}
impl NetworkConfig {
#[must_use]
pub(crate) fn from_source(
source: &NetworkSource,
file_config: &File,
merge_bool: impl Fn(Option<bool>, Option<bool>) -> bool,
merge_u64: impl Fn(u64, Option<u64>, u64) -> u64,
) -> Self {
Self {
source: source.source.clone(),
timeout: merge_u64(source.timeout, file_config.timeout, 10),
ca_cert: source.ca_cert.clone().or(file_config.ca_cert.clone()),
tls_version: source
.tls_version
.clone()
.or(file_config.tls_version.clone()),
pin_certs: merge_bool(source.pin_certs, file_config.pin_certs),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ServerSelection {
pub server_ids: Vec<String>,
pub exclude_ids: Vec<String>,
}
impl ServerSelection {
#[must_use]
pub(crate) fn from_source(source: &ServerSource) -> Self {
Self {
server_ids: source.server_ids.clone(),
exclude_ids: source.exclude_ids.clone(),
}
}
}
#[derive(Debug, Default, Clone, Deserialize)]
pub struct File {
pub no_download: Option<bool>,
pub no_upload: Option<bool>,
pub single: Option<bool>,
pub bytes: Option<bool>,
pub simple: Option<bool>,
pub csv: Option<bool>,
pub csv_delimiter: Option<char>,
pub csv_header: Option<bool>,
pub json: Option<bool>,
pub timeout: Option<u64>,
pub profile: Option<String>,
pub theme: Option<String>,
pub custom_user_agent: Option<String>,
pub strict: Option<bool>,
pub ca_cert: Option<String>,
pub tls_version: Option<String>,
pub pin_certs: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct Config {
pub output: OutputConfig,
pub test: TestSelection,
pub network: NetworkConfig,
pub servers: ServerSelection,
pub custom_user_agent: Option<String>,
pub strict: bool,
}
pub trait ConfigProvider: Send + Sync {
fn config(&self) -> &Config;
}
impl ConfigProvider for Config {
fn config(&self) -> &Config {
self
}
}
impl Config {
#[allow(deprecated)]
#[must_use]
pub fn from_args(args: &crate::cli::Args) -> Self {
let source = ConfigSource::from_args(args);
Self::from_source(&source)
}
#[allow(deprecated)]
#[must_use]
pub fn from_args_with_file(
source: &ConfigSource,
file_config: Option<File>,
) -> (Self, ValidationResult) {
let config = Self::from_source_with_file(source, file_config);
let mut validation = ValidationResult::ok();
if let Some(ref profile_name) = source.output.profile {
if crate::profiles::UserProfile::validate(profile_name).is_err() {
validation = validation.with_warning(format!(
"Unknown profile '{}'. Valid options: {}. Using 'power-user'.",
profile_name,
crate::profiles::UserProfile::VALID_NAMES.join(", ")
));
}
}
(config, validation)
}
#[must_use]
pub fn from_source(source: &ConfigSource) -> Self {
let file_config = load_config_file().unwrap_or_default();
Self::from_source_with_file(source, Some(file_config))
}
#[must_use]
pub(crate) fn from_source_with_file(source: &ConfigSource, file_config: Option<File>) -> Self {
let file = file_config.unwrap_or_default();
let strict = source.strict_config.unwrap_or(file.strict.unwrap_or(false));
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
if cli == default {
file.unwrap_or(default)
} else {
cli
}
};
let output = OutputConfig::from_source(&source.output, &file, merge_bool);
let test = TestSelection::from_source(&source.test, &file, merge_bool);
let network = NetworkConfig::from_source(&source.network, &file, merge_bool, merge_u64);
let servers = ServerSelection::from_source(&source.servers);
Self {
output,
test,
network,
servers,
custom_user_agent: file.custom_user_agent.clone(),
strict,
}
}
#[must_use]
pub fn validate_and_report(
&self,
source: &ConfigSource,
file_config: Option<File>,
) -> ValidationResult {
let file = file_config.unwrap_or_else(|| load_config_file().unwrap_or_default());
let mut validation = validate_config(&file);
if let Some(ref profile_name) = source.output.profile {
if crate::profiles::UserProfile::validate(profile_name).is_err() {
validation = validation.with_warning(format!(
"Unknown profile '{}'. Valid options: {}. Using 'power-user'.",
profile_name,
crate::profiles::UserProfile::VALID_NAMES.join(", ")
));
}
}
validation
}
#[must_use]
pub fn should_save_history(&self) -> bool {
if self.format().is_some_and(|f| f.is_machine_readable()) {
return false;
}
if self.json() || self.csv() {
return false;
}
true
}
#[must_use]
pub fn no_download(&self) -> bool {
self.test.no_download
}
#[must_use]
pub fn no_upload(&self) -> bool {
self.test.no_upload
}
#[must_use]
pub fn single(&self) -> bool {
self.test.single
}
#[must_use]
pub fn bytes(&self) -> bool {
self.output.bytes
}
#[must_use]
pub fn simple(&self) -> bool {
self.output.simple
}
#[must_use]
pub fn csv(&self) -> bool {
self.output.csv
}
#[must_use]
pub fn json(&self) -> bool {
self.output.json
}
#[must_use]
pub fn quiet(&self) -> bool {
self.output.quiet
}
#[must_use]
pub fn list(&self) -> bool {
self.output.list
}
#[must_use]
pub fn minimal(&self) -> bool {
self.output.minimal
}
#[must_use]
pub fn theme(&self) -> Theme {
self.output.theme
}
#[must_use]
pub fn csv_delimiter(&self) -> char {
self.output.csv_delimiter
}
#[must_use]
pub fn csv_header(&self) -> bool {
self.output.csv_header
}
#[must_use]
pub fn profile(&self) -> Option<&str> {
self.output.profile.as_deref()
}
#[must_use]
pub fn format(&self) -> Option<Format> {
self.output.format
}
#[must_use]
pub fn timeout(&self) -> u64 {
self.network.timeout
}
#[must_use]
pub fn source(&self) -> Option<&str> {
self.network.source.as_deref()
}
#[must_use]
pub fn ca_cert(&self) -> Option<&str> {
self.network.ca_cert.as_deref()
}
#[must_use]
pub(crate) fn ca_cert_path(&self) -> Option<PathBuf> {
self.network.ca_cert.as_ref().map(PathBuf::from)
}
#[must_use]
pub fn tls_version(&self) -> Option<&str> {
self.network.tls_version.as_deref()
}
#[must_use]
pub fn pin_certs(&self) -> bool {
self.network.pin_certs
}
#[must_use]
pub fn server_ids(&self) -> &[String] {
&self.servers.server_ids
}
#[must_use]
pub fn exclude_ids(&self) -> &[String] {
&self.servers.exclude_ids
}
#[must_use]
pub fn custom_user_agent(&self) -> Option<&str> {
self.custom_user_agent.as_deref()
}
#[must_use]
pub fn strict(&self) -> bool {
self.strict
}
}
#[cfg(test)]
mod tests;