1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use std::{env, net::SocketAddr, path::PathBuf};

use anyhow::{bail, Context};
use base64::Engine;
use clap::{Args, Parser};

use crate::config_file::ConfigFile;

#[derive(Clone, derivative::Derivative)]
#[derivative(Debug)]
pub struct AppConfig {
    pub bind_addr: SocketAddr,
    pub config_file: ConfigFile,
}

impl AppConfig {
    /// Parse from `std::env::args_os()` and/or `std::env::vars()`,
    /// # Panics
    /// Panics if a valid configuration cannot be created from environment variables and CLI
    /// arguments
    pub fn try_parse() -> anyhow::Result<Self> {
        #[allow(clippy::expect_used)]
        AppArgs::parse().try_into()
    }
}

#[derive(Parser, Debug)]
#[clap(author, about, long_version = long_version(), version = version(), help_template=help())]
pub struct AppArgs {
    /// IP Address to bind webserver to, e.g. 0.0.0.0:8080
    #[clap(long, value_parser, env = "ROUTEROS_DHCP_LEASES_WEBUI_BIND_ADDR")]
    bind_addr: SocketAddr,

    #[command(flatten)]
    config_file: ConfigFileArg,
}

#[derive(Args, Debug)]
#[group(required = true, multiple = false)]
pub struct ConfigFileArg {
    /// Path to config file
    #[clap(long, value_parser, env = "ROUTEROS_DHCP_LEASES_WEBUI_CONFIG_FILE")]
    config_file: Option<PathBuf>,

    /// Content of the config file in base64
    #[clap(
        long,
        value_parser,
        env = "ROUTEROS_DHCP_LEASES_WEBUI_CONFIG_FILE_CONTENT_B64"
    )]
    config_file_content_b64: Option<String>,
}

impl TryFrom<AppArgs> for AppConfig {
    type Error = anyhow::Error;

    fn try_from(value: AppArgs) -> Result<Self, Self::Error> {
        let AppArgs {
            bind_addr,
            config_file,
        } = value;

        let config_file = if let Some(config_file_content) = config_file.config_file_content_b64 {
            let content = base64::engine::general_purpose::STANDARD
                .decode(config_file_content)
                .with_context(|| "Failed to decode config file content from base64")?;
            ConfigFile::from_yaml_reader(&content[..])
                .with_context(|| "Failed to load config file from content")?
        } else if let Some(config_file_path) = config_file.config_file {
            if !config_file_path.is_file() {
                bail!("Config file `{}` doesn't exist", config_file_path.display())
            }

            ConfigFile::load_from_yaml(&config_file_path)
                .with_context(|| "Failed to load config file")?
        } else {
            bail!("No config file provided");
        };

        Ok(Self {
            bind_addr,
            config_file,
        })
    }
}


const fn version() -> &'static str {
    concat!(
        env!("CARGO_PKG_VERSION"),
        " git:",
        env!("VERGEN_GIT_DESCRIBE")
    )
}

const fn long_version() -> &'static str {
    concat!(
        env!("CARGO_PKG_VERSION"),
        "\n",
        "\nAuthors:\t",
        env!("CARGO_PKG_AUTHORS"),
        "\nCommit SHA:\t",
        env!("VERGEN_GIT_SHA"),
        "\nCommit Date:\t",
        env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
        "\nDescribe:\t",
        env!("VERGEN_GIT_DESCRIBE"),
        "\nrustc Version:\t",
        env!("VERGEN_RUSTC_SEMVER"),
        "\nrustc SHA:\t",
        env!("VERGEN_RUSTC_COMMIT_HASH"),
        "\ncargo Target:\t",
        env!("VERGEN_CARGO_TARGET_TRIPLE"),
        "\ncargo opt:\t",
        env!("VERGEN_CARGO_OPT_LEVEL"),
    )
}

const fn help() -> &'static str {
    "{before-help}{name} {version}
{author-with-newline}{about-with-newline}
{usage-heading} {usage}

{all-args}{after-help}
"
}