diff --git a/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart b/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart index 617e081e..ed951873 100644 --- a/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart +++ b/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart @@ -28,6 +28,7 @@ export 'package:shorebird_code_push_protocol/src/messages/get_activity_heatmap/g export 'package:shorebird_code_push_protocol/src/messages/get_apps/get_apps_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_gcp_download_speed_test_url/get_gcp_download_speed_test_url200_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_gcp_upload_speed_test_url/get_gcp_upload_speed_test_url200_response.dart'; +export 'package:shorebird_code_push_protocol/src/messages/get_new_devices/get_new_devices_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_organization_apps/get_organization_apps_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_organization_users/get_organization_users_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_organizations_response.dart'; diff --git a/packages/shorebird_code_push_protocol/lib/src/messages/get_new_devices/get_new_devices_response.dart b/packages/shorebird_code_push_protocol/lib/src/messages/get_new_devices/get_new_devices_response.dart new file mode 100644 index 00000000..057d13a6 --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/messages/get_new_devices/get_new_devices_response.dart @@ -0,0 +1,88 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; + +/// {@template get_new_devices_response} +/// The response body for GET /apps/{appId}/metrics/new-devices. +/// {@endtemplate} +@immutable +class GetNewDevicesResponse { + /// {@macro get_new_devices_response} + const GetNewDevicesResponse({ + required this.current, + required this.previous, + required this.windowDays, + required this.asOf, + }); + + /// Converts a `Map` to a [GetNewDevicesResponse]. + factory GetNewDevicesResponse.fromJson(Map json) { + return parseFromJson( + 'GetNewDevicesResponse', + json, + () => GetNewDevicesResponse( + current: json['current'] as int, + previous: checkedKey(json, 'previous') as int?, + windowDays: json['window_days'] as int, + asOf: DateTime.parse(json['as_of'] as String), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static GetNewDevicesResponse? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return GetNewDevicesResponse.fromJson(json); + } + + /// Devices first seen in the window `[as_of − window_days, as_of)`. + /// An exact (non-HLL) count. + final int current; + + /// Devices first seen in the equal-length window immediately + /// preceding the current one, or null when that window would begin + /// before the metrics data floor (comparing against + /// partially-recorded history would show a misleading delta). + final int? previous; + + /// The window length in days. Hardcoded server-side in v1; echoed + /// so clients label the metric from the response rather than + /// assuming a length. + final int windowDays; + + /// Server's UTC timestamp at the moment the response was + /// constructed. Not a freshness indicator for the underlying + /// data, which is refreshed by an hourly scheduled query and + /// may lag by up to ~1 hour. + final DateTime asOf; + + /// Converts a [GetNewDevicesResponse] to a `Map`. + Map toJson() { + return { + 'current': current, + 'previous': previous, + 'window_days': windowDays, + 'as_of': asOf.toIso8601String(), + }; + } + + @override + int get hashCode => Object.hashAll([ + current, + previous, + windowDays, + asOf, + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is GetNewDevicesResponse && + current == other.current && + previous == other.previous && + windowDays == other.windowDays && + asOf == other.asOf; + } +} diff --git a/packages/shorebird_code_push_protocol/test/generated/messages/get_new_devices/get_new_devices_response_test.dart b/packages/shorebird_code_push_protocol/test/generated/messages/get_new_devices/get_new_devices_response_test.dart new file mode 100644 index 00000000..55d20019 --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/messages/get_new_devices/get_new_devices_response_test.dart @@ -0,0 +1,30 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('GetNewDevicesResponse', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = GetNewDevicesResponse( + current: 0, + previous: 0, + windowDays: 0, + asOf: DateTime.utc(2024), + ); + final parsed = GetNewDevicesResponse.maybeFromJson(instance.toJson()); + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(GetNewDevicesResponse.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => GetNewDevicesResponse.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +}