Skip to content
Merged
Next Next commit
🎨 added filter classes and re-organized use case helpers
  • Loading branch information
sarbagyastha committed Dec 22, 2022
commit c82c11f9f982e32f45c09e14770e1519eacd5b12
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import 'last_login_entity.dart';

class LastLoginUseCase extends UseCase<LastLoginEntity> {
LastLoginUseCase()
: super(entity: LastLoginEntity(), outputFilters: {
LastLoginUIOutput: _lastLoginUIOutput,
LastLoginCTAUIOutput: _lastLoginCTAUIOutput,
});

static LastLoginUIOutput _lastLoginUIOutput(LastLoginEntity entity) =>
LastLoginUIOutput(
lastLogin: entity.lastLogin,
);
: super(
entity: LastLoginEntity(),
outputFilters: {
LastLoginCTAUIOutput: _lastLoginCTAUIOutput,
},
filters: [LastLoginUIOutputFilter()],
);

static LastLoginCTAUIOutput _lastLoginCTAUIOutput(LastLoginEntity entity) =>
LastLoginCTAUIOutput(
Expand Down Expand Up @@ -57,3 +55,11 @@ class LastLoginDateInput extends SuccessInput {

LastLoginDateInput(this.lastLogin);
}

class LastLoginUIOutputFilter
extends OutputFilter<LastLoginEntity, LastLoginUIOutput> {
@override
LastLoginUIOutput transform(LastLoginEntity entity) {
return LastLoginUIOutput(lastLogin: entity.lastLogin);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ void main() {

// Subscription shortcut to mock a successful response from a Gateway

useCase.subscribe(
LastLoginDateOutput,
(_) => Right<FailureInput, LastLoginDateInput>(
LastLoginDateInput(currentDate)));
useCase.subscribe<LastLoginDateOutput, LastLoginDateInput>(
(_) => Right<FailureInput, LastLoginDateInput>(
LastLoginDateInput(currentDate),
),
);

var output = useCase.getOutput<LastLoginUIOutput>();
expect(output, LastLoginUIOutput(lastLogin: DateTime.parse('1900-01-01')));
Expand All @@ -35,10 +36,12 @@ void main() {
final useCase = LastLoginUseCase();

// Subscription shortcut to mock a failure in the response from a Gateway
useCase.subscribe(LastLoginDateOutput, (output) {
expect(output, LastLoginDateOutput());
return Left<FailureInput, LastLoginDateInput>(FailureInput());
});
useCase.subscribe<LastLoginDateOutput, LastLoginDateInput>(
(output) {
expect(output, LastLoginDateOutput());
return Left<FailureInput, LastLoginDateInput>(FailureInput());
},
);

await useCase.fetchCurrentDate();

Expand Down
10 changes: 4 additions & 6 deletions packages/clean_framework/lib/src/providers/gateway.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ abstract class Gateway<O extends Output, R extends Request,
'',
) {
_useCase = useCase ?? provider!.getUseCaseFromContext(context!);
_useCase.subscribe(
O,
(O output) async => _processRequest(buildRequest(output)),
_useCase.subscribe<O, S>(
(output) => _processRequest(buildRequest(output as O)),
);
}

Expand Down Expand Up @@ -54,9 +53,8 @@ abstract class BridgeGateway<SUBSCRIBER_OUTPUT extends Output,
required UseCase publisherUseCase,
}) : _subscriberUseCase = subscriberUseCase,
_publisherUseCase = publisherUseCase {
_subscriberUseCase.subscribe(
SUBSCRIBER_OUTPUT,
(SUBSCRIBER_OUTPUT output) {
_subscriberUseCase.subscribe<SUBSCRIBER_OUTPUT, SUBSCRIBER_INPUT>(
(output) {
return Right<FailureInput, SUBSCRIBER_INPUT>(
onResponse(
_publisherUseCase.getOutput<PUBLISHER_OUTPUT>(),
Expand Down
145 changes: 39 additions & 106 deletions packages/clean_framework/lib/src/providers/use_case.dart
Original file line number Diff line number Diff line change
@@ -1,34 +1,44 @@
import 'dart:async';

import 'package:clean_framework/clean_framework_providers.dart';
import 'package:either_dart/either.dart';
import 'package:equatable/equatable.dart';
import 'package:clean_framework/src/providers/entity.dart';
import 'package:clean_framework/src/providers/use_case/use_case_helpers.dart';
import 'package:clean_framework/src/providers/use_case_debounce_mixin.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:meta/meta.dart';

typedef OutputBuilder<T extends Entity> = Output Function(T);
export 'package:clean_framework/src/providers/use_case/use_case_helpers.dart';

abstract class UseCase<E extends Entity> extends StateNotifier<E> {
part 'use_case/helpers/use_case_filter.dart';

typedef InputCallback<E extends Entity, I extends Input> = E Function(I);

abstract class UseCase<E extends Entity> extends StateNotifier<E>
with UseCaseDebounceMixin {
UseCase({
required E entity,
Map<Type, OutputBuilder<E>>? outputFilters,
Map<Type, Function>? inputFilters,
}) : _outputFilters = outputFilters ?? const {},
_inputFilters = inputFilters ?? const {},
super(entity);

final Map<Type, OutputBuilder<E>> _outputFilters;
final Map<Type, Function> _inputFilters;
OutputFilterMap<E>? outputFilters,
InputFilterMap<E>? inputFilters,
List<UseCaseFilter<E>>? filters,
}) : _outputFilters = Map.of(outputFilters ?? const {}),
_inputFilters = Map.of(inputFilters ?? const {}),
super(entity) {
if (filters != null && filters.isNotEmpty) {
_outputFilters.addEntries(
filters.whereType<OutputFilter<E, Output>>().map((f) => f._entry),
);
_inputFilters.addEntries(
filters.whereType<InputFilter<E, Input>>().map((f) => f._entry),
);
}
}

final Map<Type, Function> _requestSubscriptions = {};
final Map<String, Timer> _debounceTimers = {};
final OutputFilterMap<E> _outputFilters;
final InputFilterMap<E> _inputFilters;
final RequestSubscriptionMap _requestSubscriptions = {};

@override
void dispose() {
for (final debounceTimer in _debounceTimers.values) {
debounceTimer.cancel();
}
_debounceTimers.clear();
clearDebounce();
super.dispose();
}

Expand All @@ -39,103 +49,26 @@ abstract class UseCase<E extends Entity> extends StateNotifier<E> {
@protected
set entity(E newEntity) => super.state = newEntity;

/// Executes the [action] so that it will only be executed
/// when there is no further repeated actions with same [tag]
/// in a given frame of [duration].
///
/// If [immediate] is false, then then first action will also be debounced.
@protected
void debounce({
required void Function() action,
required String tag,
Duration duration = const Duration(milliseconds: 300),
bool immediate = true,
}) {
final timer = _debounceTimers[tag];

final timerPending = timer?.isActive ?? false;
final canExecute = immediate && !timerPending;

timer?.cancel();
_debounceTimers[tag] = Timer(
duration,
() {
_debounceTimers.remove(tag);
if (!immediate) action();
},
);

if (canExecute) action();
}

O getOutput<O extends Output>() {
final filter = _outputFilters[O];
if (filter == null) {
throw StateError('Output filter not defined for $O');
}
return filter(entity) as O;
}
O getOutput<O extends Output>() => _outputFilters<O>(entity);

void setInput<I extends Input>(I input) {
if (_inputFilters[I] == null) {
throw StateError('Input processor not defined for $I');
}
final processor = _inputFilters[I]! as InputProcessor<I, E>;
entity = processor(input, entity);
entity = _inputFilters<I>(entity, input);
}

void subscribe(Type outputType, Function callback) {
if (_requestSubscriptions[outputType] != null) {
throw StateError('A subscription for $outputType already exists');
}
_requestSubscriptions[outputType] = callback;
void subscribe<O extends Output, I extends Input>(
RequestSubscription<I> subscription,
) {
_requestSubscriptions.add<O>(subscription);
}

@protected
Future<void> request<O extends Output, S extends SuccessInput>(
O output, {
required E Function(S successInput) onSuccess,
required E Function(FailureInput failureInput) onFailure,
required InputCallback<E, S> onSuccess,
required InputCallback<E, FailureInput> onFailure,
}) async {
final callback = _requestSubscriptions[O] ??
(_) => Left<NoSubscriptionFailureInput, S>(
NoSubscriptionFailureInput(O),
);
final input = await _requestSubscriptions<O, S>(output);

// ignore: avoid_dynamic_calls
final either = await callback(output) as Either<FailureInput, S>;
entity = either.fold(onFailure, onSuccess);
entity = input.fold(onFailure, onSuccess);
}
}

typedef InputCallback = void Function<I extends Input>(I input);

typedef InputProcessor<I extends Input, E extends Entity> = E Function(
I input,
E entity,
);

typedef SubscriptionFilter<E extends Entity, V extends ViewModel> = V Function(
E entity,
);

@immutable
abstract class Output extends Equatable {
@override
bool get stringify => true;
}

@immutable
abstract class Input {}

class SuccessInput extends Input {}

class FailureInput extends Input {
FailureInput({this.message = ''});
final String message;
}

class NoSubscriptionFailureInput extends FailureInput {
NoSubscriptionFailureInput(Type t)
: super(message: 'No subscription exists for this request of $t');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:clean_framework/src/providers/use_case/helpers/output.dart';
import 'package:meta/meta.dart';

@immutable
abstract class Input {}

class SuccessInput extends Input {}

class FailureInput extends Input {
FailureInput({this.message = ''});

final String message;
}

class NoSubscriptionFailureInput<O extends Output> extends FailureInput {
NoSubscriptionFailureInput()
: super(message: 'No subscription exists for this request of $O');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:clean_framework/src/providers/entity.dart';
import 'package:clean_framework/src/providers/use_case/helpers/input.dart';

typedef InputProcessor<E extends Entity> = E Function(dynamic, E);

typedef InputFilterMap<E extends Entity> = Map<Type, InputProcessor<E>>;

extension InputFilterMapExtension<E extends Entity> on InputFilterMap<E> {
E call<I extends Input>(E entity, I input) {
final processor = this[I];

if (processor == null) {
throw StateError('Input processor not defined for $I');
}

return processor(input, entity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable
abstract class Output extends Equatable {
@override
bool get stringify => true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:clean_framework/src/providers/entity.dart';
import 'package:clean_framework/src/providers/use_case/helpers/output.dart';

typedef OutputBuilder<E extends Entity> = Output Function(E);

typedef OutputFilterMap<E extends Entity> = Map<Type, OutputBuilder<E>>;

extension OutputFilterMapExtension<E extends Entity> on OutputFilterMap<E> {
O call<O extends Output>(E entity) {
final builder = this[O];

if (builder == null) {
throw StateError(
'Output filter not defined for "$O".\n'
'Filters available for: ${keys.join(', ')}',
);
}

return builder(entity) as O;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'dart:async';

import 'package:clean_framework/src/providers/use_case/helpers/input.dart';
import 'package:clean_framework/src/providers/use_case/helpers/output.dart';
import 'package:either_dart/either.dart';

typedef RequestSubscriptionMap<I extends Input>
= Map<Type, RequestSubscription<I>>;

typedef Result<I extends Input> = FutureOr<Either<FailureInput, I>>;

typedef RequestSubscription<I extends Input> = Result<I> Function(dynamic);

extension RequestSubscriptionMapExtension<I extends Input>
on RequestSubscriptionMap<I> {
void add<O extends Output>(RequestSubscription<I> subscription) {
if (this[O] != null) {
throw StateError('A subscription for $O already exists');
}

this[O] = subscription;
}

Result<S> call<O extends Output, S extends SuccessInput>(
O output,
) async {
final subscription = this[O];

if (subscription == null) {
return Left<NoSubscriptionFailureInput, S>(
NoSubscriptionFailureInput<O>(),
);
}

final result = await subscription(output);
return result as Either<FailureInput, S>;
}
}
Loading