v1.0.0 - Initial release #1

Merged
hans merged 8 commits from dev into main 2026-05-13 13:06:35 +02:00
11 changed files with 779 additions and 0 deletions
+287
View File
@@ -0,0 +1,287 @@
name: CI and Release
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
format:
name: format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Dart
uses: dart-lang/setup-dart@v1
- name: Install dependencies
run: dart pub get
- name: Check formatting
run: dart format --output=none --set-exit-if-changed .
analyze:
name: analyze
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Dart
uses: dart-lang/setup-dart@v1
- name: Install dependencies
run: dart pub get
- name: Run analyzer
run: dart analyze --fatal-infos
test:
name: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Dart
uses: dart-lang/setup-dart@v1
- name: Install dependencies
run: dart pub get
- name: Run tests
run: dart test
pana:
name: pana
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Dart
uses: dart-lang/setup-dart@v1
- name: Install dependencies
run: dart pub get
- name: Run pana and enforce full score
run: |
set -euo pipefail
dart pub global activate pana
export PATH="$PATH:$HOME/.pub-cache/bin"
pana . | tee /tmp/pana.log
SCORE_LINE="$(grep -Eo 'Points: [0-9]+/[0-9]+' /tmp/pana.log | tail -n1 || true)"
if [ -z "$SCORE_LINE" ]; then
echo "Could not parse pana score output."
exit 1
fi
SCORE="${SCORE_LINE#Points: }"
GOT="${SCORE%/*}"
MAX="${SCORE#*/}"
if [ "$GOT" -ne "$MAX" ]; then
echo "Pana score must be full. Got $SCORE."
exit 1
fi
echo "Pana full score confirmed: $SCORE"
version_and_changelog:
name: version-and-changelog
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate release metadata for PRs into main
run: |
set -euo pipefail
if [ "${GITHUB_EVENT_NAME}" != "pull_request" ]; then
echo "Not a pull_request event; skipping version/changelog gate."
exit 0
fi
if [ -z "${GITHUB_BASE_REF:-}" ]; then
echo "GITHUB_BASE_REF is not set for pull_request event."
exit 1
fi
git fetch origin "${GITHUB_BASE_REF}" --depth=1
PR_VERSION="$(sed -nE 's/^version:\s*([^[:space:]]+)\s*$/\1/p' pubspec.yaml | head -n1)"
BASE_HAS_PUBSPEC="false"
BASE_VERSION=""
if git cat-file -e "origin/${GITHUB_BASE_REF}:pubspec.yaml" 2>/dev/null; then
BASE_HAS_PUBSPEC="true"
BASE_VERSION="$(git show "origin/${GITHUB_BASE_REF}:pubspec.yaml" | sed -nE 's/^version:\s*([^[:space:]]+)\s*$/\1/p' | head -n1)"
fi
if [ -z "$PR_VERSION" ]; then
echo "Unable to read version from PR pubspec.yaml."
exit 1
fi
if [ "$BASE_HAS_PUBSPEC" = "true" ]; then
if [ -z "$BASE_VERSION" ]; then
echo "Unable to read version from base branch pubspec.yaml."
exit 1
fi
parse_semver_core() {
printf '%s' "$1" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/'
}
BASE_CORE="$(parse_semver_core "$BASE_VERSION")"
PR_CORE="$(parse_semver_core "$PR_VERSION")"
if ! printf '%s' "$BASE_CORE" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Base version is not valid semver: $BASE_VERSION"
exit 1
fi
if ! printf '%s' "$PR_CORE" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "PR version is not valid semver: $PR_VERSION"
exit 1
fi
BASE_MAJOR="${BASE_CORE%%.*}"
BASE_REST="${BASE_CORE#*.}"
BASE_MINOR="${BASE_REST%%.*}"
BASE_PATCH="${BASE_REST#*.}"
PR_MAJOR="${PR_CORE%%.*}"
PR_REST="${PR_CORE#*.}"
PR_MINOR="${PR_REST%%.*}"
PR_PATCH="${PR_REST#*.}"
if [ "$PR_MAJOR" -lt "$BASE_MAJOR" ]; then
echo "pubspec.yaml version must be greater than base version ($BASE_VERSION -> $PR_VERSION)."
exit 1
fi
if [ "$PR_MAJOR" -eq "$BASE_MAJOR" ] && [ "$PR_MINOR" -lt "$BASE_MINOR" ]; then
echo "pubspec.yaml version must be greater than base version ($BASE_VERSION -> $PR_VERSION)."
exit 1
fi
if [ "$PR_MAJOR" -eq "$BASE_MAJOR" ] && [ "$PR_MINOR" -eq "$BASE_MINOR" ] && [ "$PR_PATCH" -le "$BASE_PATCH" ]; then
echo "pubspec.yaml version must be at least 0.0.1 greater than base version ($BASE_VERSION -> $PR_VERSION)."
exit 1
fi
else
echo "Base branch has no pubspec.yaml; treating this as first release."
fi
if git diff --quiet "origin/${GITHUB_BASE_REF}...HEAD" -- CHANGELOG.md; then
echo "CHANGELOG.md must be updated in the PR."
exit 1
fi
if ! grep -Fq "## $PR_VERSION" CHANGELOG.md && ! grep -Fq "## [$PR_VERSION]" CHANGELOG.md; then
echo "CHANGELOG.md must include a heading for version $PR_VERSION."
exit 1
fi
if [ "$BASE_HAS_PUBSPEC" = "true" ]; then
echo "Version/changelog gate passed: $BASE_VERSION -> $PR_VERSION"
else
echo "Version/changelog gate passed for first release: $PR_VERSION"
fi
publish:
name: publish
runs-on: ubuntu-latest
needs:
- format
- analyze
- test
- pana
- version_and_changelog
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Dart
uses: dart-lang/setup-dart@v1
- name: Publish package and create Gitea release on push to main
env:
PUB_CREDENTIALS_JSON: ${{ secrets.PUB_CREDENTIALS_JSON }}
RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FALLBACK_RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -euo pipefail
if [ "${GITHUB_EVENT_NAME}" != "push" ] || [ "${GITHUB_REF}" != "refs/heads/main" ]; then
echo "Not a push to main; skipping publish."
exit 0
fi
PACKAGE_NAME="$(sed -nE 's/^name:\s*([^[:space:]]+)\s*$/\1/p' pubspec.yaml | head -n1)"
PACKAGE_VERSION="$(sed -nE 's/^version:\s*([^[:space:]]+)\s*$/\1/p' pubspec.yaml | head -n1)"
if [ -z "$PACKAGE_NAME" ] || [ -z "$PACKAGE_VERSION" ]; then
echo "Failed to parse package name/version from pubspec.yaml."
exit 1
fi
if curl -fsSL "https://pub.dev/api/packages/${PACKAGE_NAME}" | grep -q "\"version\":\"${PACKAGE_VERSION}\""; then
echo "${PACKAGE_NAME} ${PACKAGE_VERSION} is already published; skipping."
exit 0
fi
if [ -z "${PUB_CREDENTIALS_JSON:-}" ]; then
echo "Missing required secret PUB_CREDENTIALS_JSON."
exit 1
fi
mkdir -p "$HOME/.pub-cache"
printf '%s' "$PUB_CREDENTIALS_JSON" > "$HOME/.pub-cache/credentials.json"
chmod 600 "$HOME/.pub-cache/credentials.json"
dart pub get
dart pub publish --dry-run
dart pub publish --force
TOKEN="${RELEASE_TOKEN:-}"
if [ -z "$TOKEN" ]; then
TOKEN="${FALLBACK_RELEASE_TOKEN:-}"
fi
if [ -z "$TOKEN" ]; then
echo "Missing release token. Provide default workflow token or RELEASE_TOKEN."
exit 1
fi
SERVER_URL="${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-}}"
REPO_PATH="${GITHUB_REPOSITORY:-${GITEA_REPOSITORY:-}}"
if [ -z "$SERVER_URL" ] || [ -z "$REPO_PATH" ]; then
echo "Missing repository context required for Gitea API release creation."
exit 1
fi
TAG="v${PACKAGE_VERSION}"
RELEASE_BY_TAG_URL="${SERVER_URL}/api/v1/repos/${REPO_PATH}/releases/tags/${TAG}"
if curl -fsS -H "Authorization: token ${TOKEN}" "$RELEASE_BY_TAG_URL" >/dev/null; then
echo "Release ${TAG} already exists; skipping release creation."
exit 0
fi
CREATE_RELEASE_URL="${SERVER_URL}/api/v1/repos/${REPO_PATH}/releases"
RELEASE_PAYLOAD="{\"tag_name\":\"${TAG}\",\"target\":\"${GITHUB_SHA}\",\"target_commitish\":\"${GITHUB_SHA}\",\"title\":\"${TAG}\",\"name\":\"${TAG}\",\"note\":\"Release ${TAG}\",\"body\":\"Release ${TAG}\",\"draft\":false,\"prerelease\":false}"
HTTP_CODE="$(curl -sS -o /tmp/gitea-release-response.json -w '%{http_code}' \
-X POST "$CREATE_RELEASE_URL" \
-H "Authorization: token ${TOKEN}" \
-H 'Content-Type: application/json' \
-d "$RELEASE_PAYLOAD")"
if [ "$HTTP_CODE" = "201" ]; then
echo "Created Gitea release for ${TAG}."
elif [ "$HTTP_CODE" = "409" ]; then
echo "Release or tag ${TAG} already exists; treating as success."
else
echo "Failed to create Gitea release. HTTP ${HTTP_CODE}."
cat /tmp/gitea-release-response.json
exit 1
fi
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env sh
set -eu
printf '%s\n' 'Running pre-commit checks: dart format + dart analyze'
dart format --output=none --set-exit-if-changed .
dart analyze --fatal-infos
printf '%s\n' 'Pre-commit checks passed.'
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env sh
set -eu
printf '%s\n' 'Running pre-push checks: dart format + dart test'
dart format --output=none --set-exit-if-changed .
dart test
printf '%s\n' 'Pre-push checks passed.'
+3
View File
@@ -0,0 +1,3 @@
## 1.0.0
- Initial version.
+28
View File
@@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2026, Hans Kokx
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+89
View File
@@ -0,0 +1,89 @@
# list_or
Ever worked with an API that usually returns a list of items, but sometimes decides to return a single item just to keep you on your toes?
`list_or` is a lightweight Dart utility that fixes this exact headache. It takes an arbitrary dynamic value, whether it's a single object, an array, a `Set`, or even `null`, and normalizes it into a standard, predictable `List<T>`.
No more writing boilerplate type-checks for every flexible JSON payload.
## Features
* **Normalizes inputs:** Wraps single values into a list, or passes through existing iterables.
* **Fully mutable:** Implements `ListMixin`, meaning you can `.add()`, `.map()`, `.remove()`, and iterate over it just like any other Dart list.
* **Type-safe:** Strictly enforces your generic type `T`. If you expect a `String` and get an `int`, it throws immediately.
* **Smart null handling:** Explicitly supports `null` values only if you define the type as nullable (e.g., `ListOr<String?>`).
* **Clean stringification:** If the list only has one item, `toString()` prints just that item instead of adding `[ ]` brackets.
## Getting Started
Add the package to your `pubspec.yaml`:
```bash
dart pub add list_or
```
Or, if you're using Flutter:
```bash
flutter pub add list_or
```
Import it in your file:
```dart
import 'package:list_or/list_or.dart';
```
## Usage
The most common use case is parsing unpredictable JSON.
Imagine an API endpoint where the `tags` field might be a single string or an array of strings depending on how many tags the user added.
```dart
import 'package:list_or/list_or.dart';
void main() {
// Scenario A: The API returns a single string
final Map<String, dynamic> json1 = {'id': 1, 'tags': 'flutter'};
// Scenario B: The API returns an array of strings
final Map<String, dynamic> json2 = {'id': 2, 'tags': ['dart', 'backend']};
// ListOr normalizes both seamlessly.
final tags1 = ListOr<String>(json1['tags']);
final tags2 = ListOr<String>(json2['tags']);
print(tags1); // Output: flutter
print(tags2); // Output: [dart, backend]
// You can interact with it exactly like a normal list
tags1.add('mobile');
print(tags1.length); // Output: 2
print(tags1.contains('mobile')); // Output: true
}
```
### Handling Nulls
If your payload might intentionally contain nulls, just make your generic type
nullable:
```dart
// This works perfectly
final nullableList = ListOr<int?>(null);
print(nullableList.length); // Output: 1
// This will throw an ArgumentError to keep your types safe
final strictList = ListOr<int>(null);
```
## Additional Information
* **Found a bug?** Open an issue on the issue tracker.
* **Want to contribute?** Pull requests are welcome. Please ensure that all existing tests pass and add new tests for any new functionality.
+30
View File
@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
+56
View File
@@ -0,0 +1,56 @@
import 'package:list_or/list_or.dart';
void main() {
print('--- Example 1: Wrapping a Single Value ---');
// A single value is converted into a list containing just that value.
final singleItem = ListOr<String>('Hello World');
print('Length: ${singleItem.length}'); // Output: 1
print('First item: ${singleItem[0]}'); // Output: Hello World
// Notice the custom toString():it prints just the string, not ['Hello World']
print('String representation: $singleItem'); // Output: Hello World
print('\n--- Example 2: Wrapping an Iterable ---');
// A standard List or Set is normalized into a modifiable ListOr.
final multipleItems = ListOr<String>(['Apple', 'Banana']);
// Because it mixes in ListMixin, you can use all standard list methods.
multipleItems.add('Cherry');
print('Length: ${multipleItems.length}'); // Output: 3
// When the length is not exactly 1, toString() acts like a normal List.
print(
'String representation: $multipleItems',
); // Output: [Apple, Banana, Cherry]
print('\n--- Example 3: Parsing Flexible JSON Data ---');
// Imagine an API that irregularly returns 'tags' as either a String or a List.
final Map<String, dynamic> jsonResponse1 = {'id': 1, 'tags': 'flutter'};
final Map<String, dynamic> jsonResponse2 = {
'id': 2,
'tags': ['dart', 'backend'],
};
// ListOr cleanly normalizes both scenarios without complex type checking.
final tags1 = ListOr<String>(jsonResponse1['tags']);
final tags2 = ListOr<String>(jsonResponse2['tags']);
print(
'Tags 1 contains "flutter": ${tags1.contains('flutter')}',
); // Output: true
print('Tags 2 length: ${tags2.length}'); // Output: 2
print('\n--- Example 4: Handling Nulls with Nullable Types ---');
// If your data might explicitly be null and you want to keep that null
// as a valid single element, you must use a nullable generic type.
final nullableList = ListOr<int?>(null);
print('Nullable list length: ${nullableList.length}'); // Output: 1
print('First item: ${nullableList.first}'); // Output: null
// Note: Trying to do the following would throw an ArgumentError,
// keeping your types safe:
// final strictList = ListOr<int>(null); // THROWS!
}
+118
View File
@@ -0,0 +1,118 @@
/// A utility library for handling flexible data structures.
///
/// This library provides the `ListOr` class, which normalizes single values or
/// iterables into a unified `List` interface. It is particularly useful for
/// parsing JSON or dynamic data where a field might interchangeably be provided
/// as a single element or a collection of elements.
library;
import 'dart:collection';
/// A list implementation that normalizes a single value or an iterable into a
/// [List<T>].
///
/// This class mixes in [ListMixin] to provide a complete list API backed by an
/// internal list. It is particularly useful when parsing flexible data
/// structures (such as JSON) where a field might be provided as either a single
/// element or a collection of elements.
class ListOr<T> extends Object with ListMixin<T> {
/// The underlying list storing the elements.
final List<T> _items;
/// Creates a [ListOr] instance from an arbitrary [value].
///
/// The factory normalizes the [value] based on its type:
/// * If [value] is an [Iterable] (e.g., `List` or `Set`), it creates a new
/// modifiable list and casts the elements to [T].
/// * If [value] is exactly of type [T] (including `null` if [T] is nullable),
/// it wraps the value in a single-element list.
///
/// Throws an [ArgumentError] if [value] is `null` but the generic type [T] is
/// non-nullable.
/// Throws an [ArgumentError] if the [value] is neither of type [T] nor an
/// [Iterable] compatible with [T].
factory ListOr(Object? value) {
if (value is Iterable) {
return ListOr._(List<T>.from(value));
}
if (value is T) {
return ListOr._(<T>[value]);
}
if (value == null) {
throw ArgumentError(
'The type $T does not accept null values. '
'Pass a value of type $T or use ListOr<$T?> instead.',
);
}
throw ArgumentError(
'Value must be of type $T or Iterable containing $T. Got ${value.runtimeType}',
);
}
/// Internal constructor to initialize the backing list.
ListOr._(this._items);
/// Returns the number of elements in the list.
@override
int get length => _items.length;
/// Updates the length of the list.
@override
set length(int newLength) {
_items.length = newLength;
}
/// Returns the element at the given [index].
@override
T operator [](int index) => _items[index];
/// Sets the element at the given [index] to [value].
@override
void operator []=(int index, T value) {
_items[index] = value;
}
/// Adds [element] to the end of this list.
@override
void add(T element) => _items.add(element);
/// Appends all objects of [iterable] to the end of this list.
@override
void addAll(Iterable<T> iterable) => _items.addAll(iterable);
/// Inserts [element] at position [index] in this list.
@override
void insert(int index, T element) => _items.insert(index, element);
/// Removes the object at position [index] from this list.
///
/// Returns the removed element.
@override
T removeAt(int index) => _items.removeAt(index);
/// Removes the first occurrence of [element] from this list.
///
/// Returns `true` if [element] was in the list, `false` otherwise.
@override
bool remove(Object? element) => _items.remove(element);
/// Removes all objects from this list; the length of the list becomes zero.
@override
void clear() => _items.clear();
/// Returns a string representation of this object.
///
/// If the list contains exactly one element, this returns the string
/// representation of that single element. Otherwise, it returns the standard
/// bracketed list representation (e.g., `[a, b]` or `[]`).
@override
String toString() {
if (_items.length == 1) {
return _items.firstOrNull.toString();
}
return _items.toString();
}
}
+12
View File
@@ -0,0 +1,12 @@
name: list_or
description: A Dart utility that seamlessly normalizes single values and iterables into a unified, type-safe List.
version: 1.0.0
repository: https://git.hadak.org/dart/list_or
issue_tracker: https://git.hadak.org/dart/list_or/issues
environment:
sdk: ^3.0.0
dev_dependencies:
lints: ^6.0.0
test: ^1.25.6
+138
View File
@@ -0,0 +1,138 @@
import 'package:list_or/list_or.dart';
import 'package:test/test.dart';
void main() {
group('ListOr Initialization', () {
test('creates from a single value', () {
final list = ListOr<String>('hello');
expect(list.length, equals(1));
expect(list.first, equals('hello'));
expect(list, equals(['hello']));
});
test('creates from a List', () {
final list = ListOr<int>([1, 2, 3]);
expect(list.length, equals(3));
expect(list, equals([1, 2, 3]));
});
test('creates from a Set (Iterable)', () {
final list = ListOr<int>({4, 5, 6});
expect(list.length, equals(3));
expect(list, equals([4, 5, 6]));
});
test('handles null correctly when T is nullable', () {
final list = ListOr<int?>(null);
expect(list.length, equals(1));
expect(list.first, isNull);
expect(list, equals([null]));
});
test('throws ArgumentError when null is passed but T is non-nullable', () {
expect(
() => ListOr<int>(null),
throwsA(
isA<ArgumentError>().having(
(e) => e.message,
'message',
contains('does not accept null values'),
),
),
);
});
test('throws ArgumentError on incompatible types', () {
expect(
() => ListOr<String>(123),
throwsA(
isA<ArgumentError>().having(
(e) => e.message,
'message',
contains('Value must be of type'),
),
),
);
});
});
group('ListOr Mutations (Bug Fix Verification)', () {
test('can add to a single-initialized list without throwing', () {
final list = ListOr<String>('Apple');
list.add('Banana'); // This would throw prior to the explicit override fix
expect(list, equals(['Apple', 'Banana']));
});
test('can addAll from another iterable', () {
final list = ListOr<int>(1);
list.addAll([2, 3, 4]);
expect(list, equals([1, 2, 3, 4]));
});
test('can insert at a specific index', () {
final list = ListOr<String>(['A', 'C']);
list.insert(1, 'B');
expect(list, equals(['A', 'B', 'C']));
});
test('can update elements using index operator', () {
final list = ListOr<int>([10, 20, 30]);
list[1] = 99;
expect(list, equals([10, 99, 30]));
});
test('can remove a specific element', () {
final list = ListOr<String>(['A', 'B', 'C']);
final result = list.remove('B');
expect(result, isTrue);
expect(list, equals(['A', 'C']));
});
test('can remove at a specific index', () {
final list = ListOr<int>([1, 2, 3]);
final removed = list.removeAt(0);
expect(removed, equals(1));
expect(list, equals([2, 3]));
});
test('can clear the list entirely', () {
final list = ListOr<String>(['A', 'B']);
list.clear();
expect(list, isEmpty);
expect(list.length, equals(0));
});
});
group('ListOr toString formatting', () {
test('formats a single element as just the element string', () {
final list = ListOr<String>('hello');
expect(list.toString(), equals('hello'));
});
test('formats multiple elements with standard list brackets', () {
final list = ListOr<int>([1, 2, 3]);
expect(list.toString(), equals('[1, 2, 3]'));
});
test('formats empty list with standard empty brackets', () {
final list = ListOr<String>(<String>[]);
expect(list.toString(), equals('[]'));
});
test('formats single null element correctly', () {
final list = ListOr<String?>(null);
expect(list.toString(), equals('null'));
});
});
}