- Overview
- Quick Start
- Introduction
- Guides
- Client Libraries
- API Reference
- Examples
- DDD Resources
- Validation user guide
- Validation developer guide
Implement the Generator
The Generator produces Java code for every application of the option within a compiled message
type. The framework calls codeFor once per message type and inlines each returned
SingleOptionCode into the generated validate() method.
Class declaration
internal class WhenGenerator : OptionGeneratorWithConverter()
Use OptionGeneratorWithConverter when your generated code needs to convert a Protobuf field
value to a Java expression — for example, to format a field value as a JSON string in an error
message. This base class injects a JavaValueConverter that handles the conversion. Use the
plain OptionGenerator base class when no value conversion is needed.
Querying the View
private val allWhenFields by lazy {
querying.select<WhenField>().all()
}
querying is injected by the framework and provides read access to all accumulated View
instances. The by lazy delegate is required because querying is not available until the
framework initialises the generator; accessing it during construction causes an error. The
query result is cached after the first call.
codeFor override
override fun codeFor(type: TypeName): List<SingleOptionCode> =
allWhenFields
.filter { it.id.type == type }
.map { GenerateWhen(it, converter).code() }
The framework calls codeFor once for each message type it processes. Filter the view list
by id.type == type to select only the fields that belong to the current message. Each
filtered view is passed to a helper that composes the actual CodeBlock.
Each SingleOptionCode wraps a CodeBlock that is inlined directly into the generated
validate() method, so the code must be a valid Java statement or block.
For complete context, see WhenGenerator.kt in the Spine Time repository.
Generated code paths
The GenerateWhen.code() method chooses the Java code shape for a single application of
the (when) option:
- For a single message field, it generates one validation block for the field value.
- For a repeated message field, it generates a
forloop and validates each element inside that loop. - For a map field, it generates a
forloop over the map’s.values()and validates each value inside that loop.
fun code(): SingleOptionCode = when {
fieldType.isMessage -> validateTime(fieldValue)
fieldType.isRepeatedMessage ->
CodeBlock(
"""
for (var element : $fieldValue) {
${validateTime(ReadVar("element"))}
}
""".trimIndent()
)
fieldType.isMap ->
CodeBlock(
"""
for (var element : $fieldValue.values()) {
${validateTime(ReadVar("element"))}
}
""".trimIndent()
)
else -> unsupportedFieldType()
}.run { SingleOptionCode(this) }
All three branches delegate to the same validateTime(...) helper, so the time comparison,
violation construction, and placeholder handling stay in one place. The difference is only
where the checked value comes from: the field getter for a single message, the loop variable
for each repeated element, or the map value variable for each map entry.
private fun validateTime(fieldValue: Expression<Any>): CodeBlock {
val isTimeOutOfBound = when (view.type) {
TFT_TIMESTAMP -> {
val operator = if (view.bound == FUTURE) "<" else ">"
"$TimestampsClass.compare($fieldValue, $SpineTime.currentTime()) $operator 0"
}
TFT_TEMPORAL -> {
val checkBound = if (view.bound == FUTURE) "isInPast" else "isInFuture"
"$fieldValue.$checkBound()"
}
else -> unsupportedFieldType()
}
return CodeBlock(
"""
if (!${field.hasDefaultValue()} && $isTimeOutOfBound) {
var fieldPath = ${parentPath.resolve(field.name)};
var typeName = ${parentName.orElse(declaringType)};
var violation = ${violation(ReadVar("fieldPath"), ReadVar("typeName"), fieldValue)};
$violations.add(violation);
}
""".trimIndent()
)
}
See the full source around GenerateWhen.code() for more details
on the implementation.