ArchitectureProcessor and customizing it
This page explains ArchitectureProcessor, one of katachi’s key elements, to deepen your understanding of katachi.
The latter half explains how to write and run your own ArchitectureProcessor.
What is ArchitectureProcessor
Section titled “What is ArchitectureProcessor”katachi’s ArchitectureProcessor is an interface that does something based on an architecture definition.
@ExperimentalKatachiApipublic interface ArchitectureProcessor<Args, R> { // Parser for the arguments public val argsSerializer: KSerializer<Args>
// Does something public fun process(context: ArchitectureProcessContext<Args>): Result<R>}Wrap the body of process in runCatching { } (or the runCatchingScoped utility). runCatchingScoped collects every problem you find with failure(...) and fails once at the end (RoleDocCoverage.kt in sample/custom-processor).
| Return value | Meaning | Handling in the Gradle task |
|---|---|---|
| Result.success | Success | [OK] |
| Result.failure | Failure (violations found, documentation out of date, the job could not be done, and so on) |
[FAILED]. The exception’s message is printed and the exit code is non-zero |
katachi’s two elements
Section titled “katachi’s two elements”Broadly speaking, katachi consists of two elements: the architecture definition and ArchitectureProcessor.
flowchart LR
D["architecture { }<br/>Architecture definition"]
D --> Layout["LayoutCheck<br/>Validates placement"]
D --> Konsist["FileConstraintCheck<br/>Evaluates konsist { } constraints"]
D --> Doc["GenerateDocumentation<br/>Generates documentation"]
D --> Template["GenerateCodeFromTemplate<br/>Generates code"]
D -.-> Custom["Your ArchitectureProcessor<br/>Aggregation, extraction, output to other formats"]
style D fill:#5B8DEF22,stroke:#5B8DEF,stroke-width:2px
style Custom fill:#BDBDBD22,stroke:#BDBDBD,stroke-dasharray:4 3,color:#8A8A8A
The architecture definition itself doesn’t execute anything. It’s ArchitectureProcessor that takes an ArchitectureProcessContext and does something with it.
- The architecture definition is the definition of your project’s architecture, written inside an
architecture { }block. - Defining an architecture by itself doesn’t do anything. It’s ArchitectureProcessor that uses it to do something.
Built-in ArchitectureProcessors
Section titled “Built-in ArchitectureProcessors”katachi ships with five ArchitectureProcessors.
| Processor | What it does | Documentation |
|---|---|---|
projectArchitecture.assert()( LayoutCheck) | Validates whether files are placed according to the architecture definition, and throws an assertion exception if there are errors. | |
FileConstraintCheck | Evaluates the constraints written in konsist { }. Passed as projectArchitecture.assert(FileConstraintCheck()). | Konsist integration |
GenerateDocumentation | Generates documentation from properties such as summary and description written in the project definition. | Document generation |
GenerateCodeFromTemplate | Generates files based on the contents of the .template { } attached to layout file declarations. | Generating code from a template |
DescribeTemplates | Lists the declarations that have a .template { }, or shows one template’s parameters and the files it produces. It writes no files. | Generating code from a template |
All of them take the project’s architecture definition and use it for validation or generation.
Running an ArchitectureProcessor
Section titled “Running an ArchitectureProcessor”There are a few patterns for running an ArchitectureProcessor.
The recommended way to run it differs per ArchitectureProcessor, so refer to each ArchitectureProcessor’s own documentation for how to run it.
If an API is provided alongside the ArchitectureProcessor (often a top-level function), you can run the ArchitectureProcessor by calling that API.
For example, for LayoutCheck, you can run that ArchitectureProcessor by calling projectArchitecture.assert().
projectArchitecture.assert() // Internally calls LayoutCheck().process(...)To run an ArchitectureProcessor from the command line, it needs to be registered through the Gradle plugin.
katachi { processors { // ...
// ⭐️ Specify the ArchitectureProcessor name and its corresponding class name register("someProcessor", "com.example.SomeProcessor") }}You can also set default values at the same time as registering.
katachi { processors { // Write defaults at the same time you register register("someProcessor", "com.example.SomeProcessor") { arg("someArg1", "someValue1") }
// Add defaults to an already-registered key args("docs") { arg("mode", "check") } }}A Gradle task is provided for each registered processor.
The task name is katachi plus the registered key with its first letter capitalized (for example, register("someProcessor", ...) becomes katachiSomeProcessor).
Example run:
$ ./gradlew katachiSomeProcessor[1/3] Processors: someProcessor
[2/3] Processing...
========================================[3/3] Katachi processor run: 1 succeeded, 0 failed
[OK] someProcessor
========================================Custom ArchitectureProcessors
Section titled “Custom ArchitectureProcessors”Because ArchitectureProcessor is provided as a simple interface, you can implement your own and freely add processing.
The sample/custom-processor module in the GitHub repository has examples of custom ArchitectureProcessors.
1. Implement ArchitectureProcessor
Section titled “1. Implement ArchitectureProcessor”Prepare a class that implements ArchitectureProcessorNoArg or ArchitectureProcessor, and implement the process method.
The context passed as the argument to the process method holds the architecture definition, arguments, and so on, so refer to these as needed to implement your ArchitectureProcessor’s logic.
If your ArchitectureProcessor needs no arguments, implement the ArchitectureProcessorNoArg interface.
Specify the return type at run time as the type argument.
// Define an object that implements ArchitectureProcessorNoArgobject CountFilesPerRole : ArchitectureProcessorNoArg<Map<String, Int>> { override fun process(context: ArchitectureProcessNoArgContext): Result<Map<String, Int>> = runCatching { // context.architecture ... the architecture definition // context.roles / groups ... the defined Roles / groups // context.declaredEntries ... the flattened list of declarations from layout // context.filesOf(role) ... the actual files that matched that Role // context.rawArgs ... the arguments passed, as raw names and values // context.log() ... outputs progress
context.log("Counting files per role...") context.roles.associate { role -> role.name to context.filesOf(role).size } }}katachi uses kotlinx.serialization to parse arguments, so add a kotlinx.serialization dependency to your project.
plugins { kotlin("jvm") version "<kotlin-version>" kotlin("plugin.serialization") version "<kotlin-version>"}If your ArchitectureProcessor needs arguments, implement the ArchitectureProcessor interface.
Specify the argument type as the first type argument and the return type as the second.
object CountFilesPerRole : ArchitectureProcessor<CountFilesPerRole.Args, Map<String, Int>> { // Pass the type of the value received via --arg, and how to read it override val argsSerializer: KSerializer<Args> = Args.serializer()
override fun process(context: ArchitectureProcessContext<Args>): Result<Map<String, Int>> = runCatching { // Refer to the arguments using context.args val args: CountFilesPerRole.Args = context.args val prefix = args.prefix
context.log("Counting files per role under $prefix...") context.roles .filter { role -> role.qualifiedName.startsWith(prefix) } .associate { role -> role.name to context.filesOf(role).size } }
@Serializable data class Args(val prefix: String = "")}For details on the information available from context, refer to ArchitectureProcessContext’s API reference.
Role metadata
In some cases, the user of an ArchitectureProcessor may need a custom property that they want specified on the architecture definition. In that case, you can use Role metadata.
For example, if you want to mark a Role that’s already deprecated but still lingers as legacy baggage as Obsolete, you can define and set a metadata key called Obsolete as follows.
// The metadata keyval Obsolete: MetadataKey<Boolean> = metadata()
var MetadataScope.obsolete: Boolean? by Obsolete
// Processor sideobject ObsoleteRoles : ArchitectureProcessorNoArg<List<String>> { override fun process(context: ArchitectureProcessNoArgContext): Result<List<String>> = runCatching { context.roles.filter { it[Obsolete] == true }.map { it.name } }}
// Usage sidearchitecture { "UseCase" { obsolete = true }}2. Provide a way to run ArchitectureProcessor
Section titled “2. Provide a way to run ArchitectureProcessor”Provide a way for users to run your ArchitectureProcessor.
If you want it runnable from a program as an API, providing an API is recommended; if you want to run it as a one-shot, running it from the command line is recommended.
Provide an extension function on Architecture. This is the same shape katachi uses to provide assert().
fun Architecture.countFilesPerRole(): Map<String, Int> = process(CountFilesPerRole, CountFilesPerRole.Args()).getOrThrow()For a definition without arguments, write process(CountFilesPerRole).
Users can call the provided API without needing to know the processor exists.
val counts = projectArchitecture.countFilesPerRole()Once you register it in katachi.processors.register, you can call it as a Gradle task whose name is derived from the key (countFilesPerRole becomes katachiCountFilesPerRole).
katachi { processors { register("countFilesPerRole", "com.example.processors.CountFilesPerRole") }}./gradlew katachiCountFilesPerRole--arg values are read according to the type of the matching field on Args. For the finer rules, such as which types split on commas, see the API reference of decodeFromStringMap.
Provide the way to run it as documentation, such as in the processor’s KDoc, so that users are told how to run it as well.
Summary
Section titled “Summary”assert(), as well as documentation and template generation, are all ArchitectureProcessors.- You can build a custom ArchitectureProcessor following the same mechanism the standard ArchitectureProcessors use.
Next steps
Section titled “Next steps”- ArchitectureProcessor API reference … The
ArchitectureProcessortype and the APIs you can use inprocess. - Document generation … The biggest built-in processor built on the same mechanism.
- See also the KDoc and source code of the document generation ArchitectureProcessor.
- Generating code from a template … The processor that produces code from the definition.
- See also the KDoc and source code of the code-from-template ArchitectureProcessor.