├── .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 | [](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 |
17 |
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 |
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 |
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 |
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 |
21 |
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 |
--------------------------------------------------------------------------------