129 changed files with 17279 additions and 6776 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"'); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,70 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:built_value/standard_json_plugin.dart'; |
||||
import 'package:dynamite/src/models/openapi/components.dart'; |
||||
import 'package:dynamite/src/models/openapi/discriminator.dart'; |
||||
import 'package:dynamite/src/models/openapi/header.dart'; |
||||
import 'package:dynamite/src/models/openapi/info.dart'; |
||||
import 'package:dynamite/src/models/openapi/license.dart'; |
||||
import 'package:dynamite/src/models/openapi/media_type.dart'; |
||||
import 'package:dynamite/src/models/openapi/open_api.dart'; |
||||
import 'package:dynamite/src/models/openapi/operation.dart'; |
||||
import 'package:dynamite/src/models/openapi/parameter.dart'; |
||||
import 'package:dynamite/src/models/openapi/path_item.dart'; |
||||
import 'package:dynamite/src/models/openapi/request_body.dart'; |
||||
import 'package:dynamite/src/models/openapi/response.dart'; |
||||
import 'package:dynamite/src/models/openapi/schema.dart'; |
||||
import 'package:dynamite/src/models/openapi/security_scheme.dart'; |
||||
import 'package:dynamite/src/models/openapi/server.dart'; |
||||
import 'package:dynamite/src/models/openapi/server_variable.dart'; |
||||
import 'package:dynamite/src/models/openapi/tag.dart'; |
||||
|
||||
export 'openapi/components.dart'; |
||||
export 'openapi/discriminator.dart'; |
||||
export 'openapi/header.dart'; |
||||
export 'openapi/info.dart'; |
||||
export 'openapi/license.dart'; |
||||
export 'openapi/media_type.dart'; |
||||
export 'openapi/open_api.dart'; |
||||
export 'openapi/operation.dart'; |
||||
export 'openapi/parameter.dart'; |
||||
export 'openapi/path_item.dart'; |
||||
export 'openapi/request_body.dart'; |
||||
export 'openapi/response.dart'; |
||||
export 'openapi/schema.dart'; |
||||
export 'openapi/security_scheme.dart'; |
||||
export 'openapi/server.dart'; |
||||
export 'openapi/server_variable.dart'; |
||||
export 'openapi/tag.dart'; |
||||
|
||||
part 'openapi.g.dart'; |
||||
|
||||
@SerializersFor([ |
||||
Components, |
||||
Discriminator, |
||||
Header, |
||||
Info, |
||||
License, |
||||
MediaType, |
||||
OpenAPI, |
||||
Operation, |
||||
Parameter, |
||||
PathItem, |
||||
RequestBody, |
||||
Response, |
||||
Schema, |
||||
SecurityScheme, |
||||
Server, |
||||
ServerVariable, |
||||
Tag, |
||||
]) |
||||
final Serializers serializers = (_$serializers.toBuilder() |
||||
..addBuilderFactory( |
||||
const FullType(BuiltMap, [ |
||||
FullType(String), |
||||
FullType(BuiltList, [FullType(String)]), |
||||
]), |
||||
MapBuilder<String, BuiltList<String>>.new, |
||||
) |
||||
..addPlugin(StandardJsonPlugin())) |
||||
.build(); |
@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND |
||||
|
||||
part of 'serializers.dart'; |
||||
part of 'openapi.dart'; |
||||
|
||||
// ************************************************************************** |
||||
// BuiltValueGenerator |
@ -1,8 +1,8 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/models/schema.dart'; |
||||
import 'package:dynamite/src/models/security_scheme.dart'; |
||||
import 'package:dynamite/src/models/openapi/schema.dart'; |
||||
import 'package:dynamite/src/models/openapi/security_scheme.dart'; |
||||
|
||||
part 'components.g.dart'; |
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/models/schema.dart'; |
||||
import 'package:dynamite/src/models/openapi/schema.dart'; |
||||
|
||||
part 'header.g.dart'; |
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/models/license.dart'; |
||||
import 'package:dynamite/src/models/openapi/license.dart'; |
||||
|
||||
part 'info.g.dart'; |
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/models/schema.dart'; |
||||
import 'package:dynamite/src/models/openapi/schema.dart'; |
||||
|
||||
part 'media_type.g.dart'; |
||||
|
@ -1,11 +1,11 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/models/components.dart'; |
||||
import 'package:dynamite/src/models/info.dart'; |
||||
import 'package:dynamite/src/models/path_item.dart'; |
||||
import 'package:dynamite/src/models/server.dart'; |
||||
import 'package:dynamite/src/models/tag.dart'; |
||||
import 'package:dynamite/src/models/openapi/components.dart'; |
||||
import 'package:dynamite/src/models/openapi/info.dart'; |
||||
import 'package:dynamite/src/models/openapi/path_item.dart'; |
||||
import 'package:dynamite/src/models/openapi/server.dart'; |
||||
import 'package:dynamite/src/models/openapi/tag.dart'; |
||||
|
||||
part 'open_api.g.dart'; |
||||
|
@ -0,0 +1,100 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/helpers/docs.dart'; |
||||
import 'package:dynamite/src/models/openapi/parameter.dart'; |
||||
import 'package:dynamite/src/models/openapi/request_body.dart'; |
||||
import 'package:dynamite/src/models/openapi/response.dart'; |
||||
|
||||
part 'operation.g.dart'; |
||||
|
||||
abstract class Operation implements Built<Operation, OperationBuilder> { |
||||
factory Operation([final void Function(OperationBuilder) updates]) = _$Operation; |
||||
|
||||
const Operation._(); |
||||
|
||||
static Serializer<Operation> get serializer => _$operationSerializer; |
||||
|
||||
String? get operationId; |
||||
|
||||
@BuiltValueField(compare: false) |
||||
String? get summary; |
||||
|
||||
@BuiltValueField(compare: false) |
||||
String? get description; |
||||
|
||||
bool? get deprecated; |
||||
|
||||
BuiltList<String>? get tags; |
||||
|
||||
BuiltList<Parameter>? get parameters; |
||||
|
||||
RequestBody? get requestBody; |
||||
|
||||
BuiltMap<String, Response>? get responses; |
||||
|
||||
BuiltList<BuiltMap<String, BuiltList<String>>>? get security; |
||||
|
||||
Iterable<String> formattedDescription( |
||||
final String methodName, { |
||||
final bool isRawRequest = false, |
||||
final bool requiresAuth = false, |
||||
}) sync* { |
||||
if (summary != null && summary!.isNotEmpty) { |
||||
yield* descriptionToDocs(summary); |
||||
yield docsSeparator; |
||||
} |
||||
|
||||
if (description != null && description!.isNotEmpty) { |
||||
yield* descriptionToDocs(description); |
||||
yield docsSeparator; |
||||
} |
||||
|
||||
if (isRawRequest) { |
||||
yield ''' |
||||
$docsSeparator This method and the response it returns is experimental. The API might change without a major version bump. |
||||
$docsSeparator |
||||
$docsSeparator Returns a [Future] containing a [DynamiteRawResponse] with the raw [HttpClientResponse] and serialization helpers.'''; |
||||
} else { |
||||
yield '$docsSeparator Returns a [Future] containing a [DynamiteResponse] with the status code, deserialized body and headers.'; |
||||
} |
||||
yield '$docsSeparator Throws a [DynamiteApiException] if the API call does not return an expected status code.'; |
||||
yield docsSeparator; |
||||
|
||||
if (parameters != null && parameters!.isNotEmpty) { |
||||
yield '$docsSeparator Parameters:'; |
||||
for (final parameter in parameters!) { |
||||
yield parameter.formattedDescription; |
||||
} |
||||
yield docsSeparator; |
||||
} |
||||
|
||||
if (responses != null && responses!.isNotEmpty) { |
||||
yield '$docsSeparator Status codes:'; |
||||
for (final response in responses!.entries) { |
||||
final statusCode = response.key; |
||||
final description = response.value.description; |
||||
|
||||
final buffer = StringBuffer() |
||||
..write('$docsSeparator ') |
||||
..write(' * $statusCode'); |
||||
|
||||
if (description.isNotEmpty) { |
||||
buffer |
||||
..write(': ') |
||||
..write(description); |
||||
} |
||||
|
||||
yield buffer.toString(); |
||||
} |
||||
yield docsSeparator; |
||||
} |
||||
|
||||
yield '$docsSeparator See:'; |
||||
if (isRawRequest) { |
||||
yield '$docsSeparator * [$methodName] for an operation that returns a [DynamiteResponse] with a stable API.'; |
||||
} else { |
||||
yield '$docsSeparator * [${methodName}Raw] for an experimental operation that returns a [DynamiteRawResponse] that can be serialized.'; |
||||
} |
||||
} |
||||
} |
@ -1,8 +1,8 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/models/operation.dart'; |
||||
import 'package:dynamite/src/models/parameter.dart'; |
||||
import 'package:dynamite/src/models/openapi/operation.dart'; |
||||
import 'package:dynamite/src/models/openapi/parameter.dart'; |
||||
|
||||
part 'path_item.g.dart'; |
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/models/media_type.dart'; |
||||
import 'package:dynamite/src/models/openapi/media_type.dart'; |
||||
|
||||
part 'request_body.g.dart'; |
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/models/header.dart'; |
||||
import 'package:dynamite/src/models/media_type.dart'; |
||||
import 'package:dynamite/src/models/openapi/header.dart'; |
||||
import 'package:dynamite/src/models/openapi/media_type.dart'; |
||||
|
||||
part 'response.g.dart'; |
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/models/server_variable.dart'; |
||||
import 'package:dynamite/src/models/openapi/server_variable.dart'; |
||||
|
||||
part 'server.g.dart'; |
||||
|
@ -1,47 +0,0 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/built_value.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:dynamite/src/helpers/docs.dart'; |
||||
import 'package:dynamite/src/models/parameter.dart'; |
||||
import 'package:dynamite/src/models/request_body.dart'; |
||||
import 'package:dynamite/src/models/response.dart'; |
||||
|
||||
part 'operation.g.dart'; |
||||
|
||||
abstract class Operation implements Built<Operation, OperationBuilder> { |
||||
factory Operation([final void Function(OperationBuilder) updates]) = _$Operation; |
||||
|
||||
const Operation._(); |
||||
|
||||
static Serializer<Operation> get serializer => _$operationSerializer; |
||||
|
||||
String? get operationId; |
||||
|
||||
@BuiltValueField(compare: false) |
||||
String? get summary; |
||||
|
||||
@BuiltValueField(compare: false) |
||||
String? get description; |
||||
|
||||
bool? get deprecated; |
||||
|
||||
BuiltList<String>? get tags; |
||||
|
||||
BuiltList<Parameter>? get parameters; |
||||
|
||||
RequestBody? get requestBody; |
||||
|
||||
BuiltMap<String, Response>? get responses; |
||||
|
||||
BuiltList<BuiltMap<String, BuiltList<String>>>? get security; |
||||
|
||||
Iterable<String> get formattedDescription sync* { |
||||
yield* descriptionToDocs(summary); |
||||
|
||||
if (summary != null && description != null) { |
||||
yield docsSeparator; |
||||
} |
||||
|
||||
yield* descriptionToDocs(description); |
||||
} |
||||
} |
@ -1,52 +0,0 @@
|
||||
import 'package:built_collection/built_collection.dart'; |
||||
import 'package:built_value/serializer.dart'; |
||||
import 'package:built_value/standard_json_plugin.dart'; |
||||
import 'package:dynamite/src/models/components.dart'; |
||||
import 'package:dynamite/src/models/discriminator.dart'; |
||||
import 'package:dynamite/src/models/header.dart'; |
||||
import 'package:dynamite/src/models/info.dart'; |
||||
import 'package:dynamite/src/models/license.dart'; |
||||
import 'package:dynamite/src/models/media_type.dart'; |
||||
import 'package:dynamite/src/models/open_api.dart'; |
||||
import 'package:dynamite/src/models/operation.dart'; |
||||
import 'package:dynamite/src/models/parameter.dart'; |
||||
import 'package:dynamite/src/models/path_item.dart'; |
||||
import 'package:dynamite/src/models/request_body.dart'; |
||||
import 'package:dynamite/src/models/response.dart'; |
||||
import 'package:dynamite/src/models/schema.dart'; |
||||
import 'package:dynamite/src/models/security_scheme.dart'; |
||||
import 'package:dynamite/src/models/server.dart'; |
||||
import 'package:dynamite/src/models/server_variable.dart'; |
||||
import 'package:dynamite/src/models/tag.dart'; |
||||
|
||||
part 'serializers.g.dart'; |
||||
|
||||
@SerializersFor([ |
||||
Components, |
||||
Discriminator, |
||||
Header, |
||||
Info, |
||||
License, |
||||
MediaType, |
||||
OpenAPI, |
||||
Operation, |
||||
Parameter, |
||||
PathItem, |
||||
RequestBody, |
||||
Response, |
||||
Schema, |
||||
SecurityScheme, |
||||
Server, |
||||
ServerVariable, |
||||
Tag, |
||||
]) |
||||
final Serializers serializers = (_$serializers.toBuilder() |
||||
..addBuilderFactory( |
||||
const FullType(BuiltMap, [ |
||||
FullType(String), |
||||
FullType(BuiltList, [FullType(String)]), |
||||
]), |
||||
MapBuilder<String, BuiltList<String>>.new, |
||||
) |
||||
..addPlugin(StandardJsonPlugin())) |
||||
.build(); |
@ -0,0 +1 @@
|
||||
export 'type_result/type_result.dart'; |
@ -0,0 +1,50 @@
|
||||
part of 'type_result.dart'; |
||||
|
||||
@immutable |
||||
class TypeResultBase extends TypeResult { |
||||
TypeResultBase( |
||||
super.className, { |
||||
super.nullable, |
||||
}); |
||||
|
||||
@override |
||||
String? get _builderFactory => null; |
||||
|
||||
@override |
||||
String? get _serializer => null; |
||||
|
||||
@override |
||||
String serialize(final String object) => object; |
||||
|
||||
@override |
||||
String encode( |
||||
final String object, { |
||||
final bool onlyChildren = false, |
||||
final String? mimeType, |
||||
}) { |
||||
switch (mimeType) { |
||||
case null: |
||||
case 'application/json': |
||||
case 'application/x-www-form-urlencoded': |
||||
if (className == 'String') { |
||||
return object; |
||||
} else { |
||||
return '$object.toString()'; |
||||
} |
||||
case 'application/octet-stream': |
||||
return 'utf8.encode($object) as Uint8List'; |
||||
default: |
||||
throw Exception('Can not encode mime type "$mimeType"'); |
||||
} |
||||
} |
||||
|
||||
@override |
||||
TypeResultBase get dartType { |
||||
final dartName = switch (name) { |
||||
'JsonObject' => 'dynamic', |
||||
_ => name, |
||||
}; |
||||
|
||||
return TypeResultBase(dartName, nullable: nullable); |
||||
} |
||||
} |
@ -1,58 +0,0 @@
|
||||
part of 'type_result.dart'; |
||||
|
||||
@immutable |
||||
class TypeResultBase extends TypeResult { |
||||
TypeResultBase( |
||||
super.className, { |
||||
super.nullable, |
||||
}); |
||||
|
||||
@override |
||||
String? get _builderFactory => null; |
||||
|
||||
@override |
||||
String? get _serializer => null; |
||||
|
||||
@override |
||||
String serialize(final String object) => object; |
||||
|
||||
@override |
||||
String encode( |
||||
final String object, { |
||||
final bool onlyChildren = false, |
||||
final String? mimeType, |
||||
}) => |
||||
name == 'String' ? object : '$object.toString()'; |
||||
|
||||
@override |
||||
String deserialize(final String object, {final bool toBuilder = false}) => '($object as $nullableName)'; |
||||
|
||||
@override |
||||
String decode(final String object) { |
||||
switch (name) { |
||||
case 'String': |
||||
return '($object as String)'; |
||||
case 'int': |
||||
return 'int.parse($object as String)'; |
||||
case 'bool': |
||||
return "($object as String == 'true')"; |
||||
case 'JsonObject': |
||||
return 'JsonObject($object)'; |
||||
default: |
||||
throw Exception('Can not decode "$name" from String'); |
||||
} |
||||
} |
||||
|
||||
@override |
||||
TypeResultBase get dartType { |
||||
final String dartName; |
||||
switch (name) { |
||||
case 'JsonObject': |
||||
dartName = 'dynamic'; |
||||
default: |
||||
dartName = name; |
||||
} |
||||
|
||||
return TypeResultBase(dartName, nullable: nullable); |
||||
} |
||||
} |
@ -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,165 +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: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; |
||||
} |
||||
} |
||||
|
||||
class DynamiteResponse<T, U> { |
||||
DynamiteResponse( |
||||
this.data, |
||||
this.headers, |
||||
); |
||||
|
||||
final T data; |
||||
|
||||
final U headers; |
||||
|
||||
@override |
||||
String toString() => 'DynamiteResponse(data: $data, headers: $headers)'; |
||||
} |
||||
|
||||
class DynamiteApiException implements Exception { |
||||
DynamiteApiException( |
||||
this.statusCode, |
||||
this.headers, |
||||
this.body, |
||||
); |
||||
|
||||
final int statusCode; |
||||
|
||||
final Map<String, String> headers; |
||||
|
||||
final String body; |
||||
|
||||
@override |
||||
String toString() => 'DynamiteApiException(statusCode: $statusCode, headers: $headers, body: $body)'; |
||||
} |
||||
|
||||
abstract class DynamiteAuthentication { |
||||
String get type; |
||||
String get scheme; |
||||
Map<String, String> get headers; |
||||
} |
||||
|
||||
class DynamiteHttpBasicAuthentication extends DynamiteAuthentication { |
||||
DynamiteHttpBasicAuthentication({ |
||||
required this.username, |
||||
required this.password, |
||||
}); |
||||
|
||||
final String username; |
||||
|
||||
final String password; |
||||
|
||||
@override |
||||
String type = 'http'; |
||||
|
||||
@override |
||||
String scheme = 'basic'; |
||||
|
||||
@override |
||||
Map<String, String> get headers => { |
||||
'Authorization': 'Basic ${base64.encode(utf8.encode('$username:$password'))}', |
||||
}; |
||||
} |
||||
|
||||
class DynamiteHttpBearerAuthentication extends DynamiteAuthentication { |
||||
DynamiteHttpBearerAuthentication({ |
||||
required this.token, |
||||
}); |
||||
|
||||
final String token; |
||||
|
||||
@override |
||||
String type = 'http'; |
||||
|
||||
@override |
||||
String scheme = 'bearer'; |
||||
|
||||
@override |
||||
Map<String, String> get headers => { |
||||
'Authorization': 'Bearer $token', |
||||
}; |
||||
} |
||||
|
||||
class DynamiteClient { |
||||
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(); |
||||
|
||||
final Uri baseURL; |
||||
|
||||
final Map<String, String>? baseHeaders; |
||||
|
||||
final HttpClient httpClient; |
||||
|
||||
final CookieJar? cookieJar; |
||||
|
||||
final List<DynamiteAuthentication> authentications; |
||||
|
||||
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; |
||||
} |
||||
} |
@ -0,0 +1,26 @@
|
||||
/// Checks the [input] against [pattern]. |
||||
/// |
||||
/// Throws an `Exception` containing the [parameterName] if the `pattern` does not match. |
||||
void checkPattern(final String input, final RegExp pattern, final String parameterName) { |
||||
if (!pattern.hasMatch(input)) { |
||||
throw Exception('Invalid value "$input" for parameter "$parameterName" with pattern "${pattern.pattern}"'); |
||||
} |
||||
} |
||||
|
||||
/// Checks the [input] length against [minLength]. |
||||
/// |
||||
/// Throws an `Exception` containing the [parameterName] if the `input` is to short. |
||||
void checkMinLength(final String input, final int minLength, final String parameterName) { |
||||
if (input.length < minLength) { |
||||
throw Exception('Parameter "$input" has to be at least $minLength characters long'); |
||||
} |
||||
} |
||||
|
||||
/// Checks the [input] length against [maxLength]. |
||||
/// |
||||
/// Throws an `Exception` containing the [parameterName] if the `input` is to long. |
||||
void checkMaxLength(final String input, final int maxLength, final String parameterName) { |
||||
if (input.length > maxLength) { |
||||
throw Exception('Parameter "$input" has to be at most $maxLength characters long'); |
||||
} |
||||
} |
@ -1 +1,2 @@
|
||||
export 'src/string_checker.dart'; |
||||
export 'src/uri.dart'; |
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue