View Javadoc
1   package com.guinetik.rr;
2   
3   import com.fasterxml.jackson.databind.ObjectMapper;
4   import com.guinetik.rr.api.ApiException;
5   import com.guinetik.rr.http.*;
6   import com.guinetik.rr.request.RequestSpec;
7   import com.guinetik.rr.result.ApiError;
8   import com.guinetik.rr.result.Result;
9   import org.slf4j.Logger;
10  import org.slf4j.LoggerFactory;
11  
12  import java.util.HashMap;
13  import java.util.Map;
14  import java.util.concurrent.CompletableFuture;
15  import java.util.concurrent.ConcurrentHashMap;
16  import java.util.function.BiFunction;
17  import java.util.regex.Pattern;
18  import java.util.regex.Matcher;
19  
20  /**
21   * Mock implementation of {@link RocketRest} for unit testing without actual HTTP requests.
22   *
23   * <p>This class simulates REST API interactions by returning predefined responses based on
24   * HTTP method and URL patterns. It supports regex matching, simulated network latency,
25   * invocation counting, and circuit breaker testing.
26   *
27   * <h2>Features</h2>
28   * <ul>
29   *   <li>Predefined mock responses for any HTTP method and URL pattern</li>
30   *   <li>Regex-based URL matching for flexible endpoint simulation</li>
31   *   <li>Simulated network latency for timing-sensitive tests</li>
32   *   <li>Invocation counting for verification in tests</li>
33   *   <li>Circuit breaker integration for resilience testing</li>
34   * </ul>
35   *
36   * <h2>Basic Usage</h2>
37   * <pre class="language-java"><code>
38   * // Create mock client
39   * RocketRestConfig config = RocketRestConfig.builder("https://api.example.com").build();
40   * RocketRestMock mockClient = new RocketRestMock(config);
41   *
42   * // Define mock responses
43   * mockClient.addMockResponse("GET", "/users/1", (url, body) -&gt; {
44   *     User user = new User();
45   *     user.setId(1);
46   *     user.setName("John Doe");
47   *     return user;
48   * });
49   *
50   * // Use in tests
51   * User user = mockClient.get("/users/1", User.class);
52   * assertEquals("John Doe", user.getName());
53   * </code></pre>
54   *
55   * <h2>Regex URL Matching</h2>
56   * <pre class="language-java">{@code
57   * // Match any user ID (use regex pattern like /users/[0-9]+)
58   * mockClient.addMockResponse("GET", "/users/[0-9]+", (url, body) -> {
59   *     // Extract ID from URL and return corresponding user
60   *     String id = url.substring(url.lastIndexOf('/') + 1);
61   *     User user = new User();
62   *     user.setId(Integer.parseInt(id));
63   *     return user;
64   * }, true);  // true enables regex matching
65   * }</pre>
66   *
67   * <h2>Simulating Latency</h2>
68   * <pre class="language-java"><code>
69   * // Add 500ms latency for slow endpoint testing
70   * mockClient.withLatency("/slow-endpoint.*", 500L);
71   *
72   * // Test timeout behavior
73   * Result&lt;Response, ApiError&gt; result = mockClient.fluent()
74   *     .get("/slow-endpoint", Response.class);
75   * </code></pre>
76   *
77   * <h2>Verifying Invocations</h2>
78   * <pre class="language-java"><code>
79   * // Make some calls
80   * mockClient.get("/users/1", User.class);
81   * mockClient.get("/users/1", User.class);
82   *
83   * // Verify call count
84   * assertEquals(2, mockClient.getInvocationCount("GET", "/users/1"));
85   *
86   * // Reset for next test
87   * mockClient.resetInvocationCounts();
88   * </code></pre>
89   *
90   * @author guinetik &lt;guinetik@gmail.com&gt;
91   * @see RocketRest
92   * @see com.guinetik.rr.http.MockRocketClient
93   * @since 1.0.0
94   */
95  public class RocketRestMock extends RocketRest {
96      private final Logger logger = LoggerFactory.getLogger(this.getClass());
97      private final ObjectMapper objectMapper = new ObjectMapper();
98  
99      // Map to store mock responses: key is "METHOD:url", value is response producer
100     private final Map<String, MockResponseDefinition> mockResponses = new HashMap<>();
101     
102     // Map to track invocation counts: key is "METHOD:url", value is count
103     private final ConcurrentHashMap<String, Integer> invocationCounts = new ConcurrentHashMap<>();
104     
105     // Map to store latency settings: key is url pattern regex, value is latency in ms
106     private final Map<Pattern, Long> latencySettings = new HashMap<>();
107 
108     // The mock client implementation
109     private final MockRocketClient mockClient;
110 
111     /**
112      * Creates a new mock client instance.
113      *
114      * @param config the configuration for the REST client
115      */
116     public RocketRestMock(RocketRestConfig config) {
117         super(config);
118         
119         // Create the base mock client
120         this.mockClient = new MockRocketClient();
121         
122         // If circuit breaker is enabled, wrap the mock client with CircuitBreakerClient
123         if (config.getDefaultOptions().getBoolean(HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_ENABLED, false)) {
124             int failureThreshold = config.getDefaultOptions().getInt(
125                 HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_FAILURE_THRESHOLD,
126                 HttpConstants.CircuitBreaker.DEFAULT_FAILURE_THRESHOLD
127             );
128             long resetTimeoutMs = config.getDefaultOptions().getLong(
129                 HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_RESET_TIMEOUT_MS,
130                 HttpConstants.CircuitBreaker.DEFAULT_RESET_TIMEOUT_MS
131             );
132             
133             // Create a circuit breaker client with the mock client
134             CircuitBreakerClient circuitBreakerClient;
135             
136             // Set the circuit breaker policy if specified
137             String policyStr = config.getDefaultOptions().getString(
138                 HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_FAILURE_POLICY,
139                 null
140             );
141             if (HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_POLICY_SERVER_ONLY.equals(policyStr)) {
142                 circuitBreakerClient = new CircuitBreakerClient(
143                     mockClient,
144                     failureThreshold,
145                     resetTimeoutMs,
146                     HttpConstants.CircuitBreaker.DEFAULT_FAILURE_DECAY_TIME_MS,
147                     CircuitBreakerClient.FailurePolicy.SERVER_ERRORS_ONLY,
148                     null
149                 );
150             } else {
151                 circuitBreakerClient = new CircuitBreakerClient(
152                     mockClient,
153                     failureThreshold,
154                     resetTimeoutMs
155                 );
156             }
157             
158             // Set the circuit breaker client as the delegate
159             this.mockClient.setDelegate(circuitBreakerClient);
160         }
161     }
162 
163     /**
164      * Mock response definition class that holds both the response producer and whether 
165      * the URL pattern should be treated as a regex pattern.
166      */
167     private static class MockResponseDefinition {
168         final BiFunction<String, Object, Object> responseProducer;
169         final boolean isRegexPattern;
170         final Pattern compiledPattern;
171         
172         MockResponseDefinition(BiFunction<String, Object, Object> responseProducer, boolean isRegexPattern, String pattern) {
173             this.responseProducer = responseProducer;
174             this.isRegexPattern = isRegexPattern;
175             this.compiledPattern = isRegexPattern ? Pattern.compile(pattern) : null;
176         }
177     }
178 
179     /**
180      * Adds a mock response for a specific HTTP method and exact URL match.
181      *
182      * @param method           HTTP method (GET, POST, PUT, DELETE)
183      * @param urlPattern       URL pattern to match exactly
184      * @param responseProducer Function that takes (url, requestBody) and returns a response object
185      */
186     public void addMockResponse(String method, String urlPattern,
187                                 BiFunction<String, Object, Object> responseProducer) {
188         addMockResponse(method, urlPattern, responseProducer, false);
189     }
190 
191     /**
192      * Adds a mock response with the option to use regex pattern matching for URLs.
193      *
194      * @param method           HTTP method (GET, POST, PUT, DELETE)
195      * @param urlPattern       URL pattern (can be a regex pattern if isRegexPattern is true)
196      * @param responseProducer Function that takes (url, requestBody) and returns a response object
197      * @param isRegexPattern   If true, the urlPattern will be treated as a regex pattern
198      */
199     public void addMockResponse(String method, String urlPattern,
200                                 BiFunction<String, Object, Object> responseProducer,
201                                 boolean isRegexPattern) {
202         String key = method + ":" + urlPattern;
203         mockResponses.put(key, new MockResponseDefinition(responseProducer, isRegexPattern, urlPattern));
204         logger.debug("Added mock response for {} {} (regex: {})", method, urlPattern, isRegexPattern);
205     }
206 
207     /**
208      * Finds a matching mock response producer for the given method and URL.
209      * Checks for exact matches first, then tries regex pattern matching.
210      */
211     private BiFunction<String, Object, Object> findMatchingResponse(String method, String url) {
212         // Build the key for direct lookup
213         String exactKey = method + ":" + url;
214         
215         // Try direct mapping first for performance
216         MockResponseDefinition exactMatch = mockResponses.get(exactKey);
217         if (exactMatch != null) {
218             // Track the invocation
219             trackInvocation(method, url);
220             return exactMatch.responseProducer;
221         }
222         
223         // If no direct match, try regex matches
224         for (Map.Entry<String, MockResponseDefinition> entry : mockResponses.entrySet()) {
225             String key = entry.getKey();
226             MockResponseDefinition def = entry.getValue();
227             
228             // Skip if not a regex pattern or if the method doesn't match
229             if (!def.isRegexPattern || !key.startsWith(method + ":")) {
230                 continue;
231             }
232             
233             // Check if the URL matches the regex pattern
234             Matcher matcher = def.compiledPattern.matcher(url);
235             if (matcher.matches()) {
236                 // Track the invocation using the pattern as the key
237                 trackInvocation(method, key.substring(method.length() + 1));
238                 return def.responseProducer;
239             }
240         }
241         
242         return null;
243     }
244     
245     /**
246      * Tracks an invocation of an endpoint for testing verification.
247      */
248     private void trackInvocation(String method, String urlPattern) {
249         String key = method + ":" + urlPattern;
250         invocationCounts.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
251     }
252     
253     /**
254      * Gets the number of times an endpoint was called.
255      * 
256      * @param method HTTP method (GET, POST, PUT, DELETE)
257      * @param urlPattern URL pattern or exact URL
258      * @return The number of invocations, or 0 if never called
259      */
260     public int getInvocationCount(String method, String urlPattern) {
261         String key = method + ":" + urlPattern;
262         return invocationCounts.getOrDefault(key, 0);
263     }
264     
265     /**
266      * Resets all invocation counters.
267      */
268     public void resetInvocationCounts() {
269         invocationCounts.clear();
270     }
271     
272     /**
273      * Adds simulated network latency for a specific URL pattern.
274      * 
275      * @param urlPatternRegex Regex pattern for matching URLs
276      * @param latencyMs Delay in milliseconds
277      */
278     public void withLatency(String urlPatternRegex, long latencyMs) {
279         latencySettings.put(Pattern.compile(urlPatternRegex), latencyMs);
280         logger.debug("Added latency of {}ms for URL pattern: {}", latencyMs, urlPatternRegex);
281     }
282     
283     /**
284      * Simulates network latency if configured for the given URL.
285      */
286     private void simulateLatency(String url) {
287         for (Map.Entry<Pattern, Long> entry : latencySettings.entrySet()) {
288             if (entry.getKey().matcher(url).matches()) {
289                 long latency = entry.getValue();
290                 logger.debug("Simulating latency of {}ms for URL: {}", latency, url);
291                 try {
292                     Thread.sleep(latency);
293                 } catch (InterruptedException e) {
294                     Thread.currentThread().interrupt();
295                 }
296                 return; // Only apply the first matching latency
297             }
298         }
299     }
300 
301     @Override
302     public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) {
303         // Simulate network latency if configured
304         simulateLatency(requestSpec.getEndpoint());
305         
306         // Execute using the mock client (which may be wrapped with CircuitBreakerClient)
307         return mockClient.execute(requestSpec);
308     }
309 
310     /**
311      * Helper method to execute a request and wrap the result in a Result object.
312      */
313     private <Req, Res> Result<Res, ApiError> executeWithResult(RequestSpec<Req, Res> requestSpec) {
314         try {
315             Res result = execute(requestSpec);
316             return Result.success(result);
317         } catch (CircuitBreakerOpenException e) {
318             return Result.failure(ApiError.circuitOpenError(e.getMessage()));
319         } catch (RocketRestException e) {
320             return Result.failure(ApiError.httpError(e.getMessage(), e.getStatusCode(), e.getResponseBody()));
321         } catch (Exception e) {
322             return Result.failure(ApiError.networkError(e.getMessage()));
323         }
324     }
325     
326     /**
327      * Provides a mock implementation for sync API calls.
328      */
329     @Override
330     public SyncApi sync() {
331         return new MockSyncApi();
332     }
333     
334     /**
335      * Provides a mock implementation for async API calls.
336      */
337     @Override
338     public AsyncApi async() {
339         return new MockAsyncApi();
340     }
341     
342     /**
343      * Provides a mock implementation for fluent API calls.
344      */
345     public FluentApi fluent() {
346         return new MockFluentApi();
347     }
348 
349     public <Req, Res> CompletableFuture<Res> executeAsync(RequestSpec<Req, Res> requestSpec) {
350         return CompletableFuture.supplyAsync(() -> execute(requestSpec));
351     }
352 
353     /**
354      * Mock implementation of RocketClient that handles the actual request execution.
355      */
356     private class MockRocketClient implements RocketClient {
357         private RocketClient delegate;
358         private boolean isExecuting = false;
359 
360         public void setDelegate(RocketClient delegate) {
361             this.delegate = delegate;
362         }
363 
364         @Override
365         public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) throws RocketRestException {
366             // If we have a delegate, and we're not yet executing, use it
367             if (delegate != null && !isExecuting) {
368                 isExecuting = true;
369                 try {
370                     return delegate.execute(requestSpec);
371                 } finally {
372                     isExecuting = false;
373                 }
374             }
375 
376             // Otherwise handle the request directly
377             BiFunction<String, Object, Object> responseProducer =
378                     findMatchingResponse(requestSpec.getMethod(), requestSpec.getEndpoint());
379 
380             if (responseProducer != null) {
381                 try {
382                     Object response = responseProducer.apply(
383                             requestSpec.getEndpoint(),
384                             requestSpec.getBody()
385                     );
386 
387                     // Convert the response to the expected type
388                     if (requestSpec.getResponseType().isInstance(response)) {
389                         @SuppressWarnings("unchecked")
390                         Res typedResponse = (Res) response;
391                         return typedResponse;
392                     } else if (response != null) {
393                         // Try to convert using ObjectMapper if types don't match directly
394                         return objectMapper.convertValue(response, requestSpec.getResponseType());
395                     }
396 
397                     throw new ApiException("Mock response could not be converted to required type: "
398                             + requestSpec.getResponseType().getName());
399                 } catch (Exception e) {
400                     if (e instanceof ApiException) {
401                         ApiException apiEx = (ApiException) e;
402                         if (apiEx.getStatusCode() > 0) {
403                             throw new RocketRestException(
404                                 apiEx.getMessage(),
405                                 apiEx.getStatusCode(),
406                                 apiEx.getResponseBody()
407                             );
408                         }
409                     }
410                     throw new RocketRestException("Failed to process mock response", e);
411                 }
412             }
413 
414             logger.warn("No mock response found for {} : {}", requestSpec.getMethod(), requestSpec.getEndpoint());
415             throw new RocketRestException("No mock response configured for "
416                     + requestSpec.getMethod() + ":" + requestSpec.getEndpoint());
417         }
418 
419         @Override
420         public void configureSsl(javax.net.ssl.SSLContext sslContext) {
421             // No-op for a mock client
422         }
423 
424         @Override
425         public void setBaseUrl(String baseUrl) {
426 
427         }
428     }
429     
430     // Inner class implementations of the API interfaces
431     
432     /**
433      * Implementation of SyncApi that delegates to the underlying DefaultApiClient.
434      */
435     private class MockSyncApi implements SyncApi {
436         @Override
437         public <T> T get(String endpoint, Class<T> responseType) {
438             return execute(createGetRequest(endpoint, responseType));
439         }
440         
441         @Override
442         public <T> T get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
443             return execute(createGetRequest(endpoint, responseType, queryParams));
444         }
445         
446         @Override
447         public <Res> Res post(String endpoint, Class<Res> responseType) {
448             return execute(createPostRequest(endpoint, responseType));
449         }
450         
451         @Override
452         public <Req, Res> Res post(String endpoint, Req body, Class<Res> responseType) {
453             return execute(createPostRequest(endpoint, body, responseType));
454         }
455         
456         @Override
457         public <Res> Res put(String endpoint, Class<Res> responseType) {
458             return execute(createPutRequest(endpoint, responseType));
459         }
460         
461         @Override
462         public <Req, Res> Res put(String endpoint, Req body, Class<Res> responseType) {
463             return execute(createPutRequest(endpoint, body, responseType));
464         }
465         
466         @Override
467         public <T> T delete(String endpoint, Class<T> responseType) {
468             return execute(createDeleteRequest(endpoint, responseType));
469         }
470         
471         @Override
472         public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) {
473             return RocketRestMock.this.execute(requestSpec);
474         }
475     }
476     
477     /**
478      * Implementation of AsyncApi that delegates to the underlying AsyncApiClient.
479      */
480     private class MockAsyncApi implements AsyncApi {
481         @Override
482         public <T> CompletableFuture<T> get(String endpoint, Class<T> responseType) {
483             return executeAsync(createGetRequest(endpoint, responseType));
484         }
485         
486         @Override
487         public <T> CompletableFuture<T> get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
488             return executeAsync(createGetRequest(endpoint, responseType, queryParams));
489         }
490         
491         @Override
492         public <Res> CompletableFuture<Res> post(String endpoint, Class<Res> responseType) {
493             return executeAsync(createPostRequest(endpoint, responseType));
494         }
495         
496         @Override
497         public <Req, Res> CompletableFuture<Res> post(String endpoint, Req body, Class<Res> responseType) {
498             return executeAsync(createPostRequest(endpoint, body, responseType));
499         }
500         
501         @Override
502         public <Res> CompletableFuture<Res> put(String endpoint, Class<Res> responseType) {
503             return executeAsync(createPutRequest(endpoint, responseType));
504         }
505         
506         @Override
507         public <Req, Res> CompletableFuture<Res> put(String endpoint, Req body, Class<Res> responseType) {
508             return executeAsync(createPutRequest(endpoint, body, responseType));
509         }
510         
511         @Override
512         public <T> CompletableFuture<T> delete(String endpoint, Class<T> responseType) {
513             return executeAsync(createDeleteRequest(endpoint, responseType));
514         }
515         
516         @Override
517         public <Req, Res> CompletableFuture<Res> execute(RequestSpec<Req, Res> requestSpec) {
518             return executeAsync(requestSpec);
519         }
520         
521         @Override
522         public void shutdown() {
523             // No-op for mock
524         }
525     }
526     
527     /**
528      * Implementation of FluentApi that delegates to the underlying FluentApiClient.
529      */
530     private class MockFluentApi implements FluentApi {
531         @Override
532         public <T> Result<T, ApiError> get(String endpoint, Class<T> responseType) {
533             return RocketRestMock.this.executeWithResult(createGetRequest(endpoint, responseType));
534         }
535         
536         @Override
537         public <T> Result<T, ApiError> get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
538             return RocketRestMock.this.executeWithResult(createGetRequest(endpoint, responseType, queryParams));
539         }
540         
541         @Override
542         public <Res> Result<Res, ApiError> post(String endpoint, Class<Res> responseType) {
543             return RocketRestMock.this.executeWithResult(createPostRequest(endpoint, responseType));
544         }
545         
546         @Override
547         public <Req, Res> Result<Res, ApiError> post(String endpoint, Req body, Class<Res> responseType) {
548             return RocketRestMock.this.executeWithResult(createPostRequest(endpoint, body, responseType));
549         }
550         
551         @Override
552         public <Res> Result<Res, ApiError> put(String endpoint, Class<Res> responseType) {
553             return RocketRestMock.this.executeWithResult(createPutRequest(endpoint, responseType));
554         }
555         
556         @Override
557         public <Req, Res> Result<Res, ApiError> put(String endpoint, Req body, Class<Res> responseType) {
558             return RocketRestMock.this.executeWithResult(createPutRequest(endpoint, body, responseType));
559         }
560         
561         @Override
562         public <T> Result<T, ApiError> delete(String endpoint, Class<T> responseType) {
563             return RocketRestMock.this.executeWithResult(createDeleteRequest(endpoint, responseType));
564         }
565         
566         @Override
567         public <Req, Res> Result<Res, ApiError> execute(RequestSpec<Req, Res> requestSpec) {
568             return RocketRestMock.this.executeWithResult(requestSpec);
569         }
570     }
571     
572     // Helper methods for creating request specs, reusing parent methods via reflection
573     private <T> RequestSpec<Void, T> createGetRequest(String endpoint, Class<T> responseType) {
574         try {
575             java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createGetRequest", String.class, Class.class);
576             method.setAccessible(true);
577             @SuppressWarnings("unchecked")
578             RequestSpec<Void, T> result = (RequestSpec<Void, T>) method.invoke(this, endpoint, responseType);
579             return result;
580         } catch (Exception e) {
581             throw new ApiException("Failed to create GET request", e);
582         }
583     }
584     
585     private <T> RequestSpec<Void, T> createGetRequest(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
586         try {
587             java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createGetRequest", String.class, Class.class, Map.class);
588             method.setAccessible(true);
589             @SuppressWarnings("unchecked")
590             RequestSpec<Void, T> result = (RequestSpec<Void, T>) method.invoke(this, endpoint, responseType, queryParams);
591             return result;
592         } catch (Exception e) {
593             throw new ApiException("Failed to create GET request with params", e);
594         }
595     }
596     
597     private <Res> RequestSpec<Void, Res> createPostRequest(String endpoint, Class<Res> responseType) {
598         try {
599             java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createPostRequest", String.class, Class.class);
600             method.setAccessible(true);
601             @SuppressWarnings("unchecked")
602             RequestSpec<Void, Res> result = (RequestSpec<Void, Res>) method.invoke(this, endpoint, responseType);
603             return result;
604         } catch (Exception e) {
605             throw new ApiException("Failed to create POST request", e);
606         }
607     }
608     
609     private <Req, Res> RequestSpec<Req, Res> createPostRequest(String endpoint, Req body, Class<Res> responseType) {
610         try {
611             java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createPostRequest", String.class, Object.class, Class.class);
612             method.setAccessible(true);
613             @SuppressWarnings("unchecked")
614             RequestSpec<Req, Res> result = (RequestSpec<Req, Res>) method.invoke(this, endpoint, body, responseType);
615             return result;
616         } catch (Exception e) {
617             throw new ApiException("Failed to create POST request with body", e);
618         }
619     }
620     
621     private <Res> RequestSpec<Void, Res> createPutRequest(String endpoint, Class<Res> responseType) {
622         try {
623             java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createPutRequest", String.class, Class.class);
624             method.setAccessible(true);
625             @SuppressWarnings("unchecked")
626             RequestSpec<Void, Res> result = (RequestSpec<Void, Res>) method.invoke(this, endpoint, responseType);
627             return result;
628         } catch (Exception e) {
629             throw new ApiException("Failed to create PUT request", e);
630         }
631     }
632     
633     private <Req, Res> RequestSpec<Req, Res> createPutRequest(String endpoint, Req body, Class<Res> responseType) {
634         try {
635             java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createPutRequest", String.class, Object.class, Class.class);
636             method.setAccessible(true);
637             @SuppressWarnings("unchecked")
638             RequestSpec<Req, Res> result = (RequestSpec<Req, Res>) method.invoke(this, endpoint, body, responseType);
639             return result;
640         } catch (Exception e) {
641             throw new ApiException("Failed to create PUT request with body", e);
642         }
643     }
644     
645     private <T> RequestSpec<Void, T> createDeleteRequest(String endpoint, Class<T> responseType) {
646         try {
647             java.lang.reflect.Method method = RocketRest.class.getDeclaredMethod("createDeleteRequest", String.class, Class.class);
648             method.setAccessible(true);
649             @SuppressWarnings("unchecked")
650             RequestSpec<Void, T> result = (RequestSpec<Void, T>) method.invoke(this, endpoint, responseType);
651             return result;
652         } catch (Exception e) {
653             throw new ApiException("Failed to create DELETE request", e);
654         }
655     }
656 }