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}