Skip to content

Commit

Permalink
v0.6 — Implemented Events (#13)
Browse files Browse the repository at this point in the history
* Implemented events

* Cleaned up auth process

* Bumped version & updated CHANGELOG.md

* Added RequestEvent

* Improved logging & added RequestEvent

* dart format
  • Loading branch information
jasonlessenich committed Feb 28, 2024
1 parent 090d671 commit 2a2dfcd
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 68 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.6
- Added `RestrrBuilder#on` & `Restrr#on`
- Added `ReadyEvent` & `RequestEvent`
- Added `RestrrOptions#disableLogging`
- Further improved Logging

## 0.5
- Added `Currency`
- Added `Restrr#retrieveAllCurrencies`
Expand Down
5 changes: 5 additions & 0 deletions lib/restrr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export 'src/entities/health_response.dart';
export 'src/entities/restrr_entity.dart';
export 'src/entities/user.dart';

/* [ /src/events ] */
export 'src/events/ready_event.dart';
export 'src/events/request_event.dart';
export 'src/events/restrr_event.dart';

/* [ /src/requests ] */
export 'src/requests/route_definitions.dart';

Expand Down
2 changes: 1 addition & 1 deletion lib/src/cache/batch_cache_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ class RestrrEntityBatchCacheView<T extends RestrrEntity> {
void clear() => _lastSnapshot = null;

bool get hasSnapshot => _lastSnapshot != null;
}
}
15 changes: 15 additions & 0 deletions lib/src/events/event_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:restrr/src/events/restrr_event.dart';

class RestrrEventHandler {
final Map<Type, Function> eventMap;

const RestrrEventHandler(this.eventMap);

void on<T extends RestrrEvent>(Function(T) callback) {
eventMap[T.runtimeType] = callback;
}

void fire<T extends RestrrEvent>(T event) {
eventMap[T.runtimeType]?.call(event);
}
}
5 changes: 5 additions & 0 deletions lib/src/events/ready_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'package:restrr/src/events/restrr_event.dart';

class ReadyEvent extends RestrrEvent {
const ReadyEvent({required super.api});
}
8 changes: 8 additions & 0 deletions lib/src/events/request_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:restrr/restrr.dart';

class RequestEvent extends RestrrEvent {
final String route;
final int? statusCode;

const RequestEvent({required super.api, required this.route, this.statusCode});
}
7 changes: 7 additions & 0 deletions lib/src/events/restrr_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import '../../restrr.dart';

abstract class RestrrEvent {
final Restrr api;

const RestrrEvent({required this.api});
}
94 changes: 49 additions & 45 deletions lib/src/restrr_base.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:logging/logging.dart';
import 'package:restrr/src/cache/batch_cache_view.dart';
import 'package:restrr/src/events/event_handler.dart';
import 'package:restrr/src/requests/route.dart';
import 'package:restrr/src/service/api_service.dart';
import 'package:restrr/src/service/currency_service.dart';
Expand All @@ -10,14 +11,17 @@ import 'cache/cache_view.dart';

class RestrrOptions {
final bool isWeb;
const RestrrOptions({this.isWeb = false});
final bool disableLogging;
const RestrrOptions({this.isWeb = false, this.disableLogging = false});
}

enum RestrrInitType { login, register, savedSession }

