Skip to content

Commit

Permalink
Merge pull request #24 in CASH/barber from adrw/20190531.renameElemen…
Browse files Browse the repository at this point in the history
…ts to master

* commit 'afdf8c782fbfc909cd5c196f2edc1dd5f26d6a3d':
  Rename Elements
  • Loading branch information
adrw committed May 31, 2019
2 parents 433187b + afdf8c7 commit 0f00803
Show file tree
Hide file tree
Showing 19 changed files with 237 additions and 243 deletions.
116 changes: 58 additions & 58 deletions client/src/main/kotlin/com/squareup/barber/Barber.kt
Original file line number Diff line number Diff line change
@@ -1,109 +1,109 @@
package com.squareup.barber

import com.squareup.barber.models.CopyModel
import com.squareup.barber.models.DocumentCopy
import com.squareup.barber.models.DocumentSpec
import com.squareup.barber.models.DocumentData
import com.squareup.barber.models.DocumentTemplate
import com.squareup.barber.models.Document
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor

/**
* 1) Keeps the installed [CopyModel], [DocumentCopy], and [DocumentSpec] in memory
* 2) Provides a render method to generate [DocumentSpec] from [DocumentCopy] template and [CopyModel] values
* 1) Keeps the installed [DocumentData], [DocumentTemplate], and [Document] in memory
* 2) Provides a render method to generate [Document] from [DocumentTemplate] template and [DocumentData] values
*/
interface Barber {
fun <C : CopyModel, D : DocumentSpec> newRenderer(
copyModelClass: KClass<out C>,
documentSpecClass: KClass<out D>
fun <C : DocumentData, D : Document> newRenderer(
documentDataClass: KClass<out C>,
documentClass: KClass<out D>
): Renderer<C, D>

fun getAllRenderers(): LinkedHashMap<RendererKey, Renderer<*, *>>

class Builder {
private val installedCopyModel: MutableSet<KClass<out CopyModel>> = mutableSetOf()
private val installedDocumentCopy: MutableMap<KClass<out CopyModel>, DocumentCopy> = mutableMapOf()
private val installedDocumentSpec: MutableSet<KClass<out DocumentSpec>> = mutableSetOf()
private val installedDocumentData: MutableSet<KClass<out DocumentData>> = mutableSetOf()
private val installedDocumentTemplate: MutableMap<KClass<out DocumentData>, DocumentTemplate> = mutableMapOf()
private val installedDocument: MutableSet<KClass<out Document>> = mutableSetOf()

/**
* Consumes a [CopyModel] and corresponding [DocumentCopy] and persists in-memory
* At boot, a service will call [installCopy] on all [CopyModel] and [DocumentCopy] to add to the in-memory Barber
* Consumes a [DocumentData] and corresponding [DocumentTemplate] and persists in-memory
* At boot, a service will call [installDocumentTemplate] on all [DocumentData] and [DocumentTemplate] to add to the in-memory Barber
*/
fun installCopy(copyModel: KClass<out CopyModel>, documentCopy: DocumentCopy) = apply {
if (installedDocumentCopy.containsKey(copyModel) && installedDocumentCopy[copyModel] != documentCopy) {
fun installDocumentTemplate(documentData: KClass<out DocumentData>, documentTemplate: DocumentTemplate) = apply {
if (installedDocumentTemplate.containsKey(documentData) && installedDocumentTemplate[documentData] != documentTemplate) {
throw BarberException(problems = listOf("""
|Attempted to install DocumentCopy that will overwrite an already installed DocumentCopy and CopyModel.
|Attempted to install DocumentTemplate that will overwrite an already installed DocumentTemplate and DocumentData.
|Already Installed
|CopyModel: $copyModel
|DocumentCopy: ${installedDocumentCopy[copyModel]}
|DocumentData: $documentData
|DocumentTemplate: ${installedDocumentTemplate[documentData]}
|
|Attempted to Install
|$documentCopy
|$documentTemplate
""".trimMargin()))
}
installedCopyModel.add(copyModel)
installedDocumentCopy[copyModel] = documentCopy
installedDocumentData.add(documentData)
installedDocumentTemplate[documentData] = documentTemplate
}

inline fun <reified C : CopyModel> installCopy(documentCopy: DocumentCopy) = installCopy(C::class, documentCopy)
inline fun <reified C : DocumentData> installDocumentTemplate(documentTemplate: DocumentTemplate) = installDocumentTemplate(C::class, documentTemplate)

/**
* Consumes a [DocumentSpec] and persists in-memory
* At boot, a service will call [installDocumentSpec] on all [DocumentSpec] to add to the in-memory Barber instance
* Consumes a [Document] and persists in-memory
* At boot, a service will call [installDocument] on all [Document] to add to the in-memory Barber instance
*/
fun installDocumentSpec(documentSpec: KClass<out DocumentSpec>) = apply {
installedDocumentSpec.add(documentSpec)
fun installDocument(document: KClass<out Document>) = apply {
installedDocument.add(document)
}

inline fun <reified D : DocumentSpec> installDocumentSpec() = installDocumentSpec(D::class)
inline fun <reified D : Document> installDocument() = installDocument(D::class)

/**
* Validates Builder inputs and returns a Barber instance with the installed and validated elements
*/
private fun validate() {
installedDocumentCopy.forEach { installedCopy ->
val copyModelClass = installedCopy.key
val documentCopy = installedCopy.value
installedDocumentTemplate.forEach { installedDocumentTemplate ->
val documentDataClass = installedDocumentTemplate.key
val documentTemplate = installedDocumentTemplate.value

// DocumentCopy must be installed with a CopyModel that is listed in its Source
if (copyModelClass != documentCopy.source) {
// DocumentTemplate must be installed with a DocumentData that is listed in its Source
if (documentDataClass != documentTemplate.source) {
throw BarberException(problems = listOf("""
|Attempted to install DocumentCopy with a CopyModel not specific in the DocumentCopy source.
|DocumentCopy.source: ${documentCopy.source}
|CopyModel: $copyModelClass
|Attempted to install DocumentTemplate with a DocumentData not specific in the DocumentTemplate source.
|DocumentTemplate.source: ${documentTemplate.source}
|DocumentData: $documentDataClass
""".trimMargin()))
}

// DocumentSpecs listed in DocumentCopy.Targets must be installed
val notInstalledDocumentSpec = documentCopy.targets.filter {
!installedDocumentSpec.contains(it)
// Documents listed in DocumentTemplate.Targets must be installed
val notInstalledDocument = documentTemplate.targets.filter {
!installedDocument.contains(it)
}
if (notInstalledDocumentSpec.isNotEmpty()) {
if (notInstalledDocument.isNotEmpty()) {
throw BarberException(problems = listOf("""
|Attempted to install DocumentCopy without the corresponding DocumentSpec being installed.
|Not installed DocumentCopy.targets:
|$notInstalledDocumentSpec
|Attempted to install DocumentTemplate without the corresponding Document being installed.
|Not installed DocumentTemplate.targets:
|$notInstalledDocument
""".trimMargin()))
}

// DocumentSpec targets must have primaryConstructor
// and installedCopy must be able to fulfill DocumentSpec target parameter requirements
val documentSpecs = documentCopy.targets
documentSpecs.forEach { documentSpec ->
// Validate that DocumentSpec has a Primary Constructor
val documentSpecConstructor = documentSpec.primaryConstructor ?: throw BarberException(
problems = listOf("No primary constructor for DocumentSpec class ${documentSpec::class}."))
// Document targets must have primaryConstructor
// and installedDocumentTemplate must be able to fulfill Document target parameter requirements
val documents = documentTemplate.targets
documents.forEach { document ->
// Validate that Document has a Primary Constructor
val documentConstructor = document.primaryConstructor ?: throw BarberException(
problems = listOf("No primary constructor for Document class ${document::class}."))

// Determine non-nullable required parameters
val requiredParameterNames = documentSpecConstructor.parameters.filter {
val requiredParameterNames = documentConstructor.parameters.filter {
!it.type.isMarkedNullable
}.map { it.name }

// Confirm that required parameters are present in installedCopy
if (!documentCopy.fields.keys.containsAll(requiredParameterNames)) {
// Confirm that required parameters are present in installedDocumentTemplate
if (!documentTemplate.fields.keys.containsAll(requiredParameterNames)) {
throw BarberException(problems = listOf("""
|Installed DocumentCopy lacks the required non-null fields for DocumentSpec target
|Missing fields: ${requiredParameterNames.filter{ !documentCopy.fields.containsKey(it) }}
|DocumentSpec target: ${documentSpec::class}
|DocumentCopy: $documentCopy
|Installed DocumentTemplate lacks the required non-null fields for Document target
|Missing fields: ${requiredParameterNames.filter{ !documentTemplate.fields.containsKey(it) }}
|Document target: ${document::class}
|DocumentTemplate: $documentTemplate
""".trimMargin()))
}
}
Expand All @@ -115,9 +115,9 @@ interface Barber {
*/
fun build(): Barber {
validate()
return RealBarber(installedDocumentCopy.toMap())
return RealBarber(installedDocumentTemplate.toMap())
}
}
}

inline fun <reified C : CopyModel, reified D : DocumentSpec> Barber.newRenderer() = newRenderer(C::class, D::class)
inline fun <reified C : DocumentData, reified D : Document> Barber.newRenderer() = newRenderer(C::class, D::class)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ package com.squareup.barber
* | valid fields are [sender, amount, cancelUrl, deposit_expected_at]
*
* TODO make exceptions typed so the above example could be parsed by a client and show rich feedback
* for DocumentCopy writers
* for DocumentTemplate writers
*/
class BarberException(
val problems: List<String>
Expand Down
58 changes: 29 additions & 29 deletions client/src/main/kotlin/com/squareup/barber/RealBarber.kt
Original file line number Diff line number Diff line change
@@ -1,55 +1,55 @@
package com.squareup.barber

import com.squareup.barber.models.CopyModel
import com.squareup.barber.models.DocumentCopy
import com.squareup.barber.models.DocumentSpec
import com.squareup.barber.models.DocumentData
import com.squareup.barber.models.DocumentTemplate
import com.squareup.barber.models.Document
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor

internal class RealBarber(val installedDocumentCopy: Map<KClass<out CopyModel>, DocumentCopy>) : Barber {
override fun <C : CopyModel, D : DocumentSpec> newRenderer(
copyModelClass: KClass<out C>,
documentSpecClass: KClass<out D>
internal class RealBarber(val installedDocumentTemplate: Map<KClass<out DocumentData>, DocumentTemplate>) : Barber {
override fun <C : DocumentData, D : Document> newRenderer(
documentDataClass: KClass<out C>,
documentClass: KClass<out D>
): Renderer<C, D> {
// Lookup installed DocumentCopy that corresponds to CopyModel
val documentCopy: DocumentCopy = installedDocumentCopy[copyModelClass] ?: throw BarberException(
// Lookup installed DocumentTemplate that corresponds to DocumentData
val documentTemplate: DocumentTemplate = installedDocumentTemplate[documentDataClass] ?: throw BarberException(
problems = listOf("""
|Attempted to render with DocumentCopy that has not been installed for CopyModel: $copyModelClass.
|Attempted to render with DocumentTemplate that has not been installed for DocumentData: $documentDataClass.
""".trimMargin()))

// Confirm that output DocumentSpec is a valid target for the DocumentCopy
if (!documentCopy.targets.contains(documentSpecClass)) {
// Confirm that output Document is a valid target for the DocumentTemplate
if (!documentTemplate.targets.contains(documentClass)) {
throw BarberException(problems = listOf("""
|Specified target $documentSpecClass not a valid target for CopyModel's corresponding DocumentCopy.
|Specified target $documentClass not a valid target for DocumentData's corresponding DocumentTemplate.
|Valid targets:
|${documentCopy.targets}
|${documentTemplate.targets}
""".trimMargin()))
}

// Pull out required parameters from DocumentSpec constructor
val documentSpecConstructor = documentSpecClass.primaryConstructor!!
val documentSpecParametersByName = documentSpecConstructor.parameters.associateBy { it.name }
// Pull out required parameters from Document constructor
val documentConstructor = documentClass.primaryConstructor!!
val documentParametersByName = documentConstructor.parameters.associateBy { it.name }

// Find missing fields in DocumentCopy
// Missing fields occur when a nullable field in DocumentSpec is not an included key in the DocumentCopy fields
// In the Parameters Map in the DocumentSpec constructor though, all parameter keys must be present (including
// Find missing fields in DocumentTemplate
// Missing fields occur when a nullable field in Document is not an included key in the DocumentTemplate fields
// In the Parameters Map in the Document constructor though, all parameter keys must be present (including
// nullable)
val missingFields = documentSpecParametersByName.filterKeys {
!it.isNullOrBlank() && !documentCopy.fields.containsKey(it)
val missingFields = documentParametersByName.filterKeys {
!it.isNullOrBlank() && !documentTemplate.fields.containsKey(it)
}

// Initialize keys for missing fields in DocumentCopy
val documentCopyFields: MutableMap<String, String?> = documentCopy.fields.toMutableMap()
missingFields.map { documentCopyFields.putIfAbsent(it.key!!, null) }
// Initialize keys for missing fields in DocumentTemplate
val documentDataFields: MutableMap<String, String?> = documentTemplate.fields.toMutableMap()
missingFields.map { documentDataFields.putIfAbsent(it.key!!, null) }

return RealRenderer(documentSpecConstructor, documentSpecParametersByName, documentCopyFields)
return RealRenderer(documentConstructor, documentParametersByName, documentDataFields)
}

override fun getAllRenderers(): LinkedHashMap<RendererKey, Renderer<*, *>> {
val renderers: LinkedHashMap<RendererKey, Renderer<*,*>> = linkedMapOf()
for (entry in installedDocumentCopy) {
val documentCopy = entry.value
documentCopy.targets.forEach { renderers[RendererKey(documentCopy.source, it)] = newRenderer(documentCopy.source, it) }
for (entry in installedDocumentTemplate) {
val documentData = entry.value
documentData.targets.forEach { renderers[RendererKey(documentData.source, it)] = newRenderer(documentData.source, it) }
}
return renderers
}
Expand Down
38 changes: 19 additions & 19 deletions client/src/main/kotlin/com/squareup/barber/RealRenderer.kt
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
package com.squareup.barber

import com.github.mustachejava.DefaultMustacheFactory
import com.squareup.barber.models.CopyModel
import com.squareup.barber.models.DocumentSpec
import com.squareup.barber.models.DocumentData
import com.squareup.barber.models.Document
import java.io.StringReader
import java.io.StringWriter
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter

class RealRenderer<C : CopyModel, D : DocumentSpec>(
private val documentSpecConstructor: KFunction<D>,
private val documentSpecParametersByName: Map<String?, KParameter>,
private val documentCopyFields: Map<String, String?>
class RealRenderer<C : DocumentData, D : Document>(
private val documentConstructor: KFunction<D>,
private val documentParametersByName: Map<String?, KParameter>,
private val documentTemplateFields: Map<String, String?>
) : Renderer<C, D> {
override fun render(copyModel: C): D {
// Render each field of DocumentCopy with passed in CopyModel context
override fun render(documentData: C): D {
// Render each field of DocumentTemplate with passed in DocumentData context
// Some of these fields now will be null since any missing fields will have been added with null values
val renderedDocumentCopyFields = documentCopyFields.mapValues {
val renderedDocumentDataFields = documentTemplateFields.mapValues {
when (it.value) {
null -> it.value
else -> renderMustache(it.value!!, copyModel)
else -> renderMustache(it.value!!, documentData)
}
}

// Zips the KParameters with corresponding rendered values from DocumentCopy
val parameters = renderedDocumentCopyFields.filter {
documentSpecParametersByName .containsKey(it.key)
// Zips the KParameters with corresponding rendered values from DocumentTemplate
val parameters = renderedDocumentDataFields.filter {
documentParametersByName .containsKey(it.key)
}.mapKeys {
documentSpecParametersByName.getValue(it.key)
documentParametersByName.getValue(it.key)
}

// Build the DocumentSpec instance with the rendered DocumentCopy parameters
return documentSpecConstructor.callBy(parameters)
// Build the Document instance with the rendered DocumentTemplate parameters
return documentConstructor.callBy(parameters)
}

companion object {
private val mustacheFactory = DefaultMustacheFactory()
// TODO split off compile and execute functions to allow for precompilation on install of DocumentCopy
fun renderMustache(mustacheTemplate: String, copyModel: CopyModel): String {
// TODO split off compile and execute functions to allow for precompilation on install of DocumentTemplate
fun renderMustache(mustacheTemplate: String, documentData: DocumentData): String {
val writer = StringWriter()
val compiledMustache = mustacheFactory.compile(StringReader(mustacheTemplate), mustacheTemplate)
compiledMustache.execute(writer, copyModel)
compiledMustache.execute(writer, documentData)
writer.flush()
return writer.toString()
}
Expand Down
10 changes: 5 additions & 5 deletions client/src/main/kotlin/com/squareup/barber/Renderer.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.squareup.barber

import com.squareup.barber.models.CopyModel
import com.squareup.barber.models.DocumentSpec
import com.squareup.barber.models.DocumentData
import com.squareup.barber.models.Document

interface Renderer<C : CopyModel, D : DocumentSpec> {
interface Renderer<C : DocumentData, D : Document> {
/**
* @return a [DocumentSpec] with the values of a [CopyModel] instance rendered in the [DocumentCopy] template
* @return a [Document] with the values of a [DocumentData] instance rendered in the [DocumentTemplate]
*/
fun render(copyModel: C): D
fun render(documentData: C): D
}
8 changes: 4 additions & 4 deletions client/src/main/kotlin/com/squareup/barber/RendererKey.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.squareup.barber

import com.squareup.barber.models.CopyModel
import com.squareup.barber.models.DocumentSpec
import com.squareup.barber.models.DocumentData
import com.squareup.barber.models.Document
import kotlin.reflect.KClass

data class RendererKey (
val copyModel: KClass<out CopyModel>,
val documentSpec: KClass<out DocumentSpec>
val documentData: KClass<out DocumentData>,
val document: KClass<out Document>
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package com.squareup.barber.models
/**
* This is a UI object that has the user-presented strings of a document.
*
* It is the output from a DocumentCopy being rendered with the DocumentCopy's corresponding input CopyModel.
* It is the output from a DocumentTemplate being rendered with the DocumentTemplate's corresponding input DocumentData.
*
* A DocumentSpec is medium specific (email, SMS, article...) and can either be the final rendered product
* A Document is medium specific (email, SMS, article...) and can either be the final rendered product
* (as in for SMS), or as the input object for a final rendering and theming step (HTML Mustache emails).
*
* Examples
* barber/test com.squareup.barber.examples.TransactionalEmailDocumentSpec
* barber/test com.squareup.barber.examples.TransactionalSmsDocumentSpec
* barber/test com.squareup.barber.examples.TransactionalEmailDocument
* barber/test com.squareup.barber.examples.TransactionalSmsDocument
*/
interface DocumentSpec
interface Document
Loading

0 comments on commit 0f00803

Please sign in to comment.