DSL Validations: Child Properties

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, ChildPropertyValidators 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.

Part 3: DSL Validations: Operators