Dart's DataResult<S> inspired by my Swift enum implementation and Avdosev's Dart Either implementation
// The code below was inspired by my swift implementation https://gist.github.com/CassiusPacheco/4378d30d69316e4a6ba28a0c3af72628 | |
// and Avdosev's Dart Either https://github.com/avdosev/either_dart/blob/master/lib/src/either.dart | |
import 'package:equatable/equatable.dart'; | |
abstract class Failure extends Equatable implements Exception { | |
@override | |
String toString() => '$runtimeType Exception'; | |
@override | |
List<Object> get props => []; | |
} | |
// General failures | |
class GenericFailure extends Failure {} | |
class APIFailure extends Failure {} | |
/// This abstraction contains either a success data of generic type `S` or a | |
/// failure error of type `Failure` as its result. | |
/// | |
/// `data` property must only be retrieved when `DataResult` was constructed by | |
/// using `DataResult.success(value)`. It can be validated by calling | |
/// `isSuccess` first. Alternatively, `dataOrElse` can be used instead since it | |
/// ensures a valid value is returned in case the result is a failure. | |
/// | |
/// `error` must only be retrieved when `DataResult` was constructed by using | |
/// `DataResult.failure(error)`. It can be validated by calling `isFailure` | |
/// first. | |
abstract class DataResult<S> extends Equatable { | |
static DataResult<S> failure<S>(Failure failure) => _FailureResult(failure); | |
static DataResult<S> success<S>(S data) => _SuccessResult(data); | |
/// Get [error] value, returns null when the value is actually [data] | |
Failure? get error => fold<Failure?>((error) => error, (data) => null); | |
/// Get [data] value, returns null when the value is actually [error] | |
S? get data => fold<S?>((error) => null, (data) => data); | |
/// Returns `true` if the object is of the `SuccessResult` type, which means | |
/// `data` will return a valid result. | |
bool get isSuccess => this is _SuccessResult<S>; | |
/// Returns `true` if the object is of the `FailureResult` type, which means | |
/// `error` will return a valid result. | |
bool get isFailure => this is _FailureResult<S>; | |
/// Returns `data` if `isSuccess` returns `true`, otherwise it returns | |
/// `other`. | |
S dataOrElse(S other) => isSuccess && data != null ? data! : other; | |
/// Sugar syntax that calls `dataOrElse` under the hood. Returns left value if | |
/// `isSuccess` returns `true`, otherwise it returns the right value. | |
S operator |(S other) => dataOrElse(other); | |
/// Transforms values of [error] and [data] in new a `DataResult` type. Only | |
/// the matching function to the object type will be executed. For example, | |
/// for a `SuccessResult` object only the [fnData] function will be executed. | |
DataResult<T> either<T>( | |
Failure Function(Failure error) fnFailure, T Function(S data) fnData); | |
/// Transforms value of [data] allowing a new `DataResult` to be returned. | |
/// A `SuccessResult` might return a `FailureResult` and vice versa. | |
DataResult<T> then<T>(DataResult<T> Function(S data) fnData); | |
/// Transforms value of [data] always keeping the original identity of the | |
/// `DataResult`, meaning that a `SuccessResult` returns a `SuccessResult` and | |
/// a `FailureResult` always returns a `FailureResult`. | |
DataResult<T> map<T>(T Function(S data) fnData); | |
/// Folds [error] and [data] into the value of one type. Only the matching | |
/// function to the object type will be executed. For example, for a | |
/// `SuccessResult` object only the [fnData] function will be executed. | |
T fold<T>(T Function(Failure error) fnFailure, T Function(S data) fnData); | |
@override | |
List<Object?> get props => [if (isSuccess) data else error]; | |
} | |
/// Success implementation of `DataResult`. It contains `data`. It's abstracted | |
/// away by `DataResult`. It shouldn't be used directly in the app. | |
class _SuccessResult<S> extends DataResult<S> { | |
final S _value; | |
_SuccessResult(this._value); | |
@override | |
_SuccessResult<T> either<T>( | |
Failure Function(Failure error) fnFailure, T Function(S data) fnData) { | |
return _SuccessResult<T>(fnData(_value)); | |
} | |
@override | |
DataResult<T> then<T>(DataResult<T> Function(S data) fnData) { | |
return fnData(_value); | |
} | |
@override | |
_SuccessResult<T> map<T>(T Function(S data) fnData) { | |
return _SuccessResult<T>(fnData(_value)); | |
} | |
@override | |
T fold<T>(T Function(Failure error) fnFailure, T Function(S data) fnData) { | |
return fnData(_value); | |
} | |
} | |
/// Failure implementation of `DataResult`. It contains `error`. It's | |
/// abstracted away by `DataResult`. It shouldn't be used directly in the app. | |
class _FailureResult<S> extends DataResult<S> { | |
final Failure _value; | |
_FailureResult(this._value); | |
@override | |
_FailureResult<T> either<T>( | |
Failure Function(Failure error) fnFailure, T Function(S data) fnData) { | |
return _FailureResult<T>(fnFailure(_value)); | |
} | |
@override | |
_FailureResult<T> map<T>(T Function(S data) fnData) { | |
return _FailureResult<T>(_value); | |
} | |
@override | |
_FailureResult<T> then<T>(DataResult<T> Function(S data) fnData) { | |
return _FailureResult<T>(_value); | |
} | |
@override | |
T fold<T>(T Function(Failure error) fnFailure, T Function(S data) fnData) { | |
return fnFailure(_value); | |
} | |
} |
import 'package:test/test.dart'; | |
import 'package:app/data_result.dart'; | |
void main() { | |
group('DataResult', () { | |
test('gets the data when it is Success result', () { | |
const data = 'hello'; | |
final dataResult = DataResult.success(data); | |
expect(dataResult.data, data); | |
}); | |
test('data returns null when it is Failure result', () { | |
final dataResult = DataResult.failure(GenericFailure()); | |
expect(dataResult.data, null); | |
}); | |
test('`isSuccess` returns true for Success result', () { | |
const data = 'hello'; | |
final dataResult = DataResult.success(data); | |
expect(dataResult.isSuccess, true); | |
}); | |
test('`isSuccess` returns false for Failure result', () { | |
final dataResult = DataResult.failure(GenericFailure()); | |
expect(dataResult.isSuccess, false); | |
}); | |
test('dataOrElse returns `data` for Success result', () { | |
const data = 'foo'; | |
final dataResult = DataResult.success(data); | |
expect(dataResult.dataOrElse('bar'), 'foo'); | |
}); | |
test('dataOrElse returns else data for Failure result', () { | |
final dataResult = DataResult.failure(GenericFailure()); | |
expect(dataResult.dataOrElse('bar'), 'bar'); | |
}); | |
test('isFailure returns true for Failure result', () { | |
final dataResult = DataResult.failure(APIFailure()); | |
expect(dataResult.isFailure, true); | |
}); | |
test('isFailure returns false for Success result', () { | |
final dataResult = DataResult.success('something'); | |
expect(dataResult.isFailure, false); | |
}); | |
test('gets error when it is Failure result', () { | |
final dataResult = DataResult.failure(APIFailure()); | |
expect(dataResult.error, APIFailure()); | |
}); | |
test('failure returns null when it is Success result', () { | |
final dataResult = DataResult.success('something'); | |
expect(dataResult.error, null); | |
}); | |
}); | |
group('DataResult | operator', () { | |
test("returns existing value if it's Success result", () { | |
const data = 'foo'; | |
final dataResult = DataResult.success(data); | |
expect(dataResult | 'bar', 'foo'); | |
}); | |
test("returns other value if it's Failure result", () { | |
final dataResult = DataResult.failure(GenericFailure()); | |
expect(dataResult | 'bar', 'bar'); | |
}); | |
}); | |
group('DataResult', () { | |
test('should be equal when two success objects have equal data', () { | |
const data = 'hello'; | |
final dataResult = DataResult.success(data); | |
const data2 = 'hello'; | |
final dataResult2 = DataResult.success(data2); | |
expect(dataResult == dataResult2, true); | |
}); | |
test('should not be equal when two success objects have different data', | |
() { | |
const data = 'hello'; | |
final dataResult = DataResult.success(data); | |
const data2 = 'hello2'; | |
final dataResult2 = DataResult.success(data2); | |
expect(dataResult == dataResult2, false); | |
}); | |
test('should be equal when two failure objects have equal error', () { | |
final dataResult = DataResult.failure(APIFailure()); | |
final dataResult2 = DataResult.failure(APIFailure()); | |
expect(dataResult == dataResult2, true); | |
}); | |
test('should not be equal when two failure objects have different error', | |
() { | |
final dataResult = DataResult.failure(GenericFailure()); | |
final dataResult2 = DataResult.failure(APIFailure()); | |
expect(dataResult == dataResult2, false); | |
}); | |
}); | |
group('DataResult fold', () { | |
test('transforms failure into a false bool', () { | |
final result = DataResult.failure<String>(GenericFailure()) | |
.fold<bool>((failure) => false, (data) => true); | |
expect(result, false); | |
}); | |
test('transforms data into a true bool', () { | |
final result = DataResult.success('yo') | |
.fold<bool>((failure) => false, (data) => true); | |
expect(result, true); | |
}); | |
}); | |
group('DataResult then', () { | |
test('bubbles up failure instead of transforming the success value', () { | |
final result = DataResult.failure<String>(GenericFailure()) | |
.then((data) => DataResult.success(1.34)); | |
expect(result.isFailure, true); | |
expect(result.error, GenericFailure()); | |
}); | |
test('transforms data into a double value', () { | |
final result = | |
DataResult.success('yo').then((data) => DataResult.success(1.34)); | |
expect(result.isSuccess, true); | |
expect(result.data, 1.34); | |
}); | |
test('transforms data into a failure', () { | |
final result = DataResult.success<String>('yo') | |
.then((data) => DataResult.failure(APIFailure())); | |
expect(result.isSuccess, false); | |
expect(result.error, APIFailure()); | |
}); | |
}); | |
group('DataResult map', () { | |
test('bubbles up failure instead of transforming the success value', () { | |
final result = | |
DataResult.failure<String>(GenericFailure()).map((data) => 1.34); | |
expect(result.isFailure, true); | |
expect(result.error, GenericFailure()); | |
}); | |
test('transforms data into a double value', () { | |
final result = DataResult.success('yo').map((data) => 1.34); | |
expect(result.isSuccess, true); | |
expect(result.data, 1.34); | |
}); | |
test('can only transform data into a failure if it is the new data type', | |
() { | |
final result = DataResult.success('yo').map((data) => APIFailure()); | |
expect(result.isSuccess, true); | |
expect(result.data, APIFailure()); | |
}); | |
}); | |
group('DataResult either', () { | |
test('only executes error changing the error type for Failure result', () { | |
final result = DataResult.failure<String>(GenericFailure()).either( | |
(error) => APIFailure(), | |
(data) => throw Exception('This will never happen for failure'), | |
); | |
expect(result.isFailure, true); | |
expect(result.error, APIFailure()); | |
}); | |
test('only executes data for Success result', () { | |
final result = DataResult.success('yo').either( | |
(error) => throw Exception('This will never happen for success'), | |
(data) => 'new value here', | |
); | |
expect(result.isSuccess, true); | |
expect(result.data, 'new value here'); | |
}); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment