Skip to main content

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