GithubHelp home page GithubHelp logo

valiktor / valiktor Goto Github PK

View Code? Open in Web Editor NEW
424.0 9.0 34.0 2.78 MB

Valiktor is a type-safe, powerful and extensible fluent DSL to validate objects in Kotlin

License: Apache License 2.0

Kotlin 100.00%
kotlin validation-library validation dsl kotlin-library kotlin-dsl validations

valiktor's Introduction

Valiktor

Valiktor is a type-safe, powerful and extensible fluent DSL to validate objects in Kotlin.

Build Status Build status Coverage Status Quality Status Code Style Maven Central Apache License

Installation

Gradle

implementation 'org.valiktor:valiktor-core:0.12.0'

Gradle (Kotlin DSL)

implementation("org.valiktor:valiktor-core:0.12.0")

Maven

<dependency>
  <groupId>org.valiktor</groupId>
  <artifactId>valiktor-core</artifactId>
  <version>0.12.0</version>
</dependency>
  • For install other modules, see Modules.

Getting Started

data class Employee(val id: Int, val name: String, val email: String) {
    init {
        validate(this) {
            validate(Employee::id).isPositive()
            validate(Employee::name).hasSize(min = 3, max = 80)
            validate(Employee::email).isNotBlank().isEmail()
        }
    }
}

How it works

The main function org.valiktor.validate expects an object and an anonymous function that will validate it. Within this, it's possible to validate the object properties by calling org.valiktor.validate with the respective property as parameter. Thanks to Kotlin's powerful reflection, it's type safe and very easy, e.g.: Employee::name. There are many validation constraints (org.valiktor.constraints.*) and extension functions (org.valiktor.functions.*) for each data type. For example, to validate that the employee's name cannot be empty: validate(Employee::name).isNotEmpty().

All the validate functions are evaluated and if any constraint is violated, a ConstraintViolationException will be thrown with a set of ConstraintViolation containing the property, the invalid value and the violated constraint.

For example, consider this data class:

data class Employee(val id: Int, val name: String)

And this invalid object:

val employee = Employee(id = 0, name = "")

Now, let's validate its id and name properties and handle the exception that will be thrown by printing the property name and the violated constraint:

try {
    validate(employee) {
        validate(Employee::id).isPositive()
        validate(Employee::name).isNotEmpty()
    }
} catch (ex: ConstraintViolationException) {
    ex.constraintViolations
        .map { "${it.property}: ${it.constraint.name}" }
        .forEach(::println)
}

This code will return:

id: Greater
name: NotEmpty

See the sample

Nested object properties

Valiktor can also validate nested objects and properties recursively.

For example, consider these data classes:

data class Employee(val company: Company)
data class Company(val city: City)
data class City(val name: String)

And this invalid object:

val employee = Employee(company = Company(city = City(name = "")))

Now, let's validate the property name of City object and handle the exception that will be thrown by printing the property name and the violated constraint:

try {
    validate(employee) {
        validate(Employee::company).validate {
            validate(Company::city).validate {
                validate(City::name).isNotEmpty()
            }
        }
    }
} catch (ex: ConstraintViolationException) {
    ex.constraintViolations
        .map { "${it.property}: ${it.constraint.name}" }
        .forEach(::println)
}

This code will return:

company.city.name: NotEmpty

See the sample

Array and collection properties

Array and collection properties can also be validated, including its elements.

For example, consider these data classes:

data class Employee(val dependents: List<Dependent>)
data class Dependent(val name: String)

And this invalid object:

val employee = Employee(
    dependents = listOf(
        Dependent(name = ""), 
        Dependent(name = ""), 
        Dependent(name = "")
    )
)

Now, let's validate the property name of all Dependent objects and handle the exception that will be thrown by printing the property name and the violated constraint:

try {
    validate(employee) {
        validate(Employee::dependents).validateForEach {
            validate(Dependent::name).isNotEmpty()
        }
    }
} catch (ex: ConstraintViolationException) {
    ex.constraintViolations
        .map { "${it.property}: ${it.constraint.name}" }
        .forEach(::println)
}

This code will return:

dependents[0].name: NotEmpty
dependents[1].name: NotEmpty
dependents[2].name: NotEmpty

See the sample

Internationalization

Valiktor provides a decoupled internationalization, that allows to maintain the validation logic in the core of the application and the internationalization in another layer, such as presentation or RESTful adapter. This guarantees some design principles proposed by Domain-Driven Design or Clean Architecture, for example.

The internationalization works by converting a collection of ConstraintViolation into a collection of ConstraintViolationMessage through the extension function org.valiktor.i18n.mapToMessage by passing the following parameters:

  • baseName: specifies the prefix name of the message properties, the default value is org/valiktor/messages.
  • locale: specifies the java.util.Locale of the message properties, the default value is the default locale of the application.

For example:

try {
    validate(employee) {
        validate(Employee::id).isPositive()
        validate(Employee::name).isNotEmpty()
    }
} catch (ex: ConstraintViolationException) {
    ex.constraintViolations
        .mapToMessage(baseName = "messages", locale = Locale.ENGLISH)
        .map { "${it.property}: ${it.message}" }
        .forEach(::println)
}

This code will return:

id: Must be greater than 1
name: Must not be empty

Supported locales

Currently the following locales are natively supported by Valiktor:

  • ca (Catalan)
  • de (German)
  • en (English)
  • es (Spanish)
  • ja (Japanese)
  • pt_BR (Portuguese/Brazil)

Customizing a message

Any constraint message of any language can be overwritten simply by adding the message key into your message bundle file. Generally the constraint key is the qualified class name plus message suffix, e.g.: org.valiktor.constraints.NotEmpty.message.

Message formatters

Some constraints have parameters of many types and these parameters need to be interpolated with the message. The default behavior of Valiktor is to call the object toString() function, but some data types require specific formatting, such as date/time and monetary values. So for these cases, there are custom formatters (org.valiktor.i18n.formatters.*).

For example:

try {
    validate(employee) {
        validate(Employee::dateOfBirth).isGreaterThan(LocalDate.of(1950, 12, 31))
    }
} catch (ex: ConstraintViolationException) {
    ex.constraintViolations
        .mapToMessage(baseName = "messages")
        .map { "${it.property}: ${it.message}" }
        .forEach(::println)
}

With en as the default locale, this code will return:

dateOfBirth: Must be greater than Dec 31, 1950

With pt_BR as the default locale, this code will return:

dateOfBirth: Deve ser maior que 31/12/1950

Currently the following types have a custom formatter supported by Valiktor:

