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

domix / dmx-fun / 25621566805

10 May 2026 06:13AM UTC coverage: 98.28% (+0.002%) from 98.278%
25621566805

Pull #433

github

web-flow
Merge d04d3f432 into df42249fa
Pull Request #433: fix(resource): enforce null contracts and polish NPE messages

1089 of 1131 branches covered (96.29%)

Branch coverage included in aggregate %.

3082 of 3113 relevant lines covered (99.0%)

5.13 hits per line

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

97.89
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
        var 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(
4✔
232
            Objects.requireNonNull(
1✔
233
                onError.apply(tryResult.getCause()),
3✔
234
                "onError returned null"
235
            )
236
        );
237
    }
238

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

293
    // -------------------------------------------------------------------------
294
    // Transformations
295
    // -------------------------------------------------------------------------
296

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

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

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

412
    // -------------------------------------------------------------------------
413
    // Internal helpers
414
    // -------------------------------------------------------------------------
415

416
    /**
417
     * Runs {@code body} with {@code resource}, always calls {@code release}, and merges
418
     * exceptions according to the class-level contract.
419
     */
420
    private static <T, R> Try<R> runBody(
421
            T resource,
422
            CheckedFunction<? super T, ? extends R> body,
423
            CheckedConsumer<? super T> release
424
    ) {
425
        Throwable bodyEx = null;
2✔
426
        R result = null;
2✔
427
        try {
428
            result = Objects.requireNonNull(body.apply(resource), "body returned null");
6✔
429
        } catch (Throwable t) {
1✔
430
            bodyEx = t;
2✔
431
        }
1✔
432
        try {
433
            release.accept(resource);
3✔
434
        } catch (Throwable releaseEx) {
1✔
435
            if (bodyEx != null) {
2✔
436
                bodyEx.addSuppressed(releaseEx);
4✔
437
            } else {
438
                bodyEx = releaseEx;
2✔
439
            }
440
        }
1✔
441
        return bodyEx != null ? Try.failure(bodyEx) : Try.success(result);
8✔
442
    }
443

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