项目作者: konform-kt

项目描述 :
Portable validations for Kotlin
高级语言: Kotlin
项目地址: git://github.com/konform-kt/konform.git
创建时间: 2018-03-07T20:32:19Z
项目社区:https://github.com/konform-kt/konform

开源协议:MIT License

下载


Test
Maven Central

Portable validations for Kotlin

  • ✅ Type-safe DSL
  • 🔗 Multi-platform support (JVM, JS, Native, Wasm)
  • 🐥 Zero dependencies

Installation

For multiplatform projects:

  1. kotlin {
  2. sourceSets {
  3. commonMain {
  4. dependencies {
  5. implementation("io.konform:konform:0.11.0")
  6. }
  7. }
  8. }
  9. }

For jvm-only projects add:

  1. dependencies {
  2. implementation("io.konform:konform-jvm:0.11.0")
  3. }

Use

Suppose you have a data class like this:

  1. data class UserProfile(
  2. val fullName: String,
  3. val age: Int?
  4. )

Using the Konform type-safe DSL you can quickly write up a validation

  1. val validateUser = Validation<UserProfile> {
  2. UserProfile::fullName {
  3. minLength(2)
  4. maxLength(100)
  5. }
  6. UserProfile::age ifPresent {
  7. minimum(0)
  8. maximum(150)
  9. }
  10. }

and apply it to your data

  1. val invalidUser = UserProfile("A", -1)
  2. val validationResult = validateUser(invalidUser)

since the validation fails the validationResult will be of type Invalid and you can get a list of validation errors by indexed access:

  1. validationResult.errors.messagesAtPath(UserProfile::fullName)
  2. // yields listOf("must have at least 2 characters")
  3. validationResult.errors.messagesAtPath(UserProfile::age)
  4. // yields listOf("must be at least '0'")

or you can get all validation errors with details as a list:

  1. validationResult.errors
  2. // yields listOf(
  3. // ValidationError(path=ValidationPath(Prop(fullName)), message=must have at least 2 characters),
  4. // ValidationError(path=ValidationPath(Prop(age)), message=must be at least '0')
  5. // )

In case the validation went through successfully you get a result of type Valid with the validated value in the value field.

  1. val validUser = UserProfile("Alice", 25)
  2. val validationResult = validateUser(validUser)
  3. // yields Valid(UserProfile("Alice", 25))

Detailed usage

Hints

You can add custom hints to validations

  1. val validateUser = Validation<UserProfile> {
  2. UserProfile::age ifPresent {
  3. minimum(0) hint "Registering before birth is not supported"
  4. }
  5. }

You can use {value} to include the .toString()-ed data in the hint

  1. val validateUser = Validation<UserProfile> {
  2. UserProfile::fullName {
  3. minLength(2) hint "'{value}' is too short a name, must be at least 2 characters long."
  4. }
  5. }

Custom context

You can add customs context to validation errors

  1. val validateUser = Validation<UserProfile> {
  2. UserProfile::age {
  3. minimum(0) userContext Severity.ERROR
  4. // You can also set multiple things at once
  5. minimum(0).replace(
  6. hint = "Registering before birth is not supported",
  7. userContext = Severity.ERROR,
  8. )
  9. }
  10. }

Custom validations

You can add custom validations on properties by using constrain

  1. val validateUser = Validation<UserProfile> {
  2. UserProfile::fullName {
  3. constrain("Name cannot contain a tab") { !it.contains("\t") }
  4. // Set a custom path for the error
  5. constrain("Name must have a non-whitespace character", path = ValidationPath.of("trimmedName")) {
  6. it.trim().isNotEmpty()
  7. }
  8. // Set custom context
  9. constrain("Must have 5 characters", userContext = Severity.ERROR) {
  10. it.size >= 5
  11. }
  12. }
  13. }

You can transform data and then add a validation on the result

  1. val validateUser = Validation<UserProfile> {
  2. validate("trimmedName", { it.fullName.trim() }) {
  3. minLength(5)
  4. }
  5. // This also required and ifPresent for nullable values
  6. required("yourName", /* ...*/) {
  7. // your validations, giving an error out if the result is null
  8. }
  9. ifPresent("yourName", /* ... */) {
  10. // your validations, only running if the result is not null
  11. }
  12. // You can use a more extensive path, for example
  13. // the path will be ".fullName.trimmed" here:
  14. validate(ValidationPath.of(UserProfile::fullName, "trimmed"), { /* ... */ }) {
  15. /* ... */
  16. }
  17. }

Split validations

You can define validations separately and run them from other validations

  1. val ageCheck = Validation<Int?> {
  2. required {
  3. minimum(21)
  4. }
  5. }
  6. val validateUser = Validation<UserProfile> {
  7. UserProfile::age {
  8. run(ageCheck)
  9. }
  10. // You can also transform the data and then run a validation against the result
  11. validate("ageMinus10", { it.age?.let { age -> age - 10 } }) {
  12. run(ageCheck)
  13. }
  14. }

