How define proper content negotiation with oneOfBody & InputStream?

Perhaps I didn’t explain myself well enough.

In tAPIr every EndpointIO is symmetric.
This makes sense since an endpoint can be interpreted either as a server route, or as client request.

So, if I have some type I own, I should be able to both parse it and format it if I want to use it in my endpoint definition.
So far, everything works great, and I even did it with great success before:

The problem I have with the Graphviz type, is that it’s asymmetric, in the sense that I can format it, but usually not parse it.

The Graphviz renderer can output dot format, which it can also parse back into a graph, but also png, svg, and a bunch of other formats, some binary, some textual, which I can’t or won’t parse back into a Graphviz graph object.

Currently, I ended up with something like:

case class Png() extends CodecFormat {
  override val mediaType: MediaType = MediaType.ImagePng
}
case class Svg() extends CodecFormat {
  override val mediaType: MediaType = MediaType("image", "svg+xml")
}

def pngDecodeIsNotSupported(is: InputStream): DecodeResult.Error =
  DecodeResult.Error(
    "data:image/png;base64," + Base64.getEncoder.encodeToString(ByteStreams.toByteArray(is)),
    new UnsupportedOperationException("Parsing a png image back into graphviz is not supported")
  )

def svgDecodeIsNotSupported(is: InputStream): DecodeResult.Error =
  DecodeResult.Error(
    new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8),
    new UnsupportedOperationException("Parsing a svg image back into graphviz is not supported")
  )

def setHeightAndRender(format: Format): GraphvizWithHeight => Renderer = gwh => {
  val gv =
    if (gwh.height <= 0) gwh.graph
    else gwh.graph.height(gwh.height)
  gv.render(format)
}

val rendererToStream: Renderer => InputStream = rr => {
  val os = new ByteArrayOutputStream()
  rr.toOutputStream(os)
  os.toInputStream
}

case class GraphvizWithHeight(graph: Graphviz, height: Int)

val dotBody: EndpointIO.Body[String, GraphvizWithHeight] =
  stringBody.map(s => GraphvizWithHeight(Graphviz.fromString(s), -1))(setHeightAndRender(Format.DOT).andThen(_.toString))

val pngBody: EndpointIO.Body[InputStream, GraphvizWithHeight] =
  inputStreamBody.mapDecode[GraphvizWithHeight](pngDecodeIsNotSupported)(rendererToStream.compose(setHeightAndRender(Format.PNG)))

val svgBody: EndpointIO.Body[InputStream, GraphvizWithHeight] =
  inputStreamBody.mapDecode[GraphvizWithHeight](svgDecodeIsNotSupported)(rendererToStream.compose(setHeightAndRender(Format.SVG)))

type VisualizeAPI = PublicEndpoint[(UUID, Int), MyError, GraphvizWithHeight, Any]
val visualize: VisualizeAPI = endpoint.get
  .in("api" / "visualize" / path[UUID]("id"))
  .in(query[Int]("height").default(768).validate(Validator.min(1)))
  .out(oneOfBody(dotBody, pngBody, svgBody))
  .errorOut(jsonBody[MyError])

A minor issue is the asymmetricity of the API. Obviously I will not parse back a binary png image back to a graph, but always failing feels a bit weird (tried my best to at least make the errors informative).

But the bigger problem is that I don’t have the suitable construct.
With the other issue, I could’ve used stringBodyUtf8AnyFormat, or customCodecJsonBody.
But I can’t figure out what should I use for input stream. There is no inputStreamAnyFormat or customCodecInputStreamBody. Maybe it doesn’t makes sense (because if it does, it means there should be so many other constructs like customCodecByteArrayBody, customCodecByteBufferBody, etc’…

What I get without it, as expected, is just this:
Screen Shot 2023-11-01 at 20.18.58
Screen Shot 2023-11-01 at 20.20.09

wrong mimetype, and input stream bodies override each other.

Hope it’s now clearer.
Thanks!!!