Skip to content

Conversation

@mickjermsurawong-stripe
Copy link
Contributor

  • This PR again should not be merged to master but to pre-autogen branch, similar to Introduce version pinning for autogen #651 (because pre-autogen branch without autogenerated code on top of it doesn't compile)
  • This PR aims to make interface on reading EventData more explicit, having 3 the read mode: safe, best attempt, and raw read on failure.
  • The proposed abstraction here has non-breaking interface getData() to existing integration, and has the same behavior except on failure case.
  • This implementation here now still uses non-pinned version, but the intention here is to release with pinned version; sorry for the inconsistency here.
    r? @ob-stripe @brandur-stripe
    cc @stripe/api-libraries

The build fail now is due to jdk 11 failing to download.

@brandur-stripe
Copy link
Contributor

Nice @mickjermsurawong-stripe! Thanks for taking a stab at this one.

I think this looks like a super plausible approach. The one thing I'd consider changing though is just the public interface that we expose — to my eye, the one in here presents the user with too many options to leaf through.

If we boil everything right down, I think what we want to do is give the user two paths for accessing event data:

  1. A "safe" option that returns either data or nothing if decoding failed.
  2. An unsafe option that returns easy access to data or throws an exception if decoding fails.

The way I'd love to see the interface work is something like this:

maybeData = event.getDataMaybe();
if (maybeData.deserialize()) {
    maybeData.getData().doSomething();
} else {
    maybeData.getRawJson().doSomething();
}

Or for the unsafe variant:

try {
    data = event.getDataMaybe().unwrap();
} catch (EventDataDeserializationException e) {
    e.getRawJson().doSomething();
}

I don't think it's too dissimilar to what you have, but IMO the current interface might be shoehorning too many methods onto Event when creating a proxy like in the above might allow us to expose a cleaner interface. Also, I think that the "helper" methods like canSafeReadData and isReadDataFailure should not be exposed to the user to help streamline the interface a bit. Thoughts?

@mickjermsurawong-stripe
Copy link
Contributor Author

Thank you @brandur-stripe!
I like your approach of making explicit the notion deserialization using the interface that asks users on how to deserialize it instead of mine trying to conceal with vague notions like "TolerantRead", or "safe/bestAttemptRead" and to deserialize EventData on behalf of user, and to maintain/present the state of that work to the user later.

@mickjermsurawong-stripe mickjermsurawong-stripe force-pushed the mickjermsurawong/best-attempt-deserialization branch 2 times, most recently from 396970c to 34ba516 Compare January 18, 2019 03:09
@mickjermsurawong-stripe
Copy link
Contributor Author

mickjermsurawong-stripe commented Jan 18, 2019

ptal @brandur-stripe, the revised interface now will work like this. I try to make the naming for the two options a little consistent.

   StripeObject stripeObject;
    if (eventData.safeDeserialize()) {
      stripeObject = eventData.getObject();
    } else {
      eventData.getObject(); // gives null
      try {
        stripeObject = eventData.forceDeserialize();
      } catch (EventDataDeserializationException e) {
        JsonElement rawJson = e.getRawJson();
        // should we offer functionality below?
        // JsonElement patchedRawJson = patch(rawJson);
        // stripeObject = eventData.deserializeStripeObject(patchedRawJson); 
      }
    }
  • What i'm not sure is if we should expose a way to deserialized the patched raw JSON in the failure mode.
  • I also didn't create a new interface for this new mode -- it is reusing the same .getData() and getObject. But that can be unexpected to user suddenly getting getObject returning null on version mismatch. I'm thinking of going with this, so making all the changes in this PR to the new class, and retain the old EventData object.
public class Event extends ApiResource implements HasId {
  ... 
  String apiVersion;
  Long created;
  @Deprecated
  EventData data;
  EventVersionedData versionedData;
  ...

@mickjermsurawong-stripe mickjermsurawong-stripe force-pushed the mickjermsurawong/best-attempt-deserialization branch from 34ba516 to 94f1103 Compare January 18, 2019 03:46
@mickjermsurawong-stripe mickjermsurawong-stripe force-pushed the mickjermsurawong/best-attempt-deserialization branch from 94f1103 to 9a4ed80 Compare January 22, 2019 16:08
Copy link
Contributor Author

@mickjermsurawong-stripe mickjermsurawong-stripe left a comment

Choose a reason for hiding this comment

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

I decided to do another iteration which gets rid major changes to the serialization logic.
The story here is that we deprecate event.getData().getObject(), in favor of event.getVersionedData().getObject().

