Skip to content

Commit d6e5c1f

Browse files
committed
josh-proxy: use clap derive
This simplifies the argument parsing a lot - we can almost parse the needed structure just with that, including all the manual default handling. The only thing that does change is the handling of upstreams - rather than populating two fields in the Arg struct, it now contains a `Vec<Remote>`. We can use clap to ensure there's at least one element (same for local), but comparison of individual args and further validation (max 2, not two of the same type) is now left to `josh-proxy.rs`. For this, a `make_upstream` is introduced, turning that list of enums with URLs into a JoshProxyUpstream. Error handling in run_proxy isn't awfully verbose, just exit nonzero, so I opted to log *that* specific error with an eprintln!, but happy to also add it for all errors (in main()). This is in preparation for josh-project#1288.
1 parent 4a5c0a1 commit d6e5c1f

File tree

2 files changed

+73
-173
lines changed

2 files changed

+73
-173
lines changed

josh-proxy/src/bin/josh-proxy.rs

+34-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#![deny(warnings)]
22
#[macro_use]
33
extern crate lazy_static;
4+
extern crate clap;
45

6+
use clap::Parser;
7+
use josh_proxy::cli::Remote;
58
use josh_proxy::{run_git_with_auth, FetchError, MetaConfig, RemoteAuth, RepoConfig, RepoUpdate};
69
use opentelemetry::global;
710
use opentelemetry::sdk::propagation::TraceContextPropagator;
@@ -37,7 +40,7 @@ fn version_str() -> String {
3740
}
3841

3942
lazy_static! {
40-
static ref ARGS: josh_proxy::cli::Args = josh_proxy::cli::parse_args_or_exit(1);
43+
static ref ARGS: josh_proxy::cli::Args = josh_proxy::cli::Args::parse();
4144
}
4245

4346
josh::regex_parsed!(
@@ -1424,20 +1427,40 @@ fn trace_http_response_code(trace_span: Span, http_status: StatusCode) {
14241427
};
14251428
}
14261429

