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

add discriminated union trait #30

Merged
merged 13 commits into from
Jan 11, 2022
Merged

add discriminated union trait #30

merged 13 commits into from
Jan 11, 2022

Conversation

lewisjkl
Copy link
Contributor

@lewisjkl lewisjkl commented Jan 6, 2022

No description provided.

@@ -41,3 +41,7 @@ structure UncheckedExample {
@trait(selector: "string")
structure uuidFormat {
}

@trait(selector: "union")
structure discriminated {
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be parameterized by the field name. Personally I've seen "@type", "type", "kind", "node_kind" and so on, so it should probably be up to the API drafter :)

Copy link
Contributor

@Baccata Baccata Jan 6, 2022

Choose a reason for hiding this comment

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

That is indeed correct. See "propertyName" in openapi : https://spec.openapis.org/oas/latest.html#discriminator-object.

I think I've seen type more than anything else, but making it explicit is probably a good call.

So it could be, for instance :

structure discriminated {
  propertyName: String 
} 

We don't care much about the mapping thing from openapi. By default we'll use the labels of union members, and hopefully @jsonName will be made usable on union members and we'll be using that.

val decoders: DecoderMap[S] =
(first +: rest).map { case alt @ Alt(_, instance, inject) =>
val encoder = { (pp: List[PayloadPath.Segment], doc: Document) =>
inject(instance.get.apply(jsonLabel(alt) :: pp, doc))
Copy link
Contributor

Choose a reason for hiding this comment

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

extract the jsonLabel call from here so that it ain't on the hot path.

jsonLabel(alt) -> encoder
}.toMap

Hinted[DocumentDecoder].onHintOpt[Discriminated, S] {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

throw new PayloadError(
PayloadPath(pp.reverse),
"Union",
"Expected more than single-key Json object"
Copy link
Contributor

@Baccata Baccata Jan 7, 2022

Choose a reason for hiding this comment

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

Not necessarily : the alternative could be an empty structure. The if (map.size > 1) is un-necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah good call, I will update and add a test case to make sure that is handled

case DObject(map) if (map.size > 1) =>
map
.get(discriminated.propertyName) match {
case Some(value: Document.DString) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

else {
in.rollbackToken()
in.setMark()
val key = if (in.skipToKey(discriminated.propertyName)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

oh really cool ! I thought we were gonna have to do that by hand, happy it's not the case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I was super happy to learn of setMark + skipToKey + rollbackToMark.. made the backtracking a lot easier

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I was aware of setMark + rollbackToMark. Kudos to @plokhotnyuk, I really appreciate jsoniter's API.

}

val altCache = new PolyFunction[Alt[JCodecMake, Z, *], JCodec] {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@Baccata
Copy link
Contributor

Baccata commented Jan 7, 2022

The implementation of the schematics look good to me. Maybe we could do with some property tests generating random schemas that have the discriminated trait, but I'm okay with us doing that later.

There's two things that I'd like to have before merging :

  1. Some smithy validator that checks that a union with the discriminated trait only has alternatives that are structures. Alternatively, a selector on the discriminated trait that achieves the same thing.
  2. Should we make discriminated a string instead of a struct ? I'm thinking maybe making it a struct is overkill, because besides the name of the discriminator, I don't think any other property is relevant.

@lewisjkl
Copy link
Contributor Author

lewisjkl commented Jan 7, 2022

Some smithy validator that checks that a union with the discriminated trait only has alternatives that are structures. Alternatively, a selector on the discriminated trait that achieves the same thing.

Agreed, thanks for the reminder.

Should we make discriminated a string instead of a struct ? I'm thinking maybe making it a struct is overkill, because besides the name of the discriminator, I don't think any other property is relevant.

Yeah, I can't think of anything else that would ever be specified in there. I will update it to be a string.

Copy link
Contributor

@Baccata Baccata left a comment

Choose a reason for hiding this comment

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

👍 pre-approving (conditioned by the addition of a validator for the discriminator trait)

public List<ValidationEvent> validate(Model model) {
return model.getShapesWithTrait(DiscriminatedUnionTrait.class).stream().flatMap(unionShape -> {
return unionShape.asUnionShape().get().getAllMembers().entrySet().stream().flatMap(entry -> {
System.out.println(model.getShape(entry.getValue().getTarget()));
Copy link
Contributor

Choose a reason for hiding this comment

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

cleanup

if (maybeTarget.isPresent() && maybeTarget.get().isStructureShape()) { // if not defined then shape won't be structure
return Stream.empty();
} else {
return Stream.of(error(entry.getValue(), String.format("Member shape corresponding to key '%s' is not a structure shape", entry.getKey())));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return Stream.of(error(entry.getValue(), String.format("Member shape corresponding to key '%s' is not a structure shape", entry.getKey())));
return Stream.of(error(entry.getValue(), String.format("Target of member '%s' is not a structure shape", entry.getKey())));

Copy link
Contributor

@Baccata Baccata left a comment

Choose a reason for hiding this comment

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

re-approving

Copy link
Member

@kubukoz kubukoz left a comment

Choose a reason for hiding this comment

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

The only thing that I'm kind of missing is an example in a .smithy file, but otherwise it looks good

@lewisjkl
Copy link
Contributor Author

The only thing that I'm kind of missing is an example in a .smithy file, but otherwise it looks good

Good call, I will add that

@lewisjkl
Copy link
Contributor Author

One thing that we will want to look at (possibly in different PR) is the OpenAPI conversion respecting the discriminated union trait. I think there should be a discriminator field in the OpenAPI representation. (https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/)

@lewisjkl lewisjkl marked this pull request as ready for review January 10, 2022 20:08
@Baccata
Copy link
Contributor

Baccata commented Jan 10, 2022

Happy for the openapi conversion to happen in a subsequent PR

@Baccata Baccata merged commit 43f2a1e into main Jan 11, 2022
@Baccata Baccata deleted the discriminated-unions branch January 11, 2022 09:48
@kubukoz
Copy link
Member

kubukoz commented Jan 11, 2022

🎉

Baccata pushed a commit that referenced this pull request May 10, 2022
* add `discriminated` trait, indicating that a union should be encoded as an object with a discriminator field. The field is defined by the string value taken by the trait
* adds a validator to ensure that all members of a discriminated unions target structure shapes 
* updates Document codecs and Json codecs to take it in consideration 
* adds an example
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.

3 participants