• Stars
    star
    157
  • Rank 230,059 (Top 5 %)
  • Language
    Kotlin
  • License
    Apache License 2.0
  • Created almost 5 years ago
  • Updated 30 days ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Barber πŸ’ˆ A type safe Kotlin JVM library for building up localized, fillable, themed documents using Mustache templating

BarberπŸ’ˆ

See the project website for documentation and APIs.

A type safe Kotlin JVM library for building up localized, fillable, themed documents using Mustache templating.

Press

Why?

An application will often have hundreds of user viewable strings to power UI, notifications, emails, and other text based user interactions.

This leads to many questions about the how to achieve the desired functionality of these user viewable strings:

  • These strings may want to be personalizable! Thus the string would now have to be a template that can render with a data blob unique to each user to produce a personalized string.
  • How can we check that the fillable openings in templates have corresponding values in the data blob?
  • How can templates and data blobs be validated at compile time to protect against runtime exceptions or user visible bugs?
  • What if we want to pass around not just a single template string, but a collection of template strings for more complex documents?
    • How could we support emails that have a subject, body, recipient, primary button...?
  • What about different languages?
    • Could the same unique data blob be used to support templates in different languages?
  • What about time or money that differ between countries that even share the same language (ie. 12 vs 24 hour time)?
    • How could that formatting localization be provided?

To answer the above questions, we built BarberπŸ’ˆ.

A type safe Kotlin JVM library for building up localized, fillable, themed documents using Mustache templating.

Getting Started

To get started using Barber, skim below to understand the different elements used to safely render your localized, fillable, themed documents using Mustache templating.

Releases

Our change log has release history. API is subject to change.

implementation("app.cash.barber:barber:0.3.3")

Snapshot builds are available.

DocumentData

DocumentData is the data that is used to render a template. In Barber, this is defined as a data class making it easy to use in Kotlin.

// Define DocumentData
data class RecipientReceipt(
  val sender: String,
  val amount: String,
  val cancelUrl: String,
  val deposit_expected_at: Instant
) : DocumentData

To render a template, for example "{{sender}} sent you {{amount}}", an instance of the above DocumentData could be passed in to fill the fillable openings in the template.

Document

The final fields of the output, rendered document.

For simple documents, this may be a single field.

// Define Document
data class TransactionalSmsDocument(
  val sms_body: String
) : Document

For more complex documents that may be used in further processing, there may be multiple fields, which can be nullable.

data class TransactionalEmailDocument(
  val subject: String,
  val headline: String,
  val short_description: String,
  val primary_button: String?,
  val primary_button_url: String?,
  val secondary_button: String?,
  val secondary_button_url: String?
) : Document

DocumentTemplate

A DocumentTemplate is the glue that connects the DocumentData to the Document. It contains:

  • fields:
    • keys fulfill all the non-nullable keys of the Document targets
    • values are Mustache templates that are rendered with the passed in source DocumentData
  • source: DocumentData that can support all fillable templates in fields
  • targets: Documents that the DocumentTemplate can render to
  • locale: Locale for the language of the fields templates
val recipientReceiptSmsDocumentTemplateEN_US = DocumentTemplate(
  fields = mapOf(
    "sms_body" to "{{sender}} sent you {{amount}}"
  ),
  source = RecipientReceipt::class,
  targets = setOf(TransactionalSmsDocument::class),
  locale = Locale.EN_US
)

Barber<DocumentData, Document>

A Barber is typed to the DocumentData -> Document relationship that it knows how to render.

Amy knows how to cut blonde hair into a Fu Manchu mustache. Joe knows how to trim brown hair into a soul patch mustache.

// A Barber who knows how to render RecipientReceipt data into a TransactionalSmsDocument
val recipientReceiptSms: Barber<RecipientReceipt, TransactionalSmsDocument>

Barbershop

A Barbershop contains all possible Barbers based on the installed DocumentData, DocumentTemplate, and Documents.

