Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/docs/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ Options can be customised by providing an implicit instance of `OpenAPIDocsOptio
This option can be applied to all enums in the schema, or only specific ones.
`SObjectInfo` input parameter is a unique identifier of object in the schema.
By default, it is fully qualified name of the class (when using `Validator.derivedEnum` or implicits from `sttp.tapir.codec.enumeratum._`).
* `defaultDecodeFailureOutput`: if an endpoint does not define a Bad Request response in `errorOut`,
tapir will try to guess if decoding of inputs may fail, and add a 400 response if necessary.
You can override this option to customize the mapping of endpoint's inputs to a default error response.
If you'd like to disable this feature, just provide a function that always returns `None`:
```scala
OpenAPIDocsOptions.default.copy(defaultDecodeFailureOutput = _ => None)
```

## OpenAPI Specification Extensions

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package sttp.tapir.docs.openapi

import sttp.model.MediaType
import sttp.tapir.internal._
import sttp.tapir.server.interceptor.decodefailure.DefaultDecodeFailureHandler
import sttp.tapir.{Codec, CodecFormat, EndpointIO, EndpointInput, SchemaType}

private[openapi] object EndpointInputToDecodeFailureOutput {
def defaultBadRequestDescription(input: EndpointInput[_]): Option[String] = {
val fallibleBasicInputs = input.asVectorOfBasicInputs(includeAuth = false).filter(inputMayFailWithBadRequest)

if (fallibleBasicInputs.nonEmpty) Some(badRequestDescription(fallibleBasicInputs))
else None
}

private def inputMayFailWithBadRequest(input: EndpointInput.Basic[_]) = input match {
case EndpointInput.FixedMethod(_, _, _) => false
case EndpointInput.FixedPath(_, _, _) => false
case EndpointIO.Empty(_, _) => false
case EndpointInput.PathCapture(_, codec, _) => decodingMayFail(codec)
case input => decodingMayFail(input.codec) || !input.codec.schema.isOptional
}

private def decodingMayFail[CF <: CodecFormat](codec: Codec[_, _, CF]): Boolean =
codec.format.mediaType != MediaType.TextPlain ||
codec.schema.hasValidation ||
codec.schema.format.nonEmpty ||
codec.schema.schemaType != SchemaType.SString()

private def badRequestDescription(fallibleBasicInputs: Vector[EndpointInput.Basic[_]]) =
fallibleBasicInputs
.map(input => DefaultDecodeFailureHandler.FailureMessages.failureSourceMessage(input))
.distinct
.mkString(", ")
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import scala.collection.immutable.ListMap

private[openapi] class EndpointToOpenAPIPaths(schemas: Schemas, securitySchemes: SecuritySchemes, options: OpenAPIDocsOptions) {
private val codecToMediaType = new CodecToMediaType(schemas)
private val endpointToOperationResponse = new EndpointToOperationResponse(schemas, codecToMediaType)
private val endpointToOperationResponse = new EndpointToOperationResponse(schemas, codecToMediaType, options)

def pathItem(e: Endpoint[_, _, _, _]): (String, PathItem) = {
import Method._
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package sttp.tapir.docs.openapi

import sttp.tapir._
import sttp.tapir.apispec.{ReferenceOr, Schema => ASchema, SchemaType => ASchemaType, _}
import sttp.tapir.apispec.{ReferenceOr, Schema => ASchema, SchemaType => ASchemaType}
import sttp.tapir.docs.apispec.exampleValue
import sttp.tapir.docs.apispec.schema.Schemas
import sttp.tapir.internal._
import sttp.tapir.openapi._

import scala.collection.immutable.ListMap

private[openapi] class EndpointToOperationResponse(objectSchemas: Schemas, codecToMediaType: CodecToMediaType) {
private[openapi] class EndpointToOperationResponse(
objectSchemas: Schemas,
codecToMediaType: CodecToMediaType,
options: OpenAPIDocsOptions
) {
def apply(e: Endpoint[_, _, _, _]): ListMap[ResponsesKey, ReferenceOr[Response]] = {
// There always needs to be at least a 200 empty response
outputToResponses(e.output, ResponsesCodeKey(200), Some(Response.Empty)) ++
inputToDefaultErrorResponses(e.input) ++
outputToResponses(e.errorOutput, ResponsesDefaultKey, None)
}

Expand Down Expand Up @@ -91,4 +96,10 @@ private[openapi] class EndpointToOperationResponse(objectSchemas: Schemas, codec
)
})
}

private def inputToDefaultErrorResponses(input: EndpointInput[_]): ListMap[ResponsesKey, ReferenceOr[Response]] =
options
.defaultDecodeFailureOutput(input)
.map(output => outputToResponses(output, ResponsesDefaultKey, None))
.getOrElse(ListMap())
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package sttp.tapir.docs.openapi

import sttp.model.Method
import sttp.model.{Method, StatusCode}
import sttp.tapir.SchemaType.SObjectInfo
import sttp.tapir.docs.apispec.defaultSchemaName
import sttp.tapir.docs.openapi.EndpointInputToDecodeFailureOutput.defaultBadRequestDescription
import sttp.tapir.{EndpointInput, EndpointOutput, oneOf, oneOfMappingValueMatcher, stringBody}

case class OpenAPIDocsOptions(
operationIdGenerator: (Vector[String], Method) => String,
schemaName: SObjectInfo => String = defaultSchemaName,
referenceEnums: SObjectInfo => Boolean = _ => false
referenceEnums: SObjectInfo => Boolean = _ => false,
defaultDecodeFailureOutput: EndpointInput[_] => Option[EndpointOutput[_]] = OpenAPIDocsOptions.defaultDecodeFailureOutput
)

object OpenAPIDocsOptions {
Expand All @@ -22,5 +25,12 @@ object OpenAPIDocsOptions {
(method.method.toLowerCase +: components.map(_.toLowerCase.capitalize)).mkString
}

val defaultDecodeFailureOutput: EndpointInput[_] => Option[EndpointOutput[_]] = input =>
defaultBadRequestDescription(input).map { description =>
oneOf(
oneOfMappingValueMatcher(StatusCode.BadRequest, stringBody.description(description)) { case _ => true }
)
}

implicit val default: OpenAPIDocsOptions = OpenAPIDocsOptions(defaultOperationIdGenerator)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ paths:
responses:
'200':
description: ''
'400':
description: 'Invalid value for: body'
content:
text/plain:
schema:
type: string
/p2:
get:
operationId: getP2
Expand All @@ -27,6 +33,12 @@ paths:
responses:
'200':
description: ''
'400':
description: 'Invalid value for: body'
content:
text/plain:
schema:
type: string
components:
schemas:
GenericEntity_String:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ paths:
responses:
'200':
description: ''
'400':
description: 'Invalid value for: body'
content:
text/plain:
schema:
type: string
components:
schemas:
Clause:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.0.3
info:
title: Entities
version: '1.0'
paths:
/:
get:
operationId: getRoot
parameters:
- name: amount
in: query
required: true
schema:
type: integer
minimum: 0
responses:
'200':
description: ''
'400':
description: 'Invalid value for: query parameter amount'
content:
text/plain:
schema:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
openapi: 3.0.3
info:
title: Entities
version: '1.0'
paths:
/:
get:
operationId: getRoot
parameters:
- name: amount
in: query
required: true
schema:
type: integer
minimum: 0
responses:
'200':
description: ''
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.0.3
info:
title: Entities
version: '1.0'
paths:
/:
get:
operationId: getRoot
parameters:
- name: amount
in: query
required: true
schema:
type: integer
minimum: 0
responses:
'200':
description: ''
'400':
description: Description defined in options.
content:
text/plain:
schema:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.0.3
info:
title: Entities
version: '1.0'
paths:
/:
get:
operationId: getRoot
parameters:
- name: amount
in: query
required: true
schema:
type: integer
minimum: 0
responses:
'200':
description: ''
'400':
description: User-defined description.
content:
text/plain:
schema:
type: string
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ paths:
responses:
'200':
description: ''
'400':
description: 'Invalid value for: query parameter friends, Invalid value
for: query parameter current-person, Invalid value for: body'
content:
text/plain:
schema:
type: string
components:
schemas:
Person:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ paths:
example: application/json
responses:
'200':
description: ''
description: ''
'400':
description: 'Invalid value for: header Content-Type: application/json'
content:
text/plain:
schema:
type: string
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ paths:
value:
Organization:
name: acme
'400':
description: 'Invalid value for: query parameter current-person, Invalid
value for: body, Invalid value for: header X-Forwarded-User, Invalid value
for: cookie cookie-param'
content:
text/plain:
schema:
type: string
components:
schemas:
Person:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ paths:
responses:
'200':
description: ''
'400':
description: 'Invalid value for: body'
content:
text/plain:
schema:
type: string
components:
schemas:
Person:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ paths:
responses:
'200':
description: ''
'400':
description: 'Invalid value for: body'
content:
text/plain:
schema:
type: string
components:
schemas:
Person:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Book'
'400':
description: 'Invalid value for: body'
content:
text/plain:
schema:
type: string
components:
schemas:
Book:
Expand Down
10 changes: 10 additions & 0 deletions docs/openapi-docs/src/test/resources/expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@
}
}
}
},
"400" : {
"description" : "Invalid value for: query parameter fruit, Invalid value for: query parameter amount",
"content" : {
"text/plain" : {
"schema" : {
"type" : "string"
}
}
}
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions docs/openapi-docs/src/test/resources/expected.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ paths:
text/plain:
schema:
type: string
'400':
description: 'Invalid value for: query parameter fruit, Invalid value for:
query parameter amount'
content:
text/plain:
schema:
type: string
/fruit/{p1}/amount/{p2}:
get:
operationId: getFruitP1AmountP2
Expand Down Expand Up @@ -55,6 +62,13 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/FruitAmount'
'400':
description: 'Invalid value for: path parameter p2, Invalid value for: query
parameter color'
content:
text/plain:
schema:
type: string
/api/delete:
delete:
operationId: deleteApiDelete
Expand Down
Loading