Rule DSL Reference
This page documents the core rule-building DSL available in konditional-core.
Rule basics
object AppFeatures : Namespace("app") {
val darkMode by boolean<Context>(default = false) {
rule(true) { platforms(Platform.IOS) }
rule(true) { locales(AppLocale.UNITED_STATES) }
}
}
Boolean sugar
val darkMode by boolean<Context>(default = false) {
enable { ios() }
disable { android() }
}
Targeting primitives
Inside a rule block (RuleScope):
locales(...)targets locale idsplatforms(...)targets platform idsversions { min(...); max(...) }targets version rangesaxis(axisHandle, ...)targets custom axes explicitly (preferred)axis(...)infers the axis from value type (requires namespace axis declaration)extension { ... }custom predicaterampUp { ... }percentage rolloutallowlist(...)stable IDs that bypass ramp-upnote("...")attaches a human-readable notealways()/matchAll()mark a catch-all rule explicitly
All targeting calls inside one rule are combined with AND semantics. Repeating
axis(...) for the same axis id widens allowed values with OR semantics within
that axis.
Namespace axis catalogs
Type-inferred axis targeting resolves through a namespace-owned AxisCatalog.
This keeps axis bindings isolated per namespace.
import io.amichne.konditional.context.axis.KonditionalExplicitId
@KonditionalExplicitId("environment")
enum class Environment(override val id: String) : AxisValue<Environment> {
PROD("prod"),
STAGE("stage"),
}
enum class Tenant : AxisValue<Tenant> {
ENTERPRISE,
}
object AppFeatures : Namespace("app") {
val environmentAxis = axis<Environment>()
val tenantAxis = axis<Tenant>()
val checkout by boolean<Context>(default = false) {
rule(true) {
axis(environmentAxis, Environment.PROD) // Explicit handle
axis(Tenant.ENTERPRISE) // Inferred from axisCatalog
}
}
}
By default, an axis ID is derived from the enum fully-qualified class name.
Apply @KonditionalExplicitId("...") when you need a stable custom axis ID.
For context values, use explicit handles:
val values = axisValues {
set(AppFeatures.environmentAxis, Environment.PROD)
}
Targeting hierarchy
Konditional compiles rule criteria into a structural targeting tree.
- Each rule becomes a
Targeting.Allconjunction. - Standard leaves represent locale, platform, version, and axis constraints.
- Each
extension { ... }adds aTargeting.Customleaf. whenContext<R> { ... }adds a guarded leaf that evaluates only when the runtime context implementsR.
When a context lacks a required capability, guarded leaves return false
without throwing. This behavior replaces legacy flat predicate composition and
keeps rule matching deterministic.
Example
val checkout by string<Context>(default = "v1") {
rule("v2") {
platforms(Platform.IOS)
versions { min(3, 0, 0) }
rampUp { 25.0 }
note("iOS v2 rollout")
}
}
Criteria-first rules (yields)
For readability (especially with complex values), you can declare criteria first and then yield a value:
val checkout by string<Context>(default = "v1") {
rule {
platforms(Platform.IOS)
versions { min(3, 0, 0) }
rampUp { 25.0 }
note("iOS v2 rollout")
} yields "v2"
}
- Guarantee:
rule { ... } yields valueis equivalent torule(value) { ... }. - Boundary: A criteria-first
rule { ... }must always be completed withyields(...)(unclosed rules fail fast at definition time).
Custom predicates
extension { ... } receives the Context type for the feature.
data class EnterpriseContext(
override val locale: AppLocale,
override val platform: Platform,
override val appVersion: Version,
override val stableId: StableId,
val subscriptionTier: Tier,
) : Context
val enterpriseOnly by boolean<EnterpriseContext>(default = false) {
rule(true) { extension { subscriptionTier == Tier.ENTERPRISE } }
}
- Multiple
extension { ... }blocks on the same rule are combined with AND semantics. - Each
extension { ... }block contributes predicate specificity.
Capability narrowing with whenContext
Use whenContext<R> { ... } when a feature is defined on a broader context
type but a rule needs an additional capability:
import io.amichne.konditional.core.dsl.rules.targeting.scopes.whenContext
val enterpriseOnly by boolean<Context>(default = false) {
rule(true) {
whenContext<EnterpriseContext> {
subscriptionTier == Tier.ENTERPRISE
}
}
}
-
Guarantee: If runtime context is not
R, the predicate returnsfalseand does not throw. -
Mechanism:
whenContextnarrows context with a safe cast and evaluates the block only on success. -
Boundary: A rule using
whenContext<R>never matches contexts that do not implementR. -
Guarantee: Custom predicates participate in specificity ordering.
-
Mechanism: Each
extension { ... }andwhenContext<R> { ... }call contributes one custom targeting leaf, and leaf specificities are summed. -
Boundary: Konditional does not validate predicate correctness or determinism.
Reusable rule sets (RuleSet)
If you want to share a group of rules across multiple flags, you can build a RuleSet and include it:
object AppFeatures : Namespace("app") {
private val ruleTemplate by string<Context>(default = "v1")
private val iosRollout = ruleTemplate.ruleSet {
rule("v2") { ios() }
}
val checkout by string<Context>(default = "v1") {
include(iosRollout)
rule("v3") { rampUp { 10.0 } }
}
}
- Rule sets are included left-to-right; when two rules are equally specific, earlier included rules win.
RuleSetsupports composition via+to combine two sets while preserving ordering.
Ramp-up allowlists
allowlist(...) bypasses the ramp-up check after the rule matches by criteria.
- Boundary: It does not override rule criteria,
isActive, or the namespace kill-switch.