42 changed files with 7144 additions and 3748 deletions
@ -0,0 +1,129 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:code_builder/code_builder.dart'; |
||||
import 'package:dynamite/src/builder/resolve_type.dart'; |
||||
import 'package:dynamite/src/builder/state.dart'; |
||||
import 'package:dynamite/src/helpers/dart_helpers.dart'; |
||||
import 'package:dynamite/src/helpers/dynamite.dart'; |
||||
import 'package:dynamite/src/models/openapi.dart' as openapi; |
||||
import 'package:dynamite/src/models/type_result.dart'; |
||||
|
||||
TypeResult? resolveMimeTypeDecode( |
||||
final openapi.Response response, |
||||
final openapi.OpenAPI spec, |
||||
final State state, |
||||
final String identifier, |
||||
) { |
||||
if (response.content != null) { |
||||
if (response.content!.length > 1) { |
||||
throw Exception('Can not work with multiple mime types right now'); |
||||
} |
||||
|
||||
for (final content in response.content!.entries) { |
||||
final mimeType = content.key; |
||||
final mediaType = content.value; |
||||
|
||||
final result = resolveType( |
||||
spec, |
||||
state, |
||||
toDartName('$identifier-$mimeType', uppercaseFirstCharacter: true), |
||||
mediaType.schema!, |
||||
); |
||||
|
||||
if (mimeType == '*/*' || mimeType == 'application/octet-stream' || mimeType.startsWith('image/')) { |
||||
return TypeResultObject('Uint8List'); |
||||
} else if (mimeType.startsWith('text/') || mimeType == 'application/javascript') { |
||||
return TypeResultBase('String'); |
||||
} else if (mimeType == 'application/json') { |
||||
return result; |
||||
} else { |
||||
throw Exception('Can not parse mime type "$mimeType"'); |
||||
} |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
Iterable<String> resolveMimeTypeEncode( |
||||
final openapi.Operation operation, |
||||
final openapi.OpenAPI spec, |
||||
final State state, |
||||
final String identifier, |
||||
final ListBuilder<Parameter> b, |
||||
) sync* { |
||||
if (operation.requestBody != null) { |
||||
if (operation.requestBody!.content!.length > 1) { |
||||
throw Exception('Can not work with multiple mime types right now'); |
||||
} |
||||
for (final content in operation.requestBody!.content!.entries) { |
||||
final mimeType = content.key; |
||||
final mediaType = content.value; |
||||
|
||||
yield "_headers['Content-Type'] = '$mimeType';"; |
||||
|
||||
final dartParameterNullable = isDartParameterNullable( |
||||
operation.requestBody!.required, |
||||
mediaType.schema, |
||||
); |
||||
|
||||
final result = resolveType( |
||||
spec, |
||||
state, |
||||
toDartName('$identifier-request-$mimeType', uppercaseFirstCharacter: true), |
||||
mediaType.schema!, |
||||
nullable: dartParameterNullable, |
||||
); |
||||
final parameterName = toDartName(result.name.replaceFirst(state.classPrefix, '')); |
||||
switch (mimeType) { |
||||
case 'application/json': |
||||
case 'application/x-www-form-urlencoded': |
||||
final dartParameterRequired = isRequired( |
||||
operation.requestBody!.required, |
||||
mediaType.schema?.$default, |
||||
); |
||||
b.add( |
||||
Parameter( |
||||
(final b) => b |
||||
..name = parameterName |
||||
..type = refer(result.nullableName) |
||||
..named = true |
||||
..required = dartParameterRequired, |
||||
), |
||||
); |
||||
|
||||
if (dartParameterNullable) { |
||||
yield 'if ($parameterName != null) {'; |
||||
} |
||||
yield '_body = utf8.encode(${result.encode(parameterName, mimeType: mimeType)}) as Uint8List;'; |
||||
if (dartParameterNullable) { |
||||
yield '}'; |
||||
} |
||||
return; |
||||
case 'application/octet-stream': |
||||
final dartParameterRequired = isRequired( |
||||
operation.requestBody!.required, |
||||
mediaType.schema?.$default, |
||||
); |
||||
b.add( |
||||
Parameter( |
||||
(final b) => b |
||||
..name = parameterName |
||||
..type = refer(result.nullableName) |
||||
..named = true |
||||
..required = dartParameterRequired, |
||||
), |
||||
); |
||||
|
||||
if (dartParameterNullable) { |
||||
yield 'if ($parameterName != null) {'; |
||||
} |
||||
yield '_body = ${result.encode(parameterName, mimeType: mimeType)};'; |
||||
if (dartParameterNullable) { |
||||
yield '}'; |
||||
} |
||||
return; |
||||
default: |
||||
throw Exception('Can not parse mime type "$mimeType"'); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,9 +1,5 @@
|
||||
include: package:neon_lints/dart.yaml |
||||
|
||||
linter: |
||||
rules: |
||||
public_member_api_docs: false |
||||
|
||||
analyzer: |
||||
exclude: |
||||
- '**.g.dart' |
||||
|
@ -1 +1,3 @@
|
||||
export 'src/http_client.dart'; |
||||
export 'package:cookie_jar/cookie_jar.dart'; |
||||
export 'src/dynamite_client.dart'; |
||||
export 'src/http_extensions.dart'; |
||||
|
@ -0,0 +1,445 @@
|
||||
import 'dart:async'; |
||||
import 'dart:convert'; |
||||
import 'dart:typed_data'; |
||||
|
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:cookie_jar/cookie_jar.dart'; |
||||
import 'package:dynamite_runtime/src/http_extensions.dart'; |
||||
import 'package:dynamite_runtime/src/uri.dart'; |
||||
import 'package:meta/meta.dart'; |
||||
import 'package:universal_io/io.dart'; |
||||
|
||||
/// Response returned by operations of a [DynamiteClient]. |
||||
/// |
||||
/// See: |
||||
/// * [DynamiteRawResponse] for an experimental implementation that can be serialized. |
||||
/// * [DynamiteApiException] as the exception that can be thrown in operations |
||||
/// * [DynamiteAuthentication] for providing authentication methods. |
||||
/// * [DynamiteClient] for the client providing operations. |
||||
@immutable |
||||
class DynamiteResponse<B, H> { |
||||
/// Creates a new dynamite response. |
||||
const DynamiteResponse( |
||||
this.statusCode, |
||||
this._body, |
||||
this._headers, |
||||
); |
||||
|
||||
/// The status code of the response. |
||||
final int statusCode; |
||||
|
||||
final B? _body; |
||||
|
||||
final H? _headers; |
||||
|
||||
/// The decoded body of the response. |
||||
B get body => _body!; |
||||
|
||||
/// The decoded headers of the response. |
||||
H get headers => _headers!; |
||||
|
||||
@override |
||||
String toString() => 'DynamiteResponse(data: $body, headers: $headers, statusCode: $statusCode)'; |
||||
} |
||||
|
||||
/// Raw response returned by operations of a [DynamiteClient]. |
||||
/// |
||||
/// This type itself is serializable. |
||||
/// |
||||
/// The api of this type might change without a major bump. |
||||
/// Use methods that return a [DynamiteResponse] instead. |
||||
/// |
||||
/// See: |
||||
/// * [DynamiteResponse] as the response returned by an operation. |
||||
/// * [DynamiteApiException] as the exception that can be thrown in operations |
||||
/// * [DynamiteAuthentication] for providing authentication methods. |
||||
/// * [DynamiteClient] for the client providing operations. |
||||
@experimental |
||||
class DynamiteRawResponse<B, H> { |
||||
/// Creates a new raw dynamite response. |
||||
/// |
||||
/// The [response] will be awaited and deserialized. |
||||
/// After [future] completes the deserialized response can be accessed |
||||
/// through [response]. |
||||
DynamiteRawResponse({ |
||||
required final Future<HttpClientResponse> response, |
||||
required this.bodyType, |
||||
required this.headersType, |
||||
required this.serializers, |
||||
}) { |
||||
final completer = Completer<DynamiteResponse<B, H>>(); |
||||
future = completer.future; |
||||
|
||||
// ignore: discarded_futures |
||||
response.then( |
||||
(final response) async { |
||||
_rawBody = switch (bodyType) { |
||||
const FullType(Uint8List) => await response.bytes, |
||||
const FullType(String) => await response.string, |
||||
_ => await response.json, |
||||
}; |
||||
_rawHeaders = response.responseHeaders; |
||||
|
||||
final body = deserializeBody<B>(_rawBody, serializers, bodyType); |
||||
final headers = deserializeHeaders<H>(_rawHeaders, serializers, headersType); |
||||
|
||||
_response = DynamiteResponse<B, H>( |
||||
response.statusCode, |
||||
body, |
||||
headers, |
||||
); |
||||
|
||||
completer.complete(_response); |
||||
}, |
||||
onError: completer.completeError, |
||||
); |
||||
} |
||||
|
||||
/// Decodes a raw dynamite response from json data. |
||||
/// |
||||
/// The [future] must not be awaited and the deserialized response can be |
||||
/// accessed immediately through [response]. |
||||
factory DynamiteRawResponse.fromJson( |
||||
final Map<String, Object?> json, { |
||||
required final Serializers serializers, |
||||
final FullType? bodyType, |
||||
final FullType? headersType, |
||||
}) { |
||||
final statusCode = json['statusCode']! as int; |
||||
final body = deserializeBody<B>(json['body'], serializers, bodyType); |
||||
final headers = deserializeHeaders<H>(json['headers'], serializers, headersType); |
||||
|
||||
final response = DynamiteResponse<B, H>( |
||||
statusCode, |
||||
body, |
||||
headers, |
||||
); |
||||
|
||||
return DynamiteRawResponse._fromJson( |
||||
response, |
||||
bodyType: bodyType, |
||||
headersType: headersType, |
||||
serializers: serializers, |
||||
); |
||||
} |
||||
|
||||
DynamiteRawResponse._fromJson( |
||||
this._response, { |
||||
required this.bodyType, |
||||
required this.headersType, |
||||
required this.serializers, |
||||
}) : future = Future.value(_response); |
||||
|
||||
/// The serializers for the header and body. |
||||
final Serializers serializers; |
||||
|
||||
/// The full type of the body. |
||||
/// |
||||
/// This is `null` if the body type is void. |
||||
final FullType? bodyType; |
||||
|
||||
/// The full type of the headers. |
||||
/// |
||||
/// This is `null` if the headers type is void. |
||||
final FullType? headersType; |
||||
|
||||
/// Future of the deserialized response. |
||||
/// |
||||
/// After this future completes the response can be accessed synchronously |
||||
/// through [response]. |
||||
late final Future<DynamiteResponse<B, H>> future; |
||||
|
||||
/// Caches the serialized response body for later serialization in [toJson]. |
||||
/// |
||||
/// Responses revived with [DynamiteRawResponse.fromJson] are not cached as |
||||
/// they are not expected to be serialized again. |
||||
Object? _rawBody; |
||||
|
||||
/// Caches the serialized response headers for later serialization in [toJson]. |
||||
/// |
||||
/// Responses revived with [DynamiteRawResponse.fromJson] are not cached as |
||||
/// they are not expected to be serialized again. |
||||
Map<String, String>? _rawHeaders; |
||||
|
||||
DynamiteResponse<B, H>? _response; |
||||
|
||||
/// Returns the deserialized response synchronously. |
||||
/// |
||||
/// Throws a `StateError` if [future] has not completed yet and `this` has |
||||
/// not been instantiated through [DynamiteRawResponse.fromJson]. |
||||
DynamiteResponse<B, H> get response { |
||||
final response = _response; |
||||
|
||||
if (response == null) { |
||||
throw StateError('The response did not finish yet. Make sure to await `this.future`.'); |
||||
} |
||||
|
||||
return response; |
||||
} |
||||
|
||||
/// Deserializes the body. |
||||
/// |
||||
/// Most efficient if the [serialized] value is already the correct type. |
||||
/// The [bodyType] should represent the return type [B]. |
||||
static B? deserializeBody<B>(final Object? serialized, final Serializers serializers, final FullType? bodyType) { |
||||
// If we use the more efficient helpers from BytesStreamExtension the serialized value can already be correct. |
||||
if (serialized is B) { |
||||
return serialized; |
||||
} |
||||
|
||||
if (bodyType != null) { |
||||
return serializers.deserialize(serialized, specifiedType: bodyType) as B?; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/// Serializes the body. |
||||
Object? serializeBody(final B? object) { |
||||
if (bodyType != null && object != null) { |
||||
return serializers.serialize(object, specifiedType: bodyType!); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/// Deserializes the headers. |
||||
/// |
||||
/// Most efficient if the [serialized] value is already the correct type. |
||||
/// The [headersType] should represent the return type [H]. |
||||
static H? deserializeHeaders<H>( |
||||
final Object? serialized, |
||||
final Serializers serializers, |
||||
final FullType? headersType, |
||||
) { |
||||
// If we use the more efficient helpers from BytesStreamExtension the serialized value can already be correct. |
||||
if (serialized is H) { |
||||
return serialized; |
||||
} |
||||
|
||||
if (headersType != null) { |
||||
return serializers.deserialize(serialized, specifiedType: headersType) as H?; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/// Serializes the headers. |
||||
Object? serializeHeaders(final H? object) { |
||||
if (headersType != null && object != null) { |
||||
return serializers.serialize(object, specifiedType: headersType!); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/// Serializes this response into json. |
||||
/// |
||||
/// To revive it again use [DynamiteRawResponse.fromJson] with the same |
||||
/// serializer and `FullType`s as this. |
||||
Map<String, Object?> toJson() => { |
||||
'statusCode': response.statusCode, |
||||
'body': _rawBody ?? serializeBody(response._body), |
||||
'headers': _rawHeaders ?? serializeHeaders(response._headers), |
||||
}; |
||||
|
||||
@override |
||||
String toString() => 'DynamiteResponse(${toJson()})'; |
||||
} |
||||
|
||||
/// The exception thrown by operations of a [DynamiteClient]. |
||||
/// |
||||
/// |
||||
/// See: |
||||
/// * [DynamiteResponse] as the response returned by an operation. |
||||
/// * [DynamiteRawResponse] as the raw response that can be serialized. |
||||
/// * [DynamiteAuthentication] for providing authentication methods. |
||||
/// * [DynamiteClient] for the client providing operations. |
||||
@immutable |
||||
class DynamiteApiException implements Exception { |
||||
/// Creates a new dynamite exception with the given information. |
||||
const DynamiteApiException( |
||||
this.statusCode, |
||||
this.headers, |
||||
this.body, |
||||
); |
||||
|
||||
/// Creates a new Exception from the given [response]. |
||||
/// |
||||
/// Tries to decode the `response` into a string. |
||||
static Future<DynamiteApiException> fromResponse(final HttpClientResponse response) async { |
||||
String body; |
||||
try { |
||||
body = await response.string; |
||||
} on FormatException { |
||||
body = 'binary'; |
||||
} |
||||
|
||||
return DynamiteApiException( |
||||
response.statusCode, |
||||
response.responseHeaders, |
||||
body, |
||||
); |
||||
} |
||||
|
||||
/// The returned status code when the exception was thrown. |
||||
final int statusCode; |
||||
|
||||
/// The returned headers when the exception was thrown. |
||||
final Map<String, String> headers; |
||||
|
||||
/// The returned body code when the exception was thrown. |
||||
final String body; |
||||
|
||||
@override |
||||
String toString() => 'DynamiteApiException(statusCode: $statusCode, headers: $headers, body: $body)'; |
||||
} |
||||
|
||||
/// Base dynamite authentication. |
||||
/// |
||||
/// 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 |
||||
/// * [DynamiteClient] for the client providing operations. |
||||
@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 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', |
||||
}; |
||||
} |
||||
|
||||
/// 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( |
||||
final Uri baseURL, { |
||||
this.baseHeaders, |
||||
final String? userAgent, |
||||
final HttpClient? httpClient, |
||||
this.cookieJar, |
||||
this.authentications = const [], |
||||
}) : httpClient = (httpClient ?? HttpClient())..userAgent = userAgent, |
||||
baseURL = baseURL.normalizeEmptyPath(); |
||||
|
||||
/// 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 HttpClient 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]. |
||||
Future<HttpClientResponse> doRequest( |
||||
final String method, |
||||
final Uri path, |
||||
final Map<String, String> headers, |
||||
final Uint8List? body, |
||||
final Set<int>? validStatuses, |
||||
) async { |
||||
final uri = baseURL.replace( |
||||
path: '${baseURL.path}${path.path}', |
||||
queryParameters: { |
||||
...baseURL.queryParameters, |
||||
...path.queryParameters, |
||||
}, |
||||
); |
||||
|
||||
final request = await httpClient.openUrl(method, uri); |
||||
for (final header in {...?baseHeaders, ...headers}.entries) { |
||||
request.headers.add(header.key, header.value); |
||||
} |
||||
if (body != null) { |
||||
request.add(body); |
||||
} |
||||
if (cookieJar != null) { |
||||
request.cookies.addAll(await cookieJar!.loadForRequest(uri)); |
||||
} |
||||
|
||||
final response = await request.close(); |
||||
if (cookieJar != null) { |
||||
await cookieJar!.saveFromResponse(uri, response.cookies); |
||||
} |
||||
|
||||
if (validStatuses?.contains(response.statusCode) ?? true) { |
||||
return response; |
||||
} else { |
||||
throw await DynamiteApiException.fromResponse(response); |
||||
} |
||||
} |
||||
} |
@ -1,249 +0,0 @@
|
||||
import 'dart:async'; |
||||
import 'dart:convert'; |
||||
import 'dart:typed_data'; |
||||
|
||||
import 'package:cookie_jar/cookie_jar.dart'; |
||||
import 'package:dynamite_runtime/src/uri.dart'; |
||||
import 'package:meta/meta.dart'; |
||||
import 'package:universal_io/io.dart'; |
||||
|
||||
export 'package:cookie_jar/cookie_jar.dart'; |
||||
|
||||
extension DynamiteHttpClientResponseBody on HttpClientResponse { |
||||
Future<Uint8List> get bodyBytes async { |
||||
final buffer = BytesBuilder(); |
||||
|
||||
await forEach(buffer.add); |
||||
|
||||
return buffer.toBytes(); |
||||
} |
||||
|
||||
Future<String> get body => transform(utf8.decoder).join(); |
||||
|
||||
Future<dynamic> get jsonBody => transform(utf8.decoder).transform(json.decoder).first; |
||||
|
||||
Map<String, String> get responseHeaders { |
||||
final responseHeaders = <String, String>{}; |
||||
headers.forEach((final name, final values) { |
||||
responseHeaders[name] = values.last; |
||||
}); |
||||
|
||||
return responseHeaders; |
||||
} |
||||
} |
||||
|
||||
/// Response returned by operations of a [DynamiteClient]. |
||||
/// |
||||
/// See: |
||||
/// * [DynamiteApiException] as the exception that can be thrown in operations |
||||
/// * [DynamiteAuthentication] for providing authentication methods. |
||||
/// * [DynamiteClient] for the client providing operations. |
||||
class DynamiteResponse<B, H> { |
||||
/// Creates a new dynamite response. |
||||
const DynamiteResponse( |
||||
this.statusCode, |
||||
this.body, |
||||
this.headers, |
||||
); |
||||
|
||||
/// The status code of the response. |
||||
final int statusCode; |
||||
|
||||
/// The decoded body of the response. |
||||
final B body; |
||||
|
||||
/// The decoded headers of the response. |
||||
final H headers; |
||||
|
||||
@override |
||||
String toString() => 'DynamiteResponse(data: $body, headers: $headers, statusCode: $statusCode)'; |
||||
} |
||||
|
||||
/// The exception thrown by operations of a [DynamiteClient]. |
||||
/// |
||||
/// |
||||
/// See: |
||||
/// * [DynamiteResponse] as the response returned by an operation. |
||||
/// * [DynamiteAuthentication] for providing authentication methods. |
||||
/// * [DynamiteClient] for the client providing operations. |
||||
@immutable |
||||
class DynamiteApiException implements Exception { |
||||
/// Creates a new dynamite exception with the given information. |
||||
const DynamiteApiException( |
||||
this.statusCode, |
||||
this.headers, |
||||
this.body, |
||||
); |
||||
|
||||
/// Creates a new Exception from the given [response]. |
||||
/// |
||||
/// Tries to decode the `response` into a string. |
||||
static Future<DynamiteApiException> fromResponse(final HttpClientResponse response) async { |
||||
String body; |
||||
try { |
||||
body = await response.body; |
||||
} on FormatException { |
||||
body = 'binary'; |
||||
} |
||||
|
||||
return DynamiteApiException( |
||||
response.statusCode, |
||||
response.responseHeaders, |
||||
body, |
||||
); |
||||
} |
||||
|
||||
/// The returned status code when the exception was thrown. |
||||
final int statusCode; |
||||
|
||||
/// The returned headers when the exception was thrown. |
||||
final Map<String, String> headers; |
||||
|
||||
/// The returned body code when the exception was thrown. |
||||
final String body; |
||||
|
||||
@override |
||||
String toString() => 'DynamiteApiException(statusCode: $statusCode, headers: $headers, body: $body)'; |
||||
} |
||||
|
||||
/// Base dynamite authentication. |
||||
/// |
||||
/// See: |
||||
/// * [DynamiteResponse] as the response returned by an operation. |
||||
/// * [DynamiteApiException] as the exception that can be thrown in operations |
||||
/// * [DynamiteClient] for the client providing operations. |
||||
@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 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', |
||||
}; |
||||
} |
||||
|
||||
/// A client for making network requests. |
||||
/// |
||||
/// See: |
||||
/// * [DynamiteResponse] as the response returned by an operation. |
||||
/// * [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( |
||||
final Uri baseURL, { |
||||
this.baseHeaders, |
||||
final String? userAgent, |
||||
final HttpClient? httpClient, |
||||
this.cookieJar, |
||||
this.authentications = const [], |
||||
}) : httpClient = (httpClient ?? HttpClient())..userAgent = userAgent, |
||||
baseURL = baseURL.normalizeEmptyPath(); |
||||
|
||||
/// 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 HttpClient 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]. |
||||
Future<HttpClientResponse> doRequest( |
||||
final String method, |
||||
final Uri path, |
||||
final Map<String, String> headers, |
||||
final Uint8List? body, |
||||
) async { |
||||
final uri = baseURL.replace( |
||||
path: '${baseURL.path}${path.path}', |
||||
queryParameters: { |
||||
...baseURL.queryParameters, |
||||
...path.queryParameters, |
||||
}, |
||||
); |
||||
|
||||
final request = await httpClient.openUrl(method, uri); |
||||
for (final header in {...?baseHeaders, ...headers}.entries) { |
||||
request.headers.add(header.key, header.value); |
||||
} |
||||
if (body != null) { |
||||
request.add(body); |
||||
} |
||||
if (cookieJar != null) { |
||||
request.cookies.addAll(await cookieJar!.loadForRequest(uri)); |
||||
} |
||||
|
||||
final response = await request.close(); |
||||
if (cookieJar != null) { |
||||
await cookieJar!.saveFromResponse(uri, response.cookies); |
||||
} |
||||
|
||||
return response; |
||||
} |
||||
} |
@ -0,0 +1,43 @@
|
||||
import 'dart:async'; |
||||
import 'dart:convert'; |
||||
import 'dart:typed_data'; |
||||
|
||||
import 'package:universal_io/io.dart'; |
||||
|
||||
/// A stream of bytes. |
||||
/// |
||||
/// Usually a `Stream<Uint8List>`. |
||||
typedef BytesStream = Stream<List<int>>; |
||||
|
||||
final _utf8JsonDecoder = utf8.decoder.fuse(json.decoder); |
||||
|
||||
/// Extension on byte streams that enable efficient transformations. |
||||
extension BytesStreamExtension on BytesStream { |
||||
/// Returns the all bytes of the stream. |
||||
Future<Uint8List> get bytes async { |
||||
final buffer = BytesBuilder(); |
||||
|
||||
await forEach(buffer.add); |
||||
|
||||
return buffer.toBytes(); |
||||
} |
||||
|
||||
/// Converts the stream into a `String` using the [utf8] encoding. |
||||
Future<String> get string => transform(utf8.decoder).join(); |
||||
|
||||
/// Converts the stream into a JSON using the [utf8] encoding. |
||||
Future<Object?> get json => transform(_utf8JsonDecoder).first; |
||||
} |
||||
|
||||
/// Extension on a http responses. |
||||
extension HttpClientResponseExtension on HttpClientResponse { |
||||
/// Returns a map of headers. |
||||
Map<String, String> get responseHeaders { |
||||
final responseHeaders = <String, String>{}; |
||||
headers.forEach((final name, final values) { |
||||
responseHeaders[name] = values.last; |
||||
}); |
||||
|
||||
return responseHeaders; |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue