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

domix / dmx-fun / 25532127843

08 May 2026 01:51AM UTC coverage: 98.388% (+0.002%) from 98.386%
25532127843

push

github

web-flow
Merge pull request #385 from domix/feature/nonemptymap/interoperability/audit

feat(NonEmptyMap): add interop methods and reach full interoperability with dmx-fun and Java types

1070 of 1111 branches covered (96.31%)

Branch coverage included in aggregate %.

3019 of 3045 relevant lines covered (99.15%)

5.02 hits per line

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

97.33
core/lib/src/main/java/dmx/fun/NonEmptyMap.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.List;
8
import java.util.Map;
9
import java.util.Objects;
10
import java.util.Optional;
11
import java.util.function.BiFunction;
12
import java.util.function.BiPredicate;
13
import java.util.function.BinaryOperator;
14
import java.util.function.Function;
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 map: a map guaranteed at construction time to contain
21
 * at least one entry.
22
 *
23
 * <p>This type makes the non-emptiness constraint part of the static type system.
24
 * APIs that require at least one entry can declare {@code NonEmptyMap<K, V>} instead of
25
 * {@code Map<K, V>} and eliminate runtime emptiness checks entirely.
26
 *
27
 * <p>Insertion order is preserved (backed by {@link LinkedHashMap}). The first entry