Each Barber knows how to handle a different combination of DocumentData -> Document.

You can call barbershop.getBarber<RecipientReceipt, TransactionalSmsDocument>() to get the Barber that can handle rendering a RecipientReceipt into a TransactionalSmsDocument.

// Get a Barber who knows how to render RecipientReceipt data into a TransactionalSmsDocument
val recipientReceiptSms = barbershop.getBarber<RecipientReceipt, TransactionalSmsDocument>()

You want a soul patch and have brown hair, get Joe! You want a Fu Manchu and have blonde hair, get Amy!

You can also get a Map of all Barbers using barbershop.getAllBarbers().

// Get all Barbers
val allBarbers: Map<BarberKey, Barber<DocumentData, Document>> = barbershop.getAllBarbers()

Note: BarberKey is a data class that let's us lookup by both DocumentData and Document.

BarbershopBuilder

A Java style Builder that installs all of the above Barber elements and returns a pre-compiled and validated Barbershop.

  • Install DocumentData and DocumentTemplate pairs with .installDocumentTemplate<RecipientReceipt>(recipientReceiptSmsDocumentTemplateEN_US).
  • Install Document with .installDocument<TransactionalSmsDocument>()
  • Optionally set a custom LocaleResolver with .setLocaleResolver(MapleSyrupOrFirstLocaleResolver())
  • Optionally set a default BarberFieldEncoding for non-annotated Document fields with .setDefaultBarberFieldEncoding(STRING_PLAINTEXT)
  • Optionally configure warning validation strictness with .setWarningsAsErrors()
  • Return the finished Barbershop with .build() as the final method call on BarbershopBuilder.
val barbershop = BarbershopBuilder()
  .installDocumentTemplate<RecipientReceipt>(recipientReceiptSmsDocumentTemplateEN_US)
  .installDocument<TransactionalSmsDocument>()
  .build()

Barber<DocumentData, Document>.render(data: DocumentData, locale: Locale)

To render the final Document, a Barber requires a DocumentData, used to fill the DocumentTemplate, and an output Locale.

First, the Barber uses a LocaleResolver to find the best Locale match from installed DocumentTemplates.

Then, using the Locale resolved DocumentTemplate, Barber renders the fields of DocumentTemplate using the passed in DocumentData.

Returned is the requested Document rendered with the personalized values of DocumentData in the closest match to the requested Locale.

// Get a Barber who knows how to render RecipientReceipt data into a TransactionalSmsDocument
val recipientReceiptSms = barbershop.getBarber<RecipientReceipt, TransactionalSmsDocument>()

// Create the RecipientReceipt instance that holds the personalized data
val sandy50Receipt = RecipientReceipt(
  sender = "Sandy Winchester",
  amount = "$50",
  cancelUrl = "https://cash.app/cancel/123",
  deposit_expected_at = Instant.parse("2019-05-21T16:02:00.00Z")
)

// Render the final document using the personalized DocumentData instance and the output Locale
val renderedSms = recipientReceiptSms.render(sandy50Receipt, EN_US)

@BarberField, BarberFieldEncoding, and HTML Character Escaping

The @BarberField annotation can be used on Document class val fields to declare that the field should be rendered and treated according to a specific BarberFieldEncoding.

BarberFieldEncoding can be for now STRING_HTML or STRING_PLAINTEXT.

By default, all fields are treated as STRING_HTML and have common HTML escaping of characters for safety.

When a field is annotated as STRING_PLAINTEXT, characters will not be escaped.

data class EncodingTestDocument(
  val no_annotation_field: String,
  @BarberField()
  val default_field: String,
  @BarberField(encoding = BarberFieldEncoding.STRING_HTML)
  val html_field: String,
  @BarberField(encoding = BarberFieldEncoding.STRING_PLAINTEXT)
  val plaintext_field: String
) : Document

