Skip to main content

gitee_cli_rs/cmd/
api.rs

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        // GET: -F fields belong on the query string (same as a plain raw GET).
53        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}