Type Formatter
kotlin.Any org.valiktor.i18n.formatters.AnyFormatter
kotlin.Array org.valiktor.i18n.formatters.ArrayFormatter
kotlin.Number org.valiktor.i18n.formatters.NumberFormatter
kotlin.collections.Iterable org.valiktor.i18n.formatters.IterableFormatter
java.util.Calendar org.valiktor.i18n.formatters.CalendarFormatter
java.util.Date org.valiktor.i18n.formatters.DateFormatter
java.time.LocalDate org.valiktor.i18n.formatters.LocalDateFormatter
java.time.LocalTime org.valiktor.i18n.formatters.LocalTimeFormatter
java.time.LocalDateTime org.valiktor.i18n.formatters.LocalDateTimeFormatter
java.time.OffsetTime org.valiktor.i18n.formatters.OffsetTimeFormatter
java.time.OffsetDateTime org.valiktor.i18n.formatters.OffsetDateTimeFormatter
java.time.ZonedDateTime org.valiktor.i18n.formatters.ZonedDateTimeFormatter
javax.money.MonetaryAmount org.valiktor.i18n.formatters.MonetaryAmountFormatter
org.joda.time.DateTime org.valiktor.i18n.formatters.DateTimeFormatter
org.joda.time.LocalDate org.valiktor.i18n.formatters.LocalDateFormatter
org.joda.time.LocalTime org.valiktor.i18n.formatters.LocalTimeFormatter
org.joda.time.LocalDateTime org.valiktor.i18n.formatters.LocalDateTimeFormatter
org.joda.money.Money org.valiktor.i18n.formatters.MoneyFormatter
org.joda.money.BigMoney org.valiktor.i18n.formatters.BigMoneyFormatter

Creating a custom formatter

Creating a custom formatter is very simple, just implement the interface org.valiktor.i18n.Formatter, like this:

object CustomFormatter : Formatter<Custom> {
    override fun format(value: Custom, messageBundle: MessageBundle) = value.toString()
}

Then add it to the list of formatters (org.valiktor.i18n.Formatters):

Formatters[Custom::class] = CustomFormatter

It's also possible to use the SPI (Service Provider Interface) provided by Valiktor using the java.util.ServiceLoader to discover the formatters automatically without adding them to the list programmatically. For this approach, it's necessary to implement the interface org.valiktor.i18n.FormatterSpi, like this:

class CustomFormatterSpi : FormatterSpi {

    override val formatters = setOf(
        Custom::class to CustomFormatter
    )
}

Then create a file org.valiktor.i18n.FormatterSpi within the directory META-INF.services with the content:

com.company.CustomFormatterSpi

See the sample

Creating a custom validation

Valiktor provides a lot of constraints and validation functions for the most common types, but in some cases this is not enough to meet all needs.

It's possible to create custom validations in three steps:

1. Define the constraint

To create a custom constraint, it's necessary to implement the interface org.valiktor.Constraint, which has these properties:

  • name: specifies the name of the constraint, the default value is the class name, e.g.: Between.
  • messageBundle: specifies the base name of the default message property file, the default value is org/valiktor/messages.
  • messageKey: specifies the name of the key in the message property file, the default value is the qualified class name plus message suffix, e.g.: org.valiktor.constraints.Between.message.
  • messageParams: specifies a Map<String, *> containing the parameters to be replaced in the message, the default values are all class properties, obtained through reflection.

For example:

data class Between<T>(val start: T, val end: T) : Constraint

2. Create the extension function

The validation logic must be within an extension function of org.valiktor.Validator<E>.Property<T>, where E represents the object and T represents the property to be validated.

There is an auxiliary function named validate that expects a Constraint and a validation function as parameters.

For example:

fun <E> Validator<E>.Property<Int?>.isBetween(start: Int, end: Int) = 
    this.validate(Between(start, end)) { it == null || it in start.rangeTo(end) }

To support suspending functions, you must use coValidate instead of validate:

suspend fun <E> Validator<E>.Property<Int?>.isBetween(start: Int, end: Int) = 
    this.coValidate(Between(start, end)) { it == null || it in start.rangeTo(end) }

And to use it:

validate(employee) {
    validate(Employee::age).isBetween(start = 1, end = 99)
}

Note: null properties are valid (it == null || ...), this is the default behavior for all Valiktor functions. If the property is nullable and cannot be null, the function isNotNull() should be used.

3. Add the internationalization messages

Add internationalization support for the custom constraint is very simple. Just add a message to each message bundle file.

For example:

  • en (e.g.: messages_en.properties):
org.valiktor.constraints.Between.message=Must be between {start} and {end}
  • pt_BR (e.g.: messages_pt_BR.properties):
org.valiktor.constraints.Between.message=Deve estar entre {start} e {end}

Note: the variables start and end are extracted through the property messageParams of the constraint Between and will be formatted in the message using the Message formatters. If you need a custom formatter, see Creating a custom formatter.

See the sample

Coroutines support

Valiktor supports suspending functions natively, so you can use it in your validations.

For example, consider this suspending function:

suspend fun countByName(name: String): Int

It cannot be called by isValid because it doesn't allow suspending functions:

validate(employee) {
    validate(Employee::name).isValid { countByName(it) == 0 } // compilation error
}

but we can use isCoValid, that expects a suspending function:

validate(employee) {
    validate(Employee::name).isCoValid { countByName(it) == 0 } // OK
}

Validating RESTful APIs

Implementing validation on REST APIs is not always so easy, so developers end up not doing it right. But the fact is that validations are extremely important to maintaining the integrity and consistency of the API, as well as maintaining the responses clear by helping the client identifying and fixing the issues.

Spring support

Valiktor provides integration with Spring WebMvc and Spring WebFlux (reactive approach) to make validating easier. The module valiktor-spring has four exception handlers:

Spring WebMvc:

  • ConstraintViolationExceptionHandler: handles ConstraintViolationException from valiktor-core.
  • InvalidFormatExceptionHandler: handles InvalidFormatException from jackson-databind.
  • MissingKotlinParameterExceptionHandler: handles MissingKotlinParameterException from jackson-module-kotlin.

Spring WebFlux:

  • ReactiveConstraintViolationExceptionHandler: handles ConstraintViolationException from valiktor-core.
  • ReactiveInvalidFormatExceptionHandler: handles InvalidFormatException from jackson-databind.
  • ReactiveMissingKotlinParameterExceptionHandler: handles MissingKotlinParameterException from jackson-module-kotlin.

All the exception handlers return the status code 422 (Unprocessable Entity) with the violated constraints in the following payload format:

{
  "errors": [
    {
      "property": "the invalid property name",
      "value": "the invalid value",
      "message": "the internationalization message",
      "constraint": {
        "name": "the constraint name",
        "params": [
          {
            "name": "the param name",
            "value": "the param value"
          }
        ]
      }
    }
  ]
}

Valiktor also use the Spring Locale Resolver to determine the locale that will be used to translate the internationalization messages.

By default, Spring resolves the locale by getting the HTTP header Accept-Language, e.g.: Accept-Language: en.

Spring WebMvc example

Consider this controller:

@RestController
@RequestMapping("/employees")
class EmployeeController {

    @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE])
    fun create(@RequestBody employee: Employee): ResponseEntity<Void> {
        validate(employee) {
            validate(Employee::id).isPositive()
            validate(Employee::name).isNotEmpty()
        }
        return ResponseEntity.created(...).build()
    }
}

