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:
wrong mimetype, and input stream bodies override each other.
Hope it’s now clearer.
Thanks!!!