Skip to content

Commit 5771e8f

Browse files
committed
refactored the commands into their own module/traits
1 parent 6a14442 commit 5771e8f

2 files changed

Lines changed: 360 additions & 181 deletions

File tree

src/commands.rs

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
use clap::{ArgMatches, App, Arg, SubCommand};
2+
use client::*;
3+
use serde_json;
4+
use std::collections::HashMap;
5+
use std::path::Path;
6+
use std::time::Duration;
7+
8+
/// A vector containing all of the runnable commands in this module.
9+
pub fn all<'a>() -> Vec<Box<Command<'a>>> {
10+
vec![
11+
Box::new(ExitCommand),
12+
Box::new(AnalyzeCommand),
13+
Box::new(ProjectsCommand),
14+
]
15+
}
16+
17+
/// Represents the intention to exit the application with a specific exit code.
18+
pub struct Exit(pub i32);
19+
20+
/// The result of running a command; either continue, or exit the application.
21+
pub type CommandResult = Result<(), Exit>;
22+
23+
/// Everything about a command that can be run in this application.
24+
///
25+
/// The `Args` type is an internal representation of the arguments collected from
26+
/// the `clap::App` described by `as_subcommand`. These simply need to be fed
27+
/// back into the `run` method.
28+
///
29+
/// Because boxing a collection of Commands with different `Args` types isn't
30+
/// possible, see the `Command` trait, which wraps this trait by combining
31+
/// the `parse` and `run` methods into `maybe_run`, hiding the `Args` type.
32+
pub trait CommandInner<'a> {
33+
34+
/// Arguments parsed from the command line, to be passed into `run` at a later point.
35+
type Args;
36+
37+
/// Get a description of this command as a `clap::App`.
38+
fn as_subcommand(&self) -> App<'static, 'static>;
39+
40+
/// Extract an `Args` instance from the CLI argument matches.
41+
///
42+
/// The `matches` passed to this method are expected to be the raw matches for
43+
/// the entire App, so implementations should typically start with
44+
/// `if let Some(inner_matches) = matches.subcommand_matches(name_of_this_command)`.
45+
///
46+
/// If the subcommand was not matched, then the match applies to some other command,
47+
/// and this method should return `None`. Otherwise, the sub-matches should be
48+
/// parsed as `Args`, or an error message.
49+
fn parse(&self, matches: &'a ArgMatches) -> Option<Result<Self::Args, &'a str>>;
50+
51+
/// Run the command using the given `args` and a reference to an `ApiClient`.
52+
///
53+
/// This should perform any necessary HTTP activity to execute the command,
54+
/// and return a result signaling the REPL should continue, or exit with a
55+
/// particular exit code.
56+
fn run(&self, client: &ApiClient, args: Self::Args) -> CommandResult;
57+
}
58+
59+
/// Wrapper trait for `CommandInner`.
60+
///
61+
/// This trait hides the `Args` type by combining the `parse` and `run` methods
62+
/// into the `maybe_run` method.
63+
pub trait Command<'a> {
64+
65+
/// Same as Command::as_subcommand
66+
fn as_subcommand(&self) -> App<'static, 'static>;
67+
68+
/// Attempt to extract relevent arguments from `matches`, then run the command with those arguments.
69+
///
70+
/// If the arguments were not intended for this command, this method should return `None`.
71+
/// If the arguments were intended for this command, but were not formatted correctly, this
72+
/// method should return `Some(Err(explanation))` where `explanation` is a string describing
73+
/// what was wrong with the arguments.
74+
/// If the arguments are correctly formed, the command should run, and this method should
75+
/// return `Some(Ok(command_result))`.
76+
fn maybe_run<'b>(&self, matches: &'a ArgMatches, client: &'b ApiClient) -> Option<Result<CommandResult, &'a str>>;
77+
}
78+
impl <'a, T, A> Command<'a> for T where T: CommandInner<'a, Args = A> {
79+
fn as_subcommand(&self) -> App<'static, 'static> {
80+
CommandInner::as_subcommand(self)
81+
}
82+
83+
fn maybe_run<'b>(&self, matches: &'a ArgMatches, client: &'b ApiClient) -> Option<Result<CommandResult, &'a str>> {
84+
let args_opt = self.parse(matches);
85+
args_opt.map(|parsed_args| {
86+
parsed_args.map(|ok_args| {
87+
self.run(client, ok_args)
88+
})
89+
})
90+
}
91+
}
92+
93+
// -------------------------------------------------------------------------------------------------
94+
// ABOVE THIS POINT: command traits and supporting structs
95+
// -
96+
// BELOW THIS POINT: commands and their implementations
97+
// -------------------------------------------------------------------------------------------------
98+
99+
100+
// -------------------------------------------------------------------------------------------------
101+
// COMMAND: exit
102+
// -------------------------------------------------------------------------------------------------
103+
pub struct ExitCommand;
104+
impl <'a> CommandInner<'a> for ExitCommand {
105+
type Args = ();
106+
107+
fn as_subcommand(&self) -> App<'static, 'static> {
108+
SubCommand::with_name("exit")
109+
.alias("quit")
110+
.about("Exit this program ('quit' works too)")
111+
}
112+
113+
fn parse(&self, matches: &'a ArgMatches) -> Option<Result<Self::Args, &'a str>> {
114+
if let Some(_) = matches.subcommand_matches("exit") {
115+
Some(Ok(()))
116+
} else {
117+
None
118+
}
119+
}
120+
121+
fn run(&self, client: &ApiClient, _args: Self::Args) -> CommandResult {
122+
if !client.get_config().no_prompt {
123+
println!("goodbye")
124+
}
125+
Err(Exit(0))
126+
}
127+
}
128+
129+
130+
// -------------------------------------------------------------------------------------------------
131+
// COMMAND: analyze
132+
// -------------------------------------------------------------------------------------------------
133+
pub struct AnalyzeCommand;
134+
pub struct AnalyzeCommandArgs<'a> {
135+
project_id: u32,
136+
files: Vec<&'a Path>,
137+
name: Option<&'a str>
138+
}
139+
impl <'a> AnalyzeCommand {
140+
// ANALYZE - helper for argument extraction
141+
fn inner_parse(&self, analyze_args: &'a ArgMatches) -> Result<AnalyzeCommandArgs<'a>, &'a str> {
142+
let project_id: u32 = analyze_args.value_of("project-id")
143+
.ok_or("project id missing")?
144+
.parse().map_err(|_| "project-id should be a number")?;
145+
// get the list of files
146+
let files = analyze_args.values_of("file")
147+
.ok_or("must specify at least one file to analyze")?
148+
.map(|file| Path::new(file))
149+
.collect();
150+
// optional name for the analysis
151+
let name = analyze_args.value_of("name");
152+
Ok(AnalyzeCommandArgs { project_id, files, name })
153+
}
154+
}
155+
impl <'a> CommandInner<'a> for AnalyzeCommand {
156+
type Args = AnalyzeCommandArgs<'a>;
157+
158+
// ANALYZE - argument specification
159+
fn as_subcommand(&self) -> App<'static, 'static> {
160+
SubCommand::with_name("analyze")
161+
.about("Analyze some files")
162+
.arg(Arg::with_name("project-id")
163+
.short("p")
164+
.long("project-id")
165+
.value_name("ID")
166+
.required(true)
167+
.takes_value(true)
168+
)
169+
.arg(Arg::with_name("name")
170+
.short("n")
171+
.long("name")
172+
.value_name("NAME")
173+
.takes_value(true)
174+
.required(false)
175+
)
176+
.arg(Arg::with_name("file")
177+
.short("f")
178+
.long("file")
179+
.value_name("FILE")
180+
.takes_value(true)
181+
.multiple(true)
182+
.required(true)
183+
)
184+
}
185+
186+
// ANALYZE - argument extraction
187+
fn parse(&self, matches: &'a ArgMatches) -> Option<Result<Self::Args, &'a str>> {
188+
if let Some(analyze_args) = matches.subcommand_matches("analyze") {
189+
// get the project id
190+
let args = self.inner_parse(analyze_args);
191+
Some(args)
192+
} else {
193+
None
194+
}
195+
}
196+
197+
// ANALYZE - execution
198+
fn run(&self, client: &ApiClient, args: AnalyzeCommandArgs<'a>) -> CommandResult {
199+
let AnalyzeCommandArgs { project_id, files, name } = args;
200+
201+
// no matter what, start the analysis
202+
let mut analysis_response: ApiResult<ApiAnalysisJobResponse> = client
203+
.start_analysis(project_id, files)
204+
.map(|resp| {
205+
println!("# Started analysis {} with job id {}", resp.analysis_id, resp.job_id);
206+
resp
207+
});
208+
209+
// if a name was specified, tell the server to set the name
210+
if let Some(name) = name {
211+
analysis_response = analysis_response.and_then(|analysis_job_response| {
212+
let analysis_id = analysis_job_response.analysis_id;
213+
214+
client.set_analysis_name(project_id, analysis_id, name)
215+
.map(|_| {
216+
println!("# Set analysis {}'s name to \"{}\"", analysis_id, name);
217+
analysis_job_response
218+
})
219+
});
220+
}
221+
222+
let analysis_result_status = analysis_response
223+
.and_then(|analysis_job_response| {
224+
let job_id = analysis_job_response.job_id;
225+
client.poll_job_completion(&job_id, Duration::from_secs(2))
226+
});
227+
228+
match analysis_result_status {
229+
Err(e) => {
230+
eprintln!("Error during analysis: {:?}", e);
231+
Err(Exit(1))
232+
},
233+
Ok(status) => {
234+
println!("# Polling done");
235+
println!("{:?}", status);
236+
Ok(())
237+
},
238+
}
239+
}
240+
}
241+
242+
243+
// -------------------------------------------------------------------------------------------------
244+
// COMMAND: projects
245+
// -------------------------------------------------------------------------------------------------
246+
pub struct ProjectsCommand;
247+
pub struct ProjectsCommandArgs<'a> {
248+
filter: Option<ApiProjectFilter<'a>>
249+
}
250+
impl <'a> ProjectsCommand {
251+
fn inner_parse(&self, project_args: &'a ArgMatches) -> Result<ProjectsCommandArgs<'a>, &'a str> {
252+
let mut metadatas = HashMap::new();
253+
for mut metadata_values in project_args.values_of("metadata") {
254+
while let Some(k) = metadata_values.next() {
255+
let v = metadata_values.next().ok_or("metadata must be given as key value pairs")?;
256+
metadatas.insert(k, v);
257+
}
258+
}
259+
let name = project_args.value_of("name");
260+
if metadatas.is_empty() && name.is_none() {
261+
Ok(ProjectsCommandArgs { filter: None })
262+
} else {
263+
let metadatas_opt = if metadatas.is_empty() { None } else { Some(metadatas) };
264+
Ok(ProjectsCommandArgs {
265+
filter: Some(ApiProjectFilter { name, metadata: metadatas_opt })
266+
})
267+
}
268+
}
269+
}
270+
impl <'a> CommandInner<'a> for ProjectsCommand {
271+
type Args = ProjectsCommandArgs<'a>;
272+
273+
fn as_subcommand(&self) -> App<'static, 'static> {
274+
SubCommand::with_name("projects")
275+
.about("Get a list of projects")
276+
.arg(Arg::with_name("name")
277+
.short("n")
278+
.long("name")
279+
.value_name("PART_OF_NAME")
280+
.help("Provide criteria by case-insensitive name matching")
281+
.takes_value(true)
282+
.required(false)
283+
)
284+
.arg(Arg::with_name("metadata")
285+
.short("m")
286+
.long("metadata")
287+
.number_of_values(2)
288+
.value_names(&["FIELD", "VALUE"])
289+
.help("Provide criteria by project metadata")
290+
.multiple(true)
291+
.required(false)
292+
)
293+
}
294+
295+
fn parse(&self, matches: &'a ArgMatches) -> Option<Result<Self::Args, &'a str>> {
296+
if let Some(project_args) = matches.subcommand_matches("projects") {
297+
Some(self.inner_parse(project_args))
298+
} else {
299+
None
300+
}
301+
}
302+
303+
fn run(&self, client: &ApiClient, args: Self::Args) -> CommandResult {
304+
let ProjectsCommandArgs { filter } = args;
305+
306+
let plist = match filter {
307+
Some(ref filter) => client.query_projects(filter),
308+
None => client.get_projects(),
309+
};
310+
match plist {
311+
Err(e) => {
312+
eprintln!("Error loading projects: {:?}", e);
313+
Err(Exit(1))
314+
},
315+
Ok(projects) => {
316+
for project in projects {
317+
println!("{}", serde_json::to_string(&project).unwrap());
318+
}
319+
Ok(())
320+
}
321+
}
322+
}
323+
}

0 commit comments

Comments
 (0)