Now, let's make two invalid requests with cURL:

  • with Accept-Language: en:
curl --header "Accept-Language: en" \
  --header "Content-Type: application/json" \
  --request POST \ 
  --data '{"id":0,"name":""}' \
  http://localhost:8080/employees

Response:

{
  "errors": [
    {
      "property": "id",
      "value": 0,
      "message": "Must be greater than 0",
      "constraint": {
        "name": "Greater",
        "params": [
          {
            "name": "value",
            "value": 0
          }
        ]
      }
    },
    {
      "property": "name",
      "value": "",
      "message": "Must not be empty",
      "constraint": {
        "name": "NotEmpty",
        "params": []
      }
    }
  ]
}
  • with Accept-Language: pt-BR:
curl --header "Accept-Language: pt-BR" \
  --header "Content-Type: application/json" \  
  --request POST \ 
  --data '{"id":0,"name":""}' \
  http://localhost:8080/employees

Response:

{
  "errors": [
    {
      "property": "id",
      "value": 0,
      "message": "Deve ser maior que 0",
      "constraint": {
        "name": "Greater",
        "params": [
          {
            "name": "value",
            "value": 0
          }
        ]
      }
    },
    {
      "property": "name",
      "value": "",
      "message": "Nรฃo deve ser vazio",
      "constraint": {
        "name": "NotEmpty",
        "params": []
      }
    }
  ]
}

Samples:

Spring WebFlux example

Consider this router using Kotlin DSL:

@Bean
fun router() = router {
    accept(MediaType.APPLICATION_JSON).nest {
        "/employees".nest {
            POST("/") { req ->
                req.bodyToMono(Employee::class.java)
                    .map {
                        validate(it) {
                            validate(Employee::id).isPositive()
                            validate(Employee::name).isNotEmpty()
                        }
                    }
                    .flatMap {
                        ServerResponse.created(...).build()
                    }
            }
        }
    }
}

Now, let's make two invalid requests with cURL:

  • with Accept-Language: en:
curl --header "Accept-Language: en" \
  --header "Content-Type: application/json" \
  --request POST \ 
  --data '{"id":0,"name":""}' \
  http://localhost:8080/employees

Response:

{
  "errors": [
    {
      "property": "id",
      "value": 0,
      "message": "Must be greater than 0",
      "constraint": {
        "name": "Greater",
        "params": [
          {
            "name": "value",
            "value": 0
          }
        ]
      }
    },
    {
      "property": "name",
      "value": "",
      "message": "Must not be empty",
      "constraint": {
        "name": "NotEmpty",
        "params": []
      }
    }
  ]
}
  • with Accept-Language: pt-BR:
curl --header "Accept-Language: pt-BR" \
  --header "Content-Type: application/json" \  
  --request POST \ 
  --data '{"id":0,"name":""}' \
  http://localhost:8080/employees

Response:

{
  "errors": [
    {
      "property": "id",
      "value": 0,
      "message": "Deve ser maior que 0",
      "constraint": {
        "name": "Greater",
        "params": [
          {
            "name": "value",
            "value": 0
          }
        ]
      }
    },
    {
      "property": "name",
      "value": "",
      "message": "Nรฃo deve ser vazio",
      "constraint": {
        "name": "NotEmpty",
        "params": []
      }
    }
  ]
}

Samples:

Custom Exception Handler

Valiktor provides an interface to customize the HTTP response, for example:

data class ValidationError(
    val errors: Map<String, String>
)

@Component
class ValidationExceptionHandler(
    private val config: ValiktorConfiguration
) : ValiktorExceptionHandler<ValidationError> {

    override fun handle(exception: ConstraintViolationException, locale: Locale) =
        ValiktorResponse(
            statusCode = HttpStatus.BAD_REQUEST,
            headers = HttpHeaders().apply {
                this.set("X-Custom-Header", "OK")
            },
            body = ValidationError(
                errors = exception.constraintViolations
                    .mapToMessage(baseName = config.baseBundleName, locale = locale)
                    .map { it.property to it.message }
                    .toMap()
            )
        )
}

You can customize status code, headers and payload by implementing the interface ValiktorExceptionHandler.

Spring Boot support

For Spring Boot applications, the module valiktor-spring-boot-starter provides auto-configuration support for the exception handlers and property support for configuration.

Currently the following properties can be configured:

Property Description
valiktor.baseBundleName The base bundle name containing the custom messages

Example with YAML format:

valiktor:
  base-bundle-name: messages

Example with Properties format:

valiktor.baseBundleName=messages

Test Assertions

Valiktor provides a module to build fluent assertions for validation tests, for example:

shouldFailValidation<Employee> {
    // some code here
}

The function shouldFailValidation asserts that a block fails with ConstraintViolationException being thrown.

It's possible to verify the constraint violations using a fluent DSL:

shouldFailValidation<Employee> {
    // some code here
}.verify {
    expect(Employee::name, " ", NotBlank)
    expect(Employee::email, "john", Email)
    expect(Employee::company) {
        expect(Company::name, "co", Size(min = 3, max = 50))
    }
}

Collections and arrays are also supported:

shouldFailValidation<Employee> {
    // some code here
}.verify {
    expectAll(Employee::dependents) {
        expectElement {
            expect(Dependent::name, " ", NotBlank)
            expect(Dependent::age, 0, Between(1, 16))
        }
        expectElement {
            expect(Dependent::name, " ", NotBlank)
            expect(Dependent::age, 17, Between(1, 16))
        }
        expectElement {
            expect(Dependent::name, " ", NotBlank)
            expect(Dependent::age, 18, Between(1, 16))
        }
    }
}

Modules

There are a number of modules in Valiktor, here is a quick overview:

valiktor-core

jar javadoc sources

The main library providing the validation engine, including:

  • 40+ validation constraints
  • 200+ validation functions for all standard Kotlin/Java types
  • Internationalization support
  • Default formatters for array, collection, date and number types

valiktor-javamoney

jar javadoc sources

This module provides support for JavaMoney API types, including:

  • Validation constraints and functions for MonetaryAmount
  • Default formatter for MonetaryAmount

valiktor-javatime

jar javadoc sources

This module provides support for JavaTime API types, including:

  • Validation constraints and functions for LocalDate, LocalDateTime, OffsetDateTime and ZonedDateTime
  • Default formatter for all LocalDate, LocalDateTime, LocalTime, OffsetDateTime, OffsetTime and ZonedDateTime

valiktor-jodamoney

jar javadoc sources

This module provides support for Joda-Money API types, including:

  • Validation constraints and functions for Money and BigMoney
  • Default formatter for Money and BigMoney

valiktor-jodatime

jar javadoc sources

This module provides support for Joda-Time API types, including:

  • Validation constraints and functions for LocalDate, LocalDateTime and DateTime
  • Default formatter for all LocalDate, LocalDateTime, LocalTime and DateTime

valiktor-spring

jar javadoc sources

Spring WebMvc and WebFlux integration, including:

  • Configuration class to set a custom base bundle name
  • Exception handler for ConstraintViolationException from Valiktor
  • Exception handlers for InvalidFormatException and MissingKotlinParameterException from Jackson

