Currently misk routes requests to actions based on the combination of path and method. The routing should take into account the content-type and accept headers as well, with the latter also influencing the unmarshalling of requests and marshalling of responses. Proposal is to introduce new @RequestContentType
and @ResponseContentType
annotations. The @RequestContentType
annotation takes a list of media ranges, and the @ResponseContentType
annotation takes a media type. When a request arrives, we find the appropriate action by:
- Checking if the action's method matches the request method
- Checking if the action's path pattern matches the request path
- Checking if the request's Content-Type can satisfy the action's
@RequestContentType
media ranges
- Checking if the action's associated
@ResponseContentType
media type can satisfy the request Accept media ranges
To handle content negotiation based unmarshalling, we remove the existing @XXXRequestBody
annotations and associated ParameterExtracter
s and introduce a new @PostRequestBody
and PostRequestBodyExtractor. The PostRequestBodyExtractor looks at the request Content-Type and finds an associated Unmarshaller for that Content-Type. Like ParameterExtractor
s, Unmarshallers
are created via factories registered with the runtime.
interface Unmarshaller {
fun unmarshal(source: BufferedSource): Any?
interface Factory {
fun create(mediaType: MediaType, parameter: KParameter): Unmarshaller?
}
}
class JsonUnmarshaller(val adapter: JsonAdapter<Any>) : Unmarshaller {
override fun unmarshal(source: BufferedSource) = adapter.fromJson(source)
class Factory @Inject internal constructor(val moshi: Moshi) : Unmarshaller.Factory {
override fun create(mediaType: MediaType, parameter: KParameter): Unmarshaller? {
if (mediaType.type() != "application" || mediaType.subtype() != "json") return null
return JsonUnmarshaller(moshi.adapter<Any>(parameter.type.javaType))
}
}
}
To handle content negotiation based marshaling, we remove the existing @XXXResponseBody
annotations and associated interceptors, and replace them with a single MarshallingInterceptor
. The MarshallingInterceptor
uses the@ResponseContentType
associated with the action to find an Marshaller
for the action’s return type. The Marshaller
s are also created via factories registered with the runtime.
interface Marshaller<in T> {
fun contentType() : MediaType
fun responseBody(o: T) : ResponseBody
interface Factory<out T> {
fun create(mediaType: MediaType, action: Action): Marshaller<T>?
}
}
class JsonMarshaller<T>(val adapter: JsonAdapter<T>) : Marshaller<T> {
override fun contentType(): MediaType = Json.MEDIA_TYPE
override fun responseBody(o: T) = object : ResponseBody {
override fun writeTo(sink: BufferedSink) {
adapter.toJson(sink, o)
}
}
class Factory @Inject internal constructor(val moshi: Moshi) : Marshaller.Factory<Any> {
override fun create(mediaType: MediaType, action: Action): Marshaller<Any>? {
if (mediaType.type() != "application" || mediaType.subtype() != "json") return null
val responseType = when {
action.returnType.rawType == Response::class.java -> {
(action.returnType.type as ParameterizedType).actualTypeArguments[0]
}
else -> action.returnType.type
}
return JsonMarshaller<Any>(moshi.adapter<Any>(responseType))
}
}
}
A single media type can have different marshallers for different response kotlin types; in the example above, a single marshaller supports actions returning Response types and actions returning T types, but you could have separate marshallers for each.
If an action lacks a @RequestContentType
annotation, then it is assumed to have the equivalent of @RequestContentType(“*/*”)
meaning accept all content types. If an action lacks a @ResponseContentType
annotation, then it is required to return a Response and handle all marshaling and header management itself.