View Javadoc
1   package com.guinetik.rr.api;
2   
3   import com.guinetik.rr.RocketRestConfig;
4   import com.guinetik.rr.http.FluentHttpClient;
5   import com.guinetik.rr.http.RocketClientFactory;
6   import com.guinetik.rr.request.RequestBuilder;
7   import com.guinetik.rr.request.RequestSpec;
8   import com.guinetik.rr.result.ApiError;
9   import com.guinetik.rr.result.Result;
10  
11  import java.util.Map;
12  
13  /**
14   * A fluent API client that uses the {@link Result} pattern instead of exceptions.
15   *
16   * <p>This client provides a functional, declarative approach to API calls where errors
17   * are returned as values rather than thrown as exceptions. This makes error handling
18   * explicit and composable.
19   *
20   * <h2>Basic Usage</h2>
21   * <pre class="language-java"><code>
22   * FluentApiClient client = new FluentApiClient("https://api.example.com", config);
23   *
24   * Result&lt;User, ApiError&gt; result = client.get("/users/1", User.class);
25   *
26   * // Handle success and failure explicitly
27   * result.match(
28   *     user -&gt; System.out.println("Found: " + user.getName()),
29   *     error -&gt; System.err.println("Error " + error.getStatusCode() + ": " + error.getMessage())
30   * );
31   * </code></pre>
32   *
33   * <h2>Chaining Operations</h2>
34   * <pre class="language-java"><code>
35   * String userName = client.get("/users/1", User.class)
36   *     .map(User::getName)
37   *     .map(String::toUpperCase)
38   *     .getOrElse("Unknown");
39   * </code></pre>
40   *
41   * <h2>POST with Body</h2>
42   * <pre class="language-java"><code>
43   * CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
44   *
45   * Result&lt;User, ApiError&gt; result = client.post("/users", request, User.class);
46   *
47   * if (result.isSuccess()) {
48   *     User created = result.getValue();
49   *     System.out.println("Created user with ID: " + created.getId());
50   * }
51   * </code></pre>
52   *
53   * @author guinetik &lt;guinetik@gmail.com&gt;
54   * @see Result
55   * @see ApiError
56   * @see AbstractApiClient
57   * @since 1.0.0
58   */
59  public class FluentApiClient extends AbstractApiClient {
60  
61      private final FluentHttpClient fluentClient;
62      
63      /**
64       * Creates a new FluentApiClient with the specified base URL and configuration.
65       *
66       * @param baseUrl The base URL for API requests
67       * @param config  The RocketRest configuration
68       */
69      public FluentApiClient(String baseUrl, RocketRestConfig config) {
70          super(baseUrl, config, createFluentHttpClient(baseUrl, config));
71          this.fluentClient = (FluentHttpClient) httpClient;
72      }
73      
74      /**
75       * Creates a new FluentApiClient with the specified base URL, configuration, and a
76       * pre-configured FluentHttpClient instance.
77       *
78       * @param baseUrl      The base URL for API requests
79       * @param config       The RocketRest configuration
80       * @param fluentClient A pre-configured FluentHttpClient instance
81       */
82      public FluentApiClient(String baseUrl, RocketRestConfig config, FluentHttpClient fluentClient) {
83          super(baseUrl, config, fluentClient);
84          this.fluentClient = fluentClient;
85      }
86      
87      /**
88       * Executes a request and returns a Result object instead of throwing exceptions.
89       *
90       * @param <Req>       The type of the request
91       * @param <Res>       The type of the response
92       * @param requestSpec The request specification
93       * @return A Result containing either the response or an ApiError
94       */
95      public <Req, Res> Result<Res, ApiError> executeWithResult(RequestSpec<Req, Res> requestSpec) {
96          // Apply any request transformations from the abstract client
97          refreshToken(); // Refresh token if needed
98          logRequest(requestSpec); // Log the request if enabled
99          
100         // Delegate to the fluent client
101         return fluentClient.executeWithResult(requestSpec);
102     }
103     
104     /**
105      * Performs a GET request using the Result pattern.
106      *
107      * @param <Res>          The type of the response
108      * @param endpoint       The API endpoint
109      * @param responseType   The class of the response type
110      * @return A Result containing either the response or an ApiError
111      */
112     public <Res> Result<Res, ApiError> get(String endpoint, Class<Res> responseType) {
113         return this.<Res>get(endpoint, null, responseType);
114     }
115     
116     /**
117      * Performs a GET request with headers using the Result pattern.
118      *
119      * @param <Res>          The type of the response
120      * @param endpoint       The API endpoint
121      * @param headers        The request headers
122      * @param responseType   The class of the response type
123      * @return A Result containing either the response or an ApiError
124      */
125     public <Res> Result<Res, ApiError> get(String endpoint, Map<String, String> headers, Class<Res> responseType) {
126         RequestSpec<Void, Res> request = RequestBuilder.<Void, Res>get(endpoint)
127                 .headers(createHeaders(headers))
128                 .responseType(responseType)
129                 .build();
130                 
131         return this.<Void, Res>executeWithResult(request);
132     }
133     
134     /**
135      * Performs a POST request using the Result pattern.
136      *
137      * @param <Req>          The type of the request body
138      * @param <Res>          The type of the response
139      * @param endpoint       The API endpoint
140      * @param body           The request body
141      * @param responseType   The class of the response type
142      * @return A Result containing either the response or an ApiError
143      */
144     public <Req, Res> Result<Res, ApiError> post(String endpoint, Req body, Class<Res> responseType) {
145         return this.<Req, Res>post(endpoint, body, null, responseType);
146     }
147     
148     /**
149      * Performs a POST request with headers using the Result pattern.
150      *
151      * @param <Req>          The type of the request body
152      * @param <Res>          The type of the response
153      * @param endpoint       The API endpoint
154      * @param body           The request body
155      * @param headers        The request headers
156      * @param responseType   The class of the response type
157      * @return A Result containing either the response or an ApiError
158      */
159     public <Req, Res> Result<Res, ApiError> post(String endpoint, Req body, Map<String, String> headers, Class<Res> responseType) {
160         RequestSpec<Req, Res> request = RequestBuilder.<Req, Res>post(endpoint)
161                 .headers(createHeaders(headers))
162                 .body(body)
163                 .responseType(responseType)
164                 .build();
165                 
166         return this.<Req, Res>executeWithResult(request);
167     }
168     
169     /**
170      * Performs a PUT request using the Result pattern.
171      *
172      * @param <Req>          The type of the request body
173      * @param <Res>          The type of the response
174      * @param endpoint       The API endpoint
175      * @param body           The request body
176      * @param responseType   The class of the response type
177      * @return A Result containing either the response or an ApiError
178      */
179     public <Req, Res> Result<Res, ApiError> put(String endpoint, Req body, Class<Res> responseType) {
180         return this.<Req, Res>put(endpoint, body, null, responseType);
181     }
182     
183     /**
184      * Performs a PUT request with headers using the Result pattern.
185      *
186      * @param <Req>          The type of the request body
187      * @param <Res>          The type of the response
188      * @param endpoint       The API endpoint
189      * @param body           The request body
190      * @param headers        The request headers
191      * @param responseType   The class of the response type
192      * @return A Result containing either the response or an ApiError
193      */
194     public <Req, Res> Result<Res, ApiError> put(String endpoint, Req body, Map<String, String> headers, Class<Res> responseType) {
195         RequestSpec<Req, Res> request = RequestBuilder.<Req, Res>put(endpoint)
196                 .headers(createHeaders(headers))
197                 .body(body)
198                 .responseType(responseType)
199                 .build();
200                 
201         return this.<Req, Res>executeWithResult(request);
202     }
203     
204     /**
205      * Performs a DELETE request using the Result pattern.
206      *
207      * @param <Res>          The type of the response
208      * @param endpoint       The API endpoint
209      * @param responseType   The class of the response type
210      * @return A Result containing either the response or an ApiError
211      */
212     public <Res> Result<Res, ApiError> delete(String endpoint, Class<Res> responseType) {
213         return this.<Res>delete(endpoint, null, responseType);
214     }
215     
216     /**
217      * Performs a DELETE request with headers using the Result pattern.
218      *
219      * @param <Res>          The type of the response
220      * @param endpoint       The API endpoint
221      * @param headers        The request headers
222      * @param responseType   The class of the response type
223      * @return A Result containing either the response or an ApiError
224      */
225     public <Res> Result<Res, ApiError> delete(String endpoint, Map<String, String> headers, Class<Res> responseType) {
226         RequestSpec<Void, Res> request = RequestBuilder.<Void, Res>delete(endpoint)
227                 .headers(createHeaders(headers))
228                 .responseType(responseType)
229                 .build();
230                 
231         return this.<Void, Res>executeWithResult(request);
232     }
233     
234     /**
235      * Shutdown the client and release resources.
236      */
237     public void shutdown() {
238         // Currently just a placeholder for API compatibility with AsyncApiClient
239         // FluentHttpClient doesn't require explicit shutdown
240     }
241     
242     /**
243      * Creates a FluentHttpClient with the appropriate options from config.
244      *
245      * @param baseUrl The base URL for API requests
246      * @param config  The RocketRest configuration
247      * @return A new FluentHttpClient
248      */
249     private static FluentHttpClient createFluentHttpClient(String baseUrl, RocketRestConfig config) {
250         return RocketClientFactory.fromConfig(config)
251                 .buildFluent();
252     }
253 }