• 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.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
 * <p>The rationale for introducing dedicated non-empty collection types instead of using
34
 * standard JDK types with runtime checks is documented in
35
 * <a href="https://domix.github.io/dmx-fun/adr/adr-018-non-empty-collections/">
36
 * ADR-018 — NonEmptyList&lt;T&gt;, NonEmptySet&lt;T&gt;, NonEmptyMap&lt;K,V&gt; as structural guarantee types</a>.
37
 *
38
 * @param <K> the type of keys
39
 * @param <V> the type of values
40
 */
41
@NullMarked
42
public final class NonEmptyMap<K, V> {
43

44
    private final K headKey;
45
    private final V headValue;
46
    private final Map<K, V> tail; // unmodifiable, does NOT include headKey
47

48
    private transient volatile Map<K, V> cachedMap;
49

50
    private NonEmptyMap(K headKey, V headValue, Map<K, V> tail) {
2✔
51
        this.headKey   = headKey;
3✔
52
        this.headValue = headValue;
3✔
53
        this.tail      = tail;
3✔
54
    }
1✔
55

56
    // -------------------------------------------------------------------------
57
    // Smart constructors
58
    // -------------------------------------------------------------------------
59

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

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

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

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

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

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

204
    // -------------------------------------------------------------------------
205
    // Accessors
206
    // -------------------------------------------------------------------------
207

208
    /**
209
     * Returns the guaranteed head key of this map.
210
     *
211
     * @return the head key (never {@code null})
212
     */
213
    public K headKey() {
214
        return headKey;
3✔
215
    }
216

217
    /**
218
     * Returns the value associated with the head key.
219
     *
220
     * @return the head value (never {@code null})
221
     */
222
    public V headValue() {
223
        return headValue;
3✔
224
    }
225

226
    /**
227
     * Returns the number of entries in this map. Always &ge; 1.
228
     *
229
     * @return the size
230
     */
231
    public int size() {
232
        return 1 + tail.size();
6✔
233
    }
234

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

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

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

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

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

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

316
    // -------------------------------------------------------------------------
317
    // Transformations
318
    // -------------------------------------------------------------------------
319

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

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

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

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

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

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

437
    // -------------------------------------------------------------------------
438
    // Interoperability — sequence
439
    // -------------------------------------------------------------------------
440

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

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

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

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

552
    // -------------------------------------------------------------------------
553
    // Object
554
    // -------------------------------------------------------------------------
555

556
    @Override
557
    public boolean equals(Object obj) {
558
        if (this == obj) return true;
3!
559
        if (!(obj instanceof NonEmptyMap<?, ?> other)) return false;
7!
560
        return toMap().equals(other.toMap());
6✔
561
    }
562

563
    @Override
564
    public int hashCode() {
565
        return toMap().hashCode();
4✔
566
    }
567

568
    @Override
569
    public String toString() {
570
        return toMap().toString();
4✔
571
    }
572
}
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