1use crate::cache::models::{CachedCommand, CachedParameter, CachedSpec};
8use crate::cli::Cli;
9use crate::config::models::GlobalConfig;
10use crate::constants;
11use crate::duration::parse_duration;
12use crate::engine::executor::RetryContext;
13use crate::error::Error;
14use crate::invocation::{ExecutionContext, OperationCall};
15use crate::response_cache::CacheConfig;
16use crate::utils::to_kebab_case;
17use clap::ArgMatches;
18use std::collections::HashMap;
19use std::io::Read as _;
20use std::path::PathBuf;
21use std::time::Duration;
22
23pub fn matches_to_operation_call(
34 spec: &CachedSpec,
35 matches: &ArgMatches,
36) -> Result<OperationCall, Error> {
37 let (operation, current_matches) = find_operation_from_matches(spec, matches)?;
38
39 let mut path_params = HashMap::new();
41 let mut query_params = HashMap::new();
42 let mut header_params = HashMap::new();
43
44 for param in &operation.parameters {
45 extract_param(
46 param,
47 current_matches,
48 &mut path_params,
49 &mut query_params,
50 &mut header_params,
51 );
52 }
53
54 let body = extract_body(operation.request_body.is_some(), current_matches)?;
56
57 let custom_headers = current_matches
59 .try_get_many::<String>("header")
60 .ok()
61 .flatten()
62 .map(|values| values.cloned().collect())
63 .unwrap_or_default();
64
65 Ok(OperationCall {
66 operation_id: operation.operation_id.clone(),
67 path_params,
68 query_params,
69 header_params,
70 body,
71 custom_headers,
72 })
73}
74
75pub fn matches_to_operation_id(spec: &CachedSpec, matches: &ArgMatches) -> Result<String, Error> {
86 let (operation, _) = find_operation_from_matches(spec, matches)?;
87 Ok(operation.operation_id.clone())
88}
89
90fn find_operation_from_matches<'a>(
92 spec: &'a CachedSpec,
93 matches: &'a ArgMatches,
94) -> Result<(&'a CachedCommand, &'a ArgMatches), Error> {
95 let mut current_matches = matches;
96 let mut subcommand_path = Vec::new();
97
98 while let Some((name, sub_matches)) = current_matches.subcommand() {
99 subcommand_path.push(name.to_string());
100 current_matches = sub_matches;
101 }
102
103 let operation_name = subcommand_path.last().ok_or_else(|| {
104 let name = "unknown".to_string();
105 let suggestions = crate::suggestions::suggest_similar_operations(spec, &name);
106 Error::operation_not_found_with_suggestions(name, &suggestions)
107 })?;
108
109 let group_name = subcommand_path
111 .len()
112 .checked_sub(2)
113 .and_then(|idx| subcommand_path.get(idx));
114
115 let operation = spec
116 .commands
117 .iter()
118 .find(|cmd| matches_effective_command_path(cmd, group_name, operation_name))
119 .or_else(|| {
123 spec.commands
124 .iter()
125 .find(|cmd| matches_effective_command_path(cmd, None, operation_name))
126 })
127 .ok_or_else(|| {
128 let suggestions = crate::suggestions::suggest_similar_operations(spec, operation_name);
129 Error::operation_not_found_with_suggestions(operation_name.clone(), &suggestions)
130 })?;
131
132 Ok((operation, current_matches))
133}
134
135fn matches_effective_command_path(
137 command: &CachedCommand,
138 group_name: Option<&String>,
139 operation_name: &str,
140) -> bool {
141 let operation_matches = effective_operation_name(command) == operation_name
142 || command
143 .aliases
144 .iter()
145 .any(|alias| to_kebab_case(alias) == operation_name);
146
147 if !operation_matches {
148 return false;
149 }
150
151 group_name.is_none_or(|group| effective_group_name(command) == *group)
152}
153
154fn effective_group_name(command: &CachedCommand) -> String {
156 command.display_group.as_ref().map_or_else(
157 || {
158 if command.name.is_empty() {
159 constants::DEFAULT_GROUP.to_string()
160 } else {
161 to_kebab_case(&command.name)
162 }
163 },
164 |group| to_kebab_case(group),
165 )
166}
167
168fn effective_operation_name(command: &CachedCommand) -> String {
170 command.display_name.as_ref().map_or_else(
171 || {
172 if command.operation_id.is_empty() {
173 command.method.to_lowercase()
174 } else {
175 to_kebab_case(&command.operation_id)
176 }
177 },
178 |name| to_kebab_case(name),
179 )
180}
181
182fn extract_param(
185 param: &CachedParameter,
186 matches: &ArgMatches,
187 path_params: &mut HashMap<String, String>,
188 query_params: &mut HashMap<String, String>,
189 header_params: &mut HashMap<String, String>,
190) {
191 let target = match param.location.as_str() {
192 "path" => path_params,
193 "query" => query_params,
194 "header" => header_params,
195 _ => return,
196 };
197
198 let is_boolean = param.schema_type.as_ref().is_some_and(|t| t == "boolean");
199
200 if !is_boolean {
201 let Some(value) = matches.try_get_one::<String>(¶m.name).ok().flatten() else {
204 return;
205 };
206 target.insert(param.name.clone(), value.clone());
207 return;
208 }
209
210 let flag_set = matches.get_flag(¶m.name);
213 if flag_set || param.location == "path" {
214 target.insert(param.name.clone(), flag_set.to_string());
215 }
216}
217
218fn extract_body(has_request_body: bool, matches: &ArgMatches) -> Result<Option<String>, Error> {
224 if !has_request_body {
225 return Ok(None);
226 }
227
228 if let Ok(Some(path)) = matches.try_get_one::<String>("body-file") {
231 let raw = if path == "-" {
232 let mut buf = String::new();
233 std::io::stdin()
234 .read_to_string(&mut buf)
235 .map_err(|e| Error::io_error(format!("Failed to read body from stdin: {e}")))?;
236 buf
237 } else {
238 std::fs::read_to_string(path)
239 .map_err(|e| Error::io_error(format!("Failed to read body file '{path}': {e}")))?
240 };
241 let content = raw.trim_end();
244 let _: serde_json::Value =
245 serde_json::from_str(content).map_err(|e| Error::invalid_json_body(e.to_string()))?;
246 return Ok(Some(content.to_owned()));
247 }
248
249 matches
250 .get_one::<String>("body")
251 .map(|body_value| {
252 let _: serde_json::Value = serde_json::from_str(body_value)
254 .map_err(|e| Error::invalid_json_body(e.to_string()))?;
255 Ok(body_value.clone())
256 })
257 .transpose()
258}
259
260#[must_use]
262pub fn extract_server_var_args(matches: &ArgMatches) -> Vec<String> {
263 matches
264 .try_get_many::<String>("server-var")
265 .ok()
266 .flatten()
267 .map(|values| values.cloned().collect())
268 .unwrap_or_default()
269}
270
271#[must_use]
273pub fn has_show_examples_flag(matches: &ArgMatches) -> bool {
274 let mut current = matches;
276 while let Some((_name, sub)) = current.subcommand() {
277 current = sub;
278 }
279
280 current.try_contains_id("show-examples").unwrap_or(false) && current.get_flag("show-examples")
281}
282
283#[allow(clippy::cast_possible_truncation)]
289pub fn cli_to_execution_context(
290 cli: &Cli,
291 global_config: Option<GlobalConfig>,
292) -> Result<ExecutionContext, Error> {
293 let config_dir = if let Ok(dir) = std::env::var(crate::constants::ENV_APERTURE_CONFIG_DIR) {
294 PathBuf::from(dir)
295 } else {
296 crate::config::manager::get_config_dir()?
297 };
298
299 let cache_config = if cli.no_cache {
301 None
302 } else {
303 Some(CacheConfig {
304 cache_dir: config_dir
305 .join(crate::constants::DIR_CACHE)
306 .join(crate::constants::DIR_RESPONSES),
307 default_ttl: Duration::from_secs(cli.cache_ttl.unwrap_or(300)),
308 max_entries: 1000,
309 enabled: cli.cache || cli.cache_ttl.is_some(),
310 allow_authenticated: false,
311 })
312 };
313
314 let retry_context = build_retry_context(cli, global_config.as_ref())?;
316
317 Ok(ExecutionContext {
318 dry_run: cli.dry_run,
319 idempotency_key: cli.idempotency_key.clone(),
320 cache_config,
321 retry_context,
322 base_url: None, global_config,
324 server_var_args: Vec::new(), auto_paginate: cli.auto_paginate,
326 })
327}
328
329#[allow(clippy::cast_possible_truncation)]
333fn build_retry_context(
334 cli: &Cli,
335 global_config: Option<&GlobalConfig>,
336) -> Result<Option<RetryContext>, Error> {
337 let defaults = global_config.map(|c| &c.retry_defaults);
338
339 let max_attempts = cli
340 .retry
341 .or_else(|| defaults.map(|d| d.max_attempts))
342 .unwrap_or(0);
343
344 if max_attempts == 0 {
345 return Ok(None);
346 }
347
348 let initial_delay_ms = if let Some(ref delay_str) = cli.retry_delay {
350 parse_duration(delay_str)?.as_millis() as u64
351 } else {
352 defaults.map_or(500, |d| d.initial_delay_ms)
353 };
354
355 let max_delay_ms = if let Some(ref delay_str) = cli.retry_max_delay {
356 parse_duration(delay_str)?.as_millis() as u64
357 } else {
358 defaults.map_or(30_000, |d| d.max_delay_ms)
359 };
360
361 let has_idempotency_key = cli.idempotency_key.is_some();
362
363 Ok(Some(RetryContext {
364 max_attempts,
365 initial_delay_ms,
366 max_delay_ms,
367 force_retry: cli.force_retry,
368 method: None, has_idempotency_key,
370 }))
371}