1use std::str::FromStr;
7
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
23#[serde(rename_all = "UPPERCASE")]
24pub enum HttpMethod {
25 Get,
26 Post,
27 Put,
28 Delete,
29 Patch,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
34#[error("unknown HTTP method: {0}")]
35pub struct UnknownHttpMethod(pub String);
36
37impl FromStr for HttpMethod {
38 type Err = UnknownHttpMethod;
39
40 fn from_str(s: &str) -> Result<Self, Self::Err> {
41 match s.to_lowercase().as_str() {
42 "get" => Ok(Self::Get),
43 "post" => Ok(Self::Post),
44 "put" => Ok(Self::Put),
45 "delete" => Ok(Self::Delete),
46 "patch" => Ok(Self::Patch),
47 _ => Err(UnknownHttpMethod(s.to_string())),
48 }
49 }
50}
51
52impl std::fmt::Display for HttpMethod {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Self::Get => write!(f, "GET"),
56 Self::Post => write!(f, "POST"),
57 Self::Put => write!(f, "PUT"),
58 Self::Delete => write!(f, "DELETE"),
59 Self::Patch => write!(f, "PATCH"),
60 }
61 }
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
74pub struct ApiRequest {
75 pub method: HttpMethod,
77 pub path: String,
79 pub query_params: Vec<(String, String)>,
81 pub body: Option<RequestBody>,
83 pub content_type: Option<String>,
85}
86
87#[derive(Clone, Debug, Serialize, Deserialize)]
96pub enum RequestBody {
97 Json(Value),
99 Multipart(MultipartBody),
102}
103
104#[derive(Clone, Debug, Serialize, Deserialize)]
106pub struct MultipartBody {
107 pub text_fields: Vec<(String, String)>,
109 pub binary_fields: Vec<BinaryField>,
111}
112
113#[derive(Clone, Debug, Serialize, Deserialize)]
115pub struct BinaryField {
116 pub field_name: String,
118 pub data: Vec<u8>,
120 pub content_type: Option<String>,
122}
123
124impl RequestBody {
125 #[must_use]
129 pub const fn as_json(&self) -> Option<&Value> {
130 match self {
131 Self::Json(v) => Some(v),
132 Self::Multipart(_) => None,
133 }
134 }
135}
136
137#[derive(Clone, Debug)]
146pub struct ApiResponse {
147 pub status: u16,
149 pub body: String,
151}
152
153#[cfg(test)]
158#[allow(clippy::expect_used)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn http_method_from_str_lowercase() {
164 assert_eq!(HttpMethod::from_str("get"), Ok(HttpMethod::Get));
165 assert_eq!(HttpMethod::from_str("post"), Ok(HttpMethod::Post));
166 assert_eq!(HttpMethod::from_str("put"), Ok(HttpMethod::Put));
167 assert_eq!(HttpMethod::from_str("delete"), Ok(HttpMethod::Delete));
168 assert_eq!(HttpMethod::from_str("patch"), Ok(HttpMethod::Patch));
169 }
170
171 #[test]
172 fn http_method_from_str_uppercase() {
173 assert_eq!(HttpMethod::from_str("GET"), Ok(HttpMethod::Get));
174 assert_eq!(HttpMethod::from_str("POST"), Ok(HttpMethod::Post));
175 }
176
177 #[test]
178 fn http_method_from_str_mixed_case() {
179 assert_eq!(HttpMethod::from_str("Get"), Ok(HttpMethod::Get));
180 assert_eq!(HttpMethod::from_str("PoSt"), Ok(HttpMethod::Post));
181 }
182
183 #[test]
184 fn http_method_from_str_unknown() {
185 assert!(HttpMethod::from_str("TRACE").is_err());
186 assert!(HttpMethod::from_str("").is_err());
187 }
188
189 #[test]
190 fn http_method_display() {
191 assert_eq!(HttpMethod::Get.to_string(), "GET");
192 assert_eq!(HttpMethod::Post.to_string(), "POST");
193 assert_eq!(HttpMethod::Put.to_string(), "PUT");
194 assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
195 assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
196 }
197
198 #[test]
199 fn http_method_serializes_uppercase() {
200 let json = serde_json::to_string(&HttpMethod::Get).expect("should serialize");
201 assert_eq!(json, "\"GET\"");
202 }
203
204 #[test]
205 fn http_method_deserializes_uppercase() {
206 let method: HttpMethod = serde_json::from_str("\"POST\"").expect("should deserialize");
207 assert_eq!(method, HttpMethod::Post);
208 }
209
210 #[test]
211 fn api_request_serializes() {
212 let req = ApiRequest {
213 method: HttpMethod::Get,
214 path: "/documents/abc123".to_string(),
215 query_params: vec![("limit".to_string(), "10".to_string())],
216 body: None,
217 content_type: None,
218 };
219 let json = serde_json::to_value(&req).expect("should serialize");
220 assert_eq!(json["method"], "GET");
221 assert_eq!(json["path"], "/documents/abc123");
222 }
223
224 #[test]
225 fn api_request_with_json_body_serializes() {
226 let req = ApiRequest {
227 method: HttpMethod::Post,
228 path: "/documents".to_string(),
229 query_params: vec![],
230 body: Some(RequestBody::Json(serde_json::json!({"name": "test"}))),
231 content_type: Some("application/json".to_string()),
232 };
233 let json = serde_json::to_value(&req).expect("should serialize");
234 assert_eq!(json["method"], "POST");
235 assert!(json["body"].is_object());
236 }
237
238 #[test]
239 fn request_body_as_json() {
240 let json_body = RequestBody::Json(serde_json::json!({"key": "value"}));
241 assert!(json_body.as_json().is_some());
242
243 let multipart_body = RequestBody::Multipart(MultipartBody {
244 text_fields: vec![],
245 binary_fields: vec![],
246 });
247 assert!(multipart_body.as_json().is_none());
248 }
249}