Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Skrutiny

A Kotlin validation library ready for Kotlin Multiplatform (KMP). Skrutiny inspects class instances at runtime, validates property values against library annotations, performs recursive validation on nested objects and collections, and allows users to register custom annotations and validators.

Latest version

skrutiny = { module = "com.elevup:skrutiny:skrutiny", version.ref = "skrutiny" }

Features

  • Runtime Annotation-based Validation: Reads annotations directly from properties at runtime.
  • Kotlin Multiplatform Ready: Core validation interfaces, annotations, registry, and models are in commonMain; runtime reflection is provided via kotlin-reflect on JVM/Android.
  • Recursive Validation:
    • Automatically inspects nested complex objects and data classes.
    • Automatically traverses items in Iterable / List / Set / Array and values in Map.
    • Identity-based cycle detection avoids infinite recursion on circular object graphs.
  • Sealed ValidationResult with Nested Valid / Invalid:
    • ValidationResult is a sealed class with nested ValidationResult.Valid and ValidationResult.Invalid.
    • Violations and errors are contained only in ValidationResult.Invalid.
  • Built-in Constraints:
    • Numbers & Decimals:
      • @Min(value = "...", inclusive = true/false)
      • @Max(value = "...", inclusive = true/false)
      • @Range(min = "...", max = "...", minInclusive = true/false, maxInclusive = true/false)
      • Exact precision for Byte, Short, Int, Long, Float, Double, and Java's BigInteger and BigDecimal.
      • Configurable inclusivity and exclusivity.
    • Strings:
      • @Length(min, max) – English error format: "Length of {field_name} is {X}, must be between {A} and {B}"
      • @Pattern(regex) – Regular expression matching with IntelliJ IDEA language injection (@Language("RegExp")) for auto-coloring, live syntax checking, and "Check RegExp" tool.
      • @Email – Email address validation
      • @NotEmpty – Non-empty check (length/size > 0)
      • @NotBlank – Non-blank check (contains non-whitespace characters)
    • Hierarchy: @Valid – Explicit nested validation
      • Takes effect only when autoInspectNestedObjects is set to false.
  • Consistent Default Error Messages: Clean, standardized English error messages with property paths (user.address.street, items[0].quantity).
  • Extensible: Easily register custom annotations and custom validator logic via classes or Kotlin DSL.

Quick Start

1. Define Validated Models

import com.elevup.skrutiny.annotations.*
import java.math.BigDecimal
import java.math.BigInteger

data class ProductPricing(
    @NotBlank
    val sku: String,

    @Pattern(regex = "^[A-Z]{3}-[0-9]{4}$")
    val productCode: String,

    @Min("0.5")
    val taxRateFloat: Float,

    // Exclusive lower bound (price > 1.00), inclusive upper bound (price <= 999.99)
    @Range(min = "1.00", max = "999.99", minInclusive = false, maxInclusive = true)
    val priceDouble: Double,

    @Min("100")
    val stockCount: BigInteger,

    @Min("0.01", inclusive = true)
    val minOrderAmount: BigDecimal
)

2. Validate

import com.elevup.skrutiny.api.ValidationResult
import com.elevup.skrutiny.api.Validator

val validator = Validator()
val pricing = ProductPricing(
    sku = "SKU-99",
    productCode = "ABC-1234",
    taxRateFloat = 0.2f, // Min is 0.5
    priceDouble = 1.00,  // Fails because minInclusive is false
    stockCount = BigInteger.valueOf(50), // Min is 100
    minOrderAmount = BigDecimal("0.005") // Min is 0.01
)

val result = validator.validate(pricing)

// Exhaustive pattern matching over nested sealed classes:
when (result) {
    is ValidationResult.Valid -> println("Product pricing is valid!")
    is ValidationResult.Invalid -> {
        println("Validation failed with ${result.violations.size} violations:")
        result.violations.forEach { v ->
            println("- ${v.propertyPath}: ${v.message} (got: ${v.invalidValue})")
        }
    }
}

Registering Custom Annotations and Validators

@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class StartsWith(val prefix: String)

class StartsWithValidator : ConstraintValidator<StartsWith, CharSequence> {
    override fun isValid(annotation: StartsWith, value: CharSequence?, context: ValidationContext): Boolean {
        if (value == null) return true
        return value.startsWith(annotation.prefix)
    }

    override fun formatErrorMessage(annotation: StartsWith, value: CharSequence?, context: ValidationContext): String {
        return "${context.propertyName} with value '$value' must start with '${annotation.prefix}'"
    }
}

val validator = Validator {
    configureRegistry {
        register(StartsWith::class, StartsWithValidator())
    }
}

Build & Verification

./gradlew check

About

A runtime validation library for Kotlin

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages