1 package com.guinetik.rr.result;
2
3 import java.util.NoSuchElementException;
4 import java.util.Optional;
5 import java.util.function.Consumer;
6 import java.util.function.Function;
7 import java.util.function.Supplier;
8
9 /**
10 * A container object representing either a successful result or an error.
11 *
12 * <p>Inspired by Rust's {@code Result<T, E>} and Scala's {@code Either[L, R]}, this class
13 * provides a functional approach to error handling without exceptions. It forces explicit
14 * handling of both success and failure cases, leading to more robust code.
15 *
16 * <h2>Creating Results</h2>
17 * <pre class="language-java"><code>
18 * // Success case
19 * Result<User, ApiError> success = Result.success(new User("John"));
20 *
21 * // Failure case
22 * Result<User, ApiError> failure = Result.failure(new ApiError(404, "Not found"));
23 * </code></pre>
24 *
25 * <h2>Checking and Extracting Values</h2>
26 * <pre class="language-java"><code>
27 * Result<User, ApiError> result = client.fluent().get("/users/1", User.class);
28 *
29 * if (result.isSuccess()) {
30 * User user = result.getValue();
31 * System.out.println("Found: " + user.getName());
32 * } else {
33 * ApiError error = result.getError();
34 * System.err.println("Error: " + error.getMessage());
35 * }
36 * </code></pre>
37 *
38 * <h2>Pattern Matching with match()</h2>
39 * <pre class="language-java"><code>
40 * result.match(
41 * user -> System.out.println("Success: " + user.getName()),
42 * error -> System.err.println("Failed: " + error.getMessage())
43 * );
44 * </code></pre>
45 *
46 * <h2>Functional Transformations</h2>
47 * <pre class="language-java"><code>
48 * // Transform the success value
49 * Result<String, ApiError> nameResult = result.map(user -> user.getName());
50 *
51 * // Transform the error
52 * Result<User, String> stringError = result.mapError(err -> err.getMessage());
53 *
54 * // Chain operations
55 * String name = result
56 * .map(User::getName)
57 * .map(String::toUpperCase)
58 * .getOrElse("Unknown");
59 * </code></pre>
60 *
61 * <h2>Safe Value Extraction</h2>
62 * <pre class="language-java"><code>
63 * // With default value
64 * User user = result.getOrElse(User.anonymous());
65 *
66 * // With lazy default
67 * User user = result.getOrElseGet(() -> loadDefaultUser());
68 *
69 * // Throw on failure
70 * User user = result.getOrElseThrow(() -> new UserNotFoundException());
71 *
72 * // Unwrap (throws RuntimeException on failure)
73 * User user = result.unwrap();
74 *
75 * // Convert to Optional
76 * Optional<User> optional = result.toOptional();
77 * </code></pre>
78 *
79 * @param <T> the type of the success value
80 * @param <E> the type of the error value
81 * @author guinetik <guinetik@gmail.com>
82 * @see com.guinetik.rr.RocketRest.FluentApi
83 * @see ApiError
84 * @since 1.0.0
85 */
86 public class Result<T, E> {
87
88 private final T value;
89 private final E error;
90 private final boolean isSuccess;
91
92 private Result(T value, E error, boolean isSuccess) {
93 this.value = value;
94 this.error = error;
95 this.isSuccess = isSuccess;
96 }
97
98 /**
99 * Creates a successful Result containing the given value.
100 *
101 * @param value the value
102 * @param <T> the type of the value
103 * @param <E> the type of the error
104 * @return a successful Result containing the value
105 */
106 public static <T, E> Result<T, E> success(T value) {
107 return new Result<>(value, null, true);
108 }
109
110 /**
111 * Creates a failed Result containing the given error.
112 *
113 * @param error the error
114 * @param <T> the type of the value
115 * @param <E> the type of the error
116 * @return a failed Result containing the error
117 */
118 public static <T, E> Result<T, E> failure(E error) {
119 return new Result<>(null, error, false);
120 }
121
122 /**
123 * Returns whether this Result is a success.
124 *
125 * @return true if this Result is a success, false otherwise
126 */
127 public boolean isSuccess() {
128 return isSuccess;
129 }
130
131 /**
132 * Returns whether this Result is a failure.
133 *
134 * @return true if this Result is a failure, false otherwise
135 */
136 public boolean isFailure() {
137 return !isSuccess;
138 }
139
140 /**
141 * Gets the value contained in this Result.
142 *
143 * @return the value
144 * @throws NoSuchElementException if this Result is a failure
145 */
146 public T getValue() {
147 if (!isSuccess) {
148 throw new NoSuchElementException("Cannot get value from a failure Result");
149 }
150 return value;
151 }
152
153 /**
154 * Gets the error contained in this Result.
155 *
156 * @return the error
157 * @throws NoSuchElementException if this Result is a success
158 */
159 public E getError() {
160 if (isSuccess) {
161 throw new NoSuchElementException("Cannot get error from a success Result");
162 }
163 return error;
164 }
165
166 /**
167 * Gets the value contained in this Result or the given default value if this Result is a failure.
168 *
169 * @param defaultValue the value to return if this Result is a failure
170 * @return the value if this Result is a success, otherwise the default value
171 */
172 public T getOrElse(T defaultValue) {
173 return isSuccess ? value : defaultValue;
174 }
175
176 /**
177 * Gets the value contained in this Result or the value supplied by the given Supplier if this Result is a failure.
178 *
179 * @param supplier the Supplier to provide the default value
180 * @return the value if this Result is a success, otherwise the value from the supplier
181 */
182 public T getOrElseGet(Supplier<? extends T> supplier) {
183 return isSuccess ? value : supplier.get();
184 }
185
186 /**
187 * Gets the value contained in this Result or throws the given exception if this Result is a failure.
188 *
189 * @param exceptionSupplier the Supplier to provide the exception to throw
190 * @param <X> the type of the exception to throw
191 * @return the value if this Result is a success
192 * @throws X if this Result is a failure
193 */
194 public <X extends Throwable> T getOrElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
195 if (isSuccess) {
196 return value;
197 }
198 throw exceptionSupplier.get();
199 }
200
201 /**
202 * Returns the value if this Result is a success, or unwraps the error by throwing an exception.
203 * This is similar to Rust's unwrap() method.
204 *
205 * @return the contained value
206 * @throws RuntimeException if this is a failure, with the error toString() as the message
207 */
208 public T unwrap() {
209 if (isSuccess) {
210 return value;
211 }
212 throw new RuntimeException("Unwrapped a failure Result: " + error);
213 }
214
215 /**
216 * Maps the value of this Result if it's a success, using the given mapping function.
217 *
218 * @param mapper the function to apply to the value
219 * @param <U> the type of the result of the mapping function
220 * @return a new Result with the mapped value if this Result is a success, otherwise a new Result with the same error
221 */
222 public <U> Result<U, E> map(Function<? super T, ? extends U> mapper) {
223 if (isSuccess) {
224 return Result.success(mapper.apply(value));
225 }
226 return Result.failure(error);
227 }
228
229 /**
230 * Maps the error of this Result if it's a failure, using the given mapping function.
231 *
232 * @param mapper the function to apply to the error
233 * @param <F> the type of the result of the mapping function
234 * @return a new Result with the mapped error if this Result is a failure, otherwise a new Result with the same value
235 */
236 public <F> Result<T, F> mapError(Function<? super E, ? extends F> mapper) {
237 if (isSuccess) {
238 return Result.success(value);
239 }
240 return Result.failure(mapper.apply(error));
241 }
242
243 /**
244 * Executes the given consumer if this Result is a success.
245 *
246 * @param consumer the consumer to execute
247 * @return this Result
248 */
249 public Result<T, E> ifSuccess(Consumer<? super T> consumer) {
250 if (isSuccess) {
251 consumer.accept(value);
252 }
253 return this;
254 }
255
256 /**
257 * Executes the given consumer if this Result is a failure.
258 *
259 * @param consumer the consumer to execute
260 * @return this Result
261 */
262 public Result<T, E> ifFailure(Consumer<? super E> consumer) {
263 if (!isSuccess) {
264 consumer.accept(error);
265 }
266 return this;
267 }
268
269 /**
270 * Converts this Result to an Optional containing the value if this Result is a success,
271 * or an empty Optional if this Result is a failure.
272 *
273 * @return an Optional containing the value if this Result is a success, otherwise an empty Optional
274 */
275 public Optional<T> toOptional() {
276 return isSuccess ? Optional.ofNullable(value) : Optional.empty();
277 }
278
279 /**
280 * Pattern-matches over this Result, executing one of the consumers depending on success/failure.
281 * <pre>
282 * result.match(value -> System.out.println(value), err -> log.error(err));
283 * </pre>
284 *
285 * @param successConsumer runs if this result is a success (receives the value)
286 * @param errorConsumer runs if this result is a failure (receives the error)
287 * @return this Result for fluent chaining
288 */
289 public Result<T, E> match(Consumer<? super T> successConsumer, Consumer<? super E> errorConsumer) {
290 if (isSuccess) {
291 successConsumer.accept(value);
292 } else {
293 errorConsumer.accept(error);
294 }
295 return this;
296 }
297
298 @Override
299 public String toString() {
300 if (isSuccess) {
301 return "Success[" + value + "]";
302 }
303 return "Failure[" + error + "]";
304 }
305 }