Skip to content

Commit

Permalink
Merge pull request #1677 from nextcloud/refactor/dynamite_runtime/spl…
Browse files Browse the repository at this point in the history
…it_dynamite_client_library

refactor(dynamite_runtime): split dynamite_client into separate libra…
  • Loading branch information
Leptopoda authored Feb 29, 2024
2 parents f552ae0 + 74b0588 commit 5e7dc5a
Show file tree
Hide file tree
Showing 8 changed files with 486 additions and 491 deletions.
5 changes: 4 additions & 1 deletion packages/dynamite/dynamite_runtime/lib/http_client.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/// The dynamite client to handle http connections.
library http_client;

export 'src/dynamite_client.dart';
export 'src/client/authentication.dart';
export 'src/client/client.dart';
export 'src/client/exception.dart';
export 'src/client/response.dart';
export 'src/http_extensions.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'dart:convert';

import 'package:meta/meta.dart';

/// Base dynamite authentication.
@immutable
sealed class DynamiteAuthentication {
/// Creates a new authentication.
const DynamiteAuthentication({
required this.type,
required this.scheme,
});

/// The base type of the authentication.
final String type;

/// The used authentication HTTP scheme.
final String? scheme;

/// The authentication headers added to a request.
Map<String, String> get headers;
}

/// Basic http authentication with username and password.
class DynamiteHttpBasicAuthentication extends DynamiteAuthentication {
/// Creates a new http basic authentication.
const DynamiteHttpBasicAuthentication({
required this.username,
required this.password,
}) : super(
type: 'http',
scheme: 'basic',
);

/// The username.
final String username;

/// The password.
final String password;

@override
Map<String, String> get headers => {
'Authorization': 'Basic ${base64.encode(utf8.encode('$username:$password'))}',
};
}

/// Http bearer authentication with a token.
class DynamiteHttpBearerAuthentication extends DynamiteAuthentication {
/// Creates a new http bearer authentication.
const DynamiteHttpBearerAuthentication({
required this.token,
}) : super(
type: 'http',
scheme: 'bearer',
);

/// The authentication token.
final String token;

@override
Map<String, String> get headers => {
'Authorization': 'Bearer $token',
};
}
126 changes: 126 additions & 0 deletions packages/dynamite/dynamite_runtime/lib/src/client/client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:cookie_jar/cookie_jar.dart';
import 'package:dynamite_runtime/http_client.dart';
import 'package:dynamite_runtime/src/utils/debug_mode.dart';
import 'package:dynamite_runtime/src/utils/uri.dart';
import 'package:http/http.dart' as http;

/// A client for making network requests.
///
/// See:
/// * [DynamiteResponse] as the response returned by an operation.
/// * [DynamiteRawResponse] as the raw response that can be serialized.
/// * [DynamiteApiException] as the exception that can be thrown in operations
/// * [DynamiteAuthentication] for providing authentication methods.
class DynamiteClient {
/// Creates a new dynamite network client.
///
/// If [httpClient] is not provided a default one will be created.
/// The [baseURL] will be normalized, removing any trailing `/`.
DynamiteClient(
Uri baseURL, {
this.baseHeaders,
http.Client? httpClient,
this.cookieJar,
this.authentications,
}) : httpClient = httpClient ?? http.Client(),
baseURL = baseURL.normalizeEmptyPath() {
if (baseURL.queryParametersAll.isNotEmpty) {
throw UnsupportedError('Dynamite can not work with a baseURL containing query parameters.');
}
}

/// The base server url used to build the request uri.
///
/// See `https://swagger.io/docs/specification/api-host-and-base-path` for
/// further information.
final Uri baseURL;

/// The base headers added to each request.
final Map<String, String>? baseHeaders;

/// The base http client.
final http.Client httpClient;

/// The optional cookie jar to persist the response cookies.
final CookieJar? cookieJar;

/// The available authentications for this client.
///
/// The first one matching the required authentication type will be used.
final List<DynamiteAuthentication>? authentications;

/// Makes a request against a given [path].
///
/// The query parameters of the [baseURL] are added.
/// The [path] is resolved against the path of the [baseURL].
/// All [baseHeaders] are added to the request.
Future<http.StreamedResponse> executeRequest(
String method,
String path, {
Map<String, String>? headers,
Uint8List? body,
Set<int>? validStatuses,
}) {
final uri = Uri.parse('$baseURL$path');

return executeRawRequest(
method,
uri,
headers: headers,
body: body,
validStatuses: validStatuses,
);
}

/// Executes a HTTP request against give full [uri].
Future<http.StreamedResponse> executeRawRequest(
String method,
Uri uri, {
Map<String, String>? headers,
Uint8List? body,
Set<int>? validStatuses,
}) async {
final request = http.Request(method, uri);

if (baseHeaders != null) {
request.headers.addAll(baseHeaders!);
}

if (headers != null) {
request.headers.addAll(headers);
}

if (body != null) {
request.bodyBytes = body;
}

if (cookieJar != null) {
final cookies = await cookieJar!.loadForRequest(uri);
if (cookies.isNotEmpty) {
request.headers['cookie'] = cookies.join('; ');
}
}

final response = await httpClient.send(request);

final cookieHeader = response.headersSplitValues['set-cookie'];
if (cookieHeader != null && cookieJar != null) {
final cookies = cookieHeader.map(Cookie.fromSetCookieValue).toList();
await cookieJar!.saveFromResponse(uri, cookies);
}

if (validStatuses?.contains(response.statusCode) ?? true) {
return response;
} else {
if (kDebugMode) {
final result = await http.Response.fromStream(response);
throw DynamiteStatusCodeException.fromResponse(result);
} else {
throw DynamiteStatusCodeException(response.statusCode);
}
}
}
}
54 changes: 54 additions & 0 deletions packages/dynamite/dynamite_runtime/lib/src/client/exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:dynamite_runtime/src/utils/debug_mode.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';

/// The exception thrown by operations of a `DynamiteClient`.
///
///
/// See:
/// * [DynamiteStatusCodeException] as the exception thrown when the response
/// returns an invalid status code.
@immutable
sealed class DynamiteApiException extends http.ClientException {
DynamiteApiException(super.message, super.uri);
}

/// An exception caused by an invalid status code in a response.
class DynamiteStatusCodeException extends DynamiteApiException {
/// Creates a new dynamite exception with the given information.
DynamiteStatusCodeException(
this.statusCode, [
Uri? url,
]) : super('Invalid status code $statusCode.', url);

/// Creates a new Exception from the given [response] for debugging.
///
/// Awaits the response and tries to decode the `response` into a string.
/// Do not use this in production for performance reasons.
factory DynamiteStatusCodeException.fromResponse(http.Response response) {
assert(kDebugMode, 'Do not use in production for performance reasons.');

String body;
try {
body = response.body;
} on FormatException {
body = 'binary';
}

return DynamiteStatusCodeException._(
response.statusCode,
response.headers,
body,
);
}

DynamiteStatusCodeException._(
this.statusCode,
Map<String, Object?> headers,
String body, [
Uri? url,
]) : super('Invalid status code $statusCode, $statusCode, headers: $headers, body: $body', url);

/// The returned status code when the exception was thrown.
final int statusCode;
}
Loading

0 comments on commit 5e7dc5a

Please sign in to comment.