From 56f5b03b2ef23e09eed7513ebb722f9e39314703 Mon Sep 17 00:00:00 2001 From: Dmytro Gundartsev Date: Tue, 2 Dec 2025 16:42:34 +0100 Subject: [PATCH 1/6] fix: Introduced unanimous single valued output in decision evaluation result --- .../CollectDecisionEvaluationResult.kt | 5 +- .../decision/DecisionEvaluationMultiOutput.kt | 11 +++ .../decision/DecisionEvaluationOutput.kt | 22 ++++-- .../decision/DecisionEvaluationResult.kt | 6 ++ .../DecisionEvaluationSingleOutput.kt | 10 +++ .../SingleDecisionEvaluationResult.kt | 3 +- .../decision/DecisionUseCase.kt | 54 +++++++++++++- docs/decision-api.md | 73 ++++++++++++++++--- 8 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationMultiOutput.kt create mode 100644 api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationSingleOutput.kt diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/CollectDecisionEvaluationResult.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/CollectDecisionEvaluationResult.kt index 6e81d82..8537925 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/CollectDecisionEvaluationResult.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/CollectDecisionEvaluationResult.kt @@ -2,8 +2,9 @@ package dev.bpmcrafters.processengineapi.decision /** * Decision evaluation result for all collect-valued hit policies. - * @since 2.0 + * @since 1.4 */ data class CollectDecisionEvaluationResult( - val result: List + val result: List, + override val meta: Map = emptyMap() ) : DecisionEvaluationResult diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationMultiOutput.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationMultiOutput.kt new file mode 100644 index 0000000..9c64acb --- /dev/null +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationMultiOutput.kt @@ -0,0 +1,11 @@ +package dev.bpmcrafters.processengineapi.decision + +/** + * Decision evaluation output representing multiple named values + * produced by a decision with multiple outputs. + * + * @since 1.4 + */ +data class DecisionEvaluationMultiOutput ( + val outputs: Map +) :DecisionEvaluationOutput diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt index 015f08e..a42ec2c 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt @@ -2,15 +2,23 @@ package dev.bpmcrafters.processengineapi.decision /** * Decision output. - * @since 2.0 + * @since 1.4 */ -data class DecisionEvaluationOutput( + +sealed interface DecisionEvaluationOutput { /** - * Keys are output names pointing to values. + * Returns as a single output value */ - val values: Map, + fun single(): DecisionEvaluationSingleOutput { + require(this is DecisionEvaluationSingleOutput) { "Decision evaluation single output expected but it was ${this::class.simpleName}" } + return this + } + /** - * Additional metadata about the task. + * Returns as multi-outputs with names */ - val meta: Map = emptyMap() -) + fun many(): DecisionEvaluationMultiOutput { + require(this is DecisionEvaluationMultiOutput) { "Decision evaluation multi output expected but it was ${this::class.simpleName}" } + return this + } +} diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt index 30d8eb5..57b5ed7 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt @@ -2,6 +2,7 @@ package dev.bpmcrafters.processengineapi.decision /** * Represents result of decision evaluation. + * */ sealed interface DecisionEvaluationResult { /** @@ -19,4 +20,9 @@ sealed interface DecisionEvaluationResult { require(this is CollectDecisionEvaluationResult) { "Decision evaluation result must be a collect but it was ${this::class.simpleName}" } return this } + + /** + * Additional metadata on evaluation result. + */ + val meta: Map } diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationSingleOutput.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationSingleOutput.kt new file mode 100644 index 0000000..55662db --- /dev/null +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationSingleOutput.kt @@ -0,0 +1,10 @@ +package dev.bpmcrafters.processengineapi.decision + +/** + * Decision evaluation output representing a single anonymous output + * + * @since 1.4 + */ +data class DecisionEvaluationSingleOutput( + val output: Any? +): DecisionEvaluationOutput diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/SingleDecisionEvaluationResult.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/SingleDecisionEvaluationResult.kt index a8439d6..9bc51d5 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/SingleDecisionEvaluationResult.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/SingleDecisionEvaluationResult.kt @@ -5,5 +5,6 @@ package dev.bpmcrafters.processengineapi.decision * @since 2.0 */ data class SingleDecisionEvaluationResult( - val result: DecisionEvaluationOutput + val result: DecisionEvaluationOutput, + override val meta: Map = emptyMap() ) : DecisionEvaluationResult diff --git a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt index 8fddf82..600f720 100644 --- a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt +++ b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt @@ -25,12 +25,64 @@ internal class DecisionUseCase( ).get() .single() .result - .values["discount"] as Double + .single().output as Double } + fun calculateCustomerOffer(customerStatus: CustomerStatus, year: Int): Offer { + return decisionApi.evaluateDecision( + DecisionByRefEvaluationCommand( + decisionRef = "customerOffer", + payloadSupplier = PayloadSupplier { + mapOf( + "customerStatus" to customerStatus, + "year" to year + ) + }, + restrictionSupplier = Supplier { + mapOf(CommonRestrictions.TENANT_ID to "tenant-1") + } + ) + ).get() + .single() + .result + .many() + .outputs + .let { Offer (it["id"] as Integer, it["name"] as String) } + } + + fun calculateCustomerOffers(customerStatus: CustomerStatus, year: Int): List { + return decisionApi.evaluateDecision( + DecisionByRefEvaluationCommand( + decisionRef = "customerOffers", + payloadSupplier = PayloadSupplier { + mapOf( + "customerStatus" to customerStatus, + "year" to year + ) + }, + restrictionSupplier = Supplier { + mapOf(CommonRestrictions.TENANT_ID to "tenant-1") + } + ) + ).get() + .collect() + .result + .map { + it.many() + .outputs + .let { Offer (it["id"] as Integer, it["name"] as String) } + } + } + + enum class CustomerStatus { SILVER, GOLD, PLATINUM } + + data class Offer ( + val id: Integer, + val name: String + ) } diff --git a/docs/decision-api.md b/docs/decision-api.md index e24cd70..c62b7f1 100644 --- a/docs/decision-api.md +++ b/docs/decision-api.md @@ -2,10 +2,18 @@ title: Decision Evaluation API --- -The Decision Evaluation API provides functionality to evaluate DMN decision. The API returns a generic -`DecisionEvaluationResult` which can be cast to single or collect result with the corresponding method. +The Decision Evaluation API provides functionality to evaluate a DMN decision. The API returns a generic +`DecisionEvaluationResult` which can be cast to a single or collect result using the corresponding method: -And here is the example code to evaluate decision: +- `single()` → returns `SingleDecisionEvaluationResult` +- `collect()` → returns `CollectDecisionEvaluationResult` + +Each of those wraps a `DecisionEvaluationOutput` value which can be either: + +- `DecisionEvaluationSingleOutput` for context-less single values (access via `single().getOutput()`), or +- `DecisionEvaluationMultiOutput` for named/context values (access via `many().getOutputs()`). + +Below are example snippets in Java showing the new API usage. ```java class DecisionUseCase { @@ -19,7 +27,7 @@ class DecisionUseCase { * @return calculated discount. */ public Double evaluateDiscount(CustomerStatus customerStatus, Integer year) { - return (Double)evaluateDecisionApi.evaluateDecision( + return (Double) evaluateDecisionApi.evaluateDecision( new DecisionByRefEvaluationCommand( "customerDiscount", () -> Map.of( @@ -28,11 +36,58 @@ class DecisionUseCase { ), Map.of(CommonRestrictions.TENANT_ID, "myTenant") ) - ).get() - .single() - .getResult() - .get("discount") - ; + ).get() // DecisionEvaluationResult + .single() // SingleDecisionEvaluationResult + .getResult() // DecisionEvaluationOutput + .single() // DecisionEvaluationSingleOutput + .getOutput(); // actual value, e.g. Double + } + + /** + * Calculates a customer offer with multiple named outputs (multi-output decision). + */ + public Offer evaluateOffer(CustomerStatus customerStatus, Integer year) { + Map outputs = evaluateDecisionApi.evaluateDecision( + new DecisionByRefEvaluationCommand( + + "customerOffer", + () -> Map.of( + "customerStatus", customerStatus, + "registrationYear", year + ), + Map.of(CommonRestrictions.TENANT_ID, "myTenant") + ) + ).get() // DecisionEvaluationResult + .single() // SingleDecisionEvaluationResult + .getResult() // DecisionEvaluationOutput + .many() // DecisionEvaluationMultiOutput + .getOutputs(); // Map + + return new Offer((Integer) outputs.get("id"), (String) outputs.get("name")); + } + + /** + * Calculates multiple customer offers (collect decision result). + */ + public List evaluateOffers(CustomerStatus customerStatus, Integer year) { + return evaluateDecisionApi.evaluateDecision( + new DecisionByRefEvaluationCommand( + "customerOffers", + () -> Map.of( + "customerStatus", customerStatus, + "registrationYear", year + ), + Map.of(CommonRestrictions.TENANT_ID, "myTenant") + ) + ).get() // DecisionEvaluationResult + .collect() // CollectDecisionEvaluationResult + .getResult() // List + .stream() + .map(o -> { + Map outputs = o.many().getOutputs(); + return new Offer((Integer) outputs.get("id"), (String) outputs.get("name")); + }) + .toList(); } } From c121e5d87d8e1090bf0882907b220b986a57f635 Mon Sep 17 00:00:00 2001 From: Dmytro Gundartsev Date: Thu, 4 Dec 2025 15:28:48 +0100 Subject: [PATCH 2/6] fix: leave values and outputs to be interpreted by the caller --- .../CollectDecisionEvaluationResult.kt | 10 ------- .../decision/DecisionEvaluationMultiOutput.kt | 11 ------- .../decision/DecisionEvaluationOutput.kt | 13 ++------- .../decision/DecisionEvaluationResult.kt | 20 +++++-------- .../DecisionEvaluationSingleOutput.kt | 10 ------- .../SingleDecisionEvaluationResult.kt | 10 ------- .../decision/DecisionUseCase.kt | 29 ++++++++++--------- 7 files changed, 25 insertions(+), 78 deletions(-) delete mode 100644 api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/CollectDecisionEvaluationResult.kt delete mode 100644 api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationMultiOutput.kt delete mode 100644 api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationSingleOutput.kt delete mode 100644 api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/SingleDecisionEvaluationResult.kt diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/CollectDecisionEvaluationResult.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/CollectDecisionEvaluationResult.kt deleted file mode 100644 index 8537925..0000000 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/CollectDecisionEvaluationResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.bpmcrafters.processengineapi.decision - -/** - * Decision evaluation result for all collect-valued hit policies. - * @since 1.4 - */ -data class CollectDecisionEvaluationResult( - val result: List, - override val meta: Map = emptyMap() -) : DecisionEvaluationResult diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationMultiOutput.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationMultiOutput.kt deleted file mode 100644 index 9c64acb..0000000 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationMultiOutput.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.bpmcrafters.processengineapi.decision - -/** - * Decision evaluation output representing multiple named values - * produced by a decision with multiple outputs. - * - * @since 1.4 - */ -data class DecisionEvaluationMultiOutput ( - val outputs: Map -) :DecisionEvaluationOutput diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt index a42ec2c..81a44d9 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt @@ -5,20 +5,13 @@ package dev.bpmcrafters.processengineapi.decision * @since 1.4 */ -sealed interface DecisionEvaluationOutput { +interface DecisionEvaluationOutput { /** * Returns as a single output value */ - fun single(): DecisionEvaluationSingleOutput { - require(this is DecisionEvaluationSingleOutput) { "Decision evaluation single output expected but it was ${this::class.simpleName}" } - return this - } - + fun withSingleOutput(): T? /** * Returns as multi-outputs with names */ - fun many(): DecisionEvaluationMultiOutput { - require(this is DecisionEvaluationMultiOutput) { "Decision evaluation multi output expected but it was ${this::class.simpleName}" } - return this - } + fun withMultipleOutputs(): Map? } diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt index 57b5ed7..e6f05fc 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt @@ -1,28 +1,22 @@ package dev.bpmcrafters.processengineapi.decision /** - * Represents result of decision evaluation. + * Represents the result of decision evaluation. * */ -sealed interface DecisionEvaluationResult { +interface DecisionEvaluationResult{ /** - * Returns the result as single. + * Returns the result excepted to be a single value */ - fun single(): SingleDecisionEvaluationResult { - require(this is SingleDecisionEvaluationResult) { "Decision evaluation result must be a single but it was ${this::class.simpleName}" } - return this - } + fun asSingleValue(): DecisionEvaluationOutput /** - * Returns the result as collect. + * Returns the result expected to be s collection of values */ - fun collect(): CollectDecisionEvaluationResult { - require(this is CollectDecisionEvaluationResult) { "Decision evaluation result must be a collect but it was ${this::class.simpleName}" } - return this - } + fun asCollectValues(): List /** * Additional metadata on evaluation result. */ - val meta: Map + fun meta(): Map } diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationSingleOutput.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationSingleOutput.kt deleted file mode 100644 index 55662db..0000000 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationSingleOutput.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.bpmcrafters.processengineapi.decision - -/** - * Decision evaluation output representing a single anonymous output - * - * @since 1.4 - */ -data class DecisionEvaluationSingleOutput( - val output: Any? -): DecisionEvaluationOutput diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/SingleDecisionEvaluationResult.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/SingleDecisionEvaluationResult.kt deleted file mode 100644 index 9bc51d5..0000000 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/SingleDecisionEvaluationResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.bpmcrafters.processengineapi.decision - -/** - * Decision evaluation result for all single-valued hit policies. - * @since 2.0 - */ -data class SingleDecisionEvaluationResult( - val result: DecisionEvaluationOutput, - override val meta: Map = emptyMap() -) : DecisionEvaluationResult diff --git a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt index 600f720..66c4a5e 100644 --- a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt +++ b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt @@ -23,9 +23,9 @@ internal class DecisionUseCase( } ) ).get() - .single() - .result - .single().output as Double + .asSingleValue() + .withSingleOutput() + ?: NO_DISCOUNT } fun calculateCustomerOffer(customerStatus: CustomerStatus, year: Int): Offer { @@ -43,11 +43,10 @@ internal class DecisionUseCase( } ) ).get() - .single() - .result - .many() - .outputs - .let { Offer (it["id"] as Integer, it["name"] as String) } + .asSingleValue() + .withMultipleOutputs() + ?.let { Offer (it["id"] as Integer, it["name"] as String) } + ?: throw IllegalStateException("No offer found") } fun calculateCustomerOffers(customerStatus: CustomerStatus, year: Int): List { @@ -65,12 +64,10 @@ internal class DecisionUseCase( } ) ).get() - .collect() - .result - .map { - it.many() - .outputs - .let { Offer (it["id"] as Integer, it["name"] as String) } + .asCollectValues() + .mapNotNull { value -> + value.withMultipleOutputs() + ?.let { Offer (it["id"] as Integer, it["name"] as String) } } } @@ -85,4 +82,8 @@ internal class DecisionUseCase( val id: Integer, val name: String ) + + companion object { + const val NO_DISCOUNT = 0.0 + } } From 19158f8d811a96ebefab708010a83ed886e6618c Mon Sep 17 00:00:00 2001 From: Simon Zambrovski Date: Thu, 4 Dec 2025 17:00:50 +0100 Subject: [PATCH 3/6] improve implementation --- .../DecisionByRefEvaluationCommand.kt | 18 ++++- .../decision/DecisionEvaluationOutput.kt | 20 +++-- .../decision/DecisionEvaluationResult.kt | 20 +++-- .../decision/EvaluateDecisionApi.kt | 3 +- .../decision/DecisionUseCase.kt | 74 ++++++++----------- 5 files changed, 75 insertions(+), 60 deletions(-) diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionByRefEvaluationCommand.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionByRefEvaluationCommand.kt index 54f5392..d004b43 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionByRefEvaluationCommand.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionByRefEvaluationCommand.kt @@ -5,7 +5,7 @@ import java.util.function.Supplier /** * Command to evaluate decision by provided reference. - * @since 2.0 + * @since 1.4 */ data class DecisionByRefEvaluationCommand( /** @@ -20,4 +20,18 @@ data class DecisionByRefEvaluationCommand( * Restrictions supplier to pass to this evaluation. */ val restrictionSupplier: Supplier> -) : DecisionEvaluationCommand, PayloadSupplier by payloadSupplier +) : DecisionEvaluationCommand, PayloadSupplier by payloadSupplier { + + /** + * Constructs an evaluate command. + * @param decisionRef decision reference. + * @param payload payload to use. + * @param restrictions restrictions for the message. + */ + constructor(decisionRef: String, payload: Map, restrictions: Map) : this( + decisionRef = decisionRef, + payloadSupplier = PayloadSupplier { payload }, + restrictionSupplier = { restrictions } + ) + +} diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt index 81a44d9..fa18112 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt @@ -2,16 +2,26 @@ package dev.bpmcrafters.processengineapi.decision /** * Decision output. + * + * Represents a result of evaluation. Might be handled * @since 1.4 */ - interface DecisionEvaluationOutput { + + /** + * Returns as a single output value converted to a type. + * This conversion is an attempt to convert the output to the given type and might fail, if the type is incompatible. + * @param T type of the output. + */ + fun asType(): T? + /** - * Returns as a single output value + * Returns as multi-output map, keyed by output name. If names are not available, use digits as fallbacks. */ - fun withSingleOutput(): T? + fun asMap(): Map + /** - * Returns as multi-outputs with names + * Additional metadata on evaluation output, if supported by the engine. */ - fun withMultipleOutputs(): Map? + fun meta(): Map = mapOf() } diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt index e6f05fc..468851c 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt @@ -2,21 +2,25 @@ package dev.bpmcrafters.processengineapi.decision /** * Represents the result of decision evaluation. - * + * @since 1.4 */ -interface DecisionEvaluationResult{ +interface DecisionEvaluationResult { /** - * Returns the result excepted to be a single value + * Returns the result excepted to be a single value. + * + * This is because the hit policy defined it to be single (single result or result of aggregation). */ - fun asSingleValue(): DecisionEvaluationOutput + fun asSingle(): DecisionEvaluationOutput? /** - * Returns the result expected to be s collection of values + * Returns the result expected to be a collection of values. + * + * This is because multiple rules have fired, and we collect multiple results without aggregation. */ - fun asCollectValues(): List + fun asList(): List /** - * Additional metadata on evaluation result. - */ + * Additional metadata on evaluation result, if supported by the engine. + */ fun meta(): Map } diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/EvaluateDecisionApi.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/EvaluateDecisionApi.kt index b352bd3..bce7a10 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/EvaluateDecisionApi.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/EvaluateDecisionApi.kt @@ -13,8 +13,7 @@ interface EvaluateDecisionApi : RestrictionAware, MetaInfoAware { /** * Evaluate decision. * @param command a command containing parameter for decision evaluation. - * @return decision evaluation result. Depending on the hit policy might either [SingleDecisionEvaluationResult] - * or [CollectDecisionEvaluationResult]. + * @return decision evaluation result. */ fun evaluateDecision(command: DecisionEvaluationCommand): CompletableFuture } diff --git a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt index 66c4a5e..3ea0065 100644 --- a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt +++ b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt @@ -1,9 +1,10 @@ package dev.bpmcrafters.processengineapi.decision import dev.bpmcrafters.processengineapi.CommonRestrictions -import dev.bpmcrafters.processengineapi.PayloadSupplier -import java.util.function.Supplier +/** + * Example use cae to demonstrate the usage of the API. + */ internal class DecisionUseCase( private val decisionApi: EvaluateDecisionApi ) { @@ -12,19 +13,16 @@ internal class DecisionUseCase( return decisionApi.evaluateDecision( DecisionByRefEvaluationCommand( decisionRef = "customerDiscount", - payloadSupplier = PayloadSupplier { - mapOf( - "customerStatus" to customerStatus, - "year" to year - ) - }, - restrictionSupplier = Supplier { - mapOf(CommonRestrictions.TENANT_ID to "tenant-1") - } + payload = mapOf( + "customerStatus" to customerStatus, + "year" to year + ), + restrictions = mapOf( + CommonRestrictions.TENANT_ID to "tenant-1" + ) ) ).get() - .asSingleValue() - .withSingleOutput() + .asSingle()?.asType() ?: NO_DISCOUNT } @@ -32,20 +30,16 @@ internal class DecisionUseCase( return decisionApi.evaluateDecision( DecisionByRefEvaluationCommand( decisionRef = "customerOffer", - payloadSupplier = PayloadSupplier { - mapOf( - "customerStatus" to customerStatus, - "year" to year - ) - }, - restrictionSupplier = Supplier { - mapOf(CommonRestrictions.TENANT_ID to "tenant-1") - } + payload = mapOf( + "customerStatus" to customerStatus, + "year" to year + ), + restrictions = mapOf( + CommonRestrictions.TENANT_ID to "tenant-1" + ) ) ).get() - .asSingleValue() - .withMultipleOutputs() - ?.let { Offer (it["id"] as Integer, it["name"] as String) } + .asSingle()?.asMap()?.let { Offer(it["id"] as Integer, it["name"] as String) } ?: throw IllegalStateException("No offer found") } @@ -53,35 +47,29 @@ internal class DecisionUseCase( return decisionApi.evaluateDecision( DecisionByRefEvaluationCommand( decisionRef = "customerOffers", - payloadSupplier = PayloadSupplier { - mapOf( - "customerStatus" to customerStatus, - "year" to year + payload = mapOf( + "customerStatus" to customerStatus, + "year" to year + ), + restrictions = + mapOf( + CommonRestrictions.TENANT_ID to "tenant-1" ) - }, - restrictionSupplier = Supplier { - mapOf(CommonRestrictions.TENANT_ID to "tenant-1") - } ) ).get() - .asCollectValues() - .mapNotNull { value -> - value.withMultipleOutputs() - ?.let { Offer (it["id"] as Integer, it["name"] as String) } - } + .asList().map { result -> result.asMap().let { Offer(it["id"] as Integer, it["name"] as String) } } } - enum class CustomerStatus { SILVER, GOLD, PLATINUM } - data class Offer ( - val id: Integer, - val name: String - ) + data class Offer( + val id: Integer, + val name: String + ) companion object { const val NO_DISCOUNT = 0.0 From 98b077591c79ca70b3757ee5ffcc1bd332a20a71 Mon Sep 17 00:00:00 2001 From: Simon Zambrovski Date: Thu, 4 Dec 2025 17:01:50 +0100 Subject: [PATCH 4/6] correct API docs --- .../processengineapi/decision/DecisionEvaluationCommand.kt | 2 +- .../bpmcrafters/processengineapi/decision/DecisionUseCase.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationCommand.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationCommand.kt index 4b8e573..e74e84f 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationCommand.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationCommand.kt @@ -4,6 +4,6 @@ import dev.bpmcrafters.processengineapi.PayloadSupplier /** * Interface for decision evaluation commands. - * @since 2.0 + * @since 1.4 */ interface DecisionEvaluationCommand : PayloadSupplier diff --git a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt index 3ea0065..68bbed2 100644 --- a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt +++ b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt @@ -4,6 +4,7 @@ import dev.bpmcrafters.processengineapi.CommonRestrictions /** * Example use cae to demonstrate the usage of the API. + * @since 1.4 */ internal class DecisionUseCase( private val decisionApi: EvaluateDecisionApi From d639711cbae456d0679e7ef0cf58e7ff038c04da Mon Sep 17 00:00:00 2001 From: Simon Zambrovski Date: Thu, 4 Dec 2025 17:09:55 +0100 Subject: [PATCH 5/6] update docs --- .../decision/DecisionUseCase.kt | 12 +++++-- docs/decision-api.md | 32 +++++++------------ 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt index 68bbed2..6a00666 100644 --- a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt +++ b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt @@ -23,7 +23,8 @@ internal class DecisionUseCase( ) ) ).get() - .asSingle()?.asType() + .asSingle() + ?.asType() ?: NO_DISCOUNT } @@ -40,7 +41,9 @@ internal class DecisionUseCase( ) ) ).get() - .asSingle()?.asMap()?.let { Offer(it["id"] as Integer, it["name"] as String) } + .asSingle() + ?.asMap() + ?.let { Offer(it["id"] as Integer, it["name"] as String) } ?: throw IllegalStateException("No offer found") } @@ -58,7 +61,10 @@ internal class DecisionUseCase( ) ) ).get() - .asList().map { result -> result.asMap().let { Offer(it["id"] as Integer, it["name"] as String) } } + .asList() + .map { result -> result + .asMap() + .let { Offer(it["id"] as Integer, it["name"] as String) } } } enum class CustomerStatus { diff --git a/docs/decision-api.md b/docs/decision-api.md index c62b7f1..2e39788 100644 --- a/docs/decision-api.md +++ b/docs/decision-api.md @@ -3,15 +3,10 @@ title: Decision Evaluation API --- The Decision Evaluation API provides functionality to evaluate a DMN decision. The API returns a generic -`DecisionEvaluationResult` which can be cast to a single or collect result using the corresponding method: +`DecisionEvaluationResult` which can be cast to a single or list result using the corresponding method: -- `single()` → returns `SingleDecisionEvaluationResult` -- `collect()` → returns `CollectDecisionEvaluationResult` - -Each of those wraps a `DecisionEvaluationOutput` value which can be either: - -- `DecisionEvaluationSingleOutput` for context-less single values (access via `single().getOutput()`), or -- `DecisionEvaluationMultiOutput` for named/context values (access via `many().getOutputs()`). +- `asSingle()` → returns `DecisionEvaluationOutput` +- `asList()` → returns `List` Below are example snippets in Java showing the new API usage. @@ -37,10 +32,8 @@ class DecisionUseCase { Map.of(CommonRestrictions.TENANT_ID, "myTenant") ) ).get() // DecisionEvaluationResult - .single() // SingleDecisionEvaluationResult - .getResult() // DecisionEvaluationOutput - .single() // DecisionEvaluationSingleOutput - .getOutput(); // actual value, e.g. Double + .asSingle() // DecisionEvaluationOutput + .asType(Double.class); // convert to double } /** @@ -58,11 +51,9 @@ class DecisionUseCase { Map.of(CommonRestrictions.TENANT_ID, "myTenant") ) ).get() // DecisionEvaluationResult - .single() // SingleDecisionEvaluationResult - .getResult() // DecisionEvaluationOutput - .many() // DecisionEvaluationMultiOutput - .getOutputs(); // Map - + .asSingle() // DecisionEvaluationOutput + .asMap(); // Map + return new Offer((Integer) outputs.get("id"), (String) outputs.get("name")); } @@ -73,18 +64,17 @@ class DecisionUseCase { return evaluateDecisionApi.evaluateDecision( new DecisionByRefEvaluationCommand( "customerOffers", - () -> Map.of( + Map.of( "customerStatus", customerStatus, "registrationYear", year ), Map.of(CommonRestrictions.TENANT_ID, "myTenant") ) ).get() // DecisionEvaluationResult - .collect() // CollectDecisionEvaluationResult - .getResult() // List + .asList() // List .stream() .map(o -> { - Map outputs = o.many().getOutputs(); + Map outputs = o.asMap(); return new Offer((Integer) outputs.get("id"), (String) outputs.get("name")); }) .toList(); From e1a29d94ffb9954f292375651214d776dfb5650b Mon Sep 17 00:00:00 2001 From: Dmytro Gundartsev Date: Fri, 5 Dec 2025 16:39:00 +0100 Subject: [PATCH 6/6] small adjustment --- .../processengineapi/decision/DecisionEvaluationOutput.kt | 8 ++++---- .../processengineapi/decision/DecisionEvaluationResult.kt | 2 +- .../processengineapi/decision/DecisionUseCase.kt | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt index fa18112..edc73b4 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationOutput.kt @@ -11,14 +11,14 @@ interface DecisionEvaluationOutput { /** * Returns as a single output value converted to a type. * This conversion is an attempt to convert the output to the given type and might fail, if the type is incompatible. - * @param T type of the output. + * @param type class which the output will be cast to */ - fun asType(): T? + fun asType(type: Class): T? /** - * Returns as multi-output map, keyed by output name. If names are not available, use digits as fallbacks. + * Returns as multi-output map, keyed by output name. Attempt on converting single output value into Map would result in a runtime exception */ - fun asMap(): Map + fun asMap(): Map? /** * Additional metadata on evaluation output, if supported by the engine. diff --git a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt index 468851c..fda51d2 100644 --- a/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt +++ b/api/src/main/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionEvaluationResult.kt @@ -10,7 +10,7 @@ interface DecisionEvaluationResult { * * This is because the hit policy defined it to be single (single result or result of aggregation). */ - fun asSingle(): DecisionEvaluationOutput? + fun asSingle(): DecisionEvaluationOutput /** * Returns the result expected to be a collection of values. diff --git a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt index 6a00666..0fef021 100644 --- a/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt +++ b/api/src/test/kotlin/dev/bpmcrafters/processengineapi/decision/DecisionUseCase.kt @@ -24,7 +24,7 @@ internal class DecisionUseCase( ) ).get() .asSingle() - ?.asType() + .asType(Double::class.java) ?: NO_DISCOUNT } @@ -42,7 +42,7 @@ internal class DecisionUseCase( ) ).get() .asSingle() - ?.asMap() + .asMap() ?.let { Offer(it["id"] as Integer, it["name"] as String) } ?: throw IllegalStateException("No offer found") } @@ -62,9 +62,9 @@ internal class DecisionUseCase( ) ).get() .asList() - .map { result -> result + .mapNotNull { result -> result .asMap() - .let { Offer(it["id"] as Integer, it["name"] as String) } } + ?.let { Offer(it["id"] as Integer, it["name"] as String) } } } enum class CustomerStatus {