mirror of
https://github.com/jlengrand/ktor.git
synced 2026-03-10 08:31:20 +00:00
Add Pebble templating feature
Signed-off-by: Till Kottmann <me@deletescape.ch>
This commit is contained in:
committed by
Sergey Mashkov
parent
785b8091fd
commit
5a237fd37f
9
ktor-features/ktor-pebble/build.gradle
Normal file
9
ktor-features/ktor-pebble/build.gradle
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
description = ''
|
||||
|
||||
kotlin.sourceSets.jvmMain.dependencies {
|
||||
api group: 'io.pebbletemplates', name: 'pebble', version: '3.1.0'
|
||||
}
|
||||
97
ktor-features/ktor-pebble/jvm/src/io/ktor/pebble/Pebble.kt
Normal file
97
ktor-features/ktor-pebble/jvm/src/io/ktor/pebble/Pebble.kt
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
package io.ktor.pebble
|
||||
|
||||
import com.mitchellbosecke.pebble.*
|
||||
import com.mitchellbosecke.pebble.template.*
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.util.*
|
||||
import io.ktor.util.cio.*
|
||||
import io.ktor.utils.io.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Response content which could be used to respond [ApplicationCalls] like `call.respond(PebbleContent(...))
|
||||
*
|
||||
* @param template name of the template to be resolved by Pebble
|
||||
* @param model which is passed into the template
|
||||
* @param locale which is used to resolve templates (optional)
|
||||
* @param etag value for `E-Tag` header (optional)
|
||||
* @param contentType response's content type which is set to `text/html;charset=utf-8` by default
|
||||
*/
|
||||
class PebbleContent(
|
||||
val template: String,
|
||||
val model: Map<String, Any>,
|
||||
val locale: Locale? = null,
|
||||
val etag: String? = null,
|
||||
val contentType: ContentType = ContentType.Text.Html.withCharset(Charsets.UTF_8)
|
||||
)
|
||||
|
||||
/**
|
||||
* Feature for providing Pebble templates as [PebbleContent]
|
||||
*/
|
||||
class Pebble(private val engine: PebbleEngine) {
|
||||
|
||||
companion object Feature : ApplicationFeature<ApplicationCallPipeline, PebbleEngine.Builder, Pebble> {
|
||||
override val key = AttributeKey<Pebble>("pebble")
|
||||
|
||||
override fun install(pipeline: ApplicationCallPipeline, configure: PebbleEngine.Builder.() -> Unit): Pebble {
|
||||
val builder = PebbleEngine.Builder().apply {
|
||||
configure(this)
|
||||
}
|
||||
val feature = Pebble(builder.build())
|
||||
|
||||
pipeline.sendPipeline.intercept(ApplicationSendPipeline.Transform) { value ->
|
||||
if (value is PebbleContent) {
|
||||
val response = feature.process(value)
|
||||
proceedWith(response)
|
||||
}
|
||||
}
|
||||
|
||||
return feature
|
||||
}
|
||||
}
|
||||
|
||||
private fun process(content: PebbleContent): PebbleOutgoingContent {
|
||||
return PebbleOutgoingContent(
|
||||
engine.getTemplate(content.template),
|
||||
content.model,
|
||||
content.locale,
|
||||
content.etag,
|
||||
content.contentType
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Content which is responded when Pebble templates are rendered.
|
||||
*
|
||||
* @param template the compiled [com.mitchellbosecke.pebble.template.PebbleTemplate] template
|
||||
* @param model the model provided into the template
|
||||
* @param locale which is used to resolve templates (optional)
|
||||
* @param etag value for `E-Tag` header (optional)
|
||||
* @param contentType response's content type which is set to `text/html;charset=utf-8` by default
|
||||
*/
|
||||
private class PebbleOutgoingContent(
|
||||
val template: PebbleTemplate,
|
||||
val model: Map<String, Any>,
|
||||
val locale: Locale?,
|
||||
etag: String?,
|
||||
override val contentType: ContentType
|
||||
) : OutgoingContent.WriteChannelContent() {
|
||||
override suspend fun writeTo(channel: ByteWriteChannel) {
|
||||
channel.bufferedWriter(contentType.charset() ?: Charsets.UTF_8).use {
|
||||
template.evaluate(it, model, locale)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (etag != null)
|
||||
versions += EntityTagVersion(etag)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
package io.ktor.pebble
|
||||
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.response.*
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* Respond with the specified [template] passing [model]
|
||||
*
|
||||
* @see PebbleContent
|
||||
*/
|
||||
suspend fun ApplicationCall.respondTemplate(
|
||||
template: String,
|
||||
model: Map<String, Any>,
|
||||
locale: Locale? = null,
|
||||
etag: String? = null,
|
||||
contentType: ContentType = ContentType.Text.Html.withCharset(
|
||||
Charsets.UTF_8
|
||||
)
|
||||
) = respond(PebbleContent(template, model, locale, etag, contentType))
|
||||
163
ktor-features/ktor-pebble/jvm/test/io/ktor/pebble/PebbleTest.kt
Normal file
163
ktor-features/ktor-pebble/jvm/test/io/ktor/pebble/PebbleTest.kt
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
package io.ktor.pebble
|
||||
|
||||
import com.mitchellbosecke.pebble.loader.*
|
||||
import io.ktor.application.*
|
||||
import io.ktor.features.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.server.testing.*
|
||||
import java.util.zip.*
|
||||
import kotlin.test.*
|
||||
|
||||
class PebbleTest {
|
||||
|
||||
@Test
|
||||
fun `Fill template and expect correct rendered content`() {
|
||||
withTestApplication {
|
||||
application.setupPebble()
|
||||
application.install(ConditionalHeaders)
|
||||
|
||||
application.routing {
|
||||
get("/") {
|
||||
call.respond(PebbleContent(TemplateWithPlaceholder, DefaultModel, etag = "e"))
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/").response.let { response ->
|
||||
|
||||
val lines = response.content!!.lines()
|
||||
|
||||
assertEquals("<p>Hello, 1</p>", lines[0])
|
||||
assertEquals("<h1>Hello World!</h1>", lines[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fill template and expect correct default content type`() {
|
||||
withTestApplication {
|
||||
application.setupPebble()
|
||||
application.install(ConditionalHeaders)
|
||||
|
||||
application.routing {
|
||||
get("/") {
|
||||
call.respond(PebbleContent(TemplateWithPlaceholder, DefaultModel, etag = "e"))
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/").response.let { response ->
|
||||
|
||||
val contentTypeText = assertNotNull(response.headers[HttpHeaders.ContentType])
|
||||
assertEquals(ContentType.Text.Html.withCharset(Charsets.UTF_8), ContentType.parse(contentTypeText))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fill template and expect eTag set when it is provided`() {
|
||||
withTestApplication {
|
||||
application.setupPebble()
|
||||
application.install(ConditionalHeaders)
|
||||
|
||||
application.routing {
|
||||
get("/") {
|
||||
call.respond(PebbleContent(TemplateWithPlaceholder, DefaultModel, etag = "e"))
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("e", handleRequest(HttpMethod.Get, "/").response.headers[HttpHeaders.ETag])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Render empty model`() {
|
||||
withTestApplication {
|
||||
application.setupPebble()
|
||||
application.install(ConditionalHeaders)
|
||||
|
||||
application.routing {
|
||||
get("/") {
|
||||
call.respond(PebbleContent(TemplateWithoutPlaceholder, emptyMap(), etag = "e"))
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/").response.let { response ->
|
||||
|
||||
val lines = response.content!!.lines()
|
||||
|
||||
assertEquals("<p>Hello, Anonymous</p>", lines[0])
|
||||
assertEquals("<h1>Hi!</h1>", lines[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Render template compressed with GZIP`() {
|
||||
withTestApplication {
|
||||
application.setupPebble()
|
||||
application.install(Compression)
|
||||
application.install(ConditionalHeaders)
|
||||
|
||||
application.routing {
|
||||
get("/") {
|
||||
call.respondTemplate(TemplateWithPlaceholder, DefaultModel, etag = "e")
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(HttpMethod.Get, "/") {
|
||||
addHeader(HttpHeaders.AcceptEncoding, "gzip")
|
||||
}.response.let { response ->
|
||||
val content = GZIPInputStream(response.byteContent!!.inputStream()).reader().readText()
|
||||
|
||||
val lines = content.lines()
|
||||
|
||||
assertEquals("<p>Hello, 1</p>", lines[0])
|
||||
assertEquals("<h1>Hello World!</h1>", lines[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Render template without eTag`() {
|
||||
withTestApplication {
|
||||
application.setupPebble()
|
||||
application.install(ConditionalHeaders)
|
||||
|
||||
application.routing {
|
||||
|
||||
get("/") {
|
||||
call.respond(PebbleContent(TemplateWithPlaceholder, DefaultModel))
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(null, handleRequest(HttpMethod.Get, "/").response.headers[HttpHeaders.ETag])
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.setupPebble() {
|
||||
install(Pebble) {
|
||||
loader(StringLoader())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DefaultModel = mapOf("id" to 1, "title" to "Hello World!")
|
||||
|
||||
private val TemplateWithPlaceholder
|
||||
get() = """
|
||||
<p>Hello, {{id}}</p>
|
||||
<h1>{{title}}</h1>
|
||||
""".trimIndent()
|
||||
private val TemplateWithoutPlaceholder: String
|
||||
get() = """
|
||||
<p>Hello, Anonymous</p>
|
||||
<h1>Hi!</h1>
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ include ':ktor-features:ktor-freemarker'
|
||||
include ':ktor-features:ktor-mustache'
|
||||
include ':ktor-features:ktor-thymeleaf'
|
||||
include ':ktor-features:ktor-velocity'
|
||||
include ':ktor-features:ktor-pebble'
|
||||
include ':ktor-features:ktor-gson'
|
||||
include ':ktor-features:ktor-jackson'
|
||||
include ':ktor-features:ktor-metrics'
|
||||
|
||||
Reference in New Issue
Block a user