1430+
/// Consume a list of strings, and return a JoshProxyUpstream struct.
1431+
fn make_upstream(remotes: &Vec<Remote>) -> josh::JoshResult<JoshProxyUpstream> {
1432+
if remotes.is_empty() {
1433+
unreachable!() // already checked in the parser
1434+
} else if remotes.len() == 1 {
1435+
Ok(match &remotes[0] {
1436+
Remote::Http(url) => JoshProxyUpstream::Http(url.to_string()),
1437+
Remote::Ssh(url) => JoshProxyUpstream::Ssh(url.to_string()),
1438+
})
1439+
} else if remotes.len() == 2 {
1440+
Ok(match (&remotes[0], &remotes[1]) {
1441+
(Remote::Http(_), Remote::Http(_)) | (Remote::Ssh(_), Remote::Ssh(_)) => {
1442+
return Err(josh_error("two remotes of the same type passed"))
1443+
}
1444+
(Remote::Http(http_url), Remote::Ssh(ssh_url))
1445+
| (Remote::Ssh(ssh_url), Remote::Http(http_url)) => JoshProxyUpstream::Both {
1446+
http: http_url.to_string(),
1447+
ssh: ssh_url.to_string(),
1448+
},
1449+
})
1450+
} else {
1451+
Err(josh_error("too many remotes"))
1452+
}
1453+
}
1454+
14271455
#[tokio::main]
14281456
async fn run_proxy() -> josh::JoshResult<i32> {
14291457
let addr = format!("[::]:{}", ARGS.port).parse()?;
1430-
let upstream = match (&ARGS.remote.http, &ARGS.remote.ssh) {
1431-
(Some(http), None) => JoshProxyUpstream::Http(http.clone()),
1432-
(None, Some(ssh)) => JoshProxyUpstream::Ssh(ssh.clone()),
1433-
(Some(http), Some(ssh)) => JoshProxyUpstream::Both {
1434-
http: http.clone(),
1435-
ssh: ssh.clone(),
1436-
},
1437-
(None, None) => return Err(josh_error("missing remote host url")),
1438-
};
1458+
let upstream = make_upstream(&ARGS.remote).map_err(|e| {
1459+
eprintln!("Upstream parsing error: {}", &e);
1460+
e
1461+
})?;
14391462

1440-
let local = std::path::PathBuf::from(&ARGS.local);
1463+
let local = std::path::PathBuf::from(&ARGS.local.as_ref().unwrap());
14411464
let local = if local.is_absolute() {
14421465
local
14431466
} else {

josh-proxy/src/cli.rs

+39-162
Original file line numberDiff line numberDiff line change
@@ -1,175 +1,52 @@
1-
use josh::{josh_error, JoshResult};
1+
#[derive(Clone, Debug)]
2+
pub enum Remote {
3+
Http(String),
4+
Ssh(String),
5+
}
26

3-
pub struct Remote {
4-
pub http: Option<String>,
5-
pub ssh: Option<String>,
7+
fn parse_remote(s: &str) -> Result<Remote, &'static str> {
8+
match s {
9+
s if s.starts_with("http://") || s.starts_with("https://") => {
10+
Ok(Remote::Http(s.to_string()))
11+
}
12+
s if s.starts_with("ssh://") => Ok(Remote::Ssh(s.to_string())),
13+
_ => return Err("unsupported scheme"),
14+
}
615
}
716

17+
#[derive(clap::Parser, Debug)]
18+
#[command(name = "josh-proxy")]
819
pub struct Args {
9-
pub remote: Remote,
10-
pub local: String,
20+
#[arg(long, required = true, value_parser = parse_remote)]
21+
pub remote: Vec<Remote>,
22+
#[arg(long, required = true)]
23+
pub local: Option<String>,
24+
#[arg(name = "poll", long)]
1125
pub poll_user: Option<String>,
26+
#[arg(long, help = "Run git gc during maintenance")]
1227
pub gc: bool,
28+
#[arg(long)]
1329
pub require_auth: bool,
30+
#[arg(long)]
1431
pub no_background: bool,
32+
33+
#[arg(
34+
short,
35+
help = "DEPRECATED - no effect! Number of concurrent upstream git fetch/push operations"
36+
)]
37+
_n: Option<String>,
38+
39+
#[arg(long, default_value = "8000")]
1540
pub port: u16,
41+
#[arg(
42+
short,
43+
default_value = "0",
44+
help = "Duration between forced cache refresh"
45+
)]
46+
#[arg(long, short)]
1647
pub cache_duration: u64,
48+
#[arg(long, help = "Duration between forced cache refresh")]
1749
pub static_resource_proxy_target: Option<String>,
50+
#[arg(long, help = "Filter to be prefixed to all queries of this instance")]
1851
pub filter_prefix: Option<String>,
1952
}
20-
21-
fn parse_int<T: std::str::FromStr>(
22-
matches: &clap::ArgMatches,
23-
arg_name: &str,
24-
default: Option<T>,
25-
) -> JoshResult<T>
26-
where
27-
<T as std::str::FromStr>::Err: std::fmt::Display,
28-
{
29-
let arg = matches.get_one::<String>(arg_name).map(|s| s.as_str());
30-
31-
let arg = match (arg, default) {
32-
(None, None) => {
33-
return Err(josh_error(&format!(
34-
"missing required argument: {}",
35-
arg_name
36-
)))
37-
}
38-
(None, Some(default)) => Ok(default),
39-
(Some(value), _) => value.parse::<T>(),
40-
};
41-
42-
arg.map_err(|e| josh_error(&format!("error parsing argument {}: {}", arg_name, e)))
43-
}
44-
45-
fn make_command() -> clap::Command {
46-
clap::Command::new("josh-proxy")
47-
.arg(
48-
clap::Arg::new("remote")
49-
.long("remote")
50-
.action(clap::ArgAction::Append),
51-
)
52-
.arg(clap::Arg::new("local").long("local"))
53-
.arg(clap::Arg::new("poll").long("poll"))
54-
.arg(
55-
clap::Arg::new("gc")
56-
.long("gc")
57-
.action(clap::ArgAction::SetTrue)
58-
.help("Run git gc during maintenance"),
59-
)
60-
.arg(
61-
clap::Arg::new("require-auth")
62-
.long("require-auth")
63-
.action(clap::ArgAction::SetTrue),
64-
)
65-
.arg(
66-
clap::Arg::new("no-background")
67-
.long("no-background")
68-
.action(clap::ArgAction::SetTrue),
69-
)
70-
.arg(clap::Arg::new("n").short('n').help(
71-
"DEPRECATED - no effect! Number of concurrent upstream git fetch/push operations",
72-
))
73-
.arg(clap::Arg::new("port").long("port"))
74-
.arg(
75-
clap::Arg::new("cache-duration")
76-
.long("cache-duration")
77-
.short('c')
78-
.help("Duration between forced cache refresh"),
79-
)
80-
.arg(
81-
clap::Arg::new("static-resource-proxy-target")
82-
.long("static-resource-proxy-target")
83-
.help("Proxy static resource requests to a different URL"),
84-
)
85-
.arg(
86-
clap::Arg::new("filter-prefix")
87-
.long("filter-prefix")
88-
.help("Filter to be prefixed to all queries of this instance"),
89-
)
90-
}
91-
92-
fn parse_remotes(values: &[String]) -> JoshResult<Remote> {
93-
let mut result = Remote {
94-
http: None,
95-
ssh: None,
96-
};
97-
98-
for value in values {
99-
match value {
100-
v if v.starts_with("http://") || v.starts_with("https://") => {
101-
result.http = match result.http {
102-
None => Some(v.clone()),
103-
Some(v) => return Err(josh_error(&format!("HTTP remote already set: {}", v))),
104-
};
105-
}
106-
v if v.starts_with("ssh://") => {
107-
result.ssh = match result.ssh {
108-
None => Some(v.clone()),
109-
Some(v) => return Err(josh_error(&format!("SSH remote already set: {}", v))),
110-
};
111-
}
112-
_ => {
113-
return Err(josh_error(&format!(
114-
"Unsupported remote protocol: {}",
115-
value
116-
)))
117-
}
118-
}
119-
}
120-
121-
Ok(result)
122-
}
123-
124-
pub fn parse_args() -> josh::JoshResult<Args> {
125-
let args = make_command().get_matches_from(std::env::args());
126-
127-
let remote = args
128-
.get_many::<String>("remote")
129-
.ok_or(josh_error("no remote specified"))?
130-
.cloned()
131-
.collect::<Vec<_>>();
132-
let remote = parse_remotes(&remote)?;
133-
134-
let local = args
135-
.get_one::<String>("local")
136-
.ok_or(josh_error("missing local directory"))?
137-
.clone();
138-
139-
let poll_user = args.get_one::<String>("poll").map(String::clone);
140-
let port = parse_int::<u16>(&args, "port", Some(8000))?;
141-
let cache_duration = parse_int::<u64>(&args, "cache-duration", Some(0))?;
142-
let static_resource_proxy_target = args
143-
.get_one::<String>("static-resource-proxy-target")
144-
.map(String::clone);
145-
146-
let filter_prefix = args.get_one::<String>("filter-prefix").map(String::clone);
147-
148-
Ok(Args {
149-
remote,
150-
local,
151-
poll_user,
152-
gc: args.get_flag("gc"),
153-
require_auth: args.get_flag("require-auth"),
154-
no_background: args.get_flag("no-background"),
155-
port,
156-
cache_duration,
157-
static_resource_proxy_target,
158-
filter_prefix,
159-
})
160-
}
161-
162-
pub fn parse_args_or_exit(code: i32) -> Args {
163-
match parse_args() {
164-
Err(e) => {
165-
eprintln!("Argument parsing error: {}", e.0);
166-
std::process::exit(code);
167-
}
168-
Ok(args) => args,
169-
}
170-
}
171-
172-
#[test]
173-
fn verify_app() {
174-
make_command().debug_assert();
175-
}

0 commit comments

Comments
 (0)