/// A builder for creating a new [Restrr] instance.
/// The [Restrr] instance is created by calling [create].
class RestrrBuilder {
final Map<Type, Function> _eventMap = {};

final RestrrInitType initType;
final Uri uri;
String? sessionId;
Expand All @@ -37,64 +41,53 @@ class RestrrBuilder {

RestrrBuilder.savedSession({required this.uri}) : initType = RestrrInitType.savedSession;

RestrrBuilder on<T extends RestrrEvent>(void Function(T) func) {
_eventMap[T.runtimeType] = func;
return this;
}

/// Creates a new session with the given [uri].
Future<RestResponse<Restrr>> create() async {
Restrr.log.info('Attempting to initialize a session (${initType.name}) with $uri');
// check if the URI is valid
final RestResponse<HealthResponse> statusResponse = await Restrr.checkUri(uri, isWeb: options.isWeb);
if (statusResponse.hasError) {
Restrr.log.warning('Invalid financrr URI: $uri');
return statusResponse.error == RestrrError.unknown
? RestrrError.invalidUri.toRestResponse()
: statusResponse.error?.toRestResponse(statusCode: statusResponse.statusCode) ??
RestrrError.invalidUri.toRestResponse();
return (statusResponse.error == RestrrError.unknown
? RestrrError.invalidUri
: statusResponse.error ?? RestrrError.invalidUri)
.toRestResponse(statusCode: statusResponse.statusCode);
}
Restrr.log.info('Host: $uri, API v${statusResponse.data!.apiVersion}');

Restrr.log.config('Host: $uri, API v${statusResponse.data!.apiVersion}');
final RestrrImpl apiImpl = RestrrImpl._(
options: options, routeOptions: RouteOptions(hostUri: uri, apiVersion: statusResponse.data!.apiVersion));
return switch (initType) {
RestrrInitType.register =>
_handleRegistration(apiImpl, username!, password!, email: email, displayName: displayName),
RestrrInitType.login => _handleLogin(apiImpl, username!, password!),
RestrrInitType.savedSession => _handleSavedSession(apiImpl),
options: options,
routeOptions: RouteOptions(hostUri: uri, apiVersion: statusResponse.data!.apiVersion),
eventMap: _eventMap);

// attempt to authenticate the user
final RestResponse<RestrrImpl> apiResponse = await switch (initType) {
RestrrInitType.register => _handleAuthProcess(apiImpl,
authFunction: () =>
apiImpl._userService.register(username!, password!, email: email, displayName: displayName)),
RestrrInitType.login =>
_handleAuthProcess(apiImpl, authFunction: () => apiImpl._userService.login(username!, password!)),
RestrrInitType.savedSession => _handleAuthProcess(apiImpl, authFunction: () => apiImpl._userService.getSelf()),
};
}

/// Logs in with the given [username] and [password].
Future<RestResponse<RestrrImpl>> _handleLogin(RestrrImpl apiImpl, String username, String password) async {
final RestResponse<User> response = await apiImpl._userService.login(username, password);
if (!response.hasData) {
Restrr.log.warning('Invalid credentials for user $username');
return RestrrError.invalidCredentials.toRestResponse(statusCode: response.statusCode);
}
apiImpl.selfUser = response.data!;
Restrr.log.info('Successfully logged in as ${apiImpl.selfUser.username}');
return RestResponse(data: apiImpl, statusCode: response.statusCode);
}

/// Registers a new user and logs in.
Future<RestResponse<RestrrImpl>> _handleRegistration(RestrrImpl apiImpl, String username, String password,
{String? email, String? displayName}) async {
final RestResponse<User> response =
await apiImpl._userService.register(username, password, email: email, displayName: displayName);
if (response.hasError) {
Restrr.log.warning('Failed to register user $username');
return response.error?.toRestResponse(statusCode: response.statusCode) ?? RestrrError.unknown.toRestResponse();
// fire [ReadyEvent] if the API is ready
if (apiResponse.hasData) {
apiImpl.eventHandler.fire(ReadyEvent(api: apiImpl));
}
apiImpl.selfUser = response.data!;
Restrr.log.info('Successfully registered & logged in as ${apiImpl.selfUser.username}');
return RestResponse(data: apiImpl, statusCode: response.statusCode);
return apiResponse;
}

/// Attempts to refresh the session with still saved credentials.
Future<RestResponse<RestrrImpl>> _handleSavedSession(RestrrImpl apiImpl) async {
final RestResponse<User> response = await apiImpl._userService.getSelf();
Future<RestResponse<RestrrImpl>> _handleAuthProcess(RestrrImpl apiImpl,
{required Future<RestResponse<User>> Function() authFunction}) async {
final RestResponse<User> response = await authFunction();
if (response.hasError) {
Restrr.log.warning('Failed to refresh session');
return response.error?.toRestResponse(statusCode: response.statusCode) ?? RestrrError.unknown.toRestResponse();
}
apiImpl.selfUser = response.data!;
Restrr.log.info('Successfully refreshed session for ${apiImpl.selfUser.username}');
return RestResponse(data: apiImpl, statusCode: response.statusCode);
}
}
Expand All @@ -105,6 +98,8 @@ abstract class Restrr {
/// Getter for the [EntityBuilder] of this [Restrr] instance.
EntityBuilder get entityBuilder;

RestrrEventHandler get eventHandler;

RestrrOptions get options;
RouteOptions get routeOptions;

Expand All @@ -120,6 +115,8 @@ abstract class Restrr {
routeOptions: RouteOptions(hostUri: uri));
}

void on<T extends RestrrEvent>(void Function(T) func);

/// Retrieves the currently authenticated user.
Future<User?> retrieveSelf({bool forceRetrieve = false});

Expand All @@ -144,6 +141,9 @@ class RestrrImpl implements Restrr {
@override
final RouteOptions routeOptions;

@override
late final RestrrEventHandler eventHandler;

/* Services */

late final UserService _userService = UserService(api: this);
Expand All @@ -156,14 +156,18 @@ class RestrrImpl implements Restrr {

late final RestrrEntityBatchCacheView<Currency> _currencyBatchCache = RestrrEntityBatchCacheView();

RestrrImpl._({required this.options, required this.routeOptions});
RestrrImpl._({required this.options, required this.routeOptions, required Map<Type, Function> eventMap})
: eventHandler = RestrrEventHandler(eventMap);

@override
late final EntityBuilder entityBuilder = EntityBuilder(api: this);

@override
late final User selfUser;

@override
void on<T extends RestrrEvent>(void Function(T) func) => eventHandler.on(func);

@override
Future<User?> retrieveSelf({bool forceRetrieve = false}) async {
return _getOrRetrieveSingle(
Expand All @@ -186,8 +190,8 @@ class RestrrImpl implements Restrr {
@override
Future<List<Currency>?> retrieveAllCurrencies({bool forceRetrieve = false}) async {
return _getOrRetrieveMulti(
batchCache: _currencyBatchCache,
retrieveFunction: (api) => api._currencyService.retrieveAllCurrencies(),
batchCache: _currencyBatchCache,
retrieveFunction: (api) => api._currencyService.retrieveAllCurrencies(),
);
}

Expand Down
60 changes: 39 additions & 21 deletions lib/src/service/api_service.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'dart:async';

import 'package:dio/dio.dart';
import 'package:logging/logging.dart';
import 'package:restrr/restrr.dart';

import '../requests/route.dart';
Expand Down Expand Up @@ -119,13 +122,14 @@ abstract class ApiService {
dynamic body,
String contentType = 'application/json'}) async {
return RequestHandler.request(
route: route,
routeOptions: api.routeOptions,
isWeb: api.options.isWeb,
mapper: mapper,
errorMap: errorMap,
body: body,
contentType: contentType);
route: route,
routeOptions: api.routeOptions,
isWeb: api.options.isWeb,
mapper: mapper,
errorMap: errorMap,
body: body,
contentType: contentType)
.then((response) => _fireEvent(route, response));
}

Future<RestResponse<bool>> noResponseRequest<T>(
Expand All @@ -134,12 +138,13 @@ abstract class ApiService {
Map<int, RestrrError> errorMap = const {},
String contentType = 'application/json'}) async {
return RequestHandler.noResponseRequest(
route: route,
routeOptions: api.routeOptions,
isWeb: api.options.isWeb,
body: body,
errorMap: errorMap,
contentType: contentType);
route: route,
routeOptions: api.routeOptions,
isWeb: api.options.isWeb,
body: body,
errorMap: errorMap,
contentType: contentType)
.then((response) => _fireEvent(route, response));
}

Future<RestResponse<List<T>>> multiRequest<T>(
Expand All @@ -150,13 +155,26 @@ abstract class ApiService {
dynamic body,
String contentType = 'application/json'}) async {
return RequestHandler.multiRequest(
route: route,
routeOptions: api.routeOptions,
isWeb: api.options.isWeb,
mapper: mapper,
errorMap: errorMap,
fullRequest: fullRequest,
body: body,
contentType: contentType);
route: route,
routeOptions: api.routeOptions,
isWeb: api.options.isWeb,
mapper: mapper,
errorMap: errorMap,
fullRequest: fullRequest,
body: body,
contentType: contentType)
.then((response) => _fireEvent(route, response));
}

Future<RestResponse<T>> _fireEvent<T>(CompiledRoute route, RestResponse<T> response) async {
if (!api.options.disableLogging) {
Restrr.log.log(
response.statusCode != null && response.statusCode! >= 400 ? Level.WARNING : Level.INFO,
'[${DateTime.now().toIso8601String()}] ${route.baseRoute.method} '
'${api.routeOptions.hostUri}${route.baseRoute.isVersioned ? '/api/v${api.routeOptions.apiVersion}' : ''}'
'${route.compiledRoute} => ${response.statusCode} (${response.hasData ? 'OK' : response.error?.name})');
}
api.eventHandler.fire(RequestEvent(api: api, route: route.compiledRoute, statusCode: response.statusCode));
return response;
}
}
1 change: 1 addition & 0 deletions lib/src/service/user_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class UserService extends ApiService {
},
mapper: (json) => api.entityBuilder.buildUser(json),
errorMap: {
404: RestrrError.invalidCredentials,
401: RestrrError.invalidCredentials,
});
}
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: restrr
description: Dart package which allows to communicate with the financrr REST API.
version: 0.5.0
version: 0.6.0
repository: https://github.com/financrr/restrr

environment:
Expand Down

0 comments on commit 2a2dfcd

Please sign in to comment.