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<User, ApiError> result = client.get("/users/1", User.class);
25 *
26 * // Handle success and failure explicitly
27 * result.match(
28 * user -> System.out.println("Found: " + user.getName()),
29 * error -> 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<User, ApiError> 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 <guinetik@gmail.com>
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 }