mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 12:16:20 -04:00
chore(mobile): properly patch openapi with custom response dto (#11753)
This commit is contained in:
parent
fdf0b16fe3
commit
5ec407b57c
12
mobile/lib/utils/openapi_patching.dart
Normal file
12
mobile/lib/utils/openapi_patching.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
dynamic upgradeDto(dynamic value, String targetType) {
|
||||||
|
switch (targetType) {
|
||||||
|
case 'UserPreferencesResponseDto':
|
||||||
|
if (value is Map) {
|
||||||
|
if (value['rating'] == null) {
|
||||||
|
value['rating'] = RatingResponse().toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -16,6 +16,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/utils/openapi_patching.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
1
mobile/openapi/lib/api_client.dart
generated
1
mobile/openapi/lib/api_client.dart
generated
@ -166,6 +166,7 @@ class ApiClient {
|
|||||||
|
|
||||||
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
|
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
|
||||||
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
|
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
|
||||||
|
upgradeDto(value, targetType);
|
||||||
try {
|
try {
|
||||||
switch (targetType) {
|
switch (targetType) {
|
||||||
case 'String':
|
case 'String':
|
||||||
|
2
mobile/openapi/lib/model/rating_response.dart
generated
2
mobile/openapi/lib/model/rating_response.dart
generated
@ -13,7 +13,7 @@ part of openapi.api;
|
|||||||
class RatingResponse {
|
class RatingResponse {
|
||||||
/// Returns a new [RatingResponse] instance.
|
/// Returns a new [RatingResponse] instance.
|
||||||
RatingResponse({
|
RatingResponse({
|
||||||
required this.enabled,
|
this.enabled = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
@ -13,5 +13,5 @@ dependencies:
|
|||||||
http: '>=0.13.0 <0.14.0'
|
http: '>=0.13.0 <0.14.0'
|
||||||
intl: any
|
intl: any
|
||||||
meta: '^1.1.8'
|
meta: '^1.1.8'
|
||||||
dev_dependencies:
|
immich_mobile:
|
||||||
test: '>=1.21.6 <1.22.0'
|
path: ../
|
||||||
|
@ -8,12 +8,18 @@ function dart {
|
|||||||
cd ./templates/mobile/serialization/native
|
cd ./templates/mobile/serialization/native
|
||||||
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
|
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
|
||||||
patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
|
patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
|
||||||
cd ../../../..
|
|
||||||
|
cd ../../
|
||||||
|
wget -O api_client.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/api_client.mustache
|
||||||
|
patch --no-backup-if-mismatch -u api_client.mustache <api_client.mustache.patch
|
||||||
|
|
||||||
|
cd ../../
|
||||||
npx --yes @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./templates/mobile
|
npx --yes @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./templates/mobile
|
||||||
|
|
||||||
# Post generate patches
|
# Post generate patches
|
||||||
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/api_client.dart.patch
|
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/api_client.dart.patch
|
||||||
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./patch/api.dart.patch
|
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./patch/api.dart.patch
|
||||||
|
patch --no-backup-if-mismatch -u ../mobile/openapi/pubspec.yaml <./patch/pubspec_immich_mobile.yaml.patch
|
||||||
# Don't include analysis_options.yaml for the generated openapi files
|
# Don't include analysis_options.yaml for the generated openapi files
|
||||||
# so that language servers can properly exclude the mobile/openapi directory
|
# so that language servers can properly exclude the mobile/openapi directory
|
||||||
rm ../mobile/openapi/analysis_options.yaml
|
rm ../mobile/openapi/analysis_options.yaml
|
||||||
@ -34,4 +40,4 @@ elif [[ $1 == 'typescript' ]]; then
|
|||||||
else
|
else
|
||||||
dart
|
dart
|
||||||
typescript
|
typescript
|
||||||
fi
|
fi
|
@ -9918,6 +9918,7 @@
|
|||||||
"RatingResponse": {
|
"RatingResponse": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
|
"default": false,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
@@ -15,6 +15,7 @@
|
@@ -15,6 +15,8 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
+import 'package:flutter/foundation.dart';
|
+import 'package:flutter/foundation.dart';
|
||||||
|
+import 'package:immich_mobile/utils/openapi_patching.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
9
open-api/patch/pubspec_immich_mobile.yaml.patch
Normal file
9
open-api/patch/pubspec_immich_mobile.yaml.patch
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Include code from immich_mobile
|
||||||
|
@@ -13,5 +13,5 @@
|
||||||
|
http: '>=0.13.0 <0.14.0'
|
||||||
|
intl: any
|
||||||
|
meta: '^1.1.8'
|
||||||
|
-dev_dependencies:
|
||||||
|
- test: '>=1.21.6 <1.22.0'
|
||||||
|
+ immich_mobile:
|
||||||
|
+ path: ../
|
264
open-api/templates/mobile/api_client.mustache
Normal file
264
open-api/templates/mobile/api_client.mustache
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
{{>header}}
|
||||||
|
{{>part_of}}
|
||||||
|
class ApiClient {
|
||||||
|
ApiClient({this.basePath = '{{{basePath}}}', this.authentication,});
|
||||||
|
|
||||||
|
final String basePath;
|
||||||
|
final Authentication? authentication;
|
||||||
|
|
||||||
|
var _client = Client();
|
||||||
|
final _defaultHeaderMap = <String, String>{};
|
||||||
|
|
||||||
|
/// Returns the current HTTP [Client] instance to use in this class.
|
||||||
|
///
|
||||||
|
/// The return value is guaranteed to never be null.
|
||||||
|
Client get client => _client;
|
||||||
|
|
||||||
|
/// Requests to use a new HTTP [Client] in this class.
|
||||||
|
set client(Client newClient) {
|
||||||
|
_client = newClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> get defaultHeaderMap => _defaultHeaderMap;
|
||||||
|
|
||||||
|
void addDefaultHeader(String key, String value) {
|
||||||
|
_defaultHeaderMap[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't use a Map<String, String> for queryParams.
|
||||||
|
// If collectionFormat is 'multi', a key might appear multiple times.
|
||||||
|
Future<Response> invokeAPI(
|
||||||
|
String path,
|
||||||
|
String method,
|
||||||
|
List<QueryParam> queryParams,
|
||||||
|
Object? body,
|
||||||
|
Map<String, String> headerParams,
|
||||||
|
Map<String, String> formParams,
|
||||||
|
String? contentType,
|
||||||
|
) async {
|
||||||
|
await authentication?.applyToParams(queryParams, headerParams);
|
||||||
|
|
||||||
|
headerParams.addAll(_defaultHeaderMap);
|
||||||
|
if (contentType != null) {
|
||||||
|
headerParams['Content-Type'] = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
final urlEncodedQueryParams = queryParams.map((param) => '$param');
|
||||||
|
final queryString = urlEncodedQueryParams.isNotEmpty ? '?${urlEncodedQueryParams.join('&')}' : '';
|
||||||
|
final uri = Uri.parse('$basePath$path$queryString');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Special case for uploading a single file which isn't a 'multipart/form-data'.
|
||||||
|
if (
|
||||||
|
body is MultipartFile && (contentType == null ||
|
||||||
|
!contentType.toLowerCase().startsWith('multipart/form-data'))
|
||||||
|
) {
|
||||||
|
final request = StreamedRequest(method, uri);
|
||||||
|
request.headers.addAll(headerParams);
|
||||||
|
request.contentLength = body.length;
|
||||||
|
body.finalize().listen(
|
||||||
|
request.sink.add,
|
||||||
|
onDone: request.sink.close,
|
||||||
|
// ignore: avoid_types_on_closure_parameters
|
||||||
|
onError: (Object error, StackTrace trace) => request.sink.close(),
|
||||||
|
cancelOnError: true,
|
||||||
|
);
|
||||||
|
final response = await _client.send(request);
|
||||||
|
return Response.fromStream(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body is MultipartRequest) {
|
||||||
|
final request = MultipartRequest(method, uri);
|
||||||
|
request.fields.addAll(body.fields);
|
||||||
|
request.files.addAll(body.files);
|
||||||
|
request.headers.addAll(body.headers);
|
||||||
|
request.headers.addAll(headerParams);
|
||||||
|
final response = await _client.send(request);
|
||||||
|
return Response.fromStream(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
final msgBody = contentType == 'application/x-www-form-urlencoded'
|
||||||
|
? formParams
|
||||||
|
: await serializeAsync(body);
|
||||||
|
final nullableHeaderParams = headerParams.isEmpty ? null : headerParams;
|
||||||
|
|
||||||
|
switch(method) {
|
||||||
|
case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,);
|
||||||
|
case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,);
|
||||||
|
case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,);
|
||||||
|
case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,);
|
||||||
|
case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,);
|
||||||
|
case 'GET': return await _client.get(uri, headers: nullableHeaderParams,);
|
||||||
|
}
|
||||||
|
} on SocketException catch (error, trace) {
|
||||||
|
throw ApiException.withInner(
|
||||||
|
HttpStatus.badRequest,
|
||||||
|
'Socket operation failed: $method $path',
|
||||||
|
error,
|
||||||
|
trace,
|
||||||
|
);
|
||||||
|
} on TlsException catch (error, trace) {
|
||||||
|
throw ApiException.withInner(
|
||||||
|
HttpStatus.badRequest,
|
||||||
|
'TLS/SSL communication failed: $method $path',
|
||||||
|
error,
|
||||||
|
trace,
|
||||||
|
);
|
||||||
|
} on IOException catch (error, trace) {
|
||||||
|
throw ApiException.withInner(
|
||||||
|
HttpStatus.badRequest,
|
||||||
|
'I/O operation failed: $method $path',
|
||||||
|
error,
|
||||||
|
trace,
|
||||||
|
);
|
||||||
|
} on ClientException catch (error, trace) {
|
||||||
|
throw ApiException.withInner(
|
||||||
|
HttpStatus.badRequest,
|
||||||
|
'HTTP connection failed: $method $path',
|
||||||
|
error,
|
||||||
|
trace,
|
||||||
|
);
|
||||||
|
} on Exception catch (error, trace) {
|
||||||
|
throw ApiException.withInner(
|
||||||
|
HttpStatus.badRequest,
|
||||||
|
'Exception occurred: $method $path',
|
||||||
|
error,
|
||||||
|
trace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ApiException(
|
||||||
|
HttpStatus.badRequest,
|
||||||
|
'Invalid HTTP operation: $method $path',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
{{#native_serialization}}
|
||||||
|
|
||||||
|
Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) async =>
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
deserialize(value, targetType, growable: growable);
|
||||||
|
|
||||||
|
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
|
||||||
|
dynamic deserialize(String value, String targetType, {bool growable = false,}) {
|
||||||
|
// Remove all spaces. Necessary for regular expressions as well.
|
||||||
|
targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
|
||||||
|
|
||||||
|
// If the expected target type is String, nothing to do...
|
||||||
|
return targetType == 'String'
|
||||||
|
? value
|
||||||
|
: fromJson(json.decode(value), targetType, growable: growable);
|
||||||
|
}
|
||||||
|
{{/native_serialization}}
|
||||||
|
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
Future<String> serializeAsync(Object? value) async => serialize(value);
|
||||||
|
|
||||||
|
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use serializeAsync() instead.')
|
||||||
|
String serialize(Object? value) => value == null ? '' : json.encode(value);
|
||||||
|
|
||||||
|
{{#native_serialization}}
|
||||||
|
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
|
||||||
|
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
|
||||||
|
upgradeDto(value, targetType);
|
||||||
|
try {
|
||||||
|
switch (targetType) {
|
||||||
|
case 'String':
|
||||||
|
return value is String ? value : value.toString();
|
||||||
|
case 'int':
|
||||||
|
return value is int ? value : int.parse('$value');
|
||||||
|
case 'double':
|
||||||
|
return value is double ? value : double.parse('$value');
|
||||||
|
case 'bool':
|
||||||
|
if (value is bool) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
final valueString = '$value'.toLowerCase();
|
||||||
|
return valueString == 'true' || valueString == '1';
|
||||||
|
case 'DateTime':
|
||||||
|
return value is DateTime ? value : DateTime.tryParse(value);
|
||||||
|
{{#models}}
|
||||||
|
{{#model}}
|
||||||
|
case '{{{classname}}}':
|
||||||
|
{{#isEnum}}
|
||||||
|
{{#native_serialization}}return {{{classname}}}TypeTransformer().decode(value);{{/native_serialization}}
|
||||||
|
{{/isEnum}}
|
||||||
|
{{^isEnum}}
|
||||||
|
return {{{classname}}}.fromJson(value);
|
||||||
|
{{/isEnum}}
|
||||||
|
{{/model}}
|
||||||
|
{{/models}}
|
||||||
|
default:
|
||||||
|
dynamic match;
|
||||||
|
if (value is List && (match = _regList.firstMatch(targetType)?.group(1)) != null) {
|
||||||
|
return value
|
||||||
|
.map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,))
|
||||||
|
.toList(growable: growable);
|
||||||
|
}
|
||||||
|
if (value is Set && (match = _regSet.firstMatch(targetType)?.group(1)) != null) {
|
||||||
|
return value
|
||||||
|
.map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,))
|
||||||
|
.toSet();
|
||||||
|
}
|
||||||
|
if (value is Map && (match = _regMap.firstMatch(targetType)?.group(1)) != null) {
|
||||||
|
return Map<String, dynamic>.fromIterables(
|
||||||
|
value.keys.cast<String>(),
|
||||||
|
value.values.map<dynamic>((dynamic v) => fromJson(v, match, growable: growable,)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on Exception catch (error, trace) {
|
||||||
|
throw ApiException.withInner(HttpStatus.internalServerError, 'Exception during deserialization.', error, trace,);
|
||||||
|
}
|
||||||
|
throw ApiException(HttpStatus.internalServerError, 'Could not find a suitable class for deserialization',);
|
||||||
|
}
|
||||||
|
{{/native_serialization}}
|
||||||
|
}
|
||||||
|
{{#native_serialization}}
|
||||||
|
|
||||||
|
/// Primarily intended for use in an isolate.
|
||||||
|
class DeserializationMessage {
|
||||||
|
const DeserializationMessage({
|
||||||
|
required this.json,
|
||||||
|
required this.targetType,
|
||||||
|
this.growable = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The JSON value to deserialize.
|
||||||
|
final String json;
|
||||||
|
|
||||||
|
/// Target type to deserialize to.
|
||||||
|
final String targetType;
|
||||||
|
|
||||||
|
/// Whether to make deserialized lists or maps growable.
|
||||||
|
final bool growable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Primarily intended for use in an isolate.
|
||||||
|
Future<dynamic> decodeAsync(DeserializationMessage message) async {
|
||||||
|
// Remove all spaces. Necessary for regular expressions as well.
|
||||||
|
final targetType = message.targetType.replaceAll(' ', '');
|
||||||
|
|
||||||
|
// If the expected target type is String, nothing to do...
|
||||||
|
return targetType == 'String'
|
||||||
|
? message.json
|
||||||
|
: json.decode(message.json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Primarily intended for use in an isolate.
|
||||||
|
Future<dynamic> deserializeAsync(DeserializationMessage message) async {
|
||||||
|
// Remove all spaces. Necessary for regular expressions as well.
|
||||||
|
final targetType = message.targetType.replaceAll(' ', '');
|
||||||
|
|
||||||
|
// If the expected target type is String, nothing to do...
|
||||||
|
return targetType == 'String'
|
||||||
|
? message.json
|
||||||
|
: ApiClient.fromJson(
|
||||||
|
json.decode(message.json),
|
||||||
|
targetType,
|
||||||
|
growable: message.growable,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
{{/native_serialization}}
|
||||||
|
|
||||||
|
/// Primarily intended for use in an isolate.
|
||||||
|
Future<String> serializeAsync(Object? value) async => value == null ? '' : json.encode(value);
|
10
open-api/templates/mobile/api_client.mustache.patch
Normal file
10
open-api/templates/mobile/api_client.mustache.patch
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
--- api_client.mustache 2024-08-13 14:29:04.056364916 -0500
|
||||||
|
+++ api_client_new.mustache 2024-08-13 14:29:36.224410735 -0500
|
||||||
|
@@ -159,6 +159,7 @@
|
||||||
|
{{#native_serialization}}
|
||||||
|
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
|
||||||
|
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
|
||||||
|
+ upgradeDto(value, targetType);
|
||||||
|
try {
|
||||||
|
switch (targetType) {
|
||||||
|
case 'String':
|
@ -87,7 +87,7 @@ class AvatarResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RatingResponse {
|
class RatingResponse {
|
||||||
enabled!: boolean;
|
enabled: boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MemoryResponse {
|
class MemoryResponse {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user