Repository: tipsy/javalin
Pull request #861 (2020-01-20 19:58:44)

[openapi] Add examples

TobiasWalle: I currently writing the tests and I just realised that the `openApiExamples` are a global variable. I think we could avoid that by saving the variable in the `OpenApiOptions` and pass it down. 
This way we could avoid the `clear` function in the tests and have avoid a potential source of errors.
I will include this in my changes, if it is okay with you @tipsy.
TobiasWalle: > I currently writing the tests and I just realised that the `openApiExamples` are a global variable. I think we could avoid that by saving the variable in the `OpenApiOptions` and pass it down.
> This way we could avoid the `clear` function in the tests and have avoid a potential source of errors.
> I will include this in my changes, if it is okay with you @tipsy.

As this would require some refactoring, I decided to do it another time, in a separate PR.
In the long term, we also should use references for the examples, instead of inlining them everytime (see https://swagger.io/docs/specification/adding-examples/#reuse). I will help with that in another PR, too.

But anyway, I added my changes in #865.
tipsy: > @TobiasWalle As this would require some refactoring, I decided to do it another time, in a separate PR.

I left it out since it's mostly a concern with testing. I don't think it's likely someone will be running two different servers with different and incompatible examples for the same class in the same process.
If you want to clean it up then that's great though, I just chose the easiest way of implementing the feature :)

Patch:
diff --git a/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt b/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt
index df98d3fa..4ecb3a07 100644
--- a/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt
+++ b/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt
@@ -16,6 +16,7 @@ import io.javalin.plugin.openapi.ui.ReDocOptions
 import io.javalin.plugin.openapi.ui.SwaggerOptions
 import io.javalin.plugin.openapi.utils.LazyDefaultValue
 import io.swagger.v3.oas.models.OpenAPI
+import io.swagger.v3.oas.models.examples.Example
 import io.swagger.v3.oas.models.info.Info
 
 class OpenApiOptions constructor(val initialConfigurationCreator: InitialConfigurationCreator) {
@@ -88,6 +89,16 @@ class OpenApiOptions constructor(val initialConfigurationCreator: InitialConfigu
         }
     }
 
+    fun examples(examples: Map<Class<*>, Map<String, Example>>) {
+        openApiExamples = examples.mapValues { it.value.toMutableMap() }.toMutableMap()
+    }
+
+    inline fun <reified T : Any> addExample(name: String, example: Example) = addExample(T::class.java, name, example)
+
+    fun <T> addExample(clazz: Class<T>, name: String, example: Example) {
+        openApiExamples.computeIfAbsent(clazz) { mutableMapOf() }[name] = example
+    }
+
     /**
      * Activate annotation scanning for specific package prefixes.
      * This will search for `OpenApi` annotations which define the `path` and `method` * property.
@@ -134,3 +145,5 @@ interface InitialConfigurationCreator {
 fun InitialConfigurationCreator(createInitialConfiguration: () -> OpenAPI) = object : InitialConfigurationCreator {
     override fun create() = createInitialConfiguration()
 }
+
+internal var openApiExamples = mutableMapOf<Class<*>, MutableMap<String, Example>>()
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedContent.kt b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedContent.kt
index 7e6f41df..41bbb188 100644
--- a/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedContent.kt
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedContent.kt
@@ -2,6 +2,7 @@ package io.javalin.plugin.openapi.dsl
 
 import io.javalin.plugin.openapi.annotations.ComposedType
 import io.javalin.plugin.openapi.annotations.ContentType
+import io.javalin.plugin.openapi.openApiExamples
 import io.javalin.plugin.openapi.external.mediaType
 import io.javalin.plugin.openapi.external.mediaTypeArrayOf
 import io.javalin.plugin.openapi.external.mediaTypeArrayOfRef
@@ -144,6 +145,9 @@ fun Content.applyDocumentedContent(documentedContent: DocumentedContent) {
             else -> mediaTypeRef(documentedContent.fromType, documentedContent.contentType)
         }
     }
+    openApiExamples[documentedContent.fromType]?.let { examples ->
+        get(documentedContent.contentType)?.examples = examples
+    }
 }
 
 fun Components.applyDocumentedContent(documentedResponse: DocumentedContent) {


Test patch:
diff --git a/src/test/java/io/javalin/openapi/TestOpenApiExample.kt b/src/test/java/io/javalin/openapi/TestOpenApiExample.kt
new file mode 100644
index 00000000..7ca045e5
--- /dev/null
+++ b/src/test/java/io/javalin/openapi/TestOpenApiExample.kt
@@ -0,0 +1,177 @@
+package io.javalin.openapi
+
+import io.javalin.Javalin
+import io.javalin.plugin.openapi.JavalinOpenApi
+import io.javalin.plugin.openapi.OpenApiOptions
+import io.javalin.plugin.openapi.OpenApiPlugin
+import io.javalin.plugin.openapi.dsl.document
+import io.javalin.plugin.openapi.dsl.documented
+import io.javalin.plugin.openapi.openApiExamples
+import io.swagger.v3.oas.models.examples.Example
+import io.swagger.v3.oas.models.info.Info
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+
+private data class ExampleUser(val name: String, val address: ExampleAddress)
+private data class ExampleAddress(val street: String, val number: Int)
+
+val examples = mapOf(
+        ExampleUser::class.java to mapOf(
+                "User example" to Example().apply {
+                    summary = "A correctly configured user"
+                    value = ExampleUser("John", ExampleAddress("Some street", 123))
+                },
+                "User example 2" to Example().apply {
+                    summary = "Another correctly configured user"
+                    value = ExampleUser("Dave", ExampleAddress("Some street", 123))
+                }
+        ),
+        ExampleAddress::class.java to mapOf(
+                "Address example" to Example().apply {
+                    summary = "A correctly configured address"
+                    value = ExampleAddress("Some street", 123)
+                }
+        )
+)
+
+val expectedJson = """
+    {
+      "openapi" : "3.0.1",
+      "info" : { },
+      "paths" : {
+        "/user" : {
+          "get" : {
+            "summary" : "Get user",
+            "operationId" : "getUser",
+            "responses" : {
+              "200" : {
+                "description" : "OK",
+                "content" : {
+                  "application/json" : {
+                    "schema" : {
+                      "$ref" : "#/components/schemas/ExampleUser"
+                    },
+                    "examples" : {
+                      "User example" : {
+                        "summary" : "A correctly configured user",
+                        "value" : {
+                          "name" : "John",
+                          "address" : {
+                            "street" : "Some street",
+                            "number" : 123
+                          }
+                        }
+                      },
+                      "User example 2" : {
+                        "summary" : "Another correctly configured user",
+                        "value" : {
+                          "name" : "Dave",
+                          "address" : {
+                            "street" : "Some street",
+                            "number" : 123
+                          }
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "/address" : {
+          "get" : {
+            "summary" : "Get address",
+            "operationId" : "getAddress",
+            "responses" : {
+              "200" : {
+                "description" : "OK",
+                "content" : {
+                  "application/json" : {
+                    "schema" : {
+                      "$ref" : "#/components/schemas/ExampleAddress"
+                    },
+                    "examples" : {
+                      "Address example" : {
+                        "summary" : "A correctly configured address",
+                        "value" : {
+                          "street" : "Some street",
+                          "number" : 123
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      "components" : {
+        "schemas" : {
+          "ExampleAddress" : {
+            "required" : [ "number", "street" ],
+            "type" : "object",
+            "properties" : {
+              "street" : {
+                "type" : "string"
+              },
+              "number" : {
+                "type" : "integer",
+                "format" : "int32"
+              }
+            }
+          },
+          "ExampleUser" : {
+            "required" : [ "address", "name" ],
+            "type" : "object",
+            "properties" : {
+              "name" : {
+                "type" : "string"
+              },
+              "address" : {
+                "$ref" : "#/components/schemas/ExampleAddress"
+              }
+            }
+          }
+        }
+      }
+    }
+""".trimIndent()
+
+class TestOpenApiExample {
+
+    private fun createApp(openApiPlugin: OpenApiPlugin) = Javalin.create {
+        it.registerPlugin(openApiPlugin)
+    }.apply {
+        get("/user", documented(document().result<ExampleUser>("200")) {})
+        get("/address", documented(document().result<ExampleAddress>("200")) {})
+    }
+
+    @Test
+    fun `examples are generated when added in addExample, and class can have multiple examples`() {
+        val app = createApp(OpenApiPlugin(OpenApiOptions(Info()).apply {
+            addExample<ExampleUser>("User example", examples[ExampleUser::class.java]!!["User example"]!!)
+            addExample<ExampleUser>("User example 2", examples[ExampleUser::class.java]!!["User example 2"]!!)
+            addExample<ExampleAddress>("Address example", examples[ExampleAddress::class.java]!!["Address example"]!!)
+        }))
+
+        val openApiJson = JavalinOpenApi.createSchema(app).asJsonString()
+
+        assertThat(openApiJson).isEqualTo(expectedJson.formatJson())
+        openApiExamples.clear()
+    }
+
+    @Test
+    fun `examples are generated when added as a map`() {
+        val app = createApp(OpenApiPlugin(OpenApiOptions(Info()).apply {
+            examples(examples)
+        }))
+
+        val openApiJson = JavalinOpenApi.createSchema(app).asJsonString()
+
+        assertThat(openApiJson).isEqualTo(expectedJson.formatJson())
+        openApiExamples.clear()
+    }
+
+}