Skip to content

Repository files navigation

requisite

requisite makes data-handling requirements visible in Kotlin types. It is a small, dependency-free Kotlin Multiplatform library for JVM and Kotlin/JS.

import io.github.slepp.requisite.*

fun findCustomer(id: Tainted<Long, Trusted>): Customer = TODO()

val customer =
    Tainted
        .fromInput(request.queryParameters["customer"])
        .trySanitize { raw -> runCatching { requireNotNull(raw).toLong() } }
        .fold(onSuccess = ::findCustomer, onFailure = ::rejectRequest)

The receiving function states the requirement. Callers must perform the named transition before the value has the required type.

API Purpose
Tainted<T, Untrusted> / Tainted<T, Trusted> route input through a destination policy
TrustedSink<T, R> store or pass a handler that only accepts trusted values
Confident<T> / Gate<T> validate a probability and choose an exhaustive action tier
Certain authorize high-confidence operations with a library-issued token
Fresh<T> / Freshness<T> check a per-value monotonic TTL and preserve stale values for recovery

Installation

The planned Maven Central coordinates for release 0.1.0 are:

dependencies {
    implementation("io.github.slepp:requisite:0.1.0")
}

JVM-only builds that do not use the Kotlin Multiplatform plugin can depend on io.github.slepp:requisite-jvm:0.1.0.

The JVM artifact is Kotlin-first. Java can consume the emitted classes and the trust SAM API, but there is no separate Java facade for Kotlin Result or Duration signatures.

Kotlin/JS consumers use the multiplatform coordinates from Kotlin Gradle source sets. requisite-js is a Kotlin/JS KLIB publication; this project does not build an npm package or export a plain JavaScript/TypeScript API.

Until Central publication is configured, use mavenLocal() after:

./gradlew -PreleaseVersion=0.1.0 publishToMavenLocal

Alternatively, depend on the project directly.

Trust transitions and typed sinks

fun load(id: Tainted<Long, Trusted>): Customer = database.find(id.trustedValue())

val parsed =
    Tainted
        .fromInput(" 42 ")
        .trySanitize { text -> runCatching { text.trim().toLong() } }

val customer = parsed.fold(onSuccess = ::load, onFailure = ::rejectRequest)

sanitize handles an infallible policy. trySanitize uses Kotlin Result for parsing or validation. Both policies may change the value type.

Use an ordinary function parameter for most sinks. TrustedSink is a SAM type for a handler that must be stored or passed:

val lookup = TrustedSink<Long, Customer> { id -> database.find(id.trustedValue()) }
val customer = trustedId.sendTo(lookup)

inspect() reads a value without changing its state. widen() explicitly lowers Trusted to Untrusted; there is no upward cast. The library does not offer a general trusted map: an arbitrary transform could mix new untrusted data into a trusted representation.

These types protect ordinary, type-checked Kotlin and Java call sites. Kotlin unchecked casts, reflection, generated bytecode, deserialization tricks, or other deliberate type-system bypasses can forge generic state. Treat the wrappers as reviewable application contracts, not as a sandbox for hostile code.

TrustState is sealed for Kotlin exhaustiveness, but Java 11 bytecode cannot encode JVM sealed-interface permits. Java source can therefore implement another TrustState. It still cannot construct a trusted Tainted value through ordinary APIs, but an exhaustive Kotlin when over an arbitrary Java-supplied TrustState can encounter an unexpected implementation.

Confidence gates

val forecast = Confident.of("strong aurora", 0.97).getOrThrow()

val outcome =
    when (val gate = forecast.gate()) {
        is Gate.HighConfidence -> wake(gate.proof, gate.value)
        is Gate.Likely -> buzz(gate.value)
        is Gate.Unsure -> record(gate.value)
    }

Confident.of accepts only finite probabilities in 0.0..1.0. Gate is sealed, so when is exhaustive. Its high tier carries Certain; users cannot construct that token or a gate tier directly.

The defaults are likely at 0.60 and certain at 0.95. Custom thresholds are validated:

