• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

domix / dmx-fun / 25410820523

06 May 2026 01:00AM UTC coverage: 98.173% (+0.005%) from 98.168%
25410820523

push

github

web-flow
Merge pull request #380 from domix/feature/resource/interoperability/audit

feat(Resource): add useAsEither — Either-integrated use variant (#295)

1022 of 1063 branches covered (96.14%)

Branch coverage included in aggregate %.

2901 of 2933 relevant lines covered (98.91%)

4.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.8
core/lib/src/main/java/dmx/fun/Resource.java
1
package dmx.fun;
2

3
import java.util.Objects;
4
import java.util.function.Function;
5
import org.jspecify.annotations.NullMarked;
6

7
/**
8
 * A functional managed resource: a value that must be acquired before use and released
9
 * afterward. {@code Resource<T>} is the composable alternative to {@code try-with-resources}.
10
 *
11
 * <p>Acquisition and release are declared together at construction time. The resource is only
12
 * live during the execution of {@link #use(CheckedFunction) use(fn)} — it is acquired just
13
 * before the body runs, and the release function is <em>always</em> called when the body
14
 * completes, whether it succeeds or throws.
15
 *
16
 * <h2>Behaviour contract</h2>
17
 * <ul>
18
 *   <li>If the body succeeds and release succeeds → {@code Try.success(result)}.</li>
19
 *   <li>If the body succeeds but release throws → {@code Try.failure(releaseException)}.</li>
20
 *   <li>If the body throws and release succeeds → {@code Try.failure(bodyException)}.</li>
21
 *   <li>If both the body and release throw → the release exception is <em>suppressed</em> onto
22
 *       the body exception (mirroring {@code try-with-resources} semantics) and
23
 *       {@code Try.failure(bodyException)} is returned.</li>
24
 * </ul>
25
 *
26
 * <h2>Composition</h2>
27
 * <ul>
28
 *   <li>{@link #map(Function) map} transforms the resource value without changing
29
 *       acquire/release.</li>
30
 *   <li>{@link #flatMap(Function) flatMap} sequences two resources; both are released in
31
 *       reverse acquisition order (inner first, then outer).</li>
32
 * </ul>
33
 *
34
 * @param <T> the type of the managed resource value
35
 */
36
@NullMarked
37
public final class Resource<T> {
38

39
    /**
40
     * Internal representation: a generic lifecycle function.
41
     * Implemented as an anonymous class (not a lambda) because the method carries its own
42
     * type parameter {@code <R>}, which Java lambdas cannot implement.
43
     */
44
    private interface Effect<T> {
45
        <R> Try<R> run(CheckedFunction<? super T, ? extends R> body);
46
    }
47

48
    private final Effect<T> effect;
49

50
    private Resource(Effect<T> effect) {
2✔
51
        this.effect = effect;
3✔
52
    }
1✔
53

54
    // -------------------------------------------------------------------------
55
    // Factories
56
    // -------------------------------------------------------------------------
57

58
    /**
59
     * Creates a {@code Resource<T>} from explicit acquire and release functions.
60
     *
61
     * <p>Example:
62
     * <pre>{@code
63
     * Resource<Connection> conn = Resource.of(
64
     *     () -> dataSource.getConnection(),
65
     *     Connection::close
66
     * );
67
     * }</pre>
68
     *
69
     * @param <T>     the resource type
70
     * @param acquire supplier that obtains the resource; may throw
71
     * @param release consumer that frees the resource; always called after the body
72
     * @return a new {@code Resource<T>}
73
     * @throws NullPointerException if {@code acquire} or {@code release} is {@code null}
74
     */
75
    public static <T> Resource<T> of(
76
            CheckedSupplier<? extends T> acquire,
77
            CheckedConsumer<? super T> release) {
78
        Objects.requireNonNull(acquire, "acquire");
4✔
79
        Objects.requireNonNull(release, "release");
4✔
80
        return new Resource<>(new Effect<>() {
18✔
81
            @Override
82
            public <R> Try<R> run(CheckedFunction<? super T, ? extends R> body) {
83
                T resource;
84
                try {
85
                    resource = acquire.get();
4✔
86
                } catch (Throwable t) {
1✔
87
                    return Try.failure(t);
3✔
88
                }
1✔
89
                return runBody(resource, body, release);
6✔
90
            }
91
        });
92
    }
93

94
    /**
95
     * Creates a {@code Resource<T>} from an {@link AutoCloseable} supplier.
96
     * The {@link AutoCloseable#close()} method is used as the release function.
97
     *
98
     * <p>Example:
99
     * <pre>{@code
100
     * Resource<BufferedReader> reader = Resource.fromAutoCloseable(
101
     *     () -> new BufferedReader(new FileReader(path))
102
     * );
103
     * Try<String> content = reader.use(r -> r.lines().collect(joining("\n")));
104
     * }</pre>
105
     *
106
     * @param <T>     the resource type, must extend {@link AutoCloseable}
107
     * @param acquire supplier that obtains the {@code AutoCloseable} resource; may throw
108
     * @return a new {@code Resource<T>}
109
     * @throws NullPointerException if {@code acquire} is {@code null}
110
     */
111
    public static <T extends AutoCloseable> Resource<T> fromAutoCloseable(
112
            CheckedSupplier<? extends T> acquire) {
113
        Objects.requireNonNull(acquire, "acquire");
4✔
114
        return of(acquire, AutoCloseable::close);
4✔
115
    }
116

117
    /**
118
     * Creates a {@code Resource<T>} from a pre-computed {@link Try Try&lt;T&gt;} and a release
119
     * function.
120
     *
121
     * <p>If {@code acquired} is already a failure, {@code use} returns that failure immediately
122
     * and the {@code release} function is <em>never called</em> — there is nothing to release.
123
     *
124
     * <p><b>One-shot contract:</b> because the resource value is pre-computed rather than
125
     * freshly acquired on each call, invoking {@link #use(CheckedFunction) use()} more than
126
     * once on the returned {@code Resource} will call {@code release} on the <em>same</em>
127
     * underlying value each time. If the resource is not idempotent with respect to release
128
     * (e.g., a JDBC {@code Connection} or an I/O stream), calling {@code use()} more than once
129
     * produces undefined behaviour. Prefer {@link #of(CheckedSupplier, CheckedConsumer) of()}
130
     * when reuse is required, as it acquires a fresh resource on every call.
131
     *
132
     * <p>Example:
133
     * <pre>{@code
134
     * Try<Connection> tryConn = Try.of(() -> dataSource.getConnection());
135
     * Resource<Connection> conn = Resource.eval(tryConn, Connection::close);
136
     * Try<List<User>> users = conn.use(c -> fetchUsers(c)); // call use() exactly once
137
     * }</pre>
138
     *
139
     * @param <T>      the resource type
140
     * @param acquired the pre-computed result of an acquire attempt; if failure, release is skipped
141
     * @param release  consumer that frees the resource when acquired successfully
142
     * @return a new {@code Resource<T>} backed by the pre-computed {@code acquired} value
143
     * @throws NullPointerException if {@code acquired} or {@code release} is {@code null}
144
     */
145
    public static <T> Resource<T> eval(
146
            Try<? extends T> acquired,
147
            CheckedConsumer<? super T> release) {
148
        Objects.requireNonNull(acquired, "acquired");
4✔
149
        Objects.requireNonNull(release, "release");
4✔
150
        return new Resource<>(new Effect<>() {
18✔
151
            @Override
152
            public <R> Try<R> run(CheckedFunction<? super T, ? extends R> body) {
153
                if (acquired.isFailure()) {
4✔
154
                    return Try.failure(acquired.getCause());
5✔
155
                }
156
                return runBody(acquired.get(), body, release);
8✔
157
            }
158
        });
159
    }
160

161
    // -------------------------------------------------------------------------
162
    // Core operation
163
    // -------------------------------------------------------------------------
164

165
    /**
166
     * Acquires the resource, applies {@code body} to it, releases the resource, and returns
167
     * the body's result wrapped in a {@link Try}.
168
     *
169
     * <p>The release function is <em>always</em> called, even when {@code body} throws.
170
     * See the class-level contract for the exact exception-merging rules.
171
     *
172
     * @param <R>  the result type
173
     * @param body function applied to the live resource; may throw
174
     * @return {@code Try.success(result)} on success, or {@code Try.failure(cause)} on any error
175
     * @throws NullPointerException if {@code body} is {@code null}
176
     */
177
    public <R> Try<R> use(CheckedFunction<? super T, ? extends R> body) {
178
        Objects.requireNonNull(body, "body");
4✔
179
        return effect.run(body);
5✔
180
    }
181

182
    /**
183
     * Acquires the resource, applies {@code body} to produce a {@link Result}, releases the
184
     * resource, and returns a {@code Result<R, E>}.
185
     *
186
     * <p>This is the {@code Result}-integrated variant of {@link #use(CheckedFunction) use()}.
187
     * It is useful when the domain layer models failures as typed {@code Result} values rather
188
     * than {@code Throwable}.
189
     *
190
     * <ul>
191
     *   <li>If acquire or release throws a {@code Throwable}, it is mapped to {@code E} via
192
     *       {@code onError} and returned as {@code Result.err(e)}.</li>
193
     *   <li>If the body returns {@code Result.err(e)}, that error is returned as-is.</li>
194
     *   <li>If both body and release fail, the release exception is suppressed onto the body
195
     *       exception and the combined throwable is passed to {@code onError}.</li>
196
     * </ul>
197
     *
198
     * <p>Example:
199
     * <pre>{@code
200
     * Result<List<User>, DbError> users = connResource.useAsResult(
201
     *     conn -> fetchUsers(conn),
202
     *     ex   -> new DbError.QueryFailed(ex.getMessage())
203
     * );
204
     * }</pre>
205
     *
206
     * @param <R>     the success type
207
     * @param <E>     the error type
208
     * @param body    function applied to the live resource; returns a {@code Result}
209
     * @param onError maps any {@code Throwable} from acquire/release/body to {@code E}
210
     * @return {@code Result.ok(value)} on success, or {@code Result.err(error)} on any failure
211
     * @throws NullPointerException if {@code body} or {@code onError} is {@code null}
212
     */
213
    public <R, E> Result<R, E> useAsResult(
214
            Function<? super T, ? extends Result<? extends R, ? extends E>> body,
215
            Function<? super Throwable, ? extends E> onError) {
216
        Objects.requireNonNull(body, "body");
4✔
217
        Objects.requireNonNull(onError, "onError");
4✔
218
        Try<Result<R, E>> tryResult = use(t -> {
5✔
219
            @SuppressWarnings("unchecked")
220
            Result<R, E> r = (Result<R, E>) Objects.requireNonNull(
5✔
221
                body.apply(t), "useAsResult body must not return null");
3✔
222
            return r;
2✔
223
        });
224
        if (tryResult.isSuccess()) {
3✔
225
            return tryResult.get();
4✔
226
        }
227
        return Result.err(onError.apply(tryResult.getCause()));
6✔
228
    }
229

230
    /**
231
     * Acquires the resource, applies {@code body} to produce an {@link Either}, releases the
232
     * resource, and returns an {@code Either<E, R>}.
233
     *
234
     * <p>This is the {@code Either}-integrated variant of {@link #use(CheckedFunction) use()},
235
     * symmetric with {@link #useAsResult(Function, Function) useAsResult()}. Use it when the
236
     * domain layer models results as neutral two-track {@code Either} values rather than
237
     * typed {@code Result} errors.
238
     *
239
     * <ul>
240
     *   <li>If acquire or release throws a {@code Throwable}, it is mapped to {@code E} via
241
     *       {@code onError} and returned as {@code Either.left(e)}.</li>
242
     *   <li>If the body returns {@code Either.left(e)}, that value is returned as-is.</li>
243
     *   <li>If both body and release fail, the release exception is suppressed onto the body
244
     *       exception and the combined throwable is passed to {@code onError}.</li>
245
     * </ul>
246
     *
247
     * <p>Example:
248
     * <pre>{@code
249
     * Either<DbError, List<User>> users = connResource.useAsEither(
250
     *     conn -> fetchUsers(conn),
251
     *     ex   -> new DbError.ConnectionFailed(ex.getMessage())
252
     * );
253
     * }</pre>
254
     *
255
     * @param <R>     the right (success) type
256
     * @param <E>     the left (error) type
257
     * @param body    function applied to the live resource; returns an {@code Either}
258
     * @param onError maps any {@code Throwable} from acquire/release/body to {@code E}
259
     * @return {@code Either.right(value)} on success, or {@code Either.left(error)} on any failure
260
     * @throws NullPointerException if {@code body} or {@code onError} is {@code null}
261
     */
262
    public <R, E> Either<E, R> useAsEither(
263
            Function<? super T, ? extends Either<? extends E, ? extends R>> body,
264
            Function<? super Throwable, ? extends E> onError) {
265
        Objects.requireNonNull(body, "body");
4✔
266
        Objects.requireNonNull(onError, "onError");
4✔
267
        var tryResult = use(t -> {
5✔
268
            @SuppressWarnings("unchecked")
269
            Either<E, R> r = (Either<E, R>) Objects.requireNonNull(
5✔
270
                body.apply(t), "useAsEither body must not return null");
3✔
271
            return r;
2✔
272
        });
273
        if (tryResult.isSuccess()) {
3✔
274
            return tryResult.get();
4✔
275
        }
276
        return Either.left(onError.apply(tryResult.getCause()));
6✔
277
    }
278

279
    // -------------------------------------------------------------------------
280
    // Transformations
281
    // -------------------------------------------------------------------------
282

283
    /**
284
     * Returns a new {@code Resource<R>} whose body receives the result of applying {@code fn}
285
     * to the acquired value. The acquire/release lifecycle of the underlying resource is
286
     * unchanged.
287
     *
288
     * <p>If {@code fn} throws, the underlying resource is still released and the exception is
289
     * captured as a {@code Try.failure}.
290
     *
291
     * @param <R> the mapped resource type
292
     * @param fn  function transforming the acquired value; must not be {@code null}
293
     * @return a new {@code Resource<R>}
294
     * @throws NullPointerException if {@code fn} is {@code null}
295
     */
296
    public <R> Resource<R> map(Function<? super T, ? extends R> fn) {
297
        Objects.requireNonNull(fn, "fn");
4✔
298
        Effect<T> self = this.effect;
3✔
299
        return new Resource<>(new Effect<>() {
24✔
300
            @Override
301
            public <S> Try<S> run(CheckedFunction<? super R, ? extends S> body) {
302
                return self.run(
8✔
303
                    t -> body.apply(fn.apply(t))
6✔
304
                );
305
            }
306
        });
307
    }
308

309
    /**
310
     * Sequences this resource with an inner resource derived from its value.
311
     * Both resources are released in reverse acquisition order: the inner resource is
312
     * released first, then this (outer) resource.
313
     *
314
     * <p>Example — connection then prepared statement:
315
     * <pre>{@code
316
     * Resource<PreparedStatement> stmt = connResource.flatMap(c ->
317
     *     Resource.of(
318
     *         () -> c.prepareStatement("SELECT * FROM users"),
319
     *         PreparedStatement::close
320
     *     )
321
     * );
322
     * Try<List<User>> result = stmt.use(ps -> mapRows(ps.executeQuery()));
323
     * }</pre>
324
     *
325
     * @param <R> the inner resource type
326
     * @param fn  function that produces the inner resource from this resource's value;
327
     *            must not be {@code null} and must not return {@code null}
328
     * @return a composed {@code Resource<R>} whose lifecycle manages both resources
329
     * @throws NullPointerException if {@code fn} is {@code null} or returns {@code null}
330
     */
331
    public <R> Resource<R> flatMap(Function<? super T, ? extends Resource<R>> fn) {
332
        Objects.requireNonNull(fn, "fn");
4✔
333
        Effect<T> self = this.effect;
3✔
334
        return new Resource<>(new Effect<>() {
24✔
335
            @Override
336
            public <S> Try<S> run(CheckedFunction<? super R, ? extends S> body) {
337
                // Run the outer lifecycle with a body that:
338
                //   1. Creates the inner resource from T
339
                //   2. Runs the inner resource with the user's body
340
                //   3. If inner failed, rethrows so the outer Effect captures it as bodyEx
341
                //      (and still releases the outer resource)
342
                //   4. If inner succeeded, returns the result so the outer Effect returns success
343
                return self.run(t -> {
8✔
344
                    var inner = Objects.requireNonNull(
5✔
345
                        fn.apply(t), "flatMap fn must not return null");
3✔
346
                    var innerResult = inner.use(body);
4✔
347
                    if (innerResult.isFailure()) {
3✔
348
                        sneakyThrow(innerResult.getCause());
×
349
                    }
350
                    return innerResult.get();
3✔
351
                });
352
            }
353
        });
354
    }
355

356
    /**
357
     * Returns a new {@code Resource<R>} whose value is obtained by applying a
358
     * {@link Try}-returning function to the acquired value.
359
     *
360
     * <p>This is the {@link Try}-integrated counterpart of {@link #map(Function) map()}.
361
     * It is useful when the transformation itself is a fallible operation already wrapped in
362
     * a {@code Try} (e.g., parsing, validation, or a call to a {@code Try.of(...)}-wrapped API).
363
     * If {@code fn} returns a failure, the underlying resource is still released and the
364
     * failure is propagated.
365
     *
366
     * <p>Example:
367
     * <pre>{@code
368
     * Resource<Config> config = rawTextResource.mapTry(text ->
369
     *     Try.of(() -> Config.parse(text))
370
     * );
371
     * Try<Integer> port = config.use(c -> c.port());
372
     * }</pre>
373
     *
374
     * @param <R> the mapped resource type
375
     * @param fn  function returning a {@code Try<R>}; must not be {@code null} or return
376
     *            {@code null}
377
     * @return a new {@code Resource<R>}
378
     * @throws NullPointerException if {@code fn} is {@code null}
379
     */
380
    public <R> Resource<R> mapTry(Function<? super T, ? extends Try<? extends R>> fn) {
381
        Objects.requireNonNull(fn, "fn");
4✔
382
        Effect<T> self = this.effect;
3✔
383
        return new Resource<>(new Effect<>() {
24✔
384
            @Override
385
            public <S> Try<S> run(CheckedFunction<? super R, ? extends S> body) {
386
                return self.run(t -> {
8✔
387
                    Try<? extends R> inner = Objects.requireNonNull(
5✔
388
                        fn.apply(t), "mapTry fn must not return null");
3✔
389
                    if (inner.isFailure()) {
3✔
390
                        sneakyThrow(inner.getCause());
×
391
                    }
392
                    return body.apply(inner.get());
5✔
393
                });
394
            }
395
        });
396
    }
397

398
    // -------------------------------------------------------------------------
399
    // Internal helpers
400
    // -------------------------------------------------------------------------
401

402
    /**
403
     * Runs {@code body} with {@code resource}, always calls {@code release}, and merges
404
     * exceptions according to the class-level contract.
405
     */
406
    private static <T, R> Try<R> runBody(
407
            T resource,
408
            CheckedFunction<? super T, ? extends R> body,
409
            CheckedConsumer<? super T> release
410
    ) {
411
        Throwable bodyEx = null;
2✔
412
        R result = null;
2✔
413
        try {
414
            result = body.apply(resource);
4✔
415
        } catch (Throwable t) {
1✔
416
            bodyEx = t;
2✔
417
        }
1✔
418
        try {
419
            release.accept(resource);
3✔
420
        } catch (Throwable releaseEx) {
1✔
421
            if (bodyEx != null) {
2✔
422
                bodyEx.addSuppressed(releaseEx);
4✔
423
            } else {
424
                bodyEx = releaseEx;
2✔
425
            }
426
        }
1✔
427
        return bodyEx != null ? Try.failure(bodyEx) : Try.success(result);
8✔
428
    }
429

430
    /**
431
     * Rethrows any {@link Throwable} without wrapping it, bypassing the compiler's
432
     * checked-exception enforcement.
433
     *
434
     * <p><b>How it works:</b> the unchecked cast {@code (E) t} is a <em>no-op at runtime</em>
435
     * because generic type parameters are erased — the JVM sees a plain {@code throw t}.
436
     * The compiler, however, believes it must only throw the checked {@code E}, so it does not
437
     * require callers inside a {@code CheckedFunction} lambda to declare the rethrown type.
438
     * The {@code @SuppressWarnings("unchecked")} suppresses the unavoidable unchecked-cast
439
     * warning that results from this intentional use of type erasure.
440
     *
441
     * <p><b>Safety:</b> the original {@code Throwable} is rethrown unchanged — no wrapping,
442
     * no information loss. The return type {@code R} is declared only so the method can appear
443
     * in throw position in expressions that require a value (e.g., {@code return sneakyThrow(t)}).
444
     */
445
    @SuppressWarnings("unchecked")
446
    private static <E extends Throwable, R> R sneakyThrow(Throwable t) throws E {
447
        throw (E) t;
2✔
448
    }
449
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc