1use std::fs;
2use std::io::{self, Read};
3use std::path::Path;
4
5use serde_json::Value;
6
7use crate::api::client::{Client, RawRequest};
8use crate::cli::ApiArgs;
9use crate::error::{GiteeError, Result};
10
11const METHODS: &[&str] = &["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
12
13pub fn execute(client: &Client, args: ApiArgs) -> Result<()> {
14 let path = normalize_endpoint(&args.endpoint)?;
15
16 let mut form: Vec<(String, String)> = Vec::new();
17 for f in &args.fields {
18 let (k, v) = parse_kv(f)?;
19 form.push((k.to_string(), v.to_string()));
20 }
21 for f in &args.raw_fields {
22 let (k, v) = parse_kv(f)?;
23 form.push((k.to_string(), v.to_string()));
24 }
25
26 let mut header_pairs: Vec<(String, String)> = Vec::new();
27 for h in &args.headers {
28 let (k, v) = parse_header(h)?;
29 header_pairs.push((k.to_string(), v.to_string()));
30 }
31
32 let body_bytes = if let Some(input) = &args.input {
33 Some(read_input(input)?)
34 } else {
35 None
36 };
37
38 let has_payload = !form.is_empty() || body_bytes.is_some();
39 let method = effective_method(args.method.as_deref(), has_payload)?;
40
41 if args.paginate && method != "GET" {
42 return Err(GiteeError::Usage("--paginate requires GET".into()));
43 }
44
45 let form_refs: Vec<(&str, &str)> = form.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
46 let header_refs: Vec<(&str, &str)> = header_pairs
47 .iter()
48 .map(|(k, v)| (k.as_str(), v.as_str()))
49 .collect();
50
51 if args.paginate {
52 let values = client.raw_paged(&path, &form_refs, &header_refs)?;
54 let text = serde_json::to_string_pretty(&values)
55 .map_err(|e| GiteeError::Usage(e.to_string()))?;
56 println!("{text}");
57 return Ok(());
58 }
59
60 let body_ref = body_bytes.as_deref();
61 let response = client.raw(&RawRequest {
62 method: &method,
63 path: &path,
64 query: &[],
65 form: &form_refs,
66 headers: &header_refs,
67 body: body_ref,
68 })?;
69 emit_stdout(&response)?;
70 Ok(())
71}
72
73fn emit_stdout(body: &str) -> Result<()> {
74 let text = match serde_json::from_str::<Value>(body) {
75 Ok(v) => serde_json::to_string_pretty(&v).map_err(|e| GiteeError::Usage(e.to_string()))?,
76 Err(_) => body.to_string(),
77 };
78 if text.ends_with('\n') {
79 print!("{text}");
80 } else {
81 println!("{text}");
82 }
83 Ok(())
84}
85
86fn read_input(spec: &str) -> Result<Vec<u8>> {
87 if spec == "-" {
88 let mut buf = Vec::new();
89 io::stdin()
90 .read_to_end(&mut buf)
91 .map_err(|e| GiteeError::Usage(format!("read stdin: {e}")))?;
92 return Ok(buf);
93 }
94 fs::read(Path::new(spec)).map_err(|e| GiteeError::Usage(format!("read file {spec}: {e}")))
95}
96
97pub(crate) fn normalize_endpoint(endpoint: &str) -> Result<String> {
98 let trimmed = endpoint.trim();
99 if trimmed.is_empty() {
100 return Err(GiteeError::Usage("endpoint required".into()));
101 }
102 if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
103 return Err(GiteeError::Usage(
104 "pass an API path, not a full URL (e.g. `user` or `/repos/owner/repo`)".into(),
105 ));
106 }
107 let mut path = trimmed.trim_start_matches('/').to_string();
108 if path.starts_with("api/v5/") {
109 path = path["api/v5/".len()..].to_string();
110 } else if path == "api/v5" {
111 path.clear();
112 }
113 if path.is_empty() {
114 Ok("/".to_string())
115 } else {
116 Ok(format!("/{path}"))
117 }
118}
119
120pub(crate) fn effective_method(explicit: Option<&str>, has_payload: bool) -> Result<String> {
121 match explicit {
122 Some(m) => {
123 let upper = m.to_uppercase();
124 if METHODS.contains(&upper.as_str()) {
125 Ok(upper)
126 } else {
127 Err(GiteeError::Usage(format!(
128 "unsupported HTTP method '{m}'; use one of: {}",
129 METHODS.join(", ")
130 )))
131 }
132 }
133 None if has_payload => Ok("POST".into()),
134 None => Ok("GET".into()),
135 }
136}
137
138pub(crate) fn parse_kv(s: &str) -> Result<(&str, &str)> {
139 let (k, v) = s
140 .split_once('=')
141 .ok_or_else(|| GiteeError::Usage(format!("expected key=value, got '{s}'")))?;
142 Ok((k, v))
143}
144
145pub(crate) fn parse_header(s: &str) -> Result<(&str, &str)> {
146 let (k, v) = s
147 .split_once(':')
148 .ok_or_else(|| GiteeError::Usage(format!("expected 'Name: value', got '{s}'")))?;
149 let key = k.trim();
150 if key.is_empty() {
151 return Err(GiteeError::Usage(format!(
152 "expected 'Name: value', got '{s}'"
153 )));
154 }
155 Ok((key, v.trim()))
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn normalize_endpoint_paths() {
164 assert_eq!(normalize_endpoint("user").unwrap(), "/user");
165 assert_eq!(normalize_endpoint("/user").unwrap(), "/user");
166 assert_eq!(normalize_endpoint("/api/v5/user").unwrap(), "/user");
167 assert_eq!(normalize_endpoint("api/v5/user").unwrap(), "/user");
168 assert_eq!(normalize_endpoint(" /api/v5/repos/o/r ").unwrap(), "/repos/o/r");
169 }
170
171 #[test]
172 fn normalize_endpoint_rejects_urls() {
173 assert!(normalize_endpoint("https://gitee.com/api/v5/user").is_err());
174 assert!(normalize_endpoint("http://example.com/user").is_err());
175 }
176
177 #[test]
178 fn effective_method_defaults() {
179 assert_eq!(effective_method(None, false).unwrap(), "GET");
180 assert_eq!(effective_method(None, true).unwrap(), "POST");
181 assert_eq!(effective_method(Some("patch"), false).unwrap(), "PATCH");
182 }
183
184 #[test]
185 fn effective_method_rejects_unknown() {
186 assert!(effective_method(Some("TRACE"), false).is_err());
187 }
188
189 #[test]
190 fn parse_kv_splits_on_first_equals() {
191 assert_eq!(parse_kv("a=b=c").unwrap(), ("a", "b=c"));
192 assert!(parse_kv("nope").is_err());
193 }
194
195 #[test]
196 fn parse_header_splits_and_trims() {
197 assert_eq!(parse_header("X-Foo: bar").unwrap(), ("X-Foo", "bar"));
198 assert_eq!(parse_header("X-Foo: bar baz ").unwrap(), ("X-Foo", "bar baz"));
199 assert!(parse_header(": missing").is_err());
200 assert!(parse_header("no-colon").is_err());
201 }
202}