Skip to main content

httpgenerator_cli/
args.rs

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}