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