Last active
August 16, 2024 07:29
-
-
Save CassiusPacheco/409e66e220ce563440df00385f39ac98 to your computer and use it in GitHub Desktop.
Dart's DataResult<S> inspired by my Swift enum implementation and Avdosev's Dart Either implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
To jazz it up add this
`extension TaskX<T extends Either<Object, U>, U> on Task {
Task<Either<Failure, U>> mapLeftToFailure() {
// ignore: unnecessary_this
return this.map(
(either) => either.leftMap((obj) {
try {
log(obj.toString());
}
}
`