mockforge_sdk/
ffi.rs

1//! FFI bindings for using `MockForge` from other languages (Python, Node.js, Go)
2//!
3//! This module provides C-compatible functions that can be called from other languages.
4
5#![allow(unsafe_code)]
6
7use crate::server::MockServer;
8use std::ffi::{CStr, CString};
9use std::os::raw::c_char;
10use std::ptr;
11use std::sync::Arc;
12use tokio::runtime::Runtime;
13use tokio::sync::Mutex;
14
15/// Opaque handle to a `MockServer`
16pub struct MockServerHandle {
17    server: Arc<Mutex<MockServer>>,
18    runtime: Runtime,
19}
20
21/// Create a new mock server
22///
23/// # Safety
24/// This function is FFI-safe
25#[no_mangle]
26pub unsafe extern "C" fn mockforge_server_new(port: u16) -> *mut MockServerHandle {
27    let runtime = match Runtime::new() {
28        Ok(rt) => rt,
29        Err(_) => return ptr::null_mut(),
30    };
31
32    // Create and start the server
33    let server = runtime.block_on(async { MockServer::new().port(port).start().await });
34
35    let server = match server {
36        Ok(s) => s,
37        Err(_) => return ptr::null_mut(),
38    };
39
40    let handle = MockServerHandle {
41        server: Arc::new(Mutex::new(server)),
42        runtime,
43    };
44
45    Box::into_raw(Box::new(handle))
46}
47
48/// Stop and destroy a mock server
49///
50/// # Safety
51/// The handle must be valid and not used after this call
52#[no_mangle]
53pub unsafe extern "C" fn mockforge_server_destroy(handle: *mut MockServerHandle) {
54    if handle.is_null() {
55        return;
56    }
57
58    let handle = Box::from_raw(handle);
59    let server = handle.server.clone();
60
61    handle.runtime.block_on(async move {
62        let mut server = server.lock().await;
63        let _ = std::mem::take(&mut *server).stop().await;
64    });
65}
66
67/// Add a stub response to the mock server
68///
69/// # Safety
70/// - handle must be valid
71/// - method, path, and body must be valid null-terminated C strings
72/// - Returns 0 on success, -1 on error
73#[no_mangle]
74pub unsafe extern "C" fn mockforge_server_stub(
75    handle: *mut MockServerHandle,
76    method: *const c_char,
77    path: *const c_char,
78    status: u16,
79    body: *const c_char,
80) -> i32 {
81    if handle.is_null() || method.is_null() || path.is_null() || body.is_null() {
82        return -1;
83    }
84
85    let handle = &*handle;
86
87    let method = match CStr::from_ptr(method).to_str() {
88        Ok(s) => s,
89        Err(_) => return -1,
90    };
91
92    let path = match CStr::from_ptr(path).to_str() {
93        Ok(s) => s,
94        Err(_) => return -1,
95    };
96
97    let body = match CStr::from_ptr(body).to_str() {
98        Ok(s) => s,
99        Err(_) => return -1,
100    };
101
102    let body_value: serde_json::Value = match serde_json::from_str(body) {
103        Ok(v) => v,
104        Err(_) => return -1,
105    };
106
107    let server = handle.server.clone();
108    let result = handle.runtime.block_on(async move {
109        let mut server = server.lock().await;
110        server.stub_response(method, path, body_value).await
111    });
112
113    match result {
114        Ok(()) => 0,
115        Err(_) => -1,
116    }
117}
118
119/// Get the server URL
120///
121/// # Safety
122/// - handle must be valid
123/// - Returns a C string that must be freed with `mockforge_free_string`
124#[no_mangle]
125pub unsafe extern "C" fn mockforge_server_url(handle: *const MockServerHandle) -> *mut c_char {
126    if handle.is_null() {
127        return ptr::null_mut();
128    }
129
130    let handle = &*handle;
131    let server = handle.server.clone();
132
133    let url = handle.runtime.block_on(async move {
134        let server = server.lock().await;
135        server.url()
136    });
137
138    match CString::new(url) {
139        Ok(s) => s.into_raw(),
140        Err(_) => ptr::null_mut(),
141    }
142}
143
144/// Free a string returned by `MockForge`
145///
146/// # Safety
147/// The string must have been allocated by `MockForge`
148#[no_mangle]
149pub unsafe extern "C" fn mockforge_free_string(s: *mut c_char) {
150    if !s.is_null() {
151        let _ = CString::from_raw(s);
152    }
153}
154
155/// Get the last error message
156///
157/// # Safety
158/// Returns a C string that must be freed with `mockforge_free_string`
159#[no_mangle]
160pub const unsafe extern "C" fn mockforge_last_error() -> *mut c_char {
161    // Thread-local error storage could be implemented here
162    ptr::null_mut()
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use std::ffi::CStr;
169
170    #[test]
171    fn test_mock_server_handle_size() {
172        // Verify the handle structure size is reasonable
173        assert!(std::mem::size_of::<MockServerHandle>() > 0);
174    }
175
176    #[test]
177    fn test_mockforge_server_new_null_on_invalid_port() {
178        unsafe {
179            // Try to create server on port 0 (which might fail in some scenarios)
180            // This tests the error path
181            let handle = mockforge_server_new(0);
182
183            // We can't reliably test for null here because port 0 is valid (OS assigns port)
184            // But we can test that the function doesn't crash
185            if !handle.is_null() {
186                mockforge_server_destroy(handle);
187            }
188        }
189    }
190
191    #[test]
192    fn test_mockforge_server_destroy_null_handle() {
193        unsafe {
194            // Should not crash when destroying null handle
195            mockforge_server_destroy(ptr::null_mut());
196        }
197    }
198
199    #[test]
200    fn test_mockforge_server_stub_null_handle() {
201        unsafe {
202            let method = CString::new("GET").unwrap();
203            let path = CString::new("/test").unwrap();
204            let body = CString::new("{}").unwrap();
205
206            let result = mockforge_server_stub(
207                ptr::null_mut(),
208                method.as_ptr(),
209                path.as_ptr(),
210                200,
211                body.as_ptr(),
212            );
213
214            assert_eq!(result, -1);
215        }
216    }
217
218    #[test]
219    fn test_mockforge_server_stub_null_method() {
220        unsafe {
221            // Create a minimal handle (won't actually use it)
222            let path = CString::new("/test").unwrap();
223            let body = CString::new("{}").unwrap();
224
225            // Test with null method
226            let result = mockforge_server_stub(
227                ptr::null_mut(),
228                ptr::null(),
229                path.as_ptr(),
230                200,
231                body.as_ptr(),
232            );
233
234            assert_eq!(result, -1);
235        }
236    }
237
238    #[test]
239    fn test_mockforge_server_stub_null_path() {
240        unsafe {
241            let method = CString::new("GET").unwrap();
242            let body = CString::new("{}").unwrap();
243
244            let result = mockforge_server_stub(
245                ptr::null_mut(),
246                method.as_ptr(),
247                ptr::null(),
248                200,
249                body.as_ptr(),
250            );
251
252            assert_eq!(result, -1);
253        }
254    }
255
256    #[test]
257    fn test_mockforge_server_stub_null_body() {
258        unsafe {
259            let method = CString::new("GET").unwrap();
260            let path = CString::new("/test").unwrap();
261
262            let result = mockforge_server_stub(
263                ptr::null_mut(),
264                method.as_ptr(),
265                path.as_ptr(),
266                200,
267                ptr::null(),
268            );
269
270            assert_eq!(result, -1);
271        }
272    }
273
274    #[test]
275    fn test_mockforge_server_stub_invalid_json() {
276        unsafe {
277            let method = CString::new("GET").unwrap();
278            let path = CString::new("/test").unwrap();
279            let body = CString::new("{invalid json").unwrap();
280
281            // Even with a null handle, invalid JSON should return -1
282            let result = mockforge_server_stub(
283                ptr::null_mut(),
284                method.as_ptr(),
285                path.as_ptr(),
286                200,
287                body.as_ptr(),
288            );
289
290            assert_eq!(result, -1);
291        }
292    }
293
294    #[test]
295    fn test_mockforge_server_url_null_handle() {
296        unsafe {
297            let url = mockforge_server_url(ptr::null());
298            assert!(url.is_null());
299        }
300    }
301
302    #[test]
303    fn test_mockforge_free_string_null() {
304        unsafe {
305            // Should not crash when freeing null string
306            mockforge_free_string(ptr::null_mut());
307        }
308    }
309
310    #[test]
311    fn test_mockforge_free_string_valid() {
312        unsafe {
313            let test_str = CString::new("test").unwrap();
314            let raw_ptr = test_str.into_raw();
315
316            // Free the string
317            mockforge_free_string(raw_ptr);
318
319            // After freeing, we shouldn't use the pointer anymore
320            // This test just verifies it doesn't crash
321        }
322    }
323
324    #[test]
325    fn test_mockforge_last_error_returns_null() {
326        unsafe {
327            let error = mockforge_last_error();
328            assert!(error.is_null());
329        }
330    }
331
332    #[test]
333    fn test_cstring_conversion_utf8() {
334        unsafe {
335            let method = CString::new("GET").unwrap();
336            let method_ptr = method.as_ptr();
337
338            let converted = CStr::from_ptr(method_ptr);
339            assert_eq!(converted.to_str().unwrap(), "GET");
340        }
341    }
342
343    #[test]
344    fn test_cstring_conversion_special_chars() {
345        unsafe {
346            let path = CString::new("/api/users/{id}").unwrap();
347            let path_ptr = path.as_ptr();
348
349            let converted = CStr::from_ptr(path_ptr);
350            assert_eq!(converted.to_str().unwrap(), "/api/users/{id}");
351        }
352    }
353
354    #[test]
355    fn test_json_value_parsing() {
356        unsafe {
357            let valid_json = CString::new(r#"{"key":"value"}"#).unwrap();
358            let json_ptr = valid_json.as_ptr();
359
360            let json_str = CStr::from_ptr(json_ptr).to_str().unwrap();
361            let result: Result<serde_json::Value, _> = serde_json::from_str(json_str);
362            assert!(result.is_ok());
363        }
364    }
365
366    #[test]
367    fn test_status_code_range() {
368        // Test that status codes are valid u16 values
369        let status_codes = [200, 201, 400, 404, 500, 503];
370
371        for &status in &status_codes {
372            assert!(status > 0);
373            assert!(status < 600);
374        }
375    }
376
377    #[test]
378    fn test_mock_server_handle_runtime_creation() {
379        // Test that Runtime can be created (this is what mockforge_server_new does)
380        let runtime = Runtime::new();
381        assert!(runtime.is_ok());
382    }
383
384    #[test]
385    fn test_arc_mutex_server_creation() {
386        // Test that we can create an Arc<Mutex<MockServer>>
387        let server = MockServer::default();
388        let _arc_server = Arc::new(Mutex::new(server));
389        // Just verify it compiles and doesn't panic
390    }
391
392    #[tokio::test]
393    async fn test_server_in_ffi_context() {
394        // Simulate what the FFI does: create server, use it in async context
395        let runtime = Runtime::new().unwrap();
396
397        runtime.block_on(async {
398            let result = MockServer::new().port(0).start().await;
399            // Port 0 should allow the OS to assign a port
400            if let Ok(mut server) = result {
401                let _ = server.stop().await;
402            }
403        });
404    }
405
406    #[test]
407    fn test_multiple_cstring_allocations() {
408        unsafe {
409            // Test that we can create multiple CStrings without issues
410            let strings = vec![
411                CString::new("GET").unwrap(),
412                CString::new("POST").unwrap(),
413                CString::new("/api/test").unwrap(),
414                CString::new(r#"{"test":true}"#).unwrap(),
415            ];
416
417            for s in strings {
418                let ptr = s.into_raw();
419                mockforge_free_string(ptr);
420            }
421        }
422    }
423
424    #[test]
425    fn test_error_code_conventions() {
426        // Verify our error code conventions
427        let success = 0;
428        let error = -1;
429
430        assert_eq!(success, 0);
431        assert_eq!(error, -1);
432        assert!(error < success);
433    }
434}