Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.5 — Implemented Currency Routes & Caching #12

Merged
merged 7 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 0.5
- Added `Currency`
- Added `Restrr#retrieveAllCurrencies`
- Added `Restrr#createCurrency`
- Added `Restrr#retrieveCurrencyById`
- Added `Restrr#deleteCurrencyById`
- Added `Restrr#updateCurrencyById`
- Added `Restrr#retrieveSelf`
- Implemented (Batch)CacheViews (some retrieve methods now have a `forceRetrieve` parameter)

## 0.4.2
- Fixed missing `isWeb` in `RestrrBuilder#create`

Expand Down
1 change: 1 addition & 0 deletions lib/restrr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export 'src/entity_builder.dart';
export 'src/restrr_base.dart';

/* [ /src/entities ] */
export 'src/entities/currency.dart';
export 'src/entities/health_response.dart';
export 'src/entities/restrr_entity.dart';
export 'src/entities/user.dart';
Expand Down
13 changes: 13 additions & 0 deletions lib/src/cache/batch_cache_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import '../../restrr.dart';

class RestrrEntityBatchCacheView<T extends RestrrEntity> {
List<T>? _lastSnapshot;

List<T>? get() => _lastSnapshot;

void update(List<T> value) => _lastSnapshot = value;

void clear() => _lastSnapshot = null;

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

class RestrrEntityCacheView<T extends RestrrEntity> {
final Map<ID, T> _cache = {};

T? get(ID id) => _cache[id];

T cache(T value) => _cache[value.id] = value;

T? remove(ID id) => _cache.remove(id);

void clear() => _cache.clear();

bool contains(ID id) => _cache.containsKey(id);
}
42 changes: 42 additions & 0 deletions lib/src/entities/currency.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:restrr/restrr.dart';

abstract class Currency implements RestrrEntity {
String get name;
String get symbol;
String get isoCode;
int get decimalPlaces;
int? get user;

bool get isCustom;

bool isCreatedBy(User user);
}

class CurrencyImpl extends RestrrEntityImpl implements Currency {
@override
final String name;
@override
final String symbol;
@override
final String isoCode;
@override
final int decimalPlaces;
@override
final int? user;

const CurrencyImpl({
required super.api,
required super.id,
required this.name,
required this.symbol,
required this.isoCode,
required this.decimalPlaces,
required this.user,
});

@override
bool get isCustom => user != null;

@override
bool isCreatedBy(User user) => this.user == user.id;
}
7 changes: 7 additions & 0 deletions lib/src/entities/restrr_entity.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import '../../restrr.dart';

typedef ID = int;

/// The base class for all Restrr entities.
/// This simply provides a reference to the Restrr instance.
abstract class RestrrEntity {
/// A reference to the Restrr instance.
Restrr get api;

ID get id;
}

class RestrrEntityImpl implements RestrrEntity {
@override
final Restrr api;
@override
final ID id;

const RestrrEntityImpl({
required this.api,
required this.id,
});
}
5 changes: 1 addition & 4 deletions lib/src/entities/user.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import '../../restrr.dart';

abstract class User extends RestrrEntity {
int get id;
String get username;
String? get email;
String? get displayName;
Expand All @@ -14,8 +13,6 @@ abstract class User extends RestrrEntity {
}

class UserImpl extends RestrrEntityImpl implements User {
@override
final int id;
@override
final String username;
@override
Expand All @@ -29,7 +26,7 @@ class UserImpl extends RestrrEntityImpl implements User {

const UserImpl({
required super.api,
required this.id,
required super.id,
required this.username,
required this.email,
required this.displayName,
Expand Down
16 changes: 15 additions & 1 deletion lib/src/entity_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class EntityBuilder {
}

User buildUser(Map<String, dynamic> json) {
return UserImpl(
final UserImpl user = UserImpl(
api: api,
id: json['id'],
username: json['username'],
Expand All @@ -24,5 +24,19 @@ class EntityBuilder {
createdAt: DateTime.parse(json['created_at']),
isAdmin: json['is_admin'],
);
return api.userCache.cache(user);
}

Currency buildCurrency(Map<String, dynamic> json) {
final CurrencyImpl currency = CurrencyImpl(
api: api,
id: json['id'],
name: json['name'],
symbol: json['symbol'],
isoCode: json['iso_code'],
decimalPlaces: json['decimal_places'],
user: json['user'],
);
return api.currencyCache.cache(currency);
}
}
8 changes: 4 additions & 4 deletions lib/src/requests/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ class Route {

Route.patch(String path, {bool isVersioned = true}) : this._('PATCH', path, isVersioned: isVersioned);

CompiledRoute compile({List<String> params = const []}) {
CompiledRoute compile({List<dynamic> params = const []}) {
if (params.length != paramCount) {
throw ArgumentError(
'Error compiling route [$method $path}]: Incorrect amount of parameters! Expected: $paramCount, Provided: ${params.length}');
}
final Map<String, String> values = {};
String compiledRoute = path;
for (String param in params) {
for (dynamic param in params) {
int paramStart = compiledRoute.indexOf('{');
int paramEnd = compiledRoute.indexOf('}');
values[compiledRoute.substring(paramStart + 1, paramEnd)] = param;
compiledRoute = compiledRoute.replaceRange(paramStart, paramEnd + 1, param);
values[compiledRoute.substring(paramStart + 1, paramEnd)] = param.toString();
compiledRoute = compiledRoute.replaceRange(paramStart, paramEnd + 1, param.toString());
}
return CompiledRoute(this, compiledRoute, values);
}
Expand Down
10 changes: 10 additions & 0 deletions lib/src/requests/route_definitions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,13 @@ class UserRoutes {
static final Route logout = Route.delete('/user/logout');
static final Route register = Route.post('/user/register');
}

class CurrencyRoutes {
const CurrencyRoutes._();

static final Route retrieveAll = Route.get('/currency');
static final Route create = Route.post('/currency/{currencyId}');
static final Route retrieveById = Route.get('/currency/{currencyId}');
static final Route deleteById = Route.delete('/currency/{currencyId}');
static final Route updateById = Route.patch('/currency/{currencyId}');
}
118 changes: 111 additions & 7 deletions lib/src/restrr_base.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 'package:logging/logging.dart';
import 'package:restrr/src/cache/batch_cache_view.dart';
import 'package:restrr/src/requests/route.dart';
import 'package:restrr/src/service/api_service.dart';
import 'package:restrr/src/service/currency_service.dart';
import 'package:restrr/src/service/user_service.dart';

import '../restrr.dart';
import 'cache/cache_view.dart';

class RestrrOptions {
final bool isWeb;
Expand Down Expand Up @@ -59,7 +62,7 @@ class RestrrBuilder {

/// 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);
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);
Expand All @@ -73,7 +76,7 @@ class RestrrBuilder {
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);
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();
Expand All @@ -85,7 +88,7 @@ class RestrrBuilder {

/// Attempts to refresh the session with still saved credentials.
Future<RestResponse<RestrrImpl>> _handleSavedSession(RestrrImpl apiImpl) async {
final RestResponse<User> response = await apiImpl.userService.getSelf();
final RestResponse<User> response = await apiImpl._userService.getSelf();
if (response.hasError) {
Restrr.log.warning('Failed to refresh session');
return response.error?.toRestResponse(statusCode: response.statusCode) ?? RestrrError.unknown.toRestResponse();
Expand All @@ -108,8 +111,6 @@ abstract class Restrr {
/// The currently authenticated user.
User get selfUser;

Future<bool> logout();

/// Checks whether the given [uri] is valid and the API is healthy.
static Future<RestResponse<HealthResponse>> checkUri(Uri uri, {bool isWeb = false}) async {
return RequestHandler.request(
Expand All @@ -118,6 +119,23 @@ abstract class Restrr {
isWeb: isWeb,
routeOptions: RouteOptions(hostUri: uri));
}

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

/// Logs out the current user.
Future<bool> logout();

Future<List<Currency>?> retrieveAllCurrencies({bool forceRetrieve = false});

Future<Currency?> createCurrency(
{required String name, required String symbol, required String isoCode, required int decimalPlaces});

Future<Currency?> retrieveCurrencyById(ID id, {bool forceRetrieve = false});

Future<bool> deleteCurrencyById(ID id);

Future<Currency?> updateCurrencyById(ID id, {String? name, String? symbol, String? isoCode, int? decimalPlaces});
}

class RestrrImpl implements Restrr {
Expand All @@ -126,7 +144,17 @@ class RestrrImpl implements Restrr {
@override
final RouteOptions routeOptions;

late final UserService userService = UserService(api: this);
/* Services */

late final UserService _userService = UserService(api: this);
late final CurrencyService _currencyService = CurrencyService(api: this);

/* Caches */

late final RestrrEntityCacheView<User> userCache = RestrrEntityCacheView();
late final RestrrEntityCacheView<Currency> currencyCache = RestrrEntityCacheView();

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

RestrrImpl._({required this.options, required this.routeOptions});

Expand All @@ -136,13 +164,89 @@ class RestrrImpl implements Restrr {
@override
late final User selfUser;

@override
Future<User?> retrieveSelf({bool forceRetrieve = false}) async {
return _getOrRetrieveSingle(
key: selfUser.id,
cacheView: userCache,
retrieveFunction: (api) => api._userService.getSelf(),
forceRetrieve: forceRetrieve);
}

@override
Future<bool> logout() async {
final RestResponse<bool> response = await UserService(api: this).logout();
final RestResponse<bool> response = await _userService.logout();
if (response.hasData && response.data! && !options.isWeb) {
await CompiledRoute.cookieJar.deleteAll();
return true;
}
return false;
}

@override
Future<List<Currency>?> retrieveAllCurrencies({bool forceRetrieve = false}) async {
return _getOrRetrieveMulti(
batchCache: _currencyBatchCache,
retrieveFunction: (api) => api._currencyService.retrieveAllCurrencies(),
);
}

@override
Future<Currency?> createCurrency(
{required String name, required String symbol, required String isoCode, required int decimalPlaces}) async {
final RestResponse<Currency> response = await _currencyService.createCurrency(
name: name, symbol: symbol, isoCode: isoCode, decimalPlaces: decimalPlaces);
return response.data;
}

@override
Future<Currency?> retrieveCurrencyById(ID id, {bool forceRetrieve = false}) async {
return _getOrRetrieveSingle(
key: id,
cacheView: currencyCache,
retrieveFunction: (api) => api._currencyService.retrieveCurrencyById(id),
forceRetrieve: forceRetrieve);
}

@override
Future<bool> deleteCurrencyById(ID id) async {
final RestResponse<bool> response = await _currencyService.deleteCurrencyById(id);
return response.hasData && response.data!;
}

@override
Future<Currency?> updateCurrencyById(ID id,
{String? name, String? symbol, String? isoCode, int? decimalPlaces}) async {
final RestResponse<Currency> response = await _currencyService.updateCurrencyById(id,
name: name, symbol: symbol, isoCode: isoCode, decimalPlaces: decimalPlaces);
return response.data;
}

Future<T?> _getOrRetrieveSingle<T extends RestrrEntity>(
{required ID key,
required RestrrEntityCacheView<T> cacheView,
required Future<RestResponse<T>> Function(RestrrImpl) retrieveFunction,
bool forceRetrieve = false}) async {
if (!forceRetrieve && cacheView.contains(key)) {
return cacheView.get(key)!;
}
final RestResponse<T> response = await retrieveFunction.call(this);
return response.hasData ? response.data : null;
}

Future<List<T>?> _getOrRetrieveMulti<T extends RestrrEntity>(
{required RestrrEntityBatchCacheView<T> batchCache,
required Future<RestResponse<List<T>>> Function(RestrrImpl) retrieveFunction,
bool forceRetrieve = false}) async {
if (!forceRetrieve && batchCache.hasSnapshot) {
return batchCache.get()!;
}
final RestResponse<List<T>> response = await retrieveFunction.call(this);
if (response.hasData) {
final List<T> remote = response.data!;
batchCache.update(remote);
return remote;
}
return null;
}
}
Loading
Loading