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

domix / dmx-fun / 25613943020

09 May 2026 10:57PM UTC coverage: 98.339%. Remained the same
25613943020

push

github

web-flow
Merge pull request #424 from domix/feature/adr/018/init

docs(adr): ADR-018 — NonEmptyList<T>, NonEmptySet<T>, NonEmptyMap<K,V> as structural guarantee types

1089 of 1131 branches covered (96.29%)

Branch coverage included in aggregate %.

3056 of 3084 relevant lines covered (99.09%)

5.1 hits per line

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

97.03
core/lib/src/main/java/dmx/fun/NonEmptySet.java
1
package dmx.fun;
2

3
import java.util.ArrayList;
4
import java.util.Collections;
5
import java.util.Iterator;
6
import java.util.LinkedHashMap;
7
import java.util.LinkedHashSet;
8
import java.util.List;
9
import java.util.Map;
10
import java.util.Objects;
11
import java.util.Optional;
12
import java.util.Set;
13
import java.util.function.Function;
14
import java.util.function.Predicate;
15
import java.util.stream.Collector;
16
import java.util.stream.Stream;
17
import org.jspecify.annotations.NullMarked;
18

19
/**
20
 * An immutable, non-empty set: a set guaranteed at construction time to contain
21
 * at least one element.
22
 *
23
 * <p>This type makes the non-emptiness constraint part of the static type system.
24
 * APIs that require at least one item can declare {@code NonEmptySet<T>} instead of
25
 * {@code Set<T>} and eliminate runtime emptiness checks entirely.
26
 *
27
 * <p>Insertion order is preserved (backed by {@link LinkedHashSet}). The first element
28
 * inserted is the {@link #head()}.
29
 *
30
 * <p>This class is {@code @NullMarked}: all elements and parameters are non-null by default.
31
 *
32
 * <p>The rationale for introducing dedicated non-empty collection types instead of using
33
 * standard JDK types with runtime checks is documented in
34
 * <a href="https://domix.github.io/dmx-fun/adr/adr-018-non-empty-collections/">
35
 * ADR-018 — NonEmptyList&lt;T&gt;, NonEmptySet&lt;T&gt;, NonEmptyMap&lt;K,V&gt; as structural guarantee types</a>.
36
 *
37
 * @param <T> the type of elements in this set
38
 */
