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

domix / dmx-fun / 25534247268

08 May 2026 03:03AM UTC coverage: 98.387% (-0.001%) from 98.388%
25534247268

push

github

web-flow
Merge pull request #386 from domix/feature/nonemptyset/interoperability/audit

feat(NonEmptySet): add interop methods, tests, and site docs (#293)

1090 of 1131 branches covered (96.37%)

Branch coverage included in aggregate %.

3059 of 3086 relevant lines covered (99.13%)

5.03 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
 * @param <T> the type of elements in this set
33
 */
34
@NullMarked
35
public final class NonEmptySet<T> implements Iterable<T> {
36

37
    private final T head;
38
    private final Set<T> tail; // unmodifiable, does NOT include head
39

40
    private transient volatile Set<T> cachedSet;
41

42
    private NonEmptySet(T head, Set<T> tail) {
2✔
43
        this.head = head;
3✔
44
        this.tail = tail;
3✔
45
    }
1✔
46

47
    // -------------------------------------------------------------------------
48
    // Smart constructors
49
    // -------------------------------------------------------------------------
50

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

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

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

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

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

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

160
    // -------------------------------------------------------------------------
161
    // Accessors
162
    // -------------------------------------------------------------------------
163

164
    /**
165
     * Returns the guaranteed head element of this set (the first inserted element).
166
     *
167
     * @return the head element (never {@code null})
168
     */
169
    public T head() {
170
        return head;
3✔
171
    }
172

173
    /**
174
     * Returns the number of elements in this set. Always &ge; 1.
175
     *
176
     * @return the size
177
     */
178
    public int size() {
179
        return 1 + tail.size();
6✔
180
    }
181

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

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

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

230
    // -------------------------------------------------------------------------
231
    // Transformations
232
    // -------------------------------------------------------------------------
233

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

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

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

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

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

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

345
    // -------------------------------------------------------------------------
346
    // Interoperability — sequence
347
    // -------------------------------------------------------------------------
348

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

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

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

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

454
    // -------------------------------------------------------------------------
455
    // Iterable
456
    // -------------------------------------------------------------------------
457

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

469
            @Override
470
            public boolean hasNext() {
471
                return !headConsumed || tailIt.hasNext();
11✔
472
            }
473

474
            @Override
475
            public T next() {
476
                if (!headConsumed) {
3✔
477
                    headConsumed = true;
3✔
478
                    return head;
4✔
479
                }
480
                return tailIt.next();
4✔
481
            }
482
        };
483
    }
484

485
    // -------------------------------------------------------------------------
486
    // Object
487
    // -------------------------------------------------------------------------
488

489
    @Override
490
    public boolean equals(Object obj) {
491
        if (this == obj) return true;
3!
492
        if (!(obj instanceof NonEmptySet<?> other)) return false;
7!
493
        return toSet().equals(other.toSet());
6✔
494
    }
495

496
    @Override
497
    public int hashCode() {
498
        return toSet().hashCode();
4✔
499
    }
500

501
    @Override
502
    public String toString() {
503
        return toSet().toString();
4✔
504
    }
505
}
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