Collections

It is also possible to validate nested data classes and properties that are collections (List, Map, etc…)

  1. data class Person(val name: String, val email: String?, val age: Int)
  2. data class Event(
  3. val organizer: Person,
  4. val attendees: List<Person>,
  5. val ticketPrices: Map<String, Double?>
  6. )
  7. val validateEvent = Validation<Event> {
  8. Event::organizer {
  9. // even though the email is nullable you can force it to be set in the validation
  10. Person::email required {
  11. // Optionally set a hint, default hint is "is required"
  12. hint = "Email address must be given"
  13. pattern(".+@bigcorp.com") hint "Organizers must have a BigCorp email address"
  14. }
  15. }
  16. // validation on the attendees list
  17. Event::attendees {
  18. maxItems(100)
  19. }
  20. // validation on individual attendees
  21. Event::attendees onEach {
  22. Person::name {
  23. minLength(2)
  24. }
  25. Person::age {
  26. minimum(18) hint "Attendees must be 18 years or older"
  27. }
  28. // Email is optional but if it is set it must be valid
  29. Person::email ifPresent {
  30. pattern(".+@.+\..+") hint "Please provide a valid email address (optional)"
  31. }
  32. }
  33. // validation on the ticketPrices Map as a whole
  34. Event::ticketPrices {
  35. minItems(1) hint "Provide at least one ticket price"
  36. }
  37. // validations for the individual entries
  38. Event::ticketPrices onEach {
  39. // Tickets may be free in which case they are null
  40. Entry<String, Double?>::value ifPresent {
  41. minimum(0.01)
  42. }
  43. }
  44. }

Errors in the ValidationResult can also be accessed using the index access method. In case of Iterables and Arrays you use the
numerical index and in case of Maps you use the key as string.

  1. // get the error messages for the first attendees age if any
  2. result.errors.messagesAtPath(Event::attendees, 0, Person::age)
  3. // get the error messages for the free ticket if any
  4. result.errors.messagesAtPath(Event::ticketPrices, "free")

Dynamic Validations

Sometimes you want to create validations that depend on the context of the actual value being validated,
or define validations for fields that depend on other fields.
Note that this will generally have worse performance than using static validations.

  1. Validation<Address> {
  2. Address::postalCode dynamic { address ->
  3. when (address.countryCode) {
  4. "US" -> pattern("[0-9]{5}")
  5. else -> pattern("[A-Z]+")
  6. }
  7. }
  8. }

if you need to use a value further in, you can capture an earlier value with dynamic.

  1. data class Numbers(val minimum: Int, val numbers: List<Int>)
  2. Validation<Numbers> {
  3. dynamic { numbers ->
  4. Numbers::numbers onEach {
  5. minimum(numbers.minimum)
  6. }
  7. }
  8. }

Subtypes

You can run validations only if the value is of a specific subtype, or require it to be specific subtype.

  1. sealed interface Animal {
  2. val name: String
  3. }
  4. data class Cat(override val name: String, val favoritePrey: String) : Animal
  5. data class Dog(override val name: String) : Animal
  6. val validateAnimal = Validation<Animal> {
  7. Animal::name {
  8. notBlank()
  9. }
  10. // Only run this validation if the current Animal is a Cat and not null
  11. ifInstanceOf<Cat> {
  12. Cat::favoritePrey {
  13. notBlank()
  14. }
  15. }
  16. }
  17. val requireCat = Validation<Animal> {
  18. // This will return an invalid result is the current Animal is not a Cat or null
  19. requireInstanceOf<Cat> {
  20. Cat::favoritePrey {
  21. // ...
  22. }
  23. }
  24. }

Recursive validation

If you have a recursive type that you can validate, this requires

1) an extra getter to get a self-reference to the validation, and
2) dynamic to create an extra instance of the validation as-needed to avoid an infinite loop

  1. data class Node(val children: List<Node>)
  2. val validation = Validation<Node> {
  3. // Use dynamic and a function to get the current validation again
  4. Node::children onEach {
  5. runDynamic { validationRef() }
  6. }
  7. }
  8. // Type must be explicitly specified on either this or the val
  9. private val validationRef get(): Validation<Node> = validation

Fail-fast validations

Konform is primarily intended to validate the complete data and return all validation errors.
However, if you want to “fail fast” and not run later validations, you can do this with andThen
on Validation or flatten on a list of validations.

  1. val fastValidation = Validation<String> { /* ... */ }
  2. val slowValidation = Validation<String> { /* ... */ }
  3. val runSlowOnlyIfFastValidationSucceeds = Validation<String> {
  4. run(fastValidation andThen slowValidation)
  5. }

Other validation libraries for Kotlin

Integration with testing libraries

  • Kotest provides various matchers for use with Konform. They can be used in your tests to assert that a given object
    is validated successfully or fails validation with specific error messages.
    See documentation.
Maintainer

David Hoepelman (Current maintainer)
Niklas Lochschmidt (Original author, co-maintainer)

License

MIT License