tapir
tapir copied to clipboard
Support for akka style `Map[String, V]` path matchers
I'm currently migrating lot of endpoints from akka-http to tapir. And sometimes we used some really handy akka features in our code. My current struggle is the Map[String, V] handling;
Here are the official akka path matchers list: https://doc.akka.io/docs/akka-http/current/routing-dsl/path-matchers.html#basic-pathmatchers
My not tested but compiling code for the Maps;
def pathFromMapWithDefault[K: PlainCodec, V](inMap: Map[K, V], default: V): EndpointInput[V] =
EndpointInput
.PathCapture(implicitly[PlainCodec[K]], None, EndpointIO.Info.empty)
.map(k => inMap.getOrElse(k, default))(_ => inMap.keys.head)
def pathFromMapWithDefault[K: PlainCodec, V](
inMap: Map[K, V],
default: V,
name: String
): EndpointInput[V] =
EndpointInput
.PathCapture(implicitly[PlainCodec[K]], Some(name), EndpointIO.Info.empty)
.map(k => inMap.getOrElse(k, default))(_ => inMap.keys.head)
def stringMapValidator[V](inMap: Map[String, V]) = {
Validator.Enum[String](inMap.keys.toList, Option(s => Some(s)))
}
def pathFromStringMap[V](inMap: Map[String, V]): EndpointInput[V] =
EndpointInput
.PathCapture(Codec.stringPlainCodecUtf8.validate(stringMapValidator(inMap)), None, EndpointIO.Info.empty)
.map(inMap)(_ => inMap.keys.head)
def pathFromStringMap[V](inMap: Map[String, V], name: String): EndpointInput[V] =
EndpointInput
.PathCapture(Codec.stringPlainCodecUtf8.validate(stringMapValidator(inMap)), Some(name), EndpointIO.Info.empty)
.map(inMap)(_ => inMap.keys.head)
def pathFromMap[K: PlainCodec, V](
inMap: Map[K, V],
encode: Option[Validator.EncodeToRaw[K]]
): EndpointInput[V] =
EndpointInput
.PathCapture(
implicitly[PlainCodec[K]].validate(Validator.Enum[K](inMap.keys.toList, encode)),
None,
EndpointIO.Info.empty
)
.map(inMap)(_ => inMap.keys.head)
def pathFromMap[K: PlainCodec, V](
inMap: Map[K, V],
encode: Option[Validator.EncodeToRaw[K]],
name: String
): EndpointInput[V] =
EndpointInput
.PathCapture(
implicitly[PlainCodec[K]].validate(Validator.Enum[K](inMap.keys.toList, encode)),
Some(name),
EndpointIO.Info.empty
)
.map(inMap)(_ => inMap.keys.head)
My main question is the .map(inMap)(_ => inMap.keys.head) line and how bad this idea is? (If we not count the Map.empty.head case.)
Also can we add this next to the official path[T]?
Interesting, I didn't know about this akka-http feature :)
Let's maybe first look at the first case, leaving validation for later.
So we have:
def pathFromMapWithDefault[K: PlainCodec, V](inMap: Map[K, V], default: V): EndpointInput[V]
decoding is quite straightforward, encoding is more tricky, as we have a value, which we need to convert to a key (this would be used when calling this endpoint as a client).
So we'd need to create a Map[V, K] - also with a default. Question is then, what to do when there's no matching key? We could either have defaultK: K, or throw an exception indicating a programming error - there's currently no other way to signal an encoding error.
The impl would be sth like:
def pathFromMapWithDefault[K: PlainCodec, V](inMap: Map[K, V], default: V): EndpointInput[V] = {
val vToK = inMap.map(_.swap)
path[K].map(k => inMap.getOrElse(k, default))(v => vToK.getOrElse(v, throw new RuntimeException(s"No key for value: $v")))
}
What do you think?
Hmm... If we should map both directions, I would use bimap with some implementations, and probably with an api with implicit converter from map. (So the def would be
def pathFromMapWithDefault[K: PlainCodec, V](inMap: BiMap[K, V], default: V): EndpointInput[V], and you need to import bimap.syntax._ for a pathFromMapWithDefault(Map.empty[A,B].toBimap, B.empty) call option. Also probably would use a default K for the 'withDefault(s)' function.)
BTW: the def pathFromStringMap[V](inMap: Map[String, V]): EndpointInput[V] works as expected; I get
- name: p3
in: path
required: true
schema:
type: string
enum:
- add
- remove
to my output yaml, the APIs runs as expected with it.
Sure, bimap would be an option as well. And yes, your implementation works for server & doc interpreters, but would fail for client interpreter (though you might not need it :) )
Okay! I will draft a PR with this.