Использование контекста запроса в виде

Использование контекста запроса в виде #

Проблема #

В виде необходимо иметь доступ к контексту:

  • Информации о текущем пользователе для отображения внутри HTML-документа.
  • Информации о разрешениях пользователя для отображения или сокрытия элементов на HTML-странице.

Эта информация нужна для каждой страницы, в результате:

  • в каждом обработчике добавляются линзы для получения контекста из запроса, даже если они не учитываются при обработке самого запроса;
  • в каждой модели добавляются поля для передачи соответствующих данных в вид.

К сожалению библиотека http4k не предлагает встроенного решения для данной проблемы.

Вариант решения #

Одним из возможных вариантов является добавление следующих классов в приложение. Они относятся к подсистеме шаблонизатора, могут быть размещены в пакете templates:

package ru.ac.uniyar.web.templates

import com.mitchellbosecke.pebble.PebbleEngine
import com.mitchellbosecke.pebble.error.LoaderException
import com.mitchellbosecke.pebble.loader.ClasspathLoader
import com.mitchellbosecke.pebble.loader.FileLoader
import org.http4k.template.ViewModel
import org.http4k.template.ViewNotFound
import java.io.StringWriter

typealias ContextAwareTemplateRenderer = (Map<String, Any?>, ViewModel) -> String

class ContextAwarePebbleTemplates(
    private val configure: (PebbleEngine.Builder) -> PebbleEngine.Builder = { it },
    private val classLoader: ClassLoader = ClassLoader.getSystemClassLoader()
) {

    private class ContextAwarePebbleTemplateRenderer(
        private val engine: PebbleEngine,
    ) : ContextAwareTemplateRenderer {
        override fun invoke(context: Map<String, Any?>, viewModel: ViewModel): String = try {
            val writer = StringWriter()
            val wholeContext = context + mapOf("model" to viewModel)
            engine.getTemplate(viewModel.template() + ".peb").evaluate(writer, wholeContext)
            writer.toString()
        } catch (_: LoaderException) {
            throw ViewNotFound(viewModel)
        }
    }

    fun CachingClasspath(baseClasspathPackage: String): ContextAwareTemplateRenderer {
        val loader = ClasspathLoader(classLoader)
        loader.prefix = if (baseClasspathPackage.isEmpty()) null else baseClasspathPackage.replace('.', '/')
        return ContextAwarePebbleTemplateRenderer(
            configure(
                PebbleEngine.Builder().loader(loader),
            ).build()
        )
    }

    fun Caching(baseTemplateDir: String): ContextAwareTemplateRenderer {
        val loader = FileLoader()
        loader.prefix = baseTemplateDir
        return ContextAwarePebbleTemplateRenderer(
            configure(
                PebbleEngine.Builder().cacheActive(true).loader(loader),
            ).build()
        )
    }

    fun HotReload(baseTemplateDir: String): ContextAwareTemplateRenderer {
        val loader = FileLoader()
        loader.prefix = baseTemplateDir
        return ContextAwarePebbleTemplateRenderer(
            configure(
                PebbleEngine.Builder().cacheActive(false).loader(loader),
            ).build()
        )
    }
}
package ru.ac.uniyar.web.templates

import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.Request
import org.http4k.lens.BiDiBodyLens
import org.http4k.lens.BiDiBodyLensSpec
import org.http4k.lens.RequestContextLens
import org.http4k.lens.string
import org.http4k.template.ViewModel

