DSL Validations: Child Properties
source link: https://dzone.com/articles/dsl-validations-child-properties
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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.
CORE ·
Note: 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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK