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

domix / dmx-fun / 25616882968

10 May 2026 01:45AM UTC coverage: 98.345%. Remained the same
25616882968

push

github

web-flow
Merge pull request #429 from domix/feature/adr/021/init

docs(adr): add ADR-021 — Resource<T> as composable managed resource

1093 of 1135 branches covered (96.3%)

Branch coverage included in aggregate %.

3067 of 3095 relevant lines covered (99.1%)

5.1 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
 * The design rationale — including the exception-merging contract, the internal
11
 * {@code Effect<T>} representation, and the alternatives considered — is documented in
12
 * <a href="https://domix.github.io/dmx-fun/adr/adr-021-resource-composable/">
13
 * ADR-021 — Resource&lt;T&gt; as composable managed resource</a>.
14
 *
15
 * <p>Acquisition and release are declared together at construction time. The resource is only
16
 * live during the execution of {@link #use(CheckedFunction) use(fn)} — it is acquired just
17
 * before the body runs, and the release function is <em>always</em> called when the body
18
 * completes, whether it succeeds or throws.
19
 *
20
 * <h2>Behaviour contract</h2>
21
 * <ul>
22
 *   <li>If the body succeeds and release succeeds → {@code Try.success(result)}.</li>
23
 *   <li>If the body succeeds but release throws → {@code Try.failure(releaseException)}.</li>
24
 *   <li>If the body throws and release succeeds → {@code Try.failure(bodyException)}.</li>
25
 *   <li>If both the body and release throw → the release exception is <em>suppressed</em> onto
26
 *       the body exception (mirroring {@code try-with-resources} semantics) and
27
 *       {@code Try.failure(bodyException)} is returned.</li>
28
 * </ul>
29
 *
30
 * <h2>Composition</h2>
31
 * <ul>
32
 *   <li>{@link #map(Function) map} transforms the resource value without changing
33
 *       acquire/release.</li>
34
 *   <li>{@link #flatMap(Function) flatMap} sequences two resources; both are released in
35
 *       reverse acquisition order (inner first, then outer).</li>
36
 * </ul>
37
 *
38
 * @param <T> the type of the managed resource value
39
 */
40
@NullMarked
41
public final class Resource<T> {
42

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

52
    private final Effect<T> effect;
53

54
    private Resource(Effect<T> effect) {
2✔
55
        this.effect = effect;
3✔
56
    }
1✔
57

58
    // -------------------------------------------------------------------------
59
    // Factories
60
    // -------------------------------------------------------------------------
61

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

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

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

165
    // -------------------------------------------------------------------------
166
    // Core operation
167
    // -------------------------------------------------------------------------
168

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

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

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

283
    // -------------------------------------------------------------------------
284
    // Transformations
285
    // -------------------------------------------------------------------------
286

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

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

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

402
    // -------------------------------------------------------------------------
403
    // Internal helpers
404
    // -------------------------------------------------------------------------
405

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

434
    /**
435
     * Rethrows any {@link Throwable} without wrapping it, bypassing the compiler's
436
     * checked-exception enforcement.
437
     *
438
     * <p><b>How it works:</b> the unchecked cast {@code (E) t} is a <em>no-op at runtime</em>
439
     * because generic type parameters are erased — the JVM sees a plain {@code throw t}.
440
     * The compiler, however, believes it must only throw the checked {@code E}, so it does not
441
     * require callers inside a {@code CheckedFunction} lambda to declare the rethrown type.
442
     * The {@code @SuppressWarnings("unchecked")} suppresses the unavoidable unchecked-cast
443
     * warning that results from this intentional use of type erasure.
444
     *
445
     * <p><b>Safety:</b> the original {@code Throwable} is rethrown unchanged — no wrapping,
446
     * no information loss. The return type {@code R} is declared only so the method can appear
447
     * in throw position in expressions that require a value (e.g., {@code return sneakyThrow(t)}).
448
     */
449
    @SuppressWarnings("unchecked")
450
    private static <E extends Throwable, R> R sneakyThrow(Throwable t) throws E {
451
        throw (E) t;
2✔
452
    }
453
}
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