DSL Validations: Child Properties
After learning how to validate simple properties on a class, the DSL is extended to allow accessing properties on sub-objects contained within the top-level class.
Join the DZone community and get the full member experience.
Join For FreeNote: This is part 2 of (an expected) 4-part series. Part 1 is found at DSL Validations: Properties.
Part 1 introduced the concept of property validators, providing the building blocks for DSL validations: access an object's property and check its value.
However, property validators are limited to simple data types. Specifically, how do you validate a property on an object contained by the base object? That's the purpose of ChildPropertyValidator
validators.
ChildPropertyValidator
The ChildPropertyValidator
is a special-case PropertyValidator
which accesses a property which itself is an object — contained within the base object — and applies a PropertyValidator
on its property.
propertyName
is informational only, used when creating a violation when validation fails;getter
is the function that returns the object property. As with a generic property validator, the generic<S>
defines the class on which the getter is called and<T>
identifies the return data type of the getter, the class of the contained object;child
is the property validator for a property on the contained object.
When the property of the contained object is not null, the property validator provided is executed against that contained object; when the contained object is null, validation fails, and a ConstraintViolation
is created.
class ChildPropertyValidator<S,T> (propertyName: String,
getter: S.() -> T?,
val child: PropertyValidator<T>)
: AbstractPropertyValidator<T, S>(propertyName, getter) {
override fun validate(source: S,
errors: MutableSet<ConstraintViolation<S>>)
: Boolean {
// Attempt to get the subdocument
val childSource = getter.invoke(source)
// If subdocument is not-null validate child document; otherwise
// generate error and return
return if (childSource != null) {
validateChild(source, childSource, errors)
} else {
errors.add(
createViolation(source,
ERROR_MESSAGE.format(propertyName),
ERROR_MESSAGE,
propertyName,
null))
false
}
}
private fun validateChild (source: S,
childSource: T,
errors: MutableSet<ConstraintViolation<S>>)
: Boolean {
val set = mutableSetOf<ConstraintViolation<T>>()
val success = child.validate(childSource, set)
// Validator interface limits errors to single type, therefore need to recast the error as the root type rather
// than the child type/source on which we were validated. Stinks, but ConstraintViolation<*> cause other problems
if (!success) {
val error = set.first()
errors.add(
createViolation(source,
error.message,
error.messageTemplate,
propertyName,
error.invalidValue))
}
return success
}
companion object {
private const val ERROR_MESSAGE = "%s is required for evaluating."
}
}
Putting It All Together
Let's define a simple Kotlin data class that defines a (very) basic Student
:
data class Address(
val line1: String?,
val line2: String?
val city: String,
val state: String,
val zipCode: String
)
data class Student(
val studentId: String,
val firstName: String?,
val lastName: String?,
val emailAddress: String?,
val localAddress: Address
)
In this example, we need to validate that the student's address has a correctly-formatted United States zip code: five digits (i.e., 12345, most common) or five digits/hyphen/four digits (i.e., 12345-6789, Zip+4). The ZipCodeFormatValidator
is the property validator that checks for either of these two formats.
The sample code demonstrates how the ZipCodeFormatValidator
is wrapped by a ChildPropertyValidator
to validate the zip code within the contained Address
object.
// Assume the student is created from a database entry
val myStudent = retrieveStudent("studentId")
// Create instance of property validator
val zipValidator = ZipCodeFormatValidator("address",
Address::zipCode)
// Create child property validator for the Student
val childValidator = ChildPropertyValidator("address.zipCode",
Student::address,
zipValidator)
// Validate the property
val violations = mutableSetOf<ConstraintViolation<T>>()
childValidator.validate(myStudent, violations)
// empty collection means successful validation
val successfullyValidated = violations.isEmpty()
CAVEAT EMPTOR: ChildPropertyValidator
is itself a PropertyValidator
and therefore it's possible to navigate multiple levels deep; however, the readability and latency likely suffers. Weigh the trade-offs of a custom class-level validation versus implementing via the DSL.
Final Comments
While seemingly benign, ChildPropertyValidator
s are a necessity for building DSL validations for anything but the most simple class definitions. In Part 3, we'll demonstrate how to combine multiple validators to do more complex class-level validations without the need of writing code.
Published at DZone with permission of Scott Sosna. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments