From 13388ea44f0b58c7d910ed693b1f19328ff7990f Mon Sep 17 00:00:00 2001 From: Elias Yishak <42216813+eliasyishak@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:52:25 -0700 Subject: [PATCH] Serializing/deserializing methods for `Event` instances (#251) * `.fromJson` and `.toJson` added + move utils func * Use nullable static method to parse string json * Update event.dart * Fix test * Tests for encoding/decoding json * Store intersection in local var * Remove exception thrown for nullable return * Use conditionals to check for any type errors * Test case added to check for invalid eventData * Update CHANGELOG.md * Refactor `Event.fromJson` to use pattern matching * Use package:collection for comparing eventData * Use range for collection version * `fromLabel` static method renaming * Fix test by refactoring unrelated DashTool static method * Remove `when` clause and check inside if statement * `_deepCollectionEquality` to global scope + nit fix * Remove collection dep + schema in dartdoc + nit fixes * Add'l context to `Event.fromJson` static method * Store intersection in local variable * Refactor `DashTool.fromLabel` --- pkgs/unified_analytics/CHANGELOG.md | 1 + pkgs/unified_analytics/lib/src/enums.dart | 12 ++-- pkgs/unified_analytics/lib/src/event.dart | 68 +++++++++++++++++-- .../lib/src/survey_handler.dart | 7 +- pkgs/unified_analytics/lib/src/utils.dart | 20 ------ pkgs/unified_analytics/test/event_test.dart | 55 ++++++++++++++- 6 files changed, 129 insertions(+), 34 deletions(-) diff --git a/pkgs/unified_analytics/CHANGELOG.md b/pkgs/unified_analytics/CHANGELOG.md index 1c827f0a7..7800b7703 100644 --- a/pkgs/unified_analytics/CHANGELOG.md +++ b/pkgs/unified_analytics/CHANGELOG.md @@ -4,6 +4,7 @@ - Get rid of `late` variables throughout implementation class, `AnalyticsImpl` - Any error events (`Event.analyticsException`) encountered within package will be sent when invoking `Analytics.close`; replacing `ErrorHandler` functionality - Exposing new method for `FakeAnalytics.sendPendingErrorEvents` to send error events on command +- Added `Event.fromJson` static method to generate instance of `Event` from JSON ## 5.8.8 diff --git a/pkgs/unified_analytics/lib/src/enums.dart b/pkgs/unified_analytics/lib/src/enums.dart index 1bceab116..a62f88965 100644 --- a/pkgs/unified_analytics/lib/src/enums.dart +++ b/pkgs/unified_analytics/lib/src/enums.dart @@ -153,6 +153,11 @@ enum DashEvent { required this.description, this.toolOwner, }); + + /// This takes in the string label for a given [DashEvent] and returns the + /// enum for that string label. + static DashEvent? fromLabel(String label) => + DashEvent.values.where((e) => e.label == label).firstOrNull; } /// Officially-supported clients of this package as logical @@ -199,10 +204,9 @@ enum DashTool { /// This takes in the string label for a given [DashTool] and returns the /// enum for that string label. - static DashTool getDashToolByLabel(String label) { - for (final tool in DashTool.values) { - if (tool.label == label) return tool; - } + static DashTool fromLabel(String label) { + final tool = DashTool.values.where((t) => t.label == label).firstOrNull; + if (tool != null) return tool; throw Exception('The tool $label from the survey metadata file is not ' 'a valid DashTool enum value\n' diff --git a/pkgs/unified_analytics/lib/src/event.dart b/pkgs/unified_analytics/lib/src/event.dart index 5ad7d13ce..8c8ec9e32 100644 --- a/pkgs/unified_analytics/lib/src/event.dart +++ b/pkgs/unified_analytics/lib/src/event.dart @@ -5,7 +5,6 @@ import 'dart:convert'; import 'enums.dart'; -import 'utils.dart'; final class Event { final DashEvent eventName; @@ -455,7 +454,6 @@ final class Event { : eventName = DashEvent.hotReloadTime, eventData = {'timeMs': timeMs}; - // TODO: eliasyishak, add better dartdocs to explain each param /// Events to be sent for the Flutter Hot Runner. Event.hotRunnerInfo({ required String label, @@ -507,6 +505,7 @@ final class Event { if (reloadVMTimeInMs != null) 'reloadVMTimeInMs': reloadVMTimeInMs, }; + // TODO: eliasyishak, add better dartdocs to explain each param /// Event that is emitted periodically to report the number of times each lint /// has been enabled. /// @@ -708,6 +707,10 @@ final class Event { if (label != null) 'label': label, }; + /// Private constructor to be used when deserializing JSON into an instance + /// of [Event]. + Event._({required this.eventName, required this.eventData}); + @override int get hashCode => Object.hash(eventName, jsonEncode(eventData)); @@ -716,11 +719,66 @@ final class Event { other is Event && other.runtimeType == runtimeType && other.eventName == eventName && - compareEventData(other.eventData, eventData); + _compareEventData(other.eventData, eventData); - @override - String toString() => jsonEncode({ + /// Converts an instance of [Event] to JSON. + String toJson() => jsonEncode({ 'eventName': eventName.label, 'eventData': eventData, }); + + @override + String toString() => toJson(); + + /// Utility function to take in two maps [a] and [b] and compares them + /// to ensure that they have the same keys and values + bool _compareEventData(Map a, Map b) { + final keySetA = a.keys.toSet(); + final keySetB = b.keys.toSet(); + final intersection = keySetA.intersection(keySetB); + + // Ensure that the keys are the same for each object + if (intersection.length != keySetA.length || + intersection.length != keySetB.length) { + return false; + } + + // Ensure that each of the key's values are the same + for (final key in a.keys) { + if (a[key] != b[key]) return false; + } + + return true; + } + + /// Returns a valid instance of [Event] if [json] follows the correct schema. + /// + /// Common use case for this static method involves clients of this package + /// that have a client-server setup where the server sends events that the + /// client creates. + static Event? fromJson(String json) { + try { + final jsonMap = jsonDecode(json) as Map; + + // Ensure that eventName is a string and a valid label and + // eventData is a nested object + if (jsonMap + case { + 'eventName': final String eventName, + 'eventData': final Map eventData, + }) { + final dashEvent = DashEvent.fromLabel(eventName); + if (dashEvent == null) return null; + + return Event._( + eventName: dashEvent, + eventData: eventData, + ); + } + + return null; + } on FormatException { + return null; + } + } } diff --git a/pkgs/unified_analytics/lib/src/survey_handler.dart b/pkgs/unified_analytics/lib/src/survey_handler.dart index cc533475e..c3c01b2eb 100644 --- a/pkgs/unified_analytics/lib/src/survey_handler.dart +++ b/pkgs/unified_analytics/lib/src/survey_handler.dart @@ -134,10 +134,9 @@ class Survey { samplingRate = json['samplingRate'] is String ? double.parse(json['samplingRate'] as String) : json['samplingRate'] as double, - excludeDashToolList = - (json['excludeDashTools'] as List).map((e) { - return DashTool.getDashToolByLabel(e as String); - }).toList(), + excludeDashToolList = (json['excludeDashTools'] as List) + .map((e) => DashTool.fromLabel(e as String)) + .toList(), conditionList = (json['conditions'] as List).map((e) { return Condition.fromJson(e as Map); }).toList(), diff --git a/pkgs/unified_analytics/lib/src/utils.dart b/pkgs/unified_analytics/lib/src/utils.dart index 7886c2f9f..86d64c889 100644 --- a/pkgs/unified_analytics/lib/src/utils.dart +++ b/pkgs/unified_analytics/lib/src/utils.dart @@ -36,26 +36,6 @@ bool checkDirectoryForWritePermissions(Directory directory) { return fileStat.modeString()[1] == 'w'; } -/// Utility function to take in two maps [a] and [b] and compares them -/// to ensure that they have the same keys and values -bool compareEventData(Map a, Map b) { - final keySetA = a.keys.toSet(); - final keySetB = b.keys.toSet(); - - // Ensure that the keys are the same for each object - if (keySetA.intersection(keySetB).length != keySetA.length || - keySetA.intersection(keySetB).length != keySetB.length) { - return false; - } - - // Ensure that each of the key's values are the same - for (final key in a.keys) { - if (a[key] != b[key]) return false; - } - - return true; -} - /// Format time as 'yyyy-MM-dd HH:mm:ss Z' where Z is the difference between the /// timezone of t and UTC formatted according to RFC 822. String formatDateTime(DateTime t) { diff --git a/pkgs/unified_analytics/test/event_test.dart b/pkgs/unified_analytics/test/event_test.dart index 4bd3464ba..b0c155967 100644 --- a/pkgs/unified_analytics/test/event_test.dart +++ b/pkgs/unified_analytics/test/event_test.dart @@ -556,7 +556,9 @@ void main() { test('Confirm all constructors were checked', () { var constructorCount = 0; for (var declaration in reflectClass(Event).declarations.keys) { - if (declaration.toString().contains('Event.')) constructorCount++; + // Count public constructors but omit private constructors + if (declaration.toString().contains('Event.') && + !declaration.toString().contains('Event._')) constructorCount++; } // Change this integer below if your PR either adds or removes @@ -568,4 +570,55 @@ void main() { '`pkgs/unified_analytics/test/event_test.dart` ' 'to reflect the changes made'); }); + + test('Serializing event to json successful', () { + final event = Event.analyticsException( + workflow: 'workflow', + error: 'error', + description: 'description', + ); + + final expectedResult = '{"eventName":"analytics_exception",' + '"eventData":{"workflow":"workflow",' + '"error":"error",' + '"description":"description"}}'; + + expect(event.toJson(), expectedResult); + }); + + test('Deserializing string to event successful', () { + final eventJson = '{"eventName":"analytics_exception",' + '"eventData":{"workflow":"workflow",' + '"error":"error",' + '"description":"description"}}'; + + final eventConstructed = Event.fromJson(eventJson); + expect(eventConstructed, isNotNull); + eventConstructed!; + + expect(eventConstructed.eventName, DashEvent.analyticsException); + expect(eventConstructed.eventData, { + 'workflow': 'workflow', + 'error': 'error', + 'description': 'description', + }); + }); + + test('Deserializing string to event unsuccessful for invalid eventName', () { + final eventJson = '{"eventName":"NOT_VALID_NAME",' + '"eventData":{"workflow":"workflow",' + '"error":"error",' + '"description":"description"}}'; + + final eventConstructed = Event.fromJson(eventJson); + expect(eventConstructed, isNull); + }); + + test('Deserializing string to event unsuccessful for invalid eventData', () { + final eventJson = '{"eventName":"analytics_exception",' + '"eventData": "not_valid_event_data"}'; + + final eventConstructed = Event.fromJson(eventJson); + expect(eventConstructed, isNull); + }); }