@@ -0,0 +1,3 @@
|
|||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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!
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user