39
@NullMarked
40
public final class NonEmptySet<T> implements Iterable<T> {
41

42
    private final T head;
43
    private final Set<T> tail; // unmodifiable, does NOT include head
44

45
    private transient volatile Set<T> cachedSet;
46

47
    private NonEmptySet(T head, Set<T> tail) {
2✔
48
        this.head = head;
3✔
49
        this.tail = tail;
3✔
50
    }
1✔
51

52
    // -------------------------------------------------------------------------
53
    // Smart constructors
54
    // -------------------------------------------------------------------------
55

56
    /**
57
     * Creates a {@code NonEmptySet} with the given head element and additional elements.
58
     *
59
     * <p>If {@code rest} contains {@code head}, the duplicate is silently ignored.
60
     *
61
     * @param head the first (mandatory) element; must not be {@code null}
62
     * @param rest additional elements; must not be {@code null}; elements must not be {@code null}
63
     * @param <T>  the element type
64
     * @return a new {@code NonEmptySet}
65
     * @throws NullPointerException if {@code head}, {@code rest}, or any element in
66
     *                              {@code rest} is {@code null}
67
     */
68
    public static <T> NonEmptySet<T> of(T head, Set<? extends T> rest) {
69
        Objects.requireNonNull(head, "head must not be null");
4✔
70
        Objects.requireNonNull(rest, "rest must not be null");
4✔
71
        Set<T> tail = new LinkedHashSet<>();
4✔
72
        for (T element : rest) {
9✔
73
            Objects.requireNonNull(element, "rest elements must not be null");
4✔
74
            if (!element.equals(head)) {
4✔
75
                tail.add(element);
4✔
76
            }
77
        }
1✔
78
        return new NonEmptySet<>(head, Collections.unmodifiableSet(tail));
7✔
79
    }
80

81
    /**
82
     * Creates a {@code NonEmptySet} containing exactly one element.
83
     *
84
     * @param head the sole element; must not be {@code null}
85
     * @param <T>  the element type
86
     * @return a singleton {@code NonEmptySet}
87
     * @throws NullPointerException if {@code head} is {@code null}
88
     */
89
    public static <T> NonEmptySet<T> singleton(T head) {
90
        Objects.requireNonNull(head, "head must not be null");
4✔
91
        return new NonEmptySet<>(head, Set.of());
6✔
92
    }
93

94
    /**
95
     * Attempts to construct a {@code NonEmptySet} from a plain {@link Set}.
96
     *
97
     * @param set  the source set; must not be {@code null}; elements must not be {@code null}
98
     * @param <T>  the element type
99
     * @return {@link Option#some(Object)} wrapping the {@code NonEmptySet} if the set is
100
     *         non-empty, or {@link Option#none()} if the set is empty
101
     * @throws NullPointerException if {@code set} or any element is {@code null}
102
     */
103
    public static <T> Option<NonEmptySet<T>> fromSet(Set<? extends T> set) {
104
        Objects.requireNonNull(set, "set must not be null");
4✔
105
        if (set.isEmpty()) {
3✔
106
            return Option.none();
2✔
107
        }
108
        set.forEach(e -> Objects.requireNonNull(e, "set elements must not be null"));
8✔
109
        return Option.some(fromSetUnsafe(set));
4✔
110
    }
111

112
    /**
113
     * Attempts to construct a singleton {@code NonEmptySet} from a JDK {@link Optional}.
114
     *
115
     * @param optional the source optional; must not be {@code null}
116
     * @param <T>      the element type
117
     * @return {@link Option#some(Object)} wrapping a singleton {@code NonEmptySet} if the
118
     *         optional is present, or {@link Option#none()} if the optional is empty
119
     * @throws NullPointerException if {@code optional} is {@code null}
120
     */
121
    public static <T> Option<NonEmptySet<T>> fromOptional(Optional<? extends T> optional) {
122
        Objects.requireNonNull(optional, "optional must not be null");
4✔
123
        return optional.isPresent()
4✔
124
            ? Option.some(NonEmptySet.singleton(optional.get()))
5✔
125
            : Option.none();
1✔
126
    }
127

128
    /**
129
     * Returns a {@link Collector} that accumulates a {@code Stream<T>} into an
130
     * {@code Option<NonEmptySet<T>>}.
131
     *
132
     * <p>Produces {@link Option#some(Object)} for a non-empty stream and {@link Option#none()}
133
     * for an empty stream. Duplicate elements are automatically deduplicated (set semantics).
134
     *
135
     * <p>Example:
136
     * <pre>{@code
137
     * Option<NonEmptySet<String>> roles =
138
     *     userRoles.stream()
139
     *         .collect(NonEmptySet.collector());
140
     * }</pre>
141
     *
142
     * @param <T> the element type
143
     * @return a collector producing {@code Option<NonEmptySet<T>>}
144
     */
145
    public static <T> Collector<T, ?, Option<NonEmptySet<T>>> collector() {
146
        return Collector.of(
8✔
147
            LinkedHashSet<T>::new,
148
            (set, t) -> set.add(Objects.requireNonNull(t, "stream elements must not be null")),
7✔
149
            (a, b) -> { a.addAll(b); return a; },
×
150
            set -> set.isEmpty() ? Option.none() : Option.some(fromSetUnsafe(set))
9✔
151
        );
152
    }
153

154
    /** Internal: builds from a known-non-empty set without Option wrapping. */
155
    private static <T> NonEmptySet<T> fromSetUnsafe(Set<? extends T> set) {
156
        Iterator<? extends T> it = set.iterator();
3✔
157
        T head = it.next();
3✔
158
        Set<T> tail = new LinkedHashSet<>();
4✔
159
        while (it.hasNext()) {
3✔
160
            tail.add(it.next());
6✔
161
        }
162
        return new NonEmptySet<>(head, Collections.unmodifiableSet(tail));
7✔
163
    }
164

165
    // -------------------------------------------------------------------------
166
    // Accessors
167
    // -------------------------------------------------------------------------
168

169
    /**
170
     * Returns the guaranteed head element of this set (the first inserted element).
171
     *
172
     * @return the head element (never {@code null})
173
     */
174
    public T head() {
175
        return head;
3✔
176
    }
177

178
    /**
179
     * Returns the number of elements in this set. Always &ge; 1.
180
     *
181
     * @return the size
182
     */
183
    public int size() {
184
        return 1 + tail.size();
6✔
185
    }
186

187
    /**
188
     * Returns {@code true} if this set contains {@code element}.
189
     *
190
     * @param element the element to test; must not be {@code null}
191
     * @return {@code true} if the element is present
192
     * @throws NullPointerException if {@code element} is {@code null}
193
     */
194
    public boolean contains(T element) {
195
        Objects.requireNonNull(element, "element must not be null");
4✔
196
        return head.equals(element) || tail.contains(element);
14✔
197
    }
198

199
    /**
200
     * Returns an unmodifiable {@link Set} containing all elements (head first, then
201
     * tail in insertion order). The same instance is returned on repeated calls
202
     * (lazily initialized, thread-safe).
203
     *
204
     * @return an unmodifiable set with all elements
205
     */
206
    public Set<T> toSet() {
207
        Set<T> s = cachedSet;
3✔
208
        if (s == null) {
2✔
209
            synchronized (this) {
4✔
210
                s = cachedSet;
3✔
211
                if (s == null) {
2!
212
                    Set<T> result = new LinkedHashSet<>();
4✔
213
                    result.add(head);
5✔
214
                    result.addAll(tail);
5✔
215
                    cachedSet = s = Collections.unmodifiableSet(result);
6✔
216
                }
217
            }
3✔
218
        }
219
        return s;
2✔
220
    }
221

222
    /**
223
     * Returns a sequential {@link Stream} of all elements in insertion order.
224
     *
225
     * <p>The stream always contains at least one element (the head). Use it to bridge
226
     * {@code NonEmptySet} into the standard Java stream API without going through
227
     * {@link #toSet()}.
228
     *
229
     * @return a non-empty stream of elements in insertion order
230
     */
231
    public Stream<T> toStream() {
232
        return toSet().stream();
4✔
233
    }
234

235
    // -------------------------------------------------------------------------
236
    // Transformations
237
    // -------------------------------------------------------------------------
238

239
    /**
240
     * Applies {@code mapper} to every element and returns a new {@code NonEmptySet}
241
     * of the results. Duplicate mapped values are deduplicated; the head is always
242
     * the mapped value of the original head.
243
     *
244
     * @param mapper a non-null function to apply to each element; must not return {@code null}
245
     * @param <R>    the result element type
246
     * @return a new {@code NonEmptySet} of mapped values
247
     * @throws NullPointerException if {@code mapper} is {@code null} or returns {@code null}
248
     */
249
    public <R> NonEmptySet<R> map(Function<? super T, ? extends R> mapper) {
250
        Objects.requireNonNull(mapper, "mapper must not be null");
4✔
251
        R newHead = Objects.requireNonNull(mapper.apply(head), "mapper must not return null");
7✔
252
        Set<R> newTail = new LinkedHashSet<>();
4✔
253
        for (T element : tail) {
10✔
254
            R mapped = Objects.requireNonNull(mapper.apply(element), "mapper must not return null");
6✔
255
            if (!mapped.equals(newHead)) {
4✔
256
                newTail.add(mapped);
4✔
257
            }
258
        }
1✔
259
        return new NonEmptySet<>(newHead, Collections.unmodifiableSet(newTail));
7✔
260
    }
261

262
    /**
263
     * Returns a new {@code NonEmptySet} containing elements that satisfy {@code predicate},
264
     * wrapped in {@link Option#some(Object)}.
265
     * Returns {@link Option#none()} if no elements pass the predicate.
266
     *
267
     * @param predicate a non-null predicate to test each element
268
     * @return {@code Some(filteredSet)} if at least one element passes, {@code None} otherwise
269
     * @throws NullPointerException if {@code predicate} is {@code null}
270
     */
271
    public Option<NonEmptySet<T>> filter(Predicate<? super T> predicate) {
272
        Objects.requireNonNull(predicate, "predicate must not be null");
4✔
273
        Set<T> result = new LinkedHashSet<>();
4✔
274
        if (predicate.test(head)) result.add(head);
10✔
275
        for (T element : tail) {
10✔
276
            if (predicate.test(element)) result.add(element);
8!
277
        }
1✔
278
        if (result.isEmpty()) return Option.none();
5✔
279
        return Option.some(fromSetUnsafe(result));
4✔
280
    }
281

282
    /**
283
     * Returns a new {@code NonEmptySet} that is the union of this set and {@code other}.
284
     * The result is always non-empty since both inputs are non-empty.
285
     *
286
     * @param other the other set; must not be {@code null}
287
     * @return a new {@code NonEmptySet} containing all elements from both sets
288
     * @throws NullPointerException if {@code other} is {@code null}
289
     */
290
    public NonEmptySet<T> union(NonEmptySet<T> other) {
291
        Objects.requireNonNull(other, "other must not be null");
4✔
292
        Set<T> combined = new LinkedHashSet<>(this.toSet());
6✔
293
        combined.addAll(other.toSet());
5✔
294
        return fromSetUnsafe(combined);
3✔
295
    }
296

297
    /**
298
     * Returns a new {@code NonEmptySet} containing only elements present in both this
299
     * set and {@code other}, wrapped in {@link Option#some(Object)}.
300
     * Returns {@link Option#none()} if the intersection is empty.
301
     *
302
     * @param other the set to intersect with; must not be {@code null}
303
     * @return {@code Some(intersection)} if at least one common element exists,
304
     *         {@code None} otherwise
305
     * @throws NullPointerException if {@code other} is {@code null}
306
     */
307
    public Option<NonEmptySet<T>> intersection(Set<? extends T> other) {
308
        Objects.requireNonNull(other, "other must not be null");
4✔
309
        Set<T> result = new LinkedHashSet<>();
4✔
310
        if (other.contains(head)) result.add(head);
10✔
311
        for (T element : tail) {
10✔
312
            if (other.contains(element)) result.add(element);
4!
313
        }
1✔
314
        if (result.isEmpty()) return Option.none();
5✔
315
        return Option.some(fromSetUnsafe(result));
4✔
316
    }
317

318
    /**
319
     * Converts this set to a {@link NonEmptyList} of its elements in insertion order.
320
     *
321
     * @return a {@code NonEmptyList<T>} with the same elements
322
     */
323
    public NonEmptyList<T> toNonEmptyList() {
324
        List<T> tailList = new ArrayList<>(tail);
6✔
325
        return NonEmptyList.of(head, tailList);
5✔
326
    }
327

328
    /**
329
     * Returns a {@link NonEmptyMap} by applying {@code valueMapper} to each element of
330
     * this set. Elements become keys; mapped results become values.
331
     * The head of this set is the head key of the returned map.
332
     *
333
     * @param valueMapper a non-null function to derive a value from each element;
334
     *                    must not return {@code null}
335
     * @param <V>         the value type
336
     * @return a new {@code NonEmptyMap<T, V>}
337
     * @throws NullPointerException if {@code valueMapper} is {@code null} or returns {@code null}
338
     */
339
    public <V> NonEmptyMap<T, V> toNonEmptyMap(Function<? super T, ? extends V> valueMapper) {
340
        Objects.requireNonNull(valueMapper, "valueMapper must not be null");
4✔
341
        V headVal = Objects.requireNonNull(valueMapper.apply(head), "valueMapper must not return null");
7✔
342
        Map<T, V> tailMap = new LinkedHashMap<>();
4✔
343
        for (T element : tail) {
10✔
344
            V val = Objects.requireNonNull(valueMapper.apply(element), "valueMapper must not return null");
6✔
345
            tailMap.put(element, val);
5✔
346
        }
1✔
347
        return NonEmptyMap.of(head, headVal, tailMap);
6✔
348
    }
349

350
    // -------------------------------------------------------------------------
351
    // Interoperability — sequence
352
    // -------------------------------------------------------------------------
353

354
    /**
355
     * Sequences a {@code NonEmptySet<Option<T>>} into an {@code Option<NonEmptySet<T>>}.
356
     *
357
     * <p>Returns {@link Option#some(Object)} containing all unwrapped values if every element
358
     * is {@code Some}; returns {@link Option#none()} as soon as any element is {@code None}
359
     * (fail-fast in inspection — the method stops iterating after the first {@code None};
360
     * elements are already materialized in the set before sequencing, so later elements are
361
     * not inspected but were already evaluated).
362
     *
363
     * @param <T> the unwrapped element type
364
     * @param nes a {@code NonEmptySet<Option<T>>}; must not be {@code null}
365
     * @return {@code Some(NonEmptySet<T>)} if all elements are present, {@code None} otherwise
366
     * @throws NullPointerException if {@code nes} is {@code null}
367
     */
368
    public static <T> Option<NonEmptySet<T>> sequenceOption(NonEmptySet<Option<T>> nes) {
369
        Objects.requireNonNull(nes, "nes must not be null");
4✔
370
        Set<T> result = new LinkedHashSet<>();
4✔
371
        for (Option<T> opt : nes) {
10✔
372
            switch (opt) {
11✔
373
                case Option.None<T> ignored -> { return Option.none(); }
5✔
374
                case Option.Some<T>(T v)   -> result.add(v);
12✔
375
            }
376
        }
1✔
377
        return Option.some(fromSetUnsafe(result)); // always non-empty since nes.size() >= 1
4✔
378
    }
379

380
    /**
381
     * Sequences a {@code NonEmptySet<Try<T>>} into a {@code Try<NonEmptySet<T>>}.
382
     *
383
     * <p>Returns {@link Try#success(Object)} if all elements succeed; returns
384
     * {@link Try#failure(Throwable)} from the first failing element (fail-fast in inspection —
385
     * the method stops iterating after the first failure; elements are already materialized in
386
     * the set before sequencing, so later elements are not inspected but were already evaluated).
387
     *
388
     * @param <T> the success value type
389
     * @param nes a {@code NonEmptySet<Try<T>>}; must not be {@code null}
390
     * @return {@code Success(NonEmptySet<T>)} if all succeed, {@code Failure} otherwise
391
     * @throws NullPointerException if {@code nes} is {@code null}
392
     */
393
    public static <T> Try<NonEmptySet<T>> sequenceTry(NonEmptySet<Try<T>> nes) {
394
        Objects.requireNonNull(nes, "nes must not be null");
4✔
395
        Set<T> result = new LinkedHashSet<>();
4✔
396
        for (Try<T> t : nes) {
10✔
397
            switch (t) {
11✔
398
                case Try.Failure<T> f -> { return Try.failure(f.cause()); }
7✔
399
                case Try.Success<T> s -> result.add(s.value());
8✔
400
            }
401
        }
1✔
402
        return Try.success(fromSetUnsafe(result)); // always non-empty since nes.size() >= 1
4✔
403
    }
404

405
    /**
406
     * Sequences a {@code NonEmptySet<Either<E, T>>} into an
407
     * {@code Either<E, NonEmptySet<T>>}.
408
     *
409
     * <p>Returns {@link Either#right(Object)} if all elements are right; returns
410
     * {@link Either#left(Object)} from the first left element (fail-fast in inspection —
411
     * the method stops iterating after the first left; elements are already materialized in
412
     * the set before sequencing, so later elements are not inspected but were already evaluated).
413
     *
414
     * @param <E> the left (error) type
415
     * @param <T> the right (success) type
416
     * @param nes a {@code NonEmptySet<Either<E, T>>}; must not be {@code null}
417
     * @return {@code right(NonEmptySet<T>)} if all are right, {@code left(E)} otherwise
418
     * @throws NullPointerException if {@code nes} is {@code null}
419
     */
420
    public static <E, T> Either<E, NonEmptySet<T>> sequenceEither(NonEmptySet<Either<E, T>> nes) {
421
        Objects.requireNonNull(nes, "nes must not be null");
4✔
422
        Set<T> result = new LinkedHashSet<>();
4✔
423
        for (Either<E, T> e : nes) {
10✔
424
            switch (e) {
11✔
425
                case Either.Left<E, T> l  -> { return Either.left(l.value()); }
7✔
426
                case Either.Right<E, T> r -> result.add(r.value());
8✔
427
            }
428
        }
1✔
429
        return Either.right(fromSetUnsafe(result)); // always non-empty since nes.size() >= 1
4✔
430
    }
431

432
    /**
433
     * Sequences a {@code NonEmptySet<Result<T, E>>} into a
434
     * {@code Result<NonEmptySet<T>, E>}.
435
     *
436
     * <p>Returns {@link Result#ok(Object)} if all elements are ok; returns
437
     * {@link Result#err(Object)} from the first error element (fail-fast in inspection —
438
     * the method stops iterating after the first err; elements are already materialized in
439
     * the set before sequencing, so later elements are not inspected but were already evaluated).
440
     *
441
     * @param <T> the ok value type
442
     * @param <E> the error type
443
     * @param nes a {@code NonEmptySet<Result<T, E>>}; must not be {@code null}
444
     * @return {@code ok(NonEmptySet<T>)} if all succeed, {@code err(E)} otherwise
445
     * @throws NullPointerException if {@code nes} is {@code null}
446
     */
447
    public static <T, E> Result<NonEmptySet<T>, E> sequenceResult(NonEmptySet<Result<T, E>> nes) {
448
        Objects.requireNonNull(nes, "nes must not be null");
4✔
449
        Set<T> result = new LinkedHashSet<>();
4✔
450
        for (Result<T, E> r : nes) {
10✔
451
            switch (r) {
11✔
452
                case Result.Err<T, E> err -> { return Result.err(err.error()); }
7✔
453
                case Result.Ok<T, E> ok   -> result.add(ok.value());
8✔
454
            }
455
        }
1✔
456
        return Result.ok(fromSetUnsafe(result)); // always non-empty since nes.size() >= 1
4✔
457
    }
458

459
    // -------------------------------------------------------------------------
460
    // Iterable
461
    // -------------------------------------------------------------------------
462

463
    /**
464
     * Returns an iterator over all elements (head first, then tail in insertion order).
465
     *
466
     * @return an iterator
467
     */
468
    @Override
469
    public Iterator<T> iterator() {
470
        return new Iterator<T>() {
14✔
471
            private boolean headConsumed = false;
3✔
472
            private final Iterator<T> tailIt = tail.iterator();
6✔
473

474
            @Override
475
            public boolean hasNext() {
476
                return !headConsumed || tailIt.hasNext();
11✔
477
            }
478

479
            @Override
480
            public T next() {
481
                if (!headConsumed) {
3✔
482
                    headConsumed = true;
3✔
483
                    return head;
4✔
484
                }
485
                return tailIt.next();
4✔
486
            }
487
        };
488
    }
489

490
    // -------------------------------------------------------------------------
491
    // Object
492
    // -------------------------------------------------------------------------
493

494
    @Override
495
    public boolean equals(Object obj) {
496
        if (this == obj) return true;
3!
497
        if (!(obj instanceof NonEmptySet<?> other)) return false;
7!
498
        return toSet().equals(other.toSet());
6✔
499
    }
500

501
    @Override
502
    public int hashCode() {
503
        return toSet().hashCode();
4✔
504
    }
505

506
    @Override
507
    public String toString() {
508
        return toSet().toString();
4✔
509
    }
510
}
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