valiktor-spring-boot-autoconfigure

jar javadoc sources

Provides auto-configuration support for valiktor-spring, including:

  • Configuration class based on properties
  • Spring WebMvc exception handlers
  • Spring WebFlux exception handlers

valiktor-spring-boot-starter

jar javadoc sources

Spring Boot Starter library including the modules valiktor-spring and valiktor-spring-boot-autoconfigure

valiktor-test

jar javadoc sources

This module provides fluent assertions for validation tests

Samples

Changelog

For latest updates see CHANGELOG.md file.

Contributing

Please read CONTRIBUTING.md for more details, and the process for submitting pull requests to us.

License

This project is licensed under the Apache License, Version 2.0 - see the LICENSE file for details.

valiktor's People

Contributors

iundarigun avatar jnfeinstein avatar mpe85 avatar otkmnb2783 avatar rodolphocouto avatar yo-galeria avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

valiktor's Issues

Spring *ExceptionHandler not working with valiktor-spring-boot-starter

Expected Behavior

ConstraintViolationException are automatically handled by ReactiveConstraintViolationExceptionHandler

Actual Behavior

ConstraintViolationException are handled by ExceptionHandlingWebHandler

Steps to Reproduce the Problem

  1. Use spring kofu setup
  2. Add org.valiktor:valiktor-spring-boot-starter:0.12.0 to deps.
  3. Send request which throws ConstraintViolationException
PS C:\Users\srawa> Invoke-RestMethod "http://localhost:8000/api/user" -Body $User2 -Method POST -ContentType "application/json" | ConvertTo-Json

Invoke-RestMethod: {"timestamp":"2020-07-19T14:17:47.153+00:00","path":"/api/user","status":500,"error":"Internal Server Error","message":"","requestId":"64027368-1","exception":"org.valiktor.ConstraintViolationException"}

Server logs:

2020-07-19 19:47:47.166 ERROR 13776 --- [ctor-http-nio-2] a.w.r.e.AbstractErrorWebExceptionHandler : [64027368-1]  500 Server Error for HTTP POST "/api/user"

org.valiktor.ConstraintViolationException: null
	at io.nooblabs.eventmanagerbackend.User.validate(EventmanagerBackendApplication.kt:62) ~[main/:na]
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	|_ checkpoint โ‡ข HTTP POST "/api/user" [ExceptionHandlingWebHandler]
Stack trace:

Example Code or Link to a Project

val app = reactiveWebApplication {
    beans {
        bean<UserHandler>()
        bean(::routes)
    }
    webFlux {
        port = if (profiles.contains("test")) 8081 else 8000
        codecs {
            string()
            jackson()
        }
    }
}

data class User(
        val login: String,
        val name: String
) {

    fun validate(): User {
        _validate(this) {
            validate(User::login)
                    .isNotBlank()
                    .hasSize(min = 4, max = 8)
            validate(User::name)
                    .isNotBlank()
                    .hasSize(max = 32)
        }
        return this
    }
}

class UserHandler {
    suspend fun createApi(request: ServerRequest): ServerResponse {
        return request.awaitBody<User>()
                .validate()
                .let { user -> ok().bodyValueAndAwait(user) }
    }
}

fun routes(userHandler: UserHandler) = coRouter {
    POST("/api/user", userHandler::createApi)
}


fun main() {
    app.run()
}

Environment

  • Valiktor version: 0.12.0
  • JDK/JRE version: adopt-openj9-14.0.1
  • Platform/OS: Win 10

Possibility to provide own error message to the custom .isValid checks

Is your feature request related to a problem? Please describe.

When for some reason one needs to use the simple .isValid { somePredicate } validation, one has no control over the error message that will flow upwards.

Describe the solution you'd like

It would be great with either another method, or extending isValid with an optional message which will be used for the exception that will be thrown in this case, such that we have some control over that, which is also extremely useful when using Spring Boot and the exception mapping.

Describe alternatives you've considered

Additional context

Add any other context or screenshots about the feature request here.

Question: What's the recommended approach for nested object validations?

I have a reasonably complex object graph, and want to be able to split up the validation of these objects into smaller unit testable chunks, yet retain the nested property structure provided when you validate inline such as the example:

try {
    validate(employee) {
        validate(Employee::company).validate {
            validate(Company::city).validate {
                validate(City::name).isNotEmpty()
            }
        }
    }
} catch (ex: ConstraintViolationException) {
    ex.constraintViolations
        .map { "${it.property}: ${it.constraint.name}" }
        .forEach(::println)
}

Is it possible to have the company.city validation in another class, yet called inline?

try {
    validate(employee) {
         //something like...
            companyValidator.doValidation(employee.company)
        
        }
    }
} catch (ex: ConstraintViolationException) {
    ex.constraintViolations
        .map { "${it.property}: ${it.constraint.name}" }
        .forEach(::println)
}

How to validate a list of strings

Hi

I'm currently building an API that accepts a list of Strings. And struggling at the moment on validating that the strings match a given pattern. And at most N are set.

Checking the length is easy

But how can I do the following:

data class Sample(val testData: List<String>) {

  init {
    validate(this) {
      validate(Sample::testData).hasSize(max = 2). ... ? Here I want to validate that the strings match a certain pattern
    }
  }
}

Would I have to write my own validator?

Best
Peter

Email Validation Broken

Expected Behavior

[email protected] should be a valid email. However, since the . is prior to the + the regex rejects this.

Actual Behavior

[email protected] is rejected because the . is prior to the +

Steps to Reproduce the Problem

  1. Validate a value with a . prior to a +
  2. The value is rejected but shouldn't be

Example Code or Link to a Project

validate("[email protected]")
            .isEmail()

Environment

  • Valiktor version: valiktor-core 0.12.0
  • JDK/JRE version: 1.8
  • Platform/OS: Mac OS

Links

Consider using a regular expression more conformant to RFC 5322 as documented here with a proposed RegEx here

hasSize not working as expected

Is your feature request related to a problem? Please describe.

I used hasSize(100) expecting the validation to be at most 100 characters long which isn't what's happening though.
The function is defined as:
hasSize(min: Int = Int.MIN_VALUE, max: Int = Int.MAX_VALUE)
which translates to hasSize(100, Int.MAX_VALUE).

Describe the solution you'd like

I think there should be a one parameter function hasSize(size: Int) to prevent these misunderstandings and the two parameter function should not have default values for min/max.

  • hasSize(size: Int) -> matches exact size / length
  • hasSize(min: Int, max: Int) -> matches size in [min, max]

Describe alternatives you've considered

To be even clearer with the intention I suggest:

  • hasSize(size: Int) -> matches exact size / length
  • hasSize(range: IntRange) -> matches size defined by IntRange

Add more sample applications

