Skip to content

Commit 13388ea

Browse files
authored
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`
1 parent 5e26782 commit 13388ea

File tree

6 files changed

+129
-34
lines changed

6 files changed

+129
-34
lines changed

pkgs/unified_analytics/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Get rid of `late` variables throughout implementation class, `AnalyticsImpl`
55
- Any error events (`Event.analyticsException`) encountered within package will be sent when invoking `Analytics.close`; replacing `ErrorHandler` functionality
66
- Exposing new method for `FakeAnalytics.sendPendingErrorEvents` to send error events on command
7+
- Added `Event.fromJson` static method to generate instance of `Event` from JSON
78

89
## 5.8.8
910

pkgs/unified_analytics/lib/src/enums.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ enum DashEvent {
153153
required this.description,
154154
this.toolOwner,
155155
});
156+
157+
/// This takes in the string label for a given [DashEvent] and returns the
158+
/// enum for that string label.
159+
static DashEvent? fromLabel(String label) =>
160+
DashEvent.values.where((e) => e.label == label).firstOrNull;
156161
}
157162

158163
/// Officially-supported clients of this package as logical
@@ -199,10 +204,9 @@ enum DashTool {
199204

200205
/// This takes in the string label for a given [DashTool] and returns the
201206
/// enum for that string label.
202-
static DashTool getDashToolByLabel(String label) {
203-
for (final tool in DashTool.values) {
204-
if (tool.label == label) return tool;
205-
}
207+
static DashTool fromLabel(String label) {
208+
final tool = DashTool.values.where((t) => t.label == label).firstOrNull;
209+
if (tool != null) return tool;
206210

207211
throw Exception('The tool $label from the survey metadata file is not '
208212
'a valid DashTool enum value\n'

pkgs/unified_analytics/lib/src/event.dart

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import 'dart:convert';
66

77
import 'enums.dart';
8-
import 'utils.dart';
98

109
final class Event {
1110
final DashEvent eventName;
@@ -455,7 +454,6 @@ final class Event {
455454
: eventName = DashEvent.hotReloadTime,
456455
eventData = {'timeMs': timeMs};
457456

458-
// TODO: eliasyishak, add better dartdocs to explain each param
459457
/// Events to be sent for the Flutter Hot Runner.
460458
Event.hotRunnerInfo({
461459
required String label,
@@ -507,6 +505,7 @@ final class Event {
507505
if (reloadVMTimeInMs != null) 'reloadVMTimeInMs': reloadVMTimeInMs,
508506
};
509507

508+
// TODO: eliasyishak, add better dartdocs to explain each param
510509
/// Event that is emitted periodically to report the number of times each lint
511510
/// has been enabled.
512511
///
@@ -708,6 +707,10 @@ final class Event {
708707
if (label != null) 'label': label,
709708
};
710709

710+
/// Private constructor to be used when deserializing JSON into an instance
711+
/// of [Event].
712+
Event._({required this.eventName, required this.eventData});
713+
711714
@override
712715
int get hashCode => Object.hash(eventName, jsonEncode(eventData));
713716

@@ -716,11 +719,66 @@ final class Event {
716719
other is Event &&
717720
other.runtimeType == runtimeType &&
718721
other.eventName == eventName &&
719-
compareEventData(other.eventData, eventData);
722+
_compareEventData(other.eventData, eventData);
720723

721-
@override
722-
String toString() => jsonEncode({
724+
/// Converts an instance of [Event] to JSON.
725+
String toJson() => jsonEncode({
723726
'eventName': eventName.label,
724727
'eventData': eventData,
725728
});
729+
730+
@override
731+
String toString() => toJson();
732+
733+
/// Utility function to take in two maps [a] and [b] and compares them
734+
/// to ensure that they have the same keys and values
735+
bool _compareEventData(Map<String, Object?> a, Map<String, Object?> b) {
736+
final keySetA = a.keys.toSet();
737+
final keySetB = b.keys.toSet();
738+
final intersection = keySetA.intersection(keySetB);
739+
740+
// Ensure that the keys are the same for each object
741+
if (intersection.length != keySetA.length ||
742+
intersection.length != keySetB.length) {
743+
return false;
744+
}
745+
746+
// Ensure that each of the key's values are the same
747+
for (final key in a.keys) {
748+
if (a[key] != b[key]) return false;
749+
}
750+
751+
return true;
752+
}
753+
754+
/// Returns a valid instance of [Event] if [json] follows the correct schema.
755+
///
756+
/// Common use case for this static method involves clients of this package
757+
/// that have a client-server setup where the server sends events that the
758+
/// client creates.
759+
static Event? fromJson(String json) {
760+
try {
761+
final jsonMap = jsonDecode(json) as Map<String, Object?>;
762+
763+
// Ensure that eventName is a string and a valid label and
764+
// eventData is a nested object
765+
if (jsonMap
766+
case {
767+
'eventName': final String eventName,
768+
'eventData': final Map<String, Object?> eventData,
769+
}) {
770+
final dashEvent = DashEvent.fromLabel(eventName);
771+
if (dashEvent == null) return null;
772+
773+
return Event._(
774+
eventName: dashEvent,
775+
eventData: eventData,
776+
);
777+
}
778+
779+
return null;
780+
} on FormatException {
781+
return null;
782+
}
783+
}
726784
}

pkgs/unified_analytics/lib/src/survey_handler.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,9 @@ class Survey {
134134
samplingRate = json['samplingRate'] is String
135135
? double.parse(json['samplingRate'] as String)
136136
: json['samplingRate'] as double,
137-
excludeDashToolList =
138-
(json['excludeDashTools'] as List<dynamic>).map((e) {
139-
return DashTool.getDashToolByLabel(e as String);
140-
}).toList(),
137+
excludeDashToolList = (json['excludeDashTools'] as List<dynamic>)
138+
.map((e) => DashTool.fromLabel(e as String))
139+
.toList(),
141140
conditionList = (json['conditions'] as List<dynamic>).map((e) {
142141
return Condition.fromJson(e as Map<String, dynamic>);
143142
}).toList(),

pkgs/unified_analytics/lib/src/utils.dart

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,26 +36,6 @@ bool checkDirectoryForWritePermissions(Directory directory) {
3636
return fileStat.modeString()[1] == 'w';
3737
}
3838

39-
/// Utility function to take in two maps [a] and [b] and compares them
40-
/// to ensure that they have the same keys and values
41-
bool compareEventData(Map<String, Object?> a, Map<String, Object?> b) {
42-
final keySetA = a.keys.toSet();
43-
final keySetB = b.keys.toSet();
44-
45-
// Ensure that the keys are the same for each object
46-
if (keySetA.intersection(keySetB).length != keySetA.length ||
47-
keySetA.intersection(keySetB).length != keySetB.length) {
48-
return false;
49-
}
50-
51-
// Ensure that each of the key's values are the same
52-
for (final key in a.keys) {
53-
if (a[key] != b[key]) return false;
54-
}
55-
56-
return true;
57-
}
58-
5939
/// Format time as 'yyyy-MM-dd HH:mm:ss Z' where Z is the difference between the
6040
/// timezone of t and UTC formatted according to RFC 822.
6141
String formatDateTime(DateTime t) {

pkgs/unified_analytics/test/event_test.dart

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,9 @@ void main() {
556556
test('Confirm all constructors were checked', () {
557557
var constructorCount = 0;
558558
for (var declaration in reflectClass(Event).declarations.keys) {
559-
if (declaration.toString().contains('Event.')) constructorCount++;
559+
// Count public constructors but omit private constructors
560+
if (declaration.toString().contains('Event.') &&
561+
!declaration.toString().contains('Event._')) constructorCount++;
560562
}
561563

562564
// Change this integer below if your PR either adds or removes
@@ -568,4 +570,55 @@ void main() {
568570
'`pkgs/unified_analytics/test/event_test.dart` '
569571
'to reflect the changes made');
570572
});
573+
574+
test('Serializing event to json successful', () {
575+
final event = Event.analyticsException(
576+
workflow: 'workflow',
577+
error: 'error',
578+
description: 'description',
579+
);
580+
581+
final expectedResult = '{"eventName":"analytics_exception",'
582+
'"eventData":{"workflow":"workflow",'
583+
'"error":"error",'
584+
'"description":"description"}}';
585+
586+
expect(event.toJson(), expectedResult);
587+
});
588+
589+
test('Deserializing string to event successful', () {
590+
final eventJson = '{"eventName":"analytics_exception",'
591+
'"eventData":{"workflow":"workflow",'
592+
'"error":"error",'
593+
'"description":"description"}}';
594+
595+
final eventConstructed = Event.fromJson(eventJson);
596+
expect(eventConstructed, isNotNull);
597+
eventConstructed!;
598+
599+
expect(eventConstructed.eventName, DashEvent.analyticsException);
600+
expect(eventConstructed.eventData, {
601+
'workflow': 'workflow',
602+
'error': 'error',
603+
'description': 'description',
604+
});
605+
});
606+
607+
test('Deserializing string to event unsuccessful for invalid eventName', () {
608+
final eventJson = '{"eventName":"NOT_VALID_NAME",'
609+
'"eventData":{"workflow":"workflow",'
610+
'"error":"error",'
611+
'"description":"description"}}';
612+
613+
final eventConstructed = Event.fromJson(eventJson);
614+
expect(eventConstructed, isNull);
615+
});
616+
617+
test('Deserializing string to event unsuccessful for invalid eventData', () {
618+
final eventJson = '{"eventName":"analytics_exception",'
619+
'"eventData": "not_valid_event_data"}';
620+
621+
final eventConstructed = Event.fromJson(eventJson);
622+
expect(eventConstructed, isNull);
623+
});
571624
}

0 commit comments

Comments
 (0)