refactor(shorebird_code_push_protocol): include patch check request/response (#3031)

This commit is contained in:
Felix Angelov
2025-04-01 12:29:11 -05:00
committed by GitHub
parent 62c5445315
commit cc596939ba
8 changed files with 408 additions and 0 deletions
@@ -13,6 +13,7 @@ export 'get_organizations/get_organizations.dart';
export 'get_release_artifacts/get_release_artifacts.dart';
export 'get_release_patches/get_release_patches.dart';
export 'get_releases/get_releases.dart';
export 'patch_check/patch_check.dart';
export 'promote_patch/promote_patch.dart';
export 'update_app_collaborator/update_app_collaborator.dart';
export 'update_patch/update_patch.dart';
@@ -0,0 +1,2 @@
export 'patch_check_request.dart';
export 'patch_check_response.dart';
@@ -0,0 +1,52 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
part 'patch_check_request.g.dart';
/// {@template patch_check_request}
/// The request body for POST /api/v1/patches/check
/// {@endtemplate}
@JsonSerializable()
class PatchCheckRequest {
/// {@macro patch_check_request}
const PatchCheckRequest({
required this.releaseVersion,
required this.patchNumber,
required this.patchHash,
required this.platform,
required this.arch,
required this.appId,
required this.channel,
});
/// Converts a `Map<String, dynamic>` to a [PatchCheckRequest]
factory PatchCheckRequest.fromJson(Map<String, dynamic> json) =>
_$PatchCheckRequestFromJson(json);
/// Converts a [PatchCheckRequest] to a `Map<String, dynamic>`
Map<String, dynamic> toJson() => _$PatchCheckRequestToJson(this);
/// The release version of the app.
final String releaseVersion;
/// The highest patch number that the client has downloaded.
/// If provided, the server will only return patches with a higher patch
/// number.
/// If not provided, the server will provide the latest available patch.
final int? patchNumber;
/// The current patch hash of the app.
final String? patchHash;
/// The platform of the app.
final ReleasePlatform platform;
/// The architecture of the app.
final String arch;
/// The ID of the app.
final String appId;
/// The channel of the app.
final String channel;
}
@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'patch_check_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PatchCheckRequest _$PatchCheckRequestFromJson(
Map<String, dynamic> json,
) => $checkedCreate(
'PatchCheckRequest',
json,
($checkedConvert) {
final val = PatchCheckRequest(
releaseVersion: $checkedConvert('release_version', (v) => v as String),
patchNumber: $checkedConvert('patch_number', (v) => (v as num?)?.toInt()),
patchHash: $checkedConvert('patch_hash', (v) => v as String?),
platform: $checkedConvert(
'platform',
(v) => $enumDecode(_$ReleasePlatformEnumMap, v),
),
arch: $checkedConvert('arch', (v) => v as String),
appId: $checkedConvert('app_id', (v) => v as String),
channel: $checkedConvert('channel', (v) => v as String),
);
return val;
},
fieldKeyMap: const {
'releaseVersion': 'release_version',
'patchNumber': 'patch_number',
'patchHash': 'patch_hash',
'appId': 'app_id',
},
);
Map<String, dynamic> _$PatchCheckRequestToJson(PatchCheckRequest instance) =>
<String, dynamic>{
'release_version': instance.releaseVersion,
'patch_number': instance.patchNumber,
'patch_hash': instance.patchHash,
'platform': _$ReleasePlatformEnumMap[instance.platform]!,
'arch': instance.arch,
'app_id': instance.appId,
'channel': instance.channel,
};
const _$ReleasePlatformEnumMap = {
ReleasePlatform.android: 'android',
ReleasePlatform.ios: 'ios',
ReleasePlatform.linux: 'linux',
ReleasePlatform.macos: 'macos',
ReleasePlatform.windows: 'windows',
};
@@ -0,0 +1,75 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'patch_check_response.g.dart';
/// {@template patch_check_response}
/// The response body for POST /api/v1/patches/check
/// {@endtemplate}
@JsonSerializable()
class PatchCheckResponse extends Equatable {
/// {@macro patch_check_response}
const PatchCheckResponse({
required this.patchAvailable,
this.patch,
this.rolledBackPatchNumbers,
});
/// Converts a `Map<String, dynamic>` to a [PatchCheckResponse]
factory PatchCheckResponse.fromJson(Map<String, dynamic> json) =>
_$PatchCheckResponseFromJson(json);
/// Converts a [PatchCheckResponse] to a `Map<String, dynamic>`
Map<String, dynamic> toJson() => _$PatchCheckResponseToJson(this);
/// Whether a patch is available.
final bool patchAvailable;
/// The patch metadata.
final PatchCheckMetadata? patch;
/// The numbers of all patches that have been rolled back for the current
/// release.
final List<int>? rolledBackPatchNumbers;
@override
List<Object?> get props => [patchAvailable, patch, rolledBackPatchNumbers];
}
/// {@template patch_check_metadata}
/// Patch metadata represents the contents of an update (patch) for a specific
/// platform and architecture.
/// {@endtemplate}
@JsonSerializable()
class PatchCheckMetadata extends Equatable {
/// {@macro patch_check_metadata}
const PatchCheckMetadata({
required this.number,
required this.downloadUrl,
required this.hash,
required this.hashSignature,
});
/// Converts a `Map<String, dynamic>` to an [PatchCheckMetadata]
factory PatchCheckMetadata.fromJson(Map<String, dynamic> json) =>
_$PatchCheckMetadataFromJson(json);
/// Converts an [PatchCheckMetadata] to a `Map<String, dynamic>`
Map<String, dynamic> toJson() => _$PatchCheckMetadataToJson(this);
/// The patch number associated with the artifact.
final int number;
/// The URL of the artifact.
final String downloadUrl;
/// The hash of the artifact.
final String hash;
/// The signature of the [hash].
@JsonKey(includeIfNull: false)
final String? hashSignature;
@override
List<Object?> get props => [number, downloadUrl, hash, hashSignature];
}
@@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'patch_check_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PatchCheckResponse _$PatchCheckResponseFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'PatchCheckResponse',
json,
($checkedConvert) {
final val = PatchCheckResponse(
patchAvailable: $checkedConvert('patch_available', (v) => v as bool),
patch: $checkedConvert(
'patch',
(v) =>
v == null
? null
: PatchCheckMetadata.fromJson(v as Map<String, dynamic>),
),
rolledBackPatchNumbers: $checkedConvert(
'rolled_back_patch_numbers',
(v) =>
(v as List<dynamic>?)?.map((e) => (e as num).toInt()).toList(),
),
);
return val;
},
fieldKeyMap: const {
'patchAvailable': 'patch_available',
'rolledBackPatchNumbers': 'rolled_back_patch_numbers',
},
);
Map<String, dynamic> _$PatchCheckResponseToJson(PatchCheckResponse instance) =>
<String, dynamic>{
'patch_available': instance.patchAvailable,
'patch': instance.patch?.toJson(),
'rolled_back_patch_numbers': instance.rolledBackPatchNumbers,
};
PatchCheckMetadata _$PatchCheckMetadataFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'PatchCheckMetadata',
json,
($checkedConvert) {
final val = PatchCheckMetadata(
number: $checkedConvert('number', (v) => (v as num).toInt()),
downloadUrl: $checkedConvert('download_url', (v) => v as String),
hash: $checkedConvert('hash', (v) => v as String),
hashSignature: $checkedConvert('hash_signature', (v) => v as String?),
);
return val;
},
fieldKeyMap: const {
'downloadUrl': 'download_url',
'hashSignature': 'hash_signature',
},
);
Map<String, dynamic> _$PatchCheckMetadataToJson(PatchCheckMetadata instance) =>
<String, dynamic>{
'number': instance.number,
'download_url': instance.downloadUrl,
'hash': instance.hash,
if (instance.hashSignature case final value?) 'hash_signature': value,
};
@@ -0,0 +1,22 @@
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
import 'package:test/test.dart';
void main() {
group(PatchCheckRequest, () {
test('can be (de)serialized', () {
const request = PatchCheckRequest(
releaseVersion: '1',
patchNumber: 2,
patchHash: '3',
platform: ReleasePlatform.android,
arch: 'arm64',
appId: 'app_123',
channel: 'channel_123',
);
expect(
PatchCheckRequest.fromJson(request.toJson()).toJson(),
equals(request.toJson()),
);
});
});
}
@@ -0,0 +1,129 @@
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
import 'package:test/test.dart';
void main() {
group(PatchCheckResponse, () {
test('uses value equality', () {
expect(
// Ignoring for value quality testing.
// ignore: prefer_const_constructors
PatchCheckResponse(patchAvailable: true),
// Ignoring for value quality testing.
// ignore: prefer_const_constructors
equals(PatchCheckResponse(patchAvailable: true)),
);
});
test('can be (de)serialized', () {
const response = PatchCheckResponse(patchAvailable: true);
expect(
PatchCheckResponse.fromJson(response.toJson()).toJson(),
equals(response.toJson()),
);
});
test('can be serialized to json without patch metadata', () {
const response = PatchCheckResponse(patchAvailable: true);
expect(response.toJson(), {
'patch_available': true,
'patch': null,
'rolled_back_patch_numbers': null,
});
});
test('PatchCheckMetadata uses value equality', () {
expect(
// Ignoring for value quality testing.
// ignore: prefer_const_constructors
PatchCheckMetadata(
number: 1,
downloadUrl: 'https://download.com',
hash: '1234',
hashSignature: null,
),
equals(
// Ignoring for value quality testing.
// ignore: prefer_const_constructors
PatchCheckMetadata(
number: 1,
downloadUrl: 'https://download.com',
hash: '1234',
hashSignature: null,
),
),
);
});
test('PatchCheckMetadata can be (de)serialized', () {
const metadata = PatchCheckMetadata(
number: 1,
downloadUrl: 'https://download.com',
hash: '1234',
hashSignature: null,
);
expect(
PatchCheckMetadata.fromJson(metadata.toJson()).toJson(),
equals(metadata.toJson()),
);
});
test('can be serialized to json with patch metadata', () {
const response = PatchCheckResponse(
patchAvailable: true,
patch: PatchCheckMetadata(
number: 1,
downloadUrl: 'https://download.com',
hash: '1234',
hashSignature: null,
),
);
expect(response.toJson(), {
'patch_available': true,
'patch': {
'number': 1,
'download_url': 'https://download.com',
'hash': '1234',
},
'rolled_back_patch_numbers': null,
});
});
group('when there is a hash signature', () {
test('includes the signature', () {
const response = PatchCheckResponse(
patchAvailable: true,
patch: PatchCheckMetadata(
number: 1,
downloadUrl: 'https://download.com',
hash: '1234',
hashSignature: 'signature',
),
);
expect(response.toJson(), {
'patch_available': true,
'patch': {
'number': 1,
'download_url': 'https://download.com',
'hash': '1234',
'hash_signature': 'signature',
},
'rolled_back_patch_numbers': null,
});
});
});
group('when rolled back patch numbers are provided', () {
test('includes the rolled back patch numbers', () {
const response = PatchCheckResponse(
patchAvailable: true,
rolledBackPatchNumbers: [1, 2, 3],
);
expect(response.toJson(), {
'patch_available': true,
'patch': null,
'rolled_back_patch_numbers': [1, 2, 3],
});
});
});
});
}