Repository: tipsy/javalin
Pull request #1115 (2020-11-15 15:11:56)

Added State Function to JavalinRenderer
The Idea is to add a state function to the JavalinRenderer similar to the one in JavalinVue. This is needed, at least for me, because I need to do some filtering and customization on reports generated using templates depending on the user and a couple of other options.

TareqK: @tipsy  just thought this might also be useful to others

tipsy: Thanks @TareqK ! What people typically do for this is:

```kotlin
ctx.render("/template.ext", baseModel(ctx) + componentModel)
```

Since the `FileRenderer`  already has access to the `Context`, you could also create your own rendering implementation for setting a base model every time.

I sort of like taking the state-function concept from `JavalinVue`, but I'm not sure having implicit values in the template model is good. It is namedspace to `javalin`, but that could also be considered a downsiden (having to prefix each property).

What do you think about getting rid of the default properties and removing the namespace?
TareqK: > What do you think about getting rid of the default properties and removing the namespace?

I think it would be a little inconsistent, since we inject javalin using $javalin in JavalinVue, i think it makes sense to keep it namespaced and with different properties for path/query/state . Also, we can be a little more sure that any data we inject unlikely to conflict with the javalin namespace. 

I think it really comes down to consistency and convenience;  I dont want to explicitly call and inject my state it into the model each time, or implement my own FileRenderer each time i want state injected.
TareqK: @tipsy  so should we merge this or do you want me to change the namespace ? 
tipsy: It's named `$javalin` in `JavalinVue`, because that's the convention Vue uses for extending its prototype. For template models, on the other hand, the convention is to put everything in a `Map<String Any>`. I would rather these two plugins be consistent within their own domain than have a "Javalin-flavored" consistency cross-domain.

Before merging this, I would make the state function return a `Map<String, Object>`, and make the default implementation empty. You should also merge the component-specific state into the base-state, overriding any duplicate keys (and perhaps logging info?).
TareqK: @tipsy  Ive gone ahead and refactored the state map and added overriding, though my Kotlin-Fu is limited so i just converted the model to a mutable map and modified that(but at least, the argument called stays immutable). Ive also added a ```state()``` utility method, similar to ```model()```, and ive added a per-render state to the ```Context``` class. Ive added tests for everything.

I also removed the namespace prefix, so you can access directly using ```queryParams``` and ```pathParams``` and ```state```. ive also made it so that array params are kept as arrays instead of being parsed to a string.

Patch:
diff --git a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/OpenApiPlugin.kt b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/OpenApiPlugin.kt
index 7b40e5ed..69f79e0c 100644
--- a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/OpenApiPlugin.kt
+++ b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/OpenApiPlugin.kt
@@ -12,39 +12,54 @@ import io.javalin.plugin.openapi.ui.SwaggerRenderer
  * Plugin for the the automatic generation of an open api schema.
  * The schema can be extracted with [JavalinOpenApi.createSchema].
  */
