Template overview
Each Kotli Template is a self-contained Kotlin artifact that encompasses all the necessary metadata for presentation and facilitates the multistep process of generating an output structure.
It comprises Template
and Processor
.
Template
It serves as the initial blueprint for a project template structure without any limitations.
The structure can be written in any language, and technically, it doesn't even have to be a "project".
The crucial aspect is that this template is utilized by the Processor
to generate the resulting structure.
It is recommended for this structure to be a functional example of a project, importable into an IDE as-is for testing and modification purposes.
Processor
The initial blueprint template can include many dependencies and implement a ton of functionality, which usually is not needed by everyone.
The Processor
serves two goals:
- Include only the required features into the output structure.
- Provide any third-party solution that utilizes the artifact with all the required metadata describing the
processor
.
Let's take a look at some existing processors
for a better understanding of the whole mechanics.
1. TemplateProcessor
Any external usage of the Kotli Template
occurs through its processor only.
A typical processor is an implementation of the BaseFeatureProcessor
class.
class AndroidComposeTemplateProcessor : BaseTemplateProcessor() {
override fun getId(): String = "template-android-compose"
override fun getType(): LayerType = LayerTypes.Android
override fun getWebUrl(): String = "https://github.com/kotlitecture/template-android-compose"
override fun createProviders(): List<FeatureProvider> = listOf(
L10NProvider(),
NavigationProvider(),
SplashProvider(),
ThemeProvider(),
...
}
override fun processBefore(state: TemplateState) {
state.onApplyRules(
"app/build.gradle",
ReplaceMarkedText(
text = "kotli.app",
marker = "{applicationId}",
replacer = state.layer.namespace,
singleLine = true
)
)
...
}
override fun processAfter(state: TemplateState) {
state.onApplyRules(
"app/src/main/kotlin",
RenamePackage("app", state.layer.namespace)
)
...
}
}
Each implementation is responsible for overriding the given methods:
getId()
- Defines the unique processor id. This id must be unique across all registered processors in the classpath.getType()
- Clarifies the type of the template and is used for a better understanding of its purpose.getWebUrl()
- Mostly an URL to the repository with the source codes of the template implementation. Although it's not required to link source codes, we prioritize open-source and its collaborative possibilities.createProviders()
- This is the main method. It registers all providers responsible for manipulating the blueprint template to form the required output architecture.processBefore()
- Used to apply somerules
to the output structure before it is delegated to providers.processAfter()
- Used to apply somerules
to the output structure after it is processed by providers.
2. FeatureProvider
Feature providers are used to group different implementations of the same functionality by different vendors.
Examples:
- Your project requires the use of analytics events, and you want to log such events into one or multiple different services (Google Analytics, Amplitude, AppsFlyer).
- Your project requires publication in different distribution channels (Maven, Google Artifact Registry, AWS CodeArtifact, etc.).
In both scenarios, you can use either one or multiple services (processors
). Depending on the scenario, you will get all the required technical solutions to either log events into multiple systems using one common method or deploy artifacts into several destinations using one generated pipeline.
FeatureProvider is responsible to group multiple similar services, present them to the user, and generate all required artifacts, making it possible to operate with the services as one.
A typical feature provider is an implementation of the BaseFeatureProvider
class.
class DistributionProvider : BaseFeatureProvider() {
override fun getId(): String = ID
override fun isMultiple(): Boolean = true
override fun getType(): FeatureType = FeatureTypes.DevOps
override fun createProcessors(): List<FeatureProcessor> = listOf(
FirebaseDistributionProcessor(),
GooglePlayDistributionProcessor()
)
companion object {
const val ID = "distribution"
}
}
Each implementation is responsible for overriding the given methods:
getId
- Unique identifier of the provider. It must be unique only across other providers of thetemplate processor
it is registered in.isMultiple
- Instructs theTemplate Processor
if it is possible to use severalfeature processors
in the output structure.getType
- Clarifies the type of the provider for a better understanding of its purpose.createProcessors
- Registers allfeature processors
of the given provider.
3. FeatureProcessor
The Feature Processor
is responsible for the inclusion or exclusion of the feature it implements in the generated template.
A feature
is any atomic integration, technical solution, or business flow that can be added to a layer during its configuration in Kotli
.
Each feature should be self-descriptive, allowing it to be presented to the user with an icon
, title
, description
, links
, and any other metadata
required to understand its value and purpose.
The primary advantage of a feature is to provide a ready-to-use solution with minimal configuration required (zero configuration is the goal).
A typical feature processor is an implementation of the BaseFeatureProcessor
class.
class FirebaseDistributionProcessor : BaseFeatureProcessor() {
override fun getId(): String = ID
override fun getWebUrl(state: TemplateState): String = "https://firebase.google.com/docs/app-distribution"
override fun getIntegrationUrl(state: TemplateState): String = "https://firebase.google.com/docs/app-distribution/android/distribute-gradle"
override fun dependencies(): List<Class<out FeatureProcessor>> = listOf(
GoogleServicesProcessor::class.java,
FirebaseProcessor::class.java
)
override fun doApply(state: TemplateState) {
state.onApplyRules("app/build.gradle",
CleanupMarkedLine("{firebase-distribution}"),
CleanupMarkedBlock("{firebase-distribution-debug}"),
CleanupMarkedBlock("{firebase-distribution-staging}")
)
...
}
override fun doRemove(state: TemplateState) {
state.onApplyRules("app/build.gradle",
RemoveMarkedLine("{firebase-distribution}"),
RemoveMarkedBlock("{firebase-distribution-debug}"),
RemoveMarkedBlock("{firebase-distribution-staging}")
)
...
}
companion object {
const val ID = "firebase-distribution"
}
}
Each implementation is responsible for overriding the given methods:
getId
- Unique identifier of the processor. It must be unique only across other processors of thetemplate processor
it is used in.getWebUrl
- Any official URL of the underlying functionality for a better understanding of its purpose by the user.getIntegrationUrl
- Any public URL of the integration guide for this feature. This guide is not required when the feature is included. Instead, it is just a knowledge base of the sources used to integrate the feature.dependencies
- If the feature has any dependencies on other features, the processor will apply them first to the prepared structure. The dependencies must also be registered in thetemplate processor
throughfeature providers
.doApply
- When the feature is selected by the user, this method is used to apply somerules
to the files affected by the feature in the blueprint template. It is not required that such files exist in the original template.doRemove
- When the feature is not selected by the user, this method will be called tocleanup
the template from any changes specific to the feature only.
4. FileRule
Any rule to be applied to files from the blueprint template during the generation of the output structure.
A typical rule is an implementation of the FileRule
class.
class CleanupMarkedLine(
private val marker: String,
private val singleLine: Boolean = false
) : FileRule() {
override fun doApply(file: TemplateFile) {
val lines = file.lines
lines.forEachIndexed { index, line ->
if (isMarked(file, line, marker)) {
lines[index] = cleanup(file, line)
if (singleLine) return
}
}
}
}
Each file rule requires implementing only one method, doApply
. This method is called during the final stage of the generation phase.
- FileRule is not bound to files it modifies.
- It is not required that files exist in the original template.
- It is possible to implement any rule with any underlying template engine to process the input files.
- The engine allows applying different rules to the same files. In such cases, each subsequent rule will operate with the modified version of the file.
5. TemplateContext
TemplateProcessor does not directly manipulate the output structure. Instead, it creates rules to be applied during the preparation phase.
TemplateContext serves to accumulate the rules to be applied to the blueprint template during the generation phase.
...
state.onApplyRules("*AnalyticsSource*", RemoveFile())
...
state.onApplyRules("app/src/main/kotlin/app/datasource/analytics/firebase", RemoveFile())
...
state.onApplyRules("app/build.gradle", CleanupMarkedLine("{baselineprofile}"), CleanupMarkedBlock("{baselineprofile-config}"))
...
Each TemplateProcessor and any subsequent FeatureProcessor is responsible for applying rules to the passed context by calling the method onApplyRules
.
This method accepts two parameters:
contextPath
- The path to the file (or files) relative to the root folder of the blueprint template.rules
- The list of rules to be applied to the files found by the providedcontextPath
.
The parameter contextPath
can be defined as a mask to apply the given rules to multiple files.
If it contains wildcard characters such as *
or ?
, it is considered a mask.
Examples:
*Source*
- selects any file from the template that hasSource
in the middle.*Source??
- selects any file from the template that hasSource
in the middle and ends with two additional symbols.
Examples
It is recommended to check how existing templates look for a better understanding of the engine concept. Here are some examples:
- Android Compose Application - https://github.com/kotlitecture/template-android-compose
- Spring Boot Application - https://github.com/kotlitecture/template-backend-spring-boot