Walidacje DSL: Właściwości podrzędne

Uwaga: To jest część 2 z (oczekiwanej) 4-częściowej serii. Część 1 można znaleźć pod adresem DSL Validations: Właściwości.

Część 1 wprowadziła koncepcję walidatorów właściwości, zapewniając bloki konstrukcyjne dla walidacji DSL: dostęp do właściwości obiektu i sprawdzenie jej wartości.

Jednak właściwość walidatory są ograniczone do prostych typy danych. Konkretnie, jak sprawdzić poprawność właściwości obiektu zawartego w obiekcie bazowym? To jest właśnie cel ChildPropertyValidator walidatorów.

ChildPropertyValidator

The ChildPropertyValidator jest szczególnym przypadkiem PropertyValidator który uzyskuje dostęp do właściwości, która sama jest obiektem – zawartym w obiekcie bazowym – i stosuje metodę PropertyValidator na swojej właściwości.

  • propertyName jest tylko informacją, używaną podczas tworzenia naruszenia, gdy walidacja nie powiedzie się;
  • getter jest funkcją zwracającą właściwość obiektu. Podobnie jak w przypadku ogólnego walidatora właściwości, ogólny walidator <S> definiuje klasę, na której wywoływany jest getter i <T> identyfikuje zwracany typ danych gettera, klasę zawartego obiektu;
  • child jest walidatorem właściwości dla właściwości zawartego obiektu.

Gdy właściwość zawartego obiektu nie ma wartości null, dostarczony walidator właściwości jest wykonywany względem tego zawartego obiektu; gdy zawarty obiekt ma wartość null, walidacja kończy się niepowodzeniem, a komunikat ConstraintViolation jest tworzony.

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."
    }
}

Jak to wszystko połączyć?

Proszę zdefiniować prostą Kotlin klasa danych która definiuje (bardzo) podstawowy 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
)

W tym przykładzie musimy sprawdzić, czy adres ucznia ma poprawnie sformatowane Stany Zjednoczone kod pocztowy: pięć cyfr (np. 12345, najczęściej) lub pięć cyfr / myślnik / cztery cyfry (np. 12345-6789, Zip+4). The ZipCodeFormatValidator jest walidatorem właściwości, który sprawdza jeden z tych dwóch formatów.

Przykładowy kod demonstruje, w jaki sposób ZipCodeFormatValidator jest zawijany przez ChildPropertyValidator aby zweryfikować kod pocztowy w zawartym Address obiektu.

// 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 jest sam w sobie PropertyValidator i dlatego możliwe jest nawigowanie na wielu poziomach głębokości; jednak czytelność i opóźnienie prawdopodobnie ucierpią. Proszę rozważyć kompromisy niestandardowej walidacji na poziomie klasy w porównaniu z implementacją za pośrednictwem funkcji DSL.

Uwagi końcowe

Chociaż pozornie łagodne, ChildPropertyValidatorsą niezbędne do tworzenia walidacji DSL dla wszystkiego poza najprostszymi definicjami klas. W części 3 pokażemy, jak połączyć wiele walidatorów, aby wykonać bardziej złożone walidacje na poziomie klasy bez konieczności pisania kodu.