// Rendered with all fields set to `You purchased 100 shares of McDonald's.`
EncodingTestDocument(
    no_annotation_field = "You purchased 100 shares of McDonald&#39;s.",
    default_field = "You purchased 100 shares of McDonald&#39;s.",
    html_field = "You purchased 100 shares of McDonald&#39;s.",
    // Note: no character escaping on the plaintext field
    plaintext_field = "You purchased 100 shares of McDonald's."
)

Locale

Barber supports installation and resolution of multiple Locales for each DocumentTemplate.

All Locale versions of a DocumentTemplate will be installed with the BarbershopBuilder.

The desired output Locale is then provided at render time and the best available option is resolved.

// Define DocumentTemplate in English
val recipientReceiptSmsDocumentTemplateEN_US = DocumentTemplate(
  fields = mapOf("sms_body" to "{{sender}} sent you {{amount}}"),
  source = RecipientReceipt::class,
  targets = setOf(TransactionalSmsDocument::class),
  locale = Locale.EN_US
)

// Define DocumentTemplate in Canadian English
val recipientReceiptSmsDocumentTemplateEN_CA = DocumentTemplate(
  fields = mapOf("sms_body" to "{{sender}} sent you {{amount}}, eh!"),
  // ... same as EN_US
  locale = Locale.EN_CA
)

// Define DocumentTemplate in Spanish
val recipientReceiptSmsDocumentTemplateES_US = DocumentTemplate(
  fields = mapOf("sms_body" to "{{sender}} te enviΓ³ {{amount}}"),
  // ... same as EN_US
  locale = Locale.ES_US
)

// Use above elements to build a Barbershop 
val barbershop = BarbershopBuilder()
  .installDocumentTemplate<RecipientReceipt>(recipientReceiptSmsDocumentTemplateEN_US)
  .installDocumentTemplate<RecipientReceipt>(recipientReceiptSmsDocumentTemplateEN_CA)
  .installDocumentTemplate<RecipientReceipt>(recipientReceiptSmsDocumentTemplateES_US)
  .installDocument<TransactionalSmsDocument>()
  .build()
  
// Get a Barber who knows how to render RecipientReceipt data into a TransactionalSmsDocument
val recipientReceiptSms = barbershop.getBarber<RecipientReceipt, TransactionalSmsDocument>()
  
// Render in each Locale
val smsEN_US = recipientReceiptSms.render(sandy50Receipt, EN_US) // = Sandy Winchester sent you $50
val smsEN_CA = recipientReceiptSms.render(sandy50Receipt, EN_CA) // = Sandy Winchester sent you $50, eh?
val smsES_US = recipientReceiptSms.render(sandy50Receipt, ES_US) // = Sandy Winchester te envio $50

LocaleResolver

Determining based on a Locale passed in at render which installed Locale to render is done by a LocaleResolver.

It is a simple interface that looks like this:

abstract class LocaleResolver {
  /**
   * @return a [Locale] from the given [options]
   * @param [options] must be valid keys for a Locale keyed Map
   */
  fun resolve(locale: Locale, options: Set<Locale>): Locale
}

Barber comes with a very simple MatchOrFirstLocaleResolver that attempts to resolve the requested Locale exactly, and otherwise chooses the first installed Locale.

For more complex resolution algorithms, you can set your own custom LocaleResolver when building your Barbershop.

val barbershop = BarbershopBuilder()
  // ...
  .setLocaleResolver(MapleSyrupOrFirstLocaleResolver) // Always tries to resolve EN_CA
  .build()

Version Compatibility & Resolution

Barber supports installation of multiple versions of the same template. When Barber is used in a service that stores in a database versioned templates, the following capabilities drastically improve the safety, reliability, and functionality of rendering versioned templates.

For example, Barber can handle variable additions, subtractions, and changes between versions. For example, if a template has a new variable added, the corresponding DocumentData class will also have a new field.

