Skip to content

Commit 15cf4da

Browse files
committed
cli: Remove lexopt parsing from service modules
Remove all lexopt-based CLI parsing from service modules since clap now handles argument parsing at the app layer via derive macros.
1 parent 53f3ae4 commit 15cf4da

11 files changed

Lines changed: 56 additions & 823 deletions

File tree

cli/src/services/completion.rs

Lines changed: 1 addition & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
use anyhow::{bail, Result};
2-
use lexopt::Arg;
3-
use lexopt::ValueExt;
4-
51
pub const NAME: &str = "completion";
62

73
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -16,71 +12,6 @@ pub struct CompletionRequest {
1612
pub shell: CompletionShell,
1713
}
1814

19-
pub fn completion_usage_text() -> &'static str {
20-
"Usage:\n sce completion --shell <bash|zsh|fish>\n\nExamples:\n sce completion --shell bash > ./sce.bash\n sce completion --shell zsh > ./_sce\n sce completion --shell fish > ~/.config/fish/completions/sce.fish"
21-
}
22-
23-
pub fn parse_completion_request(args: Vec<String>) -> Result<CompletionRequest> {
24-
let mut parser = lexopt::Parser::from_args(args);
25-
let mut shell = None;
26-
27-
while let Some(arg) = parser.next()? {
28-
match arg {
29-
Arg::Long("shell") => {
30-
if shell.is_some() {
31-
bail!(
32-
"Option '--shell' may only be provided once. Run 'sce completion --help' to see valid usage."
33-
);
34-
}
35-
let value = parser.value()?;
36-
let raw = value.string()?;
37-
shell = Some(parse_shell(&raw)?);
38-
}
39-
Arg::Long("help") | Arg::Short('h') => {
40-
bail!("Use 'sce completion --help' for completion usage.");
41-
}
42-
Arg::Long(option) => {
43-
bail!(
44-
"Unknown completion option '--{}'. Run 'sce completion --help' to see valid usage.",
45-
option
46-
);
47-
}
48-
Arg::Short(option) => {
49-
bail!(
50-
"Unknown completion option '-{}'. Run 'sce completion --help' to see valid usage.",
51-
option
52-
);
53-
}
54-
Arg::Value(value) => {
55-
bail!(
56-
"Unexpected completion argument '{}'. Run 'sce completion --help' to see valid usage.",
57-
value.string()?
58-
);
59-
}
60-
}
61-
}
62-
63-
let Some(shell) = shell else {
64-
bail!(
65-
"Missing required option '--shell <bash|zsh|fish>'. Run 'sce completion --help' to see valid usage."
66-
);
67-
};
68-
69-
Ok(CompletionRequest { shell })
70-
}
71-
72-
fn parse_shell(raw: &str) -> Result<CompletionShell> {
73-
match raw {
74-
"bash" => Ok(CompletionShell::Bash),
75-
"zsh" => Ok(CompletionShell::Zsh),
76-
"fish" => Ok(CompletionShell::Fish),
77-
_ => bail!(
78-
"Unsupported shell '{}'. Valid values: bash, zsh, fish.",
79-
raw
80-
),
81-
}
82-
}
83-
8415
pub fn render_completion(request: CompletionRequest) -> String {
8516
match request.shell {
8617
CompletionShell::Bash => bash_completion_script().to_string(),
@@ -269,36 +200,7 @@ complete -c sce -n "__fish_seen_subcommand_from completion" -l shell -r -a "bash
269200

270201
#[cfg(test)]
271202
mod tests {
272-
use super::{parse_completion_request, render_completion, CompletionRequest, CompletionShell};
273-
274-
#[test]
275-
fn parse_requires_shell() {
276-
let error = parse_completion_request(vec![]).expect_err("missing --shell should fail");
277-
assert!(error
278-
.to_string()
279-
.contains("Missing required option '--shell"));
280-
}
281-
282-
#[test]
283-
fn parse_accepts_shell_value() {
284-
let request = parse_completion_request(vec!["--shell".to_string(), "zsh".to_string()])
285-
.expect("request should parse");
286-
assert_eq!(request.shell, CompletionShell::Zsh);
287-
}
288-
289-
#[test]
290-
fn parse_rejects_duplicate_shell_option() {
291-
let error = parse_completion_request(vec![
292-
"--shell".to_string(),
293-
"bash".to_string(),
294-
"--shell".to_string(),
295-
"zsh".to_string(),
296-
])
297-
.expect_err("duplicate --shell should fail");
298-
assert!(error
299-
.to_string()
300-
.contains("Option '--shell' may only be provided once"));
301-
}
203+
use super::{render_completion, CompletionRequest, CompletionShell};
302204

303205
#[test]
304206
fn render_bash_completion_is_deterministic() {

cli/src/services/config.rs

Lines changed: 3 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use std::path::{Path, PathBuf};
22

33
use anyhow::{anyhow, bail, Context, Result};
4-
use lexopt::{Arg, ValueExt};
54
use serde_json::{json, Value};
65

76
use crate::services::output_format::OutputFormat;
@@ -73,7 +72,6 @@ impl ValueSource {
7372

7473
#[derive(Clone, Debug, Eq, PartialEq)]
7574
pub enum ConfigSubcommand {
76-
Help,
7775
Show(ConfigRequest),
7876
Validate(ConfigRequest),
7977
}
@@ -136,108 +134,8 @@ struct FileConfigValue<T> {
136134
source: ConfigPathSource,
137135
}
138136

139-
pub fn parse_config_subcommand(mut args: Vec<String>) -> Result<ConfigSubcommand> {
140-
if args.is_empty() {
141-
bail!("Missing config subcommand. Run 'sce config --help' to see valid usage.");
142-
}
143-
144-
if let [only] = args.as_slice() {
145-
if only == "--help" || only == "-h" {
146-
return Ok(ConfigSubcommand::Help);
147-
}
148-
}
149-
150-
let subcommand = args.remove(0);
151-
let tail = args;
152-
match subcommand.as_str() {
153-
"show" => Ok(ConfigSubcommand::Show(parse_config_request(tail)?)),
154-
"validate" => Ok(ConfigSubcommand::Validate(parse_config_request(tail)?)),
155-
_ => bail!(
156-
"Unknown config subcommand '{}'. Run 'sce config --help' to see valid usage.",
157-
subcommand
158-
),
159-
}
160-
}
161-
162-
fn parse_config_request(args: Vec<String>) -> Result<ConfigRequest> {
163-
let mut parser = lexopt::Parser::from_args(args);
164-
let mut request = ConfigRequest {
165-
report_format: ReportFormat::Text,
166-
config_path: None,
167-
log_level: None,
168-
timeout_ms: None,
169-
};
170-
171-
while let Some(arg) = parser.next()? {
172-
match arg {
173-
Arg::Long("format") => {
174-
let value = parser
175-
.value()
176-
.context("Option '--format' requires a value")?;
177-
let raw = value.string()?;
178-
request.report_format = ReportFormat::parse(&raw, "sce config --help")?;
179-
}
180-
Arg::Long("config") => {
181-
let value = parser
182-
.value()
183-
.context("Option '--config' requires a path value")?;
184-
if request.config_path.is_some() {
185-
bail!(
186-
"Option '--config' may only be provided once. Run 'sce config --help' to see valid usage."
187-
);
188-
}
189-
request.config_path = Some(PathBuf::from(value.string()?));
190-
}
191-
Arg::Long("log-level") => {
192-
let value = parser
193-
.value()
194-
.context("Option '--log-level' requires a value")?;
195-
let raw = value.string()?;
196-
request.log_level = Some(LogLevel::parse(&raw, "--log-level")?);
197-
}
198-
Arg::Long("timeout-ms") => {
199-
let value = parser
200-
.value()
201-
.context("Option '--timeout-ms' requires a numeric value")?;
202-
let raw = value.string()?;
203-
let timeout = raw
204-
.parse::<u64>()
205-
.map_err(|_| anyhow!("Invalid timeout '{}' from --timeout-ms.", raw))?;
206-
request.timeout_ms = Some(timeout);
207-
}
208-
Arg::Long("help") | Arg::Short('h') => {
209-
bail!(
210-
"Use 'sce config --help' for config usage. Command-local help does not accept additional arguments."
211-
);
212-
}
213-
Arg::Long(option) => {
214-
bail!(
215-
"Unknown config option '--{}'. Run 'sce config --help' to see valid usage.",
216-
option
217-
);
218-
}
219-
Arg::Short(option) => {
220-
bail!(
221-
"Unknown config option '-{}'. Run 'sce config --help' to see valid usage.",
222-
option
223-
);
224-
}
225-
Arg::Value(value) => {
226-
let raw = value.string()?;
227-
bail!(
228-
"Unexpected config argument '{}'. Run 'sce config --help' to see valid usage.",
229-
raw
230-
);
231-
}
232-
}
233-
}
234-
235-
Ok(request)
236-
}
237-
238137
pub fn run_config_subcommand(subcommand: ConfigSubcommand) -> Result<String> {
239138
match subcommand {
240-
ConfigSubcommand::Help => Ok(config_usage_text().to_string()),
241139
ConfigSubcommand::Show(request) => {
242140
let cwd = std::env::current_dir().context("Failed to determine current directory")?;
243141
let runtime = resolve_runtime_config(&request, &cwd)?;
@@ -251,10 +149,6 @@ pub fn run_config_subcommand(subcommand: ConfigSubcommand) -> Result<String> {
251149
}
252150
}
253151

254-
pub fn config_usage_text() -> &'static str {
255-
"Usage:\n sce config show [--config <path>] [--log-level <error|warn|info|debug>] [--timeout-ms <value>] [--format <text|json>]\n sce config validate [--config <path>] [--log-level <error|warn|info|debug>] [--timeout-ms <value>] [--format <text|json>]\n\nResolution precedence: flags > env > config file > defaults\nConfig discovery order: --config, SCE_CONFIG_FILE, then discovered global+local defaults (global merged first, local overrides per key)\nEnvironment keys: SCE_CONFIG_FILE, SCE_LOG_LEVEL, SCE_TIMEOUT_MS"
256-
}
257-
258152
fn resolve_runtime_config(request: &ConfigRequest, cwd: &Path) -> Result<RuntimeConfig> {
259153
resolve_runtime_config_with(
260154
request,
@@ -606,9 +500,9 @@ fn format_resolved_value_text(key: &str, value: &str, source: ValueSource) -> St
606500
#[cfg(test)]
607501
mod tests {
608502
use super::{
609-
format_show_output, format_validate_output, parse_config_subcommand,
610-
resolve_runtime_config_with, ConfigPathSource, ConfigRequest, ConfigSubcommand,
611-
LoadedConfigPath, LogLevel, ReportFormat, ResolvedValue, RuntimeConfig, ValueSource,
503+
format_show_output, format_validate_output, resolve_runtime_config_with, ConfigPathSource,
504+
ConfigRequest, ConfigSubcommand, LoadedConfigPath, LogLevel, ReportFormat, ResolvedValue,
505+
RuntimeConfig, ValueSource,
612506
};
613507
use anyhow::Result;
614508
use serde_json::Value;
@@ -623,52 +517,6 @@ mod tests {
623517
}
624518
}
625519

626-
#[test]
627-
fn parser_routes_show_subcommand() -> Result<()> {
628-
let parsed = parse_config_subcommand(vec!["show".to_string()])?;
629-
assert_eq!(parsed, ConfigSubcommand::Show(request()));
630-
Ok(())
631-
}
632-
633-
#[test]
634-
fn parser_routes_validate_subcommand_with_options() -> Result<()> {
635-
let parsed = parse_config_subcommand(vec![
636-
"validate".to_string(),
637-
"--format".to_string(),
638-
"json".to_string(),
639-
"--log-level".to_string(),
640-
"debug".to_string(),
641-
"--timeout-ms".to_string(),
642-
"100".to_string(),
643-
"--config".to_string(),
644-
"./demo.json".to_string(),
645-
])?;
646-
assert_eq!(
647-
parsed,
648-
ConfigSubcommand::Validate(ConfigRequest {
649-
report_format: ReportFormat::Json,
650-
config_path: Some(PathBuf::from("./demo.json")),
651-
log_level: Some(LogLevel::Debug),
652-
timeout_ms: Some(100),
653-
})
654-
);
655-
Ok(())
656-
}
657-
658-
#[test]
659-
fn parser_rejects_invalid_format_with_help_guidance() {
660-
let error = parse_config_subcommand(vec![
661-
"show".to_string(),
662-
"--format".to_string(),
663-
"yaml".to_string(),
664-
])
665-
.expect_err("invalid format should fail");
666-
assert_eq!(
667-
error.to_string(),
668-
"Invalid --format value 'yaml'. Valid values: text, json. Run 'sce config --help' to see valid usage."
669-
);
670-
}
671-
672520
#[test]
673521
fn resolver_applies_precedence_flag_then_env_then_config_then_default() -> Result<()> {
674522
let req = ConfigRequest {

0 commit comments

Comments
 (0)