Skip to content

Commit

Permalink
Merge pull request #1078 from softwaremill/interceptors-content-negot…
Browse files Browse the repository at this point in the history
…iation

Interceptors content negotiation
  • Loading branch information
adamw authored Mar 30, 2021
2 parents 90029ef + 8c531b7 commit 0e091f5
Show file tree
Hide file tree
Showing 28 changed files with 809 additions and 533 deletions.
123 changes: 64 additions & 59 deletions apispec/openapi-model/src/main/scala/sttp/tapir/openapi/OpenAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,18 @@ case class Components(

// todo: $ref
case class PathItem(
summary: Option[String],
description: Option[String],
get: Option[Operation],
put: Option[Operation],
post: Option[Operation],
delete: Option[Operation],
options: Option[Operation],
head: Option[Operation],
patch: Option[Operation],
trace: Option[Operation],
servers: List[Server],
parameters: List[ReferenceOr[Parameter]]
summary: Option[String] = None,
description: Option[String] = None,
get: Option[Operation] = None,
put: Option[Operation] = None,
post: Option[Operation] = None,
delete: Option[Operation] = None,
options: Option[Operation] = None,
head: Option[Operation] = None,
patch: Option[Operation] = None,
trace: Option[Operation] = None,
servers: List[Server] = List.empty,
parameters: List[ReferenceOr[Parameter]] = List.empty
) {
def mergeWith(other: PathItem): PathItem = {
PathItem(
Expand All @@ -93,32 +93,32 @@ case class PathItem(

// todo: external docs, callbacks, security
case class Operation(
tags: List[String],
summary: Option[String],
description: Option[String],
tags: List[String] = List.empty,
summary: Option[String] = None,
description: Option[String] = None,
operationId: String,
parameters: List[ReferenceOr[Parameter]],
requestBody: Option[ReferenceOr[RequestBody]],
responses: ListMap[ResponsesKey, ReferenceOr[Response]],
deprecated: Option[Boolean],
security: List[SecurityRequirement],
servers: List[Server]
parameters: List[ReferenceOr[Parameter]] = List.empty,
requestBody: Option[ReferenceOr[RequestBody]] = None,
responses: ListMap[ResponsesKey, ReferenceOr[Response]] = ListMap.empty,
deprecated: Option[Boolean] = None,
security: List[SecurityRequirement] = List.empty,
servers: List[Server] = List.empty
)

case class Parameter(
name: String,
in: ParameterIn.ParameterIn,
description: Option[String],
required: Option[Boolean],
deprecated: Option[Boolean],
allowEmptyValue: Option[Boolean],
style: Option[ParameterStyle.ParameterStyle],
explode: Option[Boolean],
allowReserved: Option[Boolean],
description: Option[String] = None,
required: Option[Boolean] = None,
deprecated: Option[Boolean] = None,
allowEmptyValue: Option[Boolean] = None,
style: Option[ParameterStyle.ParameterStyle] = None,
explode: Option[Boolean] = None,
allowReserved: Option[Boolean] = None,
schema: ReferenceOr[Schema],
example: Option[ExampleValue],
examples: ListMap[String, ReferenceOr[Example]],
content: ListMap[String, MediaType]
example: Option[ExampleValue] = None,
examples: ListMap[String, ReferenceOr[Example]] = ListMap.empty,
content: ListMap[String, MediaType] = ListMap.empty
)

object ParameterIn extends Enumeration {
Expand All @@ -145,47 +145,52 @@ object ParameterStyle extends Enumeration {
case class RequestBody(description: Option[String], content: ListMap[String, MediaType], required: Option[Boolean])

case class MediaType(
schema: Option[ReferenceOr[Schema]],
example: Option[ExampleValue],
examples: ListMap[String, ReferenceOr[Example]],
encoding: ListMap[String, Encoding]
schema: Option[ReferenceOr[Schema]] = None,
example: Option[ExampleValue] = None,
examples: ListMap[String, ReferenceOr[Example]] = ListMap.empty,
encoding: ListMap[String, Encoding] = ListMap.empty
)

case class Encoding(
contentType: Option[String],
headers: ListMap[String, ReferenceOr[Header]],
style: Option[ParameterStyle.ParameterStyle],
explode: Option[Boolean],
allowReserved: Option[Boolean]
contentType: Option[String] = None,
headers: ListMap[String, ReferenceOr[Header]] = ListMap.empty,
style: Option[ParameterStyle.ParameterStyle] = None,
explode: Option[Boolean] = None,
allowReserved: Option[Boolean] = None
)

sealed trait ResponsesKey
case object ResponsesDefaultKey extends ResponsesKey
case class ResponsesCodeKey(code: Int) extends ResponsesKey

// todo: links
case class Response(description: String, headers: ListMap[String, ReferenceOr[Header]], content: ListMap[String, MediaType]) {
case class Response(
description: String,
headers: ListMap[String, ReferenceOr[Header]] = ListMap.empty,
content: ListMap[String, MediaType] = ListMap.empty
)

def merge(other: Response): Response =
Response(
description,
headers ++ other.headers,
content ++ other.content
)
object Response {
val Empty: Response = Response("", ListMap.empty, ListMap.empty)
}

case class Example(summary: Option[String], description: Option[String], value: Option[ExampleValue], externalValue: Option[String])
case class Example(
summary: Option[String] = None,
description: Option[String] = None,
value: Option[ExampleValue] = None,
externalValue: Option[String] = None
)

case class Header(
description: Option[String],
required: Option[Boolean],
deprecated: Option[Boolean],
allowEmptyValue: Option[Boolean],
style: Option[ParameterStyle.ParameterStyle],
explode: Option[Boolean],
allowReserved: Option[Boolean],
schema: Option[ReferenceOr[Schema]],
example: Option[ExampleValue],
examples: ListMap[String, ReferenceOr[Example]],
content: ListMap[String, MediaType]
description: Option[String] = None,
required: Option[Boolean] = None,
deprecated: Option[Boolean] = None,
allowEmptyValue: Option[Boolean] = None,
style: Option[ParameterStyle.ParameterStyle] = None,
explode: Option[Boolean] = None,
allowReserved: Option[Boolean] = None,
schema: Option[ReferenceOr[Schema]] = None,
example: Option[ExampleValue] = None,
examples: ListMap[String, ReferenceOr[Example]] = ListMap.empty,
content: ListMap[String, MediaType] = ListMap.empty
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import org.http4s._
import org.http4s.headers.`Content-Type`
import sttp.capabilities.Streams
import sttp.capabilities.fs2.Fs2Streams
import sttp.model.ResponseMetadata
import sttp.tapir.Codec.PlainCodec
import sttp.tapir.internal.{CombineParams, Params, ParamsAsAny, RichEndpointOutput, SplitParams}
import sttp.tapir.client.AbstractEndpointToClient
import sttp.tapir.internal.{Params, ParamsAsAny, RichEndpointOutput, SplitParams}
import sttp.tapir.{
Codec,
CodecFormat,
Expand All @@ -20,14 +22,14 @@ import sttp.tapir.{
EndpointOutput,
Mapping,
RawBodyType,
StreamBodyIO
StreamBodyIO,
WebSocketBodyOutput
}

import java.io.{ByteArrayInputStream, File, InputStream}
import java.nio.ByteBuffer
import scala.collection.Seq

private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Http4sClientOptions) {
private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Http4sClientOptions) extends AbstractEndpointToClient {

def toHttp4sRequest[I, E, O, R, F[_]: ContextShift: Effect](
e: Endpoint[I, E, O, R],
Expand Down Expand Up @@ -179,11 +181,10 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
val output = if (code.isSuccess) e.output else e.errorOutput

// headers with cookies
val headers: Map[String, List[String]] = response.headers.toList.groupBy(_.name.value).mapValues(_.map(_.value)).toMap
val headers = response.headers.toList.map(h => sttp.model.Header(h.name.toString(), h.value)).toVector

parser(response).map { responseBody =>
val params = getOutputParams(output, responseBody, headers, code, response.status.reason)

val params = getOutputParams(output, responseBody, ResponseMetadata(code, response.status.reason, headers))
params.map(_.asAny).map(p => if (code.isSuccess) Right(p.asInstanceOf[O]) else Left(p.asInstanceOf[E]))
}
}
Expand Down Expand Up @@ -223,65 +224,6 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
}.headOption
}

private def getOutputParams(
output: EndpointOutput[_],
body: => Any,
headers: Map[String, Seq[String]],
code: sttp.model.StatusCode,
statusText: String
): DecodeResult[Params] = {
output match {
case s: EndpointOutput.Single[_] =>
(s match {
case EndpointIO.Body(_, codec, _) => codec.decode(body)
case EndpointIO.StreamBodyWrapper(StreamBodyIO(_, codec, _, _)) => codec.decode(body)
case EndpointOutput.WebSocketBodyWrapper(_) =>
DecodeResult.Error("", new IllegalArgumentException("WebSocket aren't supported yet"))
case EndpointIO.Header(name, codec, _) => codec.decode(headers(name).toList)
case EndpointIO.Headers(codec, _) =>
val h = headers.flatMap { case (k, v) => v.map(sttp.model.Header(k, _)) }.toList
codec.decode(h)
case EndpointOutput.StatusCode(_, codec, _) => codec.decode(code)
case EndpointOutput.FixedStatusCode(_, codec, _) => codec.decode(())
case EndpointIO.FixedHeader(_, codec, _) => codec.decode(())
case EndpointIO.Empty(codec, _) => codec.decode(())
case EndpointOutput.OneOf(mappings, codec) =>
mappings
.find(mapping => mapping.statusCode.isEmpty || mapping.statusCode.contains(code)) match {
case Some(mapping) =>
getOutputParams(mapping.output, body, headers, code, statusText).flatMap(p => codec.decode(p.asAny))
case None =>
DecodeResult.Error(
statusText,
new IllegalArgumentException(s"Cannot find mapping for status code ${code} in outputs $output")
)
}

case EndpointIO.MappedPair(wrapped, codec) =>
getOutputParams(wrapped, body, headers, code, statusText).flatMap(p => codec.decode(p.asAny))
case EndpointOutput.MappedPair(wrapped, codec) =>
getOutputParams(wrapped, body, headers, code, statusText).flatMap(p => codec.decode(p.asAny))

}).map(ParamsAsAny)

case EndpointOutput.Void() => DecodeResult.Error("", new IllegalArgumentException("Cannot convert a void output to a value!"))
case EndpointOutput.Pair(left, right, combine, _) => handleOutputPair(left, right, combine, body, headers, code, statusText)
case EndpointIO.Pair(left, right, combine, _) => handleOutputPair(left, right, combine, body, headers, code, statusText)
}
}

private def handleOutputPair(
left: EndpointOutput[_],
right: EndpointOutput[_],
combine: CombineParams,
body: => Any,
headers: Map[String, Seq[String]],
code: sttp.model.StatusCode,
statusText: String
): DecodeResult[Params] = {
val l = getOutputParams(left, body, headers, code, statusText)
val r = getOutputParams(right, body, headers, code, statusText)
l.flatMap(leftParams => r.map(rightParams => combine(leftParams, rightParams)))
}

override def decodeWebSocketBody(o: WebSocketBodyOutput[_, _, _, _, _], body: Any): DecodeResult[Any] =
DecodeResult.Error("", new IllegalArgumentException("WebSocket aren't supported yet"))
}
Loading

0 comments on commit 0e091f5

Please sign in to comment.