Skip to content
  • Advanced

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.

katachi’s ArchitectureProcessor is an interface that does something based on an architecture definition.

@ExperimentalKatachiApi
public 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

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.

katachi ships with five ArchitectureProcessors.

ProcessorWhat it doesDocumentation
projectArchitecture.assert()
(LayoutCheck)
Validates whether files are placed according to the architecture definition, and throws an assertion exception if there are errors.
FileConstraintCheckEvaluates the constraints written in konsist { }. Passed as projectArchitecture.assert(FileConstraintCheck()).Konsist integration
GenerateDocumentationGenerates documentation from properties such as summary and description written in the project definition.Document generation
GenerateCodeFromTemplateGenerates files based on the contents of the .template { } attached to layout file declarations.Generating code from a template
DescribeTemplatesLists 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.

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(...)

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.

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 ArchitectureProcessorNoArg
object 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 }
}
}

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 key
val Obsolete: MetadataKey<Boolean> = metadata()
var MetadataScope.obsolete: Boolean? by Obsolete
// Processor side
object ObsoleteRoles : ArchitectureProcessorNoArg<List<String>> {
override fun process(context: ArchitectureProcessNoArgContext): Result<List<String>> = runCatching {
context.roles.filter { it[Obsolete] == true }.map { it.name }
}
}
// Usage side
architecture {
"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()

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.

  • 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.