Sample projects are very important to help new users. Currently there are only Spring sample applications and therefore it's necessary to add others, like:

  • valiktor-sample-hello-world: A very simple project demonstrating how the Valiktor engine works.

  • valiktor-sample-nested-properties: A sample project demonstrating how to validate nested properties.

  • valiktor-sample-collections: A sample project demonstrating how to validate collections.

  • valiktor-sample-custom-constraint: A simple project demonstrating a custom constraint created with Valiktor.

  • valiktor-sample-custom-formatter: A simple project demonstrating a custom formatter created with Valiktor.

  • valiktor-sample-javatime: A simple project demonstrating validations with JavaTime API properties.

  • valiktor-sample-javamoney: A simple project demonstrating validations with JavaMoney API properties.

Violation of multiple constraints for a single property is not being reported.

Expected Behavior

If more than one validation constraint fails for a single property, in the ConstraintViolations Set are present multiple entries for that specific property and not just the first violated constraint.

Actual Behavior

The set of ConstraintViolations is filled just with the first violated constraint for that specific property.

Steps to Reproduce the Problem

  1. Validate a property with more than one constraint.
  2. Fill the property with a value that will violate the constraints.
  3. Print the set of violated constraints.

Example Code or Link to a Project

In the Validator.kt file the validate function in the if statement is checked if there's already a constraint violation for a specific property:

fun validate(constraint: (T?) -> Constraint, isValid: (T?) -> Boolean): Property<T> {
            val value = this.property.get(obj)
            if ([email protected] { it.property == this.property.name } && !isValid(value)) {
                [email protected] += DefaultConstraintViolation(this.property.name, value, constraint(value))
            }
            return this
        }

Environment

  • Valiktor version: 0.9.0
  • JDK/JRE version: 1.8
  • Platform/OS: macOs

Validator for value returned from a function

Is your feature request related to a problem? Please describe.

The problem space this feature request arises from is the following:

In my Spring Boot Kotlin project, I'm using OpenAPI generator to generate API clients. I am generating Java client code, because I need the clients to use the Spring WebClient under the hood (because of a special authentication setup) and there is (to my knowledge) no way to generate Kotlin client code based on Spring WebClient.

Now, I need to validate the responses from those clients which are returned in classical PoJos with private members and public getters. Unfortunately, I can neither validate those PoJos with:

validate(response) {
    validate(ApiResponse::id).isNotBlank()
}

nor with:

validate(response) {
    validate(ApiResponse::getId).isNotBlank()
}

because both of those references point to KFunction and not a KProperty.

Describe the solution you'd like

The coolest way would be to have a validate function which takes a KFunction instead of a KProperty and validates the returned value in the same look and feel as the property validation.

Describe alternatives you've considered

Right now I'm using https://github.com/rcapraro/kalidation because it allows me to do something like that:

constraints<ApiResponse>{
    returnOf(ApiResponse::getId) {
        notBlank()
    }
}

Nonetheless, I think that the feature set of valiktor is way better. Therefore, using kalidation feels like a step backwards.

Compare two properties between each other

Hi! I have a question. Let's make little example

data class Foo(val name: String, val bar: List<Bar>) {

data class Bar(val first: Int, val second: Int)

}

and than I would like to validate, that property first in class Bar less than second

fun validation(foo: Foo) {
  validate(foo) {
    validate(Foo::bar).validateForEach {
      validate(Foo.Bar::first).isLessThan(Foo.Bar::second) // It's wrong, just as an example
    }
  }
} 

So problem is how can I compare my two properties between each other? How i can get value of another property? Thx

Implement an exception handler for com.fasterxml.jackson.databind.exc.InvalidFormatException

Problem:

Jackson has a specific exception that is thrown when the data is invalid, e.g an enum constant that does not exist or a boolean value different than true / false.

These cases can be handled by Valiktor to return the status code 422 with a custom payload, following the ideia of ValiktorJacksonExceptionHandler and ValiktorJacksonReactiveExceptionHandler.

Solution:

Implement a custom exception handler for InvalidFormatException.

Add support for Joda-Money

Joda-Money provides simple value types, representing currency and money.

Valiktor already provides a module for Java Money API support and it should provide a module (valiktor-jodamoney) with constraints and validation functions for Joda-Money types.

valiktor-spring-boot-starter doesn't work with spring boot 3

Expected Behavior

Error Message

Http failure response for http://localhost:8087/api/codes: 422 OK

Additional Error Information


{
  "errors": [
    {
      "property": "codeClearing",
      "value": "098764321",
      "message": "Size must be greater than or equal to 10",
      "constraint": {
        "name": "Size",
        "params": [
          {
            "name": "min",
            "value": 10
          }
        ]
      }
    },
    {
      "property": "fv",
      "value": 1.2,
      "message": "Decimal digits must be greater than or equal to 2",
      "constraint": {
        "name": "DecimalDigits",
        "params": [
          {
            "name": "min",
            "value": 2
          }
        ]
      }
    },
    {
      "property": "end",
      "value": "2022-10-28",
      "message": "Must be greater than 2022-12-07",
      "constraint": {
        "name": "Greater",
        "params": [
          {
            "name": "value",
            "value": "2022-12-07"
          }
        ]
      }
    }
  ]
}

Actual Behavior

Error Message

Http failure response for http://localhost:8087/api/codes: 500 OK

Additional Error Information

{
  "timestamp": "2022-12-07T15:28:16.641+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "trace": "org.valiktor.ConstraintViolationException\n\tat com.budgetbox.ref.clearing.model.CodeKt.validateCodeDocument(Code.kt:51)\n\tat com.budgetbox.ref.clearing.model.CodeEventHandler.handleBeforeCreates(RefRepository.kt:82)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:281)\n\tat org.springframework.data.rest.core.event.AnnotatedEventHandlerInvoker.onApplicationEvent(AnnotatedEventHandlerInvoker.java:83)\n\tat org.springframework.data.rest.core.event.AnnotatedEventHandlerInvoker.onApplicationEvent(AnnotatedEventHandlerInvoker.java:48)\n\tat org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176)\n\tat org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169)\n\tat org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143)\n\tat org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:413)\n\tat org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:370)\n\tat org.springframework.data.rest.webmvc.RepositoryEntityController.createAndReturn(RepositoryEntityController.java:469)\n\tat org.springframework.data.rest.webmvc.RepositoryEntityController.postCollectionResource(RepositoryEntityController.java:262)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:973)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1003)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:906)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:731)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:880)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\n\tat org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:109)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1739)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\n",
  "message": "No message available",
  "path": "/api/codes"
}

Steps to Reproduce the Problem

  1. Migration spring boot 2 to spring boot 3

Example Code or Link to a Project

Environment

  • Valiktor version: 0.12.0
  • JDK/JRE version: 17
  • Platform/OS: Ubuntu

make validate() also accept bound properties

Is your feature request related to a problem? Please describe.

For brevity, I would like to be able to avoid writing the class name over over and over again inside my validation block, i.e.:

    validate(this) {
        validate(OrganizationAdminsitrator::name).isNotNull().isNotBlank()
        validate(OrganizationAdminsitrator::taxId).isNotNull().isNotBlank()
        validate(OrganizationAdminsitrator::phone).isNotNull().isNotBlank()
        ...
    }