  • Introduced a proxy class EventVersionedData so there's no overloading original EventData fully represents to the actual JSON.
  • The versioned data in constructed during a method call, to get properties of the original EventData and also to include api version of Event.apiVersion.
  • The versioned data has interfaces safeDeserialize forceDeserialize for safe/unsafe, and a recovery method retryDeserialize

* Get event data with explicit deserialization support. This object contains superset
* information of {@link EventData}.
*/
public EventVersionedData getVersionedData() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Entry point to the new proxy object that is preferred over the original EventData

* Raw JSON object intended to be deserialized as {@code StripeObject}. The deserialization
* should be deferred to the user. See the now deprecated method {@link EventData#getObject()}.
*/
JsonObject object;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Having JSON makes sure that class-specific deserialization failure is deferred (not during deserializing the outer class Event) and can be handled in the new API EventVersionedData.

Although there is type change here, but the exposed API to client getting StripeObject is still the same via getObject deprecated method below.

StripeObject object = ApiResource.GSON.fromJson(
entry.getValue(), cl != null ? cl : StripeRawJsonObject.class);
eventData.setObject(object);
eventData.setObject(entry.getValue().getAsJsonObject());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

EventData no longer tries to deserialize risky class-specific JSON, and it is exposed in EventVersionedData instead making using of the method below deserializeStripeObject

* event.getApiVersion());
* stripeObject = versionedData.retryDeserialize(compatibleJson);
* }
* }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The flow of how client may use this new object.

@brandur-stripe
Copy link
Contributor

Nice @mickjermsurawong-stripe!

LGTM. The only thing I'd argue for are some name changes:

  • getVersionedData: IMO, this should start with getData* just so that it sorts nicely with the existing method getData (which makes it more discoverable). getDataVersioned is a little awkward, but maybe something like getDataDeserializer, getDataWrapper, or getDataProxy?
  • EventVersionedData: Keep this in line if anything changes above. I'm not sure that "versioned" is important enough to be in the name although I definitely see where you're coming from. (I'd probably go with EventDataDeserializer because this feels more like a deserializer than a data object.)
  • safeDeserialize + forceDeserialize: As above, I think these should start with the same suffix so that they lie next to each other when someone's looking at them in an autocomplete dialog. I'd probably go with deserialize and deserialzeUnsafe, or something along those lines.

@mickjermsurawong-stripe mickjermsurawong-stripe force-pushed the mickjermsurawong/best-attempt-deserialization branch from f5365a9 to 2df017e Compare January 23, 2019 01:12
@mickjermsurawong-stripe mickjermsurawong-stripe force-pushed the mickjermsurawong/best-attempt-deserialization branch from 2df017e to d4af253 Compare January 23, 2019 01:26
@mickjermsurawong-stripe
Copy link
Contributor Author

thank you @brandur-stripe !

  • Renamed the EventVersionedData to EventDataObjectDeserializer. This is because it actually deserializes Event#data#object, and there already exists EventDataDeserializer that does Event#data
  • Renamed safeDeserialize/forceDeserialize/retryDeserialize to deserialize/deserializeUnsafe/deserializeUnsafeWith
  • Updated that raw JSON object returned to the user is a deep copy, since we expose/invite the user to modify/transform.

@brandur-stripe
Copy link
Contributor

Woot. Thanks @mickjermsurawong-stripe!

@ob-stripe Mind taking a look at this one as well? You probably have more informed ideas than I do.

@mickjermsurawong-stripe
Copy link
Contributor Author

mickjermsurawong-stripe commented Jan 24, 2019

Thank you @ob-stripe for the discussion on Slack.

  • I've addressed your concern of exposing GSON-based JSON for user to transform.
    Returns the raw JSON as string for more custom usage.
    For allowing user to transform JSON and retry deserialization, I want to get the benefit of having GSON JsonObject for easy manipulation but also want to hide it away from user for general usage.
    Here, I implemented CompatibilityTransformer essentially a "lambda" that provides the raw JSON and some metadata for user to decide how to transform the JSON.
  Event event = Event.retrieve("evt_123");
  EventDataObjectDeserializer deserializer = event.getDataObjectDeserializer();
  StripeObject stripeObject;
  if (deserializer.deserialize()) {
    stripeObject = deserializer.getObject();
  } else {
    try {
      stripeObject = deserializer.deserializeUnsafe();
    } catch (EventDataDeserializationException e) {
      stripeObject = deserializer.deserializeUnsafeWith(
          new EventDataObjectDeserializer.CompatibilityTransformer() {
            @Override
            public JsonObject transform(JsonObject rawJsonObject,
                                        String apiVersion,
                                        String eventType) {
              if (apiVersion.equals("2018-05-01")) {
              	String fooVal = rawJsonObject.remove("foo");
              	rawJsonObject.add("foo_v2", fooVal);
              }
              if (apiVersion.equals("2018-08-01")) {
              	anotherTransform(rawJsonObject);
              }
              return rawJsonObject;
            }
          });
      );
    }
  }

On a separate note, I know this is essentially looks like pushing users to do their own forward compatibility, but I think it's the fullest support we can provide when we move to pinned-version, given the current state of our backed.
ptal @ob-stripe

@mickjermsurawong-stripe
Copy link
Contributor Author

Addressed the pending concern, and from discussion on slack @ob-stripe agrees with this as a temporary solution for event endpoint integration.
From the team discussion on Monday, this is a valid approach for now. Self-approving here
LGTM

@mickjermsurawong-stripe
Copy link
Contributor Author

Rebasing to autogen/8.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants