Skip to content

Instantly share code, notes, and snippets.

@CassiusPacheco
Last active August 16, 2024 07:29
Show Gist options
  • Save CassiusPacheco/409e66e220ce563440df00385f39ac98 to your computer and use it in GitHub Desktop.
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
// 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');
});
});
}
@fredgrott
Copy link

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());

      return obj as Failure;
    } catch (e) {

      log(obj.toString());

      throw obj;
    }
  }),
);

}
}
`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment