guardrail-dev / guardrail

Principled code generation from OpenAPI specifications
https://guardrail.dev
MIT License
526 stars 133 forks source link

Sharing models between two OpenAPI specs #701

Open dvgica opened 4 years ago

dvgica commented 4 years ago

Scenario

Suppose I have two APIs, each with their own OpenAPI spec apples.yaml and oranges.yaml. They are both fed to Guardrail to generate routes and models in the same project, but using different ScalaServer instances with different packages (say com.example.apples and com.example.oranges).

While they are mostly unrelated APIs and deserve separate specs, they share some definitions, e.g. Foo. In OpenAPI, we can put those definitions in common.yaml and then reference them from the specs using $ref: "common.yaml#/definitions/Foo".

Guardrail handles this scenario by generating both com.example.apples.definitions.Foo and com.example.oranges.definitions.Foo. This works OK-ish until you want to write some common code around Foo, and have to deal with both types. In reality, there should only be one Foo type, used by both APIs.

I haven't found a great way to deal with this, but maybe I'm missing something. What I would like is for there to be a com.example.common.definitions.Foo that can be referenced in either API.

Current Workaround

Right now I'm using a separate ScalaModels instance to build common.yaml, this yields com.example.common.definitions.Foo. I then prevent Guardrail from generating com.example.apples.definitions.Foo and com.example.oranges.definitions.Foo by purposefully breaking the OpenAPI reference: changing $ref: "common.yaml#/definitions/Foo" to $ref: "<common_file>#/definitions/Foo". Guardrail says Unable to find definition for Foo, just inlining and carries on. Finally, I use an extra import on the apples/oranges ScalaServer to add com.example.common.definitions._, providing the single Foo definition to the APIs.

This is a bit of a hack. It requires that I do some text processing on the API specs to replace <common_file> with common.yaml before the specs can be used by other OpenAPI tooling.

Other Options

I considered generating the apples and oranges code in the same package. This would result in a single Foo definition I think. It's a bit messy, since they're unrelated, but more importantly, it doesn't protect against model name conflicts, for example if both APIs define a non-common Bar model, this won't work.

Possible Solution?

What I really want to do is dictate that a definition should be generated in a package separate from the API routes. What if x-jvm-package was allowed on definitions? Then in common.yaml I could have:

definitions:
  Foo:
    x-jvm-package: com.example.common

And it would only be generated once. It would still be up to me to add the extra import to my ScalaServers, or I suppose Guardrail could do this automatically if we wanted to get fancy.

But, I'm very open to other ideas, and also hoping that there's already a way to do this that I missed.

kelnos commented 4 years ago

I think allowing x-jvm-package there would be a good idea. This would also be good motivation to thread fully qualified names through to more places; just referring to it by its FQN would be safer and more reliable than playing import juggling games.