Skip to content

Commit

Permalink
#26 Implement ExtraTypesHandler which enables adding custom types sup…
Browse files Browse the repository at this point in the history
…port by clients (#32)
  • Loading branch information
jakipatryk committed Jan 11, 2024
1 parent fdfff63 commit 83a862e
Show file tree
Hide file tree
Showing 10 changed files with 412 additions and 105 deletions.
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

0 comments on commit 83a862e

Please sign in to comment.