1use crate::error::{Result, RetrofitError};
2use reqwest::Method;
3use serde::Serialize;
4use std::collections::HashMap;
5
6#[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 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 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 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 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 pub fn build_url(&self) -> Result<String> {
63 let mut url = format!("{}{}", self.base_url, self.path);
64
65 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 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 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}