guardrail-dev / guardrail-dev/guardrail
Circe polymorphic support should use inheritance where appropriate
- Dominant language
- Scala
- Stars
- 541
- Forks
- 138
- PR merge metrics
- No merged PRs in 30d
Description
I was working on generating protocol modules for a random project (sadly the API itself isn't well suited to guardrail at present), and found some things that made it a bit difficult to work with.
Consider (`components/schemas`):
```
Response:
type: object
required:
- requestId
properties:
requestId:
type: string
FooResponse:
allOf:
- $ref: "#/components/schemas/Response"
- type: object
required:
- foo
properties:
foo:
type: string
BarResponse:
allOf:
- $ref: "#/components/schemas/Response"
- type: object
required:
- bar
properties:
bar:
type: string
```
guardrail generates three classes:
```
case class Response(requestId: String)
case class FooResponse(requestId: String, foo: String)
case class BarResponse(requestId: String, bar: string)
```
This can be less than ideal. I have an API I'm implementing with a single endpoint, and the request payload has some sort of type field, and then the handler for the request will check the type of request, and then return an instance of `FooResponse` or `BarResponse`, depending on the type. Ideally, I'd want to be able to do this:
```
def handleRequest(input: SomeRequestType): Response = ???
```
However, since `FooResponse` and `BarResponse` are unrelated types, I cannot do this. (And in fact, I never end up using `Response` at all, since it's not useful for anything!) Yes, I could:
1. Push the request type checking out a bit more, and have separate `handleFoo()` and `handleBar()` methods, and put the serialization in a slightly awkward place, or
2. Create a `ResponseWrapper` that could hold either `FooResponse` or `BarResponse`, and write a custom Circe `Encoder` for it, or
3. (What I actually did, for fun) use shapeless's `Coproduct` to create a "parent" type, and then manually write a delegating Circe `Encoder` for it (you also lose the ability to do pattern matching since it's super awkward to do that with `Coproduct`).
But none of that is what I _really_ want to do, and it all makes the code harder to read.
So, ideally I'd like guardrail to create class hierarchies where appropriate for Circe classes. The Jackson generator already does this, and has a few simple rules to ensure that we don't end up with things like multiple inheritance.
The thing that's more difficult with Scala is avoiding case-class inheritance. To make this work, we'd want generated:
```
class Response(requestId: String)
object Response {
// might as well do this so callers don't need to care about the class/case-class distinction
def apply(requestId: String): Response = new Response(requestId)
}
case class FooResponse(requestId: String, foo: String) extends Response(requestId)
case class BarResponse(requestId: String, bar: string) extends Response(requestId)
```
This does become a bit awkward in guardrail because `RenderDTOClass` doesn't know if it's rendering a "leaf" class or not (it knows if it has parents, but not children), so we'd have to figure out how to pass along that bit of information.
Extra credit: guardrail could maybe also figure out that `Response` could be a `trait` since no one needs to instantiate it directly. But that might be impossible to guarantee.
Contributor guide
Assessment
This issue has not been assessed yet.