Describe the solution you'd like

I wonder if it would be possible to make validate also accept references to properties bound to objects, like so:

    validate(this) {
        validate(it::name).isNotNull().isNotBlank()
        validate(it::taxId).isNotNull().isNotBlank()
        validate(it::phone).isNotNull().isNotBlank()
        ...
    }

Describe alternatives you've considered

I have only dabbled around the source code for valiktor, but I suppose that one solution would be for validate() functions to accept KProperty0<T?> as well. I can try to hammer away something on a PR if you think it'd be worth it.

Add support for Joda-Time

Joda-Time is the de facto standard date and time library for Java prior to Java SE 8 and it is still widely used in many projects, especially on Android.

Valiktor should provide a module (valiktor-jodatime) with constraints and validation functions for Joda-Time types.

Question: How do I validate dependent fields - Multiple fields in single validation logic

I am not able to figure out how to validate dependent fields e.g.

data class Vehicle {
    val name: String,
    val type: VehicleType,       // VehicleType is either TWO_WHEELER or FOUR_WHEELER
    val fuel: FuelType              // FuelType is either PETROL or DIESEL
}

There should be a easy way to define a constraint such that if vehicle.type is VehicleType.TWO_WHEELER, vehicle.type cannot be FuelType.DIESEL

I have no idea how to achieve this right now.

Support for JSR303

Is your feature request related to a problem? Please describe.

We see no mention of it on the github/issues/etc, but do you support JSR303?

Describe the solution you'd like

Support for JSR303

Additional context

JSR303 is important for validation on http endpoints to return proper error codes.

Add support for custom exception handlers

The default exception handler is very helpful, but in some cases we need to implement custom responses with custom status code, headers and payload.

The valiktor-spring module provides exception handlers for Valiktor and Jackson exceptions. To avoid implementing all of them, it would be nice to have a converter to customize status code, headers and payload in one place.

Remove message key prefix restriction

Problem:

Currently there is a rule where the message key must starts with the prefix "org.valiktor". This restriction is unnecessary because custom constraints do not start with this prefix.

Solution:

Remove the restriction here.

isWebsite does not support internal domains or ports

Expected Behavior

Urls like "http://localhost:8000" or ""http://foo.com:8080" pass isWebsite() validator.

Actual Behavior

Any URL with a port or without a domain suffix will fail the regex matching.

Steps to Reproduce the Problem

  1. Create a string validator which uses isWebsite (eg: validate(MyCommand::myUrl).isNotEmpty().isWebsite()
  2. Pass it a value with a port or an internal domain name,

Example Code or Link to a Project

data class MyCommand(
    val url1: String,
    val url2: String,
) : BaseCommand() {
    init {
        validate(this) {
            validate(MyCommand::url1).isNotEmpty().isWebsite()
            validate(MyCommand::url2).isNotEmpty().isWebsite()
        }
    }
}

Json Payload

{
    "url1": "http://localhost/",
    "url2": "http://mydomain.com:8000"
}

Environment

  • Valiktor version: 0.12.0
  • JDK/JRE version: 17
  • Platform/OS: Windows

Using validate(this) in init occurs error when it comes to open / abstract class

Expected Behavior

Probably this report is due to my lack of proficiency in Kotlin. I want to validate values of a constructor in abstract or open class. If there is no way to achieve it as below. Please guide me how to do the same thing.

abstract class Quantity(open val value: Int) : BaseValueObject() {
    init {
        validate(this) {
            validate(Quantity::value).hasDigits()
        }
    }
}

Actual Behavior

image

Steps to Reproduce the Problem

  1. Write abstract class as above

Example Code or Link to a Project

As above

Environment

  • Valiktor version: 0.12.0
  • JDK/JRE version: Open JDK 11
  • Platform/OS: Mac

Implement nicer stacktrace

Is your feature request related to a problem? Please describe.

I often write few happy path examples for validation. However, if we hit an error when running them stacktrace is not helpful. We get

org.valiktor.ConstraintViolationException
	at org.valiktor.ValidatorKt.validate(Validator.kt:38)

It would be good to know what happened by just looking into stracktrace

Describe the solution you'd like

Maybe implementing toString in ConstraintViolationException would suffice.

Alternative solution

Maybe adding shouldPassValidation method to test framework would be even better?

Validation of uninitialized properties

Validation fails with UninitializedPropertyAccessException if I try to validate uninitialized lateinit variable.
For example:

class A {
    lateinit var v: String
}
validate(A()) {
    validate(A::v).isNotEmpty()
}

fails with exception kotlin.UninitializedPropertyAccessException: lateinit property v has not been initialized

I haven't found any solution in documentation for this situation (please correct me if I'm wrong). So here is my solution:

Create Uninitialized constraint and update validate method in Property to catch the exception.
Unitialized constraint is quite simple:

object Uninitialized : Constraint

Then new message should be added to messages.properties:

org.valiktor.constraints.Uninitialized.message=Must be initialized

And finally validate method can be updated like this:

fun validate(constraint: (T?) -> Constraint, isValid: (T?) -> Boolean): Property<T> {
    val value = try {
        this.property.get(this.obj)
    } catch(e: UninitializedPropertyAccessException) {
        [email protected] += DefaultConstraintViolation(
                property = this.property.name,
                value = null,
                constraint = Uninitialized
        )
        return this
    }
    if (!isValid(value)) {
        [email protected] += DefaultConstraintViolation(
            property = this.property.name,
            value = value,
            constraint = constraint(value)
        )
    }
    return this
}

Similarly coValidate should be updated as well.

Implement a path structure for property constraint violation

Actually, the property from ConstraintViolation is a path composed of all nested properties separated by . similar to JSONPath, e.g: employee.dependents[0].name.

This structure is not type-safe and it is not flexible. An important improvement would be to implement a linked list or a recursive structure to store each property and its respective parent and convert to JSONPath only when it is necessary, like JSON payloads.

A good inspiration for this feature is Path interface from Bean Validation API.

validation by number of matches for list

Is your feature request related to a problem? Please describe.

I have a list of abstract instances and I want to validate the exactly 1 and only a element is of subtype X.

Describe the solution you'd like

To validate 1 match, I have to do something like:

validate(SomeEntity::myList)
    .validate { it.single { listItem -> listItem is MySpecificSubClass } }

For N matches I would have to add a counter or something.

Describe alternatives you've considered

I have to keep like an external counter or so.

Additional context

No additional data

Support MultiPlatform

Hi
I was wondering if there was a desire to make this MultiPatform? Or is there anything that prevents it from supporting multiplatform?

Regards

Add Support for Map<K, V>

Is your feature request related to a problem? Please describe.

When validating a Map there isn't a way to run using this lib. It doesn't recognize the entirely map only if i pass the Set or Collection.

Describe the solution you'd like

A validation for keys apart from values and vice versa to check the map content with the parameter value.

