View Javadoc
1   package com.guinetik.rr.result;
2   
3   /**
4    * Represents an API error in the {@link Result} pattern.
5    *
6    * <p>This class encapsulates information about API errors without using exceptions,
7    * providing a functional approach to error handling. It includes the error message,
8    * HTTP status code (when applicable), response body, and error type classification.
9    *
10   * <h2>Error Types</h2>
11   * <ul>
12   *   <li>{@link ErrorType#HTTP_ERROR} - Server returned an error status code</li>
13   *   <li>{@link ErrorType#NETWORK_ERROR} - Connection or network failure</li>
14   *   <li>{@link ErrorType#PARSE_ERROR} - Failed to parse response body</li>
15   *   <li>{@link ErrorType#AUTH_ERROR} - Authentication/authorization failure</li>
16   *   <li>{@link ErrorType#CONFIG_ERROR} - Client misconfiguration</li>
17   *   <li>{@link ErrorType#CIRCUIT_OPEN} - Circuit breaker is open</li>
18   * </ul>
19   *
20   * <h2>Creating Errors</h2>
21   * <pre class="language-java"><code>
22   * // HTTP error with status code
23   * ApiError notFound = ApiError.httpError("User not found", 404, responseBody);
24   *
25   * // Network error
26   * ApiError networkError = ApiError.networkError("Connection refused");
27   *
28   * // Parse error
29   * ApiError parseError = ApiError.parseError("Invalid JSON", responseBody);
30   * </code></pre>
31   *
32   * <h2>Handling Errors</h2>
33   * <pre class="language-java"><code>
34   * Result&lt;User, ApiError&gt; result = client.fluent().get("/users/1", User.class);
35   *
36   * result.ifFailure(error -&gt; {
37   *     if (error.isType(ErrorType.HTTP_ERROR)) {
38   *         if (error.hasStatusCode(404)) {
39   *             System.out.println("User not found");
40   *         } else if (error.hasStatusCode(500)) {
41   *             System.out.println("Server error");
42   *         }
43   *     } else if (error.isType(ErrorType.NETWORK_ERROR)) {
44   *         System.out.println("Check your connection");
45   *     }
46   * });
47   * </code></pre>
48   *
49   * @author guinetik &lt;guinetik@gmail.com&gt;
50   * @see Result
51   * @see com.guinetik.rr.api.FluentApiClient
52   * @since 1.0.0
53   */
54  public class ApiError {
55      private final String message;
56      private final int statusCode;
57      private final String responseBody;
58      private final ErrorType errorType;
59  
60      /**
61       * Enum representing different types of errors that can occur.
62       */
63      public enum ErrorType {
64          /** HTTP error from server */
65          HTTP_ERROR,
66          
67          /** Network connectivity error */
68          NETWORK_ERROR,
69          
70          /** Error parsing response */
71          PARSE_ERROR,
72          
73          /** Authentication/authorization error */
74          AUTH_ERROR,
75          
76          /** Client configuration error */
77          CONFIG_ERROR,
78          
79          /** Circuit breaker is open */
80          CIRCUIT_OPEN,
81          
82          /** Unknown error */
83          UNKNOWN
84      }
85  
86      /**
87       * Constructs a new ApiError with the specified parameters.
88       * 
89       * @param message Error message
90       * @param statusCode HTTP status code (may be 0 for non-HTTP errors)
91       * @param responseBody Response body or null if not available
92       * @param errorType Type of error that occurred
93       */
94      public ApiError(String message, int statusCode, String responseBody, ErrorType errorType) {
95          this.message = message;
96          this.statusCode = statusCode;
97          this.responseBody = responseBody;
98          this.errorType = errorType;
99      }
100 
101     /**
102      * Constructs an HTTP error with status code.
103      * 
104      * @param message Error message
105      * @param statusCode HTTP status code
106      * @param responseBody Response body
107      * @return A new ApiError representing an HTTP error
108      */
109     public static ApiError httpError(String message, int statusCode, String responseBody) {
110         return new ApiError(message, statusCode, responseBody, ErrorType.HTTP_ERROR);
111     }
112 
113     /**
114      * Constructs a network error.
115      * 
116      * @param message Error message
117      * @return A new ApiError representing a network error
118      */
119     public static ApiError networkError(String message) {
120         return new ApiError(message, 0, null, ErrorType.NETWORK_ERROR);
121     }
122 
123     /**
124      * Constructs a parse error.
125      * 
126      * @param message Error message
127      * @param responseBody The response that couldn't be parsed
128      * @return A new ApiError representing a parse error
129      */
130     public static ApiError parseError(String message, String responseBody) {
131         return new ApiError(message, 0, responseBody, ErrorType.PARSE_ERROR);
132     }
133 
134     /**
135      * Constructs an authentication error.
136      * 
137      * @param message Error message
138      * @param statusCode HTTP status code (typically 401)
139      * @param responseBody Response body
140      * @return A new ApiError representing an authentication error
141      */
142     public static ApiError authError(String message, int statusCode, String responseBody) {
143         return new ApiError(message, statusCode, responseBody, ErrorType.AUTH_ERROR);
144     }
145 
146     /**
147      * Constructs a configuration error.
148      * 
149      * @param message Error message
150      * @return A new ApiError representing a configuration error
151      */
152     public static ApiError configError(String message) {
153         return new ApiError(message, 0, null, ErrorType.CONFIG_ERROR);
154     }
155 
156     /**
157      * Constructs a circuit breaker open error.
158      * 
159      * @param message Error message
160      * @return A new ApiError representing a circuit breaker open error
161      */
162     public static ApiError circuitOpenError(String message) {
163         return new ApiError(message, 0, null, ErrorType.CIRCUIT_OPEN);
164     }
165 
166     /**
167      * Gets the error message.
168      * 
169      * @return The error message
170      */
171     public String getMessage() {
172         return message;
173     }
174 
175     /**
176      * Gets the HTTP status code.
177      * 
178      * @return The HTTP status code, or 0 if not applicable
179      */
180     public int getStatusCode() {
181         return statusCode;
182     }
183 
184     /**
185      * Gets the response body.
186      * 
187      * @return The response body, or null if not available
188      */
189     public String getResponseBody() {
190         return responseBody;
191     }
192 
193     /**
194      * Gets the error type.
195      * 
196      * @return The error type
197      */
198     public ErrorType getErrorType() {
199         return errorType;
200     }
201 
202     /**
203      * Checks if this error is of the specified type.
204      * 
205      * @param type The error type to check
206      * @return true if this error is of the specified type, false otherwise
207      */
208     public boolean isType(ErrorType type) {
209         return this.errorType == type;
210     }
211 
212     /**
213      * Checks if this error is an HTTP error with the specified status code.
214      * 
215      * @param statusCode The HTTP status code to check
216      * @return true if this error is an HTTP error with the specified status code
217      */
218     public boolean hasStatusCode(int statusCode) {
219         return this.statusCode == statusCode;
220     }
221 
222     @Override
223     public String toString() {
224         StringBuilder sb = new StringBuilder();
225         sb.append(errorType).append(": ").append(message);
226         
227         if (statusCode > 0) {
228             sb.append(" (Status: ").append(statusCode).append(")");
229         }
230         
231         return sb.toString();
232     }
233 }