From 80c95d1219975cbdf3c2efb8c1871953cce4525b Mon Sep 17 00:00:00 2001 From: Bartlomiej Baj Date: Wed, 10 Jan 2024 10:52:51 +0200 Subject: [PATCH 1/6] Fix Enumerations in tests not being private --- .../OpenAPIModelRegistrationSpec.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src/test/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistrationSpec.scala b/library/src/test/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistrationSpec.scala index 3661ee1..539c565 100644 --- a/library/src/test/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistrationSpec.scala +++ b/library/src/test/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistrationSpec.scala @@ -76,13 +76,13 @@ class OpenAPIModelRegistrationSpec extends AnyFlatSpec { b: Map[Int, Arrays] ) - object Things extends Enumeration { + private object Things extends Enumeration { type Thing = Value val Pizza, TV, Radio = Value } - object DifferentThing extends Enumeration { + private object DifferentThing extends Enumeration { type DifferentThing = Value val Cleaner = Value @@ -93,7 +93,7 @@ class OpenAPIModelRegistrationSpec extends AnyFlatSpec { def unrelatedDef: Int = 123 } - object ThingsWithRenaming extends Enumeration { + private object ThingsWithRenaming extends Enumeration { type ThingWithRenaming = Value val Pizza = Value("PIZZA") From 4b5171e610b42a59db43cff32e097408a599ddb8 Mon Sep 17 00:00:00 2001 From: Bartlomiej Baj Date: Wed, 10 Jan 2024 10:54:18 +0200 Subject: [PATCH 2/6] Add ExtraTypesHandling with ExtraTypesHandler to make it possible by clients to handle their custom types --- .../OpenAPIModelRegistration.scala | 103 ++++++++++++-- .../OpenAPIModelRegistrationSpec.scala | 127 +++++++++++++++++- 2 files changed, 218 insertions(+), 12 deletions(-) diff --git a/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala b/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala index f06a162..84c6767 100644 --- a/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala +++ b/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala @@ -25,12 +25,18 @@ import scala.annotation.tailrec import scala.collection.JavaConverters._ import scala.reflect.runtime.universe._ -class OpenAPIModelRegistration(components: Components) { +import OpenAPIModelRegistration._ + +class OpenAPIModelRegistration( + components: Components, + extraTypesHandler: ExtraTypesHandling.ExtraTypesHandler = ExtraTypesHandling.noExtraHandling +) { /** - * Registers given top-level case class. + * Registers given top-level type. * Top-level means one that is directly either response from some endpoint or is in request body. - * All child case classes are registered automatically by this method, they don't need to be registered separately. + * All child case classes of a case class are registered automatically by this method, + * they don't need to be registered separately. * * For example for such model: * {{{ @@ -52,6 +58,9 @@ class OpenAPIModelRegistration(components: Components) { * openAPIModelRegistration.register[Response] * }}} * and `A` and `B` will be registered automatically. + * + * To support custom types not supported by the library, + * construct [[OpenAPIModelRegistration]] with [[OpenAPIModelRegistration.ExtraTypesHandling.ExtraTypesHandler]]. */ def register[T: TypeTag](): Unit = { val tpe = typeOf[T] @@ -61,14 +70,25 @@ class OpenAPIModelRegistration(components: Components) { private case class OpenAPISimpleType(tpe: String, format: Option[String] = None) @tailrec - private def handleType(tpe: Type): Schema[_] = tpe.dealias match { - case t if tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isCaseClass => handleCaseClass(t) - case t if t <:< typeOf[Map[_, _]] => handleMap(t) - case t if t <:< typeOf[Option[_]] => handleType(t.typeArgs.head) - case t if t <:< typeOf[Seq[_]] || t <:< typeOf[Array[_]] => handleSeqLike(t) - case t if t <:< typeOf[Set[_]] => handleSet(t) - case t if t <:< typeOf[Enumeration#Value] => handleEnum(t) - case t => handleSimpleType(t) + private def handleType(tpe: Type): Schema[_] = { + if (extraTypesHandler.isDefinedAt(tpe)) handleExtraTypes(tpe) + else + tpe.dealias match { + case t if tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isCaseClass => handleCaseClass(t) + case t if t <:< typeOf[Map[_, _]] => handleMap(t) + case t if t <:< typeOf[Option[_]] => handleType(t.typeArgs.head) + case t if t <:< typeOf[Seq[_]] || t <:< typeOf[Array[_]] => handleSeqLike(t) + case t if t <:< typeOf[Set[_]] => handleSet(t) + case t if t <:< typeOf[Enumeration#Value] => handleEnum(t) + case t => handleSimpleType(t) + } + } + + private def handleExtraTypes(tpe: Type): Schema[_] = { + val (childTypesToBeResolved, handleFn) = extraTypesHandler(tpe) + val resolvedChildTypes: Map[Type, Schema[_]] = childTypesToBeResolved.map(t => (t, handleType(t))).toMap + val context = RegistrationContext(components) + handleFn(resolvedChildTypes, context) } private def handleCaseClass(tpe: Type): Schema[_] = { @@ -150,3 +170,64 @@ class OpenAPIModelRegistration(components: Components) { } } + +object OpenAPIModelRegistration { + + /** + * Context of model registration. + * Currently contains only `Components` that can be mutated if needed + * (for example to add schema object to be used as schema reference by other types). + */ + case class RegistrationContext(components: Components) + + object ExtraTypesHandling { + + /** + * ExtraTypesHandler is a partial function which can be used to: + * - handle custom types that are not supported by the library. + * - overwrite handling of types that are supported by the library (for example to include some custom format) + * + * It takes [[Type]] as an input, and should produce a pair of : + * - [[ChildTypesToBeResolved]] which is a set of types which the ExtraTypesHandler + * needs to be resolved by the library before it can perform its handling + * - [[HandleFn]] which performs the handling; + * it is a function which takes [[ResolvedChildTypes]] + * (map of [[ChildTypesToBeResolved]] to [[Schema]] resolved by the library) + * and [[RegistrationContext]], and should perform all the handling needed and at the end return [[Schema]] + */ + type ExtraTypesHandler = PartialFunction[ + Type, + (ChildTypesToBeResolved, HandleFn) + ] + + type ChildTypesToBeResolved = Set[Type] + type ResolvedChildTypes = Map[Type, Schema[_]] + type HandleFn = (ResolvedChildTypes, RegistrationContext) => Schema[_] + + /** + * [[ExtraTypesHandler]] which doesn't add/overwrite support for any type. + */ + val noExtraHandling: ExtraTypesHandler = PartialFunction.empty + + /** + * Creates simple [[ExtraTypesHandler]] which doesn't need [[RegistrationContext]] + * nor doesn't have child types to be resolved by the library. + * + * Example: + * {{{ + * ExtraTypesHandling.simpleMapping { + * case t if t =:= typeOf[JsonNode] => + * val schema = new Schema + * schema.setType("string") + * schema.setFormat("json") + * } + * }}} + */ + def simpleMapping(getSchemaOfTypes: PartialFunction[Type, Schema[_]]): ExtraTypesHandler = + getSchemaOfTypes.andThen { schema => + (Set.empty, (_, _) => schema) + } + + } + +} diff --git a/library/src/test/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistrationSpec.scala b/library/src/test/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistrationSpec.scala index 539c565..5712a94 100644 --- a/library/src/test/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistrationSpec.scala +++ b/library/src/test/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistrationSpec.scala @@ -18,13 +18,14 @@ package za.co.absa.springdocopenapiscala import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.media.Schema - import org.scalatest import org.scalatest.flatspec.AnyFlatSpec +import za.co.absa.springdocopenapiscala.OpenAPIModelRegistration.ExtraTypesHandling import java.time.{Instant, LocalDate, LocalDateTime, ZonedDateTime} import java.util.UUID import scala.collection.JavaConverters._ +import scala.reflect.runtime.universe._ class OpenAPIModelRegistrationSpec extends AnyFlatSpec { @@ -110,6 +111,25 @@ class OpenAPIModelRegistrationSpec extends AnyFlatSpec { a: ThingsWithRenaming.ThingWithRenaming ) + private case class CustomClassComplexChild(a: Option[Int]) + + private class CustomClass(val complexChild: CustomClassComplexChild) { + // these won't be included + val meaningOfLife: Int = 42 + val alphabetHead: String = "abc" + } + + private class CustomClassReducingToSimpleType(json: String) { + def complexLogic: String = json.trim + } + + private case class ForCustomHandling( + customClass: CustomClass, + a: String, + b: Int, + customClassReducingToSimpleType: CustomClassReducingToSimpleType + ) + behavior of "register" it should "add schema of a case class that contain fields with simple types to injected components" in { @@ -229,6 +249,111 @@ class OpenAPIModelRegistrationSpec extends AnyFlatSpec { assertEnumIsStringAndHasFollowingOptions(actualSchemas, "EnumsWithRenaming.a", Set("PIZZA", "TV", "RADIO")) } + it should "use provided ExtraTypesHandler to handle custom types" in { + import ExtraTypesHandling._ + + val components = new Components + val extraTypesHandler: ExtraTypesHandler = (tpe: Type) => + tpe match { + case t if t =:= typeOf[CustomClass] => + val childTypesToBeResolvedByTheLibrary = Set(typeOf[CustomClassComplexChild]) + val handleFn: HandleFn = (resolvedChildTypesSchemas, context) => { + val name = "CustomClass" + val customClassComplexChildResolvedSchema = resolvedChildTypesSchemas(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) + + case t if t =:= typeOf[CustomClassReducingToSimpleType] => + val handleFn: HandleFn = (_, _) => { + val schema = new Schema + schema.setType("string") + schema.setFormat("json") + schema + } + (Set.empty, handleFn) + } + val openAPIModelRegistration = new OpenAPIModelRegistration(components, extraTypesHandler) + + openAPIModelRegistration.register[ForCustomHandling]() + + val actualSchemas = components.getSchemas + + assertRefIsAsExpected( + actualSchemas, + "ForCustomHandling.customClass", + "#/components/schemas/CustomClass" + ) + assertTypeAndFormatAreAsExpected( + actualSchemas, + "ForCustomHandling.a", + "string", + None + ) + assertTypeAndFormatAreAsExpected( + actualSchemas, + "ForCustomHandling.b", + "integer", + Some("int32") + ) + assertTypeAndFormatAreAsExpected( + actualSchemas, + "ForCustomHandling.customClassReducingToSimpleType", + "string", + Some("json") + ) + assertRefIsAsExpected( + actualSchemas, + "CustomClass.complexChild", + "#/components/schemas/CustomClassComplexChild" + ) + assertPredicateForPath( + actualSchemas, + "CustomClass", + schema => schema.getProperties.size == 1 + ) + } + + it should + "use provided ExtraTypesHandler to overwrite common types that would normally be handled by the library" in { + val components = new Components + val extraTypesHandler = ExtraTypesHandling.simpleMapping { + case t if t =:= typeOf[String] => + val schema = new Schema + schema.setType("string") + schema.setFormat("my-custom-format") + schema + } + val openAPIModelRegistration = new OpenAPIModelRegistration(components, extraTypesHandler) + + openAPIModelRegistration.register[SimpleTypesMaybeInOption]() + + val actualSchemas = components.getSchemas + + assertTypeAndFormatAreAsExpected( + actualSchemas, + "SimpleTypesMaybeInOption.a", + "string", + Some("my-custom-format") + ) + assertTypeAndFormatAreAsExpected( + actualSchemas, + "SimpleTypesMaybeInOption.b", + "string", + Some("my-custom-format") + ) + + // these are to make sure that types not handled by `extraTypesHandler` still work as expected + assertTypeAndFormatAreAsExpected(actualSchemas, "SimpleTypesMaybeInOption.c", "integer", Some("int32")) + assertTypeAndFormatAreAsExpected(actualSchemas, "SimpleTypesMaybeInOption.d", "string", Some("date-time")) + assertTypeAndFormatAreAsExpected(actualSchemas, "SimpleTypesMaybeInOption.e", "string", Some("date-time")) + } + private def assertTypeAndFormatAreAsExpected( actualSchemas: java.util.Map[String, Schema[_]], fieldPath: String, From d8db455ec742c4909c59e41a7c92819635908eaa Mon Sep 17 00:00:00 2001 From: Bartlomiej Baj Date: Wed, 10 Jan 2024 10:57:37 +0200 Subject: [PATCH 3/6] Add ExtraTypesHandler to Bundle --- .../za/co/absa/springdocopenapiscala/Bundle.scala | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/library/src/main/scala/za/co/absa/springdocopenapiscala/Bundle.scala b/library/src/main/scala/za/co/absa/springdocopenapiscala/Bundle.scala index b916ff5..5be841e 100644 --- a/library/src/main/scala/za/co/absa/springdocopenapiscala/Bundle.scala +++ b/library/src/main/scala/za/co/absa/springdocopenapiscala/Bundle.scala @@ -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 [[OpenAPISScalaCustomizer]] + * @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 openAPISScalaCustomizer = new OpenAPISScalaCustomizer(components) From ea9551c18455ba3278386bd4a766c7a9f95dca48 Mon Sep 17 00:00:00 2001 From: Bartlomiej Baj Date: Wed, 10 Jan 2024 16:33:25 +0200 Subject: [PATCH 4/6] Add ExtraTypesHandler to examples --- README.md | 168 ++++++++++-------- .../simple/build.sbt | 3 +- .../simple/OpenAPIConfiguration.scala | 14 +- .../simple/model/ExampleModelRequest.scala | 4 +- .../simple/build.sbt | 3 +- .../simple/OpenAPIConfiguration.scala | 13 +- .../simple/model/ExampleModelRequest.scala | 4 +- .../OpenAPIModelRegistration.scala | 1 + 8 files changed, 124 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 3d1e213..83e8122 100644 --- a/README.md +++ b/README.md @@ -165,90 +165,95 @@ class ExampleController @Autowired()(openAPIModelRegistration: OpenAPIModelRegis 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" + } } + } } + } } ``` @@ -318,7 +323,8 @@ Can be found in this repo: [link](examples/springdoc-openapi-scala-2/simple). It "required": [ "a", "b", - "d" + "d", + "e" ], "properties": { "a": { @@ -339,6 +345,10 @@ Can be found in this repo: [link](examples/springdoc-openapi-scala-2/simple). It "OptionB", "OptionA" ] + }, + "e": { + "type": "string", + "format": "json" } } }, diff --git a/examples/springdoc-openapi-scala-1/simple/build.sbt b/examples/springdoc-openapi-scala-1/simple/build.sbt index 32352bc..52f6324 100644 --- a/examples/springdoc-openapi-scala-1/simple/build.sbt +++ b/examples/springdoc-openapi-scala-1/simple/build.sbt @@ -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 diff --git a/examples/springdoc-openapi-scala-1/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/OpenAPIConfiguration.scala b/examples/springdoc-openapi-scala-1/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/OpenAPIConfiguration.scala index 324a46c..2d3d8ec 100644 --- a/examples/springdoc-openapi-scala-1/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/OpenAPIConfiguration.scala +++ b/examples/springdoc-openapi-scala-1/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/OpenAPIConfiguration.scala @@ -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 { @@ -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 diff --git a/examples/springdoc-openapi-scala-1/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/model/ExampleModelRequest.scala b/examples/springdoc-openapi-scala-1/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/model/ExampleModelRequest.scala index 952f663..44f0146 100644 --- a/examples/springdoc-openapi-scala-1/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/model/ExampleModelRequest.scala +++ b/examples/springdoc-openapi-scala-1/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/model/ExampleModelRequest.scala @@ -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) diff --git a/examples/springdoc-openapi-scala-2/simple/build.sbt b/examples/springdoc-openapi-scala-2/simple/build.sbt index b37d399..34b64b1 100644 --- a/examples/springdoc-openapi-scala-2/simple/build.sbt +++ b/examples/springdoc-openapi-scala-2/simple/build.sbt @@ -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 diff --git a/examples/springdoc-openapi-scala-2/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/OpenAPIConfiguration.scala b/examples/springdoc-openapi-scala-2/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/OpenAPIConfiguration.scala index 3e25243..c62645b 100644 --- a/examples/springdoc-openapi-scala-2/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/OpenAPIConfiguration.scala +++ b/examples/springdoc-openapi-scala-2/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/OpenAPIConfiguration.scala @@ -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} @@ -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 diff --git a/examples/springdoc-openapi-scala-2/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/model/ExampleModelRequest.scala b/examples/springdoc-openapi-scala-2/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/model/ExampleModelRequest.scala index 50cb89d..5679549 100644 --- a/examples/springdoc-openapi-scala-2/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/model/ExampleModelRequest.scala +++ b/examples/springdoc-openapi-scala-2/simple/src/main/scala/za/co/absa/springdocopenapiscala/examples/simple/model/ExampleModelRequest.scala @@ -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 @@ -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) diff --git a/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala b/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala index 2bad3dd..40f1b26 100644 --- a/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala +++ b/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala @@ -230,6 +230,7 @@ object OpenAPIModelRegistration { * val schema = new Schema * schema.setType("string") * schema.setFormat("json") + * schema * } * }}} */ From eca13eba469da20264452d3188e0805d0e2461f9 Mon Sep 17 00:00:00 2001 From: Bartlomiej Baj Date: Wed, 10 Jan 2024 16:58:34 +0200 Subject: [PATCH 5/6] Update docs --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/README.md b/README.md index 83e8122..9ea4408 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,62 @@ 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 From d535aa995d2cad62781adf02693b1d4c388d8946 Mon Sep 17 00:00:00 2001 From: jakipatryk Date: Wed, 10 Jan 2024 17:39:32 +0200 Subject: [PATCH 6/6] Apply code review suggestion Co-authored-by: Daniel K --- .../absa/springdocopenapiscala/OpenAPIModelRegistration.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala b/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala index 40f1b26..e4ae591 100644 --- a/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala +++ b/library/src/main/scala/za/co/absa/springdocopenapiscala/OpenAPIModelRegistration.scala @@ -197,7 +197,7 @@ object OpenAPIModelRegistration { * - handle custom types that are not supported by the library. * - overwrite handling of types that are supported by the library (for example to include some custom format) * - * It takes [[Type]] as an input, and should produce a pair of : + * It takes [[Type]] as an input, and should produce a pair of: * - [[ChildTypesToBeResolved]] which is a set of types which the ExtraTypesHandler * needs to be resolved by the library before it can perform its handling * - [[HandleFn]] which performs the handling;