val metadata = mapOf("key1" to "value1", "key2" to "value2")
validate(medata).containsKey("key1")
validate(medata).containsValue("value1")
validate(medata).doesNotContainsKey("key2")
validate(medata).doesNotContainsValue("value2")
validate(medata).containsAllKeys(listOf("key1","key2"))
validate(medata).containsAllValues(listOf("value1","value2"))
validate(medata).containsAllKeys("key1","key2")
validate(medata).containsAllValues("value1","value2")
validate(medata).doesNotContainsAllKeys(listOf("key1","key2"))
validate(medata).doesNotContainsAllValues(listOf("value1","value2"))
validate(medata).doesNotContainsAllKeys("key1","key2")
validate(medata).doesNotContainsAllValues("value1","value2")

Describe alternatives you've considered

Maybe using entries to get both of then, but it would be a problem.

Add validateForEachIndexed function on collections and arrays

Currently collections and arrays can be validated through validateForEach, but there is not a way to access the index of the current element within this function.

My suggestion is to create another function called validateForEachIndexed that can provide the current index and value as parameters:

validate(employee) {
    validate(Employee::dependents).validateForEachIndexed { index, dependent ->        
        validate(Dependent::name).isNotEmpty()
    }
}

Does the library supports ktor framework?

Is your feature request related to a problem? Please describe.

i use valiktor with ktor framework,i use it to check client request params when i call call.receive<UserLoginRequest>() function to receive an invalidate param,it not throw a ConstraintViolationException.

Describe the solution you'd like

I expect it to throw an exception when i call call.receive<UserLoginRequest>() function with an invalidate request param.

Describe alternatives you've considered

may be this library not support ktor framework.

Additional context

data class UserLoginRequest(val username: String, val password: String) {
    init {
        validate(this) {
            validate(UserLoginRequest::username).hasSize(6, 20)
            validate(UserLoginRequest::password).hasSize(6, 16)
        }
    }
}

Add isIn and isNotIn on Map validation

Is your feature request related to a problem? Please describe.

I'm trying to use this feature to validate if the map keys contains all my parameters list of keys to evaluate.

Describe the solution you'd like

val metadata: Map<String, String> = mapOf()
validate(metadata).isInKeys("key1", "key2")
validate(metadata).isInKeys(listOf("key1", "key2"))
validate(metadata).isInKeys(setOf("key1", "key2"))
validate(metadata).isNotInKeys("key1", "key2")
validate(metadata).isNotInKeys(listOf("key1", "key2"))
validate(metadata).isInValues("value1", "value2")
validate(metadata).isInValues(listOf("value1", "value2"))
validate(metadata).isInValues(setOf("value1", "value2"))
validate(metadata).isNotInValues(listOf("value1", "value2"))
validate(metadata).isNotInValues("value1", "value2")

Describe alternatives you've considered

Create a extension function to use for myself, using doesNotContainsAllKeys or doesNotContainsAllKeys

Additional context

N/A

Validate a collection of primitive values

Is your feature request related to a problem? Please describe.

Giving Valiktor a try and it's not clear to me how to validate a collection of primitive values. For example, is it not possible to validate each value in a Set<String> with isNotBlank()?

Describe the solution you'd like

Perhaps something like:

data class Request(val emailAddresses: List<String>? = null)

