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, ChildPropertyValidator
są 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.