data class ContextAwareViewRender(
    private val templateRenderer: ContextAwareTemplateRenderer,
    private val baseBodyLensSpec: BiDiBodyLensSpec<String>,
    private val contextLenses: Map<String, RequestContextLens<*>> = emptyMap(),
) {
    companion object {
        fun withContentType(
            templateRenderer: ContextAwareTemplateRenderer,
            contentType: ContentType,
        ): ContextAwareViewRender =
            ContextAwareViewRender(
                templateRenderer,
                Body.string(contentType),
            )
    }

    fun associateContextLens(key: String, lens: RequestContextLens<*>): ContextAwareViewRender {
        val newContextLenses = contextLenses + mapOf(key to lens)
        return this.copy(contextLenses = newContextLenses)
    }

    fun associateContextLenses(lenses: Map<String, RequestContextLens<*>>): ContextAwareViewRender {
        val newContextLenses = contextLenses + lenses
        return this.copy(contextLenses = newContextLenses)
    }

    operator fun invoke(request: Request): BiDiBodyLens<ViewModel> {
        return baseBodyLensSpec.map<ViewModel>(
            {
                throw UnsupportedOperationException("Cannot parse a ViewModel")
            },
            { viewModel: ViewModel ->
                templateRenderer(extractContext(request), viewModel)
            }
        ).toLens()
    }

    private fun extractContext(request: Request): Map<String, Any?> =
        contextLenses.mapValues {
            it.value(request)
        }
}

Класс ContextAwarePebbleTemplates является аналогом класса PebbleTemplates, и он обеспечивает создание HTML-документов из Pebble-шаблонов. Ключевое отличие состоит в том, что пользователю предоставляется возможность задания контекста для формирования шаблона. Т.е. внутри Pebble-шаблона помимо переменной model могут быть доступны другие данные, получаемые из ассоциированных с объектом линз.

Его использование не отличается от использования PebbleTemplates:

val renderer = ContextAwarePebbleTemplates().HotReload("src/main/resources")

Для создания HTML-документа данному объекту необходимо передать контекст и модель:

data class SomeModel(val info: text) : ViewModel

val model = SomeModel("сообщение")
val document = renderer(mapOf("extra" to "Экстра", "num" to 42), model)

Внутри шаблона SomeModel.peb будут доступны 3 переменные:

  • extra со значением Экстра;
  • num со значением 42;
  • model с содержимым объекта model.

Для того, чтобы была возможность обеспечить удобную работу с контекстом предоставляется второй класс: ContextAwareViewRender. В задачи данного класса входит:

  • Сохранение ссылки на шаблонизатор.
  • Сохранение линз для доступа к контексту запроса.
  • Создание линзы для записи HTML-документа в тело ответа.

При инициализации данного класса ему необходимо передать ссылки на шаблонизатор и линзы для получения данных контекста:

val htmlView = ContextAwareViewRender.withContentType(renderer, ContentType.TEXT_HTML)

val contexts = RequestContexts()
val currentWorkerLens: RequestContextLens<Worker?> = RequestContextKey.optional(contexts, "worker")
val permissionsLens: RequestContextLens<RolePermissions> =
    RequestContextKey.required(contexts, name = "permissions")
val htmlViewWithContext = htmlView
    .associateContextLens("currentWorker", currentWorkerLens)
    .associateContextLens("permissions", permissionsLens)

Ссылку на данный объект можно передать вместо линзы для использования шаблонизатора. Внутри обработчика запроса с помощью данного объекта можно создать линзу и применить её для формирования ответа:

class ShowWorkersListHandler(
    private val listWorkersPerPageQuery: ListWorkersPerPageQuery,
    private val permissionsLens: RequestContextLens<RolePermissions>,
    private val htmlView: ContextAwareViewRender,
) {

    companion object {
        private val pageLens = Query.int().defaulted("page", 1)
    }

    override fun invoke(Request: request) {
        val permissions = permissionsLens(request)
        if (!permissions.listWorkers) {
            return Response(Status.UNAUTHORIZED)
        }

        val page = lensOrDefault(pageLens, request, 1)
        val pagedResult = listWorkersPerPageOperation.listOnPage(page)
        val paginator = Paginator(pagedResult.pageCount, page, request.uri)
        val model = WorkersListVM(pagedResult.values, paginator)
        return Response(Status.OK).with(htmlView(request) of model)
    }
}

Стоит отметить, что если HTTP-обработчику необходимы данные из контекста запроса, то при его создании необходимо явно передать соответствующую линзу.

Задача #

Интегрируйте данные классы в приложение по управлению теугольниками, убрав

  • передачу линз для получения контекста в обработчики HTTP-запросов;
  • лишние поля из моделей, содержащих информацию о пользователе и его разрешениях.

© A. M. Васильев, 2024, CC BY-SA 4.0, andrey@crafted.su