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

codegen: implement oneOf support #3624

Merged
merged 14 commits into from
Apr 3, 2024

Conversation

hughsimpson
Copy link
Contributor

@hughsimpson hughsimpson commented Mar 19, 2024

Supports oneOf schemas in codegen.

For both circe and jsoniter, when a oneOf schema has no discriminator, on decode we iterate over the subtype schemas in their declared order and stop on the first successfully decoded type. Obviously this means that it's possible to roundtrip an object to a different type if the serialised form can be validly decoded to multiple different subtypes, so I've added in a check that the models can be safely disambiguated this way (the check can be disabled with openapiValidateNonDiscriminatedOneOfs := false in case of false positives or if you want to live like a cowboy). This falling-back method is probably not the most efficient implementation, but writing a more efficient one generically is beyond me.

For jsoniter-scala support this requires the serdes to go into a separate file to the case class defns if oneOf is used (because macros I guess), so this pr splits all json serdes out into a new ${openapiObject}JsonSerdes file; this in turn requires that the routes cannot be declared in the same object, so the codegen will throw an error if any endpoints are declared in the base object and the schemas contain a oneOf defn and the serde lib of choice is jsoniter. ((actually this check turned out to be excessive, and it's perfectly legal to just define the serdes into another file and import them back into the base object.))

Another remaining error condition is if we have an ADT Foo with a discriminator and a subtype Bar, Bar is used at the 'top level' as a req or resp JSON body, and the JSON lib is jsoniter-scala. Comment on throw site, but basically I dunno how to support that .

This also splits the codec definitions out of the object declarations, so that we can retain a bit more consistency between the treatment of the various cases (i.e. fooJsonDecoder/fooJsonCodec will now live on its own inside the ${base}JsonSerdes object, rather than in the Foo companion object). Except for enums when using circe. Serdes are still configured directly on those.

@hughsimpson
Copy link
Contributor Author

hughsimpson commented Mar 19, 2024

It was a bit of a faff getting the libs to respect the discriminator arg (or lack thereof). At this point I kinda want to have a test that the autogenerated serdes are doing the right thing with the ADTs... it's not totally obvious to me how to write that test, other than to have a separate testing submodule for each param combo we want to validate.

val apiTypes = schema.types.collect { case ref: OpenapiSchemaRef if jsonParamRefs.contains(ref.stripped) => ref }
if (apiTypes.nonEmpty)
throw new NotImplementedError(
s"A class cannot be used both in a oneOf at the top level when using jsoniter serdes at $name (problematic children: ${apiTypes})"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the problem here is that if there's a serde declared for a subtype of the ADT, then the discriminator is not added on serialization via the parent trait (I guess because jsoniter sees the subtype serde and goes 'cool I'll use that one to serialise'). I don't know how to get around this. Or to test for it. I might open up a pr to set up some downstream test modules that use the code gerated here to serialise and stuff, 'cause how else do you even test this?

@hughsimpson
Copy link
Contributor Author

Will resolve the merge conflicts in a few mins

| case (Some(v), _) => Some(v)
| case (None, next) =>
| scala.util.Try(next.asInstanceOf[$jsoniterPkgCore.JsonValueCodec[$name]].decodeValue(in, default))
| .fold(_ => { in.rollbackToMark(); None }, Some(_))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This feels clunky and gross and inefficient, but then I suppose that's why it's a good idea to use discriminators.

@hughsimpson
Copy link
Contributor Author

hughsimpson commented Mar 21, 2024

I'm going to stop twiddling with this pr for now. If I need anything else I'll add it to a follow-up... (Excluding any requests for changes, of course)

@hughsimpson
Copy link
Contributor Author

hughsimpson commented Mar 27, 2024

So notably this won't generate discriminators for the adt schemas. I do have a branch that fixes that, but it requires deriving all the schemas semi-automatically; and, furthermore, splitting the output into even more files under some circumstances because it turns out that you hit the javac class size limit in an irreconcilable way when doing that. I finally have it working though and can push it up here, though, if there's interest... Or I can hold it back for another pr.

I'm also looking at fixing, improving and then enabling the plugin tests, but that might take me a while to complete.

@hughsimpson
Copy link
Contributor Author

Any comments welcome! Even if it's just 'sorry, probably won't get to this for a while' or something. This would nearly meet my needs for a 10yo application so would be great to get over the line (I say nearly. Would still need a couple of tiny things around file uploads and whatnot...)

Copy link
Member

@kciesielski kciesielski left a comment

Choose a reason for hiding this comment

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

Thanks! A few comments, but nothing major. Impressive work, especially around handling non-discriminated values. I would be even OK with leaving such cases aside as unsupported (initially), but it's great you've put the effort into supporting it where possible.

@hughsimpson
Copy link
Contributor Author

hughsimpson commented Mar 30, 2024

Thanks! I'll address these comments in a the next couple of days.

Edit: addressed comments

@hughsimpson
Copy link
Contributor Author

@kciesielski @adamw I know you're looking at the netty-cats pr at the moment but do you reckon you can squeeze this one into the next release? Would be awesome for me because I'm actually using this stuff, but also I think probably just quite nice for the codegen in general.

@adamw
Copy link
Member

adamw commented Apr 2, 2024

@hughsimpson sure, once this gets merged we'll do a release :) I'll leave the review/merging to @kciesielski, though

@kciesielski
Copy link
Member

LGTM, happy to merge. Looking forward to #3647 to get some wider test coverage.

@kciesielski kciesielski merged commit 0196f34 into softwaremill:master Apr 3, 2024
23 checks passed
@hughsimpson hughsimpson deleted the oneOf_only branch April 3, 2024 08:04
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.

None yet

3 participants