1use crate::{
4 apis::ManagedApis,
5 cmd::{
6 check::check_impl, debug::debug_impl, generate::generate_impl,
7 list::list_impl,
8 },
9 environment::{BlessedSource, Environment, GeneratedSource, ResolvedEnv},
10 git::GitRevision,
11 output::OutputOpts,
12};
13use anyhow::Result;
14use camino::Utf8PathBuf;
15use clap::{Args, Parser, Subcommand};
16use std::process::ExitCode;
17
18#[derive(Debug, Parser)]
22pub struct App {
23 #[clap(flatten)]
24 output_opts: OutputOpts,
25
26 #[clap(subcommand)]
27 command: Command,
28}
29
30impl App {
31 pub fn exec(self, env: &Environment, apis: &ManagedApis) -> ExitCode {
34 let result = match self.command {
35 Command::Debug(args) => args.exec(env, apis, &self.output_opts),
36 Command::List(args) => args.exec(apis, &self.output_opts),
37 Command::Generate(args) => args.exec(env, apis, &self.output_opts),
38 Command::Check(args) => args.exec(env, apis, &self.output_opts),
39 };
40
41 match result {
42 Ok(exit_code) => exit_code,
43 Err(error) => {
44 eprintln!("failure: {:#}", error);
45 ExitCode::FAILURE
46 }
47 }
48 }
49}
50
51#[derive(Debug, Subcommand)]
52pub enum Command {
53 Debug(DebugArgs),
55
56 List(ListArgs),
61
62 Generate(GenerateArgs),
64
65 Check(CheckArgs),
67}
68
69#[derive(Debug, Args)]
70pub struct BlessedSourceArgs {
71 #[clap(
85 long,
86 env("OPENAPI_MGR_BLESSED_FROM_GIT"),
87 value_name("REVISION[:PATH]")
88 )]
89 pub blessed_from_git: Option<String>,
90
91 #[clap(
96 long,
97 conflicts_with("blessed_from_git"),
98 env("OPENAPI_MGR_BLESSED_FROM_DIR"),
99 value_name("DIRECTORY")
100 )]
101 pub blessed_from_dir: Option<Utf8PathBuf>,
102}
103
104impl BlessedSourceArgs {
105 pub(crate) fn to_blessed_source(
106 &self,
107 env: &ResolvedEnv,
108 ) -> Result<BlessedSource, anyhow::Error> {
109 assert!(
110 self.blessed_from_dir.is_none() || self.blessed_from_git.is_none()
111 );
112
113 if let Some(local_directory) = &self.blessed_from_dir {
114 return Ok(BlessedSource::Directory {
115 local_directory: local_directory.clone(),
116 });
117 }
118
119 let (revision_str, maybe_directory) = match &self.blessed_from_git {
120 None => (env.default_git_branch.as_str(), None),
121 Some(arg) => match arg.split_once(":") {
122 Some((r, d)) => (r, Some(d)),
123 None => (arg.as_str(), None),
124 },
125 };
126 let revision = GitRevision::from(String::from(revision_str));
127 let directory = Utf8PathBuf::from(maybe_directory.map_or(
128 env.openapi_rel_dir(),
130 |d| d.as_ref(),
131 ));
132 Ok(BlessedSource::GitRevisionMergeBase { revision, directory })
133 }
134}
135
136#[derive(Debug, Args)]
137pub struct GeneratedSourceArgs {
138 #[clap(long, value_name("DIRECTORY"))]
141 pub generated_from_dir: Option<Utf8PathBuf>,
142}
143
144impl From<GeneratedSourceArgs> for GeneratedSource {
145 fn from(value: GeneratedSourceArgs) -> Self {
146 match value.generated_from_dir {
147 Some(local_directory) => {
148 GeneratedSource::Directory { local_directory }
149 }
150 None => GeneratedSource::Generated,
151 }
152 }
153}
154
155#[derive(Debug, Args)]
156pub struct LocalSourceArgs {
157 #[clap(long, env("OPENAPI_MGR_DIR"), value_name("DIRECTORY"))]
159 dir: Option<Utf8PathBuf>,
160}
161
162#[derive(Debug, Args)]
163pub struct DebugArgs {
164 #[clap(flatten)]
165 local: LocalSourceArgs,
166 #[clap(flatten)]
167 blessed: BlessedSourceArgs,
168 #[clap(flatten)]
169 generated: GeneratedSourceArgs,
170}
171
172impl DebugArgs {
173 fn exec(
174 self,
175 env: &Environment,
176 apis: &ManagedApis,
177 output: &OutputOpts,
178 ) -> anyhow::Result<ExitCode> {
179 let env = env.resolve(self.local.dir)?;
180 let blessed_source = self.blessed.to_blessed_source(&env)?;
181 let generated_source = GeneratedSource::from(self.generated);
182 debug_impl(apis, &env, &blessed_source, &generated_source, output)?;
183 Ok(ExitCode::SUCCESS)
184 }
185}
186
187#[derive(Debug, Args)]
188pub struct ListArgs {
189 #[clap(long, short)]
191 verbose: bool,
192}
193
194impl ListArgs {
195 fn exec(
196 self,
197 apis: &ManagedApis,
198 output: &OutputOpts,
199 ) -> anyhow::Result<ExitCode> {
200 list_impl(apis, self.verbose, output)?;
201 Ok(ExitCode::SUCCESS)
202 }
203}
204
205#[derive(Debug, Args)]
206pub struct GenerateArgs {
207 #[clap(flatten)]
208 local: LocalSourceArgs,
209 #[clap(flatten)]
210 blessed: BlessedSourceArgs,
211 #[clap(flatten)]
212 generated: GeneratedSourceArgs,
213}
214
215impl GenerateArgs {
216 fn exec(
217 self,
218 env: &Environment,
219 apis: &ManagedApis,
220 output: &OutputOpts,
221 ) -> anyhow::Result<ExitCode> {
222 let env = env.resolve(self.local.dir)?;
223 let blessed_source = self.blessed.to_blessed_source(&env)?;
224 let generated_source = GeneratedSource::from(self.generated);
225 Ok(generate_impl(
226 apis,
227 &env,
228 &blessed_source,
229 &generated_source,
230 output,
231 )?
232 .to_exit_code())
233 }
234}
235
236#[derive(Debug, Args)]
237pub struct CheckArgs {
238 #[clap(flatten)]
239 local: LocalSourceArgs,
240 #[clap(flatten)]
241 blessed: BlessedSourceArgs,
242 #[clap(flatten)]
243 generated: GeneratedSourceArgs,
244}
245
246impl CheckArgs {
247 fn exec(
248 self,
249 env: &Environment,
250 apis: &ManagedApis,
251 output: &OutputOpts,
252 ) -> anyhow::Result<ExitCode> {
253 let env = env.resolve(self.local.dir)?;
254 let blessed_source = self.blessed.to_blessed_source(&env)?;
255 let generated_source = GeneratedSource::from(self.generated);
256 Ok(check_impl(apis, &env, &blessed_source, &generated_source, output)?
257 .to_exit_code())
258 }
259}
260
261pub const NEEDS_UPDATE_EXIT_CODE: u8 = 4;
266
267pub const FAILURE_EXIT_CODE: u8 = 100;
272
273#[cfg(test)]
274mod test {
275 use super::{
276 App, BlessedSourceArgs, CheckArgs, Command, GeneratedSourceArgs,
277 LocalSourceArgs,
278 };
279 use crate::environment::{BlessedSource, Environment, GeneratedSource};
280 use assert_matches::assert_matches;
281 use camino::{Utf8Path, Utf8PathBuf};
282 use clap::Parser;
283
284 #[test]
285 fn test_arg_parsing() {
286 let app = App::parse_from(["dummy", "check"]);
288 assert_matches!(
289 app.command,
290 Command::Check(CheckArgs {
291 local: LocalSourceArgs { dir: None },
292 blessed: BlessedSourceArgs {
293 blessed_from_git: None,
294 blessed_from_dir: None
295 },
296 generated: GeneratedSourceArgs { generated_from_dir: None },
297 })
298 );
299
300 let app = App::parse_from(["dummy", "check", "--dir", "foo"]);
302 assert_matches!(app.command, Command::Check(CheckArgs {
303 local: LocalSourceArgs { dir: Some(local_dir) },
304 blessed:
305 BlessedSourceArgs { blessed_from_git: None, blessed_from_dir: None },
306 generated: GeneratedSourceArgs { generated_from_dir: None },
307 }) if local_dir == "foo");
308
309 let app = App::parse_from([
311 "dummy",
312 "check",
313 "--dir",
314 "foo",
315 "--generated-from-dir",
316 "bar",
317 ]);
318 assert_matches!(app.command, Command::Check(CheckArgs {
319 local: LocalSourceArgs { dir: Some(local_dir) },
320 blessed:
321 BlessedSourceArgs { blessed_from_git: None, blessed_from_dir: None },
322 generated: GeneratedSourceArgs { generated_from_dir: Some(generated_dir) },
323 }) if local_dir == "foo" && generated_dir == "bar");
324
325 let app = App::parse_from([
327 "dummy",
328 "check",
329 "--dir",
330 "foo",
331 "--generated-from-dir",
332 "bar",
333 "--blessed-from-dir",
334 "baz",
335 ]);
336 assert_matches!(app.command, Command::Check(CheckArgs {
337 local: LocalSourceArgs { dir: Some(local_dir) },
338 blessed:
339 BlessedSourceArgs { blessed_from_git: None, blessed_from_dir: Some(blessed_dir) },
340 generated: GeneratedSourceArgs { generated_from_dir: Some(generated_dir) },
341 }) if local_dir == "foo" && generated_dir == "bar" && blessed_dir == "baz");
342
343 let app = App::parse_from([
345 "dummy",
346 "check",
347 "--blessed-from-git",
348 "some/other/upstream",
349 ]);
350 assert_matches!(app.command, Command::Check(CheckArgs {
351 local: LocalSourceArgs { dir: None },
352 blessed:
353 BlessedSourceArgs { blessed_from_git: Some(git), blessed_from_dir: None },
354 generated: GeneratedSourceArgs { generated_from_dir: None },
355 }) if git == "some/other/upstream");
356
357 let error = App::try_parse_from([
359 "dummy",
360 "check",
361 "--blessed-from-git",
362 "git_revision",
363 "--blessed-from-dir",
364 "dir",
365 ])
366 .unwrap_err();
367 assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict);
368 assert!(error.to_string().contains(
369 "error: the argument '--blessed-from-git <REVISION[:PATH]>' cannot \
370 be used with '--blessed-from-dir <DIRECTORY>"
371 ));
372 }
373
374 #[test]
376 fn test_local_args() {
377 #[cfg(unix)]
378 const ABS_DIR: &str = "/tmp";
379 #[cfg(windows)]
380 const ABS_DIR: &str = "C:\\tmp";
381
382 {
383 let env = Environment::new(
384 "cargo openapi".to_owned(),
385 Utf8PathBuf::from(ABS_DIR),
386 Utf8PathBuf::from("foo"),
387 )
388 .expect("loading environment");
389 let env = env.resolve(None).expect("resolving environment");
390 assert_eq!(
391 env.openapi_abs_dir(),
392 Utf8Path::new(ABS_DIR).join("foo")
393 );
394 }
395
396 {
397 let error = Environment::new(
398 "cargo openapi".to_owned(),
399 Utf8PathBuf::from(ABS_DIR),
400 Utf8PathBuf::from(ABS_DIR),
401 )
402 .unwrap_err();
403 assert_eq!(
404 error.to_string(),
405 format!(
406 "default_openapi_dir must be a relative path with \
407 normal components, found: {}",
408 ABS_DIR
409 )
410 );
411 }
412
413 {
414 let current_dir =
415 Utf8PathBuf::try_from(std::env::current_dir().unwrap())
416 .unwrap();
417 let env = Environment::new(
418 "cargo openapi".to_owned(),
419 current_dir.clone(),
420 Utf8PathBuf::from("foo"),
421 )
422 .expect("loading environment");
423 let env = env
424 .resolve(Some(Utf8PathBuf::from("bar")))
425 .expect("resolving environment");
426 assert_eq!(env.openapi_abs_dir(), current_dir.join("bar"));
427 }
428 }
429
430 #[test]
432 fn test_generated_args() {
433 let source = GeneratedSource::from(GeneratedSourceArgs {
434 generated_from_dir: None,
435 });
436 assert_matches!(source, GeneratedSource::Generated);
437
438 let source = GeneratedSource::from(GeneratedSourceArgs {
439 generated_from_dir: Some(Utf8PathBuf::from("/tmp")),
440 });
441 assert_matches!(
442 source,
443 GeneratedSource::Directory { local_directory }
444 if local_directory == "/tmp"
445 );
446 }
447
448 #[test]
450 fn test_blessed_args() {
451 #[cfg(unix)]
452 const ABS_DIR: &str = "/tmp";
453 #[cfg(windows)]
454 const ABS_DIR: &str = "C:\\tmp";
455
456 let env = Environment::new("cargo openapi", ABS_DIR, "foo-openapi")
457 .unwrap()
458 .with_default_git_branch("upstream/dev".to_owned());
459 let env = env.resolve(None).unwrap();
460
461 let source = BlessedSourceArgs {
462 blessed_from_git: None,
463 blessed_from_dir: None,
464 }
465 .to_blessed_source(&env)
466 .unwrap();
467 assert_matches!(
468 source,
469 BlessedSource::GitRevisionMergeBase { revision, directory }
470 if *revision == "upstream/dev" && directory == "foo-openapi"
471 );
472
473 let source = BlessedSourceArgs {
475 blessed_from_git: Some(String::from("my/other/main")),
476 blessed_from_dir: None,
477 }
478 .to_blessed_source(&env)
479 .unwrap();
480 assert_matches!(
481 source,
482 BlessedSource::GitRevisionMergeBase { revision, directory}
483 if *revision == "my/other/main" && directory == "foo-openapi"
484 );
485
486 let source = BlessedSourceArgs {
488 blessed_from_git: Some(String::from(
489 "my/other/main:other_openapi/bar",
490 )),
491 blessed_from_dir: None,
492 }
493 .to_blessed_source(&env)
494 .unwrap();
495 assert_matches!(
496 source,
497 BlessedSource::GitRevisionMergeBase { revision, directory}
498 if *revision == "my/other/main" &&
499 directory == "other_openapi/bar"
500 );
501
502 let source = BlessedSourceArgs {
504 blessed_from_git: None,
505 blessed_from_dir: Some(Utf8PathBuf::from("/tmp")),
506 }
507 .to_blessed_source(&env)
508 .unwrap();
509 assert_matches!(
510 source,
511 BlessedSource::Directory { local_directory }
512 if local_directory == "/tmp"
513 );
514 }
515}