// git diff of the new version of the DocumentData
data class RecipientReceipt(
  val sender: String,
  val amount: String,
+  val recipient: String,
) : DocumentData

Assuming a microservice environment where old clients could exist sending the old DocumentData missing the new field, Barber does the right thing and falls back to the newest, compatible template to render.

Compatibility in this case refers to ensuring that the signature (variables and types) of the DocumentData provided can fulfill the chosen template.

By default, Barber chooses the newest compatible version, or throws an exception if a version is explicitly provided that does not exist or is incompatible.

For more complex resolution algorithms, you can set your own custom VersionResolver when building your Barbershop.

val barbershop = BarbershopBuilder()
  // ...
  .setVersionResolver(SpecifiedOrNewestCompatibleVersionResolver) // Always returns newest compatible version, ignores explicitly provided version
  .build()

Integration with Guice

If you use Guice, creating a module that automatically binds all possible typed Barber instances is simple. See the code example below.

package com.your.service.package

import com.google.inject.AbstractModule
import com.google.inject.Key
import com.google.inject.util.Types
import app.cash.barber.Barber
import app.cash.barber.Barbershop
import app.cash.barber.models.BarberKey
import app.cash.barber.models.Document
import app.cash.barber.models.DocumentData

/**
 * Binds all the barbers so they may be injected directly like so:
 *
 * ```
 * @Inject var barber: Barber<RecipientReceipt, TransactionalSmsDocument>
 * ```
 */
class BarberModule(private val barbershop: Barbershop) : AbstractModule() {
  override fun configure() {
    barbershop.getAllBarbers().forEach { (barberKey, barber) ->
      bind(barberKey.asGuiceKey()).toInstance(barber)
    }
  }

  private fun BarberKey.asGuiceKey(): Key<Barber<DocumentData, Document>> {
    val barberType = Types.newParameterizedType(Barber::class.java, documentData.java, document.java)
    @Suppress("UNCHECKED_CAST") // We know this cast is safe dynamically.
    return Key.get(barberType) as Key<Barber<DocumentData, Document>>
  }
}

Coming Soon

FieldStemming

Automatically replace Money, DateTime, and Instant types with BarberMoney, BarberDateTime, and BarberInstant that let templates call out localized formatted output of each type.

Example

BarberInstant(Instant(2019-05-15T15:23:11), EN_US)
= mapOf(
    "date" to "May 15, 2019", 
    "time" to "3:23 pm",
    "casual" to "tomorrow at 3pm"
 )

BarberMoney(Money(50_00), EN_US)
= mapOf(
    "full" to "$50.00"
    "casual" to "$50"
 )

License

Copyright 2019 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

More Repositories

1

sqldelight

SQLDelight - Generates typesafe Kotlin APIs from SQL
Kotlin
5,918
star
2

turbine

A small testing library for kotlinx.coroutines Flow
Kotlin
2,323
star
3

paparazzi

Render your Android screens without a physical device or emulator
Kotlin
2,164
star
4

zipline

Run Kotlin/JS libraries in Kotlin/JVM and Kotlin/Native programs
C
1,914
star
5

molecule

Build a StateFlow stream using Jetpack Compose
Kotlin
1,710
star
6

contour

Layouts with lambdas 😎
Kotlin
1,522
star
7

redwood

Multiplatform reactive UI for Android, iOS, and web using Kotlin and Jetpack Compose
Kotlin
1,458
star
8

InflationInject

Constructor-inject views during XML layout inflation
Kotlin
908
star
9

pranadb

Go
615
star
10

licensee

Gradle plugin which validates the licenses of your dependency graph match what you expect
Kotlin
599
star
11

hermit

🐚 Hermit manages isolated, self-bootstrapping sets of tools in software projects.
Go
557
star
12

AccessibilitySnapshot

Easy regression testing for iOS accessibility
Swift
522
star
13

exhaustive

