Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serializing/deserializing methods for Event instances #251

Conversation

eliasyishak
Copy link
Contributor

@eliasyishak eliasyishak commented Mar 14, 2024

Fixes:

Usage workflow

/// Serializing
final event = Event.analyticsException(
  workflow: 'workflow',
  error: 'error',
  description: 'description',
);

print(event.toJson());
// {
//     "eventName": "analytics_exception",
//     "eventData": {
//         "workflow": "workflow",
//         "error": "error",
//         "description": "description"
//     }
// }

/// Deserializing
final jsonString = '''
{
    "eventName": "analytics_exception",
    "eventData": {
        "workflow": "workflow",
        "error": "error",
        "description": "description"
    }
}
''';

final constructedEvent = Event.fromJson(jsonString);

  • I’ve reviewed the contributor guide and applied the relevant portions to this PR.
Contribution guidelines:

Note that many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.

@eliasyishak eliasyishak marked this pull request as ready for review March 25, 2024 18:04
pkgs/unified_analytics/lib/src/enums.dart Outdated Show resolved Hide resolved
pkgs/unified_analytics/lib/src/enums.dart Outdated Show resolved Hide resolved
/// to ensure that they have the same keys and values.
///
/// Used for the equality operator.
bool _compareEventData(Map<String, Object?> a, Map<String, Object?> b) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of creating our own, can we use something from package:collection? like DeepCollectionEquality?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, this commit has it updated to use DeepCollectionEquality.equals to compare instead

@christopherfujino, i know we discussed trimming down dependencies for this package, do you think this is a valid reason to add another dep?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on offline discussions with @christopherfujino, going to remove the dep on collection and use the same function for comparing two separate maps to reduce dependencies for this package

Comment on lines 785 to 806
final jsonMap = jsonDecode(json) as Map<String, Object?>;

// Ensure the required keys are present
if (!jsonMap.containsKey('eventName') ||
!jsonMap.containsKey('eventData')) {
return null;
}

// Ensure the values for each key is the correct type
final eventName = jsonMap['eventName'];
final eventData = jsonMap['eventData'];
if (eventName is! String || eventData is! Map<String, Object?>) {
return null;
}

// Retrieve the correct DashEvent enum from the provided label
final dashEvent = DashEvent.getDashEventByLabel(eventName);
if (dashEvent == null) {
return null;
}

return Event._(eventName: dashEvent, eventData: eventData);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can simplify this using pattern matching:

if (jsonMap
    case  {
      'eventName': final String eventName, // you might be able to go further here to validate the DashEvent check in the pattern
      'eventData': final Map<String, Object?> eventData,
    }) {
  // Retrieve the correct DashEvent enum from the provided label
  final dashEvent = DashEvent.getDashEventByLabel(eventName);
  if (dashEvent == null) {
    return null;
  }
  return Event._(eventName: dashEvent, eventData: eventData);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice, this is a clever, had to catch up on Craigs pattern matching video to understand the syntax but it looks like this use case is highlighted pretty much exactly!

https://www.youtube.com/watch?v=aLvlqD4QS7Y&ab_channel=Flutter

Thank you for the suggestion! :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Still digging through if this is possible) but i was able to simplify by adding a when clause to the case as the below

  static Event? fromJson(String json) {
    try {
      final jsonMap = jsonDecode(json) as Map<String, Object?>;

      // 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<String, Object?> eventData,
          } when DashEvent.getDashEventByLabel(eventName) != null) {
        return Event._(
          eventName: DashEvent.getDashEventByLabel(eventName)!,
          eventData: eventData,
        );
      }

      return null;
    } on FormatException {
      return null;
    }
  }

But it seems like i would have to make a call to DashEvent.getDashEventByLabel twice, do you know if its possible to assign this variable AND have it get checked within the when clause?

If it's not possible, i'll probably remove the when clause and leave the check within the body of the if

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive by comment--at least with this particular code, I think the solution is to pull DashEvent.getDashEventByLabel(eventName) into a local variable before the if statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't have access to that variable above the if statement though because it gets assigned in the if-case statement.. unless we don't check if it is a string first

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh yeah, I see what you mean. carry on :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I'm now sure if DashEvent.getDashEventByLabel(eventName) can be stored in a variable. Maybe @munificent knows?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't always a great way to interleave a series of pattern matches and guards so that you can incrementally bind stuff after a guard passes. In this case, you could do:

if (jsonMap
    case {
      'eventName': final String eventName,
      'eventData': final Map<String, Object?> eventData,
    }) {
  if (DashEvent.getDashEventByLabel(eventName) case var event?) {
    return Event._(eventName: event, eventData: eventData);
  }
}

Whether you like seeing nested if-case statements like this is a matter of taste. :)

If you wanted to really jam everything into a pattern, you could make DashEvent.getDashEventByLabel an extension on String:

extension DashEventExtensions on String {
  // TODO: Whatever the real return type should be.
  Object? get dashEventForLabel => ...
}

You can call extension getters in object patterns, so that would let you do:

if (jsonMap
    case {
      'eventName': String(dashEventForLabel: var event?),
      'eventData': final Map<String, Object?> eventData,
    }) {
  return Event._(
    eventName: event,
    eventData: eventData,
  );
}

Then the pattern will only match if the map's 'eventName' key is mapped to a string where calling dashEventForLabel on it returns a non-null value. When that happens, the value is bound to event.

But this feels to me like twisting the language out of shape and I wouldn't do it (though it is kind of cool that you can).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've discussed making case an expression instead of directly built into the if-case statement. If we did that, then you could probably chain them using && like:

if (jsonMap
    case {
      'eventName': final String eventName,
      'eventData': final Map<String, Object?> eventData,
    } &&
    DashEvent.getDashEventByLabel(eventName) case var event?) {
  return Event._(eventName: event, eventData: eventData);
}

But that's not in the language now and it's not clear if it's a good idea. It raises lots of tricky questions around the scope of the pattern variables.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@munificent yeah looks like your example in the second comment would be ideal but i can see how it get tricky.. i went with just having a separate conditional within the body of the if-case so it's easy to understand, thank you for the thorough comment!

Comment on lines 208 to 210
for (final tool in DashTool.values) {
if (tool.label == label) return tool;
}
Copy link
Contributor

@kenzieschmoll kenzieschmoll Mar 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use the simplified syntax from above for consistency
DashTool.values.where((t) => t.label == label).firstOrNull;

Copy link
Member

@christopherfujino christopherfujino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@eliasyishak eliasyishak merged commit 13388ea into dart-lang:main Mar 29, 2024
6 checks passed
@eliasyishak eliasyishak deleted the 250-helper-method-to-turn-json-into-an-event-instance branch March 29, 2024 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants