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

Commit

Permalink
Further cleaned up package structure & added some docs
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonlessenich committed Feb 18, 2024
1 parent d88b250 commit e56fd25
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 91 deletions.
1 change: 1 addition & 0 deletions lib/restrr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export 'src/restrr_base.dart';

/* [ /src/entities ] */
export 'src/entities/health_response.dart';
export 'src/entities/restrr_entity.dart';
export 'src/entities/user.dart';

/* [ /src/requests ] */
Expand Down
17 changes: 17 additions & 0 deletions lib/src/entities/restrr_entity.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import '../../restrr.dart';

/// 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;
}

class RestrrEntityImpl implements RestrrEntity {
@override
final Restrr api;

const RestrrEntityImpl({
required this.api,
});
}
7 changes: 5 additions & 2 deletions lib/src/entities/user.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
abstract class User {
import '../../restrr.dart';

abstract class User extends RestrrEntity {
int get id;
String get username;
String? get email;
DateTime get createdAt;
bool get isAdmin;
}

class UserImpl implements User {
class UserImpl extends RestrrEntityImpl implements User {
@override
final int id;
@override
Expand All @@ -19,6 +21,7 @@ class UserImpl implements User {
final bool isAdmin;

const UserImpl({
required super.api,
required this.id,
required this.username,
required this.email,
Expand Down
5 changes: 3 additions & 2 deletions lib/src/entity_builder.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:restrr/restrr.dart';

/// Defines how to build entities from JSON responses.
class EntityBuilder {
final RestrrImpl api;

Expand All @@ -13,9 +14,9 @@ class EntityBuilder {
);
}

static User buildUser(Map<String, dynamic> json) {
Restrr.log.info(json);
User buildUser(Map<String, dynamic> json) {
return UserImpl(
api: api,
id: json['id'],
username: json['username'],
email: json['email'],
Expand Down
3 changes: 1 addition & 2 deletions lib/src/requests/responses/rest_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ class RestResponse<T> {

bool get hasData => data != null;

static Future<RestResponse<T>> fromError<T>(
Future<ErrorResponse> error) async {
static Future<RestResponse<T>> fromError<T>(Future<ErrorResponse> error) async {
return RestResponse(error: await error);
}
}
42 changes: 13 additions & 29 deletions lib/src/requests/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,15 @@ class Route {
: assert(StringUtils.count(path, '{') == StringUtils.count(path, '}')),
paramCount = StringUtils.count(path, '{');

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

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

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

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

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

static Future<ErrorResponse> asErrorResponse(DioException error) async {
// TODO: implement
Expand All @@ -45,8 +40,7 @@ class Route {
int paramStart = compiledRoute.indexOf('{');
int paramEnd = compiledRoute.indexOf('}');
values[compiledRoute.substring(paramStart + 1, paramEnd)] = param;
compiledRoute =
compiledRoute.replaceRange(paramStart, paramEnd + 1, param);
compiledRoute = compiledRoute.replaceRange(paramStart, paramEnd + 1, param);
}
return CompiledRoute(this, compiledRoute, values);
}
Expand All @@ -58,23 +52,19 @@ class CompiledRoute {
final Map<String, String> parameters;
Map<String, String>? queryParameters;

CompiledRoute(this.baseRoute, this.compiledRoute, this.parameters,
{this.queryParameters});
CompiledRoute(this.baseRoute, this.compiledRoute, this.parameters, {this.queryParameters});

CompiledRoute withQueryParams(Map<String, String> params) {
String newRoute = compiledRoute;
params.forEach((key, value) {
newRoute =
'$newRoute${queryParameters == null || queryParameters!.isEmpty ? '?' : '&'}$key=$value';
newRoute = '$newRoute${queryParameters == null || queryParameters!.isEmpty ? '?' : '&'}$key=$value';
queryParameters ??= {};
queryParameters![key] = value;
});
return CompiledRoute(baseRoute, newRoute, parameters,
queryParameters: queryParameters);
return CompiledRoute(baseRoute, newRoute, parameters, queryParameters: queryParameters);
}

Future<Response> submit(
{dynamic body, String contentType = 'application/json'}) {
Future<Response> submit({dynamic body, String contentType = 'application/json'}) {
if (!Restrr.hostInformation.hasHostUrl) {
throw StateError('Host URL is not set!');
}
Expand All @@ -87,23 +77,17 @@ class CompiledRoute {
headers: headers,
data: body,
method: baseRoute.method.toString(),
baseUrl:
_buildBaseUrl(Restrr.hostInformation, baseRoute.isVersioned)))
baseUrl: _buildBaseUrl(Restrr.hostInformation, baseRoute.isVersioned)))
.then((value) {
Restrr.log.info(
'[$compiledRoute] => ${value.statusCode} ${value.statusMessage}');
return value;
});
}

String _buildBaseUrl(HostInformation hostInformation, bool isVersioned) {
String effectiveHostUrl = hostInformation.hostUri!.toString();
if (effectiveHostUrl.endsWith('/')) {
effectiveHostUrl =
effectiveHostUrl.substring(0, effectiveHostUrl.length - 1);
effectiveHostUrl = effectiveHostUrl.substring(0, effectiveHostUrl.length - 1);
}
return isVersioned
? '$effectiveHostUrl/api/v${hostInformation.apiVersion}'
: '$effectiveHostUrl/api';
return isVersioned ? '$effectiveHostUrl/api/v${hostInformation.apiVersion}' : '$effectiveHostUrl/api';
}
}
61 changes: 31 additions & 30 deletions lib/src/restrr_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,52 +22,53 @@ class HostInformation {
}
}

enum SessionInitType { login, register }
///
enum RestrrInitType { login, register }

/// A builder for creating a new [Restrr] instance.
/// The [Restrr] instance is created by calling [create].
class RestrrBuilder {
final SessionInitType sessionInitType;
final RestrrInitType initType;
final Uri uri;
String? sessionId;
String? username;
String? password;
String? mfaCode;

RestrrBuilder.login(
{required this.uri, required this.username, required this.password})
: sessionInitType = SessionInitType.login;
RestrrBuilder.login({required this.uri, required this.username, required this.password})
: initType = RestrrInitType.login;

Future<RestResponse<RestrrImpl>> create() async {
Restrr.log.info(
'Attempting to initialize a session (${sessionInitType.name}) with $uri');
final RestResponse<HealthResponse> statusResponse =
await Restrr.checkUri(uri);
/// 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);
if (!statusResponse.hasData) {
Restrr.log.warning('Invalid financrr URI: $uri');
return RestrrError.invalidUri.toRestResponse();
}
Restrr.log.info(
'Updated host information: $uri, API v${statusResponse.data!.apiVersion}');
final RestrrImpl? api = await switch (sessionInitType) {
SessionInitType.register => throw UnimplementedError(),
SessionInitType.login => _handleLogin(username!, password!),
Restrr.log.info('Host: $uri, API v${statusResponse.data!.apiVersion}');
// create the API instance
final RestrrImpl? api = await switch (initType) {
RestrrInitType.register => throw UnimplementedError(),
RestrrInitType.login => _handleLogin(username!, password!),
};
if (api == null) {
Restrr.log.warning('Invalid credentials for user $username');
return RestrrError.invalidCredentials.toRestResponse();
}
Restrr.log.info('Successfully logged in as ${api.selfUser.username}');
return RestResponse(data: api);
}

/// Handles the login process.
/// Returns a [RestrrImpl] instance if the login was successful, otherwise null.
Future<RestrrImpl?> _handleLogin(String username, String password) async {
final RestResponse<bool> response =
await UserService.login(username, password);
if (!response.hasData) {
return null;
}
final RestResponse<User> userResponse = await UserService.getSelf();
final RestrrImpl api = RestrrImpl._();
final RestResponse<User> userResponse = await UserService(api: api).login(username, password);
if (!userResponse.hasData) {
return null;
}
return RestrrImpl._(selfUser: userResponse.data!);
return api..selfUser = userResponse.data!;
}
}

Expand All @@ -81,27 +82,27 @@ abstract class Restrr {
/// The currently authenticated user.
User get selfUser;

/// 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(
route: StatusRoutes.health.compile(),
mapper: (json) => EntityBuilder.buildHealthResponse(json))
.then((response) {
route: StatusRoutes.health.compile(),
mapper: (json) => EntityBuilder.buildHealthResponse(json)).then((response) {
if (response.hasData && response.data!.healthy) {
hostInformation =
hostInformation.copyWith(apiVersion: response.data!.apiVersion);
// if successful, update the API version
hostInformation = hostInformation.copyWith(apiVersion: response.data!.apiVersion);
}
return response;
});
}
}

class RestrrImpl implements Restrr {
RestrrImpl._({required this.selfUser});
RestrrImpl._();

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

@override
final User selfUser;
late final User selfUser;
}
25 changes: 9 additions & 16 deletions lib/src/service/api_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@ abstract class ApiService {
const ApiService({required this.api});

/// Tries to execute a request, using the [CompiledRoute] and maps the received data using the
/// specified [mapper] function, ultimately returning the entity in an [ApiResponse].
/// specified [mapper] function, ultimately returning the entity in an [RestResponse].
///
/// If this fails, this will return an [ApiResponse] containing an error.
/// If this fails, this will return an [RestResponse] containing an error.
static Future<RestResponse<T>> request<T>(
{required CompiledRoute route,
required T Function(dynamic) mapper,
dynamic body,
String contentType = 'application/json'}) async {
try {
final Response<dynamic> response =
await route.submit(body: body, contentType: contentType);
final Response<dynamic> response = await route.submit(body: body, contentType: contentType);
return RestResponse(data: mapper.call(response.data));
} on DioException catch (e) {
return RestResponse.fromError(Route.asErrorResponse(e));
Expand All @@ -26,11 +25,9 @@ abstract class ApiService {

/// Tries to execute a request, using the [CompiledRoute], without expecting any response.
///
/// If this fails, this will return an [ApiResponse] containing an error.
/// If this fails, this will return an [RestResponse] containing an error.
static Future<RestResponse<bool>> noResponseRequest<T>(
{required CompiledRoute route,
dynamic body,
String contentType = 'application/json'}) async {
{required CompiledRoute route, dynamic body, String contentType = 'application/json'}) async {
try {
await route.submit(body: body, contentType: contentType);
return const RestResponse(data: true);
Expand All @@ -40,26 +37,22 @@ abstract class ApiService {
}

/// Tries to execute a request, using the [CompiledRoute] and maps the received list of data using the
/// specified [mapper] function, ultimately returning the list of entities in an [ApiResponse].
/// specified [mapper] function, ultimately returning the list of entities in an [RestResponse].
///
/// If this fails, this will return an [ApiResponse] containing an error.
/// If this fails, this will return an [RestResponse] containing an error.
static Future<RestResponse<List<T>>> multiRequest<T>(
{required CompiledRoute route,
required T Function(dynamic) mapper,
Function(String)? fullRequest,
dynamic body,
String contentType = 'application/json'}) async {
try {
final Response<dynamic> response =
await route.submit(body: body, contentType: contentType);
final Response<dynamic> response = await route.submit(body: body, contentType: contentType);
if (response.data is! List<dynamic>) {
throw StateError('Received response is not a list!');
}
fullRequest?.call(response.data.toString());
return RestResponse(
data: (response.data as List<dynamic>)
.map((single) => mapper.call(single))
.toList());
return RestResponse(data: (response.data as List<dynamic>).map((single) => mapper.call(single)).toList());
} on DioException catch (e) {
return RestResponse.fromError(Route.asErrorResponse(e));
}
Expand Down
14 changes: 6 additions & 8 deletions lib/src/service/user_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@ import '../../restrr.dart';
class UserService extends ApiService {
const UserService({required super.api});

static Future<RestResponse<bool>> login(
String username, String password) async {
return ApiService.noResponseRequest(
Future<RestResponse<User>> login(String username, String password) async {
return ApiService.request(
route: UserRoutes.login.compile(),
body: {
'username': username,
'password': password,
});
},
mapper: (json) => api.entityBuilder.buildUser(json));
}

static Future<RestResponse<User>> getSelf() async {
return ApiService.request(
route: UserRoutes.me.compile(),
mapper: (json) => EntityBuilder.buildUser(json));
Future<RestResponse<User>> getSelf() async {
return ApiService.request(route: UserRoutes.me.compile(), mapper: (json) => api.entityBuilder.buildUser(json));
}
}
Loading

0 comments on commit e56fd25

Please sign in to comment.