Skip to content

Commit

Permalink
Initial support of multiple examples
Browse files Browse the repository at this point in the history
  • Loading branch information
matwojcik committed Mar 18, 2020
1 parent 4b54b18 commit fb04da9
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 21 deletions.
61 changes: 48 additions & 13 deletions core/src/main/scala/sttp/tapir/EndpointIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import sttp.model.{Method, MultiQueryParams}
import sttp.tapir.Codec.PlainCodec
import sttp.tapir.CodecForMany.PlainCodecForMany
import sttp.tapir.CodecForOptional.PlainCodecForOptional
import sttp.tapir.EndpointIO.Info
import sttp.tapir.EndpointIO.{Example, Info}
import sttp.tapir.internal._
import sttp.tapir.model.ServerRequest
import sttp.tapir.typelevel.{FnComponents, ParamConcat}
Expand Down Expand Up @@ -50,35 +50,50 @@ object EndpointInput {
case class PathCapture[T](codec: PlainCodec[T], name: Option[String], info: EndpointIO.Info[T]) extends Basic[T] {
def name(n: String): PathCapture[T] = copy(name = Some(n))
def description(d: String): PathCapture[T] = copy(info = info.description(d))
def example(t: T): PathCapture[T] = copy(info = info.example(t))
def example(t: T): PathCapture[T] = copy(info = info.examples(t))
def example(example: Example[T]): PathCapture[T] = copy(info = info.examples(List(example)))
def examples(examples: List[Example[T]]): PathCapture[T] = copy(info = info.examples(examples))
def examples(ts: T*): PathCapture[T] = copy(info = info.examples(ts: _*))
def validate(v: Validator[T]): PathCapture[T] = copy(codec = codec.validate(v))
def show: String = addValidatorShow(s"/[${name.getOrElse("")}]", codec.validator)
}

case class PathsCapture(info: EndpointIO.Info[Seq[String]]) extends Basic[Seq[String]] {
def description(d: String): PathsCapture = copy(info = info.description(d))
def example(t: Seq[String]): PathsCapture = copy(info = info.example(t))
def example(t: Seq[String]): PathsCapture = copy(info = info.examples(t))
def example(example: Example[Seq[String]]): PathsCapture = copy(info = info.examples(List(example)))
def examples(examples: List[Example[Seq[String]]]): PathsCapture = copy(info = info.examples(examples))
def examples(ts: Seq[String]*): PathsCapture = copy(info = info.examples(ts: _*))
def show = s"/..."
}

case class Query[T](name: String, codec: PlainCodecForMany[T], info: EndpointIO.Info[T]) extends Basic[T] {
def description(d: String): Query[T] = copy(info = info.description(d))
def example(t: T): Query[T] = copy(info = info.example(t))
def example(t: T): Query[T] = copy(info = info.examples(t))
def example(example: Example[T]): Query[T] = copy(info = info.examples(List(example)))
def examples(examples: List[Example[T]]): Query[T] = copy(info = info.examples(examples))
def examples(ts: T*): Query[T] = copy(info = info.examples(ts: _*))
def deprecated(): Query[T] = copy(info = info.deprecated(true))
def validate(v: Validator[T]): Query[T] = copy(codec = codec.validate(v))
def show: String = addValidatorShow(s"?$name", codec.validator)
}

case class QueryParams(info: EndpointIO.Info[MultiQueryParams]) extends Basic[MultiQueryParams] {
def description(d: String): QueryParams = copy(info = info.description(d))
def example(t: MultiQueryParams): QueryParams = copy(info = info.example(t))
def example(t: MultiQueryParams): QueryParams = copy(info = info.examples(t))
def example(example: Example[MultiQueryParams]): QueryParams = copy(info = info.examples(List(example)))
def examples(examples: List[Example[MultiQueryParams]]): QueryParams = copy(info = info.examples(examples))
def examples(ts: MultiQueryParams*): QueryParams = copy(info = info.examples(ts: _*))
def deprecated(): QueryParams = copy(info = info.deprecated(true))
def show = s"?..."
}

case class Cookie[T](name: String, codec: PlainCodecForOptional[T], info: EndpointIO.Info[T]) extends Basic[T] {
def description(d: String): Cookie[T] = copy(info = info.description(d))
def example(t: T): Cookie[T] = copy(info = info.example(t))
def example(t: T): Cookie[T] = copy(info = info.examples(t))
def example(example: Example[T]): Cookie[T] = copy(info = info.examples(List(example)))
def examples(examples: List[Example[T]]): Cookie[T] = copy(info = info.examples(examples))
def examples(ts: T*): Cookie[T] = copy(info = info.examples(ts: _*))
def deprecated(): Cookie[T] = copy(info = info.deprecated(true))
def validate(v: Validator[T]): Cookie[T] = copy(codec = codec.validate(v))
def show: String = addValidatorShow(s"{cookie $name}", codec.validator)
Expand Down Expand Up @@ -251,7 +266,10 @@ object EndpointIO {

case class Body[T, CF <: CodecFormat, R](codec: CodecForOptional[T, CF, R], info: Info[T]) extends Basic[T] {
def description(d: String): Body[T, CF, R] = copy(info = info.description(d))
def example(t: T): Body[T, CF, R] = copy(info = info.example(t))
def example(t: T): Body[T, CF, R] = copy(info = info.examples(t))
def example(example: Example[T]): Body[T, CF, R] = copy(info = info.examples(List(example)))
def examples(examples: List[Example[T]]): Body[T, CF, R] = copy(info = info.examples(examples))
def examples(ts: T*): Body[T, CF, R] = copy(info = info.examples(ts: _*))
def validate(v: Validator[T]): Body[T, CF, R] = copy(codec = codec.validate(v))
def show: String = addValidatorShow(s"{body as ${codec.meta.format.mediaType}}", codec.validator)
}
Expand All @@ -268,15 +286,21 @@ object EndpointIO {

case class Header[T](name: String, codec: PlainCodecForMany[T], info: Info[T]) extends Basic[T] {
def description(d: String): Header[T] = copy(info = info.description(d))
def example(t: T): Header[T] = copy(info = info.example(t))
def example(t: T): Header[T] = copy(info = info.examples(t))
def example(example: Example[T]): Header[T] = copy(info = info.examples(List(example)))
def examples(examples: List[Example[T]]): Header[T] = copy(info = info.examples(examples))
def examples(ts: T*): Header[T] = copy(info = info.examples(ts: _*))
def deprecated(): Header[T] = copy(info = info.deprecated(true))
def validate(v: Validator[T]): Header[T] = copy(codec = codec.validate(v))
def show: String = addValidatorShow(s"{header $name}", codec.validator)
}

case class Headers(info: Info[Seq[(String, String)]]) extends Basic[Seq[(String, String)]] {
def description(d: String): Headers = copy(info = info.description(d))
def example(t: Seq[(String, String)]): Headers = copy(info = info.example(t))
def example(t: Seq[(String, String)]): Headers = copy(info = info.examples(t))
def example(example: Example[Seq[(String, String)]]): Headers = copy(info = info.examples(List(example)))
def examples(examples: List[Example[Seq[(String, String)]]]): Headers = copy(info = info.examples(examples))
def examples(ts: Seq[(String, String)]*): Headers = copy(info = info.examples(ts: _*))
def show = s"{multiple headers}"
}

Expand Down Expand Up @@ -310,13 +334,21 @@ object EndpointIO {

//

case class Info[T](description: Option[String], example: Option[T], deprecated: Boolean) {
case class Example[T](value: T, name: Option[String], summary: Option[String])

object Example {
def of[T](t: T): Example[T] = Example(t, None, None)
}

case class Info[T](description: Option[String], examples: List[Example[T]], deprecated: Boolean) {
def description(d: String): Info[T] = copy(description = Some(d))
def example(t: T): Info[T] = copy(example = Some(t))
def example: Option[T] = examples.headOption.map(_.value)
def examples(ts: List[Example[T]]): Info[T] = copy(examples = ts)
def examples(ts: T*): Info[T] = examples(ts.map(t => Example.of(t)).toList)
def deprecated(d: Boolean): Info[T] = copy(deprecated = d)
}
object Info {
def empty[T]: Info[T] = Info[T](None, None, deprecated = false)
def empty[T]: Info[T] = Info[T](None, Nil, deprecated = false)
}
}

Expand All @@ -343,7 +375,10 @@ sealed trait StreamingEndpointIO[I, +S] {
object StreamingEndpointIO {
case class Body[S, CF <: CodecFormat](schema: Schema[_], format: CF, info: EndpointIO.Info[String]) extends StreamingEndpointIO[S, S] {
def description(d: String): Body[S, CF] = copy(info = info.description(d))
def example(t: String): Body[S, CF] = copy(info = info.example(t))
def example(t: String): Body[S, CF] = copy(info = info.examples(t))
def example(example: Example[String]): Body[S, CF] = copy(info = info.examples(List(example)))
def examples(examples: List[Example[String]]): Body[S, CF] = copy(info = info.examples(examples))
def examples(ts: String*): Body[S, CF] = copy(info = info.examples(ts: _*))

private[tapir] override def toEndpointIO: EndpointIO.StreamBodyWrapper[S, CF] = EndpointIO.StreamBodyWrapper(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class DecodeInputsTest extends FlatSpec with Matchers {
case class X(v: String)
val e = new RuntimeException()
implicit val xCodec: PlainCodec[X] = Codec.stringPlainCodecUtf8.map(_ => throw e)(_.v)
val input = EndpointInput.Query[X]("x", implicitly, EndpointIO.Info(None, None, deprecated = false))
val input = EndpointInput.Query[X]("x", implicitly, EndpointIO.Info(None, Nil, deprecated = false))

// when & then
DecodeInputs(input, StubDecodeInputContext) shouldBe DecodeInputsResult.Failure(input, DecodeResult.Error("List(v)", e))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package sttp.tapir.docs.openapi

import sttp.tapir.docs.openapi.schema.{ObjectSchemas, TypeData}
import sttp.tapir.openapi.{MediaType => OMediaType, _}
import sttp.tapir.{CodecFormat, Schema => TSchema, _}
import sttp.tapir.{CodecFormat, EndpointIO, Schema => TSchema, _}

import scala.collection.immutable.ListMap

Expand All @@ -18,6 +18,24 @@ private[openapi] class CodecToMediaType(objectSchemas: ObjectSchemas) {
)
}

def apply[T, CF <: CodecFormat](o: CodecForOptional[T, CF, _], examples: List[EndpointIO.Example[T]]): ListMap[String, OMediaType] = {
val (singleExample, multipleExamples) = splitExamples(examples)

ListMap(
o.meta.format.mediaType.noCharset.toString -> OMediaType(
Some(objectSchemas(o)),
singleExample.flatMap(example => exampleValue(o, example.value)),
multipleExamples.zipWithIndex.map{
case (example, i) =>
example.name.getOrElse(s"Example$i") -> Right(Example(summary = example.summary, description = None, value = exampleValue(o, example.value), externalValue = None))
}.toListMap
,
ListMap.empty
)
)
}


def apply[CF <: CodecFormat](
schema: TSchema[_],
format: CF,
Expand All @@ -32,4 +50,34 @@ private[openapi] class CodecToMediaType(objectSchemas: ObjectSchemas) {
)
)
}

def apply[CF <: CodecFormat](
schema: TSchema[_],
format: CF,
examples: List[EndpointIO.Example[String]]
): ListMap[String, OMediaType] = {
val (singleExample, multipleExamples) = splitExamples(examples)

ListMap(
format.mediaType.noCharset.toString -> OMediaType(
Some(objectSchemas(TypeData(schema, Validator.pass))),
singleExample.map(example => ExampleSingleValue(example.value)),
multipleExamples.zipWithIndex.map{
case (example, i) =>
example.name.getOrElse(s"Example$i") -> Right(Example(summary = example.summary, description = None, value = Some(ExampleSingleValue(example.value)), externalValue = None))
}.toListMap,
ListMap.empty
)
)
}

private def splitExamples[T](examples: List[EndpointIO.Example[T]]): (Option[EndpointIO.Example[T]], List[EndpointIO.Example[T]]) =
examples match {
case (example@EndpointIO.Example(_, None, _)) :: Nil =>
(Some(example), Nil)
case Nil =>
(None, Nil)
case examples =>
(None, examples)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ private[openapi] class EndpointToOpenApiPaths(objectSchemas: ObjectSchemas, secu
private def operationInputBody(inputs: Vector[EndpointInput.Basic[_]]) = {
inputs.collect {
case EndpointIO.Body(codec, info) =>
Right(RequestBody(info.description, codecToMediaType(codec, info.example), Some(!codec.meta.schema.isOptional)))
Right(RequestBody(info.description, codecToMediaType(codec, info.examples), Some(!codec.meta.schema.isOptional)))
case EndpointIO.StreamBodyWrapper(StreamingEndpointIO.Body(s, mt, i)) =>
Right(RequestBody(i.description, codecToMediaType(s, mt, i.example), Some(true)))
Right(RequestBody(i.description, codecToMediaType(s, mt, i.examples), Some(true)))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ private[openapi] class EndpointToOperationResponse(objectSchemas: ObjectSchemas,
}

val bodies = outputs.collect {
case EndpointIO.Body(m, i) => (i.description, codecToMediaType(m, i.example))
case EndpointIO.Body(m, i) => (i.description, codecToMediaType(m, i.examples))
case EndpointIO.StreamBodyWrapper(StreamingEndpointIO.Body(s, mt, i)) =>
(i.description, codecToMediaType(s, mt, i.example))
(i.description, codecToMediaType(s, mt, i.examples))
}
val body = bodies.headOption

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
openapi: 3.0.1
info:
title: Entities
version: '1.0'
paths:
/:
post:
operationId: postRoot
parameters:
- name: friends
in: query
required: false
schema:
type: array
items:
type: string
examples:
Matt's friends:
summary: Friends of matt
value:
- amy
- greg
Alans's friends:
summary: Friends of alan
value:
- robert
- ania
- name: current-person
in: query
required: true
schema:
type: string
example: alan
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Person'
examples:
Bob:
value:
name: bob
age: 23
Alan:
summary: Alan summary
value:
name: alan
age: 50
required: true
responses:
'200':
description: ''
components:
schemas:
Person:
required:
- name
- age
type: object
properties:
name:
type: string
age:
type: integer
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
openapi: 3.0.1
info:
title: Entities
version: '1.0'
paths:
/:
post:
operationId: postRoot
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Person'
examples:
Example0:
value:
name: bob
age: 23
Example1:
value:
name: matt
age: 30
required: true
responses:
'200':
description: ''
components:
schemas:
Person:
required:
- name
- age
type: object
properties:
name:
type: string
age:
type: integer
Loading

0 comments on commit fb04da9

Please sign in to comment.