1use clap::{Arg, ArgAction, Command, CommandFactory, Parser, ValueEnum};
2use httpgenerator_core::OutputType;
3use std::fmt;
4
5const HELP_EXAMPLES: &str = "\
6Examples:
7 httpgenerator ./openapi.json
8 httpgenerator ./openapi.json --output ./
9 httpgenerator ./openapi.json --output-type onefile
10 httpgenerator https://petstore.swagger.io/v2/swagger.json
11 httpgenerator https://petstore3.swagger.io/api/v3/openapi.json --base-url https://petstore3.swagger.io
12 httpgenerator ./openapi.json --authorization-header Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9c
13 httpgenerator ./openapi.json --azure-scope [Some Application ID URI]/.default
14 httpgenerator ./openapi.json --generate-intellij-tests
15 httpgenerator ./openapi.json --custom-header X-Custom-Header: Value --custom-header X-Another-Header: AnotherValue";
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
18pub enum OutputTypeArg {
19 #[default]
20 #[value(name = "OneRequestPerFile")]
21 OneRequestPerFile,
22 #[value(name = "OneFile")]
23 OneFile,
24 #[value(name = "OneFilePerTag")]
25 OneFilePerTag,
26}
27
28impl OutputTypeArg {
29 pub const fn as_str(self) -> &'static str {
30 match self {
31 OutputTypeArg::OneRequestPerFile => "OneRequestPerFile",
32 OutputTypeArg::OneFile => "OneFile",
33 OutputTypeArg::OneFilePerTag => "OneFilePerTag",
34 }
35 }
36}
37
38impl fmt::Display for OutputTypeArg {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 formatter.write_str(self.as_str())
41 }
42}
43
44impl From<OutputTypeArg> for OutputType {
45 fn from(value: OutputTypeArg) -> Self {
46 match value {
47 OutputTypeArg::OneRequestPerFile => OutputType::OneRequestPerFile,
48 OutputTypeArg::OneFile => OutputType::OneFile,
49 OutputTypeArg::OneFilePerTag => OutputType::OneFilePerTag,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Parser, PartialEq, Eq)]
55#[command(
56 name = "httpgenerator",
57 bin_name = "httpgenerator",
58 version,
59 about = "Generate .http files from OpenAPI specifications",
60 disable_help_flag = true,
61 disable_version_flag = true
62)]
63pub struct CliArgs {
64 #[arg(
65 value_name = "URL or input file",
66 help = "URL or file path to OpenAPI Specification file"
67 )]
68 pub open_api_path: Option<String>,
69
70 #[arg(
71 short = 'o',
72 long = "output",
73 value_name = "OUTPUT",
74 default_value = "./",
75 help = "Output directory"
76 )]
77 pub output_folder: String,
78
79 #[arg(
80 long = "no-logging",
81 default_value_t = false,
82 help = "Don't log errors or collect telemetry"
83 )]
84 pub no_logging: bool,
85
86 #[arg(
87 long = "skip-validation",
88 default_value_t = false,
89 help = "Skip validation of OpenAPI Specification file"
90 )]
91 pub skip_validation: bool,
92
93 #[arg(
94 long = "authorization-header",
95 value_name = "HEADER",
96 help = "Authorization header to use for all requests"
97 )]
98 pub authorization_header: Option<String>,
99
100 #[arg(
101 long = "load-authorization-header-from-environment",
102 default_value_t = false,
103 help = "Load the authorization header from an environment variable or define it in the .http file. You can use --authorization-header-variable-name to specify the environment variable name."
104 )]
105 pub authorization_header_from_environment_variable: bool,
106
107 #[arg(
108 long = "authorization-header-variable-name",
109 value_name = "VARIABLE-NAME",
110 default_value = "authorization",
111 help = "Name of the environment variable to load the authorization header from"
112 )]
113 pub authorization_header_variable_name: String,
114
115 #[arg(
116 long = "content-type",
117 value_name = "CONTENT-TYPE",
118 default_value = "application/json",
119 help = "Default Content-Type header to use for all requests"
120 )]
121 pub content_type: String,
122
123 #[arg(
124 long = "base-url",
125 value_name = "BASE-URL",
126 help = "Default Base URL to use for all requests. Use this if the OpenAPI spec doesn't explicitly specify a server URL."
127 )]
128 pub base_url: Option<String>,
129
130 #[arg(
131 long = "output-type",
132 value_name = "OUTPUT-TYPE",
133 default_value_t = OutputTypeArg::OneRequestPerFile,
134 ignore_case = true,
135 help = "OneRequestPerFile generates one .http file per request. OneFile generates a single .http file for all requests. OneFilePerTag generates one .http file per first tag associated with each request."
136 )]
137 pub output_type: OutputTypeArg,
138
139 #[arg(
140 long = "azure-scope",
141 value_name = "SCOPE",
142 help = "Azure Entra ID Scope to use for retrieving Access Token for Authorization header"
143 )]
144 pub azure_scope: Option<String>,
145
146 #[arg(
147 long = "azure-tenant-id",
148 value_name = "TENANT-ID",
149 help = "Azure Entra ID Tenant ID to use for retrieving Access Token for Authorization header"
150 )]
151 pub azure_tenant_id: Option<String>,
152
153 #[arg(
154 long = "timeout",
155 value_name = "SECONDS",
156 default_value_t = 120,
157 help = "Timeout (in seconds) for writing files to disk"
158 )]
159 pub timeout: u64,
160
161 #[arg(
162 long = "generate-intellij-tests",
163 default_value_t = false,
164 help = "Generate IntelliJ tests that assert whether the response status code is 200"
165 )]
166 pub generate_intellij_tests: bool,
167
168 #[arg(
169 long = "custom-header",
170 value_name = "HEADER",
171 help = "Add custom HTTP headers to the generated request"
172 )]
173 pub custom_headers: Vec<String>,
174
175 #[arg(
176 long = "skip-headers",
177 default_value_t = false,
178 help = "Don't generate header parameters in the files"
179 )]
180 pub skip_headers: bool,
181}
182
183impl Default for CliArgs {
184 fn default() -> Self {
185 Self {
186 open_api_path: None,
187 output_folder: "./".to_string(),
188 no_logging: false,
189 skip_validation: false,
190 authorization_header: None,
191 authorization_header_from_environment_variable: false,
192 authorization_header_variable_name: "authorization".to_string(),
193 content_type: "application/json".to_string(),
194 base_url: None,
195 output_type: OutputTypeArg::OneRequestPerFile,
196 azure_scope: None,
197 azure_tenant_id: None,
198 timeout: 120,
199 generate_intellij_tests: false,
200 custom_headers: Vec::new(),
201 skip_headers: false,
202 }
203 }
204}
205
206pub fn build_command() -> Command {
207 CliArgs::command()
208 .override_usage("httpgenerator [URL or input file] [OPTIONS]")
209 .after_help(HELP_EXAMPLES)
210 .term_width(100)
211 .arg(
212 Arg::new("help")
213 .short('h')
214 .long("help")
215 .help("Print help information")
216 .action(ArgAction::Help),
217 )
218 .arg(
219 Arg::new("version")
220 .short('v')
221 .long("version")
222 .help("Print version information")
223 .action(ArgAction::Version),
224 )
225}
226
227#[cfg(test)]
228mod tests {
229 use clap::Parser;
230
231 use super::{CliArgs, OutputTypeArg, build_command};
232
233 #[test]
234 fn defaults_match_current_cli_surface() {
235 let args = CliArgs::parse_from(["httpgenerator", "./openapi.json"]);
236
237 assert_eq!(args.open_api_path.as_deref(), Some("./openapi.json"));
238 assert_eq!(args.output_folder, "./");
239 assert!(!args.no_logging);
240 assert!(!args.skip_validation);
241 assert_eq!(args.authorization_header, None);
242 assert!(!args.authorization_header_from_environment_variable);
243 assert_eq!(args.authorization_header_variable_name, "authorization");
244 assert_eq!(args.content_type, "application/json");
245 assert_eq!(args.base_url, None);
246 assert_eq!(args.output_type, OutputTypeArg::OneRequestPerFile);
247 assert_eq!(args.azure_scope, None);
248 assert_eq!(args.azure_tenant_id, None);
249 assert_eq!(args.timeout, 120);
250 assert!(!args.generate_intellij_tests);
251 assert!(args.custom_headers.is_empty());
252 assert!(!args.skip_headers);
253 }
254
255 #[test]
256 fn parses_repeated_headers_and_explicit_output_type() {
257 let args = CliArgs::parse_from([
258 "httpgenerator",
259 "./openapi.json",
260 "--output-type",
261 "OneFilePerTag",
262 "--custom-header",
263 "X-First: one",
264 "--custom-header",
265 "X-Second: two",
266 ]);
267
268 assert_eq!(args.output_type, OutputTypeArg::OneFilePerTag);
269 assert_eq!(
270 args.custom_headers,
271 vec!["X-First: one".to_string(), "X-Second: two".to_string()]
272 );
273 }
274
275 #[test]
276 fn help_command_uses_httpgenerator_identity_and_examples() {
277 let help = build_command().render_long_help().to_string();
278
279 assert!(help.contains("Usage: httpgenerator [URL or input file] [OPTIONS]"));
280 assert!(help.contains("Examples:"));
281 assert!(help.contains("httpgenerator ./openapi.json --output-type onefile"));
282 assert!(help.contains("httpgenerator https://petstore.swagger.io/v2/swagger.json"));
283 assert!(!help.contains("httpgenerator-cli"));
284 }
285}