1use crate::cache::models::{CachedCommand, CachedSecurityScheme, CachedSpec};
2use crate::config::models::GlobalConfig;
3use crate::config::url_resolver::BaseUrlResolver;
4use crate::error::Error;
5use clap::ArgMatches;
6use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
7use reqwest::Method;
8use serde_json::Value;
9use std::str::FromStr;
10
11pub async fn execute_request(
34 spec: &CachedSpec,
35 matches: &ArgMatches,
36 base_url: Option<&str>,
37 dry_run: bool,
38 idempotency_key: Option<&str>,
39 global_config: Option<&GlobalConfig>,
40) -> Result<(), Error> {
41 let operation = find_operation(spec, matches)?;
43
44 let resolver = BaseUrlResolver::new(spec);
46 let resolver = if let Some(config) = global_config {
47 resolver.with_global_config(config)
48 } else {
49 resolver
50 };
51 let base_url = resolver.resolve(base_url);
52
53 let url = build_url(&base_url, &operation.path, operation, matches)?;
55
56 let client = reqwest::Client::new();
58
59 let mut headers = build_headers(spec, operation, matches)?;
61
62 if let Some(key) = idempotency_key {
64 headers.insert(
65 HeaderName::from_static("idempotency-key"),
66 HeaderValue::from_str(key).map_err(|_| Error::InvalidIdempotencyKey)?,
67 );
68 }
69
70 let method = Method::from_str(&operation.method).map_err(|_| Error::InvalidHttpMethod {
72 method: operation.method.clone(),
73 })?;
74
75 let headers_clone = headers.clone(); let mut request = client.request(method.clone(), &url).headers(headers);
77
78 let mut current_matches = matches;
81 while let Some((_name, sub_matches)) = current_matches.subcommand() {
82 current_matches = sub_matches;
83 }
84
85 if operation.request_body.is_some() {
87 if let Some(body_value) = current_matches.get_one::<String>("body") {
88 let json_body: Value =
89 serde_json::from_str(body_value).map_err(|e| Error::InvalidJsonBody {
90 reason: e.to_string(),
91 })?;
92 request = request.json(&json_body);
93 }
94 }
95
96 if dry_run {
98 let dry_run_info = serde_json::json!({
99 "dry_run": true,
100 "method": operation.method,
101 "url": url,
102 "headers": headers_clone.iter().map(|(k, v)| (k.as_str(), v.to_str().unwrap_or("<binary>"))).collect::<std::collections::HashMap<_, _>>(),
103 "operation_id": operation.operation_id
104 });
105 println!("{}", serde_json::to_string_pretty(&dry_run_info).unwrap());
106 return Ok(());
107 }
108
109 println!("Executing {method} {url}");
111 let response = request.send().await.map_err(|e| Error::RequestFailed {
112 reason: e.to_string(),
113 })?;
114
115 let status = response.status();
116 let response_text = response
117 .text()
118 .await
119 .map_err(|e| Error::ResponseReadError {
120 reason: e.to_string(),
121 })?;
122
123 if !status.is_success() {
125 return Err(Error::HttpError {
126 status: status.as_u16(),
127 body: if response_text.is_empty() {
128 "(empty response)".to_string()
129 } else {
130 response_text
131 },
132 });
133 }
134
135 if !response_text.is_empty() {
137 if let Ok(json_value) = serde_json::from_str::<Value>(&response_text) {
139 if let Ok(pretty) = serde_json::to_string_pretty(&json_value) {
140 println!("{pretty}");
141 } else {
142 println!("{response_text}");
143 }
144 } else {
145 println!("{response_text}");
146 }
147 }
148
149 Ok(())
150}
151
152fn find_operation<'a>(
154 spec: &'a CachedSpec,
155 matches: &ArgMatches,
156) -> Result<&'a CachedCommand, Error> {
157 let mut current_matches = matches;
159 let mut subcommand_path = Vec::new();
160
161 while let Some((name, sub_matches)) = current_matches.subcommand() {
162 subcommand_path.push(name);
163 current_matches = sub_matches;
164 }
165
166 if let Some(operation_name) = subcommand_path.last() {
169 for command in &spec.commands {
170 let kebab_id = to_kebab_case(&command.operation_id);
172 if &kebab_id == operation_name || command.method.to_lowercase() == *operation_name {
173 return Ok(command);
174 }
175 }
176 }
177
178 Err(Error::OperationNotFound)
179}
180
181fn build_url(
183 base_url: &str,
184 path_template: &str,
185 operation: &CachedCommand,
186 matches: &ArgMatches,
187) -> Result<String, Error> {
188 let mut url = format!("{}{}", base_url.trim_end_matches('/'), path_template);
189
190 let mut current_matches = matches;
192 while let Some((_name, sub_matches)) = current_matches.subcommand() {
193 current_matches = sub_matches;
194 }
195
196 let mut start = 0;
199 while let Some(open) = url[start..].find('{') {
200 let open_pos = start + open;
201 if let Some(close) = url[open_pos..].find('}') {
202 let close_pos = open_pos + close;
203 let param_name = &url[open_pos + 1..close_pos];
204
205 if let Some(value) = current_matches.get_one::<String>(param_name) {
206 url.replace_range(open_pos..=close_pos, value);
207 start = open_pos + value.len();
208 } else {
209 return Err(Error::MissingPathParameter {
210 name: param_name.to_string(),
211 });
212 }
213 } else {
214 break;
215 }
216 }
217
218 let mut query_params = Vec::new();
220 for arg in current_matches.ids() {
221 let arg_str = arg.as_str();
222 let is_query_param = operation
224 .parameters
225 .iter()
226 .any(|p| p.name == arg_str && p.location == "query");
227 if is_query_param {
228 if let Some(value) = current_matches.get_one::<String>(arg_str) {
229 query_params.push(format!("{}={}", arg_str, urlencoding::encode(value)));
230 }
231 }
232 }
233
234 if !query_params.is_empty() {
235 url.push('?');
236 url.push_str(&query_params.join("&"));
237 }
238
239 Ok(url)
240}
241
242fn build_headers(
244 spec: &CachedSpec,
245 operation: &CachedCommand,
246 matches: &ArgMatches,
247) -> Result<HeaderMap, Error> {
248 let mut headers = HeaderMap::new();
249
250 headers.insert("User-Agent", HeaderValue::from_static("aperture/0.1.0"));
252 headers.insert("Accept", HeaderValue::from_static("application/json"));
253
254 let mut current_matches = matches;
256 while let Some((_name, sub_matches)) = current_matches.subcommand() {
257 current_matches = sub_matches;
258 }
259
260 for param in &operation.parameters {
262 if param.location == "header" {
263 if let Some(value) = current_matches.get_one::<String>(¶m.name) {
264 let header_name =
265 HeaderName::from_str(¶m.name).map_err(|e| Error::InvalidHeaderName {
266 name: param.name.clone(),
267 reason: e.to_string(),
268 })?;
269 let header_value =
270 HeaderValue::from_str(value).map_err(|e| Error::InvalidHeaderValue {
271 name: param.name.clone(),
272 reason: e.to_string(),
273 })?;
274 headers.insert(header_name, header_value);
275 }
276 }
277 }
278
279 for security_scheme_name in &operation.security_requirements {
281 if let Some(security_scheme) = spec.security_schemes.get(security_scheme_name) {
282 add_authentication_header(&mut headers, security_scheme)?;
283 }
284 }
285
286 if let Ok(Some(custom_headers)) = current_matches.try_get_many::<String>("header") {
289 for header_str in custom_headers {
290 let (name, value) = parse_custom_header(header_str)?;
291 let header_name =
292 HeaderName::from_str(&name).map_err(|e| Error::InvalidHeaderName {
293 name: name.clone(),
294 reason: e.to_string(),
295 })?;
296 let header_value =
297 HeaderValue::from_str(&value).map_err(|e| Error::InvalidHeaderValue {
298 name: name.clone(),
299 reason: e.to_string(),
300 })?;
301 headers.insert(header_name, header_value);
302 }
303 }
304
305 Ok(headers)
306}
307
308fn parse_custom_header(header_str: &str) -> Result<(String, String), Error> {
310 let colon_pos = header_str
312 .find(':')
313 .ok_or_else(|| Error::InvalidHeaderFormat {
314 header: header_str.to_string(),
315 })?;
316
317 let name = header_str[..colon_pos].trim();
318 let value = header_str[colon_pos + 1..].trim();
319
320 if name.is_empty() {
321 return Err(Error::EmptyHeaderName);
322 }
323
324 let expanded_value = if value.starts_with("${") && value.ends_with('}') {
326 let var_name = &value[2..value.len() - 1];
328 std::env::var(var_name).unwrap_or_else(|_| value.to_string())
329 } else {
330 value.to_string()
331 };
332
333 Ok((name.to_string(), expanded_value))
334}
335
336fn add_authentication_header(
338 headers: &mut HeaderMap,
339 security_scheme: &CachedSecurityScheme,
340) -> Result<(), Error> {
341 if let Some(aperture_secret) = &security_scheme.aperture_secret {
343 let secret_value =
345 std::env::var(&aperture_secret.name).map_err(|_| Error::SecretNotSet {
346 scheme_name: security_scheme.name.clone(),
347 env_var: aperture_secret.name.clone(),
348 })?;
349
350 match security_scheme.scheme_type.as_str() {
352 "apiKey" => {
353 if let (Some(location), Some(param_name)) =
354 (&security_scheme.location, &security_scheme.parameter_name)
355 {
356 if location == "header" {
357 let header_name = HeaderName::from_str(param_name).map_err(|e| {
358 Error::InvalidHeaderName {
359 name: param_name.clone(),
360 reason: e.to_string(),
361 }
362 })?;
363 let header_value = HeaderValue::from_str(&secret_value).map_err(|e| {
364 Error::InvalidHeaderValue {
365 name: param_name.clone(),
366 reason: e.to_string(),
367 }
368 })?;
369 headers.insert(header_name, header_value);
370 }
371 }
373 }
374 "http" => {
375 if let Some(scheme) = &security_scheme.scheme {
376 match scheme.as_str() {
377 "bearer" => {
378 let auth_value = format!("Bearer {secret_value}");
379 let header_value = HeaderValue::from_str(&auth_value).map_err(|e| {
380 Error::InvalidHeaderValue {
381 name: "Authorization".to_string(),
382 reason: e.to_string(),
383 }
384 })?;
385 headers.insert("Authorization", header_value);
386 }
387 "basic" => {
388 let auth_value = format!("Basic {secret_value}");
390 let header_value = HeaderValue::from_str(&auth_value).map_err(|e| {
391 Error::InvalidHeaderValue {
392 name: "Authorization".to_string(),
393 reason: e.to_string(),
394 }
395 })?;
396 headers.insert("Authorization", header_value);
397 }
398 _ => {
399 return Err(Error::UnsupportedAuthScheme {
400 scheme: scheme.clone(),
401 });
402 }
403 }
404 }
405 }
406 _ => {
407 return Err(Error::UnsupportedSecurityScheme {
408 scheme_type: security_scheme.scheme_type.clone(),
409 });
410 }
411 }
412 }
413
414 Ok(())
415}
416
417fn to_kebab_case(s: &str) -> String {
419 let mut result = String::new();
420 let mut prev_lowercase = false;
421
422 for (i, ch) in s.chars().enumerate() {
423 if ch.is_uppercase() && i > 0 && prev_lowercase {
424 result.push('-');
425 }
426 result.push(ch.to_ascii_lowercase());
427 prev_lowercase = ch.is_lowercase();
428 }
429
430 result
431}