Skip to content
This repository has been archived by the owner on Dec 30, 2024. It is now read-only.

Commit

Permalink
0.4 — Improved Route Handling (#9)
Browse files Browse the repository at this point in the history
* Added isWeb flag to Route#compile which disables caching and network checks

* Moved isWeb flag to ApiService

* Moved route config to `RouteOptions`

* Added `statusCode` to `RestResponse`

* Some more cleanup

* dart format
  • Loading branch information
jasonlessenich authored Feb 26, 2024
1 parent 533524c commit 6ea4e9e
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 124 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.4
- Added `statusCode` to `RestResponse`
- Moved route config to `RouteOptions`
- Removed export of `ApiService`s

## 0.3.3
- Fixed missing options param

Expand Down
5 changes: 0 additions & 5 deletions lib/restrr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,12 @@ export 'src/entities/restrr_entity.dart';
export 'src/entities/user.dart';

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

/* [ /src/requests/responses ] */
export 'src/requests/responses/restrr_errors.dart';
export 'src/requests/responses/rest_response.dart';

/* [ /src/service ] */
export 'src/service/api_service.dart';
export 'src/service/user_service.dart';

/* [ /src/utils ] */
export 'src/utils/io_utils.dart';
export 'src/utils/string_utils.dart';
4 changes: 3 additions & 1 deletion lib/src/requests/responses/rest_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import '../../../restrr.dart';
class RestResponse<T> {
final T? data;
final RestrrError? error;
final int? statusCode;

const RestResponse({this.data, this.error});
const RestResponse({this.data, this.error, required this.statusCode});

bool get hasData => data != null;
bool get hasError => error != null;
bool get hasStatusCode => statusCode != null;
}
2 changes: 1 addition & 1 deletion lib/src/requests/responses/restrr_errors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ enum RestrrError {

const RestrrError({this.clientError = false});

RestResponse<T> toRestResponse<T>() => RestResponse(error: this);
RestResponse<T> toRestResponse<T>({int? statusCode}) => RestResponse(error: this, statusCode: statusCode);
}
41 changes: 23 additions & 18 deletions lib/src/requests/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart';

import '../../restrr.dart';

class RouteOptions {
final Uri hostUri;
final int apiVersion;
const RouteOptions({required this.hostUri, this.apiVersion = -1});
}

class Route {
/// The HTTP method of this route.
final String method;
Expand Down Expand Up @@ -49,7 +55,7 @@ class Route {
}

class CompiledRoute {
static CookieJar? cookieJar;
static final CookieJar cookieJar = PersistCookieJar();

final Route baseRoute;
final String compiledRoute;
Expand All @@ -68,30 +74,29 @@ class CompiledRoute {
return CompiledRoute(baseRoute, newRoute, parameters, queryParameters: queryParameters);
}

Future<Response> submit({dynamic body, String contentType = 'application/json'}) {
if (!Restrr.hostInformation.hasHostUrl) {
throw StateError('Host URL is not set!');
Future<Response> submit(
{required RouteOptions routeOptions, dynamic body, bool isWeb = false, String contentType = 'application/json'}) {
if (baseRoute.isVersioned && routeOptions.apiVersion == -1) {
throw StateError('Cannot submit a versioned route without specifying the API version!');
}
Dio dio = Dio();
if (cookieJar != null) {
dio.interceptors.add(CookieManager(cookieJar!));
if (!isWeb) {
dio.interceptors.add(CookieManager(cookieJar));
}
Map<String, dynamic> headers = {};
headers['Content-Type'] = contentType;
return dio
.fetch(RequestOptions(
path: compiledRoute,
headers: headers,
data: body,
method: baseRoute.method.toString(),
baseUrl: _buildBaseUrl(Restrr.hostInformation, baseRoute.isVersioned)));
Map<String, dynamic> headers = {'Content-Type': contentType};
return dio.fetch(RequestOptions(
path: compiledRoute,
headers: headers,
data: body,
method: baseRoute.method.toString(),
baseUrl: _buildBaseUrl(routeOptions, baseRoute.isVersioned)));
}

String _buildBaseUrl(HostInformation hostInformation, bool isVersioned) {
String effectiveHostUrl = hostInformation.hostUri!.toString();
String _buildBaseUrl(RouteOptions options, bool isVersioned) {
String effectiveHostUrl = options.hostUri.toString();
if (effectiveHostUrl.endsWith('/')) {
effectiveHostUrl = effectiveHostUrl.substring(0, effectiveHostUrl.length - 1);
}
return isVersioned ? '$effectiveHostUrl/api/v${hostInformation.apiVersion}' : '$effectiveHostUrl/api';
return isVersioned ? '$effectiveHostUrl/api/v${options.apiVersion}' : '$effectiveHostUrl/api';
}
}
2 changes: 1 addition & 1 deletion lib/src/requests/route_definitions.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import '../../restrr.dart';
import 'package:restrr/src/requests/route.dart';

class StatusRoutes {
const StatusRoutes._();
Expand Down
119 changes: 49 additions & 70 deletions lib/src/restrr_base.dart
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
import 'package:cookie_jar/cookie_jar.dart';
import 'package:logging/logging.dart';
import 'package:restrr/src/requests/route.dart';
import 'package:restrr/src/service/api_service.dart';
import 'package:restrr/src/service/user_service.dart';

import '../restrr.dart';

class HostInformation {
final Uri? hostUri;
final int apiVersion;

bool get hasHostUrl => hostUri != null;

const HostInformation({required this.hostUri, this.apiVersion = 1});

const HostInformation.empty()
: hostUri = null,
apiVersion = -1;

HostInformation copyWith({Uri? hostUri, int? apiVersion}) {
return HostInformation(
hostUri: hostUri ?? this.hostUri,
apiVersion: apiVersion ?? this.apiVersion,
);
}
}

class RestrrOptions {
final bool isWeb;
final CookieJar? cookieJar;

const RestrrOptions({this.isWeb = false, this.cookieJar});

bool get canUseCookieJar => cookieJar != null && !isWeb;
const RestrrOptions({this.isWeb = false});
}

enum RestrrInitType { login, register, refresh }
enum RestrrInitType { login, register, savedSession }

/// A builder for creating a new [Restrr] instance.
/// The [Restrr] instance is created by calling [create].
Expand All @@ -54,102 +32,103 @@ class RestrrBuilder {
{required this.uri, required this.username, required this.password, this.email, this.displayName})
: initType = RestrrInitType.register;

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

/// Creates a new session with the given [uri].
Future<RestResponse<Restrr>> create() async {
CompiledRoute.cookieJar = options.canUseCookieJar ? options.cookieJar : null;
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);
if (statusResponse.hasError) {
Restrr.log.warning('Invalid financrr URI: $uri');
return statusResponse.error == RestrrError.unknown
? RestrrError.invalidUri.toRestResponse()
: statusResponse.error?.toRestResponse() ?? RestrrError.invalidUri.toRestResponse();
: statusResponse.error?.toRestResponse(statusCode: statusResponse.statusCode) ??
RestrrError.invalidUri.toRestResponse();
}
Restrr.log.info('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(username!, password!, email: email, displayName: displayName),
RestrrInitType.login => _handleLogin(username!, password!),
RestrrInitType.refresh => _handleRefresh(),
RestrrInitType.register =>
_handleRegistration(apiImpl, username!, password!, email: email, displayName: displayName),
RestrrInitType.login => _handleLogin(apiImpl, username!, password!),
RestrrInitType.savedSession => _handleSavedSession(apiImpl),
};
}

Future<RestResponse<RestrrImpl>> _handleLogin(String username, String password) async {
final RestrrImpl api = RestrrImpl._(options: options);
final RestResponse<User> userResponse = await UserService(api: api).login(username, password);
if (!userResponse.hasData) {
/// 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();
return RestrrError.invalidCredentials.toRestResponse(statusCode: response.statusCode);
}
api.selfUser = userResponse.data!;
Restrr.log.info('Successfully logged in as ${api.selfUser.username}');
return RestResponse(data: api);
apiImpl.selfUser = response.data!;
Restrr.log.info('Successfully logged in as ${apiImpl.selfUser.username}');
return RestResponse(data: apiImpl, statusCode: response.statusCode);
}

Future<RestResponse<RestrrImpl>> _handleRegistration(String username, String password,
/// Registers a new user and logs in.
Future<RestResponse<RestrrImpl>> _handleRegistration(RestrrImpl apiImpl, String username, String password,
{String? email, String? displayName}) async {
final RestrrImpl api = RestrrImpl._(options: options);
final RestResponse<User> response =
await UserService(api: api).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() ?? RestrrError.unknown.toRestResponse();
return response.error?.toRestResponse(statusCode: response.statusCode) ?? RestrrError.unknown.toRestResponse();
}
api.selfUser = response.data!;
Restrr.log.info('Successfully registered & logged in as ${api.selfUser.username}');
return RestResponse(data: api);
apiImpl.selfUser = response.data!;
Restrr.log.info('Successfully registered & logged in as ${apiImpl.selfUser.username}');
return RestResponse(data: apiImpl, statusCode: response.statusCode);
}

Future<RestResponse<RestrrImpl>> _handleRefresh() async {
final RestrrImpl api = RestrrImpl._(options: options);
final RestResponse<User> response = await UserService(api: api).getSelf();
/// Attempts to refresh the session with still saved credentials.
Future<RestResponse<RestrrImpl>> _handleSavedSession(RestrrImpl apiImpl) async {
final RestResponse<User> response = await apiImpl.userService.getSelf();
if (response.hasError) {
Restrr.log.warning('Failed to refresh session');
return response.error?.toRestResponse() ?? RestrrError.unknown.toRestResponse();
return response.error?.toRestResponse(statusCode: response.statusCode) ?? RestrrError.unknown.toRestResponse();
}
api.selfUser = response.data!;
Restrr.log.info('Successfully refreshed session for ${api.selfUser.username}');
return RestResponse(data: api);
apiImpl.selfUser = response.data!;
Restrr.log.info('Successfully refreshed session for ${apiImpl.selfUser.username}');
return RestResponse(data: apiImpl, statusCode: response.statusCode);
}
}

abstract class Restrr {
static final Logger log = Logger('Restrr');
static HostInformation hostInformation = HostInformation.empty();

/// Getter for the [EntityBuilder] of this [Restrr] instance.
EntityBuilder get entityBuilder;

RestrrOptions get options;
RouteOptions get routeOptions;

/// 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) async {
hostInformation = hostInformation.copyWith(hostUri: uri, apiVersion: -1);
return ApiService.request(
static Future<RestResponse<HealthResponse>> checkUri(Uri uri, {bool isWeb = false}) async {
return RequestHandler.request(
route: StatusRoutes.health.compile(),
mapper: (json) => EntityBuilder.buildHealthResponse(json)).then((response) {
if (response.hasData && response.data!.healthy) {
// if successful, update the API version
hostInformation = hostInformation.copyWith(apiVersion: response.data!.apiVersion);
}
return response;
});
mapper: (json) => EntityBuilder.buildHealthResponse(json),
isWeb: isWeb,
routeOptions: RouteOptions(hostUri: uri));
}
}

class RestrrImpl implements Restrr {
@override
final RestrrOptions options;
@override
final RouteOptions routeOptions;

late final UserService userService = UserService(api: this);

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

@override
late final EntityBuilder entityBuilder = EntityBuilder(api: this);
Expand All @@ -160,8 +139,8 @@ class RestrrImpl implements Restrr {
@override
Future<bool> logout() async {
final RestResponse<bool> response = await UserService(api: this).logout();
if (response.hasData && response.data! && options.canUseCookieJar) {
await CompiledRoute.cookieJar?.deleteAll();
if (response.hasData && response.data! && !options.isWeb) {
await CompiledRoute.cookieJar.deleteAll();
return true;
}
return false;
Expand Down
Loading

0 comments on commit 6ea4e9e

Please sign in to comment.