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::path::PathBuf;
20use std::time::Duration;
21
22pub fn matches_to_operation_call(
33 spec: &CachedSpec,
34 matches: &ArgMatches,
35) -> Result<OperationCall, Error> {
36 let (operation, current_matches) = find_operation_from_matches(spec, matches)?;
37
38 let mut path_params = HashMap::new();
40 let mut query_params = HashMap::new();
41 let mut header_params = HashMap::new();
42
43 for param in &operation.parameters {
44 extract_param(
45 param,
46 current_matches,
47 &mut path_params,
48 &mut query_params,
49 &mut header_params,
50 );
51 }
52
53 let body = extract_body(operation.request_body.is_some(), current_matches)?;
55
56 let custom_headers = current_matches
58 .try_get_many::<String>("header")
59 .ok()
60 .flatten()
61 .map(|values| values.cloned().collect())
62 .unwrap_or_default();
63
64 Ok(OperationCall {
65 operation_id: operation.operation_id.clone(),
66 path_params,
67 query_params,
68 header_params,
69 body,
70 custom_headers,
71 })
72}
73
74pub fn matches_to_operation_id(spec: &CachedSpec, matches: &ArgMatches) -> Result<String, Error> {
85 let (operation, _) = find_operation_from_matches(spec, matches)?;
86 Ok(operation.operation_id.clone())
87}
88
89fn find_operation_from_matches<'a>(
91 spec: &'a CachedSpec,
92 matches: &'a ArgMatches,
93) -> Result<(&'a CachedCommand, &'a ArgMatches), Error> {
94 let mut current_matches = matches;
95 let mut subcommand_path = Vec::new();
96
97 while let Some((name, sub_matches)) = current_matches.subcommand() {
98 subcommand_path.push(name.to_string());
99 current_matches = sub_matches;
100 }
101
102 let operation_name = subcommand_path.last().ok_or_else(|| {
103 let name = "unknown".to_string();
104 let suggestions = crate::suggestions::suggest_similar_operations(spec, &name);
105 Error::operation_not_found_with_suggestions(name, &suggestions)
106 })?;
107
108 let group_name = subcommand_path
110 .len()
111 .checked_sub(2)
112 .and_then(|idx| subcommand_path.get(idx));
113
114 let operation = spec
115 .commands
116 .iter()
117 .find(|cmd| matches_effective_command_path(cmd, group_name, operation_name))
118 .or_else(|| {
122 spec.commands
123 .iter()
124 .find(|cmd| matches_effective_command_path(cmd, None, operation_name))
125 })
126 .ok_or_else(|| {
127 let suggestions = crate::suggestions::suggest_similar_operations(spec, operation_name);
128 Error::operation_not_found_with_suggestions(operation_name.clone(), &suggestions)
129 })?;
130
131 Ok((operation, current_matches))
132}
133
134fn matches_effective_command_path(
136 command: &CachedCommand,
137 group_name: Option<&String>,
138 operation_name: &str,
139) -> bool {
140 let operation_matches = effective_operation_name(command) == operation_name
141 || command
142 .aliases
143 .iter()
144 .any(|alias| to_kebab_case(alias) == operation_name);
145
146 if !operation_matches {
147 return false;
148 }
149
150 group_name.is_none_or(|group| effective_group_name(command) == *group)
151}
152
153fn effective_group_name(command: &CachedCommand) -> String {
155 command.display_group.as_ref().map_or_else(
156 || {
157 if command.name.is_empty() {
158 constants::DEFAULT_GROUP.to_string()
159 } else {
160 to_kebab_case(&command.name)
161 }
162 },
163 |group| to_kebab_case(group),
164 )
165}
166
167fn effective_operation_name(command: &CachedCommand) -> String {
169 command.display_name.as_ref().map_or_else(
170 || {
171 if command.operation_id.is_empty() {
172 command.method.to_lowercase()
173 } else {
174 to_kebab_case(&command.operation_id)
175 }
176 },
177 |name| to_kebab_case(name),
178 )
179}
180
181fn extract_param(
184 param: &CachedParameter,
185 matches: &ArgMatches,
186 path_params: &mut HashMap<String, String>,
187 query_params: &mut HashMap<String, String>,
188 header_params: &mut HashMap<String, String>,
189) {
190 let target = match param.location.as_str() {
191 "path" => path_params,
192 "query" => query_params,
193 "header" => header_params,
194 _ => return,
195 };
196
197 let is_boolean = param.schema_type.as_ref().is_some_and(|t| t == "boolean");
198
199 if !is_boolean {
200 let Some(value) = matches.try_get_one::<String>(¶m.name).ok().flatten() else {
203 return;
204 };
205 target.insert(param.name.clone(), value.clone());
206 return;
207 }
208
209 let flag_set = matches.get_flag(¶m.name);
212 if flag_set || param.location == "path" {
213 target.insert(param.name.clone(), flag_set.to_string());
214 }
215}
216
217fn extract_body(has_request_body: bool, matches: &ArgMatches) -> Result<Option<String>, Error> {
219 if !has_request_body {
220 return Ok(None);
221 }
222
223 matches
224 .get_one::<String>("body")
225 .map(|body_value| {
226 let _: serde_json::Value = serde_json::from_str(body_value)
228 .map_err(|e| Error::invalid_json_body(e.to_string()))?;
229 Ok(body_value.clone())
230 })
231 .transpose()
232}
233
234#[must_use]
236pub fn extract_server_var_args(matches: &ArgMatches) -> Vec<String> {
237 matches
238 .try_get_many::<String>("server-var")
239 .ok()
240 .flatten()
241 .map(|values| values.cloned().collect())
242 .unwrap_or_default()
243}
244
245#[must_use]
247pub fn has_show_examples_flag(matches: &ArgMatches) -> bool {
248 let mut current = matches;
250 while let Some((_name, sub)) = current.subcommand() {
251 current = sub;
252 }
253
254 current.try_contains_id("show-examples").unwrap_or(false) && current.get_flag("show-examples")
255}
256
257#[allow(clippy::cast_possible_truncation)]
263pub fn cli_to_execution_context(
264 cli: &Cli,
265 global_config: Option<GlobalConfig>,
266) -> Result<ExecutionContext, Error> {
267 let config_dir = if let Ok(dir) = std::env::var(crate::constants::ENV_APERTURE_CONFIG_DIR) {
268 PathBuf::from(dir)
269 } else {
270 crate::config::manager::get_config_dir()?
271 };
272
273 let cache_config = if cli.no_cache {
275 None
276 } else {
277 Some(CacheConfig {
278 cache_dir: config_dir
279 .join(crate::constants::DIR_CACHE)
280 .join(crate::constants::DIR_RESPONSES),
281 default_ttl: Duration::from_secs(cli.cache_ttl.unwrap_or(300)),
282 max_entries: 1000,
283 enabled: cli.cache || cli.cache_ttl.is_some(),
284 allow_authenticated: false,
285 })
286 };
287
288 let retry_context = build_retry_context(cli, global_config.as_ref())?;
290
291 Ok(ExecutionContext {
292 dry_run: cli.dry_run,
293 idempotency_key: cli.idempotency_key.clone(),
294 cache_config,
295 retry_context,
296 base_url: None, global_config,
298 server_var_args: Vec::new(), })
300}
301
302#[allow(clippy::cast_possible_truncation)]
306fn build_retry_context(
307 cli: &Cli,
308 global_config: Option<&GlobalConfig>,
309) -> Result<Option<RetryContext>, Error> {
310 let defaults = global_config.map(|c| &c.retry_defaults);
311
312 let max_attempts = cli
313 .retry
314 .or_else(|| defaults.map(|d| d.max_attempts))
315 .unwrap_or(0);
316
317 if max_attempts == 0 {
318 return Ok(None);
319 }
320
321 let initial_delay_ms = if let Some(ref delay_str) = cli.retry_delay {
323 parse_duration(delay_str)?.as_millis() as u64
324 } else {
325 defaults.map_or(500, |d| d.initial_delay_ms)
326 };
327
328 let max_delay_ms = if let Some(ref delay_str) = cli.retry_max_delay {
329 parse_duration(delay_str)?.as_millis() as u64
330 } else {
331 defaults.map_or(30_000, |d| d.max_delay_ms)
332 };
333
334 let has_idempotency_key = cli.idempotency_key.is_some();
335
336 Ok(Some(RetryContext {
337 max_attempts,
338 initial_delay_ms,
339 max_delay_ms,
340 force_retry: cli.force_retry,
341 method: None, has_idempotency_key,
343 }))
344}