├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── backport.yml │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── api-check-ignore.xml ├── pom.xml └── src ├── main └── scala │ └── org │ └── camunda │ └── dmn │ ├── Audit.scala │ ├── DmnEngine.scala │ ├── FunctionalHelper.scala │ ├── NoUnpackValueMapper.scala │ ├── evaluation │ ├── BusinessKnowledgeEvaluator.scala │ ├── ContextEvaluator.scala │ ├── DecisionEvaluator.scala │ ├── DecisionTableEvaluator.scala │ ├── FunctionDefinitionEvaluator.scala │ ├── InvocationEvaluator.scala │ ├── ListEvaluator.scala │ ├── LiteralExpressionEvaluator.scala │ ├── RelationEvaluator.scala │ └── TypeChecker.scala │ ├── package.scala │ └── parser │ ├── DmnParser.scala │ └── ParsedDmn.scala └── test ├── resources ├── META-INF │ └── services │ │ ├── org.camunda.feel.context.CustomFunctionProvider │ │ └── org.camunda.feel.valuemapper.CustomValueMapper ├── bkm │ ├── BkmWithContext.dmn │ ├── BkmWithDecisionTable.dmn │ ├── BkmWithLiteralExpression.dmn │ ├── BkmWithRelation.dmn │ └── BkmWithoutEncapsulatedLogic.dmn ├── config │ ├── bkm_with_spaces_and_dash.dmn │ ├── decision_with_dash.dmn │ ├── decision_with_invalid_expression.dmn │ ├── decision_with_spaces.dmn │ ├── decision_with_spaces_in_item_definition_and_item_component.dmn │ ├── with_invalid_bkm.dmn │ ├── with_invalid_decision.dmn │ └── with_specific_date_time.dmn ├── context │ ├── ContextWithInvocation.dmn │ ├── Eligibility.dmn │ ├── NestedContext.dmn │ └── SimpleContext.dmn ├── decisiontable │ ├── adjustments.dmn │ ├── adjustments_default-output.dmn │ ├── adjustments_empty_output_name.dmn │ ├── applicantRiskRating.dmn │ ├── applicantRiskRating_priority.dmn │ ├── discount.dmn │ ├── discount_collect_max.dmn │ ├── discount_default-output.dmn │ ├── empty-expression.dmn │ ├── expression-language.dmn │ ├── holidays_collect.dmn │ ├── holidays_collect_sum.dmn │ ├── holidays_output_order.dmn │ ├── insuranceFee.dmn │ ├── invalid-expression.dmn │ ├── personLoanCompliance.dmn │ ├── routingRules.dmn │ ├── specialDiscount.dmn │ └── studentFinancialPackageEligibility.dmn ├── dmn1.1 │ └── greeting.dmn ├── dmn1.2 │ └── greeting.dmn ├── dmn1.3 │ └── greeting.dmn ├── dmn1.4 │ └── greeting.dmn ├── dmn1.5 │ └── greeting.dmn ├── functionDefinition │ ├── ApplicantData.dmn │ ├── FeelUserFunction.dmn │ ├── RequiredBkmWithContext.dmn │ ├── RequiredBkmWithFunction.dmn │ ├── RequiredDecisionWithContext.dmn │ └── RequiredDecisionWithFunction.dmn ├── invocation │ ├── discount.dmn │ ├── missingKnowledgeRequirement.dmn │ ├── missingParameters.dmn │ └── withoutParameters.dmn ├── list │ └── ApplicantData.dmn ├── literalexpression │ ├── greeting.dmn │ └── type-mismatch.dmn ├── log4j2.xml ├── relation │ └── ApplicantData.dmn ├── requirements │ ├── ApplicantData.dmn │ ├── cyclic-dependencies-in-bkm.dmn │ ├── cyclic-dependencies-in-decisions.dmn │ ├── discount.dmn │ └── non-cyclic-nested-dependencies-in-decisions.dmn └── spi │ └── SpiTests.dmn └── scala └── org └── camunda └── dmn ├── AuditLogTest.scala ├── BkmTest.scala ├── ContextTest.scala ├── DecisionTableHitPolicyTest.scala ├── DecisionTableTest.scala ├── DecisionTest.scala ├── DmnEngineConfigurationTest.scala ├── DmnEngineTest.scala ├── DmnParserTest.scala ├── DmnVersionCompatibilityTest.scala ├── FunctionDefinitionTest.scala ├── InvocationTest.scala ├── ListTest.scala ├── LiteralExpressionTest.scala ├── RelationTest.scala ├── RequiredDecisionTest.scala └── spi ├── DmnEngineSpiTest.scala ├── MyCustomFunctionProvider.scala └── MyCustomValueMapper.scala /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # About: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 2 | # 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence, 5 | # @global-owner1 and @global-owner2 will be requested for 6 | # review when someone opens a pull request. 7 | * @saig0 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'type: bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. ... 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Environment** 21 | * DMN engine version: `1.x.y` 22 | * Integration: 23 | * Camunda BPM: `7.x.y` 24 | * Camunda Cloud/Zeebe: `1.x.y` 25 | * Standalone with REST API 26 | * Embedded as library in custom application 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'type: enhancement' 6 | assignees: saig0 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Related issues** 17 | 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Related issues 6 | 7 | 8 | 9 | closes # 10 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport labeled merged pull requests 2 | on: 3 | pull_request: 4 | types: [closed] 5 | issue_comment: 6 | types: [created] 7 | jobs: 8 | build: 9 | name: Create backport PRs 10 | runs-on: ubuntu-latest 11 | # Only run when pull request is merged 12 | # or when a comment containing `/backport` is created 13 | if: > 14 | ( 15 | github.event_name == 'pull_request' && 16 | github.event.pull_request.merged 17 | ) || ( 18 | github.event_name == 'issue_comment' && 19 | github.event.issue.pull_request && 20 | contains(github.event.comment.body, '/backport') 21 | ) 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Create backport PRs 25 | uses: zeebe-io/backport-action@v0.0.8 26 | with: 27 | # Required 28 | # Token to authenticate requests to GitHub 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | # Required 32 | # Working directory for the backport action 33 | github_workspace: ${{ github.workspace }} 34 | 35 | # Optional 36 | # Regex pattern to match github labels 37 | # Must contain a capture group for target branchname 38 | # label_pattern: ^backport ([^ ]+)$ 39 | 40 | # Optional 41 | # Template used as description in the pull requests created by this action. 42 | # Placeholders can be used to define variable values. 43 | # These are indicated by a dollar sign and curly braces (`${placeholder}`). 44 | # Please refer to this action's README for all available placeholders. 45 | pull_description: |- 46 | # Description 47 | Backport of #${pull_number} to `${target_branch}`. 48 | 49 | relates to ${issue_refs} -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build project with Maven 2 | on: 3 | pull_request: 4 | push: 5 | branches-ignore: [ master ] 6 | schedule: 7 | - cron: '2 2 * * 1-5' # run nightly master builds on weekdays 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@f1d3225b5376a0791fdee5a0e8eac5289355e43a # pin@v2 15 | - name: Java setup 16 | uses: actions/setup-java@e54a62b3df9364d4b4c1c29c7225e57fe605d7dd # pin@v1 17 | with: 18 | java-version: 11 19 | - name: Cache 20 | uses: actions/cache@99d99cd262b87f5f8671407a1e5c1ddfa36ad5ba # pin@v1 21 | with: 22 | path: ~/.m2/repository 23 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 24 | restore-keys: | 25 | ${{ runner.os }}-maven- 26 | - name: Run Maven 27 | run: mvn -B clean test com.mycila:license-maven-plugin:check 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # If this workflow is triggered by a push to master, it 2 | # deploys a SNAPSHOT 3 | # If this workflow is triggered by publishing a Release, it 4 | # deploys a RELEASE with the selected version 5 | # updates the project version by incrementing the patch version 6 | # commits the version update change to the repository's default branch. 7 | name: Deploy artifacts with Maven 8 | on: 9 | push: 10 | branches: [ master ] 11 | release: 12 | types: [ published ] 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-20.04 16 | steps: 17 | - uses: actions/checkout@f1d3225b5376a0791fdee5a0e8eac5289355e43a # pin@v2 18 | - name: Cache 19 | uses: actions/cache@0781355a23dac32fd3bac414512f4b903437991a # pin@v2 20 | with: 21 | path: ~/.m2/repository 22 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 23 | restore-keys: | 24 | ${{ runner.os }}-maven- 25 | - name: Set up Java environment 26 | uses: actions/setup-java@e54a62b3df9364d4b4c1c29c7225e57fe605d7dd # pin@v1 27 | with: 28 | java-version: 11 29 | gpg-private-key: ${{ secrets.MAVEN_CENTRAL_GPG_SIGNING_KEY_SEC }} 30 | gpg-passphrase: MAVEN_CENTRAL_GPG_PASSPHRASE 31 | - name: Deploy SNAPSHOT / Release 32 | uses: camunda-community-hub/community-action-maven-release@a9e964bf56978eef9bca81551cecceebb246a8e5 # pin@v1 33 | with: 34 | release-version: ${{ github.event.release.tag_name }} 35 | release-profile: community-action-maven-release 36 | nexus-usr: ${{ secrets.NEXUS_USR }} 37 | nexus-psw: ${{ secrets.NEXUS_PSW }} 38 | maven-usr: ${{ secrets.COMMUNITY_HUB_MAVEN_CENTRAL_OSS_USR }} 39 | maven-psw: ${{ secrets.COMMUNITY_HUB_MAVEN_CENTRAL_OSS_PSW }} 40 | maven-gpg-passphrase: ${{ secrets.MAVEN_CENTRAL_GPG_SIGNING_KEY_PASSPHRASE }} 41 | github-token: ${{ secrets.GITHUB_TOKEN }} 42 | id: release 43 | - if: github.event.release 44 | name: Attach artifacts to GitHub Release (Release only) 45 | uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # pin@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ github.event.release.upload_url }} 50 | asset_path: ${{ steps.release.outputs.artifacts_archive_path }} 51 | asset_name: ${{ steps.release.outputs.artifacts_archive_path }} 52 | asset_content_type: application/zip 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | bin/ 5 | 6 | # sbt specific 7 | .cache* 8 | .history 9 | .lib/ 10 | dist/* 11 | target/ 12 | lib_managed/ 13 | src_managed/ 14 | project/boot/ 15 | project/plugins/project/ 16 | 17 | # Eclipse specific 18 | .settings 19 | .classpath 20 | .project 21 | 22 | # Scala-IDE specific 23 | .scala_dependencies 24 | .worksheet 25 | 26 | # Intellij specific 27 | .idea 28 | *.iml 29 | /*/*.iml 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DMN Scala 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.camunda.bpm.extension.dmn.scala/dmn-engine/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.camunda.bpm.extension.dmn.scala/dmn-engine) 4 | 5 | An engine to execute decisions according to the [DMN specification](http://www.omg.org/spec/DMN/About-DMN/) that is written in Scala. It uses the [FEEL-Scala engine](https://github.com/camunda/feel-scala) to evaluate FEEL expressions. 6 | 7 | The DMN engine started as a slack time project, grown into a community-driven project, and is now officially maintained by [Camunda](https://camunda.org/). :rocket: 8 | 9 | It is integrated into [Camunda 8](https://github.com/camunda/camunda) to evaluate DMN decisions. 10 | 11 | **Features:** ✨ 12 | * Support for the latest version of DMN (Compliance Level 3) 13 | * Pluggable auditing for history or validation 14 | * Extensible by custom functions and object mappers 15 | 16 | The coverage of the DMN standard is measured by the [DMN TCK](https://dmn-tck.github.io/tck/index.html). 17 | 18 | ## Usage 19 | 20 | Please have a look at Camunda's [DMN documentation](https://docs.camunda.io/docs/components/modeler/dmn/) and [FEEL documentation](https://docs.camunda.io/docs/components/modeler/feel/what-is-feel/). The documentation describes how to model DMN decisions and write FEEL expressions (e.g. data types, language constructs, builtin-functions, etc.). 21 | 22 | ## Install 23 | 24 | Add the DMN engine as a dependency to your project. 25 | 26 | ```xml 27 | 28 | org.camunda.bpm.extension.dmn.scala 29 | dmn-engine 30 | ${version} 31 | 32 | ``` 33 | 34 | ### Custom Functions 35 | 36 | The engine comes with a bunch of [built-in functions](https://docs.camunda.io/docs/components/modeler/feel/builtin-functions/feel-built-in-functions-introduction/) which are defined by the DMN specification. 37 | It allows you to define your own functions as FEEL expressions (using the keyword `function`) or as context entry element. 38 | 39 | However, the engine provides also an SPI to add custom functions that are written in Scala / Java. The classes are loaded via Java's service loader mechanism. Please have a look at the [documentation](https://camunda.github.io/feel-scala/docs/reference/developer-guide/function-provider-spi) to see how to implement the SPI. 40 | 41 | ### Custom Object Mapper 42 | 43 | The engine has a transformer (aka `ValueMapper`) to transform the incoming variables into FEEL types and to transform the decision result back into regular Scala types. 44 | 45 | If you need to transform custom types or change the result types then you can implement a SPI. The implementation is loaded via Java's service loader mechanism. Please have a look at the 46 | [documentation](https://camunda.github.io/feel-scala/docs/reference/developer-guide/value-mapper-spi) to see how to implement the SPI. 47 | 48 | ## Contribution 49 | 50 | Found a bug? Please report it using [Github Issues](https://github.com/camunda/dmn-scala/issues). 51 | 52 | Want to extend, improve or fix a bug? [Pull Requests](https://github.com/camunda/dmn-scala/pulls) are very welcome. 53 | 54 | Want to discuss something? Take a look at the [Camunda Forum](https://forum.camunda.io/tag/dmn-engine-feel). 55 | 56 | ## License 57 | 58 | [Apache License, Version 2.0](./LICENSE) 59 | -------------------------------------------------------------------------------- /api-check-ignore.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org/camunda/dmn/** 5 | 6 | 7002 7 | *$anonfun* 8 | 9 | 10 | org/camunda/dmn/** 11 | 12 | 7004 13 | *$anonfun* 14 | * 15 | * 16 | 17 | 18 | org/camunda/dmn/** 19 | 20 | 7005 21 | * 22 | 23 | * 24 | * 25 | 26 | 27 | org/camunda/dmn/** 28 | 29 | 7006 30 | *$anonfun* 31 | * 32 | * 33 | 34 | 35 | org/camunda/dmn/** 36 | 37 | 7011 38 | *$anonfun* 39 | 40 | 41 | org/camunda/dmn/Audit$AuditLog 42 | 43 | 7002 44 | * 45 | 46 | 47 | org/camunda/dmn/DmnEngine 48 | 49 | 7006 50 | org.camunda.feel.FeelEngine feelEngine() 51 | * 52 | * 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/Audit.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.parser._ 19 | import org.camunda.feel.syntaxtree.Val 20 | 21 | object Audit { 22 | 23 | trait AuditLogListener { 24 | 25 | def onEval(log: AuditLog) 26 | 27 | def onFailure(log: AuditLog) 28 | } 29 | 30 | case class AuditLog(dmn: ParsedDmn, entries: List[AuditLogEntry]) 31 | 32 | case class AuditLogEntry(id: String, 33 | name: String, 34 | decisionLogic: ParsedDecisionLogic, 35 | result: EvaluationResult) 36 | 37 | ///// evaluation results 38 | 39 | sealed trait EvaluationResult { 40 | val result: Val 41 | } 42 | 43 | case class SingleEvaluationResult(override val result: Val) 44 | extends EvaluationResult 45 | 46 | case class ContextEvaluationResult(entries: Map[String, Val], 47 | override val result: Val) 48 | extends EvaluationResult 49 | 50 | ///// decision table results 51 | 52 | case class DecisionTableEvaluationResult(inputs: List[EvaluatedInput], 53 | matchedRules: List[EvaluatedRule], 54 | override val result: Val) 55 | extends EvaluationResult 56 | 57 | case class EvaluatedInput(input: ParsedInput, value: Val) 58 | 59 | case class EvaluatedOutput(output: ParsedOutput, value: Val) 60 | 61 | case class EvaluatedRule(rule: ParsedRule, outputs: List[EvaluatedOutput]) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/FunctionalHelper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine.Failure 19 | 20 | object FunctionalHelper { 21 | 22 | def mapEither[T, R](it: Iterable[T], 23 | f: T => Either[Failure, R]): Either[Failure, List[R]] = { 24 | foldEither[T, List[R]](List(), it, { 25 | case (xs, x) => 26 | f(x).map(xs :+ _) 27 | }) 28 | } 29 | 30 | def foldEither[T, R](start: R, 31 | it: Iterable[T], 32 | f: (R, T) => Either[Failure, R]): Either[Failure, R] = { 33 | 34 | val startValue: Either[Failure, R] = Right(start) 35 | 36 | (startValue /: it)((xs, x) => 37 | xs.flatMap { xs => 38 | f(xs, x) 39 | }) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/NoUnpackValueMapper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.feel.syntaxtree.Val 19 | import org.camunda.feel.valuemapper.{CustomValueMapper, ValueMapper} 20 | 21 | class NoUnpackValueMapper(valueMapper: ValueMapper) extends CustomValueMapper { 22 | 23 | override def toVal(x: Any, innerValueMapper: Any => Val): Option[Val] = 24 | Some(valueMapper.toVal(x)) 25 | 26 | override def unpackVal(value: Val, 27 | innerValueMapper: Val => Any): Option[Any] = 28 | Some(value) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/evaluation/BusinessKnowledgeEvaluator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.evaluation 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.FunctionalHelper._ 20 | import org.camunda.dmn.parser.{ 21 | ParsedBusinessKnowledgeModel, 22 | ParsedDecisionLogic 23 | } 24 | import org.camunda.feel.syntaxtree.{Val, ValError, ValFunction} 25 | import org.camunda.feel.valuemapper.ValueMapper 26 | 27 | class BusinessKnowledgeEvaluator( 28 | eval: (ParsedDecisionLogic, EvalContext) => Either[Failure, Val], 29 | valueMapper: ValueMapper) { 30 | 31 | def eval(bkm: ParsedBusinessKnowledgeModel, 32 | context: EvalContext): Either[Failure, Val] = { 33 | 34 | evalRequiredKnowledge(bkm.requiredBkms, context) 35 | .flatMap(functions => { 36 | 37 | val evalContext = 38 | context.copy(variables = context.variables ++ functions, 39 | currentElement = bkm) 40 | 41 | validateParameters(bkm.parameters, evalContext) 42 | .flatMap(_ => eval(bkm.logic, evalContext)) 43 | }) 44 | } 45 | 46 | def createFunction( 47 | bkm: ParsedBusinessKnowledgeModel, 48 | context: EvalContext): Either[Failure, (String, ValFunction)] = { 49 | 50 | evalRequiredKnowledge(bkm.requiredBkms, context).map(functions => { 51 | 52 | val evalContext = context.copy(variables = context.variables ++ functions, 53 | currentElement = bkm) 54 | 55 | val function = createFunction(bkm.logic, bkm.parameters, evalContext) 56 | 57 | bkm.name -> function 58 | }) 59 | } 60 | 61 | private def evalRequiredKnowledge( 62 | knowledgeRequirements: Iterable[ParsedBusinessKnowledgeModel], 63 | context: EvalContext): Either[Failure, List[(String, ValFunction)]] = { 64 | mapEither( 65 | knowledgeRequirements, 66 | (bkm: ParsedBusinessKnowledgeModel) => createFunction(bkm, context)) 67 | } 68 | 69 | private def validateArguments( 70 | parameters: Iterable[(String, String)], 71 | args: List[Val], 72 | context: EvalContext): Either[Failure, List[(String, Val)]] = { 73 | mapEither[((String, String), Val), (String, Val)](parameters.zip(args), { 74 | case ((name, typeRef), arg) => 75 | TypeChecker 76 | .isOfType(arg, typeRef) 77 | .map(name -> _) 78 | }) 79 | } 80 | 81 | private def validateParameters( 82 | parameters: Iterable[(String, String)], 83 | context: EvalContext): Either[Failure, List[Any]] = { 84 | mapEither[(String, String), Any]( 85 | parameters, { 86 | case (name, typeRef) => 87 | context.variables 88 | .get(name) 89 | .map(v => TypeChecker.isOfType(valueMapper.toVal(v), typeRef)) 90 | .getOrElse(Left(Failure(s"no parameter found with name '${name}'"))) 91 | } 92 | ) 93 | } 94 | 95 | private def createFunction(expression: ParsedDecisionLogic, 96 | parameters: Iterable[(String, String)], 97 | context: EvalContext): ValFunction = { 98 | val parameterNames = parameters.map(_._1).toList 99 | 100 | ValFunction( 101 | params = parameterNames, 102 | invoke = args => { 103 | 104 | val result = validateArguments(parameters, args, context).flatMap( 105 | arguments => 106 | eval(expression, 107 | context.copy(variables = context.variables ++ arguments))) 108 | 109 | result match { 110 | case Right(value) => value 111 | case Left(failure) => ValError(failure.message) 112 | } 113 | } 114 | ) 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/evaluation/ContextEvaluator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.evaluation 17 | 18 | import org.camunda.dmn.Audit.ContextEvaluationResult 19 | import org.camunda.dmn.DmnEngine._ 20 | import org.camunda.dmn.FunctionalHelper._ 21 | import org.camunda.dmn.parser.{ParsedContext, ParsedDecisionLogic} 22 | import org.camunda.feel.context.Context._ 23 | import org.camunda.feel.syntaxtree.{Val, ValContext, ValFunction} 24 | 25 | class ContextEvaluator( 26 | eval: (ParsedDecisionLogic, EvalContext) => Either[Failure, Val]) { 27 | 28 | def eval(context: ParsedContext, ctx: EvalContext): Either[Failure, Val] = { 29 | 30 | val auditContext = new AuditContext(Map.empty) 31 | 32 | val result = evalContextEntries(context.entries, ctx).flatMap { results => 33 | auditContext.entryResults = results 34 | 35 | evalContextResult(context.aggregationEntry, results, ctx) 36 | } 37 | 38 | ctx.audit(context, 39 | result, 40 | r => 41 | ContextEvaluationResult(entries = auditContext.entryResults, 42 | result = r)) 43 | 44 | result 45 | } 46 | 47 | private def evalContextEntries( 48 | entries: Iterable[(String, ParsedDecisionLogic)], 49 | ctx: EvalContext): Either[Failure, Map[String, Val]] = { 50 | foldEither[(String, ParsedDecisionLogic), Map[String, Val]]( 51 | Map[String, Val](), 52 | entries, { 53 | case (result, (name, expr)) => 54 | // a context entry must be able to access the result of previous entries 55 | val context = ctx.copy(variables = ctx.variables ++ result) 56 | 57 | eval(expr, context) 58 | .map(v => result + (name -> v)) 59 | } 60 | ) 61 | } 62 | 63 | private def evalContextResult(aggregationEntry: Option[ParsedDecisionLogic], 64 | results: Map[String, Val], 65 | ctx: EvalContext): Either[Failure, Val] = { 66 | 67 | aggregationEntry 68 | .map(expr => { 69 | val evalContext = ctx.copy(variables = ctx.variables ++ results) 70 | 71 | eval(expr, evalContext) 72 | }) 73 | .getOrElse { 74 | val functions = results 75 | .filter { case (k, v) => v.isInstanceOf[ValFunction] } 76 | .map { case (k, f) => k -> List(f.asInstanceOf[ValFunction]) } 77 | 78 | Right( 79 | ValContext(StaticContext(variables = results, functions = functions))) 80 | } 81 | } 82 | 83 | private class AuditContext(var entryResults: Map[String, Val]) 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/evaluation/DecisionEvaluator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.evaluation 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.FunctionalHelper._ 20 | import org.camunda.feel.syntaxtree.{Val, ValFunction} 21 | import org.camunda.dmn.parser.{ 22 | ParsedDecision, 23 | ParsedDecisionLogic, 24 | ParsedBusinessKnowledgeModel 25 | } 26 | 27 | class DecisionEvaluator( 28 | eval: (ParsedDecisionLogic, EvalContext) => Either[Failure, Val], 29 | evalBkm: (ParsedBusinessKnowledgeModel, 30 | EvalContext) => Either[Failure, (String, ValFunction)]) { 31 | 32 | def eval(decision: ParsedDecision, 33 | context: EvalContext): Either[Failure, Val] = { 34 | 35 | evalDecision(decision, context) 36 | .map { case (name, result) => result } 37 | } 38 | 39 | private def evalDecision( 40 | decision: ParsedDecision, 41 | context: EvalContext): Either[Failure, (String, Val)] = { 42 | 43 | evalRequiredDecisions(decision.requiredDecisions, context) 44 | .flatMap(decisionResults => { 45 | evalRequiredKnowledge(decision.requiredBkms, context) 46 | .flatMap(functions => { 47 | 48 | val decisionEvaluationContext = context.copy( 49 | variables = context.variables ++ decisionResults ++ functions, 50 | currentElement = decision) 51 | 52 | eval(decision.logic, decisionEvaluationContext) 53 | .flatMap( 54 | result => 55 | decision.resultType 56 | .map(typeRef => TypeChecker.isOfType(result, typeRef)) 57 | .getOrElse(Right(result))) 58 | .map(decision.resultName -> _) 59 | }) 60 | }) 61 | } 62 | 63 | private def evalRequiredDecisions( 64 | requiredDecisions: Iterable[ParsedDecision], 65 | context: EvalContext): Either[Failure, List[(String, Val)]] = { 66 | mapEither(requiredDecisions, 67 | (d: ParsedDecision) => evalDecision(d, context)) 68 | } 69 | 70 | private def evalRequiredKnowledge( 71 | requiredBkms: Iterable[ParsedBusinessKnowledgeModel], 72 | context: EvalContext): Either[Failure, List[(String, ValFunction)]] = { 73 | mapEither(requiredBkms, 74 | (bkm: ParsedBusinessKnowledgeModel) => evalBkm(bkm, context)) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/evaluation/FunctionDefinitionEvaluator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.evaluation 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.FunctionalHelper._ 20 | import org.camunda.dmn.parser.{ParsedExpression, ParsedFunctionDefinition} 21 | import org.camunda.feel.syntaxtree.{Val, ValError, ValFunction} 22 | 23 | class FunctionDefinitionEvaluator( 24 | eval: (ParsedExpression, EvalContext) => Either[Failure, Val]) { 25 | 26 | def eval(function: ParsedFunctionDefinition, 27 | context: EvalContext): Either[Failure, Val] = { 28 | Right(createFunction(function.expression, function.parameters, context)) 29 | } 30 | 31 | private def createFunction(expression: ParsedExpression, 32 | parameters: Iterable[(String, String)], 33 | context: EvalContext): ValFunction = { 34 | val parameterNames = parameters.map(_._1).toList 35 | 36 | ValFunction( 37 | params = parameterNames, 38 | invoke = args => { 39 | val result = validateArguments(parameters, args, context).flatMap( 40 | arguments => 41 | eval(expression, 42 | context.copy(variables = context.variables ++ arguments))) 43 | 44 | result match { 45 | case Right(value) => value 46 | case Left(failure) => ValError(failure.message) 47 | } 48 | } 49 | ) 50 | } 51 | 52 | private def validateArguments( 53 | parameters: Iterable[(String, String)], 54 | args: List[Val], 55 | context: EvalContext): Either[Failure, List[(String, Val)]] = { 56 | mapEither[((String, String), Val), (String, Val)](parameters.zip(args), { 57 | case ((name, typeRef), arg) => 58 | TypeChecker 59 | .isOfType(arg, typeRef) 60 | .map(name -> _) 61 | }) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/evaluation/InvocationEvaluator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.evaluation 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.FunctionalHelper._ 20 | import org.camunda.dmn.parser.{ 21 | ParsedBusinessKnowledgeModel, 22 | ParsedExpression, 23 | ParsedInvocation 24 | } 25 | import org.camunda.feel.syntaxtree.Val 26 | 27 | class InvocationEvaluator( 28 | eval: (ParsedExpression, EvalContext) => Either[Failure, Val], 29 | evalBkm: (ParsedBusinessKnowledgeModel, EvalContext) => Either[Failure, Val]) { 30 | 31 | def eval(invocation: ParsedInvocation, 32 | context: EvalContext): Either[Failure, Val] = { 33 | 34 | val result = evalParameters(invocation.bindings, context).flatMap { p => 35 | val ctx = context.copy(variables = context.variables ++ p.toMap) 36 | evalBkm(invocation.invocation, ctx) 37 | } 38 | 39 | context.audit(invocation, result) 40 | result 41 | } 42 | 43 | private def evalParameters( 44 | bindings: Iterable[(String, ParsedExpression)], 45 | context: EvalContext): Either[Failure, List[(String, Any)]] = { 46 | mapEither[(String, ParsedExpression), (String, Any)](bindings, { 47 | case (name, expr) => 48 | eval(expr, context) 49 | .map(name -> _) 50 | }) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/evaluation/ListEvaluator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.evaluation 17 | 18 | import scala.collection.JavaConverters._ 19 | import org.camunda.dmn.DmnEngine._ 20 | import org.camunda.dmn.FunctionalHelper._ 21 | import org.camunda.bpm.model.dmn.instance.{Expression, List} 22 | import org.camunda.dmn.parser.{ParsedDecisionLogic, ParsedList} 23 | import org.camunda.feel.syntaxtree.{Val, ValError, ValList} 24 | import org.camunda.dmn.Audit.SingleEvaluationResult 25 | 26 | class ListEvaluator( 27 | eval: (ParsedDecisionLogic, EvalContext) => Either[Failure, Val]) { 28 | 29 | def eval(list: ParsedList, context: EvalContext): Either[Failure, Val] = { 30 | 31 | val result = mapEither(list.entries, 32 | (expr: ParsedDecisionLogic) => eval(expr, context)) 33 | .map(ValList) 34 | 35 | context.audit(list, result) 36 | result 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/evaluation/LiteralExpressionEvaluator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.evaluation 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.parser._ 20 | import org.camunda.feel 21 | import org.camunda.feel.api.{FailedEvaluationResult, FeelEngineApi, SuccessfulEvaluationResult} 22 | import org.camunda.feel.context.Context.StaticContext 23 | import org.camunda.feel.syntaxtree.{Val, ValBoolean, ValFunction} 24 | 25 | class LiteralExpressionEvaluator(feelEngine: FeelEngineApi) { 26 | 27 | def evalExpression(literalExpression: ParsedLiteralExpression, 28 | context: EvalContext): Either[Failure, Val] = { 29 | 30 | val result = evalExpression(literalExpression.expression, context) 31 | context.audit(literalExpression, result) 32 | result 33 | } 34 | 35 | def evalExpression(expression: ParsedExpression, 36 | context: EvalContext): Either[Failure, Val] = { 37 | expression match { 38 | case FeelExpression(exp) => evalFeelExpression(exp, context) 39 | case EmptyExpression => Right(ValBoolean(true)) 40 | case ExpressionFailure(failure) => Left(Failure(message = failure)) 41 | case other => 42 | Left(Failure(message = s"Failed to evaluate expression '$other'")) 43 | } 44 | } 45 | 46 | private def evalFeelExpression(expression: feel.syntaxtree.ParsedExpression, 47 | context: EvalContext): Either[Failure, Val] = { 48 | val functions = context.variables 49 | .filter { case (k, v) => v.isInstanceOf[ValFunction] } 50 | .map { case (k, f) => k -> List(f.asInstanceOf[ValFunction]) } 51 | 52 | val evalContext = 53 | StaticContext(variables = context.variables, functions = functions) 54 | 55 | feelEngine.evaluate( 56 | expression = expression, 57 | context = evalContext 58 | ) match { 59 | case SuccessfulEvaluationResult(result: Val, _) => Right(result) 60 | case SuccessfulEvaluationResult(other, _) => Left(Failure(s"expected value but found '$other'")) 61 | case FailedEvaluationResult(failure, _) => Left(Failure(failure.message)) 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/evaluation/RelationEvaluator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.evaluation 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.FunctionalHelper._ 20 | import org.camunda.dmn.parser.{ 21 | ParsedDecisionLogic, 22 | ParsedRelation, 23 | ParsedRelationRow 24 | } 25 | import org.camunda.feel.context.Context.StaticContext 26 | import org.camunda.feel.syntaxtree.{Val, ValContext, ValError, ValList} 27 | 28 | class RelationEvaluator( 29 | eval: (ParsedDecisionLogic, EvalContext) => Either[Failure, Val]) { 30 | 31 | def eval(relation: ParsedRelation, 32 | context: EvalContext): Either[Failure, Val] = { 33 | 34 | val result = mapEither( 35 | relation.rows, 36 | (row: ParsedRelationRow) => { 37 | val columns = 38 | mapEither[(String, ParsedDecisionLogic), (String, Val)](row.columns, { 39 | case (column, expr) => 40 | eval(expr, context) 41 | .map(r => column -> r) 42 | }) 43 | 44 | columns.map(values => ValContext(StaticContext(values.toMap))) 45 | } 46 | ).map(ValList) 47 | 48 | context.audit(relation, result) 49 | result 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/evaluation/TypeChecker.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.evaluation 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.feel.syntaxtree._ 20 | 21 | object TypeChecker { 22 | 23 | val FEEL_PREFIX = "feel:" 24 | 25 | def isOfType(value: Val, typeRef: String): Either[Failure, Val] = { 26 | if (typeRef == null || typeRef.isEmpty) { 27 | Right(value) 28 | 29 | } else if (value == ValNull) { 30 | Right(value) 31 | 32 | } else { 33 | // accept type with and without prefix 34 | val `type` = typeRef.replaceAll(FEEL_PREFIX, "") 35 | 36 | `type` match { 37 | case "string" => 38 | value match { 39 | case v: ValString => Right(v) 40 | case other => Left(Failure(s"expected 'string' but found '$other'")) 41 | } 42 | case "number" => 43 | value match { 44 | case v: ValNumber => Right(v) 45 | case other => Left(Failure(s"expected 'number' but found '$other'")) 46 | } 47 | case "boolean" => 48 | value match { 49 | case v: ValBoolean => Right(v) 50 | case other => 51 | Left(Failure(s"expected 'boolean' but found '$other'")) 52 | } 53 | case "time" => 54 | value match { 55 | case v: ValTime => Right(v) 56 | case v: ValLocalTime => Right(v) 57 | case other => Left(Failure(s"expected 'time' but found '$other'")) 58 | } 59 | case "date" => 60 | value match { 61 | case v: ValDate => Right(v) 62 | case other => Left(Failure(s"expected 'date' but found '$other'")) 63 | } 64 | case "dateTime" => 65 | value match { 66 | case v: ValDateTime => Right(v) 67 | case v: ValLocalDateTime => Right(v) 68 | case other => 69 | Left(Failure(s"expected 'dateTime' but found '$other'")) 70 | } 71 | case "dayTimeDuration" => 72 | value match { 73 | case v: ValDayTimeDuration => Right(v) 74 | case other => 75 | Left(Failure(s"expected 'dayTimeDuration' but found '$other'")) 76 | } 77 | case "yearMonthDuration" => 78 | value match { 79 | case v: ValYearMonthDuration => Right(v) 80 | case other => 81 | Left(Failure(s"expected 'yearMonthDuration' but found '$other'")) 82 | } 83 | case other => Right(value) // ignore 84 | } 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda 17 | 18 | import org.slf4j.LoggerFactory 19 | 20 | package object dmn { 21 | 22 | val logger = LoggerFactory.getLogger("org.camunda.dmn.DmnEngine") 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/org/camunda/dmn/parser/ParsedDmn.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.parser 17 | 18 | import org.camunda.bpm.model.dmn.DmnModelInstance 19 | 20 | import org.camunda.bpm.model.dmn.HitPolicy 21 | import org.camunda.bpm.model.dmn.BuiltinAggregator 22 | import org.camunda.feel 23 | 24 | case class ParsedDmn(model: DmnModelInstance, 25 | decisions: Iterable[ParsedDecision]) { 26 | 27 | val decisionsById: Map[String, ParsedDecision] = 28 | decisions.map(d => d.id -> d).toMap 29 | val decisionsByName: Map[String, ParsedDecision] = 30 | decisions.map(d => d.name -> d).toMap 31 | } 32 | 33 | // --------------- 34 | 35 | sealed trait ParsedExpression 36 | 37 | case class ExpressionFailure(failure: String) extends ParsedExpression 38 | 39 | case class FeelExpression(expression: feel.syntaxtree.ParsedExpression) 40 | extends ParsedExpression 41 | 42 | case object EmptyExpression extends ParsedExpression 43 | 44 | // --------------- 45 | 46 | sealed trait ParsedDecisionLogicContainer { 47 | val id: String 48 | val name: String 49 | val logic: ParsedDecisionLogic 50 | } 51 | 52 | case class ParsedDecision(id: String, 53 | name: String, 54 | logic: ParsedDecisionLogic, 55 | resultName: String, 56 | resultType: Option[String], 57 | requiredDecisions: Iterable[ParsedDecision], 58 | requiredBkms: Iterable[ParsedBusinessKnowledgeModel]) 59 | extends ParsedDecisionLogicContainer 60 | 61 | case class ParsedBusinessKnowledgeModel( 62 | id: String, 63 | name: String, 64 | logic: ParsedDecisionLogic, 65 | parameters: Iterable[(String, String)], 66 | requiredBkms: Iterable[ParsedBusinessKnowledgeModel]) 67 | extends ParsedDecisionLogicContainer 68 | 69 | sealed trait ParsedDecisionLogic 70 | 71 | case class ParsedInvocation(bindings: Iterable[(String, ParsedExpression)], 72 | invocation: ParsedBusinessKnowledgeModel) 73 | extends ParsedDecisionLogic 74 | 75 | case class ParsedContext(entries: Iterable[(String, ParsedDecisionLogic)], 76 | aggregationEntry: Option[ParsedDecisionLogic]) 77 | extends ParsedDecisionLogic 78 | 79 | case class ParsedList(entries: Iterable[ParsedDecisionLogic]) 80 | extends ParsedDecisionLogic 81 | 82 | case class ParsedRelation(rows: Iterable[ParsedRelationRow]) 83 | extends ParsedDecisionLogic 84 | 85 | case class ParsedRelationRow(columns: Iterable[(String, ParsedDecisionLogic)]) 86 | 87 | case class ParsedLiteralExpression(expression: ParsedExpression) 88 | extends ParsedDecisionLogic 89 | 90 | case class ParsedFunctionDefinition(expression: ParsedExpression, 91 | parameters: Iterable[(String, String)]) 92 | extends ParsedDecisionLogic 93 | 94 | case class ParsedDecisionTable(inputs: Iterable[ParsedInput], 95 | outputs: Iterable[ParsedOutput], 96 | rules: Iterable[ParsedRule], 97 | hitPolicy: HitPolicy, 98 | aggregation: BuiltinAggregator) 99 | extends ParsedDecisionLogic 100 | 101 | case class ParsedRule(id: String, 102 | inputEntries: Iterable[ParsedExpression], 103 | outputEntries: Iterable[(String, ParsedExpression)]) 104 | 105 | case class ParsedInput(id: String, name: String, expression: ParsedExpression) 106 | 107 | case class ParsedOutput(id: String, 108 | name: String, 109 | label: String, 110 | value: Option[String], 111 | defaultValue: Option[ParsedExpression]) 112 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/services/org.camunda.feel.context.CustomFunctionProvider: -------------------------------------------------------------------------------- 1 | org.camunda.dmn.spi.MyCustomFunctionProvider -------------------------------------------------------------------------------- /src/test/resources/META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper: -------------------------------------------------------------------------------- 1 | org.camunda.dmn.spi.MyCustomValueMapper -------------------------------------------------------------------------------- /src/test/resources/bkm/BkmWithContext.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Calculation(x, y) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | a + b 20 | 21 | 22 | 23 | 24 | 25 | a * b 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/test/resources/bkm/BkmWithDecisionTable.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Discount(x, y) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | customer 18 | 19 | 20 | 21 | orderSize 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 0.1 31 | 32 | 33 | 34 | 35 | 36 | 37 | = 10]]> 38 | 39 | 0.15 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 0.05 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/test/resources/bkm/BkmWithLiteralExpression.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sum(x, y) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | a+b 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/resources/bkm/BkmWithRelation.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Calculation(x, y) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | "A" 21 | 22 | 23 | a + b 24 | 25 | 26 | 27 | 28 | "B" 29 | 30 | 31 | (a + b) * 1.5 32 | 33 | 34 | 35 | 36 | "C" 37 | 38 | 39 | (a + b) * 1.75 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/test/resources/bkm/BkmWithoutEncapsulatedLogic.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | x + y 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/resources/config/bkm_with_spaces_and_dash.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | "Hello " + greetingName(First Name, Last-Name) 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | First Name + " " + Last-Name 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/test/resources/config/decision_with_dash.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | name 13 | 14 | 15 | 16 | 17 | "Hello " + First-Name 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/resources/config/decision_with_invalid_expression.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | "Hello " + name ! 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/resources/config/decision_with_spaces.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | name 13 | 14 | 15 | 16 | 17 | "Hello " + First Name 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/resources/config/decision_with_spaces_in_item_definition_and_item_component.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | string 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | name 22 | 23 | 24 | 25 | 26 | 27 | 28 | "Hello " + First Name + " " + greetingData.Last Name 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/test/resources/config/with_invalid_bkm.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | "Hello " + name 13 | 14 | 15 | 16 | 17 | 18 | 19 | "invalid" ! 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/test/resources/config/with_invalid_decision.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | "Hello " + name 10 | 11 | 12 | 13 | 14 | 15 | "invalid" ! 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/test/resources/config/with_specific_date_time.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | now() 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/test/resources/context/ContextWithInvocation.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | customer 10 | 11 | 12 | 13 | orderSize 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 0.1 23 | 24 | 25 | 26 | 27 | 28 | 29 | = 10]]> 30 | 31 | 0.15 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 0.05 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Discount table 55 | 56 | 57 | 58 | 59 | Customer 60 | 61 | 62 | 63 | 64 | 65 | OrderSize 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | false 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/test/resources/context/Eligibility.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Applicant.Age 9 | 10 | 11 | 12 | 13 | 14 | Applicant.Monthly.Income 15 | 16 | 17 | 18 | 19 | 20 | Affordability.PreBureauRiskCategory 21 | 22 | 23 | 24 | 25 | 26 | Affordability.InstallmentAffordable 27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/test/resources/context/NestedContext.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | "EMPLOYED" 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 10000.00 18 | 19 | 20 | 21 | 22 | 23 | 2500.00 24 | 25 | 26 | 27 | 28 | 29 | 3000.00 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/test/resources/context/SimpleContext.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 51 9 | 10 | 11 | 12 | 13 | 14 | "M" 15 | 16 | 17 | 18 | 19 | 20 | "EMPLOYED" 21 | 22 | 23 | 24 | 25 | 26 | false 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/adjustments.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | customer 7 | 8 | 9 | 10 | orderSize 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 0.1 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | = 10]]> 30 | 31 | 0.15 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 0.05 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/adjustments_default-output.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | customer 7 | 8 | 9 | 10 | orderSize 11 | 12 | 13 | 14 | 0.05 15 | 16 | 17 | 18 | "Ground" 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 0.1 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | = 10]]> 36 | 37 | 0.15 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 0.05 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/adjustments_empty_output_name.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | customer 8 | 9 | 10 | 11 | 12 | orderSize 13 | 14 | 15 | 16 | 17 | 18 | 19 | "Business" 20 | 21 | 22 | < 10 23 | 24 | 25 | 0.1 26 | 27 | 28 | "Air" 29 | 30 | 31 | 32 | 33 | 34 | "Business" 35 | 36 | 37 | >= 10 38 | 39 | 40 | 0.15 41 | 42 | 43 | "Air" 44 | 45 | 46 | 47 | 48 | "Private" 49 | 50 | 51 | 52 | 53 | 54 | 0.05 55 | 56 | 57 | "Air" 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/applicantRiskRating.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | applicantAge 7 | 8 | 9 | 10 | medicalHistory 11 | 12 | 13 | 14 | 15 | 60]]> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 60]]> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | [25..60] 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/applicantRiskRating_priority.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | applicantAge 7 | 8 | 9 | 10 | medicalHistory 11 | 12 | 13 | 14 | "High", "Medium", "Low" 15 | 16 | 17 | 18 | = 25]]> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 60]]> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/discount.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | customer 8 | 9 | 10 | 11 | 12 | orderSize 13 | 14 | 15 | 16 | 17 | 18 | "Business" 19 | 20 | 21 | < 10 22 | 23 | 24 | 0.1 25 | 26 | 27 | 28 | 29 | 30 | "Business" 31 | 32 | 33 | >= 9 34 | 35 | 36 | 0.15 37 | 38 | 39 | 40 | 41 | "Private" 42 | 43 | 44 | 45 | 46 | 47 | 0.05 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/discount_collect_max.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | customer 7 | 8 | 9 | 10 | orderSize 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 0.1 20 | 21 | 22 | 23 | 24 | 25 | = 10]]> 26 | 27 | 0.15 28 | 29 | 30 | 31 | 32 | 33 | = 15]]> 34 | 35 | 0.06 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 0.05 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/discount_default-output.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | customer 7 | 8 | 9 | 10 | orderSize 11 | 12 | 13 | 14 | 15 | 0.05 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 0.1 24 | 25 | 26 | 27 | 28 | 29 | 30 | = 10]]> 31 | 32 | 0.15 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 0.05 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/empty-expression.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | orderSize 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/expression-language.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | customer 7 | 8 | 9 | 10 | orderSize 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 0.1 20 | 21 | 22 | 23 | 24 | 25 | 26 | 10]]> 27 | 28 | 0.15 29 | 30 | 31 | 32 | 33 | 34 | - 35 | 36 | 0.05 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/holidays_collect.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | age 7 | 8 | 9 | 10 | yearsOfService 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 20 | 21 | 22 | 23 | 24 | = 60]]> 25 | 26 | 27 | 28 | 3 29 | 30 | 31 | 32 | 33 | 34 | = 30]]> 35 | 36 | 3 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 5 45 | 46 | 47 | 48 | = 60]]> 49 | 50 | 51 | 52 | 5 53 | 54 | 55 | 56 | 57 | 58 | = 30]]> 59 | 60 | 5 61 | 62 | 63 | 64 | [18..60) 65 | 66 | [15..30) 67 | 68 | 2 69 | 70 | 71 | 72 | [45..60) 73 | 74 | 75 | 76 | 2 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/holidays_collect_sum.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | age 8 | 9 | 10 | 11 | 12 | yearsOfService 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 22 25 | 26 | 27 | 28 | 29 | < 18 30 | 31 | 32 | 33 | 34 | 35 | 5 36 | 37 | 38 | 39 | 40 | >= 60 41 | 42 | 43 | 44 | 45 | 46 | 5 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | >= 30 55 | 56 | 57 | 5 58 | 59 | 60 | 61 | 62 | [18..60) 63 | 64 | 65 | [15..30) 66 | 67 | 68 | 2 69 | 70 | 71 | 72 | 73 | >= 60 74 | 75 | 76 | 77 | 78 | 79 | 3 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | >= 30 88 | 89 | 90 | 3 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/holidays_output_order.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | age 8 | 9 | 10 | 11 | 12 | yearsOfService 13 | 14 | 15 | 16 | 17 | 22, 5, 3, 2 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 22 29 | 30 | 31 | 32 | 33 | 34 | >= 60 35 | 36 | 37 | 38 | 39 | 40 | 3 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | >= 30 49 | 50 | 51 | 3 52 | 53 | 54 | 55 | 56 | < 18 57 | 58 | 59 | 60 | 61 | 62 | 5 63 | 64 | 65 | 66 | 67 | >= 60 68 | 69 | 70 | 71 | 72 | 73 | 5 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | >= 30 82 | 83 | 84 | 5 85 | 86 | 87 | 88 | 89 | [18..60) 90 | 91 | 92 | [15..30) 93 | 94 | 95 | 2 96 | 97 | 98 | 99 | 100 | [45..60) 101 | 102 | 103 | < 30 104 | 105 | 106 | 2 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/insuranceFee.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | years 7 | 8 | 9 | 10 | 11 | 12 | 13 | 200 14 | 15 | 16 | 17 | 2]]> 18 | 19 | 190 20 | 21 | 22 | 23 | 5]]> 24 | 25 | 170 26 | 27 | 28 | 29 | 10]]> 30 | 31 | 150 32 | 33 | 34 | 35 | 15]]> 36 | 37 | 100 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/invalid-expression.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | customer 7 | 8 | 9 | 10 | orderSize 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 0.1 20 | 21 | 22 | 23 | 24 | 25 | 26 | 10L]]> 27 | 28 | 0.15 29 | 30 | 31 | 32 | 33 | 34 | - 35 | 36 | 0.05 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/personLoanCompliance.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | creditRating 7 | 8 | 9 | 10 | creditBalance 11 | 12 | 13 | 14 | loanBalance 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | = 10000]]> 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | = 50000]]> 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/routingRules.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | age 7 | 8 | 9 | 10 | riskCategory 11 | 12 | 13 | 14 | deptReview 15 | 16 | 17 | 18 | "DECLINE","REFER","ACCEPT" 19 | 20 | 21 | 22 | "LEVEL 2","LEVEL 1", "NONE" 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/specialDiscount.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | typeOfOrder 7 | 8 | 9 | 10 | customerLocation 11 | 12 | 13 | 14 | typeOfCustomer 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 10 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 0 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 5 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/test/resources/decisiontable/studentFinancialPackageEligibility.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | gpa 7 | 8 | 9 | 10 | acitvitiesCount 11 | 12 | 13 | 14 | socialMembership 15 | 16 | 17 | 18 | 19 | 3.5]]> 20 | 21 | = 4]]> 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 3.0]]> 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 3.0]]> 40 | 41 | = 2]]> 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/test/resources/dmn1.1/greeting.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | "Hello " + name 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/dmn1.2/greeting.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | "Hello " + name 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/dmn1.3/greeting.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | "Hello " + name 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/dmn1.4/greeting.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | "Hello " + name 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/dmn1.5/greeting.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | "Hello " + name 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/functionDefinition/ApplicantData.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | (amount *rate/12) / (1 - (1 + rate/12)**-term) 13 | 14 | 15 | 16 | 17 | 18 | PMT(0.25, 36, 100000.00) 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/resources/functionDefinition/FeelUserFunction.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | function(a,b) a + b 9 | 10 | 11 | 12 | 13 | add(2, 3) 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/test/resources/functionDefinition/RequiredBkmWithContext.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | function(x) x + 1 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | lib().incrFn(4) 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/resources/functionDefinition/RequiredBkmWithFunction.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | function(x) x + 1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | lib()(4) 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/test/resources/functionDefinition/RequiredDecisionWithContext.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | function(x) x + 1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | lib.incrFn(4) 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/resources/functionDefinition/RequiredDecisionWithFunction.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | function(x) x + 1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | lib(4) 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/test/resources/invocation/discount.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | customer 10 | 11 | 12 | 13 | orderSize 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 0.1 23 | 24 | 25 | 26 | 27 | 28 | 29 | = 10]]> 30 | 31 | 0.15 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 0.05 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Discount table 52 | 53 | 54 | 55 | 56 | Customer 57 | 58 | 59 | 60 | 61 | 62 | OrderSize 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/test/resources/invocation/missingKnowledgeRequirement.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | customer 10 | 11 | 12 | 13 | orderSize 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 0.1 23 | 24 | 25 | 26 | 27 | 28 | 29 | = 10]]> 30 | 31 | 0.15 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 0.05 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Discount table 49 | 50 | 51 | 52 | 53 | Customer 54 | 55 | 56 | 57 | 58 | 59 | OrderSize 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/test/resources/invocation/missingParameters.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | customer 10 | 11 | 12 | 13 | orderSize 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 0.1 23 | 24 | 25 | 26 | 27 | 28 | 29 | = 10]]> 30 | 31 | 0.15 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 0.05 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Discount table 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/test/resources/invocation/withoutParameters.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 51 10 | 11 | 12 | 13 | 14 | 15 | "M" 16 | 17 | 18 | 19 | 20 | 21 | "EMPLOYED" 22 | 23 | 24 | 25 | 26 | 27 | false 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Applicant Data 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/test/resources/list/ApplicantData.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 2500 10 | 11 | 12 | 3000 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/test/resources/literalexpression/greeting.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | "Hello " + name 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/literalexpression/type-mismatch.dmn: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | "Hello " + name 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/resources/relation/ApplicantData.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | date("2008-03-12") 14 | 15 | 16 | "home mortgage" 17 | 18 | 19 | 100 20 | 21 | 22 | 23 | 24 | date("2011-04-01") 25 | 26 | 27 | "foreclosure warning" 28 | 29 | 30 | 150 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/test/resources/requirements/ApplicantData.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 51 30 | 31 | 32 | 33 | 34 | 35 | "M" 36 | 37 | 38 | 39 | 40 | 41 | "EMPLOYED" 42 | 43 | 44 | 45 | 46 | 47 | false 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 10000.00 59 | 60 | 61 | 62 | 63 | 64 | 2500.00 65 | 66 | 67 | 68 | 69 | 70 | 3000.00 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/test/resources/requirements/cyclic-dependencies-in-bkm.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Calculation(x, y) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | a + b 20 | 21 | 22 | 23 | 24 | 25 | Multiply(a * b) 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | a * b 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/test/resources/requirements/cyclic-dependencies-in-decisions.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | true 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/test/resources/requirements/discount.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | customer 8 | 9 | 10 | 11 | orderSize 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 0.1 21 | 22 | 23 | 24 | 25 | 26 | 27 | = 10]]> 28 | 29 | 0.15 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0.05 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Discount * 100 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/test/resources/requirements/non-cyclic-nested-dependencies-in-decisions.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | [a, b] 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | a + 1 22 | 23 | 24 | 25 | 26 | 27 | 1 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/test/resources/spi/SpiTests.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | input 6 | 7 | 8 | 9 | 10 | "foobar" 11 | 12 | 13 | 14 | 15 | incr(x) 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/AuditLogTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.parser._ 20 | import org.camunda.dmn.Audit._ 21 | import org.camunda.feel.syntaxtree.{ 22 | ValBoolean, 23 | ValError, 24 | ValNull, 25 | ValNumber, 26 | ValString 27 | } 28 | import org.scalatest.flatspec.AnyFlatSpec 29 | import org.scalatest.matchers.should.Matchers 30 | 31 | class AuditLogTest extends AnyFlatSpec with Matchers with DecisionTest { 32 | 33 | lazy val discountDecision = parse("/decisiontable/discount.dmn") 34 | lazy val eligibilityContext = parse("/context/Eligibility.dmn") 35 | lazy val greeting = parse("/literalexpression/greeting.dmn") 36 | lazy val discountBkm = parse("/invocation/discount.dmn") 37 | lazy val bkmFunction = parse("/bkm/BkmWithLiteralExpression.dmn") 38 | 39 | "The audit log" should "contains the result of a decision table" in { 40 | 41 | eval(discountDecision, 42 | "discount", 43 | Map("customer" -> "Business", "orderSize" -> 7)) 44 | 45 | auditLog.entries should have size(1) 46 | 47 | val auditLogEntry = auditLog.entries.head 48 | auditLogEntry.id should be("discount") 49 | auditLogEntry.name should be("Discount") 50 | auditLogEntry.decisionLogic shouldBe a[ParsedDecisionTable] 51 | auditLogEntry.result shouldBe a[DecisionTableEvaluationResult] 52 | 53 | val result = 54 | auditLogEntry.result.asInstanceOf[DecisionTableEvaluationResult] 55 | result.inputs.size should be(2) 56 | 57 | result.inputs(0).input.id should be("input1") 58 | result.inputs(0).input.name should be("Customer") 59 | result.inputs(0).value should be(ValString("Business")) 60 | 61 | result.inputs(1).input.name should be("Order Size") 62 | result.inputs(1).value should be(ValNumber(7)) 63 | 64 | result.matchedRules.size should be(1) 65 | result.matchedRules(0).outputs.size should be(1) 66 | result.matchedRules(0).outputs(0).output.name should be("discount") 67 | result.matchedRules(0).outputs(0).value should be(ValNumber(0.1)) 68 | 69 | result.result should be(ValNumber(0.1)) 70 | } 71 | 72 | it should "contains the result of a decision table if no rule matched" in { 73 | 74 | eval(discountDecision, 75 | "discount", 76 | Map("customer" -> "Other", "orderSize" -> 7)) 77 | 78 | auditLog.entries should have size (1) 79 | 80 | val auditLogEntry = auditLog.entries.head 81 | auditLogEntry.id should be("discount") 82 | auditLogEntry.name should be("Discount") 83 | auditLogEntry.decisionLogic shouldBe a[ParsedDecisionTable] 84 | auditLogEntry.result shouldBe a[DecisionTableEvaluationResult] 85 | 86 | val result = 87 | auditLogEntry.result.asInstanceOf[DecisionTableEvaluationResult] 88 | result.inputs.size should be(2) 89 | 90 | result.inputs(0).input.id should be("input1") 91 | result.inputs(0).input.name should be("Customer") 92 | result.inputs(0).value should be(ValString("Other")) 93 | 94 | result.inputs(1).input.name should be("Order Size") 95 | result.inputs(1).value should be(ValNumber(7)) 96 | 97 | result.matchedRules.size should be(0) 98 | 99 | result.result should be(ValNull) 100 | } 101 | 102 | it should "contains the result of a decision table if a failure occurred" in { 103 | 104 | eval(discountDecision, 105 | "discount", 106 | Map("customer" -> "Business", "orderSize" -> 9)) 107 | 108 | auditLog.entries should have size (1) 109 | 110 | val auditLogEntry = auditLog.entries.head 111 | auditLogEntry.id should be("discount") 112 | auditLogEntry.name should be("Discount") 113 | auditLogEntry.decisionLogic shouldBe a[ParsedDecisionTable] 114 | auditLogEntry.result shouldBe a[DecisionTableEvaluationResult] 115 | 116 | val result = 117 | auditLogEntry.result.asInstanceOf[DecisionTableEvaluationResult] 118 | result.inputs.size should be(2) 119 | 120 | result.inputs(0).input.id should be("input1") 121 | result.inputs(0).input.name should be("Customer") 122 | result.inputs(0).value should be(ValString("Business")) 123 | 124 | result.inputs(1).input.name should be("Order Size") 125 | result.inputs(1).value should be(ValNumber(9)) 126 | 127 | result.matchedRules.size should be(2) 128 | result.matchedRules(0).outputs.size should be(1) 129 | result.matchedRules(0).outputs(0).output.name should be("discount") 130 | result.matchedRules(0).outputs(0).value should be(ValNumber(0.1)) 131 | 132 | result.result should be(ValError( 133 | "multiple values aren't allowed for UNIQUE hit policy. found: 'List(Map(discount -> 0.1), Map(discount -> 0.15))'")) 134 | } 135 | 136 | it should "contains the result of a context" in { 137 | 138 | val variables = Map( 139 | "Applicant" -> Map("Age" -> 51, "Monthly" -> Map("Income" -> 10000.00)), 140 | "Affordability" -> Map("PreBureauRiskCategory" -> "DECLINE", 141 | "InstallmentAffordable" -> true)) 142 | 143 | eval(eligibilityContext, "eligibility", variables) 144 | 145 | auditLog.entries should have size(1) 146 | 147 | val auditLogEntry = auditLog.entries.head 148 | auditLogEntry.id should be("eligibility") 149 | auditLogEntry.name should be("Eligibility") 150 | auditLogEntry.decisionLogic shouldBe a[ParsedContext] 151 | auditLogEntry.result shouldBe a[ContextEvaluationResult] 152 | 153 | val result = auditLogEntry.result.asInstanceOf[ContextEvaluationResult] 154 | result.entries.size should be(4) 155 | 156 | result.entries("Age") should be(ValNumber(51)) 157 | result.entries("MonthlyIncome") should be(ValNumber(10000.00)) 158 | result.entries("PreBureauRiskCategory") should be(ValString("DECLINE")) 159 | result.entries("InstallmentAffordable") should be(ValBoolean(true)) 160 | 161 | result.result should be(ValString("INELIGIBLE")) 162 | } 163 | 164 | it should "contains the result of an expression" in { 165 | 166 | eval(greeting, "greeting", Map("name" -> "John")) 167 | 168 | auditLog.entries should have size(1) 169 | 170 | val auditLogEntry = auditLog.entries.head 171 | auditLogEntry.id should be("greeting") 172 | auditLogEntry.name should be("GreetingMessage") 173 | auditLogEntry.decisionLogic shouldBe a[ParsedLiteralExpression] 174 | auditLogEntry.result shouldBe a[SingleEvaluationResult] 175 | 176 | val result = auditLogEntry.result.asInstanceOf[SingleEvaluationResult] 177 | result.result should be(ValString("Hello John")) 178 | } 179 | 180 | it should "contains the result of a BKM invocation" in { 181 | 182 | eval(discountBkm, 183 | "discount", 184 | Map("Customer" -> "Business", "OrderSize" -> 7)) 185 | 186 | auditLog.entries should have size(2) 187 | 188 | val bkmAuditLogEntry = auditLog.entries.head 189 | val rootAuditLogEntry = auditLog.entries.last 190 | 191 | rootAuditLogEntry.id should be("discount") 192 | rootAuditLogEntry.name should be("Discount") 193 | rootAuditLogEntry.decisionLogic shouldBe a[ParsedInvocation] 194 | rootAuditLogEntry.result shouldBe a[SingleEvaluationResult] 195 | 196 | val result = rootAuditLogEntry.result.asInstanceOf[SingleEvaluationResult] 197 | result.result should be(ValNumber(0.1)) 198 | 199 | bkmAuditLogEntry.id should be("bkm_discount") 200 | bkmAuditLogEntry.name should be("Discount table") 201 | bkmAuditLogEntry.decisionLogic shouldBe a[ParsedDecisionTable] 202 | bkmAuditLogEntry.result shouldBe a[DecisionTableEvaluationResult] 203 | bkmAuditLogEntry.result.result should be(ValNumber(0.1)) 204 | } 205 | 206 | it should "contains the result of a BKM function invocation" in { 207 | 208 | eval(bkmFunction, "literalExpression", Map("x" -> 2, "y" -> 3)) 209 | 210 | auditLog.entries should have size (2) 211 | 212 | val bkmAuditLogEntry = auditLog.entries.head 213 | val rootAuditLogEntry = auditLog.entries.last 214 | 215 | rootAuditLogEntry.id should be("literalExpression") 216 | rootAuditLogEntry.name should be("BKM with Literal Expression") 217 | rootAuditLogEntry.decisionLogic shouldBe a[ParsedLiteralExpression] 218 | rootAuditLogEntry.result shouldBe a[SingleEvaluationResult] 219 | 220 | val result = rootAuditLogEntry.result.asInstanceOf[SingleEvaluationResult] 221 | result.result should be(ValNumber(5)) 222 | 223 | bkmAuditLogEntry.name should be("Sum") 224 | bkmAuditLogEntry .decisionLogic shouldBe a[ParsedLiteralExpression] 225 | bkmAuditLogEntry.result shouldBe a[SingleEvaluationResult] 226 | bkmAuditLogEntry.result.result should be(ValNumber(5)) 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/BkmTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.scalatest.flatspec.AnyFlatSpec 19 | import org.scalatest.matchers.should.Matchers 20 | 21 | class BkmTest extends AnyFlatSpec with Matchers with DecisionTest { 22 | 23 | lazy val literalExpression = parse("/bkm/BkmWithLiteralExpression.dmn") 24 | lazy val context = parse("/bkm/BkmWithContext.dmn") 25 | lazy val relation = parse("/bkm/BkmWithRelation.dmn") 26 | lazy val decisionTable = parse("/bkm/BkmWithDecisionTable.dmn") 27 | lazy val withoutEncapsulatedLogic = parse( 28 | "/bkm/BkmWithoutEncapsulatedLogic.dmn") 29 | 30 | "A BKM with a Literal Expression" should "be invoked as function" in { 31 | eval(literalExpression, "literalExpression", Map("x" -> 2, "y" -> 3)) should be( 32 | 5) 33 | } 34 | 35 | "A BKM with a Context" should "be invoked as function" in { 36 | eval(context, "context", Map("x" -> 2, "y" -> 3)) should be( 37 | Map( 38 | "Sum" -> 5, 39 | "Multiply" -> 6 40 | )) 41 | } 42 | 43 | "A BKM with a Relation" should "be invoked as function" in { 44 | eval(relation, "relation", Map("x" -> 2, "y" -> 3)) should be( 45 | List( 46 | Map("rate" -> "A", "fee" -> 5), 47 | Map("rate" -> "B", "fee" -> 7.5), 48 | Map("rate" -> "C", "fee" -> 8.75) 49 | )) 50 | } 51 | 52 | "A BKM with a Decision Table" should "be invoked as function" in { 53 | eval(decisionTable, "decisionTable", Map("x" -> "Business", "y" -> 7)) should be( 54 | 0.1) 55 | } 56 | 57 | "A BKM without encapsulated logic" should "be ignored" in { 58 | eval(withoutEncapsulatedLogic, "literalExpression", Map("x" -> 2, "y" -> 3)) should be( 59 | 5) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/ContextTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.scalatest.flatspec.AnyFlatSpec 19 | import org.scalatest.matchers.should.Matchers 20 | 21 | class ContextTest extends AnyFlatSpec with Matchers with DecisionTest { 22 | 23 | lazy val simpleContext = parse("/context/SimpleContext.dmn") 24 | lazy val nestedContext = parse("/context/NestedContext.dmn") 25 | lazy val eligibilityContext = parse("/context/Eligibility.dmn") 26 | lazy val contextWithInvocation = parse("/context/ContextWithInvocation.dmn") 27 | 28 | "A context" should "return static values" in { 29 | eval(simpleContext, "applicantData", Map()) should be( 30 | Map("Age" -> 51, 31 | "MaritalStatus" -> "M", 32 | "EmploymentStatus" -> "EMPLOYED", 33 | "ExistingCustomer" -> false)) 34 | } 35 | 36 | it should "invocate BKM" in { 37 | eval(contextWithInvocation, 38 | "discount", 39 | Map("Customer" -> "Business", "OrderSize" -> 7)) should be( 40 | Map("Discount" -> 0.1, "ExistingCustomer" -> false)) 41 | } 42 | 43 | it should "return nested values" in { 44 | eval(nestedContext, "applicantData", Map()) should be( 45 | Map("EmploymentStatus" -> "EMPLOYED", 46 | "Monthly" -> Map("Income" -> 10000.00, 47 | "Repayments" -> 2500.00, 48 | "Expenses" -> 3000.00))) 49 | } 50 | 51 | "A context with final result" should "return only final value" in { 52 | val variables = Map( 53 | "Applicant" -> Map("Age" -> 51, "Monthly" -> Map("Income" -> 10000.00)), 54 | "Affordability" -> Map("PreBureauRiskCategory" -> "DECLINE", 55 | "InstallmentAffordable" -> true)) 56 | 57 | eval(eligibilityContext, "eligibility", variables) should be("INELIGIBLE") 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/DecisionTableTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.scalatest.flatspec.AnyFlatSpec 19 | import org.scalatest.matchers.should.Matchers 20 | 21 | class DecisionTableTest extends AnyFlatSpec with Matchers with DecisionTest { 22 | 23 | lazy val discountDecision = parse("/decisiontable/discount.dmn") 24 | lazy val discountWithDefaultOutputDecision = parse( 25 | "/decisiontable/discount_default-output.dmn") 26 | 27 | lazy val adjustmentsDecision = parse("/decisiontable/adjustments.dmn") 28 | lazy val adjustmentsWithDefaultOutputDecision = parse( 29 | "/decisiontable/adjustments_default-output.dmn") 30 | lazy val adjustmentsWithEmptyOutputNameDecision = 31 | getClass.getResourceAsStream( 32 | "/decisiontable/adjustments_empty_output_name.dmn") 33 | 34 | lazy val routingRulesDecision = parse("/decisiontable/routingRules.dmn") 35 | lazy val holidaysDecision = parse("/decisiontable/holidays_output_order.dmn") 36 | 37 | "A decision table with single output" should "return single value" in { 38 | eval(discountDecision, 39 | "discount", 40 | Map("customer" -> "Business", "orderSize" -> 7)) should be(0.1) 41 | } 42 | 43 | it should "return value list" in { 44 | eval(holidaysDecision, "holidays", Map("age" -> 58, "yearsOfService" -> 31)) should be( 45 | List(22, 5, 3)) 46 | } 47 | 48 | it should "return null if no rule match" in { 49 | eval(discountDecision, 50 | "discount", 51 | Map("customer" -> "Something else", "orderSize" -> 9)) should be(None) 52 | } 53 | 54 | it should "return the default-output if no rule match" in { 55 | eval(discountWithDefaultOutputDecision, 56 | "discount", 57 | Map("customer" -> "Something else", "orderSize" -> 9)) should be(0.05) 58 | } 59 | 60 | "A decision table with multiple outputs" should "return single values" in { 61 | val context = Map("customer" -> "Business", "orderSize" -> 7) 62 | 63 | eval(adjustmentsDecision, "adjustments", context) should be( 64 | Map("discount" -> 0.1, "shipping" -> "Air")) 65 | } 66 | 67 | it should "return value list" in { 68 | val context = 69 | Map("age" -> 25, "riskCategory" -> "MEDIUM", "deptReview" -> true) 70 | 71 | eval(routingRulesDecision, "routingRules", context) should be( 72 | List( 73 | Map("routing" -> "REFER", 74 | "reviewLevel" -> "LEVEL 2", 75 | "reason" -> "Applicant under dept review"), 76 | Map("routing" -> "ACCEPT", 77 | "reviewLevel" -> "NONE", 78 | "reason" -> "Acceptable") 79 | )) 80 | } 81 | 82 | it should "return null if no rule match" in { 83 | val context = Map("customer" -> "Something else", "orderSize" -> 9) 84 | 85 | eval(adjustmentsDecision, "adjustments", context) should be(None) 86 | } 87 | 88 | it should "return the default-output if no rule match" in { 89 | val context = Map("customer" -> "Something else", "orderSize" -> 9) 90 | 91 | eval(adjustmentsWithDefaultOutputDecision, "adjustments", context) should be( 92 | Map("discount" -> 0.05, "shipping" -> "Ground")) 93 | } 94 | 95 | it should "fail if an output name is empty" in { 96 | val result = engine.parse(adjustmentsWithEmptyOutputNameDecision) 97 | 98 | result.isLeft should be(true) 99 | result.left.map( 100 | _.message should be("no output name defined for `Discount`")) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/DecisionTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.parser.ParsedDmn 20 | import org.camunda.dmn.Audit.AuditLog 21 | import org.camunda.dmn.Audit.AuditLogListener 22 | 23 | import java.io.InputStream 24 | 25 | trait DecisionTest { 26 | 27 | val engine = new DmnEngine(auditLogListeners = List(new TestAuditLogListener)) 28 | 29 | def parse(file: String): ParsedDmn = { 30 | parseDmn(file).dmn 31 | } 32 | 33 | def parseDmn(dmn: String): ParsedResult = { 34 | val stream = getClass.getResourceAsStream(dmn) 35 | new ParsedResult(engine.parse(stream)) 36 | } 37 | 38 | class ParsedResult(val parserResult: Either[Failure, ParsedDmn]) { 39 | def failure: Failure = { 40 | parserResult match { 41 | case Right(_) => 42 | throw new AssertionError( 43 | "Expected parsing to fail, but was successful") 44 | case Left(failure) => failure 45 | } 46 | } 47 | 48 | def dmn: ParsedDmn = { 49 | parserResult match { 50 | case Right(dmn) => dmn 51 | case Left(failure) => throw new AssertionError(failure.message) 52 | } 53 | } 54 | } 55 | 56 | def eval(decision: ParsedDmn, id: String, context: Map[String, Any]): Any = 57 | engine.eval(decision, id, context) match { 58 | case Right(result) => result.value 59 | case Left(EvalFailure(failure, _)) => failure 60 | } 61 | 62 | var lastAuditLog: AuditLog = _ 63 | 64 | class TestAuditLogListener extends AuditLogListener { 65 | override def onEval(log: AuditLog) = { 66 | lastAuditLog = log 67 | } 68 | override def onFailure(log: AuditLog) = { 69 | lastAuditLog = log 70 | } 71 | } 72 | 73 | def auditLog = lastAuditLog 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/DmnEngineConfigurationTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.parser.{ExpressionFailure, ParsedLiteralExpression} 20 | import org.camunda.feel.FeelEngineClock 21 | import org.scalatest.flatspec.AnyFlatSpec 22 | import org.scalatest.matchers.should.Matchers 23 | 24 | import java.time.{ZoneId, ZonedDateTime} 25 | 26 | class DmnEngineConfigurationTest extends AnyFlatSpec with Matchers { 27 | 28 | private val engineWithEscapeNames = new DmnEngine( 29 | configuration = Configuration( 30 | escapeNamesWithSpaces = true, 31 | escapeNamesWithDashes = true 32 | )) 33 | 34 | private val engineWithLazyEvaluation = new DmnEngine( 35 | configuration = Configuration( 36 | lazyEvaluation = true 37 | )) 38 | 39 | private val customClock = new FeelEngineClock { 40 | override def getCurrentTime: ZonedDateTime = ZonedDateTime.of( 41 | 2021, 12, 21, 18, 9, 5, 333, ZoneId.of("Europe/Berlin")) 42 | } 43 | private val engineWithCustomClock = new DmnEngine( 44 | clock = customClock 45 | ) 46 | 47 | private def decisionWithSpaces = 48 | getClass.getResourceAsStream("/config/decision_with_spaces.dmn") 49 | private def decisionWithSpacesInItemDefinitionAndItemComponents = 50 | getClass.getResourceAsStream("/config/decision_with_spaces_in_item_definition_and_item_component.dmn") 51 | private def decisionWithDash = 52 | getClass.getResourceAsStream("/config/decision_with_dash.dmn") 53 | private def bkmWithSpacesAndDash = 54 | getClass.getResourceAsStream("/config/bkm_with_spaces_and_dash.dmn") 55 | private def decisionWithInvalidExpression = 56 | getClass.getResourceAsStream("/config/decision_with_invalid_expression.dmn") 57 | private def decisionWithOtherInvalidDecision = 58 | getClass.getResourceAsStream("/config/with_invalid_decision.dmn") 59 | private def decisionWithInvalidBkm = 60 | getClass.getResourceAsStream("/config/with_invalid_bkm.dmn") 61 | private def decisionWithSpecificDateTime = 62 | getClass.getResourceAsStream("/config/with_specific_date_time.dmn") 63 | 64 | "A DMN engine with escaped names" should "evaluate a decision with spaces" in { 65 | 66 | val result = engineWithEscapeNames 67 | .parse(decisionWithSpaces) 68 | .flatMap(engineWithEscapeNames.eval(_, "greeting", Map("name" -> "DMN"))) 69 | 70 | result.isRight should be(true) 71 | result.map(_.value should be("Hello DMN")) 72 | } 73 | 74 | it should "evaluate a decision with spaces in item definitions and item components" in { 75 | 76 | val result = engineWithEscapeNames 77 | .parse(decisionWithSpacesInItemDefinitionAndItemComponents) 78 | .flatMap(engineWithEscapeNames.eval(_, "greeting", Map("name" -> "Luke", "greetingData" -> Map("Last Name" -> "Skywalker")))) 79 | 80 | result.isRight should be(true) 81 | result.map(_.value should be("Hello Luke Skywalker")) 82 | } 83 | 84 | it should "evaluate a decision with dash" in { 85 | 86 | val result = engineWithEscapeNames 87 | .parse(decisionWithDash) 88 | .flatMap(engineWithEscapeNames.eval(_, "greeting", Map("name" -> "DMN"))) 89 | 90 | result.isRight should be(true) 91 | result.map(_.value should be("Hello DMN")) 92 | } 93 | 94 | it should "invoke a BKM with spaces & dash in parameter" in { 95 | val result = engineWithEscapeNames 96 | .parse(bkmWithSpacesAndDash) 97 | .flatMap( 98 | engineWithEscapeNames.eval(_, 99 | "greeting", 100 | Map("First Name" -> "Luke", 101 | "Last-Name" -> "Skywalker"))) 102 | 103 | result.isRight should be(true) 104 | result.map(_.value should be("Hello Luke Skywalker")) 105 | } 106 | 107 | "A DMN engine with lazy evaluation" should "ignore invalid expression on parsing" in { 108 | 109 | val result = engineWithLazyEvaluation.parse(decisionWithInvalidExpression) 110 | 111 | result.isRight should be(true) 112 | result.map(_.decisionsById("greeting").logic match { 113 | case ParsedLiteralExpression(expression) => 114 | expression shouldBe a[ExpressionFailure] 115 | }) 116 | } 117 | 118 | it should "report failure on evaluation" in { 119 | 120 | val result = engineWithLazyEvaluation 121 | .parse(decisionWithInvalidExpression) 122 | .flatMap( 123 | engineWithLazyEvaluation.eval(_, "greeting", Map("name" -> "DMN"))) 124 | 125 | result.isRight should be(false) 126 | result.left.map { 127 | case EvalFailure(Failure(failure), _) => 128 | failure should startWith("FEEL expression: failed to parse expression") 129 | } 130 | } 131 | 132 | it should "ignore other invalid decision" in { 133 | val result = engineWithLazyEvaluation 134 | .parse(decisionWithOtherInvalidDecision) 135 | .flatMap( 136 | engineWithLazyEvaluation.eval(_, "greeting", Map("name" -> "DMN"))) 137 | 138 | result.isRight should be(true) 139 | result.map(_.value should be("Hello DMN")) 140 | } 141 | 142 | it should "ignore other invalid BKM" in { 143 | val result = engineWithLazyEvaluation 144 | .parse(decisionWithInvalidBkm) 145 | .flatMap( 146 | engineWithLazyEvaluation.eval(_, "greeting", Map("name" -> "DMN"))) 147 | 148 | result.isRight should be(true) 149 | result.map(_.value should be("Hello DMN")) 150 | } 151 | 152 | "A DMN engine with custom clock" should "use that clock in decision" in { 153 | val result = engineWithCustomClock.parse(decisionWithSpecificDateTime) 154 | .flatMap(engineWithCustomClock.eval(_, "currentTime", Map.empty[String, Any])) 155 | 156 | result.isRight should be(true) 157 | result.map(_.value should be(customClock.getCurrentTime)) 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/DmnEngineTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.camunda.dmn.parser.ParsedDmn 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | import org.scalatest.matchers.should.Matchers 22 | 23 | import java.io.InputStream 24 | 25 | class DmnEngineTest extends AnyFlatSpec with Matchers { 26 | 27 | private val engine = new DmnEngine 28 | 29 | private def discountDecision = 30 | getClass.getResourceAsStream("/decisiontable/discount.dmn") 31 | 32 | private def invalidExpressionDecision = 33 | getClass.getResourceAsStream("/decisiontable/invalid-expression.dmn") 34 | 35 | private def expressionLanguageDecision = 36 | getClass.getResourceAsStream("/decisiontable/expression-language.dmn") 37 | 38 | private def emptyExpressionDecision = 39 | getClass.getResourceAsStream("/decisiontable/empty-expression.dmn") 40 | 41 | private def parse(resource: InputStream): ParsedDmn = { 42 | engine.parse(resource) match { 43 | case Right(decision) => decision 44 | case Left(failure) => throw new AssertionError(failure) 45 | } 46 | } 47 | 48 | "A DMN engine" should "evaluate a decision table" in { 49 | 50 | val parsedDmn = parse(discountDecision) 51 | val result = engine.eval(parsedDmn, 52 | "discount", 53 | Map("customer" -> "Business", "orderSize" -> 7)) 54 | 55 | result.isRight should be(true) 56 | result.map(_.value should be(0.1)) 57 | } 58 | 59 | it should "parse and evaluate a decision table" in { 60 | 61 | val parseResult = engine.parse(discountDecision) 62 | parseResult.isRight should be(true) 63 | 64 | parseResult.map { parsedDmn => 65 | val result = engine.eval(parsedDmn, 66 | "discount", 67 | Map("customer" -> "Business", "orderSize" -> 7)) 68 | 69 | result.isRight should be(true) 70 | result.map(_.value should be(0.1)) 71 | } 72 | } 73 | 74 | it should "report parse failures" in { 75 | 76 | val parseResult = engine.parse(invalidExpressionDecision) 77 | 78 | parseResult.isLeft should be(true) 79 | parseResult.left.map( 80 | _.message should include("FEEL unary-tests: failed to parse expression")) 81 | } 82 | 83 | it should "report parse failures on evaluation" in { 84 | 85 | val result = 86 | engine.eval(invalidExpressionDecision, "discount", Map[String, Any]()) 87 | 88 | result.isLeft should be(true) 89 | result.left.map( 90 | _.message should include("FEEL unary-tests: failed to parse expression")) 91 | } 92 | 93 | it should "report an evaluation failure" in { 94 | 95 | val parsedDmn = parse(discountDecision) 96 | val result = engine.eval( 97 | parsedDmn, 98 | "discount", 99 | Map[String, Any]("customer" -> "Business", "orderSize" -> 9)) 100 | 101 | result.isLeft should be(true) 102 | result.left.map( 103 | _.failure.message should include("multiple values aren't allowed for UNIQUE hit policy")) 104 | } 105 | 106 | it should "report parse failures if expression language is set" in { 107 | 108 | val parseResult = engine.parse(expressionLanguageDecision) 109 | 110 | parseResult.isLeft should be(true) 111 | parseResult.left.map( 112 | _.message should include("Expression language 'groovy' is not supported")) 113 | } 114 | 115 | it should "report parse failures if an expression has no content" in { 116 | 117 | val parseResult = engine.parse(emptyExpressionDecision) 118 | 119 | parseResult.isLeft should be(true) 120 | 121 | val failure = parseResult.left.get 122 | 123 | failure.message should include( 124 | "The expression 'inputExpression1' must not be empty.") 125 | } 126 | 127 | it should "evaluate a decision and return the audit log" in { 128 | val parsedDmn = parse(discountDecision) 129 | val result = engine.eval(parsedDmn, 130 | "discount", 131 | Map("customer" -> "Business", "orderSize" -> 7)) 132 | 133 | result.isRight should be(true) 134 | result.map { 135 | case Result(value, auditLog) => 136 | value should be(0.1) 137 | 138 | auditLog.dmn should be(parsedDmn) 139 | auditLog.entries.head.id should be("discount") 140 | auditLog.entries.head.name should be("Discount") 141 | } 142 | } 143 | 144 | it should "report an evaluation failure and return the audit log" in { 145 | val parsedDmn = parse(discountDecision) 146 | val result = 147 | engine.eval(parsedDmn, 148 | "discount", 149 | Map("customer" -> "Business", "orderSize" -> 9)) 150 | 151 | result.isLeft should be(true) 152 | result.left.map { 153 | case EvalFailure(failure, auditLog) => 154 | failure.message should include("multiple values aren't allowed for UNIQUE hit policy") 155 | 156 | auditLog.dmn should be(parsedDmn) 157 | auditLog.entries.head.id should be("discount") 158 | auditLog.entries.head.name should be("Discount") 159 | } 160 | } 161 | 162 | it should "report a failure if no decision exists for the given id" in { 163 | 164 | val parseResult = engine.parse(discountDecision) 165 | parseResult.isRight should be(true) 166 | 167 | parseResult.map { parsedDmn => 168 | val result = engine.eval( 169 | dmn = parsedDmn, 170 | decisionId = "not-existing", 171 | variables = Map("customer" -> "Business")) 172 | 173 | result.isLeft should be(true) 174 | result.left.map( 175 | _.failure.message should be("no decision found with id 'not-existing'") 176 | ) 177 | } 178 | } 179 | 180 | it should "report a failure if no decision exists for the given name" in { 181 | 182 | val parseResult = engine.parse(discountDecision) 183 | parseResult.isRight should be(true) 184 | 185 | parseResult.map { parsedDmn => 186 | val result = engine.evalByName( 187 | dmn = parsedDmn, 188 | decisionName = "not-existing", 189 | variables = Map("customer" -> "Business")) 190 | 191 | result.isLeft should be(true) 192 | result.left.map( 193 | _.failure.message should be("no decision found with name 'not-existing'") 194 | ) 195 | } 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/DmnParserTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.scalatest.flatspec.AnyFlatSpec 19 | import org.scalatest.matchers.should.Matchers 20 | 21 | class DmnParserTest extends AnyFlatSpec with Matchers with DecisionTest { 22 | 23 | "A DMN file with cyclic dependencies between decisions" should "return an error" in { 24 | val failure = 25 | parseDmn("/requirements/cyclic-dependencies-in-decisions.dmn").failure 26 | 27 | failure.message should be( 28 | "Invalid DMN model: Cyclic dependencies between decisions detected.") 29 | } 30 | 31 | "A DMN file with non-cyclic nested dependencies" should "be parsed successfully" in { 32 | val parsedResult = parseDmn("/requirements/non-cyclic-nested-dependencies-in-decisions.dmn") 33 | 34 | parsedResult.parserResult.isRight should be(true) 35 | } 36 | 37 | "A DMN file with cyclic dependencies between BKMs" should "return an error" in { 38 | val failure = 39 | parseDmn("/requirements/cyclic-dependencies-in-bkm.dmn").failure 40 | 41 | failure.message should be( 42 | "Invalid DMN model: Cyclic dependencies between BKMs detected.") 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/DmnVersionCompatibilityTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | 22 | class DmnVersionCompatibilityTest 23 | extends AnyFlatSpec 24 | with Matchers 25 | with DecisionTest { 26 | 27 | private def dmn1_1_decision = parse("/dmn1.1/greeting.dmn") 28 | 29 | private def dmn1_2_decision = parse("/dmn1.2/greeting.dmn") 30 | 31 | private def dmn1_3_decision = parse("/dmn1.3/greeting.dmn") 32 | 33 | private def dmn1_4_decision = parse("/dmn1.4/greeting.dmn") 34 | 35 | private def dmn1_5_decision = parse("/dmn1.5/greeting.dmn") 36 | 37 | "The DMN engine" should "evaluate a DMN 1.1 decision" in { 38 | eval(dmn1_1_decision, "greeting", Map("name" -> "DMN")) should be( 39 | "Hello DMN") 40 | } 41 | 42 | it should "evaluate a DMN 1.2 decision" in { 43 | eval(dmn1_2_decision, "greeting", Map("name" -> "DMN")) should be( 44 | "Hello DMN") 45 | } 46 | 47 | it should "evaluate a DMN 1.3 decision" in { 48 | eval(dmn1_3_decision, "greeting", Map("name" -> "DMN")) should be( 49 | "Hello DMN") 50 | } 51 | 52 | it should "evaluate a DMN 1.4 decision" in { 53 | eval(dmn1_4_decision, "greeting", Map("name" -> "DMN")) should be( 54 | "Hello DMN") 55 | } 56 | 57 | it should "evaluate a DMN 1.5 decision" in { 58 | eval(dmn1_4_decision, "greeting", Map("name" -> "DMN")) should be( 59 | "Hello DMN") 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/FunctionDefinitionTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import java.time.LocalDate 20 | 21 | import org.scalatest.flatspec.AnyFlatSpec 22 | import org.scalatest.matchers.should.Matchers 23 | 24 | class FunctionDefinitionTest 25 | extends AnyFlatSpec 26 | with Matchers 27 | with DecisionTest { 28 | 29 | private lazy val applicantData = parse("/functionDefinition/ApplicantData.dmn") 30 | private lazy val userFunction = parse("/functionDefinition/FeelUserFunction.dmn") 31 | private lazy val requiredDecisionWithFunction = parse( 32 | "/functionDefinition/RequiredDecisionWithFunction.dmn") 33 | private lazy val requiredDecisionWithContext = parse( 34 | "/functionDefinition/RequiredDecisionWithContext.dmn") 35 | private lazy val requiredBkmWithFunction = parse( 36 | "/functionDefinition/RequiredBkmWithFunction.dmn") 37 | private lazy val requiredBkmWithContext = parse( 38 | "/functionDefinition/RequiredBkmWithContext.dmn") 39 | 40 | "A function definition" should "be invoked inside a context" in { 41 | val rate: BigDecimal = 0.25 42 | val term: BigDecimal = 36 43 | val amount: BigDecimal = 100000 44 | val expected = (amount * rate / 12) / (1 - (1 + rate / 12) 45 | .pow(-36)) // ~ 3975.982590125562 46 | 47 | eval(applicantData, "applicantData", Map()) should be(expected) 48 | } 49 | 50 | "A FEEL user function" should "be invoked inside a context" in { 51 | eval(userFunction, "userFunction", Map()) should be(5) 52 | } 53 | 54 | it should "be invoked from required decision with function" in { 55 | eval(requiredDecisionWithFunction, "calculation", Map()) should be(5) 56 | } 57 | 58 | it should "be invoked from required decision with context" in { 59 | eval(requiredDecisionWithContext, "calculation", Map()) should be(5) 60 | } 61 | 62 | // TODO (saig0): support invoking required BKM as functions (#12) 63 | ignore should "be invoked from required BKM with function" in { 64 | eval(requiredBkmWithFunction, "calculation", Map()) should be(5) 65 | } 66 | 67 | // TODO (saig0): support invoking required BKM as functions (#12) 68 | ignore should "be invoked from required BKM with context" in { 69 | eval(requiredBkmWithContext, "calculation", Map()) should be(5) 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/InvocationTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | 22 | class InvocationTest extends AnyFlatSpec with Matchers with DecisionTest { 23 | 24 | lazy val discountDecision = parse("/invocation/discount.dmn") 25 | lazy val withoutParameters = parse("/invocation/withoutParameters.dmn") 26 | lazy val missingParameter = parse("/invocation/missingParameters.dmn") 27 | lazy val missingKnowledgeRequirementDecision = 28 | getClass.getResourceAsStream("/invocation/missingKnowledgeRequirement.dmn") 29 | 30 | "An invocation" should "execute a BKM with parameters" in { 31 | eval(discountDecision, 32 | "discount", 33 | Map("Customer" -> "Business", "OrderSize" -> 7)) should be(0.1) 34 | } 35 | 36 | it should "execute a BKM without parameters" in { 37 | eval(withoutParameters, "applicantData", Map()) should be( 38 | Map("Age" -> 51, 39 | "MaritalStatus" -> "M", 40 | "EmploymentStatus" -> "EMPLOYED", 41 | "ExistingCustomer" -> false)) 42 | } 43 | 44 | it should "fail if parameter is not set" in { 45 | eval(missingParameter, "discount", Map("OrderSize" -> 7)) should be( 46 | Failure("no parameter found with name 'customer'")) 47 | } 48 | 49 | it should "fail if parameter has the wrong type" in { 50 | eval(discountDecision, 51 | "discount", 52 | Map("Customer" -> "Business", "OrderSize" -> "foo")) should be( 53 | Failure("expected 'number' but found '\"foo\"'")) 54 | } 55 | 56 | it should "fail if knowledge requirement is missing" in { 57 | val result = engine.parse(missingKnowledgeRequirementDecision) 58 | 59 | result.isLeft should be(true) 60 | result.left.map( 61 | _.message should be("no BKM found with name 'Discount table'")) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/ListTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | 22 | class ListTest extends AnyFlatSpec with Matchers with DecisionTest { 23 | 24 | lazy val applicantData = parse("/list/ApplicantData.dmn") 25 | 26 | "A list with literal expressions" should "return result as list" in { 27 | eval(applicantData, "applicantData", Map()) should be( 28 | Map("MonthlyOutgoings" -> List(2500, 3000))) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/LiteralExpressionTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | 22 | class LiteralExpressionTest 23 | extends AnyFlatSpec 24 | with Matchers 25 | with DecisionTest { 26 | 27 | lazy val greeting = parse("/literalexpression/greeting.dmn") 28 | lazy val typeMismatch = parse("/literalexpression/type-mismatch.dmn") 29 | 30 | "A literal expression" should "be evaluated as decision" in { 31 | eval(greeting, "greeting", Map("name" -> "John")) should be("Hello John") 32 | } 33 | 34 | it should "fail when result doesn't match type" in { 35 | eval(typeMismatch, "greeting", Map("name" -> "Frank")) should be( 36 | Failure("expected 'number' but found '\"Hello Frank\"'")) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/RelationTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import java.time.LocalDate 20 | 21 | import org.scalatest.flatspec.AnyFlatSpec 22 | import org.scalatest.matchers.should.Matchers 23 | 24 | class RelationTest extends AnyFlatSpec with Matchers with DecisionTest { 25 | 26 | lazy val applicantData = parse("/relation/ApplicantData.dmn") 27 | 28 | "A relation" should "return a list of contexts" in { 29 | eval(applicantData, "applicantData", Map()) should be( 30 | Map("CreditHistory" -> List( 31 | Map( 32 | "recordDate" -> LocalDate.parse("2008-03-12"), 33 | "event" -> "home mortgage", 34 | "weight" -> 100 35 | ), 36 | Map( 37 | "recordDate" -> LocalDate.parse("2011-04-01"), 38 | "event" -> "foreclosure warning", 39 | "weight" -> 150 40 | ) 41 | ))) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/RequiredDecisionTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn 17 | 18 | import org.camunda.dmn.DmnEngine._ 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | 22 | class RequiredDecisionTest extends AnyFlatSpec with Matchers with DecisionTest { 23 | 24 | lazy val discountDecision = parse("/requirements/discount.dmn") 25 | lazy val applicantDataDecision = parse("/requirements/ApplicantData.dmn") 26 | 27 | "A decision" should "evaluate a required decision" in { 28 | eval(discountDecision, 29 | "price", 30 | Map("customer" -> "Business", "orderSize" -> 7)) should be(10) 31 | } 32 | 33 | it should "evaluate multiple required decision" in { 34 | eval(applicantDataDecision, "income", Map()) should be(10000.00) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/spi/DmnEngineSpiTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.spi 17 | 18 | import org.camunda.dmn.DmnEngine 19 | import org.camunda.dmn.parser.ParsedDmn 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | import org.scalatest.matchers.should.Matchers 22 | 23 | class DmnEngineSpiTest extends AnyFlatSpec with Matchers { 24 | 25 | val engine = new DmnEngine 26 | 27 | lazy val decision: ParsedDmn = { 28 | val resource = getClass.getResourceAsStream("/spi/SpiTests.dmn") 29 | engine.parse(resource) match { 30 | case Right(decision) => decision 31 | case Left(failure) => throw new AssertionError(failure) 32 | } 33 | } 34 | 35 | "A custom value mapper" should "transform the input" in { 36 | 37 | val result = engine.eval(decision, "varInput", Map("input" -> "bar")) 38 | 39 | result.isRight should be(true) 40 | result.map(_.value should be("baz")) 41 | } 42 | 43 | it should "transform the output" in { 44 | 45 | val result = engine.eval(decision, "varOutput", Map[String, Any]()) 46 | 47 | result.isRight should be(true) 48 | result.map(_.value should be("baz")) 49 | } 50 | 51 | "A custom function provider" should "provide a function" in { 52 | 53 | val result = engine.eval(decision, "invFunction", Map("x" -> 2)) 54 | 55 | result.isRight should be(true) 56 | result.map(_.value should be(3)) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/spi/MyCustomFunctionProvider.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.spi 17 | 18 | import org.camunda.feel.context.CustomFunctionProvider 19 | import org.camunda.feel.syntaxtree.{ValError, ValFunction, ValNumber} 20 | 21 | class MyCustomFunctionProvider extends CustomFunctionProvider { 22 | 23 | val functions = Map( 24 | "incr" -> 25 | ValFunction( 26 | params = List("x"), 27 | invoke = (args) => 28 | args.head match { 29 | case ValNumber(x) => ValNumber(x + 1) 30 | case x => ValError(s"expected number but found '$x'") 31 | } 32 | ) 33 | ) 34 | 35 | def getFunction(name: String): Option[ValFunction] = functions.get(name) 36 | 37 | override def functionNames(): Iterable[String] = functions.keys 38 | } 39 | -------------------------------------------------------------------------------- /src/test/scala/org/camunda/dmn/spi/MyCustomValueMapper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2022 Camunda Services GmbH (info@camunda.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.camunda.dmn.spi 17 | 18 | import org.camunda.feel.syntaxtree.{Val, ValString} 19 | import org.camunda.feel.valuemapper.CustomValueMapper 20 | 21 | class MyCustomValueMapper extends CustomValueMapper { 22 | 23 | override def toVal(x: Any, innerValueMapper: Any => Val): Option[Val] = { 24 | x match { 25 | case "bar" => Some(ValString("baz")) 26 | case _ => None 27 | } 28 | } 29 | 30 | override def unpackVal(value: Val, 31 | innerValueMapper: Val => Any): Option[Any] = { 32 | value match { 33 | case ValString("foobar") => Some("baz") 34 | case _ => None 35 | } 36 | } 37 | 38 | } 39 | --------------------------------------------------------------------------------