val thresholds = Thresholds.of(likely = 0.75, certain = 0.99).getOrThrow()
val gate = forecast.gateWith(thresholds)

The certain threshold cannot be lower than 0.95, keeping the token's meaning consistent across consumers.

Certain is intentionally a small capability, not a dependent proof. A token means that some library gate met a threshold of at least 0.95; it is reusable and is not bound to a value, model, request, time, or action. Keep the token and gated value together, and introduce a domain-specific authorization type when that stronger binding matters.

Constructors and internal issuers are hidden from ordinary Java source with private constructors and @JvmSynthetic. Reflection, handcrafted bytecode, and Java nullability violations remain outside the guarantee.

Monotonic freshness

import kotlin.time.Duration.Companion.seconds

val quote = Fresh.fetch(price, 30.seconds)

when (val checked = quote.check()) {
    is Freshness.Current -> charge(checked.value)
    is Freshness.Stale -> {
        audit(checked.staleValue.value)
        val retried = fetchPrice()
    }
    is Freshness.Unavailable -> {
        recordClockFailure(checked.error)
        val retried = fetchPrice()
    }
}

Each Fresh value owns its fetch mark and TTL. Age uses Kotlin's monotonic TimeSource abstraction rather than storing wall-clock timestamps. Freshness.Stale retains T, its age, and its TTL for refresh, audit, or fallback logic. It deliberately cannot reset its own TTL: recovery must perform the application's real fetch.

check() is exhaustive for ordinary clock failures. A TimeMark exception becomes Freshness.Unavailable with InvalidTimeMark; a negative elapsed reading becomes InvalidFetchTime and fails closed. Positive infinite ages and TTLs are handled without undefined infinity - infinity arithmetic. fold() accepts current, stale, and unavailable handlers.

Fresh.fetchedAt accepts an existing TimeMark for caches and deterministic tests, and rejects future or invalid marks.

Fresh.fetch throws InvalidTtl for a negative TTL and propagates an exception from a custom TimeSource.markNow() because no usable Fresh value exists yet. Once constructed, check() turns ordinary mark-reading exceptions into the typed unavailable branch; fatal platform errors are not intercepted.

Kotlin/JVM uses the platform monotonic source. On Kotlin/JS, Kotlin uses process.hrtime() on Node.js, performance.now() in browsers when available, and falls back to Date.now(). That last fallback is wall-clock based: a backward jump is detected and fails closed, while a forward jump can expire a value early.

Deliberate omissions

The Rust library's Live / with_live API is not ported. Kotlin cannot express the higher-ranked lifetime that prevents a scoped value from escaping a closure. A wrapper or contracts-based imitation would look safer than it is, so this library offers no approximation.

Arrow was evaluated for Either and Raise interop. The core uses Kotlin Result and sealed results instead: adding Arrow to every target would not strengthen a type contract and would force a dependency on users who do not otherwise need it. Arrow applications can adapt at their boundaries:

fun <T> Result<T>.toEither(): arrow.core.Either<Throwable, T> =
    fold(
        onSuccess = { arrow.core.Either.Right(it) },
        onFailure = { arrow.core.Either.Left(it) },
    )

No Arrow types appear in the published API.

Development

The build uses JDK 21. Kotlin and Java compilation target Java 11 with Java --release 11; compiler warnings fail the build. Signing validation also requires Bash and GnuPG.

./gradlew ktlintCheck allTests build checkKotlinAbi
./gradlew dokkaGeneratePublicationHtml publishAllPublicationsToBuildRepository
./gradlew :examples:payment-flow:run
scripts/verify-publications.sh
scripts/signing-smoke.sh
scripts/reproducibility-smoke.sh

Runtime tests run on JVM and Node.js. JVM tests also invoke the Kotlin compiler and JDK javac against independent consumer modules. Positive controls must compile; negative cases assert diagnostic categories for trust promotion, private proof/tier construction, hidden issuers, confidence misuse, and adversarial Java null-proof attempts.

License

Licensed under either

at your option.

About

Typed validated values, explicit trust transitions, exhaustive confidence levels, and per-value TTL freshness for Kotlin.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages