Browse Source

Merge pull request #846 from nextcloud/refactor/nextcloud/dynamite_response

Refactor/nextcloud/dynamite response
pull/655/head
Nikolas Rimikis 2 years ago committed by GitHub
parent
commit
540107539f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      packages/app/integration_test/screenshot_test.dart
  2. 759
      packages/dynamite/dynamite/lib/src/builder/client.dart
  3. 16
      packages/dynamite/dynamite/lib/src/builder/header_serializer.dart
  4. 5
      packages/dynamite/dynamite/lib/src/builder/imports.dart
  5. 13
      packages/dynamite/dynamite/lib/src/builder/ofs_builder.dart
  6. 9
      packages/dynamite/dynamite/lib/src/builder/resolve_enum.dart
  7. 9
      packages/dynamite/dynamite/lib/src/builder/resolve_interface.dart
  8. 129
      packages/dynamite/dynamite/lib/src/builder/resolve_mime_type.dart
  9. 9
      packages/dynamite/dynamite/lib/src/builder/resolve_object.dart
  10. 9
      packages/dynamite/dynamite/lib/src/builder/resolve_type.dart
  11. 10
      packages/dynamite/dynamite/lib/src/builder/serializer.dart
  12. 2
      packages/dynamite/dynamite/lib/src/builder/state.dart
  13. 2
      packages/dynamite/dynamite/lib/src/helpers/built_value.dart
  14. 8
      packages/dynamite/dynamite/lib/src/helpers/docs.dart
  15. 7
      packages/dynamite/dynamite/lib/src/helpers/dynamite.dart
  16. 2
      packages/dynamite/dynamite/lib/src/helpers/type_result.dart
  17. 70
      packages/dynamite/dynamite/lib/src/models/openapi.dart
  18. 2
      packages/dynamite/dynamite/lib/src/models/openapi.g.dart
  19. 4
      packages/dynamite/dynamite/lib/src/models/openapi/components.dart
  20. 0
      packages/dynamite/dynamite/lib/src/models/openapi/components.g.dart
  21. 0
      packages/dynamite/dynamite/lib/src/models/openapi/discriminator.dart
  22. 0
      packages/dynamite/dynamite/lib/src/models/openapi/discriminator.g.dart
  23. 2
      packages/dynamite/dynamite/lib/src/models/openapi/header.dart
  24. 0
      packages/dynamite/dynamite/lib/src/models/openapi/header.g.dart
  25. 2
      packages/dynamite/dynamite/lib/src/models/openapi/info.dart
  26. 0
      packages/dynamite/dynamite/lib/src/models/openapi/info.g.dart
  27. 0
      packages/dynamite/dynamite/lib/src/models/openapi/license.dart
  28. 0
      packages/dynamite/dynamite/lib/src/models/openapi/license.g.dart
  29. 2
      packages/dynamite/dynamite/lib/src/models/openapi/media_type.dart
  30. 0
      packages/dynamite/dynamite/lib/src/models/openapi/media_type.g.dart
  31. 10
      packages/dynamite/dynamite/lib/src/models/openapi/open_api.dart
  32. 0
      packages/dynamite/dynamite/lib/src/models/openapi/open_api.g.dart
  33. 100
      packages/dynamite/dynamite/lib/src/models/openapi/operation.dart
  34. 0
      packages/dynamite/dynamite/lib/src/models/openapi/operation.g.dart
  35. 18
      packages/dynamite/dynamite/lib/src/models/openapi/parameter.dart
  36. 0
      packages/dynamite/dynamite/lib/src/models/openapi/parameter.g.dart
  37. 4
      packages/dynamite/dynamite/lib/src/models/openapi/path_item.dart
  38. 0
      packages/dynamite/dynamite/lib/src/models/openapi/path_item.g.dart
  39. 2
      packages/dynamite/dynamite/lib/src/models/openapi/request_body.dart
  40. 0
      packages/dynamite/dynamite/lib/src/models/openapi/request_body.g.dart
  41. 4
      packages/dynamite/dynamite/lib/src/models/openapi/response.dart
  42. 0
      packages/dynamite/dynamite/lib/src/models/openapi/response.g.dart
  43. 2
      packages/dynamite/dynamite/lib/src/models/openapi/schema.dart
  44. 0
      packages/dynamite/dynamite/lib/src/models/openapi/schema.g.dart
  45. 0
      packages/dynamite/dynamite/lib/src/models/openapi/security_scheme.dart
  46. 0
      packages/dynamite/dynamite/lib/src/models/openapi/security_scheme.g.dart
  47. 2
      packages/dynamite/dynamite/lib/src/models/openapi/server.dart
  48. 0
      packages/dynamite/dynamite/lib/src/models/openapi/server.g.dart
  49. 0
      packages/dynamite/dynamite/lib/src/models/openapi/server_variable.dart
  50. 0
      packages/dynamite/dynamite/lib/src/models/openapi/server_variable.g.dart
  51. 0
      packages/dynamite/dynamite/lib/src/models/openapi/tag.dart
  52. 0
      packages/dynamite/dynamite/lib/src/models/openapi/tag.g.dart
  53. 47
      packages/dynamite/dynamite/lib/src/models/operation.dart
  54. 52
      packages/dynamite/dynamite/lib/src/models/serializers.dart
  55. 1
      packages/dynamite/dynamite/lib/src/models/type_result.dart
  56. 50
      packages/dynamite/dynamite/lib/src/models/type_result/base.dart
  57. 0
      packages/dynamite/dynamite/lib/src/models/type_result/enum.dart
  58. 0
      packages/dynamite/dynamite/lib/src/models/type_result/list.dart
  59. 0
      packages/dynamite/dynamite/lib/src/models/type_result/map.dart
  60. 0
      packages/dynamite/dynamite/lib/src/models/type_result/object.dart
  61. 2
      packages/dynamite/dynamite/lib/src/models/type_result/type_result.dart
  62. 11
      packages/dynamite/dynamite/lib/src/openapi_builder.dart
  63. 58
      packages/dynamite/dynamite/lib/src/type_result/base.dart
  64. 2
      packages/dynamite/dynamite/test/type_result_test.dart
  65. 4
      packages/dynamite/dynamite_runtime/analysis_options.yaml
  66. 4
      packages/dynamite/dynamite_runtime/lib/http_client.dart
  67. 445
      packages/dynamite/dynamite_runtime/lib/src/dynamite_client.dart
  68. 165
      packages/dynamite/dynamite_runtime/lib/src/http_client.dart
  69. 43
      packages/dynamite/dynamite_runtime/lib/src/http_extensions.dart
  70. 26
      packages/dynamite/dynamite_runtime/lib/src/string_checker.dart
  71. 1
      packages/dynamite/dynamite_runtime/lib/utils.dart
  72. 1
      packages/dynamite/dynamite_runtime/pubspec.yaml
  73. 18
      packages/neon/neon/lib/src/bloc/result.dart
  74. 6
      packages/neon/neon/lib/src/blocs/apps.dart
  75. 6
      packages/neon/neon/lib/src/blocs/capabilities.dart
  76. 2
      packages/neon/neon/lib/src/blocs/login_check_account.dart
  77. 2
      packages/neon/neon/lib/src/blocs/login_check_server_status.dart
  78. 6
      packages/neon/neon/lib/src/blocs/login_flow.dart
  79. 3
      packages/neon/neon/lib/src/blocs/next_push.dart
  80. 2
      packages/neon/neon/lib/src/blocs/push_notifications.dart
  81. 5
      packages/neon/neon/lib/src/blocs/unified_search.dart
  82. 6
      packages/neon/neon/lib/src/blocs/user_details.dart
  83. 4
      packages/neon/neon/lib/src/blocs/user_statuses.dart
  84. 2
      packages/neon/neon/lib/src/pages/home.dart
  85. 7
      packages/neon/neon/lib/src/utils/push_utils.dart
  86. 172
      packages/neon/neon/lib/src/utils/request_manager.dart
  87. 7
      packages/neon/neon/lib/src/widgets/cached_image.dart
  88. 24
      packages/neon/neon/lib/src/widgets/user_avatar.dart
  89. 2
      packages/neon/neon_files/lib/blocs/browser.dart
  90. 6
      packages/neon/neon_news/lib/blocs/articles.dart
  91. 16
      packages/neon/neon_news/lib/blocs/news.dart
  92. 6
      packages/neon/neon_notes/lib/blocs/note.dart
  93. 6
      packages/neon/neon_notes/lib/blocs/notes.dart
  94. 11
      packages/neon/neon_notifications/lib/blocs/notifications.dart
  95. 45
      packages/nextcloud/lib/src/api/comments.openapi.dart
  96. 4410
      packages/nextcloud/lib/src/api/core.openapi.dart
  97. 333
      packages/nextcloud/lib/src/api/dashboard.openapi.dart
  98. 147
      packages/nextcloud/lib/src/api/dav.openapi.dart
  99. 1305
      packages/nextcloud/lib/src/api/files.openapi.dart
  100. 135
      packages/nextcloud/lib/src/api/files_external.openapi.dart
  101. Some files were not shown because too many files have changed in this diff Show More

3
packages/app/integration_test/screenshot_test.dart

