1use std::path::PathBuf;
2
3use clap::{CommandFactory, Parser, Subcommand};
4use clap_serde_derive::ClapSerde;
5use config::ConfigError;
6use toml::Value;
7
8use crate::config_source::{ConfigSource, track_env_sources};
9use crate::docs::Docs;
10use crate::local::Local;
11use crate::log::{LogFormat, LogLevel};
12use crate::oauth2::OAuth2;
13use crate::origin::Origin;
14use crate::postgresql::Postgresql;
15use crate::proxy::Proxy;
16use crate::registry::Registry;
17use crate::s3::S3;
18use crate::settings::Settings;
19use crate::setup::Setup;
20use crate::toolchain::Toolchain;
21use crate::{SourceMap, compile_time_config};
22
23fn track_settings_differences(
29 sources: &mut SourceMap,
30 current: &Settings,
31 previous: &Settings,
32 source: ConfigSource,
33) {
34 let Ok(current_value) = Value::try_from(current) else {
35 return;
36 };
37 let Ok(previous_value) = Value::try_from(previous) else {
38 return;
39 };
40
41 let (Value::Table(current_table), Value::Table(previous_table)) =
42 (current_value, previous_value)
43 else {
44 return;
45 };
46
47 for (section_name, section_value) in ¤t_table {
48 let Some(previous_section) = previous_table.get(section_name) else {
50 continue;
51 };
52
53 let (Value::Table(current_fields), Value::Table(previous_fields)) =
55 (section_value, previous_section)
56 else {
57 continue;
58 };
59
60 for (field_name, field_value) in current_fields {
62 if let Some(previous_field) = previous_fields.get(field_name)
63 && field_value != previous_field
64 {
65 sources.insert(format!("{section_name}.{field_name}"), source);
66 }
67 }
68 }
69}
70
71#[derive(Parser)]
72#[command(name = "kellnr", version, about)]
73pub struct Cli {
74 #[arg(id = "config", short = 'c', long = "config", global = true)]
76 pub config_file: Option<PathBuf>,
77
78 #[command(subcommand)]
79 pub command: Option<Command>,
80}
81
82#[derive(Parser, Default)]
84pub struct ServerArgs {
85 #[command(flatten)]
86 pub local: <Local as ClapSerde>::Opt,
87
88 #[command(flatten)]
89 pub origin: <Origin as ClapSerde>::Opt,
90
91 #[command(flatten)]
92 pub registry: <Registry as ClapSerde>::Opt,
93
94 #[command(flatten)]
95 pub docs: <Docs as ClapSerde>::Opt,
96
97 #[command(flatten)]
98 pub proxy: <Proxy as ClapSerde>::Opt,
99
100 #[command(flatten)]
101 pub postgresql: <Postgresql as ClapSerde>::Opt,
102
103 #[command(flatten)]
104 pub s3: <S3 as ClapSerde>::Opt,
105
106 #[command(flatten)]
107 pub setup: <Setup as ClapSerde>::Opt,
108
109 #[command(flatten)]
110 pub oauth2: <OAuth2 as ClapSerde>::Opt,
111
112 #[command(flatten)]
113 pub toolchain: <Toolchain as ClapSerde>::Opt,
114
115 #[arg(id = "log-format", long = "log-format", value_enum)]
118 pub log_format: Option<LogFormat>,
119
120 #[arg(id = "log-level", short = 'l', long = "log-level", value_enum)]
122 pub log_level: Option<LogLevel>,
123
124 #[arg(id = "log-level-web-server", long = "log-level-web-server", value_enum)]
126 pub log_level_web_server: Option<LogLevel>,
127}
128
129#[derive(Subcommand)]
130#[allow(clippy::large_enum_variant)]
131pub enum Command {
132 Start {
134 #[command(flatten)]
135 server: ServerArgs,
136 },
137 Config {
139 #[command(subcommand)]
140 action: ConfigAction,
141 },
142}
143
144#[derive(Subcommand)]
145pub enum ConfigAction {
146 Show {
148 #[arg(long = "no-defaults")]
150 no_defaults: bool,
151
152 #[arg(long = "sources")]
154 show_sources: bool,
155 },
156 Init {
158 #[arg(short = 'o', long = "output")]
160 output: Option<PathBuf>,
161 },
162}
163
164#[derive(Debug, Clone, Default)]
166pub struct ShowConfigOptions {
167 pub no_defaults: bool,
169 pub show_sources: bool,
171}
172
173pub enum CliResult {
174 RunServer(Settings),
175 ShowConfig {
176 settings: Settings,
177 options: ShowConfigOptions,
178 },
179 InitConfig {
180 settings: Settings,
181 output: PathBuf,
182 },
183 ShowHelp,
184}
185
186pub fn parse_cli() -> Result<CliResult, ConfigError> {
187 let cli = Cli::parse();
188
189 if let Some(Command::Config {
191 action: ConfigAction::Init { output },
192 }) = &cli.command
193 {
194 let output_path = output
195 .clone()
196 .unwrap_or_else(|| PathBuf::from("kellnr.toml"));
197 return Ok(CliResult::InitConfig {
198 settings: Settings::default(),
199 output: output_path,
200 });
201 }
202
203 let env_config_file = std::env::var("KELLNR_CONFIG_FILE").ok();
205 let config_file: Option<PathBuf> = cli
206 .config_file
207 .or(env_config_file.map(PathBuf::from))
208 .or(compile_time_config::KELLNR_COMPTIME__CONFIG_FILE.map(PathBuf::from));
209
210 let mut settings = Settings::try_from(config_file.as_deref())?;
212
213 if config_file.is_some() {
215 track_toml_sources(&mut settings);
216 }
217
218 track_env_sources(&mut settings.sources);
221
222 match cli.command {
224 Some(Command::Start { server }) => {
225 track_and_merge_cli(&mut settings, server);
227
228 Ok(CliResult::RunServer(settings))
229 }
230 Some(Command::Config { action }) => match action {
231 ConfigAction::Show {
232 no_defaults,
233 show_sources,
234 } => Ok(CliResult::ShowConfig {
235 settings,
236 options: ShowConfigOptions {
237 no_defaults,
238 show_sources,
239 },
240 }),
241 ConfigAction::Init { .. } => {
242 unreachable!()
244 }
245 },
246 None => {
247 Cli::command().print_help().ok();
249 Ok(CliResult::ShowHelp)
250 }
251 }
252}
253
254pub fn get_settings_with_cli() -> Result<Settings, ConfigError> {
256 match parse_cli()? {
257 CliResult::RunServer(settings)
258 | CliResult::ShowConfig { settings, .. }
259 | CliResult::InitConfig { settings, .. } => Ok(settings),
260 CliResult::ShowHelp => Ok(Settings::default()),
261 }
262}
263
264fn track_toml_sources(settings: &mut Settings) {
269 let current = settings.clone();
270 let defaults = Settings::default();
271 track_settings_differences(
272 &mut settings.sources,
273 ¤t,
274 &defaults,
275 ConfigSource::Toml,
276 );
277}
278
279fn track_and_merge_cli(settings: &mut Settings, server: ServerArgs) {
288 let before = settings.clone();
290
291 settings.local = settings.local.clone().merge(server.local);
293 settings.origin = settings.origin.clone().merge(server.origin);
294 settings.registry = settings.registry.clone().merge(server.registry);
295 settings.docs = settings.docs.clone().merge(server.docs);
296 settings.proxy = settings.proxy.clone().merge(server.proxy);
297 settings.postgresql = settings.postgresql.clone().merge(server.postgresql);
298 settings.s3 = settings.s3.clone().merge(server.s3);
299 settings.setup = settings.setup.clone().merge(server.setup);
300 settings.oauth2 = settings.oauth2.clone().merge(server.oauth2);
301 settings.toolchain = settings.toolchain.clone().merge(server.toolchain);
302
303 if let Some(format) = server.log_format {
305 settings.log.format = format;
306 }
307 if let Some(level) = server.log_level {
308 settings.log.level = level;
309 }
310 if let Some(level) = server.log_level_web_server {
311 settings.log.level_web_server = level;
312 }
313
314 let current = settings.clone();
317 track_settings_differences(&mut settings.sources, ¤t, &before, ConfigSource::Cli);
318}