An annotation and Kotlin compiler plugin for enforcing a when statement is exhaustive
Kotlin
465
star
14

multiplatform-paging

A library that packages AndroidX Paging for Kotlin/Multiplatform.
Kotlin
452
star
15

misk

Microservice Kontainer
Kotlin
386
star
16

copper

A content provider wrapper for reactive queries
Kotlin
300
star
17

paraphrase

A Gradle plugin that generates type-safe formatters for Android string resources in the ICU message format.
Kotlin
157
star
18

stagehand

Modern, type-safe API for building animations on iOS
Swift
126
star
19

hermit-packages

Hermit manages isolated, self-bootstrapping sets of tools in software projects.
HCL
111
star
20

quiver

Quiver is a collection of extension methods and handy functions to make the wonderful functional programming Kotlin library, Arrow, even better.
Kotlin
95
star
21

spirit

Online Schema Change Tool for MySQL 8.0
Go
87
star
22

pivit

Go
82
star
23

tempest

Typesafe DynamoDB for Kotlin and Java.
Kotlin
80
star
24

better-dynamic-features

Making dynamic feature modules better
Kotlin
70
star
25

misk-web

Micro-Frontends React + Redux + Typescript Framework
TypeScript
65
star
26

blip

Sublime MySQL monitoring
Go
59
star
27

logquacious

Logquacious (lq) is a fast and simple log viewer.
TypeScript
58
star
28

wisp

Wisp is a collection of kotlin modules providing various features and utilities, including config, logging, feature flags and more.
Kotlin
58
star
29

certifikit

Kotlin Certificate processing library.
Kotlin
40
star
30

backfila

Service that manages backfill state, calling into other services to do batched work
Kotlin
30
star
31

cash-app-pay-android-sdk

Cash Android PayKit SDK for merchant integrations with Cash App Pay
Kotlin
29
star
32

nostrino

A Kotlin SDK for Nostr
Kotlin
28
star
33

transflect

Kubernetes operator using Istio to set up Envoy's gRPC-JSON transcoding.
Go
24
star
34

cmmc

K8S ConfigMap Merging Controller
Go
20
star
35

cloner

Go
20
star
36

kfsm

Finite state machinery in Kotlin
Kotlin
19
star
37

yet-another-aws-exporter

A Prometheus metrics exporter for AWS that fills in gaps CloudWatch doesn't cover
Go
17
star
38

cash-app-pay-ios-sdk

Swift
14
star
39

protosync

ProtoSync synchronises remote .proto files to a local directory
Go
13
star
40

cash-pay-pay-sdk-android-sample-app

Cash App Pay Kit SDK Sample app for Android.
Kotlin
12
star
41

trifle

Security functionality for interoperability/interaction with core services.
Swift
11
star
42

AardvarkReveal

Generate and attach a Reveal file to your Aardvark bug reports
Swift
10
star
43

ln-invoice

Parse lightning network payment requests (invoices) in Kotlin.
Kotlin
10
star
44

kfactories

Set of factories and utils to create effective and lightweight property-based testing strategies.
Kotlin
9
star
45

jooq-encryption

Kotlin
9
star
46

hermit-ij-plugin

Kotlin
8
star
47

cash-app-pay-sdk-ios-sample-app

Swift
7
star
48

AardvarkCrashReport

AardvarkCrashReport makes it easy to provide high quality data about crashes in your bug reports
Swift
7
star
49

awsu

su for aws roles
Go
7
star
50

check-signature-action

Shell
6
star
51

csop

Go
6
star
52

activate-hermit

Github Action to activate a Hermit environment.
Shell
6
star
53

kruit

TypeScript
4
star
54

chronicler

Kotlin
3
star
55

hermit-build

Relocatable/static builds for Hermit packages
Makefile
3
star
56

s3-copy-gradle-plugin

1
star
57

.github

1
star
58

hermit-package-version

Shell
1
star
59

cash-app-pay-sandbox-releases

1
star