1use napi_derive::napi;
4use std::ptr;
5
6use eggfetch_ffi::ErrorHandle;
7
8#[napi]
27pub struct EggfetchClient {
28 inner: usize,
29}
30
31#[napi]
32impl EggfetchClient {
33 #[napi(constructor)]
39 pub fn new() -> napi::Result<Self> {
40 let inner = unsafe { eggfetch_ffi::eggfetch_client_new() };
41 if inner.is_null() {
42 return Err(napi::Error::from_reason(
43 "failed to create eggfetch client: allocation failed or runtime unavailable",
44 ));
45 }
46 Ok(Self {
47 inner: inner as usize,
48 })
49 }
50
51 #[napi]
57 pub async fn get(&self, url: String) -> napi::Result<crate::EggfetchResponse> {
58 self.send_request("GET", &url, None).await
59 }
60
61 #[napi]
67 pub async fn post(
68 &self,
69 url: String,
70 body: Option<String>,
71 ) -> napi::Result<crate::EggfetchResponse> {
72 self.send_request("POST", &url, body.as_deref()).await
73 }
74
75 #[napi]
81 pub async fn put(
82 &self,
83 url: String,
84 body: Option<String>,
85 ) -> napi::Result<crate::EggfetchResponse> {
86 self.send_request("PUT", &url, body.as_deref()).await
87 }
88
89 #[napi]
95 pub async fn patch(
96 &self,
97 url: String,
98 body: Option<String>,
99 ) -> napi::Result<crate::EggfetchResponse> {
100 self.send_request("PATCH", &url, body.as_deref()).await
101 }
102
103 #[napi]
109 pub async fn delete(&self, url: String) -> napi::Result<crate::EggfetchResponse> {
110 self.send_request("DELETE", &url, None).await
111 }
112
113 #[napi]
119 pub async fn head(&self, url: String) -> napi::Result<crate::EggfetchResponse> {
120 self.send_request("HEAD", &url, None).await
121 }
122
123 #[napi]
129 pub async fn options(&self, url: String) -> napi::Result<crate::EggfetchResponse> {
130 self.send_request("OPTIONS", &url, None).await
131 }
132
133 #[napi]
139 pub async fn request(
140 &self,
141 method: String,
142 url: String,
143 body: Option<String>,
144 ) -> napi::Result<crate::EggfetchResponse> {
145 self.send_request(&method, &url, body.as_deref()).await
146 }
147}
148
149impl EggfetchClient {
150 fn send_request(
151 &self,
152 method: &str,
153 url: &str,
154 body: Option<&str>,
155 ) -> impl std::future::Future<Output = napi::Result<crate::EggfetchResponse>> + Send {
156 let client_ptr = self.inner;
157 let method = method.to_owned();
158 let url = url.to_owned();
159 let body = body.map(String::from);
160
161 async move {
162 napi::bindgen_prelude::spawn_blocking(move || {
163 let client = client_ptr as *mut eggfetch_ffi::ClientHandle;
164 let method_c = std::ffi::CString::new(method)
165 .map_err(|e| napi::Error::from_reason(format!("invalid method string: {e}")))?;
166 let url_c = std::ffi::CString::new(url)
167 .map_err(|e| napi::Error::from_reason(format!("invalid url string: {e}")))?;
168
169 let req = unsafe {
170 eggfetch_ffi::eggfetch_client_request(client, method_c.as_ptr(), url_c.as_ptr())
171 };
172 if req.is_null() {
173 return Err(napi::Error::from_reason("failed to create request"));
174 }
175
176 if let Some(body_str) = &body {
177 let body_c = match std::ffi::CString::new(body_str.as_str()) {
178 Ok(body_c) => body_c,
179 Err(e) => {
180 unsafe {
181 eggfetch_ffi::eggfetch_request_free(req);
182 }
183 return Err(napi::Error::from_reason(format!(
184 "invalid body string: {e}"
185 )));
186 }
187 };
188 let rc =
189 unsafe { eggfetch_ffi::eggfetch_request_body_str(req, body_c.as_ptr()) };
190 if rc != 0 {
191 unsafe {
192 eggfetch_ffi::eggfetch_request_free(req);
193 }
194 return Err(napi::Error::from_reason(format!(
195 "failed to set request body (code {rc})"
196 )));
197 }
198 }
199
200 let mut err: *mut ErrorHandle = ptr::null_mut();
201 let resp = unsafe { eggfetch_ffi::eggfetch_client_send(client, req, &mut err) };
202
203 if resp.is_null() {
204 if !err.is_null() {
205 let kind = unsafe { eggfetch_ffi::eggfetch_error_kind(err) };
206 let msg = unsafe { eggfetch_ffi::eggfetch_error_message(err) };
207 let kind_str = if kind.is_null() {
208 "unknown".to_owned()
209 } else {
210 unsafe { std::ffi::CStr::from_ptr(kind) }
211 .to_string_lossy()
212 .into_owned()
213 };
214 let msg_str = if msg.is_null() {
215 "unknown error".to_owned()
216 } else {
217 unsafe { std::ffi::CStr::from_ptr(msg) }
218 .to_string_lossy()
219 .into_owned()
220 };
221 unsafe {
222 if !kind.is_null() {
223 eggfetch_ffi::eggfetch_string_free(kind);
224 }
225 if !msg.is_null() {
226 eggfetch_ffi::eggfetch_string_free(msg);
227 }
228 eggfetch_ffi::eggfetch_error_free(err);
229 }
230 return Err(napi::Error::from_reason(format!(
231 "eggfetch error [{kind_str}]: {msg_str}"
232 )));
233 }
234 return Err(napi::Error::from_reason(
235 "request failed with unknown error",
236 ));
237 }
238
239 Ok(crate::EggfetchResponse::from_raw(resp))
240 })
241 .await
242 .map_err(|e| napi::Error::from_reason(format!("request worker failed: {e}")))?
243 }
244 }
245}
246
247impl Drop for EggfetchClient {
248 fn drop(&mut self) {
249 if self.inner != 0 {
250 unsafe {
251 eggfetch_ffi::eggfetch_client_free(self.inner as *mut eggfetch_ffi::ClientHandle);
252 }
253 }
254 }
255}