From 9286403b4940f7d29eacbb6bad090217a4a9d108 Mon Sep 17 00:00:00 2001 From: Matthias Beerens <3512339+Matthiee@users.noreply.github.com> Date: Tue, 26 Dec 2023 08:13:35 +0100 Subject: [PATCH] [Breaking] Improve request typing (#3) * Infer type from given request * Update changelog * Update readme * Add external integration tests * Bump `0.2.0` * Avoid minification * Add additional event handler --- CHANGELOG.md | 6 +- README.md | 3 +- example/example.dart | 37 ++--- .../handler/request_handler_store.dart | 19 +-- .../pipeline/pipeline_behavior_store.dart | 49 +++++-- .../pipeline/pipeline_configurator.dart | 6 +- lib/src/request/request_manager.dart | 17 +-- pubspec.yaml | 2 +- test/integration/choreography_test.dart | 3 +- test/integration/external_test.dart | 131 ++++++++++++++++++ test/integration/request_test.dart | 4 +- test/mocks.dart | 5 + test/test_data.dart | 11 +- .../pipeline_behavior_store_test.dart | 26 ++-- .../request_handler_store_test.dart | 10 +- test/unit/request/requests_manager_test.dart | 27 ++-- 16 files changed, 260 insertions(+), 96 deletions(-) create mode 100644 test/integration/external_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index cac2527..c99e38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## 0.1.1 - 2023-12-22 +## 0.2.0 + +- `RequestManager.send` now only accepts a single generic argument, `TResponse`, which is the type of the response body. The `TRequest` type argument has been removed. The type of the Response will be inferred based on the given `Request` (#3) + +## 0.1.1 - Add `registerFactory` and `registerFunction` methods to `RequestManager`. diff --git a/README.md b/README.md index 9d082d9..71780f2 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,7 @@ Future main() async { mediator.requests.register(MyQueryHandler()); - final response = await mediator.requests - .send(MyQuery()); + final Something response = await mediator.requests.send(MyQuery()); print(response); } diff --git a/example/example.dart b/example/example.dart index 2a20a3a..7125b00 100644 --- a/example/example.dart +++ b/example/example.dart @@ -20,17 +20,20 @@ Future main() async { .map((event) => event.count) .distinct() .subscribeFunction( - (count) => print('[$CountEvent handler] received count: $count'), + (count) => print('[CountEvent handler] received count: $count'), + ); + + mediator.events.on().subscribeFunction( + (count) => print('[Other Event Handler] received: $count'), ); const getUserQuery = GetUserByIdQuery(123); print('Sending $getUserQuery request'); - final resp = - await mediator.requests.send(getUserQuery); + final resp = await mediator.requests.send(getUserQuery); - print('Got $GetUserByIdQuery response: $resp'); + print('Got $getUserQuery response: $resp'); print('---'); @@ -38,7 +41,7 @@ Future main() async { print('Sending command $order66Command'); - await mediator.requests.send(order66Command); + await mediator.requests.send(order66Command); print('Command $order66Command completed'); @@ -58,7 +61,7 @@ class CountEvent implements DomainEvent { const CountEvent(this.count); @override - String toString() => '$CountEvent(count: $count)'; + String toString() => 'CountEvent(count: $count)'; } class MyCommand implements Command { @@ -66,15 +69,15 @@ class MyCommand implements Command { const MyCommand(this.command); @override - String toString() => '$MyCommand(command: $command)'; + String toString() => 'MyCommand(command: $command)'; } class MyCommandHandler implements CommandHandler { @override Future handle(MyCommand request) async { - print('[$MyCommandHandler] Executing "$request"'); + print('[MyCommandHandler] Executing "$request"'); await Future.delayed(const Duration(milliseconds: 500)); - print('[$MyCommandHandler] "$request" completed'); + print('[MyCommandHandler] "$request" completed'); } } @@ -83,15 +86,15 @@ class GetUserByIdQuery implements Query { const GetUserByIdQuery(this.userId); @override - String toString() => '$GetUserByIdQuery(userId: $userId)'; + String toString() => 'GetUserByIdQuery(userId: $userId)'; } class GetUserByIdQueryHandler implements QueryHandler { @override Future handle(GetUserByIdQuery request) async { - print('[$GetUserByIdQueryHandler] handeling $request'); + print('[GetUserByIdQueryHandler] handeling $request'); final user = await getUserByIdAsync(request.userId); - print('[$GetUserByIdQueryHandler] got $user'); + print('[GetUserByIdQueryHandler] got $user'); return user; } } @@ -100,10 +103,10 @@ class LoggingBehavior implements PipelineBehavior { @override Future handle(request, RequestHandlerDelegate next) async { try { - print('[$LoggingBehavior] [${request.runtimeType}] Before'); + print('[LoggingBehavior] [$request] Before'); return await next(); } finally { - print('[$LoggingBehavior] [${request.runtimeType}] After'); + print('[LoggingBehavior] [$request] After'); } } } @@ -115,7 +118,7 @@ class LoggingEventObserver implements EventObserver { Set> handlers, ) { print( - '[$LoggingEventObserver] onDispatch "$event" with ${handlers.length} handlers', + '[LoggingEventObserver] onDispatch "$event" with ${handlers.length} handlers', ); } @@ -126,7 +129,7 @@ class LoggingEventObserver implements EventObserver { Object error, StackTrace stackTrace, ) { - print('[$LoggingEventObserver] onError $event -> $handler ($error)'); + print('[LoggingEventObserver] onError $event -> $handler ($error)'); } @override @@ -143,7 +146,7 @@ class User { const User(this.id, this.name); @override - String toString() => '$User(id: $id, name: $name)'; + String toString() => 'User(id: $id, name: $name)'; } Future getUserByIdAsync(int id) async { diff --git a/lib/src/request/handler/request_handler_store.dart b/lib/src/request/handler/request_handler_store.dart index c8e3fc5..da367eb 100644 --- a/lib/src/request/handler/request_handler_store.dart +++ b/lib/src/request/handler/request_handler_store.dart @@ -51,23 +51,26 @@ class RequestHandlerStore { _handlers.remove(TRequest); } - /// Returns the registered [RequestHandler]'s for [TRequest]. - RequestHandler - getHandlerFor>() { - final handler = _handlers[TRequest] ?? _handlerFactories[TRequest]?.call(); + /// Returns the registered [RequestHandler]'s for [request]. + RequestHandler getHandlerFor( + Request request, + ) { + final requestType = request.runtimeType; + final handler = + _handlers[requestType] ?? _handlerFactories[requestType]?.call(); assert( handler != null, - 'getHandlerFor<$TResponse, $TRequest> did not have a registered handler. ' + 'getHandlerFor<$TResponse, $requestType> did not have a registered handler. ' 'Make sure to register the request handler first.', ); assert( - handler is RequestHandler, + handler is RequestHandler>, 'The registered handler is of the wrong type got $handler but was ' - 'expecting a type of RequestHandler<$TResponse, $TRequest>', + 'expecting a type of RequestHandler<$TResponse, $requestType>', ); - return handler as RequestHandler; + return handler!; } } diff --git a/lib/src/request/pipeline/pipeline_behavior_store.dart b/lib/src/request/pipeline/pipeline_behavior_store.dart index fd2b8ab..725bf41 100644 --- a/lib/src/request/pipeline/pipeline_behavior_store.dart +++ b/lib/src/request/pipeline/pipeline_behavior_store.dart @@ -3,23 +3,34 @@ import 'package:dart_mediator/src/request/pipeline/pipeline_configurator.dart'; import 'package:dart_mediator/src/request/pipeline/pipeline_behavior.dart'; class PipelineBehaviorStore implements PipelineConfigurator { - final _handlers = >{}; - final _handlerFactories = >{}; + final _handlers = >{}; + final _handlerFactories = >{}; final _genericHandlers = {}; final _genericHandlerFactories = {}; @override - void register( + void register>( PipelineBehavior behavior, ) { - _handlers.add(behavior); + final handlers = _handlers.putIfAbsent( + TRequest, + () => [], + ); + + handlers.add(behavior); } @override - void registerFactory( + void registerFactory>( PipelineBehaviorFactory factory, ) { - _handlerFactories.add(factory); + final handlers = _handlerFactories.putIfAbsent( + TRequest, + () => [], + ); + + handlers.add(factory); } @override @@ -38,29 +49,37 @@ class PipelineBehaviorStore implements PipelineConfigurator { @override void unregister(PipelineBehavior behavior) { - _handlers.remove(behavior); + for (final handlers in _handlers.values) { + handlers.remove(behavior); + } _genericHandlers.remove(behavior); } @override void unregisterFactory(PipelineBehaviorFactory factory) { - _handlerFactories.remove(factory); + for (final handlers in _handlerFactories.values) { + handlers.remove(factory); + } _genericHandlerFactories.remove(factory); } /// Returns all [PipelineBehavior]'s that match. - List getPipelines>() { - final handlerFactories = _handlerFactories - .whereType>() - .map((factory) => factory()); + List getPipelines( + Request request, + ) { + final requestType = request.runtimeType; + + final handlerFactories = + _handlerFactories[requestType]?.map((factory) => factory()); final genericFactories = _genericHandlerFactories.map((factory) => factory()); + final handlers = _handlers[requestType]; + return [ - ..._handlers.whereType>(), - ...handlerFactories, + if (handlers != null) ...handlers, + if (handlerFactories != null) ...handlerFactories, ..._genericHandlers, ...genericFactories, ]; diff --git a/lib/src/request/pipeline/pipeline_configurator.dart b/lib/src/request/pipeline/pipeline_configurator.dart index 383e312..90b8bec 100644 --- a/lib/src/request/pipeline/pipeline_configurator.dart +++ b/lib/src/request/pipeline/pipeline_configurator.dart @@ -1,4 +1,5 @@ import 'package:dart_mediator/src/request/pipeline/pipeline_behavior.dart'; +import 'package:dart_mediator/src/request/request.dart'; /// Factory to create a [PipelineBehavior]. typedef PipelineBehaviorFactory @@ -9,7 +10,7 @@ abstract interface class PipelineConfigurator { /// /// When using a generic [PipelineBehavior] the [registerGeneric] should be /// used instead. - void register( + void register>( PipelineBehavior behavior, ); @@ -17,7 +18,8 @@ abstract interface class PipelineConfigurator { /// /// When using a generic [PipelineBehavior] the [registerGenericFactory] should /// be used instead. - void registerFactory( + void registerFactory>( PipelineBehaviorFactory factory, ); diff --git a/lib/src/request/request_manager.dart b/lib/src/request/request_manager.dart index 36ef224..363a51e 100644 --- a/lib/src/request/request_manager.dart +++ b/lib/src/request/request_manager.dart @@ -57,14 +57,13 @@ class RequestManager { /// This request can be wrapped by [PipelineBehavior]'s see [pipeline]. /// /// This will return [TResponse]. - Future - send>( - TRequest request, + Future send( + Request request, ) async { - final handler = _requestHandlerStore.getHandlerFor(); + final handler = _requestHandlerStore.getHandlerFor(request) + as RequestHandler>; - final pipelines = - _pipelineBehaviorStore.getPipelines(); + final pipelines = _pipelineBehaviorStore.getPipelines(request); FutureOr handle() => handler.handle(request); @@ -73,11 +72,13 @@ class RequestManager { (next, pipeline) => () => pipeline.handle(request, next), ); - final response = await executionPlan(); + final futureOrResult = executionPlan(); + final response = + futureOrResult is Future ? await futureOrResult : futureOrResult; assert( response is TResponse, - '$TRequest expected a return type of $TResponse but ' + '$request expected a return type of $TResponse but ' 'got one of type ${response.runtimeType}. ' 'One of the registered pipelines is not correctly returning the ' '`next()` call. Pipelines used: $pipelines', diff --git a/pubspec.yaml b/pubspec.yaml index 5422079..c669ea9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: dart_mediator description: > A simple yet highly configurable Mediator implementation that allows sending requests and publishing events. -version: 0.1.1 +version: 0.2.0 repository: https://github.com/MatthiWare/mediator.dart environment: diff --git a/test/integration/choreography_test.dart b/test/integration/choreography_test.dart index 5e30950..b9c9f02 100644 --- a/test/integration/choreography_test.dart +++ b/test/integration/choreography_test.dart @@ -44,8 +44,7 @@ void main() { ), ); - final stock = await mediator.requests - .send, GetInventoryQuery>(GetInventoryQuery()); + final stock = await mediator.requests.send(GetInventoryQuery()); expect(stock, { 'mouse': 8, diff --git a/test/integration/external_test.dart b/test/integration/external_test.dart new file mode 100644 index 0000000..06b066b --- /dev/null +++ b/test/integration/external_test.dart @@ -0,0 +1,131 @@ +import 'package:dart_mediator/mediator.dart'; +import 'package:meta/meta.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + group('External Integration', () { + late MockMediator mockMediator; + late MockEventManager mockEventManager; + late MockRequestManager mockRequestManager; + late ExternalClass externalClass; + + setUp(() { + mockMediator = MockMediator(); + mockEventManager = MockEventManager(); + mockRequestManager = MockRequestManager(); + + when(() => mockMediator.events).thenReturn(mockEventManager); + when(() => mockMediator.requests).thenReturn(mockRequestManager); + + externalClass = ExternalClass(mockMediator); + }); + + setUpAll(() { + registerFallbackValue(MockEventHandler()); + }); + + group('subscribe', () { + test('should subscribe to event', () async { + // Arrange + final mockEventSubscriptionBuilder = + MockEventSubscriptionBuilder(); + + when(() => mockEventManager.on()) + .thenReturn(mockEventSubscriptionBuilder); + + when(() => mockEventSubscriptionBuilder.subscribe(any())) + .thenReturn(MockEventSubscription()); + + // Act + externalClass.subscribe(); + + // Assert + verify(() => mockEventSubscriptionBuilder + .subscribe(any>())); + }); + }); + + group('doSomeWorkAsync', () { + test('should send query and dispatch event', () async { + // Arrange + const query = DoSomethingQuery(1); + const event = WorkCompletedEvent('1'); + + when(() => mockRequestManager.send(query)).thenAnswer((_) async => '1'); + when(() => mockEventManager.dispatch(event)).thenAnswer((_) async {}); + + // Act + await externalClass.doSomeWorkAsync(); + + // Assert + verify(() => mockMediator.requests.send(query)); + verify(() => mockMediator.events.dispatch(event)); + }); + }); + }); +} + +@immutable +class DoSomethingQuery implements Query { + final int param; + + const DoSomethingQuery(this.param); + + @override + bool operator ==(Object other) { + return other is DoSomethingQuery && other.param == param; + } + + @override + int get hashCode => param.hashCode; +} + +class DoSomethingQueryHandler + implements QueryHandler { + @override + Future handle(DoSomethingQuery query) async { + return query.param.toString(); + } +} + +@immutable +class WorkCompletedEvent implements DomainEvent { + final String result; + + const WorkCompletedEvent(this.result); + + @override + bool operator ==(Object other) { + return other is WorkCompletedEvent && other.result == result; + } + + @override + int get hashCode => result.hashCode; +} + +class WorkCompletedEventHandler implements EventHandler { + @override + Future handle(WorkCompletedEvent event) async { + print(event.result); + } +} + +class ExternalClass { + final Mediator mediator; + + ExternalClass(this.mediator); + + void subscribe() { + mediator.events + .on() + .subscribe(WorkCompletedEventHandler()); + } + + Future doSomeWorkAsync() async { + final result = await mediator.requests.send(DoSomethingQuery(1)); + mediator.events.dispatch(WorkCompletedEvent(result)); + } +} diff --git a/test/integration/request_test.dart b/test/integration/request_test.dart index 037aade..c26a959 100644 --- a/test/integration/request_test.dart +++ b/test/integration/request_test.dart @@ -25,9 +25,7 @@ void main() { test('it handles the request', () async { mediator.requests.register(GetDataQueryHandler()); - final data = await mediator.requests.send( - GetDataQuery(123), - ); + final data = await mediator.requests.send(GetDataQuery(123)); expect(data, '123'); }); diff --git a/test/mocks.dart b/test/mocks.dart index 3907cfb..8aa28fe 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -7,6 +7,8 @@ class MockMediator extends Mock implements Mediator {} class MockRequestManager extends Mock implements RequestManager {} +class MockEventManager extends Mock implements EventManager {} + class MockDispatchStrategy extends Mock implements DispatchStrategy {} class MockEventHandlerStore extends Mock implements EventHandlerStore {} @@ -29,4 +31,7 @@ class MockPipelineBehavior extends Mock class MockEventSubscription extends Mock implements EventSubscription {} +class MockEventSubscriptionBuilder extends Mock + implements EventSubscriptionBuilder {} + final throwsAssertionError = throwsA(TypeMatcher()); diff --git a/test/test_data.dart b/test/test_data.dart index 929db17..109ab47 100644 --- a/test/test_data.dart +++ b/test/test_data.dart @@ -35,9 +35,12 @@ class WrappingBehavior implements PipelineBehavior { class DelayBehavior implements PipelineBehavior { @override Future handle(request, RequestHandlerDelegate next) async { - print('$DelayBehavior: Before'); - await Future.delayed(const Duration(milliseconds: 10)); - await next(); - print('$DelayBehavior: After'); + try { + print('$DelayBehavior: Before'); + await Future.delayed(const Duration(milliseconds: 10)); + return await next(); + } finally { + print('$DelayBehavior: After'); + } } } diff --git a/test/unit/request/pipeline_behavior/pipeline_behavior_store_test.dart b/test/unit/request/pipeline_behavior/pipeline_behavior_store_test.dart index 9fc215d..487915c 100644 --- a/test/unit/request/pipeline_behavior/pipeline_behavior_store_test.dart +++ b/test/unit/request/pipeline_behavior/pipeline_behavior_store_test.dart @@ -6,9 +6,11 @@ import '../../../mocks.dart'; void main() { group('PipelineBehaviorStore', () { late PipelineBehaviorStore pipelineBehaviorStore; + late MockRequest mockRequest; setUp(() { pipelineBehaviorStore = PipelineBehaviorStore(); + mockRequest = MockRequest(); }); group('register', () { @@ -21,7 +23,7 @@ void main() { ); expect( - pipelineBehaviorStore.getPipelines>(), + pipelineBehaviorStore.getPipelines(mockRequest), [mockBehavior], ); }); @@ -37,7 +39,7 @@ void main() { ); expect( - pipelineBehaviorStore.getPipelines>(), + pipelineBehaviorStore.getPipelines(mockRequest), [mockBehavior], ); }); @@ -53,7 +55,7 @@ void main() { ); expect( - pipelineBehaviorStore.getPipelines>(), + pipelineBehaviorStore.getPipelines(mockRequest), [mockBehavior], ); }); @@ -70,7 +72,7 @@ void main() { ); expect( - pipelineBehaviorStore.getPipelines>(), + pipelineBehaviorStore.getPipelines(mockRequest), [mockBehavior], ); }); @@ -89,7 +91,7 @@ void main() { ); expect( - pipelineBehaviorStore.getPipelines>(), + pipelineBehaviorStore.getPipelines(mockRequest), [], ); }); @@ -102,7 +104,7 @@ void main() { ); expect( - pipelineBehaviorStore.getPipelines>(), + pipelineBehaviorStore.getPipelines(mockRequest), [], ); }); @@ -122,7 +124,7 @@ void main() { ); expect( - pipelineBehaviorStore.getPipelines>(), + pipelineBehaviorStore.getPipelines(mockRequest), [], ); }); @@ -137,17 +139,17 @@ void main() { ); expect( - pipelineBehaviorStore.getPipelines>(), + pipelineBehaviorStore.getPipelines(mockRequest), [], ); }); }); - group('getPipelines{TResponse, TRequest}', () { - test('it returns the request handler', () { + group('getPipelines', () { + test('it returns the correct pipelines for the given request', () { final correctBehavior = MockPipelineBehavior>(); final incorrectBehavior = - MockPipelineBehavior>(); + MockPipelineBehavior>(); final logBehavior = MockPipelineBehavior(); correctFactory() => correctBehavior; @@ -162,7 +164,7 @@ void main() { pipelineBehaviorStore.registerFactory(incorrectFactory); expect( - pipelineBehaviorStore.getPipelines>(), + pipelineBehaviorStore.getPipelines(mockRequest), [correctBehavior, correctBehavior, logBehavior, logBehavior], ); }); diff --git a/test/unit/request/request_handler/request_handler_store_test.dart b/test/unit/request/request_handler/request_handler_store_test.dart index 2a1afda..db40260 100644 --- a/test/unit/request/request_handler/request_handler_store_test.dart +++ b/test/unit/request/request_handler/request_handler_store_test.dart @@ -67,7 +67,7 @@ void main() { group('getHandlerFor{TResponse, TRequest}', () { test('it throws when the handler does not exist', () { expect( - () => requestHandlerStore.getHandlerFor>(), + () => requestHandlerStore.getHandlerFor(MockRequest()), throwsAssertionError, ); }); @@ -79,7 +79,7 @@ void main() { requestHandlerStore.register(handlerWithWrongTypes); expect( - () => requestHandlerStore.getHandlerFor>(), + () => requestHandlerStore.getHandlerFor(MockRequest()), throwsAssertionError, ); }); @@ -89,8 +89,10 @@ void main() { requestHandlerStore.register(correctHandler); + final result = requestHandlerStore.getHandlerFor(MockRequest()); + expect( - requestHandlerStore.getHandlerFor>(), + result, correctHandler, ); }); @@ -102,7 +104,7 @@ void main() { requestHandlerStore.registerFactory(correctHandlerFactory); expect( - requestHandlerStore.getHandlerFor>(), + requestHandlerStore.getHandlerFor(MockRequest()), mockHandler, ); }); diff --git a/test/unit/request/requests_manager_test.dart b/test/unit/request/requests_manager_test.dart index 59ac15e..da87a34 100644 --- a/test/unit/request/requests_manager_test.dart +++ b/test/unit/request/requests_manager_test.dart @@ -88,15 +88,13 @@ void main() { test('it handles the request', () async { when(() => mockRequestHandler.handle(mockRequest)).thenReturn(output); - when(() => mockRequestHandlerStore - .getHandlerFor>()) + when(() => mockRequestHandlerStore.getHandlerFor(mockRequest)) .thenReturn(mockRequestHandler); - when(() => mockPipelineBehaviorStore - .getPipelines>()).thenReturn([]); + when(() => mockPipelineBehaviorStore.getPipelines(mockRequest)) + .thenReturn([]); - final result = await requestsManager - .send>(mockRequest); + final result = await requestsManager.send(mockRequest); verify(() => mockRequestHandler.handle(mockRequest)); @@ -115,8 +113,7 @@ void main() { when(() => mockRequestHandler.handle(mockRequest)).thenReturn(output); - when(() => mockRequestHandlerStore - .getHandlerFor>()) + when(() => mockRequestHandlerStore.getHandlerFor(mockRequest)) .thenReturn(mockRequestHandler); when(() => mockBehavior.handle(mockRequest, captureAny())) @@ -129,12 +126,10 @@ void main() { return handler(); }); - when(() => mockPipelineBehaviorStore - .getPipelines>()) + when(() => mockPipelineBehaviorStore.getPipelines(mockRequest)) .thenReturn([mockBehavior]); - final result = await requestsManager - .send>(mockRequest); + final result = await requestsManager.send(mockRequest); verify(() => mockRequestHandler.handle(mockRequest)); @@ -152,8 +147,7 @@ void main() { when(() => mockRequestHandler.handle(mockRequest)).thenReturn(output); - when(() => mockRequestHandlerStore - .getHandlerFor>()) + when(() => mockRequestHandlerStore.getHandlerFor(mockRequest)) .thenReturn(mockRequestHandler); when(() => mockWrongBehavior.handle(mockRequest, captureAny())) @@ -164,12 +158,11 @@ void main() { await handler(); // don't return on purpose }); - when(() => mockPipelineBehaviorStore - .getPipelines>()) + when(() => mockPipelineBehaviorStore.getPipelines(mockRequest)) .thenReturn([mockWrongBehavior]); await expectLater( - () => requestsManager.send>(mockRequest), + () => requestsManager.send(mockRequest), throwsAssertionError, ); });