-class OpenApiPlugin(private val options: OpenApiOptions) : Plugin, PluginLifecycleInit {
+class OpenApiPlugin(private vararg val options: OpenApiOptions) : Plugin, PluginLifecycleInit {
 
     lateinit var openApiHandler: OpenApiHandler
 
+    private val handlerMap = mutableMapOf<String, OpenApiHandler>()
+
     override fun init(app: Javalin) {
-        openApiHandler = OpenApiHandler(app, options)
+        if (options.isEmpty()) {
+            throw IllegalArgumentException("The OpenApiPlugin requires at least one set of Options")
+        }
+        openApiHandler = OpenApiHandler(app, options.first())
+        options.forEach {
+            it.path?.let { path ->
+                handlerMap.putIfAbsent(path, OpenApiHandler(app, it))
+            }
+        }
     }
 
     override fun apply(app: Javalin) {
         Util.ensureDependencyPresent(OptionalDependency.SWAGGER_CORE)
-        if (options.path == null && (options.swagger != null || options.reDoc != null)) {
-            throw IllegalStateException("""
+        options.forEach { options ->
+            if (options.path == null && (options.swagger != null || options.reDoc != null)) {
+                throw IllegalStateException("""
                 Swagger or ReDoc is enabled, but there is no endpoint available for the OpenApi schema.
                 Please use the `path` option of the OpenApiPlugin to set a schema endpoint.
             """.trimIndent().replace("\n", " "))
-        }
+            }
 
-        options.path?.let { path ->
-            app.get(path, openApiHandler, options.roles)
+            options.path?.let { path ->
+                app.get(path, handlerMap[path]!!, options.roles)
 
-            options.swagger?.let {
-                Util.assertWebjarInstalled(OptionalDependency.SWAGGERUI)
-                app.get(it.path, SwaggerRenderer(options), options.roles)
-            }
+                options.swagger?.let {
+                    Util.assertWebjarInstalled(OptionalDependency.SWAGGERUI)
+                    app.get(it.path, SwaggerRenderer(options), options.roles)
+                }
 
-            options.reDoc?.let {
-                Util.assertWebjarInstalled(OptionalDependency.REDOC)
-                app.get(it.path, ReDocRenderer(options), options.roles)
-            }
+                options.reDoc?.let {
+                    Util.assertWebjarInstalled(OptionalDependency.REDOC)
+                    app.get(it.path, ReDocRenderer(options), options.roles)
+                }
 
-            if (options.swagger != null || options.reDoc != null) {
-                app.config.enableWebjars()
+                if (options.swagger != null || options.reDoc != null) {
+                    app.config.enableWebjars()
+                }
             }
+
+
         }
+
     }
 }
diff --git a/javalin/src/main/java/io/javalin/plugin/rendering/JavalinRenderer.kt b/javalin/src/main/java/io/javalin/plugin/rendering/JavalinRenderer.kt
index 696fa83e..893c6903 100644
--- a/javalin/src/main/java/io/javalin/plugin/rendering/JavalinRenderer.kt
+++ b/javalin/src/main/java/io/javalin/plugin/rendering/JavalinRenderer.kt
@@ -32,12 +32,15 @@ object JavalinRenderer {
         register(JavalinJte, ".jte")
     }
 
+    @JvmField
+    var baseModelFunction: (Context) -> Map<String, Any?> = { mapOf<String, Any>() }
+
     fun renderBasedOnExtension(filePath: String, model: Map<String, Any?>, ctx: Context): String {
         val extension = if (filePath.hasTwoDots) filePath.doubleExtension else filePath.extension
         val renderer = extensions[extension]
                 ?: extensions[filePath.extension] // fallback to a non-double extension
-                ?: throw IllegalArgumentException("No Renderer registered for extension '${filePath.extension}'.")
-        return renderer.render(filePath, model, ctx)
+                ?: throw IllegalArgumentException("No Renderer registered for extension '${filePath.extension}'.") 
+        return renderer.render(filePath, baseModelFunction(ctx) + model, ctx)//overrides the base model
     }
 
     @JvmStatic
diff --git a/pom.xml b/pom.xml
index e5eef4b2..e1bd21d9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,7 +62,7 @@
         <surefire.plugin.version>2.22.2</surefire.plugin.version>
 
         <!-- DEPENDENCIES -->
-        <classgraph.version>4.8.66</classgraph.version>
+        <classgraph.version>4.8.90</classgraph.version>
         <commonmark.version>0.14.0</commonmark.version>
         <freemarker.version>2.3.30</freemarker.version>
         <guava.version>28.2-jre</guava.version>


Test patch:
diff --git a/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldMultiSwagger.kt b/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldMultiSwagger.kt
new file mode 100644
index 00000000..3faf6bbd
--- /dev/null
+++ b/javalin-openapi/src/test/kotlin/io/javalin/examples/HelloWorldMultiSwagger.kt
@@ -0,0 +1,96 @@
+/*
+ * Javalin - https://javalin.io
+ * Copyright 2017 David Åse
+ * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+ */
+package io.javalin.examples
+
+import io.javalin.Javalin
+import io.javalin.http.Context
+import io.javalin.http.InternalServerErrorResponse
+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.ui.ReDocOptions
+import io.javalin.plugin.openapi.ui.RedocOptionsObject
+import io.javalin.plugin.openapi.ui.RedocOptionsTheme
+import io.javalin.plugin.openapi.ui.SwaggerOptions
+import io.swagger.v3.oas.models.info.Info
+
+
+data class Item(val id: Int, val name: String, val stock: Int, val price: Double)
+
+object ItemRepository {
+    private val items = mutableListOf<Item>()
+    fun getItems() = items.toList()
+    fun getItem(id: Int) = items.find { it.id == id }
+    fun addItem(item: Item) = items.add(item)
+}
+
+val getItemsDocs = document().jsonArray<User>("200") { it.description = "Returns all items" }
+
+fun getItemsHandler(ctx: Context) {
+    val users = ItemRepository.getItems()
+    ctx.json(users)
+}
+
+val getItemDocs = document()
+        .pathParam<Int>("id")
+        .json<Item>("200") { it.description = "Returns item with id" }
+        .result<Unit>("404")
+
+fun getItemHandler(ctx: Context) {
+    val itemId = ctx.pathParam<Int>("id").get()
+    val item = ItemRepository.getItem(itemId)
+    if (item == null) {
+        ctx.status(404)
+    } else {
+        ctx.json(item)
+    }
+}
+
+val addItemDocs = document()
+        .body<Item>()
+        .result<Unit>("400")
+        .result<Unit>("204")
+
+fun addItemHandler(ctx: Context) {
+    val item = ctx.body<Item>()
+    ItemRepository.addItem(item)
+    ctx.status(204)
+}
+
+
+fun main() {
+    val app = Javalin.create {
+        val fullOpenApiOptions = OpenApiOptions(Info().version("1.0").description("My Application"))
+                .path("/swagger-docs")
+                .swagger(SwaggerOptions("/swagger").title("My Swagger Documentation"))
+                .reDoc(ReDocOptions("/redoc", RedocOptionsObject(
+                        hideDownloadButton = true,
+                        theme = RedocOptionsTheme(
+                                spacingUnit = 10,
+                                isTypographyOptimizeSpeed = true
+                        )
+                )).title("My ReDoc Documentation"))
+                .defaultDocumentation { documentation -> documentation.json<InternalServerErrorResponse>("500") }
+        val itemsOnly = OpenApiOptions(Info().version("1.0").description("My Application"))
+                .path("/swagger-docs-items")
+                .swagger(SwaggerOptions("/swagger-items").title("My Swagger Documentation -- ITEMS only"))
+                .defaultDocumentation { documentation -> documentation.json<InternalServerErrorResponse>("500") }
+                .ignorePath("/users*")
+        it.registerPlugin(OpenApiPlugin(fullOpenApiOptions, itemsOnly))
+    }
+
+    with(app) {
+        get("/users", documented(getUsersDocs, ::getUsersHandler))
+        get("/users/:id", documented(getUserDocs, ::getUserHandler))
+        post("/users", documented(addUserDocs, ::addUserHandler))
+        get("/items", documented(getItemsDocs, ::getItemsHandler))
+        get("/items/:id", documented(getItemDocs, ::getItemHandler))
+        post("/items", documented(addItemDocs, ::addItemHandler))
+    }
+
+    app.start()
+}
diff --git a/javalin/src/test/java/io/javalin/TestTemplates.kt b/javalin/src/test/java/io/javalin/TestTemplates.kt
index 9e8b5ce2..eb12bfba 100644
--- a/javalin/src/test/java/io/javalin/TestTemplates.kt
+++ b/javalin/src/test/java/io/javalin/TestTemplates.kt
@@ -36,6 +36,8 @@ class TestTemplates {
         setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader")
     }
 
+    private val defaultBaseModel =  model("foo","bar")
+
     @Test
     fun `velocity templates work`() = TestUtil.test { app, http ->
         JavalinVelocity.configure(defaultVelocityEngine)
@@ -195,5 +197,19 @@ class TestTemplates {
         assertThat(http.getBody("/hello")).isEqualTo("<h1>Hello jTwig!</h1>")
     }
 
+    @Test
+    fun `base Model works`() = TestUtil.test { app, http ->
+        JavalinRenderer.baseModelFunction =  {ctx-> defaultBaseModel + mapOf("queryParams" to ctx.queryParamMap(), "pathParams" to ctx.pathParamMap())};
+        app.get("/hello/:pp") { ctx -> ctx.render("/templates/freemarker/test-with-base.ftl", model("message", "Hello Freemarker!")) }
+        assertThat(http.getBody("/hello/world?im=good")).isEqualTo("<h1>good</h1><h2>world</h2><h3>bar</h3>")
+    }
+
+    @Test
+    fun `base model overwrite works`() = TestUtil.test { app, http ->
+        JavalinRenderer.baseModelFunction =  {ctx-> defaultBaseModel + mapOf("queryParams" to ctx.queryParamMap(), "pathParams" to ctx.pathParamMap())};
+        app.get("/hello/:pp") { ctx -> ctx.render("/templates/freemarker/test-with-base.ftl", model("foo", "baz")) }
+        assertThat(http.getBody("/hello/world?im=good")).isEqualTo("<h1>good</h1><h2>world</h2><h3>baz</h3>")
+    }
+
     data class JteTestPage(val hello: String, val world: String)
 }
diff --git a/javalin/src/test/resources/templates/freemarker/test-with-base.ftl b/javalin/src/test/resources/templates/freemarker/test-with-base.ftl
new file mode 100644
index 00000000..1a0d41b7
--- /dev/null
+++ b/javalin/src/test/resources/templates/freemarker/test-with-base.ftl
@@ -0,0 +1,1 @@
+<h1>${queryParams.im[0]}</h1><h2>${pathParams.pp}</h2><h3>${foo}</h3>
\ No newline at end of file