Skip to content

#26 Implement ExtraTypesHandler which enables adding custom types support by clients #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
224 changes: 145 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,96 +159,157 @@ class ExampleController @Autowired()(openAPIModelRegistration: OpenAPIModelRegis
}
```

### Adding support for custom types
To add support for custom types (or overwrite handling of any type supported by the library)
one can create a custom `ExtraTypesHandler` and provide it when creating a `Bundle`.

There are multiple ways to do so, the simplest is to use `ExtraTypesHandling.simpleMapping`, for example:
```scala
OpenAPIModelRegistration.ExtraTypesHandling.simpleMapping {
case t if t =:= typeOf[JsValue] =>
val schema = new Schema
schema.setType("string")
schema.setFormat("json")
schema
}
```
This `ExtraTypesHandler` handles `JsValue` by mapping it to simple OpenAPI `string` type with `json` format.

But `ExtraTypesHandler` can also be much more powerful, for example:
```scala
case class CustomClassComplexChild(a: Option[Int])

class CustomClass(val complexChild: CustomClassComplexChild) {
// these won't be included
val meaningOfLife: Int = 42
val alphabetHead: String = "abc"
}

...

val extraTypesHandler: ExtraTypesHandler = (tpe: Type) =>
tpe match {
case t if t =:= typeOf[CustomClass] =>
val childTypesToBeResolvedByTheLibrary = Set(typeOf[CustomClassComplexChild])

val handleFn: HandleFn = (resolvedChildTypes, context) => {
val name = "CustomClass"
val customClassComplexChildResolvedSchema = resolvedChildTypes(typeOf[CustomClassComplexChild])
val schema = new Schema
schema.addProperty("complexChild", customClassComplexChildResolvedSchema)
context.components.addSchemas(name, schema)
val schemaReference = new Schema
schemaReference.set$ref(s"#/components/schemas/$name")
schemaReference
}

(childTypesToBeResolvedByTheLibrary, handleFn)
}
```
This `ExtraTypesHandler` handles `CustomClass`.
`CustomClass` uses `CustomClassComplexChild`,
so the handler requests the library to resolve its type (`childTypesToBeResolvedByTheLibrary`).
This resolved type is available as input to `HandleFn`.
Then, in `handleFn`, the handler creates a `Schema` object for `CustomClass`,
adds it to `Components` so that it can be referenced by name `CustomClass`,
and returns reference to that object.


## Examples

### Simple example for springdoc-openapi-scala-1
Can be found in this repo: [link](examples/springdoc-openapi-scala-1/simple). It generates the following OpenAPI JSON doc:
```json
{
"openapi": "3.0.1",
"info": {
"title": "Example API with springdoc-openapi v1.x",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Generated server url"
}
],
"paths": {
"/api/v1/example/some-endpoint": {
"post": {
"tags": [
"example-controller"
],
"operationId": "someEndpoint",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExampleModelRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExampleModelResponse"
}
}
}
}
"openapi": "3.0.1",
"info": {
"title": "Example API with springdoc-openapi v1.x",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Generated server url"
}
],
"paths": {
"/api/v1/example/some-endpoint": {
"post": {
"tags": [
"example-controller"
],
"operationId": "someEndpoint",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExampleModelRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExampleModelResponse"
}
}
}
}
}
},
"components": {
"schemas": {
"ExampleModelRequest": {
"required": [
"a",
"b"
],
"properties": {
"a": {
"type": "integer",
"format": "int32"
},
"b": {
"type": "string"
},
"c": {
"type": "integer",
"format": "int32"
}
}
},
"ExampleModelResponse": {
"required": [
"d",
"e"
],
"properties": {
"d": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
},
"e": {
"type": "boolean"
}
}
}
}
},
"components": {
"schemas": {
"ExampleModelRequest": {
"required": [
"a",
"b",
"d"
],
"properties": {
"a": {
"type": "integer",
"format": "int32"
},
"b": {
"type": "string"
},
"c": {
"type": "integer",
"format": "int32"
},
"d": {
"type": "string",
"format": "json"
}
}
},
"ExampleModelResponse": {
"required": [
"d",
"e"
],
"properties": {
"d": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
},
"e": {
"type": "boolean"
}
}
}
}
}
}
```

Expand Down Expand Up @@ -318,7 +379,8 @@ Can be found in this repo: [link](examples/springdoc-openapi-scala-2/simple). It
"required": [
"a",
"b",
"d"
"d",
"e"
],
"properties": {
"a": {
Expand All @@ -339,6 +401,10 @@ Can be found in this repo: [link](examples/springdoc-openapi-scala-2/simple). It
"OptionB",
"OptionA"
]
},
"e": {
"type": "string",
"format": "json"
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion examples/springdoc-openapi-scala-1/simple/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ lazy val root = (project in file("."))
libraryDependencies ++= Seq(
"za.co.absa" %% "springdoc-openapi-scala-1" % `springdoc-openapi-scala-1-version`,
"org.springdoc" % "springdoc-openapi-webmvc-core" % "1.7.0",
"org.springframework.boot" % "spring-boot-starter-web" % "2.6.6"
"org.springframework.boot" % "spring-boot-starter-web" % "2.6.6",
"com.fasterxml.jackson.core" % "jackson-databind" % "2.16.1"
),
webappWebInfClasses := true,
inheritJarManifest := true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@

package za.co.absa.springdocopenapiscala.examples.simple

import com.fasterxml.jackson.databind.JsonNode
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.media.Schema
import org.springdoc.core.customizers.OpenApiCustomiser
import org.springframework.context.annotation.{Bean, Configuration}

import za.co.absa.springdocopenapiscala.{Bundle, OpenAPIModelRegistration}

import scala.reflect.runtime.universe.typeOf

@Configuration
class OpenAPIConfiguration {

Expand All @@ -33,7 +36,14 @@ class OpenAPIConfiguration {
.title("Example API with springdoc-openapi v1.x")
.version("1.0.0")
)
)
),
OpenAPIModelRegistration.ExtraTypesHandling.simpleMapping {
case t if t =:= typeOf[JsonNode] =>
val schema = new Schema
schema.setType("string")
schema.setFormat("json")
schema
}
)

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@

package za.co.absa.springdocopenapiscala.examples.simple.model

case class ExampleModelRequest(a: Int, b: String, c: Option[Int])
import com.fasterxml.jackson.databind.JsonNode

case class ExampleModelRequest(a: Int, b: String, c: Option[Int], d: JsonNode)
3 changes: 2 additions & 1 deletion examples/springdoc-openapi-scala-2/simple/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ lazy val root = (project in file("."))
libraryDependencies ++= Seq(
"za.co.absa" %% "springdoc-openapi-scala-2" % `springdoc-openapi-scala-2-version`,
"org.springdoc" % "springdoc-openapi-starter-webmvc-api" % "2.3.0",
"org.springframework.boot" % "spring-boot-starter-web" % "3.2.0"
"org.springframework.boot" % "spring-boot-starter-web" % "3.2.0",
"org.playframework" %% "play-json" % "3.0.1"
),
webappWebInfClasses := true,
inheritJarManifest := true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ package za.co.absa.springdocopenapiscala.examples.simple

import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.media.Schema
import org.springdoc.core.customizers.OpenApiCustomizer
import org.springframework.context.annotation.{Bean, Configuration}
import play.api.libs.json.JsValue

import scala.reflect.runtime.universe.typeOf

import za.co.absa.springdocopenapiscala.{Bundle, OpenAPIModelRegistration}

Expand All @@ -33,7 +37,14 @@ class OpenAPIConfiguration {
.title("Example API with springdoc-openapi v2.x")
.version("1.0.0")
)
)
),
OpenAPIModelRegistration.ExtraTypesHandling.simpleMapping {
case t if t =:= typeOf[JsValue] =>
val schema = new Schema
schema.setType("string")
schema.setFormat("json")
schema
}
)

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package za.co.absa.springdocopenapiscala.examples.simple.model

import play.api.libs.json.JsValue

object SimpleEnums extends Enumeration {

type SimpleEnum = Value
Expand All @@ -24,4 +26,4 @@ object SimpleEnums extends Enumeration {

}

case class ExampleModelRequest(a: Int, b: String, c: Option[Int], d: SimpleEnums.SimpleEnum)
case class ExampleModelRequest(a: Int, b: String, c: Option[Int], d: SimpleEnums.SimpleEnum, e: JsValue)
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,24 @@
package za.co.absa.springdocopenapiscala

import io.swagger.v3.oas.models.{Components, OpenAPI}

import za.co.absa.springdocopenapiscala.OpenAPIModelRegistration.ExtraTypesHandling
import za.co.absa.springdocopenapiscala.SpringdocOpenAPIVersionSpecificTypes._

/**
* Glues all components of `springdoc-openapi-scala` together
* and enables additional customization (for example to set info).
* and enables additional customization (for example to set info or support custom types).
*
* @param extraOpenAPICustomizers additional customizers that are executed after [[OpenAPIScalaCustomizer]]
* @param extraTypesHandler [[ExtraTypesHandling.ExtraTypesHandler]] to be used in model registration
*/
class Bundle(extraOpenAPICustomizers: Seq[OpenApiCustomizer] = Seq.empty) {
class Bundle(
extraOpenAPICustomizers: Seq[OpenApiCustomizer] = Seq.empty,
extraTypesHandler: ExtraTypesHandling.ExtraTypesHandler = ExtraTypesHandling.noExtraHandling
) {

private val components = new Components

val modelRegistration: OpenAPIModelRegistration = new OpenAPIModelRegistration(components)
val modelRegistration: OpenAPIModelRegistration = new OpenAPIModelRegistration(components, extraTypesHandler)

val customizer: OpenApiCustomizer = {
val openAPIScalaCustomizer = new OpenAPIScalaCustomizer(components)
Expand Down
Loading