validate(Request(listOf("[email protected]", "goodbye")) {
   validate(Request::emailAddresses)
    .isNotNull()
    .hasSize(min = 1)
    .eachIsNotBlank()
    .eachIsEmail()
}

Describe alternatives you've considered

Otherwise it seems I have to make the type for the collection a value or data class with one field:

value class StringValue(val value: String)

data class Request(val emailAddresses: List<StringValue>? = null)

validate(Request(listOf("[email protected]", "goodbye")) {
   validate(Request::emailAddresses)
    .isNotNull()
    .hasSize(min = 1)
    .validateForEach {
      validate(StringValue::value)
        .isNotBlank()
        .isEmail()
    }
}

Additional context

While I don't personally mind value classes like this, it's not exactly leading to the API design I'd like for JSON/HTTP interfaces.

Load resource bundles with context class loader

Problem

Using Valiktor 0.12.0, I added some custom constraints including a custom resource bundle for i18n. Therefore, I implemented a custom interface extending the Constraint interface and overrode the messageBundle property as a base interface for my constraints.

My application is running on Quarkus 1.13 and the validation messages from the default and the custom resource bundle are loaded when running the application from the JAR file or in tests, using ConstraintViolation.toMessage(). However, Quarkus offers a development mode (see https://quarkus.io/guides/getting-started#development-mode) which provides things like hot deployment and against which you usually work during development. In dev mode the lookup of the resource bundle does not work, though it is correctly located in the classpath.

The root cause seems to be a class loader issue since I can successfully look the resource bundle up in my code, but it doesn't work from the classes in the Valiktor lib.

I can imagine that this problem could also appear in other contexts with special class loading behaviour like application servers or OSGi.

Solution

I found the following Issue in the Quarkus Github Repo (quarkusio/quarkus#8103) where they propose to always look up resource bundles passing the context class loader:

ResourceBundle.getBundle(baseName, locale, Thread.currentThread().getContextClassLoader())

Could Valiktor be changed so that resource bundles are always looked up like described or maybe add this as an alternative when the existing mechanism cannot find a bundle?

Alternatives

As a workaround, we are using Valiktor now without i18n, which is not a long term solution.

Validate receiver object properties to shorten the code

Problem

Currently we have to provide references to a class properties to validate like that

data class LongLongLongClassNameEmployee(val id: Int, val name: String, val email: String) {
    init {
        validate(this) {
            validate(LongLongLongClassNameEmployee::id).isPositive()
            validate(LongLongLongClassNameEmployee::name).hasSize(min = 3, max = 80)
            validate(LongLongLongClassNameEmployee::email).isNotBlank().isEmail()
        }
    }
}
  • It becomes very annoying when class names are long.
  • It is also confusing and questionable: why we have to provide type if we already work with an instance of this class provided through validate(this)?

The problem is that we can't currently provide property references like that:

data class LongLongLongClassNameEmployee(val id: Int, val name: String, val email: String) {
    init {
        validate(this) { employee ->
            validate(employee::id).isPositive()
            validate(employee::name).hasSize(min = 3, max = 80)
            validate(employee::email).isNotBlank().isEmail()
        }
    }
}

or even like this:

data class LongLongLongClassNameEmployee(val id: Int, val name: String, val email: String) {
    init {
        validate(this) {
            validate(it::id).isPositive()
            validate(it::name).hasSize(min = 3, max = 80)
            validate(it::email).isNotBlank().isEmail()
        }
    }
}

This is due to a limitation of Validator implementation that works only with KProperty1 property types of a Kotlin Reflect package.

When we reference class property Kotlin returns KProperty1 type which is now used in Validator implementation.
When we reference class instance property Kotlin returns KProperty0 type which is not supported by Validator implementation.

Possible solution

Validator could work with both KProperty0 and KProperty1 types (by overloading validate functions) that will give us the way to get property from a validation receiver directly

Suggested working test:

@Test
    fun `should validate by KProperty0`() {
        validate(Employee(company = Company(1))) {
            validate(it::company).isNotNull().validate { company ->
                validate(company::id).isNotNull()
            }
            validate(it::id).isNull()
            validate(it::address).isNull()
            validate(it::name).isNull()
        }
    }

Question: Is it possible to access the validated property's value in the custom messages?

I was wondering if it's possible to access the validated property's value when creating custom messages files.
For example, imagine an address entity

data class Address(
    val street: String,
    val state: String,
    val zipcode: String
)

In addition, I created a custom validation that checks that the zipcode is of a given length (user provided):

data class IsOfLength(val allowedLength: Int?) : Constraint

fun <E, T> Validator<E>.Property<T?>.isOfLength(allowedLength: Int?) = this.validate(IsOfLength(allowedLength)) { it.length == allowedLength }

And finally my messages_en.properties file:

com.myProject.constraints.IsOfLength.message=zipcode {propertyValue} must be of length {allowedLength}

Now when I validate the address:

val myAddress = Address("street", "NY", "12345")

validate(entity){
    validate(Address::zipcode).isOfLength(5)
}

The message I would want to see is:
zipcode 12345 must be of length 5
However, propertyValue is obviously not part of the constraint params.

My question is, did I miss something and we can easily access it in our custom message file, or the only way to do it is to pass the property value directly to the constraint and use it like that.
e.g.

IsOfLength(val propertyValue: String, val allowedLength: Int?)

This would work but we have tons of validations and making a change to all of them to add the property value is cumbersome.

An alternative approach is to just format the message text upon handling the errors in the ValidationExceptionHandler class.
Since I can access the Constraint.value in the handle function there, I could inject the value directly in the Constraint.message using a simple string format.
It's significantly better than updating all the validations we already have, but I lose the custom formatters functionality valiktor provides by doing it, and it is still something I'd like to avoid if such functionality already exists and I just missed it.

Thanks in advance!

Non-exception-based API

Is your feature request related to a problem? Please describe.

This library looks very interesting! But with exceptions generally being sophisticated GOTO statements, using them for validations feels very wrong. Especially since validation failure is not really an exception at all: it is expected to happen on a regular basis and part of the regular business process.

Describe the solution you'd like

I'd like an API that returns some kind of ValidationResult object which can be either "OK" or "Failed" and which contains all the information about the failure(s), including error messages and failed values.

Personally I really like the API of Kalidation which uses arrow.core.Validated (to be replaced with a regular Either in upcoming Arrow releases) - although in turn its many dependencies give it a much bigger footprint than Valiktor.

Describe alternatives you've considered

I was thinking about creating a wrapper method that catches the exception and encapsulates the information in a validation result object. Looking at #75 I'm not sure that it would be possible to get all the information from the exception, though.

Additional context

Expected an exception of class org.valiktor.ConstraintViolationException to be thrown, but was completed successfully

Hi all,

We have a problem with our Tests in maven with version 0.11.0 and 0.12.0, in Kotlin.
It works on my env, it works on another dev env.
It doesn't work in Jenkins and on another developper env !
I cannot reproduce this problem on my environnement, it's crazy !

If you have any idea, I hope you will be able to help us :-)

Reported error is :
[ERROR] Failures:
[ERROR] RecommendationDocumentTest.Assert Validate Recommendation with empty code throw ConstraintViolationException:89 Expected an exception of class org.valiktor.ConstraintViolationException to be thrown, but was completed successfully.
[ERROR] RecommendationDocumentTest.Assert Validate Recommendation with null code throw ConstraintViolationException:75 Expected an exception of class org.valiktor.ConstraintViolationException to be thrown, but was completed successfully.

We have a data class with a validate function. In our tests we expect to validate that the validate function
works well for different usecase.

`
// the validate function in the object
fun validateRecommendationDocument(recommendationDocument: RecommendationDocument) {
validateBrief(recommendationDocument.brief)

validate(recommendationDocument) {
    validate(RecommendationDocument::code).isNotNull().isNotBlank()

    if (!recommendationDocument.id.isNullOrBlank()) {
        validate(RecommendationDocument::createdBy).isNotNull().isNotBlank()
        validate(RecommendationDocument::creationDate).isNotNull()
    }
}

}

// Tests

@Test
fun `Assert Validate Recommendation with null code throw ConstraintViolationException`() {
    shouldFailValidation<RecommendationDocument> {
        validateRecommendationDocument(validReco.copy(code = null))
    }.verify {
        expect(RecommendationDocument::code, null, NotNull)
    }
}

@Test
fun `Assert Validate Recommendation with empty code throw ConstraintViolationException`() {
    shouldFailValidation<RecommendationDocument> {
        validateRecommendationDocument(validReco.copy(code = ""))
    }.verify {
        expect(RecommendationDocument::code, "  ", NotBlank)
    }
}

`

Environment

  • Valiktor version: 0.11.0 / 0.12.0
  • JDK/JRE version: Java version: 11.0.8, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
  • Platform/OS: Linux

Provide a sample for Kotlin/JS

Is your feature request related to a problem? Please describe.

My Gradle build script is configured to produce JavaScript code (id("org.jetbrains.kotlin.js") version "1.3.70-eap-274"). I tried adding valiktor-core to the dependencies but I could not import anything from org.valiktor.*

Describe the solution you'd like

Probably, I'm doing it wrong or it does not support JavaScript yet. So, please add a JS-related sample or add support for this target build type.

Thank you.

Add property as parameter in nested and collection validation functions

Valiktor can validate nested objects and properties recursively using validate function:

validate(employee) {
    validate(Employee::company).validate {
        validate(Company::city).validate {
            validate(City::name).isNotEmpty()
        }
    }
}

Array and collection properties can also be validated, including its elements through validateForEach function:

validate(employee) {
    validate(Employee::dependents).validateForEach {
        validate(Dependent::name).isNotEmpty()
    }
}

Actually the both functions (validate and validateForEach) don't receive any parameter in the inner function. My suggestion is to provide the current object as parameter to make the validations more flexible and powerful:

validate(employee) { employee -> 
    validate(Employee::company).validate { company -> 
        validate(Company::city).validate { city -> 
            validate(City::name).isNotEmpty()
        }
    }
}
validate(employee) { employee ->
    validate(Employee::dependents).validateForEach { dependent ->        
        validate(Dependent::name).isNotEmpty()
    }
}

Ability to validate strings as numbers

Is your feature request related to a problem? Please describe.

Ability to validate if a string is a number.
Additionally, if it is a number, ability to validate as a number.
eg: if it's positive, is less than or greater than x, etc.

Describe the solution you'd like

Assuming a system which receives parameters as strings via REST.
(which is actually all http requests, as string, and string arrays, are the only parameter type).

I'd like to be able to validate as follows:

validate(Employee::age).isNumber().isPositive()

Additional context

Add any other context or screenshots about the feature request here.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.