@ -53,6 +53,7 @@ Future<Account> getAccount(final String username) async {
loginName: username,
password: username,
).core.appPassword.getAppPassword())
.body
.ocs
.data
.apppassword;
@ -149,7 +150,7 @@ Future<void> main() async {
final folder = await account.client.news.createFolder(name: 'test');
await account.client.news.addFeed(
url: nasaFeedURL,
folderId: folder.folders.single.id,
folderId: folder.body.folders.single.id,
);
await runTestApp(

759
packages/dynamite/dynamite/lib/src/builder/client.dart

@ -1,121 +1,21 @@
import 'package:built_collection/built_collection.dart';
import 'package:code_builder/code_builder.dart';
import 'package:collection/collection.dart';
import 'package:dynamite/src/builder/resolve_mime_type.dart';
import 'package:dynamite/src/builder/resolve_object.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/helpers/type_result.dart';
import 'package:dynamite/src/models/open_api.dart';
import 'package:dynamite/src/models/path_item.dart';
import 'package:dynamite/src/models/response.dart';
import 'package:dynamite/src/models/schema.dart';
import 'package:dynamite/src/type_result/type_result.dart';
List<Class> generateDynamiteOverrides(final State state) => [
Class(
(final b) => b
..name = '${state.classPrefix}Response'
..types.addAll([
refer('T'),
refer('U'),
])
..extend = refer('DynamiteResponse<T, U>')
..constructors.add(
Constructor(
(final b) => b
..requiredParameters.addAll(
['data', 'headers'].map(
(final name) => Parameter(
(final b) => b
..name = name
..toSuper = true,
),
),
),
),
)
..methods.add(
Method(
(final b) => b
..name = 'toString'
..returns = refer('String')
..annotations.add(refer('override'))
..lambda = true
..body = Code(
"'${state.classPrefix}Response(data: \$data, headers: \$headers)'",
),
),
),
),
Class(
(final b) => b
..name = '${state.classPrefix}ApiException'
..extend = refer('DynamiteApiException')
..constructors.add(
Constructor(
(final b) => b
..requiredParameters.addAll(
['statusCode', 'headers', 'body'].map(
(final name) => Parameter(
(final b) => b
..name = name
..toSuper = true,
),
),
),
),
)
..methods.addAll([
Method(
(final b) => b
..name = 'fromResponse'
..returns = refer('Future<${state.classPrefix}ApiException>')
..static = true
..modifier = MethodModifier.async
..requiredParameters.add(
Parameter(
(final b) => b
..name = 'response'
..type = refer('HttpClientResponse'),
),
)
..body = Code('''
String body;
try {
body = await response.body;
} on FormatException {
body = 'binary';
}
return ${state.classPrefix}ApiException(
response.statusCode,
response.responseHeaders,
body,
);
'''),
),
Method(
(final b) => b
..name = 'toString'
..returns = refer('String')
..annotations.add(refer('override'))
..lambda = true
..body = Code(
"'${state.classPrefix}ApiException(statusCode: \$statusCode, headers: \$headers, body: \$body)'",
),
),
]),
),
];
import 'package:dynamite/src/models/openapi.dart' as openapi;
import 'package:dynamite/src/models/type_result.dart';
import 'package:intersperse/intersperse.dart';
Iterable<Class> generateClients(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
) sync* {
yield* generateDynamiteOverrides(state);
final tags = generateTags(spec);
yield buildRootClient(spec, state, tags);
@ -125,7 +25,7 @@ Iterable<Class> generateClients(
}
Class buildRootClient(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
final List<String> tags,
) =>
@ -219,12 +119,12 @@ super(
);
}
b.methods.addAll(buildTags(spec, state, tags, null));
b.methods.addAll(buildTags(spec, state, null));
},
);
Class buildClient(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
final List<String> tags,
final String tag,
@ -267,366 +167,343 @@ Class buildClient(
);
}
b.methods.addAll(buildTags(spec, state, tags, tag));
b.methods.addAll(buildTags(spec, state, tag));
},
);
Iterable<Method> buildTags(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
final List<String> tags,
final String? tag,
) sync* {
final isRootClient = tag == null;
final client = tag == null ? 'this' : '_rootClient';
final paths = generatePaths(spec, tag);
for (final pathEntry in paths.entries) {
for (final operationEntry in pathEntry.value.operations.entries) {
yield Method(
(final b) {
final httpMethod = operationEntry.key.name;
final operation = operationEntry.value;
final operationId = operation.operationId ?? toDartName('$httpMethod-${pathEntry.key}');
final parameters = [
...?pathEntry.value.parameters,
...?operation.parameters,
]..sort(sortRequiredParameters);
b
..name = toDartName(filterMethodName(operationId, tag ?? ''))
..modifier = MethodModifier.async
..docs.addAll(operation.formattedDescription);
if (operation.deprecated ?? false) {
b.annotations.add(refer('Deprecated').call([refer("''")]));
}
final httpMethod = operationEntry.key.name;
final operation = operationEntry.value;
final operationId = operation.operationId ?? toDartName('$httpMethod-${pathEntry.key}');
final parameters = [
...?pathEntry.value.parameters,
...?operation.parameters,
]..sort(sortRequiredParameters);
final name = toDartName(filterMethodName(operationId, tag ?? ''));
var responses = <openapi.Response, List<int>>{};
if (operation.responses != null) {
for (final responseEntry in operation.responses!.entries) {
final statusCode = int.parse(responseEntry.key);
final response = responseEntry.value;
responses[response] ??= [];
responses[response]!.add(statusCode);
}
var responses = <Response, List<int>>{};
if (operation.responses != null) {
for (final responseEntry in operation.responses!.entries) {
final statusCode = int.parse(responseEntry.key);
final response = responseEntry.value;
if (responses.length > 1) {
print('$operationId uses more than one response schema but we only generate the first one');
responses = Map.fromEntries([responses.entries.first]);
}
}
responses[response] ??= [];
responses[response]!.add(statusCode);
}
final code = StringBuffer();
final acceptHeader = responses.keys
.map((final response) => response.content?.keys)
.whereNotNull()
.expand((final element) => element)
.toSet()
.join(',');
code.writeln('''
var _path = '${pathEntry.key}';
final _queryParameters = <String, dynamic>{};
final _headers = <String, String>{${acceptHeader.isNotEmpty ? "'Accept': '$acceptHeader'," : ''}};
Uint8List? _body;
''');
buildAuthCheck(
responses,
pathEntry,
operation,
spec,
client,
).forEach(code.writeln);
final operationParameters = ListBuilder<Parameter>();
final annotations = operation.deprecated ?? false ? refer('Deprecated').call([refer("''")]) : null;
var returnDataType = 'void';
var returnHeadersType = 'void';
for (final parameter in parameters) {
final dartParameterNullable = isDartParameterNullable(
parameter.required,
parameter.schema,
);
final result = resolveType(
spec,
state,
toDartName(
'$operationId-${parameter.name}',
uppercaseFirstCharacter: true,
),
parameter.schema!,
nullable: dartParameterNullable,
).dartType;
buildPatternCheck(result, parameter).forEach(code.writeln);
final defaultValueCode = parameter.schema?.$default != null
? valueToEscapedValue(result, parameter.schema!.$default.toString())
: null;
operationParameters.add(
Parameter(
(final b) {
b
..named = true
..name = toDartName(parameter.name)
..required = parameter.isDartRequired;
if (parameter.schema != null) {
b.type = refer(result.nullableName);
}
if (defaultValueCode != null) {
b.defaultTo = Code(defaultValueCode);
}
},
),
);
if (responses.length > 1) {
print('$operationId uses more than one response schema but we only generate the first one');
responses = Map.fromEntries([responses.entries.first]);
}
}
if (dartParameterNullable) {
code.writeln('if (${toDartName(parameter.name)} != null) {');
}
final value = result.encode(
toDartName(parameter.name),
onlyChildren: result is TypeResultList && parameter.$in == 'query',
);
if (defaultValueCode != null && parameter.$in == 'query') {
code.writeln('if (${toDartName(parameter.name)} != $defaultValueCode) {');
}
switch (parameter.$in) {
case 'path':
code.writeln(
"_path = _path.replaceAll('{${parameter.name}}', Uri.encodeQueryComponent($value));",
);
case 'query':
code.writeln(
"_queryParameters['${parameter.name}'] = $value;",
);
case 'header':
code.writeln(
"_headers['${parameter.name}'] = $value;",
);
default:
throw Exception('Can not work with parameter in "${parameter.$in}"');
}
if (defaultValueCode != null && parameter.$in == 'query') {
code.writeln('}');
}
if (dartParameterNullable) {
code.writeln('}');
}
}
resolveMimeTypeEncode(operation, spec, state, operationId, operationParameters).forEach(code.writeln);
for (final responseEntry in responses.entries) {
final response = responseEntry.key;
final statusCodes = responseEntry.value;
TypeResult? headersType;
if (response.headers != null) {
final identifier =
'${tag != null ? toDartName(tag, uppercaseFirstCharacter: true) : null}${toDartName(operationId, uppercaseFirstCharacter: true)}Headers';
headersType = resolveObject(
spec,
state,
identifier,
openapi.Schema(
(final b) => b
..properties.replace(
response.headers!.map(
(final headerName, final value) => MapEntry(
headerName.toLowerCase(),
value.schema!,
),
),
),
),
isHeader: true,
);
}
final acceptHeader = responses.keys
.map((final response) => response.content?.keys)
.whereNotNull()
.expand((final element) => element)
.toSet()
.join(',');
final code = StringBuffer('''
var _path = '${pathEntry.key}';
final _queryParameters = <String, dynamic>{};
final _headers = <String, String>{${acceptHeader.isNotEmpty ? "'Accept': '$acceptHeader'," : ''}};
Uint8List? _body;
''');
final dataType = resolveMimeTypeDecode(
response,
spec,
state,
toDartName(
'$operationId-response${responses.entries.length > 1 ? '-${responses.entries.toList().indexOf(responseEntry)}' : ''}',
uppercaseFirstCharacter: true,
),
);
final security = operation.security ?? spec.security ?? BuiltList();
final securityRequirements = security.where((final requirement) => requirement.isNotEmpty);
final isOptionalSecurity = securityRequirements.length != security.length;
code.write(' // coverage:ignore-start\n');
for (final requirement in securityRequirements) {
final securityScheme = spec.components!.securitySchemes![requirement.keys.single]!;
code.write('''
if (${isRootClient ? 'this' : '_rootClient'}.authentications.where((final a) => a.type == '${securityScheme.type}' && a.scheme == '${securityScheme.scheme}').isNotEmpty) {
_headers.addAll(${isRootClient ? 'this' : '_rootClient'}.authentications.singleWhere((final a) => a.type == '${securityScheme.type}' && a.scheme == '${securityScheme.scheme}').headers);
}
''');
if (securityRequirements.last != requirement) {
code.write('else ');
}
}
if (securityRequirements.isNotEmpty && !isOptionalSecurity) {
code.write('''
else {
throw Exception('Missing authentication for ${securityRequirements.map((final r) => r.keys.single).join(' or ')}');
}
''');
}
code.write(' // coverage:ignore-end\n');
code.writeln(
'final _uri = Uri(path: _path, queryParameters: _queryParameters.isNotEmpty ? _queryParameters : null);',
);
for (final parameter in parameters) {
final dartParameterNullable = isDartParameterNullable(
parameter.required,
parameter.schema,
);
if (dataType != null) {
returnDataType = dataType.name;
}
if (headersType != null) {
returnHeadersType = headersType.name;
}
final result = resolveType(
spec,
state,
toDartName(
'$operationId-${parameter.name}',
uppercaseFirstCharacter: true,
),
parameter.schema!,
nullable: dartParameterNullable,
).dartType;
if (result.name == 'String') {
if (parameter.schema?.pattern != null) {
code.write('''
if (!RegExp(r'${parameter.schema!.pattern!}').hasMatch(${toDartName(parameter.name)})) {
throw Exception('Invalid value "\$${toDartName(parameter.name)}" for parameter "${toDartName(parameter.name)}" with pattern "\${r'${parameter.schema!.pattern!}'}"'); // coverage:ignore-line
}
code.writeln('''
return DynamiteRawResponse<$returnDataType, $returnHeadersType>(
response: $client.doRequest(
'$httpMethod',
_uri,
_headers,
_body,
''');
}
if (parameter.schema?.minLength != null) {
code.write('''
if (${toDartName(parameter.name)}.length < ${parameter.schema!.minLength!}) {
throw Exception('Parameter "${toDartName(parameter.name)}" has to be at least ${parameter.schema!.minLength!} characters long'); // coverage:ignore-line
}
''');
}
if (parameter.schema?.maxLength != null) {
code.write('''
if (${toDartName(parameter.name)}.length > ${parameter.schema!.maxLength!}) {
throw Exception('Parameter "${toDartName(parameter.name)}" has to be at most ${parameter.schema!.maxLength!} characters long'); // coverage:ignore-line
}
''');
}
}
final defaultValueCode = parameter.schema?.$default != null
? valueToEscapedValue(result, parameter.schema!.$default.toString())
: null;
b.optionalParameters.add(
Parameter(
(final b) {
b
..named = true
..name = toDartName(parameter.name)
..required = parameter.isDartRequired;
if (parameter.schema != null) {
b.type = refer(result.nullableName);
}
if (defaultValueCode != null) {
b.defaultTo = Code(defaultValueCode);
}
},
),
);
if (responses.values.isNotEmpty) {
final codes = statusCodes.join(',');
code.writeln('const {$codes},');
}
if (dartParameterNullable) {
code.write('if (${toDartName(parameter.name)} != null) {');
}
final value = result.encode(
toDartName(parameter.name),
onlyChildren: result is TypeResultList && parameter.$in == 'query',
);
if (defaultValueCode != null && parameter.$in == 'query') {
code.write('if (${toDartName(parameter.name)} != $defaultValueCode) {');
}
switch (parameter.$in) {
case 'path':
code.write(
"_path = _path.replaceAll('{${parameter.name}}', Uri.encodeQueryComponent($value));",
);
case 'query':
code.write(
"_queryParameters['${parameter.name}'] = $value;",
);
case 'header':
code.write(
"_headers['${parameter.name}'] = $value;",
);
default:
throw Exception('Can not work with parameter in "${parameter.$in}"');
}
if (defaultValueCode != null && parameter.$in == 'query') {
code.write('}');
}
if (dartParameterNullable) {
code.write('}');
}
}
code.writeln('''
),
bodyType: ${dataType?.fullType},
headersType: ${headersType?.fullType},
serializers: _jsonSerializers,
);
''');
}
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;
code.write("_headers['Content-Type'] = '$mimeType';");
final dartParameterNullable = isDartParameterNullable(
operation.requestBody!.required,
mediaType.schema,
);
final result = resolveType(
spec,
state,
toDartName('$operationId-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.optionalParameters.add(
Parameter(
(final b) => b
..name = parameterName
..type = refer(result.nullableName)
..named = true
..required = dartParameterRequired,
),
);
if (dartParameterNullable) {
code.write('if ($parameterName != null) {');
}
code.write(
'_body = utf8.encode(${result.encode(parameterName, mimeType: mimeType)}) as Uint8List;',
);
if (dartParameterNullable) {
code.write('}');
}
default:
throw Exception('Can not parse mime type "$mimeType"');
}
}
}
yield Method((final b) {
b
..name = name
..modifier = MethodModifier.async
..docs.addAll(operation.formattedDescription(name));
code.write(
'''
final _response = await ${isRootClient ? 'this' : '_rootClient'}.doRequest(
'$httpMethod',
Uri(path: _path, queryParameters: _queryParameters.isNotEmpty ? _queryParameters : null),
_headers,
_body,
);
''',
);
if (annotations != null) {
b.annotations.add(annotations);
}
for (final responseEntry in responses.entries) {
final response = responseEntry.key;
final statusCodes = responseEntry.value;
code.write(
'if (${statusCodes.map((final statusCode) => '_response.statusCode == $statusCode').join(' || ')}) {',
);
final parameters = operationParameters.build();
final rawParameters = parameters.map((final p) => '${p.name}: ${p.name},').join('\n');
String? headersType;
String? headersValue;
if (response.headers != null) {
final identifier =
'${tag != null ? toDartName(tag, uppercaseFirstCharacter: true) : null}${toDartName(operationId, uppercaseFirstCharacter: true)}Headers';
final result = resolveObject(
spec,
state,
identifier,
Schema(
(final b) => b
..properties.replace(
response.headers!.map(
(final headerName, final value) => MapEntry(
headerName.toLowerCase(),
value.schema!,
),
),
),
),
isHeader: true,
);
headersType = result.name;
headersValue = result.deserialize('_response.responseHeaders');
}
b
..optionalParameters.addAll(parameters)
..returns = refer('Future<DynamiteResponse<$returnDataType, $returnHeadersType>>')
..body = Code('''
final rawResponse = ${name}Raw(
$rawParameters
);
String? dataType;
String? dataValue;
bool? dataNeedsAwait;
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(
'$operationId-response${responses.entries.length > 1 ? '-${responses.entries.toList().indexOf(responseEntry)}' : ''}-$mimeType',
uppercaseFirstCharacter: true,
),
mediaType.schema!,
);
if (mimeType == '*/*' || mimeType == 'application/octet-stream' || mimeType.startsWith('image/')) {
dataType = 'Uint8List';
dataValue = '_response.bodyBytes';
dataNeedsAwait = true;
} else if (mimeType.startsWith('text/') || mimeType == 'application/javascript') {
dataType = 'String';
dataValue = '_response.body';
dataNeedsAwait = true;
} else if (mimeType == 'application/json') {
dataType = result.name;
if (result.name == 'dynamic') {
dataValue = '';
} else if (result.name == 'String') {
dataValue = '_response.body';
dataNeedsAwait = true;
} else if (result is TypeResultEnum || result is TypeResultBase) {
dataValue = result.deserialize(result.decode('await _response.body'));
dataNeedsAwait = false;
} else {
dataValue = result.deserialize('await _response.jsonBody');
dataNeedsAwait = false;
}
} else {
throw Exception('Can not parse mime type "$mimeType"');
}
}
}
return rawResponse.future;
''');
});
if (headersType != null && dataType != null) {
b.returns = refer('Future<${state.classPrefix}Response<$dataType, $headersType>>');
code.write(
'return ${state.classPrefix}Response<$dataType, $headersType>(${dataNeedsAwait ?? false ? 'await ' : ''}$dataValue, $headersValue,);',
);
} else if (headersType != null) {
b.returns = refer('Future<$headersType>');
code.write('return $headersValue;');
} else if (dataType != null) {
b.returns = refer('Future<$dataType>');
code.write('return $dataValue;');
} else {
b.returns = refer('Future<void>');
code.write('return;');
}
yield Method(
(final b) {
b
..name = '${name}Raw'
..docs.addAll(operation.formattedDescription(name, isRawRequest: true))
..annotations.add(refer('experimental'));
code.write('}');
if (annotations != null) {
b.annotations.add(annotations);
}
code.write(
'throw await ${state.classPrefix}ApiException.fromResponse(_response); // coverage:ignore-line\n',
);
b.body = Code(code.toString());
b
..optionalParameters.addAll(operationParameters.build())
..returns = refer('DynamiteRawResponse<$returnDataType, $returnHeadersType>')
..body = Code(code.toString());
},
);
}
}
}
Map<String, PathItem> generatePaths(final OpenAPI spec, final String? tag) {
final paths = <String, PathItem>{};
Iterable<String> buildPatternCheck(
final TypeResult result,
final openapi.Parameter parameter,
) sync* {
final value = toDartName(parameter.name);
final name = "'$value'";
final schema = parameter.schema;
if (result.name == 'String' && schema != null) {
if (schema.pattern != null) {
yield "checkPattern($value, RegExp(r'${schema.pattern!}'), $name); // coverage:ignore-line";
}
if (schema.minLength != null) {
yield 'checkMinLength($value, ${schema.minLength}, $name); // coverage:ignore-line';
}
if (schema.maxLength != null) {
yield 'checkMaxLength($value, ${schema.maxLength}, $name); // coverage:ignore-line';
}
}
}
Iterable<String> buildAuthCheck(
final Map<openapi.Response, List<int>> responses,
final MapEntry<String, openapi.PathItem> pathEntry,
final openapi.Operation operation,
final openapi.OpenAPI spec,
final String client,
) sync* {
final security = operation.security ?? spec.security ?? BuiltList();
final securityRequirements = security.where((final requirement) => requirement.isNotEmpty);
final isOptionalSecurity = securityRequirements.length != security.length;
if (securityRequirements.isEmpty) {
return;
}
yield '''
// coverage:ignore-start
final authentication = $client.authentications.firstWhereOrNull(
(final auth) => switch (auth) {
''';
yield* securityRequirements.map((final requirement) {
final securityScheme = spec.components!.securitySchemes![requirement.keys.single]!;
final dynamiteAuth = toDartName(
'Dynamite-${securityScheme.type}-${securityScheme.scheme}-Authentication',
uppercaseFirstCharacter: true,
);
return '$dynamiteAuth()';
}).intersperse(' || ');
yield '''
=> true,
_ => false,
},
);
''';
yield '''
if(authentication != null) {
_headers.addAll(
authentication.headers,
);
}
''';
if (!isOptionalSecurity) {
yield '''
else {
throw Exception('Missing authentication for ${securityRequirements.map((final r) => r.keys.single).join(' or ')}');
}
''';
}
yield '// coverage:ignore-end';
}
Map<String, openapi.PathItem> generatePaths(final openapi.OpenAPI spec, final String? tag) {
final paths = <String, openapi.PathItem>{};
if (spec.paths != null) {
for (final path in spec.paths!.entries) {
@ -637,21 +514,21 @@ Map<String, PathItem> generatePaths(final OpenAPI spec, final String? tag) {
paths[path.key] ??= path.value;
paths[path.key]!.rebuild((final b) {
switch (operationEntry.key) {
case PathItemOperation.get:
case openapi.PathItemOperation.get:
b.get.replace(operation);
case PathItemOperation.put:
case openapi.PathItemOperation.put:
b.put.replace(operation);
case PathItemOperation.post:
case openapi.PathItemOperation.post:
b.post.replace(operation);
case PathItemOperation.delete:
case openapi.PathItemOperation.delete:
b.delete.replace(operation);
case PathItemOperation.options:
case openapi.PathItemOperation.options:
b.options.replace(operation);
case PathItemOperation.head:
case openapi.PathItemOperation.head:
b.head.replace(operation);
case PathItemOperation.patch:
case openapi.PathItemOperation.patch:
b.patch.replace(operation);
case PathItemOperation.trace:
case openapi.PathItemOperation.trace:
b.trace.replace(operation);
}
});
@ -663,7 +540,7 @@ Map<String, PathItem> generatePaths(final OpenAPI spec, final String? tag) {
return paths;
}
List<String> generateTags(final OpenAPI spec) {
List<String> generateTags(final openapi.OpenAPI spec) {
final tags = <String>[];
if (spec.paths != null) {

16
packages/dynamite/dynamite/lib/src/builder/header_serializer.dart

@ -3,11 +3,15 @@ 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/open_api.dart';
import 'package:dynamite/src/models/schema.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/openapi.dart' as openapi;
import 'package:dynamite/src/models/type_result.dart';
Spec buildHeaderSerializer(final State state, final String identifier, final OpenAPI spec, final Schema schema) =>
Spec buildHeaderSerializer(
final State state,
final String identifier,
final openapi.OpenAPI spec,
final openapi.Schema schema,
) =>
Class(
(final b) => b
..name = '_\$${state.classPrefix}${identifier}Serializer'
@ -107,8 +111,8 @@ return result.build();
Iterable<String> deserializeProperty(
final State state,
final String identifier,
final OpenAPI spec,
final Schema schema,
final openapi.OpenAPI spec,
final openapi.Schema schema,
) sync* {
for (final property in schema.properties!.entries) {
final propertyName = property.key;

5
packages/dynamite/dynamite/lib/src/builder/imports.dart

@ -4,7 +4,9 @@ import 'package:path/path.dart' as p;
List<Spec> generateImports(final AssetId outputId) => [
const Code('// ignore_for_file: camel_case_types'),
const Code('// ignore_for_file: discarded_futures'),
const Code('// ignore_for_file: public_member_api_docs'),
const Code('// ignore_for_file: unreachable_switch_case'),
Directive.import('dart:convert'),
Directive.import('dart:typed_data'),
const Code(''),
@ -13,8 +15,11 @@ List<Spec> generateImports(final AssetId outputId) => [
Directive.import('package:built_value/json_object.dart'),
Directive.import('package:built_value/serializer.dart'),
Directive.import('package:built_value/standard_json_plugin.dart'),
Directive.import('package:collection/collection.dart'),
Directive.import('package:dynamite_runtime/content_string.dart'),
Directive.import('package:dynamite_runtime/http_client.dart'),
Directive.import('package:dynamite_runtime/utils.dart'),
Directive.import('package:meta/meta.dart'),
Directive.import('package:universal_io/io.dart'),
const Code(''),
Directive.export('package:dynamite_runtime/http_client.dart'),

13
packages/dynamite/dynamite/lib/src/builder/ofs_builder.dart

@ -5,15 +5,14 @@ import 'package:dynamite/src/builder/resolve_type.dart';
import 'package:dynamite/src/builder/state.dart';
import 'package:dynamite/src/helpers/built_value.dart';
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/models/open_api.dart';
import 'package:dynamite/src/models/schema.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/openapi.dart' as openapi;
import 'package:dynamite/src/models/type_result.dart';
TypeResult resolveAllOf(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
final String identifier,
final Schema schema, {
final openapi.Schema schema, {
final bool nullable = false,
}) {
final result = TypeResultObject(
@ -65,10 +64,10 @@ TypeResult resolveAllOf(
}
TypeResult resolveOfs(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
final String identifier,
final Schema schema, {
final openapi.Schema schema, {
final bool nullable = false,
}) {
if (schema.allOf != null) {

9
packages/dynamite/dynamite/lib/src/builder/resolve_enum.dart

@ -4,15 +4,14 @@ import 'package:dynamite/src/builder/state.dart';
import 'package:dynamite/src/helpers/built_value.dart';
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/helpers/type_result.dart';
import 'package:dynamite/src/models/open_api.dart';
import 'package:dynamite/src/models/schema.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/openapi.dart' as openapi;
import 'package:dynamite/src/models/type_result.dart';
TypeResult resolveEnum(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
final String identifier,
final Schema schema,
final openapi.Schema schema,
final TypeResult subResult, {
final bool nullable = false,
}) {

9
packages/dynamite/dynamite/lib/src/builder/resolve_interface.dart

@ -4,15 +4,14 @@ import 'package:dynamite/src/builder/state.dart';
import 'package:dynamite/src/helpers/built_value.dart';
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/helpers/dynamite.dart';
import 'package:dynamite/src/models/open_api.dart';
import 'package:dynamite/src/models/schema.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/openapi.dart' as openapi;
import 'package:dynamite/src/models/type_result.dart';
TypeResultObject resolveInterface(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
final String identifier,
final Schema schema,
final openapi.Schema schema,
) {
final result = TypeResultObject(
'${state.classPrefix}$identifier',

129
packages/dynamite/dynamite/lib/src/builder/resolve_mime_type.dart

@ -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"');
}
}
}
}

9
packages/dynamite/dynamite/lib/src/builder/resolve_object.dart

@ -5,15 +5,14 @@ import 'package:dynamite/src/builder/state.dart';
import 'package:dynamite/src/helpers/built_value.dart';
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/helpers/type_result.dart';
import 'package:dynamite/src/models/open_api.dart';
import 'package:dynamite/src/models/schema.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/openapi.dart' as openapi;
import 'package:dynamite/src/models/type_result.dart';
TypeResultObject resolveObject(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
final String identifier,
final Schema schema, {
final openapi.Schema schema, {
final bool nullable = false,
final bool isHeader = false,
}) {

9
packages/dynamite/dynamite/lib/src/builder/resolve_type.dart

@ -2,15 +2,14 @@ import 'package:dynamite/src/builder/ofs_builder.dart';
import 'package:dynamite/src/builder/resolve_enum.dart';
import 'package:dynamite/src/builder/resolve_object.dart';
import 'package:dynamite/src/builder/state.dart';
import 'package:dynamite/src/models/open_api.dart';
import 'package:dynamite/src/models/schema.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/openapi.dart' as openapi;
import 'package:dynamite/src/models/type_result.dart';
TypeResult resolveType(
final OpenAPI spec,
final openapi.OpenAPI spec,
final State state,
final String identifier,
final Schema schema, {
final openapi.Schema schema, {
final bool ignoreEnum = false,
final bool nullable = false,
}) {

10
packages/dynamite/dynamite/lib/src/builder/serializer.dart

@ -13,19 +13,9 @@ List<Spec> buildSerializer(final State state) {
.map(Code.new),
const Code(').build();'),
const Code(''),
Code('Serializers get ${state.variablePrefix}Serializers => _serializers;'),
const Code(''),
const Code(
'final Serializers _jsonSerializers = (_serializers.toBuilder()..addPlugin(StandardJsonPlugin())..addPlugin(const ContentStringPlugin())).build();',
),
const Code(''),
Code(
'T deserialize${state.classPrefix}<T>(final Object data) => _serializers.deserialize(data, specifiedType: FullType(T))! as T;',
),
const Code(''),
Code(
'Object? serialize${state.classPrefix}<T>(final T data) => _serializers.serialize(data, specifiedType: FullType(T));',
),
const Code('// coverage:ignore-end'),
];
}

2
packages/dynamite/dynamite/lib/src/builder/state.dart

@ -1,6 +1,6 @@
import 'package:code_builder/code_builder.dart';
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/type_result.dart';
class State {
State(final String prefix)

2
packages/dynamite/dynamite/lib/src/helpers/built_value.dart

@ -1,7 +1,7 @@
import 'package:built_collection/built_collection.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/type_result.dart';
const interfaceSuffix = 'Interface';

8
packages/dynamite/dynamite/lib/src/helpers/docs.dart

@ -3,7 +3,13 @@ const docsSeparator = '///';
Iterable<String> descriptionToDocs(final String? description) sync* {
if (description != null && description.isNotEmpty) {
for (final line in description.split('\n')) {
yield '$docsSeparator $line';
final buffer = StringBuffer('$docsSeparator ')..write(line);
if (!line.endsWith('.')) {
buffer.write('.');
}
yield buffer.toString();
}
}
}

7
packages/dynamite/dynamite/lib/src/helpers/dynamite.dart

@ -1,8 +1,7 @@
// ignore_for_file: avoid_positional_boolean_parameters
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/models/parameter.dart';
import 'package:dynamite/src/models/schema.dart';
import 'package:dynamite/src/models/openapi.dart' as openapi;
String filterMethodName(final String operationId, final String tag) {
final expandedTag = tag.split('/').toList();
@ -20,7 +19,7 @@ String clientName(final String tag) => '${toDartName(tag, uppercaseFirstCharacte
bool isDartParameterNullable(
final bool? required,
final Schema? schema,
final openapi.Schema? schema,
) =>
(!(required ?? false) && schema?.$default == null) || (schema?.nullable ?? false);
@ -30,7 +29,7 @@ bool isRequired(
) =>
(required ?? false) && default_ == null;
int sortRequiredParameters(final Parameter a, final Parameter b) {
int sortRequiredParameters(final openapi.Parameter a, final openapi.Parameter b) {
if (a.isDartRequired != b.isDartRequired) {
if (a.isDartRequired && !b.isDartRequired) {
return -1;

2
packages/dynamite/dynamite/lib/src/helpers/type_result.dart

@ -1,5 +1,5 @@
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/type_result.dart';
String valueToEscapedValue(final TypeResult result, final String value) {
if (result is TypeResultBase && result.name == 'String') {

70
packages/dynamite/dynamite/lib/src/models/openapi.dart

@ -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();

2
packages/dynamite/dynamite/lib/src/models/serializers.g.dart → packages/dynamite/dynamite/lib/src/models/openapi.g.dart

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'serializers.dart';
part of 'openapi.dart';
// **************************************************************************
// BuiltValueGenerator

4
packages/dynamite/dynamite/lib/src/models/components.dart → packages/dynamite/dynamite/lib/src/models/openapi/components.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/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';

0
packages/dynamite/dynamite/lib/src/models/components.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/components.g.dart

0
packages/dynamite/dynamite/lib/src/models/discriminator.dart → packages/dynamite/dynamite/lib/src/models/openapi/discriminator.dart

0
packages/dynamite/dynamite/lib/src/models/discriminator.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/discriminator.g.dart

2
packages/dynamite/dynamite/lib/src/models/header.dart → packages/dynamite/dynamite/lib/src/models/openapi/header.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';

0
packages/dynamite/dynamite/lib/src/models/header.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/header.g.dart

2
packages/dynamite/dynamite/lib/src/models/info.dart → packages/dynamite/dynamite/lib/src/models/openapi/info.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';

0
packages/dynamite/dynamite/lib/src/models/info.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/info.g.dart

0
packages/dynamite/dynamite/lib/src/models/license.dart → packages/dynamite/dynamite/lib/src/models/openapi/license.dart

0
packages/dynamite/dynamite/lib/src/models/license.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/license.g.dart

2
packages/dynamite/dynamite/lib/src/models/media_type.dart → packages/dynamite/dynamite/lib/src/models/openapi/media_type.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';

0
packages/dynamite/dynamite/lib/src/models/media_type.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/media_type.g.dart

10
packages/dynamite/dynamite/lib/src/models/open_api.dart → packages/dynamite/dynamite/lib/src/models/openapi/open_api.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
packages/dynamite/dynamite/lib/src/models/open_api.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/open_api.g.dart

100
packages/dynamite/dynamite/lib/src/models/openapi/operation.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.';
}
}
}

0
packages/dynamite/dynamite/lib/src/models/operation.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/operation.g.dart

18
packages/dynamite/dynamite/lib/src/models/parameter.dart → packages/dynamite/dynamite/lib/src/models/openapi/parameter.dart

@ -1,7 +1,9 @@
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/helpers/docs.dart';
import 'package:dynamite/src/helpers/dynamite.dart';
import 'package:dynamite/src/models/schema.dart';
import 'package:dynamite/src/models/openapi/schema.dart';
part 'parameter.g.dart';
@ -25,4 +27,18 @@ abstract class Parameter implements Built<Parameter, ParameterBuilder> {
Schema? get schema;
bool get isDartRequired => isRequired(required, schema?.$default);
String get formattedDescription {
final name = toDartName(this.name);
final buffer = StringBuffer()
..write('$docsSeparator * ')
..write('[$name]');
if (description != null) {
buffer.write(' $description');
}
return buffer.toString();
}
}

0
packages/dynamite/dynamite/lib/src/models/parameter.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/parameter.g.dart

4
packages/dynamite/dynamite/lib/src/models/path_item.dart → packages/dynamite/dynamite/lib/src/models/openapi/path_item.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/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';

0
packages/dynamite/dynamite/lib/src/models/path_item.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/path_item.g.dart

2
packages/dynamite/dynamite/lib/src/models/request_body.dart → packages/dynamite/dynamite/lib/src/models/openapi/request_body.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';

0
packages/dynamite/dynamite/lib/src/models/request_body.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/request_body.g.dart

4
packages/dynamite/dynamite/lib/src/models/response.dart → packages/dynamite/dynamite/lib/src/models/openapi/response.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';

0
packages/dynamite/dynamite/lib/src/models/response.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/response.g.dart

2
packages/dynamite/dynamite/lib/src/models/schema.dart → packages/dynamite/dynamite/lib/src/models/openapi/schema.dart

@ -3,7 +3,7 @@ import 'package:built_value/built_value.dart';
import 'package:built_value/json_object.dart';
import 'package:built_value/serializer.dart';
import 'package:dynamite/src/helpers/docs.dart';
import 'package:dynamite/src/models/discriminator.dart';
import 'package:dynamite/src/models/openapi/discriminator.dart';
part 'schema.g.dart';

0
packages/dynamite/dynamite/lib/src/models/schema.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/schema.g.dart

0
packages/dynamite/dynamite/lib/src/models/security_scheme.dart → packages/dynamite/dynamite/lib/src/models/openapi/security_scheme.dart

0
packages/dynamite/dynamite/lib/src/models/security_scheme.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/security_scheme.g.dart

2
packages/dynamite/dynamite/lib/src/models/server.dart → packages/dynamite/dynamite/lib/src/models/openapi/server.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';

0
packages/dynamite/dynamite/lib/src/models/server.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/server.g.dart

0
packages/dynamite/dynamite/lib/src/models/server_variable.dart → packages/dynamite/dynamite/lib/src/models/openapi/server_variable.dart

0
packages/dynamite/dynamite/lib/src/models/server_variable.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/server_variable.g.dart

0
packages/dynamite/dynamite/lib/src/models/tag.dart → packages/dynamite/dynamite/lib/src/models/openapi/tag.dart

0
packages/dynamite/dynamite/lib/src/models/tag.g.dart → packages/dynamite/dynamite/lib/src/models/openapi/tag.g.dart

47
packages/dynamite/dynamite/lib/src/models/operation.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);
}
}

52
packages/dynamite/dynamite/lib/src/models/serializers.dart

@ -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();

1
packages/dynamite/dynamite/lib/src/models/type_result.dart

@ -0,0 +1 @@
export 'type_result/type_result.dart';

50
packages/dynamite/dynamite/lib/src/models/type_result/base.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);
}
}

0
packages/dynamite/dynamite/lib/src/type_result/enum.dart → packages/dynamite/dynamite/lib/src/models/type_result/enum.dart

0
packages/dynamite/dynamite/lib/src/type_result/list.dart → packages/dynamite/dynamite/lib/src/models/type_result/list.dart

0
packages/dynamite/dynamite/lib/src/type_result/map.dart → packages/dynamite/dynamite/lib/src/models/type_result/map.dart

0
packages/dynamite/dynamite/lib/src/type_result/object.dart → packages/dynamite/dynamite/lib/src/models/type_result/object.dart

2
packages/dynamite/dynamite/lib/src/type_result/type_result.dart → packages/dynamite/dynamite/lib/src/models/type_result/type_result.dart

@ -8,7 +8,7 @@ part 'map.dart';
part 'object.dart';
@immutable
abstract class TypeResult {
sealed class TypeResult {
TypeResult(
this.className, {
this.generics = const [],

11
packages/dynamite/dynamite/lib/src/openapi_builder.dart

@ -11,9 +11,8 @@ import 'package:dynamite/src/builder/resolve_type.dart';
import 'package:dynamite/src/builder/serializer.dart';
import 'package:dynamite/src/builder/state.dart';
import 'package:dynamite/src/helpers/dart_helpers.dart';
import 'package:dynamite/src/models/open_api.dart';
import 'package:dynamite/src/models/serializers.dart';
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/openapi.dart' as openapi;
import 'package:dynamite/src/models/type_result.dart';
class OpenAPIBuilder implements Builder {
@override
@ -34,13 +33,13 @@ class OpenAPIBuilder implements Builder {
);
final spec = switch (inputId.extension) {
'.json' => serializers.deserializeWith(
OpenAPI.serializer,
'.json' => openapi.serializers.deserializeWith(
openapi.OpenAPI.serializer,
json.decode(await buildStep.readAsString(inputId)),
)!,
'.yaml' => checkedYamlDecode(
await buildStep.readAsString(inputId),
(final m) => serializers.deserializeWith(OpenAPI.serializer, m)!,
(final m) => openapi.serializers.deserializeWith(openapi.OpenAPI.serializer, m)!,
),
_ => throw StateError('Openapi specs can only be yaml or json.'),
};

58
packages/dynamite/dynamite/lib/src/type_result/base.dart

@ -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);
}
}

2
packages/dynamite/dynamite/test/type_result_test.dart

@ -1,4 +1,4 @@
import 'package:dynamite/src/type_result/type_result.dart';
import 'package:dynamite/src/models/type_result.dart';
import 'package:test/test.dart';
void main() {

4
packages/dynamite/dynamite_runtime/analysis_options.yaml

@ -1,9 +1,5 @@
include: package:neon_lints/dart.yaml
linter:
rules:
public_member_api_docs: false
analyzer:
exclude:
- '**.g.dart'

4
packages/dynamite/dynamite_runtime/lib/http_client.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';

445
packages/dynamite/dynamite_runtime/lib/src/dynamite_client.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);
}
}
}

165
packages/dynamite/dynamite_runtime/lib/src/http_client.dart

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

43
packages/dynamite/dynamite_runtime/lib/src/http_extensions.dart

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

26
packages/dynamite/dynamite_runtime/lib/src/string_checker.dart

@ -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
packages/dynamite/dynamite_runtime/lib/utils.dart

@ -1 +1,2 @@
export 'src/string_checker.dart';
export 'src/uri.dart';

1
packages/dynamite/dynamite_runtime/pubspec.yaml

@ -8,6 +8,7 @@ environment:
dependencies:
built_value: ^8.6.3
cookie_jar: ^4.0.8
meta: ^1.9.1
universal_io: ^2.2.2
dev_dependencies:

18
packages/neon/neon/lib/src/bloc/result.dart

@ -42,11 +42,19 @@ class Result<T> {
isCached: isCached,
);
Result<T> asLoading() => Result(
data,
error,
isLoading: true,
isCached: isCached,
Result<T> asLoading() => copyWith(isLoading: true);
Result<T> copyWith({
final T? data,
final Object? error,
final bool? isLoading,
final bool? isCached,
}) =>
Result(
data ?? this.data,
error ?? this.error,
isLoading: isLoading ?? this.isLoading,
isCached: isCached ?? this.isCached,
);
bool get hasError => error != null;

6
packages/neon/neon/lib/src/blocs/apps.dart

@ -199,12 +199,12 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates
@override
Future<void> refresh() async {
await RequestManager.instance
.wrapNextcloud<List<CoreNavigationEntry>, CoreNavigationGetAppsNavigationResponseApplicationJson>(
.wrapNextcloud<List<CoreNavigationEntry>, CoreNavigationGetAppsNavigationResponseApplicationJson, void>(
_account.id,
'apps-apps',
apps,
() async => _account.client.core.navigation.getAppsNavigation(),
(final response) => response.ocs.data.toList(),
_account.client.core.navigation.getAppsNavigationRaw(),
(final response) => response.body.ocs.data.toList(),
);
}

6
packages/neon/neon/lib/src/blocs/capabilities.dart

@ -39,12 +39,12 @@ class CapabilitiesBloc extends InteractiveBloc implements CapabilitiesBlocEvents
@override
Future<void> refresh() async {
await RequestManager.instance.wrapNextcloud<CoreOcsGetCapabilitiesResponseApplicationJson_Ocs_Data,
CoreOcsGetCapabilitiesResponseApplicationJson>(
CoreOcsGetCapabilitiesResponseApplicationJson, void>(
_account.id,
'capabilities',
capabilities,
() async => _account.client.core.ocs.getCapabilities(),
(final response) => response.ocs.data,
_account.client.core.ocs.getCapabilitiesRaw(),
(final response) => response.body.ocs.data,
);
}
}

2
packages/neon/neon/lib/src/blocs/login_check_account.dart

@ -59,7 +59,7 @@ class LoginCheckAccountBloc extends InteractiveBloc
final account = Account(
serverURL: serverURL,
username: response.ocs.data.id,
username: response.body.ocs.data.id,
password: password,
userAgent: neonUserAgent,
);

2
packages/neon/neon/lib/src/blocs/login_check_server_status.dart

@ -47,7 +47,7 @@ class LoginCheckServerStatusBloc extends InteractiveBloc
);
final status = await client.core.getStatus();
state.add(Result.success(status));
state.add(Result.success(status.body));
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());

6
packages/neon/neon/lib/src/blocs/login_flow.dart

@ -54,14 +54,14 @@ class LoginFlowBloc extends InteractiveBloc implements LoginFlowBlocEvents, Logi
init.add(Result.loading());
final initResponse = await _client.core.clientFlowLoginV2.init();
init.add(Result.success(initResponse));
init.add(Result.success(initResponse.body));
_cancelPollTimer();
_pollTimer = Timer.periodic(const Duration(seconds: 1), (final _) async {
try {
final resultResponse = await _client.core.clientFlowLoginV2.poll(token: initResponse.poll.token);
final resultResponse = await _client.core.clientFlowLoginV2.poll(token: initResponse.body.poll.token);
_cancelPollTimer();
_resultController.add(resultResponse);
_resultController.add(resultResponse.body);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());

3
packages/neon/neon/lib/src/blocs/next_push.dart

@ -46,7 +46,8 @@ class NextPushBloc extends Bloc implements NextPushBlocEvents, NextPushBlocState
for (final account in _accountsBloc.accounts.value) {
if (!_supported.containsKey(account)) {
try {
_supported[account] = (await account.client.uppush.check()).success;
final response = await account.client.uppush.check();
_supported[account] = response.body.success;
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());

2
packages/neon/neon/lib/src/blocs/push_notifications.dart

@ -96,7 +96,7 @@ class PushNotificationsBloc extends Bloc implements PushNotificationsBlocEvents,
await _storage.setString(_keyLastEndpoint(account), endpoint);
debugPrint(
'Account $instance registered for push notifications ${json.encode(subscription.ocs.data.toJson())}',
'Account $instance registered for push notifications ${json.encode(subscription.body.ocs.data.toJson())}',
);
},
onMessage: PushUtils.onMessage,

5
packages/neon/neon/lib/src/blocs/unified_search.dart

@ -88,7 +88,8 @@ class UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBlocEven
try {
results.add(results.value.asLoading());
final providers = (await _account.client.core.unifiedSearch.getProviders()).ocs.data;
final response = await _account.client.core.unifiedSearch.getProviders();
final providers = response.body.ocs.data;
results.add(
Result.success(Map.fromEntries(_getLoadingProviders(providers))),
);
@ -117,7 +118,7 @@ class UnifiedSearchBloc extends InteractiveBloc implements UnifiedSearchBlocEven
providerId: provider.id,
term: _term,
);
_updateResults(provider, Result.success(response.ocs.data));
_updateResults(provider, Result.success(response.body.ocs.data));
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());

6
packages/neon/neon/lib/src/blocs/user_details.dart

@ -39,12 +39,12 @@ class UserDetailsBloc extends InteractiveBloc implements UserDetailsBlocEvents,
@override
Future<void> refresh() async {
await RequestManager.instance
.wrapNextcloud<ProvisioningApiUserDetails, ProvisioningApiUsersGetCurrentUserResponseApplicationJson>(
.wrapNextcloud<ProvisioningApiUserDetails, ProvisioningApiUsersGetCurrentUserResponseApplicationJson, void>(
_account.id,
'user-details',
userDetails,
() async => _account.client.provisioningApi.users.getCurrentUser(),
(final response) => response.ocs.data,
_account.client.provisioningApi.users.getCurrentUserRaw(),
(final response) => response.body.ocs.data,
);
}
}

4
packages/neon/neon/lib/src/blocs/user_statuses.dart

@ -73,7 +73,7 @@ class UserStatusesBloc extends InteractiveBloc implements UserStatusesBlocEvents
final response = await _account.client.userStatus.heartbeat.heartbeat(
status: isAway ? 'away' : 'online',
);
data = response.ocs.data;
data = response.body.ocs.data;
} catch (e) {
// 204 is returned if the heartbeat failed because the current status is different. Ignore this and fetch the normal status
if (e is! DynamiteApiException || e.statusCode != 204) {
@ -84,7 +84,7 @@ class UserStatusesBloc extends InteractiveBloc implements UserStatusesBlocEvents
if (data == null) {
final response = await _account.client.userStatus.statuses.find(userId: username);
data = response.ocs.data;
data = response.body.ocs.data;
}
_updateStatus(username, Result.success(data));

2
packages/neon/neon/lib/src/pages/home.dart

@ -82,7 +82,7 @@ class _HomePageState extends State<HomePage> {
Future<void> _checkMaintenanceMode() async {
try {
final status = await _account.client.core.getStatus();
if (status.maintenance && mounted) {
if (status.body.maintenance && mounted) {
await _showProblem(
AppLocalizations.of(context).errorServerInMaintenanceMode,
);

7
packages/neon/neon/lib/src/utils/push_utils.dart

@ -102,10 +102,9 @@ class PushUtils {
try {
account = accounts.tryFind(instance);
if (account != null) {
notification =
(await account.client.notifications.endpoint.getNotification(id: pushNotification.subject.nid!))
.ocs
.data;
final response =
await account.client.notifications.endpoint.getNotification(id: pushNotification.subject.nid!);
notification = response.body.ocs.data;
if (notification.icon?.endsWith('.svg') ?? false) {
// Only SVG icons are supported right now (should be most of them)

172
packages/neon/neon/lib/src/utils/request_manager.dart

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
@ -13,7 +14,10 @@ import 'package:xml/xml.dart' as xml;
typedef UnwrapCallback<T, R> = T Function(R);
typedef SerializeCallback<T> = String Function(T);
typedef DeserializeCallback<T> = T Function(String);
typedef NextcloudApiCallback<T> = Future<T> Function();
typedef NextcloudApiCallback<T> = AsyncValueGetter<T>;
const maxRetries = 3;
const defaultTimeout = Duration(seconds: 30);
class RequestManager {
RequestManager();
@ -34,26 +38,32 @@ class RequestManager {
Cache? _cache;
Future<void> wrapNextcloud<T, R>(
Future<void> wrapNextcloud<T, B, H>(
final String clientID,
final String k,
final BehaviorSubject<Result<T>> subject,
final NextcloudApiCallback<R> call,
final UnwrapCallback<T, R> unwrap, {
final DynamiteRawResponse<B, H> rawResponse,
final UnwrapCallback<T, DynamiteResponse<B, H>> unwrap, {
final bool disableTimeout = false,
final bool emitEmptyCache = false,
}) async =>
_wrap<T, R>(
_wrap<T, DynamiteRawResponse<B, H>>(
clientID,
k,
subject,
call,
unwrap,
(final data) => json.encode(serializeNextcloud<R>(data)),
(final data) => deserializeNextcloud<R>(json.decode(data) as Object),
() async {
await rawResponse.future;
return rawResponse;
},
(final rawResponse) => unwrap(rawResponse.response),
(final data) => json.encode(data),
(final data) => DynamiteRawResponse<B, H>.fromJson(
json.decode(data) as Map<String, Object?>,
serializers: rawResponse.serializers,
bodyType: rawResponse.bodyType,
headersType: rawResponse.headersType,
),
disableTimeout,
emitEmptyCache,
0,
);
Future<void> wrapWebDav<T>(
@ -75,7 +85,6 @@ class RequestManager {
(final data) => WebDavMultistatus.fromXmlElement(xml.XmlDocument.parse(data).rootElement),
disableTimeout,
emitEmptyCache,
0,
);
Future<void> _wrap<T, R>(
@ -85,44 +94,44 @@ class RequestManager {
final NextcloudApiCallback<R> call,
final UnwrapCallback<T, R> unwrap,
final SerializeCallback<R> serialize,
final DeserializeCallback<R> deserialize,
final bool disableTimeout,
final bool emitEmptyCache,
final int retries,
) async {
if (subject.valueOrNull?.hasData ?? false) {
subject.add(
Result(
subject.value.data,
null,
isLoading: true,
isCached: true,
),
);
} else {
subject.add(Result.loading());
}
final DeserializeCallback<R> deserialize, [
final bool disableTimeout = false,
final bool emitEmptyCache = false,
final int retries = 0,
]) async {
// emit loading state with the current value if present
final value = subject.valueOrNull?.copyWith(isLoading: true) ?? Result.loading();
subject.add(value);
final key = '$clientID-$k';
await _emitCached(
key,
subject,
unwrap,
deserialize,
emitEmptyCache,
true,
null,
unawaited(
_emitCached(
key,
subject,
unwrap,
deserialize,
emitEmptyCache,
),
);
try {
final response = await (disableTimeout ? call() : timeout(call));
await _cache?.set(key, await compute(serialize, response));
final response = await timeout(call, disableTimeout: disableTimeout);
subject.add(Result.success(unwrap(response)));
final serialized = serialize(response);
await _cache?.set(key, serialized);
} on TimeoutException catch (e, s) {
debugPrint('Request timed out ...');
debugPrint(e.toString());
debugPrintStack(stackTrace: s, maxFrames: 5);
_emitError<T>(e, subject);
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
if (e is DynamiteApiException && e.statusCode >= 500 && retries < 3) {
debugPrintStack(stackTrace: s, maxFrames: 5);
if (e is DynamiteApiException && e.statusCode >= 500 && retries < maxRetries) {
debugPrint('Retrying...');
await _wrap(
clientID,
@ -136,58 +145,79 @@ class RequestManager {
emitEmptyCache,
retries + 1,
);
return;
}
if (!(await _emitCached(
key,
subject,
unwrap,
deserialize,
emitEmptyCache,
false,
e,
))) {
subject.add(Result.error(e));
} else {
_emitError<T>(e, subject);
}
}
}
/// Re emits the current result with the given [error].
///
/// Defaults to a [Result.error] if none has been emitted yet.
void _emitError<T>(
final Object error,
final BehaviorSubject<Result<T>> subject,
) {
final value = subject.valueOrNull?.copyWith(error: error, isLoading: false) ?? Result.error(error);
subject.add(value);
}
Future<bool> _emitCached<T, R>(
final String key,
final BehaviorSubject<Result<T>> subject,
final UnwrapCallback<T, R> unwrap,
final DeserializeCallback<R> deserialize,
final bool emitEmptyCache,
final bool loading,
final Object? error,
) async {
T? cached;
try {
if (_cache != null && await _cache!.has(key)) {
cached = unwrap(await compute(deserialize, (await _cache!.get(key))!));
if (_cache != null && await _cache!.has(key)) {
try {
final cacheValue = await _cache!.get(key);
final cached = unwrap(deserialize(cacheValue!));
// If the network fetch is faster than fetching the cached value the
// subject can be closed before emitting.
if (subject.value.hasUncachedData) {
return true;
}
subject.add(
subject.value.copyWith(
data: cached,
isCached: true,
),
);
return true;
} catch (e, s) {
debugPrint(e.toString());
debugPrintStack(stackTrace: s, maxFrames: 5);
}
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
}
if (cached != null || emitEmptyCache) {
if (emitEmptyCache && !subject.value.hasUncachedData) {
subject.add(
Result(
cached,
error,
isLoading: loading,
subject.value.copyWith(
isCached: true,
),
);
return true;
}
return false;
}
Future<T> timeout<T>(
final NextcloudApiCallback<T> call,
) =>
call().timeout(const Duration(seconds: 30));
final NextcloudApiCallback<T> call, {
final bool disableTimeout = false,
final Duration timeout = const Duration(seconds: 30),
}) {
if (disableTimeout) {
return call();
}
return call().timeout(timeout);
}
}
@internal

7
packages/neon/neon/lib/src/widgets/cached_image.dart

@ -17,7 +17,7 @@ typedef ImageDownloader = FutureOr<Uint8List> Function();
typedef CacheWriter = Future<void> Function(CacheManager cacheManager, Uint8List image);
typedef ErrorWidgetBuilder = Widget? Function(BuildContext, dynamic);
typedef ApiImageDownloader = FutureOr<Uint8List> Function(NextcloudClient client);
typedef ApiImageDownloader = FutureOr<DynamiteResponse<Uint8List, dynamic>> Function(NextcloudClient client);
class NeonCachedImage extends StatefulWidget {
const NeonCachedImage({
@ -223,7 +223,10 @@ class NeonApiImage extends StatelessWidget {
final account = NeonProvider.of<AccountsBloc>(context).activeAccount.value!;
return NeonCachedImage.custom(
getImage: () async => getImage(account.client),
getImage: () async {
final response = await getImage(account.client);
return response.body;
},
cacheKey: '${account.id}-$cacheKey',
);
}

24
packages/neon/neon/lib/src/widgets/user_avatar.dart

@ -64,19 +64,17 @@ class _UserAvatarState extends State<NeonUserAvatar> {
child: NeonCachedImage.custom(
cacheKey: '${widget.account.id}-avatar-${widget.username}-$brightness$pixelSize',
getImage: () async {
if (brightness == Brightness.dark) {
return (await widget.account.client.core.avatar.getAvatarDark(
userId: widget.username,
size: pixelSize,
))
.data;
} else {
return (await widget.account.client.core.avatar.getAvatar(
userId: widget.username,
size: pixelSize,
))
.data;
}
final response = switch (brightness) {
Brightness.dark => await widget.account.client.core.avatar.getAvatarDark(
userId: widget.username,
size: pixelSize,
),
Brightness.light => await widget.account.client.core.avatar.getAvatar(
userId: widget.username,
size: pixelSize,
),
};
return response.body;
},
),
),

2
packages/neon/neon_files/lib/blocs/browser.dart

@ -47,7 +47,7 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents
account.id,
'files-${path.value.join('/')}',
files,
() async => account.client.webdav.propfind(
() => account.client.webdav.propfind(
Uri(pathSegments: path.value),
prop: WebDavPropWithoutValues.fromBools(
davgetcontenttype: true,

6
packages/neon/neon_news/lib/blocs/articles.dart

@ -115,16 +115,16 @@ class NewsArticlesBloc extends InteractiveBloc implements NewsArticlesBlocEvents
}
}
await RequestManager.instance.wrapNextcloud<List<NewsArticle>, NewsListArticles>(
await RequestManager.instance.wrapNextcloud<List<NewsArticle>, NewsListArticles, void>(
account.id,
'news-articles-${type.index}-$id-$getRead',
articles,
() async => account.client.news.listArticles(
account.client.news.listArticlesRaw(
type: type.index,
id: id ?? 0,
getRead: getRead ?? true ? 1 : 0,
),
(final response) => response.items.toList(),
(final response) => response.body.items.toList(),
);
}

16
packages/neon/neon_news/lib/blocs/news.dart

@ -92,23 +92,23 @@ class NewsBloc extends InteractiveBloc implements NewsBlocEvents, NewsBlocStates
@override
Future<void> refresh() async {
await Future.wait([
RequestManager.instance.wrapNextcloud<List<NewsFolder>, NewsListFolders>(
RequestManager.instance.wrapNextcloud<List<NewsFolder>, NewsListFolders, void>(
account.id,
'news-folders',
folders,
() async => account.client.news.listFolders(),
(final response) => response.folders.toList(),
account.client.news.listFoldersRaw(),
(final response) => response.body.folders.toList(),
),
RequestManager.instance.wrapNextcloud<List<NewsFeed>, NewsListFeeds>(
RequestManager.instance.wrapNextcloud<List<NewsFeed>, NewsListFeeds, void>(
account.id,
'news-feeds',
feeds,
() async => account.client.news.listFeeds(),
account.client.news.listFeedsRaw(),
(final response) {
if (response.newestItemId != null) {
_newestItemId = response.newestItemId!;
if (response.body.newestItemId != null) {
_newestItemId = response.body.newestItemId!;
}
return response.feeds.toList();
return response.body.feeds.toList();
},
),
mainArticlesBloc.reload(),

6
packages/neon/neon_notes/lib/blocs/note.dart

@ -29,11 +29,11 @@ class NotesNoteBloc extends InteractiveBloc implements NotesNoteBlocEvents, Note
}
// ignore: avoid_void_async
void _wrapAction(final Future<NotesNote> Function(String etag) call) async {
void _wrapAction(final Future<DynamiteResponse<NotesNote, dynamic>> Function(String etag) call) async {
await _updateQueue.add(() async {
try {
final data = await call(_etag);
_emitNote(data);
final response = await call(_etag);
_emitNote(response.body);
await _notesBloc.refresh();
} catch (e, s) {
debugPrint(e.toString());

6
packages/neon/neon_notes/lib/blocs/notes.dart

@ -44,12 +44,12 @@ class NotesBloc extends InteractiveBloc implements NotesBlocEvents, NotesBlocSta
@override
Future<void> refresh() async {
await RequestManager.instance.wrapNextcloud<List<NotesNote>, BuiltList<NotesNote>>(
await RequestManager.instance.wrapNextcloud<List<NotesNote>, BuiltList<NotesNote>, void>(
account.id,
'notes-notes',
notes,
() async => account.client.notes.getNotes(),
List<NotesNote>.from,
account.client.notes.getNotesRaw(),
(final response) => List<NotesNote>.from(response.body),
);
}

11
packages/neon/neon_notifications/lib/blocs/notifications.dart

@ -50,16 +50,13 @@ class NotificationsBloc extends InteractiveBloc
@override
Future<void> refresh() async {
await RequestManager.instance
.wrapNextcloud<List<NotificationsNotification>, NotificationsEndpointListNotificationsResponseApplicationJson>(
await RequestManager.instance.wrapNextcloud<List<NotificationsNotification>,
NotificationsEndpointListNotificationsResponseApplicationJson, void>(
_account.id,
'notifications-notifications',
notifications,
() async {
final response = await _account.client.notifications.endpoint.listNotifications();
return response.data;
},
(final response) => response.ocs.data.toList(),
_account.client.notifications.endpoint.listNotificationsRaw(),
(final response) => response.body.ocs.data.toList(),
);
}

45
packages/nextcloud/lib/src/api/comments.openapi.dart

@ -1,53 +1,18 @@
// ignore_for_file: camel_case_types
// ignore_for_file: discarded_futures
// ignore_for_file: public_member_api_docs
// ignore_for_file: unreachable_switch_case
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:dynamite_runtime/content_string.dart';
import 'package:dynamite_runtime/http_client.dart';
import 'package:universal_io/io.dart';
export 'package:dynamite_runtime/http_client.dart';
part 'comments.openapi.g.dart';
class CommentsResponse<T, U> extends DynamiteResponse<T, U> {
CommentsResponse(
super.data,
super.headers,
);
@override
String toString() => 'CommentsResponse(data: $data, headers: $headers)';
}
class CommentsApiException extends DynamiteApiException {
CommentsApiException(
super.statusCode,
super.headers,
super.body,
);
static Future<CommentsApiException> fromResponse(final HttpClientResponse response) async {
String body;
try {
body = await response.body;
} on FormatException {
body = 'binary';
}
return CommentsApiException(
response.statusCode,
response.responseHeaders,
body,
);
}
@override
String toString() => 'CommentsApiException(statusCode: $statusCode, headers: $headers, body: $body)';
}
class CommentsClient extends DynamiteClient {
CommentsClient(
super.baseURL, {
@ -133,14 +98,8 @@ final Serializers _serializers = (Serializers().toBuilder()
..add(CommentsCapabilities_Files.serializer))
.build();
Serializers get commentsSerializers => _serializers;
final Serializers _jsonSerializers = (_serializers.toBuilder()
..addPlugin(StandardJsonPlugin())
..addPlugin(const ContentStringPlugin()))
.build();
T deserializeComments<T>(final Object data) => _serializers.deserialize(data, specifiedType: FullType(T))! as T;
Object? serializeComments<T>(final T data) => _serializers.serialize(data, specifiedType: FullType(T));
// coverage:ignore-end

4410
packages/nextcloud/lib/src/api/core.openapi.dart

File diff suppressed because it is too large Load Diff

333
packages/nextcloud/lib/src/api/dashboard.openapi.dart

@ -1,55 +1,23 @@
// ignore_for_file: camel_case_types
// ignore_for_file: discarded_futures
// ignore_for_file: public_member_api_docs
// ignore_for_file: unreachable_switch_case
import 'dart:typed_data';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:collection/collection.dart';
import 'package:dynamite_runtime/content_string.dart';
import 'package:dynamite_runtime/http_client.dart';
import 'package:meta/meta.dart';
import 'package:universal_io/io.dart';
export 'package:dynamite_runtime/http_client.dart';
part 'dashboard.openapi.g.dart';
class DashboardResponse<T, U> extends DynamiteResponse<T, U> {
DashboardResponse(
super.data,
super.headers,
);
@override
String toString() => 'DashboardResponse(data: $data, headers: $headers)';
}
class DashboardApiException extends DynamiteApiException {
DashboardApiException(
super.statusCode,
super.headers,
super.body,
);
static Future<DashboardApiException> fromResponse(final HttpClientResponse response) async {
String body;
try {
body = await response.body;
} on FormatException {
body = 'binary';
}
return DashboardApiException(
response.statusCode,
response.responseHeaders,
body,
);
}
@override
String toString() => 'DashboardApiException(statusCode: $statusCode, headers: $headers, body: $body)';
}
class DashboardClient extends DynamiteClient {
DashboardClient(
super.baseURL, {
@ -77,69 +45,169 @@ class DashboardDashboardApiClient {
final DashboardClient _rootClient;
/// Get the widgets
Future<DashboardDashboardApiGetWidgetsResponseApplicationJson> getWidgets({final bool oCSAPIRequest = true}) async {
/// Get the widgets.
///
/// Returns a [Future] containing a [DynamiteResponse] with the status code, deserialized body and headers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: Widgets returned
///
/// See:
/// * [getWidgetsRaw] for an experimental operation that returns a [DynamiteRawResponse] that can be serialized.
Future<DynamiteResponse<DashboardDashboardApiGetWidgetsResponseApplicationJson, void>> getWidgets({
final bool oCSAPIRequest = true,
}) async {
final rawResponse = getWidgetsRaw(
oCSAPIRequest: oCSAPIRequest,
);
return rawResponse.future;
}
/// Get the widgets.
///
/// This method and the response it returns is experimental. The API might change without a major version bump.
///
/// Returns a [Future] containing a [DynamiteRawResponse] with the raw [HttpClientResponse] and serialization helpers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: Widgets returned
///
/// See:
/// * [getWidgets] for an operation that returns a [DynamiteResponse] with a stable API.
@experimental
DynamiteRawResponse<DashboardDashboardApiGetWidgetsResponseApplicationJson, void> getWidgetsRaw({
final bool oCSAPIRequest = true,
}) {
const path = '/ocs/v2.php/apps/dashboard/api/v1/widgets';
final queryParameters = <String, dynamic>{};
final headers = <String, String>{
'Accept': 'application/json',
};
Uint8List? body;
// coverage:ignore-start
if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'bearer').isNotEmpty) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'bearer').headers,
);
} else if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'basic').isNotEmpty) {
// coverage:ignore-start
final authentication = _rootClient.authentications.firstWhereOrNull(
(final auth) => switch (auth) {
DynamiteHttpBearerAuthentication() || DynamiteHttpBasicAuthentication() => true,
_ => false,
},
);
if (authentication != null) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'basic').headers,
authentication.headers,
);
} else {
throw Exception('Missing authentication for bearer_auth or basic_auth');
}
// coverage:ignore-end
// coverage:ignore-end
headers['OCS-APIRequest'] = oCSAPIRequest.toString();
final response = await _rootClient.doRequest(
'get',
Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null),
headers,
body,
final uri = Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null);
return DynamiteRawResponse<DashboardDashboardApiGetWidgetsResponseApplicationJson, void>(
response: _rootClient.doRequest(
'get',
uri,
headers,
body,
const {200},
),
bodyType: const FullType(DashboardDashboardApiGetWidgetsResponseApplicationJson),
headersType: null,
serializers: _jsonSerializers,
);
if (response.statusCode == 200) {
return _jsonSerializers.deserialize(
await response.jsonBody,
specifiedType: const FullType(DashboardDashboardApiGetWidgetsResponseApplicationJson),
)! as DashboardDashboardApiGetWidgetsResponseApplicationJson;
}
throw await DashboardApiException.fromResponse(response); // coverage:ignore-line
}
/// Get the items for the widgets
Future<DashboardDashboardApiGetWidgetItemsResponseApplicationJson> getWidgetItems({
/// Get the items for the widgets.
///
/// Returns a [Future] containing a [DynamiteResponse] with the status code, deserialized body and headers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [sinceIds] Array indexed by widget Ids, contains date/id from which we want the new items
/// * [limit] Limit number of result items per widget
/// * [widgets] Limit results to specific widgets
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: Widget items returned
///
/// See:
/// * [getWidgetItemsRaw] for an experimental operation that returns a [DynamiteRawResponse] that can be serialized.
Future<DynamiteResponse<DashboardDashboardApiGetWidgetItemsResponseApplicationJson, void>> getWidgetItems({
final ContentString<BuiltMap<String, String>>? sinceIds,
final int limit = 7,
final List<String> widgets = const <String>[],
final bool oCSAPIRequest = true,
}) async {
final rawResponse = getWidgetItemsRaw(
sinceIds: sinceIds,
limit: limit,
widgets: widgets,
oCSAPIRequest: oCSAPIRequest,
);
return rawResponse.future;
}
/// Get the items for the widgets.
///
/// This method and the response it returns is experimental. The API might change without a major version bump.
///
/// Returns a [Future] containing a [DynamiteRawResponse] with the raw [HttpClientResponse] and serialization helpers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [sinceIds] Array indexed by widget Ids, contains date/id from which we want the new items
/// * [limit] Limit number of result items per widget
/// * [widgets] Limit results to specific widgets
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: Widget items returned
///
/// See:
/// * [getWidgetItems] for an operation that returns a [DynamiteResponse] with a stable API.
@experimental
DynamiteRawResponse<DashboardDashboardApiGetWidgetItemsResponseApplicationJson, void> getWidgetItemsRaw({
final ContentString<BuiltMap<String, String>>? sinceIds,
final int limit = 7,
final List<String> widgets = const <String>[],
final bool oCSAPIRequest = true,
}) {
const path = '/ocs/v2.php/apps/dashboard/api/v1/widget-items';
final queryParameters = <String, dynamic>{};
final headers = <String, String>{
'Accept': 'application/json',
};
Uint8List? body;
// coverage:ignore-start
if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'bearer').isNotEmpty) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'bearer').headers,
);
} else if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'basic').isNotEmpty) {
// coverage:ignore-start
final authentication = _rootClient.authentications.firstWhereOrNull(
(final auth) => switch (auth) {
DynamiteHttpBearerAuthentication() || DynamiteHttpBasicAuthentication() => true,
_ => false,
},
);
if (authentication != null) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'basic').headers,
authentication.headers,
);
} else {
throw Exception('Missing authentication for bearer_auth or basic_auth');
}
// coverage:ignore-end
// coverage:ignore-end
if (sinceIds != null) {
queryParameters['sinceIds'] = _jsonSerializers.serialize(
sinceIds,
@ -155,47 +223,102 @@ class DashboardDashboardApiClient {
queryParameters['widgets[]'] = widgets.map((final e) => e);
}
headers['OCS-APIRequest'] = oCSAPIRequest.toString();
final response = await _rootClient.doRequest(
'get',
Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null),
headers,
body,
final uri = Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null);
return DynamiteRawResponse<DashboardDashboardApiGetWidgetItemsResponseApplicationJson, void>(
response: _rootClient.doRequest(
'get',
uri,
headers,
body,
const {200},
),
bodyType: const FullType(DashboardDashboardApiGetWidgetItemsResponseApplicationJson),
headersType: null,
serializers: _jsonSerializers,
);
if (response.statusCode == 200) {
return _jsonSerializers.deserialize(
await response.jsonBody,
specifiedType: const FullType(DashboardDashboardApiGetWidgetItemsResponseApplicationJson),
)! as DashboardDashboardApiGetWidgetItemsResponseApplicationJson;
}
throw await DashboardApiException.fromResponse(response); // coverage:ignore-line
}
/// Get the items for the widgets
Future<DashboardDashboardApiGetWidgetItemsV2ResponseApplicationJson> getWidgetItemsV2({
/// Get the items for the widgets.
///
/// Returns a [Future] containing a [DynamiteResponse] with the status code, deserialized body and headers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [sinceIds] Array indexed by widget Ids, contains date/id from which we want the new items
/// * [limit] Limit number of result items per widget
/// * [widgets] Limit results to specific widgets
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: Widget items returned
///
/// See:
/// * [getWidgetItemsV2Raw] for an experimental operation that returns a [DynamiteRawResponse] that can be serialized.
Future<DynamiteResponse<DashboardDashboardApiGetWidgetItemsV2ResponseApplicationJson, void>> getWidgetItemsV2({
final ContentString<BuiltMap<String, String>>? sinceIds,
final int limit = 7,
final List<String> widgets = const <String>[],
final bool oCSAPIRequest = true,
}) async {
final rawResponse = getWidgetItemsV2Raw(
sinceIds: sinceIds,
limit: limit,
widgets: widgets,
oCSAPIRequest: oCSAPIRequest,
);
return rawResponse.future;
}
/// Get the items for the widgets.
///
/// This method and the response it returns is experimental. The API might change without a major version bump.
///
/// Returns a [Future] containing a [DynamiteRawResponse] with the raw [HttpClientResponse] and serialization helpers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [sinceIds] Array indexed by widget Ids, contains date/id from which we want the new items
/// * [limit] Limit number of result items per widget
/// * [widgets] Limit results to specific widgets
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: Widget items returned
///
/// See:
/// * [getWidgetItemsV2] for an operation that returns a [DynamiteResponse] with a stable API.
@experimental
DynamiteRawResponse<DashboardDashboardApiGetWidgetItemsV2ResponseApplicationJson, void> getWidgetItemsV2Raw({
final ContentString<BuiltMap<String, String>>? sinceIds,
final int limit = 7,
final List<String> widgets = const <String>[],
final bool oCSAPIRequest = true,
}) {
const path = '/ocs/v2.php/apps/dashboard/api/v2/widget-items';
final queryParameters = <String, dynamic>{};
final headers = <String, String>{
'Accept': 'application/json',
};
Uint8List? body;
// coverage:ignore-start
if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'bearer').isNotEmpty) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'bearer').headers,
);
} else if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'basic').isNotEmpty) {
// coverage:ignore-start
final authentication = _rootClient.authentications.firstWhereOrNull(
(final auth) => switch (auth) {
DynamiteHttpBearerAuthentication() || DynamiteHttpBasicAuthentication() => true,
_ => false,
},
);
if (authentication != null) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'basic').headers,
authentication.headers,
);
} else {
throw Exception('Missing authentication for bearer_auth or basic_auth');
}
// coverage:ignore-end
// coverage:ignore-end
if (sinceIds != null) {
queryParameters['sinceIds'] = _jsonSerializers.serialize(
sinceIds,
@ -211,19 +334,19 @@ class DashboardDashboardApiClient {
queryParameters['widgets[]'] = widgets.map((final e) => e);
}
headers['OCS-APIRequest'] = oCSAPIRequest.toString();
final response = await _rootClient.doRequest(
'get',
Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null),
headers,
body,
final uri = Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null);
return DynamiteRawResponse<DashboardDashboardApiGetWidgetItemsV2ResponseApplicationJson, void>(
response: _rootClient.doRequest(
'get',
uri,
headers,
body,
const {200},
),
bodyType: const FullType(DashboardDashboardApiGetWidgetItemsV2ResponseApplicationJson),
headersType: null,
serializers: _jsonSerializers,
);
if (response.statusCode == 200) {
return _jsonSerializers.deserialize(
await response.jsonBody,
specifiedType: const FullType(DashboardDashboardApiGetWidgetItemsV2ResponseApplicationJson),
)! as DashboardDashboardApiGetWidgetItemsV2ResponseApplicationJson;
}
throw await DashboardApiException.fromResponse(response); // coverage:ignore-line
}
}
@ -681,14 +804,8 @@ final Serializers _serializers = (Serializers().toBuilder()
))
.build();
Serializers get dashboardSerializers => _serializers;
final Serializers _jsonSerializers = (_serializers.toBuilder()
..addPlugin(StandardJsonPlugin())
..addPlugin(const ContentStringPlugin()))
.build();
T deserializeDashboard<T>(final Object data) => _serializers.deserialize(data, specifiedType: FullType(T))! as T;
Object? serializeDashboard<T>(final T data) => _serializers.serialize(data, specifiedType: FullType(T));
// coverage:ignore-end

147
packages/nextcloud/lib/src/api/dav.openapi.dart

@ -1,54 +1,22 @@
// ignore_for_file: camel_case_types
// ignore_for_file: discarded_futures
// ignore_for_file: public_member_api_docs
// ignore_for_file: unreachable_switch_case
import 'dart:typed_data';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:collection/collection.dart';
import 'package:dynamite_runtime/content_string.dart';
import 'package:dynamite_runtime/http_client.dart';
import 'package:meta/meta.dart';
import 'package:universal_io/io.dart';
export 'package:dynamite_runtime/http_client.dart';
part 'dav.openapi.g.dart';
class DavResponse<T, U> extends DynamiteResponse<T, U> {
DavResponse(
super.data,
super.headers,
);
@override
String toString() => 'DavResponse(data: $data, headers: $headers)';
}
class DavApiException extends DynamiteApiException {
DavApiException(
super.statusCode,
super.headers,
super.body,
);
static Future<DavApiException> fromResponse(final HttpClientResponse response) async {
String body;
try {
body = await response.body;
} on FormatException {
body = 'binary';
}
return DavApiException(
response.statusCode,
response.responseHeaders,
body,
);
}
@override
String toString() => 'DavApiException(statusCode: $statusCode, headers: $headers, body: $body)';
}
class DavClient extends DynamiteClient {
DavClient(
super.baseURL, {
@ -76,49 +44,106 @@ class DavDirectClient {
final DavClient _rootClient;
/// Get a direct link to a file
Future<DavDirectGetUrlResponseApplicationJson> getUrl({
/// Get a direct link to a file.
///
/// Returns a [Future] containing a [DynamiteResponse] with the status code, deserialized body and headers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [fileId] ID of the file
/// * [expirationTime] Duration until the link expires
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: Direct link returned
/// * 404: File not found
/// * 400: Getting direct link is not possible
/// * 403: Missing permissions to get direct link
///
/// See:
/// * [getUrlRaw] for an experimental operation that returns a [DynamiteRawResponse] that can be serialized.
Future<DynamiteResponse<DavDirectGetUrlResponseApplicationJson, void>> getUrl({
required final int fileId,
final int? expirationTime,
final bool oCSAPIRequest = true,
}) async {
final rawResponse = getUrlRaw(
fileId: fileId,
expirationTime: expirationTime,
oCSAPIRequest: oCSAPIRequest,
);
return rawResponse.future;
}
/// Get a direct link to a file.
///
/// This method and the response it returns is experimental. The API might change without a major version bump.
///
/// Returns a [Future] containing a [DynamiteRawResponse] with the raw [HttpClientResponse] and serialization helpers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [fileId] ID of the file
/// * [expirationTime] Duration until the link expires
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: Direct link returned
/// * 404: File not found
/// * 400: Getting direct link is not possible
/// * 403: Missing permissions to get direct link
///
/// See:
/// * [getUrl] for an operation that returns a [DynamiteResponse] with a stable API.
@experimental
DynamiteRawResponse<DavDirectGetUrlResponseApplicationJson, void> getUrlRaw({
required final int fileId,
final int? expirationTime,
final bool oCSAPIRequest = true,
}) {
const path = '/ocs/v2.php/apps/dav/api/v1/direct';
final queryParameters = <String, dynamic>{};
final headers = <String, String>{
'Accept': 'application/json',
};
Uint8List? body;
// coverage:ignore-start
if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'bearer').isNotEmpty) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'bearer').headers,
);
} else if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'basic').isNotEmpty) {
// coverage:ignore-start
final authentication = _rootClient.authentications.firstWhereOrNull(
(final auth) => switch (auth) {
DynamiteHttpBearerAuthentication() || DynamiteHttpBasicAuthentication() => true,
_ => false,
},
);
if (authentication != null) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'basic').headers,
authentication.headers,
);
} else {
throw Exception('Missing authentication for bearer_auth or basic_auth');
}
// coverage:ignore-end
// coverage:ignore-end
queryParameters['fileId'] = fileId.toString();
if (expirationTime != null) {
queryParameters['expirationTime'] = expirationTime.toString();
}
headers['OCS-APIRequest'] = oCSAPIRequest.toString();
final response = await _rootClient.doRequest(
'post',
Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null),
headers,
body,
final uri = Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null);
return DynamiteRawResponse<DavDirectGetUrlResponseApplicationJson, void>(
response: _rootClient.doRequest(
'post',
uri,
headers,
body,
const {200},
),
bodyType: const FullType(DavDirectGetUrlResponseApplicationJson),
headersType: null,
serializers: _jsonSerializers,
);
if (response.statusCode == 200) {
return _jsonSerializers.deserialize(
await response.jsonBody,
specifiedType: const FullType(DavDirectGetUrlResponseApplicationJson),
)! as DavDirectGetUrlResponseApplicationJson;
}
throw await DavApiException.fromResponse(response); // coverage:ignore-line
}
}
@ -333,14 +358,8 @@ final Serializers _serializers = (Serializers().toBuilder()
..add(DavCapabilities_Dav.serializer))
.build();
Serializers get davSerializers => _serializers;
final Serializers _jsonSerializers = (_serializers.toBuilder()
..addPlugin(StandardJsonPlugin())
..addPlugin(const ContentStringPlugin()))
.build();
T deserializeDav<T>(final Object data) => _serializers.deserialize(data, specifiedType: FullType(T))! as T;
Object? serializeDav<T>(final T data) => _serializers.serialize(data, specifiedType: FullType(T));
// coverage:ignore-end

1305
packages/nextcloud/lib/src/api/files.openapi.dart

File diff suppressed because it is too large Load Diff

135
packages/nextcloud/lib/src/api/files_external.openapi.dart

@ -1,5 +1,7 @@
// ignore_for_file: camel_case_types
// ignore_for_file: discarded_futures
// ignore_for_file: public_member_api_docs
// ignore_for_file: unreachable_switch_case
import 'dart:typed_data';
import 'package:built_collection/built_collection.dart';
@ -7,50 +9,16 @@ import 'package:built_value/built_value.dart';
import 'package:built_value/json_object.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:collection/collection.dart';
import 'package:dynamite_runtime/content_string.dart';
import 'package:dynamite_runtime/http_client.dart';
import 'package:meta/meta.dart';
import 'package:universal_io/io.dart';
export 'package:dynamite_runtime/http_client.dart';
part 'files_external.openapi.g.dart';
class FilesExternalResponse<T, U> extends DynamiteResponse<T, U> {
FilesExternalResponse(
super.data,
super.headers,
);
@override
String toString() => 'FilesExternalResponse(data: $data, headers: $headers)';
}
class FilesExternalApiException extends DynamiteApiException {
FilesExternalApiException(
super.statusCode,
super.headers,
super.body,
);
static Future<FilesExternalApiException> fromResponse(final HttpClientResponse response) async {
String body;
try {
body = await response.body;
} on FormatException {
body = 'binary';
}
return FilesExternalApiException(
response.statusCode,
response.responseHeaders,
body,
);
}
@override
String toString() => 'FilesExternalApiException(statusCode: $statusCode, headers: $headers, body: $body)';
}
class FilesExternalClient extends DynamiteClient {
FilesExternalClient(
super.baseURL, {
@ -78,41 +46,86 @@ class FilesExternalApiClient {
final FilesExternalClient _rootClient;
/// Get the mount points visible for this user
Future<FilesExternalApiGetUserMountsResponseApplicationJson> getUserMounts({final bool oCSAPIRequest = true}) async {
/// Get the mount points visible for this user.
///
/// Returns a [Future] containing a [DynamiteResponse] with the status code, deserialized body and headers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: User mounts returned
///
/// See:
/// * [getUserMountsRaw] for an experimental operation that returns a [DynamiteRawResponse] that can be serialized.
Future<DynamiteResponse<FilesExternalApiGetUserMountsResponseApplicationJson, void>> getUserMounts({
final bool oCSAPIRequest = true,
}) async {
final rawResponse = getUserMountsRaw(
oCSAPIRequest: oCSAPIRequest,
);
return rawResponse.future;
}
/// Get the mount points visible for this user.
///
/// This method and the response it returns is experimental. The API might change without a major version bump.
///
/// Returns a [Future] containing a [DynamiteRawResponse] with the raw [HttpClientResponse] and serialization helpers.
/// Throws a [DynamiteApiException] if the API call does not return an expected status code.
///
/// Parameters:
/// * [oCSAPIRequest] Required to be true for the API request to pass
///
/// Status codes:
/// * 200: User mounts returned
///
/// See:
/// * [getUserMounts] for an operation that returns a [DynamiteResponse] with a stable API.
@experimental
DynamiteRawResponse<FilesExternalApiGetUserMountsResponseApplicationJson, void> getUserMountsRaw({
final bool oCSAPIRequest = true,
}) {
const path = '/ocs/v2.php/apps/files_external/api/v1/mounts';
final queryParameters = <String, dynamic>{};
final headers = <String, String>{
'Accept': 'application/json',
};
Uint8List? body;
// coverage:ignore-start
if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'bearer').isNotEmpty) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'bearer').headers,
);
} else if (_rootClient.authentications.where((final a) => a.type == 'http' && a.scheme == 'basic').isNotEmpty) {
// coverage:ignore-start
final authentication = _rootClient.authentications.firstWhereOrNull(
(final auth) => switch (auth) {
DynamiteHttpBearerAuthentication() || DynamiteHttpBasicAuthentication() => true,
_ => false,
},
);
if (authentication != null) {
headers.addAll(
_rootClient.authentications.singleWhere((final a) => a.type == 'http' && a.scheme == 'basic').headers,
authentication.headers,
);
} else {
throw Exception('Missing authentication for bearer_auth or basic_auth');
}
// coverage:ignore-end
// coverage:ignore-end
headers['OCS-APIRequest'] = oCSAPIRequest.toString();
final response = await _rootClient.doRequest(
'get',
Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null),
headers,
body,
final uri = Uri(path: path, queryParameters: queryParameters.isNotEmpty ? queryParameters : null);
return DynamiteRawResponse<FilesExternalApiGetUserMountsResponseApplicationJson, void>(
response: _rootClient.doRequest(
'get',
uri,
headers,
body,
const {200},
),
bodyType: const FullType(FilesExternalApiGetUserMountsResponseApplicationJson),
headersType: null,
serializers: _jsonSerializers,
);
if (response.statusCode == 200) {
return _jsonSerializers.deserialize(
await response.jsonBody,
specifiedType: const FullType(FilesExternalApiGetUserMountsResponseApplicationJson),
)! as FilesExternalApiGetUserMountsResponseApplicationJson;
}
throw await FilesExternalApiException.fromResponse(response); // coverage:ignore-line
}
}
@ -374,14 +387,8 @@ final Serializers _serializers = (Serializers().toBuilder()
))
.build();
Serializers get filesExternalSerializers => _serializers;
final Serializers _jsonSerializers = (_serializers.toBuilder()
..addPlugin(StandardJsonPlugin())
..addPlugin(const ContentStringPlugin()))
.build();
T deserializeFilesExternal<T>(final Object data) => _serializers.deserialize(data, specifiedType: FullType(T))! as T;
Object? serializeFilesExternal<T>(final T data) => _serializers.serialize(data, specifiedType: FullType(T));
// coverage:ignore-end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save