28
 * inserted is the {@link #headKey()} / {@link #headValue()}.
29
 *
30
 * <p>This class is {@code @NullMarked}: all keys, values, and parameters are non-null
31
 * by default.
32
 *
33
 * @param <K> the type of keys
34
 * @param <V> the type of values
35
 */
36
@NullMarked
37
public final class NonEmptyMap<K, V> {
38

39
    private final K headKey;
40
    private final V headValue;
41
    private final Map<K, V> tail; // unmodifiable, does NOT include headKey
42

43
    private transient volatile Map<K, V> cachedMap;
44

45
    private NonEmptyMap(K headKey, V headValue, Map<K, V> tail) {
2✔
46
        this.headKey   = headKey;
3✔
47
        this.headValue = headValue;
3✔
48
        this.tail      = tail;
3✔
49
    }
1✔
50

51
    // -------------------------------------------------------------------------
52
    // Smart constructors
53
    // -------------------------------------------------------------------------
54

55
    /**
56
     * Creates a {@code NonEmptyMap} with the given head entry and additional entries.
57
     *
58
     * <p>If {@code rest} contains {@code key}, that duplicate is ignored — the provided
59
     * {@code value} is always used for the head key.
60
     *
61
     * @param key   the head key; must not be {@code null}
62
     * @param value the head value; must not be {@code null}
63
     * @param rest  additional entries; must not be {@code null}; keys and values must not be {@code null}
64
     * @param <K>   the key type
65
     * @param <V>   the value type
66
     * @return a new {@code NonEmptyMap}
67
     * @throws NullPointerException if {@code key}, {@code value}, {@code rest}, or any
68
     *                              key/value inside {@code rest} is {@code null}
69
     */
70
    public static <K, V> NonEmptyMap<K, V> of(K key, V value, Map<? extends K, ? extends V> rest) {
71
        Objects.requireNonNull(key,   "key must not be null");
4✔
72
        Objects.requireNonNull(value, "value must not be null");
4✔
73
        Objects.requireNonNull(rest,  "rest must not be null");
4✔
74
        Map<K, V> tail = new LinkedHashMap<>();
4✔
75
        rest.forEach((k, v) -> {
5✔
76
            Objects.requireNonNull(k, "rest keys must not be null");
4✔
77
            Objects.requireNonNull(v, "rest values must not be null");
4✔
78
            if (!k.equals(key)) {
4✔
79
                tail.put(k, v);
5✔
80
            }
81
        });
1✔
82
        return new NonEmptyMap<>(key, value, Collections.unmodifiableMap(tail));
8✔
83
    }
84

85
    /**
86
     * Creates a {@code NonEmptyMap} containing exactly one entry.
87
     *
88
     * @param key   the sole key; must not be {@code null}
89
     * @param value the sole value; must not be {@code null}
90
     * @param <K>   the key type
91
     * @param <V>   the value type
92
     * @return a singleton {@code NonEmptyMap}
93
     * @throws NullPointerException if {@code key} or {@code value} is {@code null}
94
     */
95
    public static <K, V> NonEmptyMap<K, V> singleton(K key, V value) {
96
        Objects.requireNonNull(key,   "key must not be null");
4✔
97
        Objects.requireNonNull(value, "value must not be null");
4✔
98
        return new NonEmptyMap<>(key, value, Map.of());
7✔
99
    }
100

101
    /**
102
     * Attempts to construct a {@code NonEmptyMap} from a plain {@link Map}.
103
     *
104
     * @param map  the source map; must not be {@code null}; keys and values must not be {@code null}
105
     * @param <K>  the key type
106
     * @param <V>  the value type
107
     * @return {@link Option#some(Object)} wrapping the {@code NonEmptyMap} if the map is
108
     *         non-empty, or {@link Option#none()} if the map is empty
109
     * @throws NullPointerException if {@code map} or any key/value is {@code null}
110
     */
111
    public static <K, V> Option<NonEmptyMap<K, V>> fromMap(Map<? extends K, ? extends V> map) {
112
        Objects.requireNonNull(map, "map must not be null");
4✔
113
        if (map.isEmpty()) {
3✔
114
            return Option.none();
2✔
115
        }
116
        map.forEach((k, v) -> {
3✔
117
            Objects.requireNonNull(k, "map keys must not be null");
4✔
118
            Objects.requireNonNull(v, "map values must not be null");
4✔
119
        });
1✔
120
        return Option.some(fromMapUnsafe(map));
4✔
121
    }
122

123
    /**
124
     * Attempts to construct a {@code NonEmptyMap} from a JDK {@link Optional} wrapping a
125
     * plain {@link Map}.
126
     *
127
     * <p>If the optional is present and the wrapped map is non-empty, returns
128
     * {@link Option#some(Object)} wrapping the {@code NonEmptyMap}. If the optional is empty,
129
     * or if the wrapped map is itself empty, returns {@link Option#none()}.
130
     *
131
     * @param optional the source optional; must not be {@code null}
132
     * @param <K>      the key type
133
     * @param <V>      the value type
134
     * @return {@link Option#some(Object)} if the optional is present and the map is non-empty,
135
     *         {@link Option#none()} otherwise
136
     * @throws NullPointerException if {@code optional} is {@code null}
137
     */
138
    public static <K, V> Option<NonEmptyMap<K, V>> fromOptional(
139
            Optional<? extends Map<? extends K, ? extends V>> optional) {
140
        Objects.requireNonNull(optional, "optional must not be null");
4✔
141
        return optional.isPresent() ? fromMap(optional.get()) : Option.none();
10✔
142
    }
143

144
    /**
145
     * Returns a {@link Collector} that accumulates a {@code Stream<T>} into an
146
     * {@code Option<NonEmptyMap<K, V>>} using the provided key and value extractor functions.
147
     *
148
     * <p>Produces {@link Option#some(Object)} for a non-empty stream and {@link Option#none()}
149
     * for an empty stream. If multiple stream elements map to the same key, later elements
150
     * overwrite earlier ones (last-write-wins semantics).
151
     *
152
     * <p>Example:
153
     * <pre>{@code
154
     * Option<NonEmptyMap<String, Integer>> result =
155
     *     employees.stream()
156
     *         .collect(NonEmptyMap.collector(Employee::name, Employee::score));
157
     * }</pre>
158
     *
159
     * @param <T>         the stream element type
160
     * @param <K>         the key type
161
     * @param <V>         the value type
162
     * @param keyMapper   extracts the key from each element; must not be {@code null} or return
163
     *                    {@code null}
164
     * @param valueMapper extracts the value from each element; must not be {@code null} or return
165
     *                    {@code null}
166
     * @return a collector producing {@code Option<NonEmptyMap<K, V>>}
167
     * @throws NullPointerException if {@code keyMapper} or {@code valueMapper} is {@code null}
168
     */
169
    public static <T, K, V> Collector<T, ?, Option<NonEmptyMap<K, V>>> collector(
170
            Function<? super T, ? extends K> keyMapper,
171
            Function<? super T, ? extends V> valueMapper) {
172
        Objects.requireNonNull(keyMapper,   "keyMapper must not be null");
4✔
173
        Objects.requireNonNull(valueMapper, "valueMapper must not be null");
4✔
174
        return Collector.of(
10✔
175
            LinkedHashMap<K, V>::new,
176
            (map, t) -> map.put(
6✔
177
                Objects.requireNonNull(keyMapper.apply(t),   "keyMapper must not return null"),
5✔
178
                Objects.requireNonNull(valueMapper.apply(t), "valueMapper must not return null")
3✔
179
            ),
180
            (a, b) -> { a.putAll(b); return a; },
×
181
            map -> NonEmptyMap.fromMap(map)
3✔
182
        );
183
    }
184

185
    /** Internal: builds from a known-non-empty map without Option wrapping. */
186
    private static <K, V> NonEmptyMap<K, V> fromMapUnsafe(Map<? extends K, ? extends V> map) {
187
        Iterator<? extends Map.Entry<? extends K, ? extends V>> it = map.entrySet().iterator();
4✔
188
        Map.Entry<? extends K, ? extends V> first = it.next();
4✔
189
        K headKey   = first.getKey();
3✔
190
        V headValue = first.getValue();
3✔
191
        Map<K, V> tail = new LinkedHashMap<>();
4✔
192
        while (it.hasNext()) {
3✔
193
            Map.Entry<? extends K, ? extends V> entry = it.next();
4✔
194
            tail.put(entry.getKey(), entry.getValue());
7✔
195
        }
1✔
196
        return new NonEmptyMap<>(headKey, headValue, Collections.unmodifiableMap(tail));
8✔
197
    }
198

199
    // -------------------------------------------------------------------------
200
    // Accessors
201
    // -------------------------------------------------------------------------
202

203
    /**
204
     * Returns the guaranteed head key of this map.
205
     *
206
     * @return the head key (never {@code null})
207
     */
208
    public K headKey() {
209
        return headKey;
3✔
210
    }
211

212
    /**
213
     * Returns the value associated with the head key.
214
     *
215
     * @return the head value (never {@code null})
216
     */
217
    public V headValue() {
218
        return headValue;
3✔
219
    }
220

221
    /**
222
     * Returns the number of entries in this map. Always &ge; 1.
223
     *
224
     * @return the size
225
     */
226
    public int size() {
227
        return 1 + tail.size();
6✔
228
    }
229

230
    /**
231
     * Returns the value associated with {@code key}, or {@link Option#none()} if absent.
232
     *
233
     * @param key the key to look up; must not be {@code null}
234
     * @return {@code Some(value)} if the key is present, {@code None} otherwise
235
     * @throws NullPointerException if {@code key} is {@code null}
236
     */
237
    public Option<V> get(K key) {
238
        Objects.requireNonNull(key, "key must not be null");
4✔
239
        if (headKey.equals(key)) return Option.some(headValue);
9✔
240
        return Option.ofNullable(tail.get(key));
6✔
241
    }
242

243
    /**
244
     * Returns {@code true} if this map contains an entry for {@code key}.
245
     *
246
     * @param key the key to test; must not be {@code null}
247
     * @return {@code true} if the key is present
248
     * @throws NullPointerException if {@code key} is {@code null}
249
     */
250
    public boolean containsKey(K key) {
251
        Objects.requireNonNull(key, "key must not be null");
4✔
252
        return headKey.equals(key) || tail.containsKey(key);
14✔
253
    }
254

255
    /**
256
     * Returns a {@link NonEmptySet} containing all keys of this map in insertion order.
257
     * The head key of this map is the head of the returned set.
258
     *
259
     * @return a {@code NonEmptySet<K>} of all keys (never {@code null})
260
     */
261
    public NonEmptySet<K> keySet() {
262
        return NonEmptySet.of(headKey, tail.keySet());
7✔
263
    }
264

265
    /**
266
     * Returns a {@link NonEmptyList} containing all values of this map in insertion order.
267
     * The head value of this map is the head of the returned list.
268
     * Duplicate values are preserved (unlike keys, values need not be unique).
269
     *
270
     * @return a {@code NonEmptyList<V>} of all values (never {@code null})
271
     */
272
    public NonEmptyList<V> values() {
273
        return NonEmptyList.of(headValue, new ArrayList<>(tail.values()));
10✔
274
    }
275

276
    /**
277
     * Returns an unmodifiable {@link Map} containing all entries (head entry first,
278
     * then tail entries in insertion order). The same instance is returned on repeated
279
     * calls (lazily initialized, thread-safe).
280
     *
281
     * @return an unmodifiable map with all entries
282
     */
283
    public Map<K, V> toMap() {
284
        Map<K, V> m = cachedMap;
3✔
285
        if (m == null) {
2✔
286
            synchronized (this) {
4✔
287
                m = cachedMap;
3✔
288
                if (m == null) {
2!
289
                    Map<K, V> result = new LinkedHashMap<>();
4✔
290
                    result.put(headKey, headValue);
7✔
291
                    result.putAll(tail);
4✔
292
                    cachedMap = m = Collections.unmodifiableMap(result);
6✔
293
                }
294
            }
3✔
295
        }
296
        return m;
2✔
297
    }
298

299
    /**
300
     * Returns a sequential {@link Stream} of all entries in insertion order.
301
     *
302
     * <p>The stream always contains at least one element (the head entry). Use it to bridge
303
     * {@code NonEmptyMap} into the standard Java stream API.
304
     *
305
     * @return a non-empty stream of {@link Map.Entry} elements in insertion order
306
     */
307
    public Stream<Map.Entry<K, V>> toStream() {
308
        return toMap().entrySet().stream();
5✔
309
    }
310

311
    // -------------------------------------------------------------------------
312
    // Transformations
313
    // -------------------------------------------------------------------------
314

315
    /**
316
     * Applies {@code mapper} to every value and returns a new {@code NonEmptyMap}
317
     * with the same keys and mapped values.
318
     *
319
     * @param mapper a non-null function to apply to each value; must not return {@code null}
320
     * @param <R>    the result value type
321
     * @return a new {@code NonEmptyMap} with mapped values
322
     * @throws NullPointerException if {@code mapper} is {@code null} or returns {@code null}
323
     */
324
    public <R> NonEmptyMap<K, R> mapValues(Function<? super V, ? extends R> mapper) {
325
        Objects.requireNonNull(mapper, "mapper must not be null");
4✔
326
        R newHead = Objects.requireNonNull(mapper.apply(headValue), "mapper must not return null");
7✔
327
        Map<K, R> newTail = new LinkedHashMap<>();
4✔
328
        tail.forEach((k, v) ->
6✔
329
            newTail.put(k, Objects.requireNonNull(mapper.apply(v), "mapper must not return null")));
10✔
330
        return new NonEmptyMap<>(headKey, newHead, Collections.unmodifiableMap(newTail));
9✔
331
    }
332

333
    /**
334
     * Applies {@code mapper} to every key-value pair and returns a new {@code NonEmptyMap}
335
     * with the same keys and mapped values.
336
     *
337
     * <p>Unlike {@link #mapValues(Function)}, the mapper receives both the key and the value,
338
     * enabling value transformations that depend on the key.
339
     *
340
     * @param mapper a non-null function to apply to each key-value pair; must not return
341
     *               {@code null}
342
     * @param <R>    the result value type
343
     * @return a new {@code NonEmptyMap} with mapped values
344
     * @throws NullPointerException if {@code mapper} is {@code null} or returns {@code null}
345
     */
346
    public <R> NonEmptyMap<K, R> mapValuesWithKey(BiFunction<? super K, ? super V, ? extends R> mapper) {
347
        Objects.requireNonNull(mapper, "mapper must not be null");
4✔
348
        R newHead = Objects.requireNonNull(mapper.apply(headKey, headValue), "mapper must not return null");
9✔
349
        Map<K, R> newTail = new LinkedHashMap<>();
4✔
350
        tail.forEach((k, v) ->
6✔
351
            newTail.put(k, Objects.requireNonNull(mapper.apply(k, v), "mapper must not return null")));
11✔
352
        return new NonEmptyMap<>(headKey, newHead, Collections.unmodifiableMap(newTail));
9✔
353
    }
354

355
    /**
356
     * Applies {@code mapper} to every key and returns a new {@code NonEmptyMap}
357
     * with the mapped keys and the original values.
358
     *
359
     * <p>If multiple keys map to the same new key, <strong>head-wins</strong> semantics apply:
360
     * the head entry is always preserved, and any tail entry whose mapped key collides with
361
     * the mapped head key is silently dropped.
362
     *
363
     * @param mapper a non-null function to apply to each key; must not return {@code null}
364
     * @param <R>    the result key type
365
     * @return a new {@code NonEmptyMap} with mapped keys
366
     * @throws NullPointerException if {@code mapper} is {@code null} or returns {@code null}
367
     */
368
    public <R> NonEmptyMap<R, V> mapKeys(Function<? super K, ? extends R> mapper) {
369
        Objects.requireNonNull(mapper, "mapper must not be null");
4✔
370
        R newHeadKey = Objects.requireNonNull(mapper.apply(headKey), "mapper must not return null");
7✔
371
        Map<R, V> newTail = new LinkedHashMap<>();
4✔
372
        tail.forEach((k, v) -> {
7✔
373
            R mapped = Objects.requireNonNull(mapper.apply(k), "mapper must not return null");
6✔
374
            if (!mapped.equals(newHeadKey)) {
4✔
375
                newTail.put(mapped, v);
5✔
376
            }
377
        });
1✔
378
        return new NonEmptyMap<>(newHeadKey, headValue, Collections.unmodifiableMap(newTail));
9✔
379
    }
380

381
    /**
382
     * Returns a new {@code NonEmptyMap} containing only entries for which {@code predicate}
383
     * returns {@code true}, wrapped in {@link Option#some(Object)}.
384
     * Returns {@link Option#none()} if no entries pass the predicate.
385
     *
386
     * @param predicate a non-null predicate to test each key-value pair
387
     * @return {@code Some(filteredMap)} if at least one entry passes, {@code None} otherwise
388
     * @throws NullPointerException if {@code predicate} is {@code null}
389
     */
390
    public Option<NonEmptyMap<K, V>> filter(BiPredicate<? super K, ? super V> predicate) {
391
        Objects.requireNonNull(predicate, "predicate must not be null");
4✔
392
        Map<K, V> result = new LinkedHashMap<>();
4✔
393
        if (predicate.test(headKey, headValue)) result.put(headKey, headValue);
14✔
394
        tail.forEach((k, v) -> { if (predicate.test(k, v)) result.put(k, v); });
17!
395
        return NonEmptyMap.fromMap(result);
3✔
396
    }
397

398
    /**
399
     * Returns a new {@code NonEmptyMap} that is the union of this map and {@code other}.
400
     * When both maps contain the same key, {@code mergeFunction} is applied to the two values.
401
     *
402
     * @param other         the other map; must not be {@code null}
403
     * @param mergeFunction function to resolve value conflicts; must not be {@code null};
404
     *                      must not return {@code null} (a null return would violate the
405
     *                      non-null value contract and is rejected immediately)
406
     * @return a new {@code NonEmptyMap} containing all entries from both maps
407
     * @throws NullPointerException if {@code other}, {@code mergeFunction}, or the result
408
     *                              of {@code mergeFunction} is {@code null}
409
     */
410
    public NonEmptyMap<K, V> merge(NonEmptyMap<K, V> other, BinaryOperator<V> mergeFunction) {
411
        Objects.requireNonNull(other,         "other must not be null");
4✔
412
        Objects.requireNonNull(mergeFunction, "mergeFunction must not be null");
4✔
413
        Map<K, V> combined = new LinkedHashMap<>(this.toMap());
6✔
414
        other.toMap().forEach((k, v) -> combined.merge(k, v,
14✔
415
            (oldVal, newVal) -> Objects.requireNonNull(
5✔
416
                mergeFunction.apply(oldVal, newVal), "mergeFunction must not return null")));
2✔
417
        return fromMapUnsafe(combined);
3✔
418
    }
419

420
    /**
421
     * Converts this map to a {@link NonEmptyList} of its entries in insertion order.
422
     *
423
     * @return a {@code NonEmptyList<Map.Entry<K, V>>}
424
     */
425
    public NonEmptyList<Map.Entry<K, V>> toNonEmptyList() {
426
        List<Map.Entry<K, V>> entries = new ArrayList<>(toMap().entrySet());
7✔
427
        Map.Entry<K, V> head = entries.get(0);
5✔
428
        List<Map.Entry<K, V>> tailList = entries.subList(1, entries.size());
6✔
429
        return NonEmptyList.of(head, List.copyOf(tailList));
5✔
430
    }
431

432
    // -------------------------------------------------------------------------
433
    // Interoperability — sequence
434
    // -------------------------------------------------------------------------
435

436
    /**
437
     * Sequences a {@code NonEmptyMap<K, Option<V>>} into an {@code Option<NonEmptyMap<K, V>>}.
438
     *
439
     * <p>Returns {@link Option#some(Object)} containing all unwrapped values if every entry's
440
     * value is {@code Some}; returns {@link Option#none()} as soon as any entry's value is
441
     * {@code None} (fail-fast in inspection — the method stops iterating after the first
442
     * {@code None}; entries are already materialized in the map before sequencing, so later
443
     * entries are not inspected but were already evaluated).
444
     *
445
     * @param <K> the key type
446
     * @param <V> the unwrapped value type
447
     * @param nem a {@code NonEmptyMap<K, Option<V>>}; must not be {@code null}
448
     * @return {@code Some(NonEmptyMap<K, V>)} if all values are present, {@code None} otherwise
449
     * @throws NullPointerException if {@code nem} is {@code null}
450
     */
451
    public static <K, V> Option<NonEmptyMap<K, V>> sequenceOption(NonEmptyMap<K, Option<V>> nem) {
452
        Objects.requireNonNull(nem, "nem must not be null");
4✔
453
        Map<K, V> result = new LinkedHashMap<>();
4✔
454
        for (Map.Entry<K, Option<V>> entry : nem.toMap().entrySet()) {
12✔
455
            switch (entry.getValue()) {
13✔
456
                case Option.None<V> ignored -> { return Option.none(); }
5✔
457
                case Option.Some<V>(V v)   -> result.put(entry.getKey(), v);
14✔
458
            }
459
        }
1✔
460
        return Option.some(fromMapUnsafe(result)); // always non-empty since nem.size() >= 1
4✔
461
    }
462

463
    /**
464
     * Sequences a {@code NonEmptyMap<K, Try<V>>} into a {@code Try<NonEmptyMap<K, V>>}.
465
     *
466
     * <p>Returns {@link Try#success(Object)} if all values succeed; returns
467
     * {@link Try#failure(Throwable)} from the first failing entry (fail-fast in inspection —
468
     * the method stops iterating after the first failure; entries are already materialized in
469
     * the map before sequencing, so later entries are not inspected but were already evaluated).
470
     *
471
     * @param <K> the key type
472
     * @param <V> the success value type
473
     * @param nem a {@code NonEmptyMap<K, Try<V>>}; must not be {@code null}
474
     * @return {@code Success(NonEmptyMap<K, V>)} if all succeed, {@code Failure} otherwise
475
     * @throws NullPointerException if {@code nem} is {@code null}
476
     */
477
    public static <K, V> Try<NonEmptyMap<K, V>> sequenceTry(NonEmptyMap<K, Try<V>> nem) {
478
        Objects.requireNonNull(nem, "nem must not be null");
4✔
479
        Map<K, V> result = new LinkedHashMap<>();
4✔
480
        for (Map.Entry<K, Try<V>> entry : nem.toMap().entrySet()) {
12✔
481
            switch (entry.getValue()) {
13✔
482
                case Try.Failure<V> f -> { return Try.failure(f.cause()); }
7✔
483
                case Try.Success<V> s -> result.put(entry.getKey(), s.value());
10✔
484
            }
485
        }
1✔
486
        return Try.success(fromMapUnsafe(result)); // always non-empty since nem.size() >= 1
4✔
487
    }
488

489
    /**
490
     * Sequences a {@code NonEmptyMap<K, Either<E, V>>} into an
491
     * {@code Either<E, NonEmptyMap<K, V>>}.
492
     *
493
     * <p>Returns {@link Either#right(Object)} if all values are right; returns
494
     * {@link Either#left(Object)} from the first left entry (fail-fast in inspection —
495
     * the method stops iterating after the first left; entries are already materialized in
496
     * the map before sequencing, so later entries are not inspected but were already evaluated).
497
     *
498
     * @param <K> the key type
499
     * @param <E> the left (error) type
500
     * @param <V> the right (success) type
501
     * @param nem a {@code NonEmptyMap<K, Either<E, V>>}; must not be {@code null}
502
     * @return {@code right(NonEmptyMap<K, V>)} if all are right, {@code left(E)} otherwise
503
     * @throws NullPointerException if {@code nem} is {@code null}
504
     */
505
    public static <K, E, V> Either<E, NonEmptyMap<K, V>> sequenceEither(
506
            NonEmptyMap<K, Either<E, V>> nem) {
507
        Objects.requireNonNull(nem, "nem must not be null");
4✔
508
        Map<K, V> result = new LinkedHashMap<>();
4✔
509
        for (Map.Entry<K, Either<E, V>> entry : nem.toMap().entrySet()) {
12✔
510
            switch (entry.getValue()) {
13✔
511
                case Either.Left<E, V> l  -> { return Either.left(l.value()); }
7✔
512
                case Either.Right<E, V> r -> result.put(entry.getKey(), r.value());
10✔
513
            }
514
        }
1✔
515
        return Either.right(fromMapUnsafe(result)); // always non-empty since nem.size() >= 1
4✔
516
    }
517

518
    /**
519
     * Sequences a {@code NonEmptyMap<K, Result<V, E>>} into a
520
     * {@code Result<NonEmptyMap<K, V>, E>}.
521
     *
522
     * <p>Returns {@link Result#ok(Object)} if all values are ok; returns
523
     * {@link Result#err(Object)} from the first error entry (fail-fast in inspection —
524
     * the method stops iterating after the first err; entries are already materialized in
525
     * the map before sequencing, so later entries are not inspected but were already evaluated).
526
     *
527
     * @param <K> the key type
528
     * @param <V> the ok value type
529
     * @param <E> the error type
530
     * @param nem a {@code NonEmptyMap<K, Result<V, E>>}; must not be {@code null}
531
     * @return {@code ok(NonEmptyMap<K, V>)} if all succeed, {@code err(E)} otherwise
532
     * @throws NullPointerException if {@code nem} is {@code null}
533
     */
534
    public static <K, V, E> Result<NonEmptyMap<K, V>, E> sequenceResult(
535
            NonEmptyMap<K, Result<V, E>> nem) {
536
        Objects.requireNonNull(nem, "nem must not be null");
4✔
537
        Map<K, V> result = new LinkedHashMap<>();
4✔
538
        for (Map.Entry<K, Result<V, E>> entry : nem.toMap().entrySet()) {
12✔
539
            switch (entry.getValue()) {
13✔
540
                case Result.Err<V, E> err -> { return Result.err(err.error()); }
7✔
541
                case Result.Ok<V, E> ok   -> result.put(entry.getKey(), ok.value());
10✔
542
            }
543
        }
1✔
544
        return Result.ok(fromMapUnsafe(result)); // always non-empty since nem.size() >= 1
4✔
545
    }
546

547
    // -------------------------------------------------------------------------
548
    // Object
549
    // -------------------------------------------------------------------------
550

551
    @Override
552
    public boolean equals(Object obj) {
553
        if (this == obj) return true;
3!
554
        if (!(obj instanceof NonEmptyMap<?, ?> other)) return false;
7!
555
        return toMap().equals(other.toMap());
6✔
556
    }
557

558
    @Override
559
    public int hashCode() {
560
        return toMap().hashCode();
4✔
561
    }
562

563
    @Override
564
    public String toString() {
565
        return toMap().toString();
4✔
566
    }
567
}
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