retrofit_rs/
request.rs

1use crate::error::{Result, RetrofitError};
2use reqwest::Method;
3use serde::Serialize;
4use std::collections::HashMap;
5
6/// HTTP request builder
7#[derive(Debug, Clone)]
8pub struct RequestBuilder {
9    base_url: String,
10    path: String,
11    method: Method,
12    headers: HashMap<String, String>,
13    query_params: HashMap<String, String>,
14    path_params: HashMap<String, String>,
15    body: Option<String>,
16}
17
18impl RequestBuilder {
19    pub fn new(base_url: String, path: String, method: Method) -> Self {
20        Self {
21            base_url,
22            path,
23            method,
24            headers: HashMap::new(),
25            query_params: HashMap::new(),
26            path_params: HashMap::new(),
27            body: None,
28        }
29    }
30
31    /// Add a header
32    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
33        self.headers.insert(key.into(), value.into());
34        self
35    }
36
37    /// Add a query parameter
38    pub fn query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
39        self.query_params.insert(key.into(), value.into());
40        self
41    }
42
43    /// Add a path parameter
44    pub fn path_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
45        self.path_params.insert(key.into(), value.into());
46        self
47    }
48
49    /// Set JSON request body
50    pub fn json_body<T: Serialize>(mut self, body: &T) -> Result<Self> {
51        let json_str = serde_json::to_string(body).map_err(|e| RetrofitError::JsonError {
52            context: format!("serializing request body for {} {}", self.method, self.path),
53            message: e.to_string(),
54        })?;
55        self.body = Some(json_str);
56        self.headers
57            .insert("Content-Type".to_string(), "application/json".to_string());
58        Ok(self)
59    }
60
61    /// Build final URL
62    pub fn build_url(&self) -> Result<String> {
63        let mut url = format!("{}{}", self.base_url, self.path);
64
65        // Replace path parameters
66        for (key, value) in &self.path_params {
67            let placeholder = format!("{{{}}}", key);
68            if url.contains(&placeholder) {
69                url = url.replace(&placeholder, value);
70            } else {
71                return Err(RetrofitError::UrlParseError {
72                    url: url.clone(),
73                    message: format!("Path parameter '{}' not found in URL template", key),
74                });
75            }
76        }
77
78        // Add query parameters
79        if !self.query_params.is_empty() {
80            let query_string: Vec<String> = self
81                .query_params
82                .iter()
83                .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
84                .collect();
85            url = format!("{}?{}", url, query_string.join("&"));
86        }
87
88        Ok(url)
89    }
90
91    pub fn method(&self) -> &Method {
92        &self.method
93    }
94
95    pub fn headers(&self) -> &HashMap<String, String> {
96        &self.headers
97    }
98
99    pub fn body(&self) -> Option<&str> {
100        self.body.as_deref()
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use serde::{Deserialize, Serialize};
108
109    #[derive(Serialize, Deserialize, Debug, PartialEq)]
110    struct TestData {
111        name: String,
112        value: i32,
113    }
114
115    #[test]
116    fn test_request_builder_new() {
117        let builder = RequestBuilder::new(
118            "https://api.example.com".to_string(),
119            "/users".to_string(),
120            Method::GET,
121        );
122
123        assert_eq!(builder.method(), &Method::GET);
124        assert_eq!(builder.headers().len(), 0);
125        assert_eq!(builder.body(), None);
126    }
127
128    #[test]
129    fn test_add_header() {
130        let builder = RequestBuilder::new(
131            "https://api.example.com".to_string(),
132            "/users".to_string(),
133            Method::GET,
134        )
135        .header("Authorization", "Bearer token")
136        .header("Accept", "application/json");
137
138        assert_eq!(builder.headers().len(), 2);
139        assert_eq!(
140            builder.headers().get("Authorization"),
141            Some(&"Bearer token".to_string())
142        );
143        assert_eq!(
144            builder.headers().get("Accept"),
145            Some(&"application/json".to_string())
146        );
147    }
148
149    #[test]
150    fn test_add_query_params() {
151        let builder = RequestBuilder::new(
152            "https://api.example.com".to_string(),
153            "/users".to_string(),
154            Method::GET,
155        )
156        .query("page", "1")
157        .query("size", "10");
158
159        let url = builder.build_url().unwrap();
160        assert!(url.contains("page=1"));
161        assert!(url.contains("size=10"));
162    }
163
164    #[test]
165    fn test_path_params() {
166        let builder = RequestBuilder::new(
167            "https://api.example.com".to_string(),
168            "/users/{id}/posts/{post_id}".to_string(),
169            Method::GET,
170        )
171        .path_param("id", "123")
172        .path_param("post_id", "456");
173
174        let url = builder.build_url().unwrap();
175        assert_eq!(url, "https://api.example.com/users/123/posts/456");
176    }
177
178    #[test]
179    fn test_path_param_not_found() {
180        let builder = RequestBuilder::new(
181            "https://api.example.com".to_string(),
182            "/users".to_string(),
183            Method::GET,
184        )
185        .path_param("id", "123");
186
187        let result = builder.build_url();
188        assert!(result.is_err());
189        match result.unwrap_err() {
190            RetrofitError::UrlParseError { message, .. } => {
191                assert!(message.contains("not found in URL"));
192            }
193            _ => panic!("Expected UrlParseError"),
194        }
195    }
196
197    #[test]
198    fn test_json_body() {
199        let data = TestData {
200            name: "test".to_string(),
201            value: 42,
202        };
203
204        let builder = RequestBuilder::new(
205            "https://api.example.com".to_string(),
206            "/users".to_string(),
207            Method::POST,
208        )
209        .json_body(&data)
210        .unwrap();
211
212        assert!(builder.body().is_some());
213        assert_eq!(
214            builder.headers().get("Content-Type"),
215            Some(&"application/json".to_string())
216        );
217
218        let body_str = builder.body().unwrap();
219        let parsed: TestData = serde_json::from_str(body_str).unwrap();
220        assert_eq!(parsed, data);
221    }
222
223    #[test]
224    fn test_build_url_with_query_encoding() {
225        let builder = RequestBuilder::new(
226            "https://api.example.com".to_string(),
227            "/search".to_string(),
228            Method::GET,
229        )
230        .query("q", "hello world")
231        .query("filter", "type=user");
232
233        let url = builder.build_url().unwrap();
234        assert!(url.contains("hello%20world"));
235        assert!(url.contains("type%3Duser"));
236    }
237
238    #[test]
239    fn test_build_url_complex() {
240        let builder = RequestBuilder::new(
241            "https://api.example.com".to_string(),
242            "/users/{id}/repos".to_string(),
243            Method::GET,
244        )
245        .path_param("id", "octocat")
246        .query("sort", "updated")
247        .query("per_page", "10");
248
249        let url = builder.build_url().unwrap();
250        // Query parameters order may vary (HashMap), so check components
251        assert!(url.starts_with("https://api.example.com/users/octocat/repos?"));
252        assert!(url.contains("sort=updated"));
253        assert!(url.contains("per_page=10"));
254        assert!(url.contains("&"));
255    }
256}