Не подтверждена Коммит 82381e87 создал по автору Erdle, Tobias's avatar Erdle, Tobias Зафиксировано автором Sergey Mashkov
Просмотр файлов

Add Mustache templating feature (#713)

владелец b93daadb
description = ''
dependencies {
compile group: 'com.github.spullara.mustache.java', name: 'compiler', version: '0.9.5'
}
package io.ktor.mustache
import com.github.mustachejava.DefaultMustacheFactory
import com.github.mustachejava.MustacheFactory
import io.ktor.application.ApplicationCallPipeline
import io.ktor.application.ApplicationFeature
import io.ktor.http.ContentType
import io.ktor.http.charset
import io.ktor.http.content.EntityTagVersion
import io.ktor.http.content.OutgoingContent
import io.ktor.http.content.versions
import io.ktor.http.withCharset
import io.ktor.response.ApplicationSendPipeline
import io.ktor.util.AttributeKey
import io.ktor.util.cio.bufferedWriter
import kotlinx.coroutines.io.ByteWriteChannel
/**
* Response content which could be used to respond [ApplicationCalls] like `call.respond(MustacheContent(...))
*
* @param template name of the template to be resolved by Mustache
* @param model which is passed into the template
* @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 MustacheContent(
val template: String,
val model: Any?,
val etag: String? = null,
val contentType: ContentType = ContentType.Text.Html.withCharset(Charsets.UTF_8)
)
/**
* Feature for providing Mustache templates as [MustacheContent]
*/
class Mustache(private val mustacheFactory: MustacheFactory) {
companion object Feature : ApplicationFeature<ApplicationCallPipeline, MustacheFactory, Mustache> {
override val key = AttributeKey<Mustache>("mustache")
override fun install(pipeline: ApplicationCallPipeline, configure: MustacheFactory.() -> Unit): Mustache {
val mustacheFactory = DefaultMustacheFactory().apply(configure)
val feature = Mustache(mustacheFactory)
pipeline.sendPipeline.intercept(ApplicationSendPipeline.Transform) { value ->
if (value is MustacheContent) {
val response = feature.process(value)
proceedWith(response)
}
}
return feature
}
}
private fun process(content: MustacheContent): MustacheOutgoingContent {
return MustacheOutgoingContent(
mustacheFactory.compile(content.template),
content.model,
content.etag,
content.contentType
)
}
/**
* Content which is responded when Mustache templates are rendered.
*
* @param template the compiled [com.github.mustachejava.Mustache] template
* @param model the model provided into the template
* @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 MustacheOutgoingContent(
val template: com.github.mustachejava.Mustache,
val model: Any?,
etag: String?,
override val contentType: ContentType
) : OutgoingContent.WriteChannelContent() {
override suspend fun writeTo(channel: ByteWriteChannel) {
channel.bufferedWriter(contentType.charset() ?: Charsets.UTF_8).use {
template.execute(it, model)
}
}
init {
if (etag != null)
versions += EntityTagVersion(etag)
}
}
}
package io.ktor.mustache
import io.ktor.application.ApplicationCall
import io.ktor.http.ContentType
import io.ktor.http.withCharset
import io.ktor.response.respond
/**
* Respond with the specified [template] passing [model]
*
* @see MustacheContent
*/
suspend fun ApplicationCall.respondTemplate(
template: String,
model: Any? = null,
etag: String? = null,
contentType: ContentType = ContentType.Text.Html.withCharset(
Charsets.UTF_8
)
) = respond(MustacheContent(template, model, etag, contentType))
package io.ktor.mustache
import com.github.mustachejava.DefaultMustacheFactory
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.Compression
import io.ktor.features.ConditionalHeaders
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.withCharset
import io.ktor.response.respond
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.testing.handleRequest
import io.ktor.server.testing.withTestApplication
import org.junit.Test
import java.util.zip.GZIPInputStream
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class MustacheTest {
@Test
fun `Fill template and expect correct rendered content`() {
withTestApplication {
application.setupMustache()
application.install(ConditionalHeaders)
application.routing {
get("/") {
call.respond(MustacheContent(TemplateWithPlaceholder, DefaultModel, "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.setupMustache()
application.install(ConditionalHeaders)
application.routing {
get("/") {
call.respond(MustacheContent(TemplateWithPlaceholder, DefaultModel, "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.setupMustache()
application.install(ConditionalHeaders)
application.routing {
get("/") {
call.respond(MustacheContent(TemplateWithPlaceholder, DefaultModel, "e"))
}
}
assertEquals("e", handleRequest(HttpMethod.Get, "/").response.headers[HttpHeaders.ETag])
}
}
@Test
fun `Render empty model`() {
withTestApplication {
application.setupMustache()
application.install(ConditionalHeaders)
application.routing {
get("/") {
call.respond(MustacheContent(TemplateWithoutPlaceholder, null, "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.setupMustache()
application.install(Compression)
application.install(ConditionalHeaders)
application.routing {
get("/") {
call.respondTemplate(TemplateWithPlaceholder, DefaultModel, "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.setupMustache()
application.install(ConditionalHeaders)
application.routing {
get("/") {
call.respond(MustacheContent(TemplateWithPlaceholder, DefaultModel))
}
}
assertEquals(null, handleRequest(HttpMethod.Get, "/").response.headers[HttpHeaders.ETag])
}
}
private fun Application.setupMustache() {
install(Mustache) {
DefaultMustacheFactory()
}
}
companion object {
private val DefaultModel = mapOf("id" to 1, "title" to "Hello World!")
private val TemplateWithPlaceholder = "withPlaceholder.mustache"
private val TemplateWithoutPlaceholder = "withoutPlaceholder.mustache"
}
}
......@@ -59,6 +59,7 @@ includeEx ':ktor-client:ktor-client-features:ktor-client-logging:ktor-client-log
includeEx ':ktor-client:ktor-client-features:ktor-client-logging:ktor-client-logging-js'
includeEx ':ktor-client:ktor-client-features:ktor-client-logging:ktor-client-logging-ios'
includeEx ':ktor-features:ktor-freemarker'
includeEx ':ktor-features:ktor-mustache'
includeEx ':ktor-features:ktor-velocity'
includeEx ':ktor-features:ktor-gson'
includeEx ':ktor-features:ktor-jackson'
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать