Skip to content

Commit 0019c0e

Browse files
authored
fix(cli): tweak required argument names and styles + validate CLI documentation drift (#45)
1 parent ae0126b commit 0019c0e

15 files changed

Lines changed: 218 additions & 69 deletions

File tree

.github/copilot-instructions.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@
1111

1212
- Prefer Conventional Commits: `feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`.
1313

14+
## CLI usage examples
15+
16+
- In every `oapi-codegen` command example (README, docs, `--help` text, the
17+
Makefile, example folders), always put options first and the positional
18+
`<SPEC_FILE>` last, matching the generated `--help` usage line
19+
(e.g. `oapi-codegen --config-file cfg.yaml --output-file out.rs spec.yaml`).
20+
21+
## CLI definition is the single source of truth
22+
23+
- The clap CLI lives in `crates/oapi-codegen/src/cli.rs`. After changing any
24+
flag, argument, help text, or the `EXAMPLES` block, run `make update-docs` to
25+
regenerate `docs/cli.md` (it is generated — never edit it by hand). CI's
26+
`make verify-generated` fails on drift.
27+
1428
# Rust Coding Conventions and Best Practices
1529

1630
Follow idiomatic Rust practices and community standards when writing Rust code.

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ node_modules/
44

55
# Lockfiles are not formatted by Prettier
66
Cargo.lock
7+
8+
# Generated from the clap CLI definition by `make update-docs`
9+
docs/cli.md

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,31 @@ test-e2e: ## Run the Docker end-to-end test (generated server + client over HTTP
5353
update-generated: ## Refresh generated files from the coverage fixtures
5454
UPDATE_GENERATED=1 cargo test -p oapi-codegen --test coverage
5555

56+
.PHONY: update-docs
57+
update-docs: ## Refresh docs/cli.md from the clap CLI definition
58+
UPDATE_DOCS=1 cargo test -p oapi-codegen --test cli_docs
59+
5660
.PHONY: generate-example
5761
generate-example: ## Regenerate the composed bookstore example from its OpenAPI spec
5862
cd examples/bookstore && \
59-
cargo run -q -p oapi-codegen -- schemas/common.yaml --config oapi-codegen-common.yaml && \
60-
cargo run -q -p oapi-codegen -- schemas/catalog.yaml --config oapi-codegen-catalog.yaml && \
61-
cargo run -q -p oapi-codegen -- openapi.yaml --config oapi-codegen-server.yaml && \
62-
cargo run -q -p oapi-codegen -- openapi.yaml --config oapi-codegen-client.yaml
63+
cargo run -q -p oapi-codegen -- --config-file oapi-codegen-common.yaml schemas/common.yaml && \
64+
cargo run -q -p oapi-codegen -- --config-file oapi-codegen-catalog.yaml schemas/catalog.yaml && \
65+
cargo run -q -p oapi-codegen -- --config-file oapi-codegen-server.yaml openapi.yaml && \
66+
cargo run -q -p oapi-codegen -- --config-file oapi-codegen-client.yaml openapi.yaml
6367

6468
.PHONY: verify-generated
65-
verify-generated: ## Regenerate all generated code and fail if it drifts from what is committed
69+
verify-generated: ## Regenerate all generated files and fail if they drift from what is committed
6670
$(MAKE) generate-example
6771
$(MAKE) update-generated
68-
@if [ -n "$$(git status --porcelain -- examples/bookstore/generated crates/oapi-codegen/tests/generated)" ]; then \
69-
echo "ERROR: generated code is out of date."; \
70-
echo "Run 'make generate-example' and 'make update-generated', then commit the result."; \
71-
git status --porcelain -- examples/bookstore/generated crates/oapi-codegen/tests/generated; \
72-
git --no-pager diff -- examples/bookstore/generated crates/oapi-codegen/tests/generated; \
72+
$(MAKE) update-docs
73+
@if [ -n "$$(git status --porcelain -- examples/bookstore/generated crates/oapi-codegen/tests/generated docs/cli.md)" ]; then \
74+
echo "ERROR: generated files are out of date."; \
75+
echo "Run 'make generate-example', 'make update-generated' and 'make update-docs', then commit the result."; \
76+
git status --porcelain -- examples/bookstore/generated crates/oapi-codegen/tests/generated docs/cli.md; \
77+
git --no-pager diff -- examples/bookstore/generated crates/oapi-codegen/tests/generated docs/cli.md; \
7378
exit 1; \
7479
fi
75-
@echo "Generated code is up to date."
80+
@echo "Generated files are up to date."
7681

7782
.PHONY: docs
7883
docs: ## Generate and open Rust documentation

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Generate idiomatic Rust APIs and clients from OpenAPI 3 specifications, inspired
77

88
```sh
99
cargo install oapi-codegen
10-
oapi-codegen --config cfg.yaml --output src/api.rs openapi.yaml
10+
oapi-codegen --config-file cfg.yaml --output-file src/api.rs openapi.yaml
1111
```
1212

1313
See [docs/](docs/) for installation, configuration, and extensions.
@@ -30,7 +30,7 @@ differ:
3030
- Blocking `reqwest` client — no async runtime forced on consumers.
3131
- Targets OpenAPI 3.0.
3232
- Reuses your `oapi-codegen` YAML config — unknown keys are ignored.
33-
- Explicit over implicit: `--config` and an output path are required, and
33+
- Explicit over implicit: `--config-file` and an output path are required, and
3434
empty generation fails loudly (Go defaults these and prints to stdout).
3535
- `x-rust-*` vendor extensions; `x-go-*` keys ignored.
3636

crates/oapi-codegen/Cargo.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,10 @@ serde_yaml = "0.9.34"
2828
syn = { version = "2.0.118", features = ["full"] }
2929

3030
[dev-dependencies]
31-
# chrono/uuid only type-check the generated model outputs (which reference
32-
# them); axum type-checks the generated server output; reqwest type-checks the
33-
# generated client output; openapiv3 backs the coverage-matrix anchor in
34-
# tests/coverage.rs. None are needed by the generator itself.
3531
axum = { version = "0.8.9", features = ["multipart"] }
3632
axum-extra = { version = "0.12.6", features = ["query", "cookie"] }
3733
chrono = { version = "0.4.45", features = ["serde"] }
34+
clap-markdown = "0.1.5"
3835
openapiv3 = "2.2.0"
3936
percent-encoding = "2.3.2"
4037
reqwest = { version = "0.12.28", default-features = false, features = ["blocking", "json", "multipart"] }

crates/oapi-codegen/src/cli.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! The command-line interface definition.
2+
//!
3+
//! The [`Cli`] struct is the single source of truth for the CLI: the binary
4+
//! parses it, and the `cli_docs` integration test renders it to Markdown so
5+
//! `docs/cli.md` never drifts from the real interface.
6+
7+
use std::path::PathBuf;
8+
9+
use clap::Parser;
10+
11+
/// Extra `--help` text with worked examples.
12+
pub const EXAMPLES: &str = "\
13+
Examples:
14+
# Write generated code, selecting artifacts in the config file:
15+
oapi-codegen --config-file oapi-codegen.yaml --output-file src/api.rs api.yaml
16+
17+
# The output path may instead come from the config's `output:` key:
18+
oapi-codegen --config-file oapi-codegen.yaml api.yaml
19+
20+
A config file is required, must enable at least one artifact, and an output
21+
path must be given via --output-file or the config's `output:` key:
22+
# oapi-codegen.yaml
23+
output: src/api.rs
24+
generate:
25+
models: true
26+
std-http-server: true
27+
client: true";
28+
29+
/// Generate idiomatic Rust from an OpenAPI 3 specification.
30+
#[derive(Debug, Parser)]
31+
#[command(name = "oapi-codegen", version, about, after_long_help = EXAMPLES)]
32+
pub struct Cli {
33+
/// Path to the OpenAPI 3 specification (YAML or JSON).
34+
pub spec_file: PathBuf,
35+
36+
/// Path to an `oapi-codegen` YAML config file (required).
37+
#[arg(short = 'c', long)]
38+
pub config_file: PathBuf,
39+
40+
/// Output file path (overrides the config `output:`). Required unless the
41+
/// config sets `output:`.
42+
#[arg(short = 'o', long)]
43+
pub output_file: Option<PathBuf>,
44+
}

crates/oapi-codegen/src/console.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub fn report_error(err: &Error) {
5656
pub fn report_no_output() {
5757
eprintln!("{} no output destination was given.", "error:".red().bold());
5858
eprintln!(
59-
" {} pass `--output <file>` on the command line, or set `output:` in your config.",
59+
" {} pass `--output-file <file>` on the command line, or set `output:` in your config.",
6060
"hint:".cyan().bold()
6161
);
6262
}

crates/oapi-codegen/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! generator, its operations ([`lower::paths`] → [`ir`]), then emit formatted Rust
66
//! source ([`emit`]). [`Config`] mirrors `oapi-codegen`'s YAML configuration.
77
8+
pub mod cli;
89
pub mod config;
910
pub mod emit;
1011
pub mod error;

crates/oapi-codegen/src/main.rs

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,11 @@ use clap::Parser;
1414
use oapi_codegen::Config;
1515
use oapi_codegen::Error;
1616
use oapi_codegen::Result;
17+
use oapi_codegen::cli::Cli;
1718
use oapi_codegen::config::Generate;
1819

1920
use crate::console::SpecStats;
2021

21-
/// Extra `--help` text with worked examples.
22-
const EXAMPLES: &str = "\
23-
Examples:
24-
# Write generated code, selecting artifacts in the config file:
25-
oapi-codegen api.yaml --config oapi-codegen.yaml --output src/api.rs
26-
27-
# The output path may instead come from the config's `output:` key:
28-
oapi-codegen api.yaml --config oapi-codegen.yaml
29-
30-
A config file is required, must enable at least one artifact, and an output
31-
path must be given via --output or the config's `output:` key:
32-
# oapi-codegen.yaml
33-
output: src/api.rs
34-
generate:
35-
models: true
36-
std-http-server: true
37-
client: true";
38-
39-
/// Generate idiomatic Rust from an OpenAPI 3 specification.
40-
#[derive(Debug, Parser)]
41-
#[command(name = "oapi-codegen", version, about, after_long_help = EXAMPLES)]
42-
struct Cli {
43-
/// Path to the OpenAPI 3 specification (YAML or JSON).
44-
spec: PathBuf,
45-
46-
/// Path to an `oapi-codegen` YAML config file (required).
47-
#[arg(short, long)]
48-
config: PathBuf,
49-
50-
/// Output file path (overrides the config `output`). Required unless the
51-
/// config sets `output`.
52-
#[arg(short, long)]
53-
output: Option<PathBuf>,
54-
}
55-
5622
/// A failure that has enough context to be reported to the user.
5723
enum CliFailure {
5824
/// A generator/loader error occurred.
@@ -116,7 +82,7 @@ fn main() -> ExitCode {
11682

11783
/// Run the generator according to the parsed CLI arguments.
11884
fn run(cli: &Cli) -> std::result::Result<(), CliFailure> {
119-
let config = Config::load(&cli.config)?;
85+
let config = Config::load(&cli.config_file)?;
12086

12187
if config.generate.embedded_spec {
12288
return Err(CliFailure::Generator(Error::Unimplemented("embedded-spec".to_owned())));
@@ -125,20 +91,20 @@ fn run(cli: &Cli) -> std::result::Result<(), CliFailure> {
12591
let generate = &config.generate;
12692
if !generate.models && !generate.std_http_server && !generate.client && !generate.server_urls {
12793
return Err(CliFailure::NoArtifacts {
128-
config: cli.config.clone(),
94+
config: cli.config_file.clone(),
12995
});
13096
}
13197

132-
let output = cli.output.clone().or_else(|| {
98+
let output = cli.output_file.clone().or_else(|| {
13399
return config.output.clone();
134100
});
135101
let output = output.ok_or(CliFailure::NoOutput)?;
136102

137-
let code = oapi_codegen::generate(&cli.spec, &config)?;
103+
let code = oapi_codegen::generate(&cli.spec_file, &config)?;
138104
if console::is_effectively_empty(&code) {
139-
let stats = spec_stats(&cli.spec, &config)?;
105+
let stats = spec_stats(&cli.spec_file, &config)?;
140106
return Err(CliFailure::EmptyOutput {
141-
spec: cli.spec.clone(),
107+
spec: cli.spec_file.clone(),
142108
stats,
143109
generate: config.generate.clone(),
144110
});

0 commit comments

Comments
 (0)