rs
1use magnus::{define_module, function, method, Error, Module, Object, Value, class, RHash, TryConvert};
2use reqwest::{Client, Method, Response};
3use std::collections::HashMap;
4use std::time::Duration;
5use tokio::runtime::Runtime;
6
7#[magnus::wrap(class = "Net::Hippie::RustResponse")]
8struct RustResponse {
9 status: u16,
10 headers: HashMap<String, String>,
11 body: String,
12}
13
14impl RustResponse {
15 fn new(status: u16, headers: HashMap<String, String>, body: String) -> Self {
16 Self {
17 status,
18 headers,
19 body,
20 }
21 }
22
23 fn code(&self) -> String {
24 self.status.to_string()
25 }
26
27 fn body(&self) -> String {
28 self.body.clone()
29 }
30
31 fn get_header(&self, name: String) -> Option<String> {
32 self.headers.get(&name.to_lowercase()).cloned()
33 }
34}
35
36#[magnus::wrap(class = "Net::Hippie::RustClient")]
37struct RustClient {
38 client: Client,
39 runtime: Runtime,
40}
41
42impl RustClient {
43 fn new() -> Result<Self, Error> {
44 let client = Client::builder()
45 .timeout(Duration::from_secs(10))
46 .connect_timeout(Duration::from_secs(10))
47 .redirect(reqwest::redirect::Policy::none())
48 .build()
49 .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
50
51 let runtime = Runtime::new()
52 .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
53
54 Ok(Self { client, runtime })
55 }
56
57 fn execute_request(
58 &self,
59 method_str: String,
60 url: String,
61 headers: Value,
62 body: String,
63 ) -> Result<RustResponse, Error> {
64 let method = match method_str.to_uppercase().as_str() {
65 "GET" => Method::GET,
66 "POST" => Method::POST,
67 "PUT" => Method::PUT,
68 "DELETE" => Method::DELETE,
69 "PATCH" => Method::PATCH,
70 _ => return Err(Error::new(magnus::exception::arg_error(), "Invalid HTTP method")),
71 };
72
73 self.runtime.block_on(async {
74 let mut request_builder = self.client.request(method, &url);
75
76 // Add headers if provided
77 if let Some(headers_hash) = RHash::from_value(headers) {
78 for (key, value) in headers_hash {
79 if let (Ok(key_str), Ok(value_str)) = (String::try_convert(key), String::try_convert(value)) {
80 request_builder = request_builder.header(&key_str, &value_str);
81 }
82 }
83 }
84
85 // Add body if not empty
86 if !body.is_empty() {
87 request_builder = request_builder.body(body);
88 }
89
90 let response = request_builder.send().await
91 .map_err(|e| self.map_reqwest_error(e))?;
92
93 self.convert_response(response).await
94 })
95 }
96
97 async fn convert_response(&self, response: Response) -> Result<RustResponse, Error> {
98 let status = response.status().as_u16();
99
100 let mut headers = HashMap::new();
101 for (key, value) in response.headers() {
102 if let Ok(value_str) = value.to_str() {
103 headers.insert(key.as_str().to_lowercase(), value_str.to_string());
104 }
105 }
106
107 let body = response.text().await
108 .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
109
110 Ok(RustResponse::new(status, headers, body))
111 }
112
113 fn map_reqwest_error(&self, error: reqwest::Error) -> Error {
114 if error.is_timeout() {
115 Error::new(magnus::exception::runtime_error(), "Net::ReadTimeout")
116 } else if error.is_connect() {
117 Error::new(magnus::exception::runtime_error(), "Errno::ECONNREFUSED")
118 } else {
119 Error::new(magnus::exception::runtime_error(), error.to_string())
120 }
121 }
122
123 fn get(&self, url: String, headers: Value, body: String) -> Result<RustResponse, Error> {
124 self.execute_request("GET".to_string(), url, headers, body)
125 }
126
127 fn post(&self, url: String, headers: Value, body: String) -> Result<RustResponse, Error> {
128 self.execute_request("POST".to_string(), url, headers, body)
129 }
130
131 fn put(&self, url: String, headers: Value, body: String) -> Result<RustResponse, Error> {
132 self.execute_request("PUT".to_string(), url, headers, body)
133 }
134
135 fn delete(&self, url: String, headers: Value, body: String) -> Result<RustResponse, Error> {
136 self.execute_request("DELETE".to_string(), url, headers, body)
137 }
138
139 fn patch(&self, url: String, headers: Value, body: String) -> Result<RustResponse, Error> {
140 self.execute_request("PATCH".to_string(), url, headers, body)
141 }
142}
143
144#[magnus::init]
145fn init() -> Result<(), Error> {
146 let net_module = define_module("Net")?;
147 let hippie_module = net_module.define_module("Hippie")?;
148
149 let rust_client_class = hippie_module.define_class("RustClient", class::object())?;
150 rust_client_class.define_singleton_method("new", function!(RustClient::new, 0))?;
151 rust_client_class.define_method("get", method!(RustClient::get, 3))?;
152 rust_client_class.define_method("post", method!(RustClient::post, 3))?;
153 rust_client_class.define_method("put", method!(RustClient::put, 3))?;
154 rust_client_class.define_method("delete", method!(RustClient::delete, 3))?;
155 rust_client_class.define_method("patch", method!(RustClient::patch, 3))?;
156
157 let rust_response_class = hippie_module.define_class("RustResponse", class::object())?;
158 rust_response_class.define_method("code", method!(RustResponse::code, 0))?;
159 rust_response_class.define_method("body", method!(RustResponse::body, 0))?;
160 rust_response_class.define_method("[]", method!(RustResponse::get_header, 1))?;
161
162 Ok(())
163}