From 10edbeb2002bbabd8c0042e54aa39dcb1c534449 Mon Sep 17 00:00:00 2001 From: Toni Rikkola Date: Wed, 6 Mar 2024 11:36:55 +0000 Subject: [PATCH 1/4] 5774 : Add YaRD --- bom/kie-yard-bom/.gitignore | 10 + bom/kie-yard-bom/pom.xml | 113 ++++++++++ bom/pom.xml | 1 + build-parent/pom.xml | 18 ++ kie-yard/kie-yard-api/pom.xml | 61 ++++++ .../org/kie/yard/api/model/DecisionLogic.java | 32 +++ .../org/kie/yard/api/model/DecisionTable.java | 70 +++++++ .../java/org/kie/yard/api/model/Element.java | 64 ++++++ .../org/kie/yard/api/model/InlineRule.java | 45 ++++ .../java/org/kie/yard/api/model/Input.java | 44 ++++ .../kie/yard/api/model/LiteralExpression.java | 35 ++++ .../org/kie/yard/api/model/Operators.java | 50 +++++ .../java/org/kie/yard/api/model/Rule.java | 24 +++ .../kie/yard/api/model/RuleDefSerializer.java | 88 ++++++++ .../org/kie/yard/api/model/WhenThenRule.java | 53 +++++ .../api/model/WhenThenRuleThenSerializer.java | 58 ++++++ .../java/org/kie/yard/api/model/YaRD.java | 82 ++++++++ .../src/main/resources/YaRD-schema.json | 191 +++++++++++++++++ kie-yard/kie-yard-core/pom.xml | 67 ++++++ .../org/kie/yard/core/DTableUnitBuilder.java | 196 ++++++++++++++++++ .../main/java/org/kie/yard/core/Firable.java | 26 +++ .../yard/core/LiteralExpressionBuilder.java | 40 ++++ .../core/LiteralExpressionInterpreter.java | 74 +++++++ .../org/kie/yard/core/QuotedExprParsed.java | 115 ++++++++++ .../java/org/kie/yard/core/StoreHandle.java | 70 +++++++ .../yard/core/SyntheticRuleUnitWrapper.java | 40 ++++ .../org/kie/yard/core/YaRDDefinitions.java | 56 +++++ .../java/org/kie/yard/core/YaRDParser.java | 134 ++++++++++++ .../java/org/kie/yard/core/YaRDRunner.java | 59 ++++++ .../yard/core/DomesticPackagePricesTest.java | 60 ++++++ .../org/kie/yard/core/ExtraCostsTest.java | 45 ++++ .../kie/yard/core/InsuranceBasePriceTest.java | 57 +++++ .../test/java/org/kie/yard/core/TestBase.java | 46 ++++ .../resources/domestic-package-prices.yml | 30 +++ .../src/test/resources/extra-costs.yml | 47 +++++ .../test/resources/insurance-base-price.yml | 30 +++ .../src/test/resources/logback-test.xml | 18 ++ .../scorecards/README-health-card.yml | 47 +++++ .../src/test/resources/scorecards/README.md | 8 + .../scorecards/branch-responsibilities.yml | 38 ++++ .../scorecards/drafts/branch-blocked-card.yml | 21 ++ .../scorecards/drafts/closed-prs-card.yml | 23 ++ .../scorecards/drafts/open-prs-card.yml | 21 ++ .../drafts/weekly-branch-health-card.yml | 22 ++ .../scorecards/git-repository-health-card.yml | 56 +++++ .../scorecards/resource-limits-card.yml | 40 ++++ .../src/test/resources/ticket-score-cards.yml | 46 ++++ kie-yard/pom.xml | 59 ++++++ pom.xml | 1 + 49 files changed, 2631 insertions(+) create mode 100644 bom/kie-yard-bom/.gitignore create mode 100644 bom/kie-yard-bom/pom.xml create mode 100644 kie-yard/kie-yard-api/pom.xml create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/DecisionLogic.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/DecisionTable.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Element.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/InlineRule.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Input.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/LiteralExpression.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Operators.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Rule.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/RuleDefSerializer.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/WhenThenRule.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/WhenThenRuleThenSerializer.java create mode 100644 kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/YaRD.java create mode 100644 kie-yard/kie-yard-api/src/main/resources/YaRD-schema.json create mode 100644 kie-yard/kie-yard-core/pom.xml create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/DTableUnitBuilder.java create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/Firable.java create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionBuilder.java create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionInterpreter.java create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/QuotedExprParsed.java create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/StoreHandle.java create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/SyntheticRuleUnitWrapper.java create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDDefinitions.java create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDRunner.java create mode 100644 kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DomesticPackagePricesTest.java create mode 100644 kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/ExtraCostsTest.java create mode 100644 kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java create mode 100644 kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/TestBase.java create mode 100644 kie-yard/kie-yard-core/src/test/resources/domestic-package-prices.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/extra-costs.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/insurance-base-price.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/logback-test.xml create mode 100644 kie-yard/kie-yard-core/src/test/resources/scorecards/README-health-card.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/scorecards/README.md create mode 100644 kie-yard/kie-yard-core/src/test/resources/scorecards/branch-responsibilities.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/branch-blocked-card.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/closed-prs-card.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/open-prs-card.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/weekly-branch-health-card.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/scorecards/git-repository-health-card.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/scorecards/resource-limits-card.yml create mode 100644 kie-yard/kie-yard-core/src/test/resources/ticket-score-cards.yml create mode 100644 kie-yard/pom.xml diff --git a/bom/kie-yard-bom/.gitignore b/bom/kie-yard-bom/.gitignore new file mode 100644 index 00000000000..606d865999a --- /dev/null +++ b/bom/kie-yard-bom/.gitignore @@ -0,0 +1,10 @@ +/target +/local + +# Eclipse, Netbeans and IntelliJ files +/.gitignore +!.gitignore +/nbproject +/*.ipr +/*.iws +/*.iml diff --git a/bom/kie-yard-bom/pom.xml b/bom/kie-yard-bom/pom.xml new file mode 100644 index 00000000000..15c0d4959d9 --- /dev/null +++ b/bom/kie-yard-bom/pom.xml @@ -0,0 +1,113 @@ + + + + 4.0.0 + + org.kie + kie-core-bom + 999-SNAPSHOT + ../kie-core-bom/pom.xml + + + kie-yard-bom + pom + + Kie YaRD BOM (Bill Of Materials) + + Import this BOM in your dependencyManagement if you want to depend on multiple Kie YaRD artifacts. + + + https://www.drools.org + + + scm:git:https://github.com/apache/incubator-kie-drools.git + scm:git:git@github.com:apache/incubator-kie-drools.git + https://github.com/apache/incubator-kie-drools + + + GitHub Issues + https://github.com/apache/incubator-kie-issues/issues + + + + All developers are listed on the team website + http://www.drools.org/community/team.html + + + + + All contributors are listed on the team website + http://www.drools.org/community/team.html + + + + + Development mailing list + dev@kie.apache.org + dev-subscribe@kie.apache.org + dev-unsubscribe@kie.apache.org + https://lists.apache.org/list.html?dev@kie.apache.org + + + setup + https://groups.google.com/forum/#!forum/drools-setup + https://groups.google.com/forum/#!forum/drools-setup + + + usage + https://groups.google.com/forum/#!forum/drools-usage + https://groups.google.com/forum/#!forum/drools-usage + + + + + + + + + + org.drools + drools-bom + pom + ${project.version} + import + + + + + + org.kie + kie-yard-api + ${project.version} + + + org.kie + kie-yard-api + ${project.version} + sources + + + org.kie + kie-yard-core + ${project.version} + + + org.kie + kie-yard-core + ${project.version} + sources + + + + + + diff --git a/bom/pom.xml b/bom/pom.xml index 411a4269a1d..b4ad741e621 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -19,6 +19,7 @@ kie-efesto-bom kie-dmn-bom kie-pmml-bom + kie-yard-bom \ No newline at end of file diff --git a/build-parent/pom.xml b/build-parent/pom.xml index f91ddc9fbea..39575453af6 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -216,6 +216,9 @@ 3.3.0 3.4.0 3.6.2 + + 0.4 + 1.1.0 @@ -1142,6 +1145,21 @@ jboss-logging ${version.org.jboss.logging} + + org.kie.j2cl.tools.yaml.mapper + common + ${version.yaml.mapper} + + + org.kie.j2cl.tools.yaml.mapper + processor + ${version.yaml.mapper} + + + ch.obermuhlner + jshell-scriptengine + ${version.jshell} + diff --git a/kie-yard/kie-yard-api/pom.xml b/kie-yard/kie-yard-api/pom.xml new file mode 100644 index 00000000000..15911e2572c --- /dev/null +++ b/kie-yard/kie-yard-api/pom.xml @@ -0,0 +1,61 @@ + + + + 4.0.0 + + org.kie + kie-yard + 999-SNAPSHOT + + + kie-yard-api + + + org.kie.yard.api + + + + + + org.drools + drools-bom + ${project.version} + pom + import + + + + + + + org.kie.j2cl.tools.yaml.mapper + common + + + + org.kie.j2cl.tools.yaml.mapper + processor + provided + + + \ No newline at end of file diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/DecisionLogic.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/DecisionLogic.java new file mode 100644 index 00000000000..8f1c8ffc164 --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/DecisionLogic.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import org.kie.j2cl.tools.yaml.mapper.api.annotation.YamlSubtype; +import org.kie.j2cl.tools.yaml.mapper.api.annotation.YamlTypeInfo; + +@YamlTypeInfo( + key = "type", + value = { + @YamlSubtype(alias = "DecisionTable", type = DecisionTable.class), + @YamlSubtype(alias = "LiteralExpression", type = LiteralExpression.class) + }) +public interface DecisionLogic { + +} diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/DecisionTable.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/DecisionTable.java new file mode 100644 index 00000000000..57678c66bdf --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/DecisionTable.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import java.util.List; + +import org.kie.j2cl.tools.yaml.mapper.api.annotation.YAMLMapper; +import org.kie.j2cl.tools.yaml.mapper.api.annotation.YamlTypeDeserializer; +import org.kie.j2cl.tools.yaml.mapper.api.annotation.YamlTypeSerializer; + +@YAMLMapper +public class DecisionTable implements DecisionLogic { + + private List inputs; + private String hitPolicy = "ANY"; + @Deprecated + private List outputComponents; + @YamlTypeSerializer(RuleDefSerializer.class) + @YamlTypeDeserializer(RuleDefSerializer.class) + private List rules; + + public void setInputs(List inputs) { + this.inputs = inputs; + } + + public void setOutputComponents(List outputComponents) { + this.outputComponents = outputComponents; + } + + public List getInputs() { + return inputs; + } + + @Deprecated + public List getOutputComponents() { + return outputComponents; + } + + public String getHitPolicy() { + return hitPolicy; + } + + public void setHitPolicy(String hitPolicy) { + this.hitPolicy = hitPolicy; + } + + public List getRules() { + return rules; + } + + public void setRules(List rules) { + this.rules = rules; + } +} diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Element.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Element.java new file mode 100644 index 00000000000..77fe1c3208e --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Element.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import java.util.List; + +import org.kie.j2cl.tools.yaml.mapper.api.annotation.YAMLMapper; + +@YAMLMapper +public class Element { + + private String name; + private String type; + private List requirements; + private DecisionLogic logic; + + public void setName(String name) { + this.name = name; + } + + public void setType(String type) { + this.type = type; + } + + public void setRequirements(List requirements) { + this.requirements = requirements; + } + + public void setLogic(DecisionLogic logic) { + this.logic = logic; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public List getRequirements() { + return requirements; + } + + public DecisionLogic getLogic() { + return logic; + } +} diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/InlineRule.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/InlineRule.java new file mode 100644 index 00000000000..79c921e15ee --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/InlineRule.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import java.util.List; + +public class InlineRule implements Rule { + + private final int rowNumber; + public List def; + + public InlineRule(int rowNumber, List data) { + this.rowNumber = rowNumber; + this.def = data; + } + + @Override + public int getRowNumber() { + return rowNumber; + } + + public List getDef() { + return def; + } + + public void setDef(List def) { + this.def = def; + } +} diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Input.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Input.java new file mode 100644 index 00000000000..a025c298695 --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Input.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import org.kie.j2cl.tools.yaml.mapper.api.annotation.YAMLMapper; + +@YAMLMapper +public class Input { + + private String name; + private String type; + + public void setName(String name) { + this.name = name; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } +} \ No newline at end of file diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/LiteralExpression.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/LiteralExpression.java new file mode 100644 index 00000000000..18e884baef8 --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/LiteralExpression.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import org.kie.j2cl.tools.yaml.mapper.api.annotation.YAMLMapper; + +@YAMLMapper +public class LiteralExpression implements DecisionLogic { + + private String expression; + + public void setExpression(String expression) { + this.expression = expression; + } + + public String getExpression() { + return expression; + } +} \ No newline at end of file diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Operators.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Operators.java new file mode 100644 index 00000000000..bae70914259 --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Operators.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import java.util.Objects; + +/** + * Interface instead of enum due to possible custom operators. + */ +public interface Operators { + + String NOT_EQUALS = "!="; + String EQUALS = "="; + String GREATER_OR_EQUAL = ">="; + String GREATER_THAN = ">"; + String LESS_OR_EQUAL = "<="; + String LESS_THAN = "<"; + + String[] ALL = {EQUALS, LESS_OR_EQUAL, LESS_THAN, GREATER_OR_EQUAL, GREATER_THAN, NOT_EQUALS}; + + static int compare(final String operator, + final String other) { + return getWeight(operator) - getWeight(other); + } + + static int getWeight(final String operator) { + for (int i = 0; i < ALL.length; i++) { + if (Objects.equals(operator, ALL[i])) { + return i; + } + } + return 0; + } +} diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Rule.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Rule.java new file mode 100644 index 00000000000..874f2650b03 --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/Rule.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +public interface Rule { + + int getRowNumber(); +} diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/RuleDefSerializer.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/RuleDefSerializer.java new file mode 100644 index 00000000000..cedd50668e1 --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/RuleDefSerializer.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import java.util.ArrayList; +import java.util.List; + +import org.kie.j2cl.tools.yaml.mapper.api.YAMLDeserializer; +import org.kie.j2cl.tools.yaml.mapper.api.YAMLSerializer; +import org.kie.j2cl.tools.yaml.mapper.api.exception.YAMLDeserializationException; +import org.kie.j2cl.tools.yaml.mapper.api.internal.deser.YAMLDeserializationContext; +import org.kie.j2cl.tools.yaml.mapper.api.internal.ser.YAMLSerializationContext; +import org.kie.j2cl.tools.yaml.mapper.api.node.YamlMapping; +import org.kie.j2cl.tools.yaml.mapper.api.node.YamlNode; +import org.kie.j2cl.tools.yaml.mapper.api.node.YamlSequence; + +public class RuleDefSerializer + implements YAMLSerializer, + YAMLDeserializer { + + private int rowNumber = 1; + + @Override + public Object deserialize(YamlMapping yamlMapping, + String s, + YAMLDeserializationContext yamlDeserializationContext) throws YAMLDeserializationException { + return deserialize(yamlMapping.getNode(s), yamlDeserializationContext); + } + + @Override + public Object deserialize(YamlNode yamlNode, + YAMLDeserializationContext yamlDeserializationContext) { + if (yamlNode instanceof YamlSequence) { + final List items = getItems(yamlNode); + return new InlineRule(rowNumber++, items); + } else if (yamlNode instanceof YamlMapping) { + final WhenThenRule whenThenRule = new WhenThenRule(rowNumber++); + final YamlNode when = ((YamlMapping) yamlNode).getNode("when"); + final YamlNode then = ((YamlMapping) yamlNode).getNode("then"); + whenThenRule.setWhen(getItems(when)); + whenThenRule.setThen(then.asScalar().value()); + return whenThenRule; + } + return new IllegalArgumentException("Unknown rule format."); + } + + private List getItems(final YamlNode yamlNode) { + final List result = new ArrayList<>(); + if (yamlNode instanceof YamlSequence) { + ((YamlSequence) yamlNode).iterator().forEachRemaining(x -> { + final Comparable value = (Comparable) x.asScalar().value(); + result.add(value); + }); + } + return result; + } + + @Override + public void serialize(YamlMapping yamlMapping, + String s, + Object objects, + YAMLSerializationContext yamlSerializationContext) { + // Not needed, we never serialize. + } + + @Override + public void serialize(YamlSequence yamlSequence, + Object objects, + YAMLSerializationContext yamlSerializationContext) { + // Not needed, we never serialize. + } +} diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/WhenThenRule.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/WhenThenRule.java new file mode 100644 index 00000000000..b552f714585 --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/WhenThenRule.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import java.util.List; + +public class WhenThenRule implements Rule { + + private final int rowNumber; + private List when; + private Object then; + + public WhenThenRule(int rowNumber) { + this.rowNumber = rowNumber; + } + + @Override + public int getRowNumber() { + return rowNumber; + } + + public List getWhen() { + return when; + } + + public Object getThen() { + return then; + } + + public void setWhen(List when) { + this.when = when; + } + + public void setThen(Object then) { + this.then = then; + } +} diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/WhenThenRuleThenSerializer.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/WhenThenRuleThenSerializer.java new file mode 100644 index 00000000000..63d2dcd5f5b --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/WhenThenRuleThenSerializer.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import java.util.Locale; + +import org.kie.j2cl.tools.yaml.mapper.api.YAMLDeserializer; +import org.kie.j2cl.tools.yaml.mapper.api.YAMLSerializer; +import org.kie.j2cl.tools.yaml.mapper.api.exception.YAMLDeserializationException; +import org.kie.j2cl.tools.yaml.mapper.api.internal.deser.YAMLDeserializationContext; +import org.kie.j2cl.tools.yaml.mapper.api.internal.ser.YAMLSerializationContext; +import org.kie.j2cl.tools.yaml.mapper.api.node.YamlMapping; +import org.kie.j2cl.tools.yaml.mapper.api.node.YamlNode; +import org.kie.j2cl.tools.yaml.mapper.api.node.YamlSequence; + +public class WhenThenRuleThenSerializer + implements YAMLSerializer, + YAMLDeserializer { + + @Override + public Object deserialize(YamlMapping yamlMapping, String key, YAMLDeserializationContext yamlDeserializationContext) throws YAMLDeserializationException { + return deserialize(yamlMapping.getNode(key), yamlDeserializationContext); + } + + @Override + public Object deserialize(YamlNode yamlNode, YAMLDeserializationContext yamlDeserializationContext) { + if (yamlNode == null || yamlNode.isEmpty()) { + return null; + } + return yamlNode.asScalar().value().toLowerCase(Locale.ROOT); + } + + @Override + public void serialize(YamlMapping yamlMapping, String s, Object o, YAMLSerializationContext yamlSerializationContext) { + // Not needed, we never serialize. + } + + @Override + public void serialize(YamlSequence yamlSequence, Object o, YAMLSerializationContext yamlSerializationContext) { + // Not needed, we never serialize. + } +} diff --git a/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/YaRD.java b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/YaRD.java new file mode 100644 index 00000000000..a0a40ba7d9c --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/java/org/kie/yard/api/model/YaRD.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.api.model; + +import java.util.List; + +import org.kie.j2cl.tools.yaml.mapper.api.annotation.YAMLMapper; + +@YAMLMapper +public class YaRD { + + private String specVersion = "alpha"; + private String kind = "YaRD"; + private String name; + private String expressionLang; + private List inputs; + private List elements; + + public void setInputs(List inputs) { + this.inputs = inputs; + } + + public void setElements(List elements) { + this.elements = elements; + } + + public String getName() { + return name; + } + + public String getExpressionLang() { + return expressionLang; + } + + public void setExpressionLang(String expressionLang) { + this.expressionLang = expressionLang; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public String getSpecVersion() { + return specVersion; + } + + public void setSpecVersion(String specVersion) { + this.specVersion = specVersion; + } + + public void setName(String name) { + this.name = name; + } + + public List getInputs() { + return inputs; + } + + public List getElements() { + return elements; + } +} diff --git a/kie-yard/kie-yard-api/src/main/resources/YaRD-schema.json b/kie-yard/kie-yard-api/src/main/resources/YaRD-schema.json new file mode 100644 index 00000000000..95d19677960 --- /dev/null +++ b/kie-yard/kie-yard-api/src/main/resources/YaRD-schema.json @@ -0,0 +1,191 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "DecisionTable-1": { + "type": "object", + "properties": { + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "hitPolicy": { + "type": "string", + "default": "ANY" + }, + "rules": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/InlineRule" + }, + { + "$ref": "#/definitions/WhenThenRule" + } + ] + } + }, + "outputComponents": { + "description": "deprecated", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "inputs", + "rules" + ] + }, + "DecisionTable-2": { + "allOf": [ + { + "$ref": "#/definitions/DecisionTable-1" + }, + { + "type": "object", + "properties": { + "type": { + "const": "DecisionTable" + } + }, + "required": [ + "type" + ] + } + ] + }, + "Element": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "requirements": { + "type": "array", + "items": { + "type": "string" + } + }, + "logic": { + "anyOf": [ + { + "$ref": "#/definitions/DecisionTable-2" + }, + { + "$ref": "#/definitions/LiteralExpression-2" + } + ] + } + }, + "required": [ + "name", + "type", + "logic" + ] + }, + "InlineRule": { + "type": "array", + "items": {} + }, + "Input": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "name", + "type" + ] + }, + "LiteralExpression-1": { + "type": "object", + "properties": { + "expression": { + "type": "string" + } + }, + "required": [ + "expression" + ] + }, + "LiteralExpression-2": { + "allOf": [ + { + "$ref": "#/definitions/LiteralExpression-1" + }, + { + "type": "object", + "properties": { + "type": { + "const": "LiteralExpression" + } + }, + "required": [ + "type" + ] + } + ] + }, + "WhenThenRule": { + "type": "object", + "properties": { + "when": { + "type": "array", + "items": {} + }, + "then": {} + }, + "required": [ + "when", + "then" + ] + } + }, + "type": "object", + "properties": { + "specVersion": { + "type": "string", + "default": "alpha" + }, + "kind": { + "type": "string", + "default": "YaRD" + }, + "name": { + "type": "string", + "description": "when not provided explicitly, implementation will attempt to deduce the name from the runtime context; if a name cannot be deduced it is an error." + }, + "expressionLang": { + "type": "string", + "description": "An implementation is free to assume a default expressionLang if not explicitly set. For the purpose of a User sharing a YaRD definition, is best to valorise this field explicit." + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/Input" + } + }, + "elements": { + "type": "array", + "items": { + "$ref": "#/definitions/Element" + } + } + }, + "required": [ + "inputs", + "elements" + ] +} \ No newline at end of file diff --git a/kie-yard/kie-yard-core/pom.xml b/kie-yard/kie-yard-core/pom.xml new file mode 100644 index 00000000000..5eeaa6596c7 --- /dev/null +++ b/kie-yard/kie-yard-core/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.kie + kie-yard + 999-SNAPSHOT + + + kie-yard-core + + + org.kie.yard.core + + + + + + org.drools + drools-bom + ${project.version} + pom + import + + + + + + + org.kie + kie-yard-api + + + org.drools + drools-ruleunits-dsl + + + org.kie.j2cl.tools.yaml.mapper + common + + + + ch.obermuhlner + jshell-scriptengine + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + ch.qos.logback + logback-classic + test + + + \ No newline at end of file diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/DTableUnitBuilder.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/DTableUnitBuilder.java new file mode 100644 index 00000000000..3bb1ba8628a --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/DTableUnitBuilder.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.drools.model.Index; +import org.drools.ruleunits.api.SingletonStore; +import org.drools.ruleunits.dsl.SyntheticRuleUnit; +import org.drools.ruleunits.dsl.SyntheticRuleUnitBuilder; +import org.kie.yard.api.model.InlineRule; +import org.kie.yard.api.model.Rule; +import org.kie.yard.api.model.WhenThenRule; + +public class DTableUnitBuilder { + + private final YaRDDefinitions definitions; + private final String name; + private final OnExecute executionAction; + private final List rules; + private final List inputs; + private final JsonMapper jsonMapper = JsonMapper.builder().build(); + + public DTableUnitBuilder( + final YaRDDefinitions definitions, + final String name, + final org.kie.yard.api.model.DecisionTable dtableDefinition) { + this.inputs = dtableDefinition.getInputs(); + if (inputs.isEmpty()) { + throw new IllegalStateException("Empty decision table?"); + } + this.definitions = definitions; + this.name = name; + this.rules = dtableDefinition.getRules(); + this.executionAction = getExecutionAction(dtableDefinition.getHitPolicy()); + } + + public SyntheticRuleUnit build() { + + final SyntheticRuleUnitBuilder unit = SyntheticRuleUnitBuilder.build(name); + for (Map.Entry> e : definitions.inputs().entrySet()) { + unit.registerDataSource(e.getKey(), e.getValue(), Object.class); + } + final StoreHandle result = StoreHandle.empty(Object.class); + unit.registerGlobal(name, result); + definitions.outputs().put(name, result); + return unit.defineRules(rulesFactory -> { + for (Rule ruleDefinition : rules) { + var rule = rulesFactory.rule(); + for (int idx = 0; idx < inputs.size(); idx++) { + final RuleCell ruleCell = parseGenericRuleCell(ruleDefinition, idx); + if (ruleCell.value != null) { + final SingletonStore dataSource = definitions.inputs().get(inputs.get(idx)); + + rule.on(dataSource).filter(ruleCell.idxtype, ruleCell.value); + } + } + rule.execute(result, storeHandle -> executionAction.onExecute(ruleDefinition, storeHandle)); + } + }); + } + + private OnExecute getExecutionAction(String hitPolicy) { + if (hitPolicy == null || Objects.equals("ANY", hitPolicy)) { + return (ruleDefinition, storeHandle) -> { + final RuleCell rc = parseGenericRuleThen(ruleDefinition); + storeHandle.set(rc.value); + }; + } else if (Objects.equals("FIRST", hitPolicy)) { + return (ruleDefinition, storeHandle) -> { + if (!storeHandle.isValuePresent()) { + final RuleCell rc = parseGenericRuleThen(ruleDefinition); + storeHandle.set(rc.value); + } + }; + } else if (Objects.equals("COLLECT", hitPolicy)) { + return (ruleDefinition, storeHandle) -> { + if (!storeHandle.isValuePresent()) { + storeHandle.set(new ArrayList<>()); + } + final RuleCell rc = parseGenericRuleThen(ruleDefinition); + + if (storeHandle.get() instanceof List list) { + list.add(resolveValue(rc)); + } + }; + } else { + throw new UnsupportedOperationException("Not implemented "); + } + } + + private Object resolveValue(final RuleCell rc) { + try { + if (rc.value instanceof String text) { + return jsonMapper.readValue(text, Map.class); + } + } catch (JsonProcessingException ignored) { + } + return rc.value; + } + + private RuleCell parseGenericRuleThen(Rule rule) { + if (rule instanceof InlineRule inlineRule) { + return parseRuleCell(inlineRule.getDef().get(inlineRule.getDef().size() - 1)); + } else if (rule instanceof WhenThenRule whenThenRule) { + return parseRuleCell((whenThenRule).getThen()); + } else { + throw new IllegalStateException("Unknown or unmanaged rule instance type?"); + } + } + + private RuleCell parseGenericRuleCell(Rule rule, int i) { + if (rule instanceof InlineRule inlineRule) { + return parseRuleCell((inlineRule).getDef().get(i)); + } else if (rule instanceof WhenThenRule whenThenRule) { + return parseRuleCell((whenThenRule).getWhen().get(i)); + } else { + throw new IllegalStateException("Unknown or unmanaged rule instance type?"); + } + } + + private RuleCell parseRuleCell(Object object) { + if (object instanceof Boolean) { + return new RuleCell(Index.ConstraintType.EQUAL, object); + } else if (object instanceof Number) { + return new RuleCell(Index.ConstraintType.EQUAL, object); + } else if (object instanceof String valueString) { + if (valueString.startsWith("<=")) { // pay attention to ordering when not using a parser like in this case. + return new RuleCell(Index.ConstraintType.LESS_OR_EQUAL, parseConstrainedCellString(valueString.substring(2))); + } else if (valueString.startsWith(">=")) { + return new RuleCell(Index.ConstraintType.GREATER_OR_EQUAL, parseConstrainedCellString(valueString.substring(2))); + } else if (valueString.startsWith("<")) { + return new RuleCell(Index.ConstraintType.LESS_THAN, parseConstrainedCellString(valueString.substring(1))); + } else if (valueString.startsWith(">")) { + return new RuleCell(Index.ConstraintType.GREATER_THAN, parseConstrainedCellString(valueString.substring(1))); + } else { + return new RuleCell(Index.ConstraintType.EQUAL, parseConstrainedCellString(valueString)); + } + } else { + throw new IllegalStateException("Unmanaged case, please report!"); + } + } + + private Object parseConstrainedCellString(String substring) { + if (Objects.equals("true", substring.trim().toLowerCase())) { + return true; + } + if (Objects.equals("false", substring.trim().toLowerCase())) { + return false; + } + try { + return Integer.parseInt(substring.trim()); + } catch (Exception e) { + + } + try { + return Long.parseLong(substring.trim()); + } catch (Exception e) { + + } + if (Objects.equals("-", substring.trim())) { + return null; + } + return substring; + } + + private interface OnExecute { + + void onExecute(Rule ruleDefinition, StoreHandle storeHandle); + } + + public static record RuleCell(Index.ConstraintType idxtype, Object value) { + + } +} diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/Firable.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/Firable.java new file mode 100644 index 00000000000..7a3548b9f18 --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/Firable.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.util.Map; + +public interface Firable { + + int fire(Map context, YaRDDefinitions units); +} \ No newline at end of file diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionBuilder.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionBuilder.java new file mode 100644 index 00000000000..6ac46f4e38b --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionBuilder.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import org.kie.yard.api.model.LiteralExpression; + +public class LiteralExpressionBuilder { + + private final YaRDDefinitions definitions; + private final String name; + private final LiteralExpression decisionLogic; + + public LiteralExpressionBuilder(YaRDDefinitions definitions, String name, LiteralExpression decisionLogic) { + this.definitions = definitions; + this.name = name; + this.decisionLogic = decisionLogic; + } + + public Firable build() { + String expr = decisionLogic.getExpression(); + definitions.outputs().put(name, StoreHandle.empty(Object.class)); + return new LiteralExpressionInterpreter(name, QuotedExprParsed.from(expr)); + } +} diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionInterpreter.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionInterpreter.java new file mode 100644 index 00000000000..31d65ece5b6 --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionInterpreter.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.util.Map; +import java.util.Map.Entry; + +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +public class LiteralExpressionInterpreter implements Firable { + + private final String name; + private final QuotedExprParsed quoted; + private final ScriptEngine engine; + private final CompiledScript compiledScript; + + public LiteralExpressionInterpreter(String nameString, QuotedExprParsed quotedExprParsed) { + this.name = nameString; + this.quoted = quotedExprParsed; + try { + ScriptEngineManager manager = new ScriptEngineManager(); + engine = manager.getEngineByName("jshell"); + Compilable compiler = (Compilable) engine; + compiledScript = compiler.compile(quoted.getRewrittenExpression()); + } catch (Exception e) { + throw new IllegalArgumentException("parse error", e); + } + } + + @Override + public int fire(Map context, YaRDDefinitions units) { + Bindings bindings = engine.createBindings(); + // deliberately escape all symbols; a normal symbol will + // never be in the detected-by-unquoting set, so this + // set can't be used to selectively put in scope + for (Entry inKV : context.entrySet()) { + bindings.put(QuotedExprParsed.escapeIdentifier(inKV.getKey()), inKV.getValue()); + } + for (Entry> outKV : units.outputs().entrySet()) { + if (!outKV.getValue().isValuePresent()) { + continue; + } + bindings.put(QuotedExprParsed.escapeIdentifier(outKV.getKey()), outKV.getValue().get()); + } + try { + var result = compiledScript.eval(bindings); + units.outputs().get(name).set(result); + return 1; + } catch (ScriptException e) { + throw new RuntimeException("interpretation failed at runtime", e); + } + } +} diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/QuotedExprParsed.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/QuotedExprParsed.java new file mode 100644 index 00000000000..451a114c72e --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/QuotedExprParsed.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.PrimitiveIterator.OfInt; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.lang.model.SourceVersion; + +public class QuotedExprParsed { + + private static final int ESCAPE_CHAR = "`".codePointAt(0); + + private final Set usedSymbols; + private final String rewrittenExpression; + + private QuotedExprParsed(List usedSymbols, String rewrittenExpression) { + this.usedSymbols = usedSymbols.stream().collect(Collectors.toUnmodifiableSet()); + this.rewrittenExpression = rewrittenExpression; + } + + public String getRewrittenExpression() { + return rewrittenExpression; + } + + public Collection getUsedSymbols() { + return usedSymbols; + } + + public static QuotedExprParsed from(String expr) { + StringBuilder rewittenExpr = new StringBuilder(); + StringBuilder quotedBuffer = new StringBuilder(); + List usedSymbols = new ArrayList<>(); + OfInt it = expr.codePoints().iterator(); + State state = State.UNQUOTED; + while (it.hasNext()) { + int c = it.nextInt(); + if (c == ESCAPE_CHAR) { + switch (state) { + case UNQUOTED: + state = State.QUOTED; + break; + case QUOTED: + state = State.UNQUOTED; + var originalSymbol = quotedBuffer.toString(); + usedSymbols.add(originalSymbol); + var escaped = escapeIdentifier(originalSymbol); + rewittenExpr.append(escaped); + quotedBuffer = new StringBuilder(); + break; + default: + throw new IllegalStateException(); + } + } else { + switch (state) { + case UNQUOTED: + rewittenExpr.appendCodePoint(c); + break; + case QUOTED: + quotedBuffer.appendCodePoint(c); + break; + default: + throw new IllegalStateException(); + } + } + } + return new QuotedExprParsed(usedSymbols, rewittenExpr.toString()); + } + + private static enum State { + UNQUOTED, + QUOTED + } + + public static String escapeIdentifier(String partOfIdentifier) { + String id = partOfIdentifier; + if (!Character.isJavaIdentifierStart(id.charAt(0))) { + id = "_" + id; + } + id = id.replaceAll("_", "__"); + if (SourceVersion.isKeyword(id)) { + id = "_" + id; + } + StringBuilder result = new StringBuilder(); + char[] cs = id.toCharArray(); + for (char c : cs) { + if (Character.isJavaIdentifierPart(c)) { + result.append(c); + } else { + result.append("_" + Integer.valueOf(c)); + } + } + return result.toString(); + } +} diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/StoreHandle.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/StoreHandle.java new file mode 100644 index 00000000000..9e6aa85f45b --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/StoreHandle.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import org.drools.ruleunits.api.DataHandle; +import org.drools.ruleunits.api.DataSource; +import org.drools.ruleunits.api.SingletonStore; + +public class StoreHandle { + + private SingletonStore wrapped; + private DataHandle datahandle; + + private StoreHandle(T value) { + wrapped = DataSource.createSingleton(); + datahandle = wrapped.set(value); + } + + private StoreHandle() { + wrapped = DataSource.createSingleton(); + datahandle = null; + } + + public static StoreHandle of(T value) { + return new StoreHandle<>(value); + } + + public static StoreHandle empty(Class type) { + return new StoreHandle<>(); + } + + public DataHandle set(T value) { + datahandle = wrapped.set(value); + return datahandle; + } + + public void clear() { + datahandle = null; + wrapped.clear(); + } + + public boolean isValuePresent() { + return !(datahandle == null); + } + + public T get() { + if (datahandle == null) { + throw new IllegalStateException("was never set"); + } + @SuppressWarnings("unchecked") + T result = (T) datahandle.getObject(); + return result; + } +} diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/SyntheticRuleUnitWrapper.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/SyntheticRuleUnitWrapper.java new file mode 100644 index 00000000000..46e5ace943a --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/SyntheticRuleUnitWrapper.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.util.Map; + +import org.drools.ruleunits.api.RuleUnitInstance; +import org.drools.ruleunits.api.RuleUnitProvider; +import org.drools.ruleunits.dsl.SyntheticRuleUnit; + +public class SyntheticRuleUnitWrapper implements Firable { + + private final SyntheticRuleUnit wrapped; + + public SyntheticRuleUnitWrapper(SyntheticRuleUnit wrapped) { + this.wrapped = wrapped; + } + + @Override + public int fire(Map context, YaRDDefinitions units) { + RuleUnitInstance unitInstance = RuleUnitProvider.get().createRuleUnitInstance(wrapped); + return unitInstance.fire(); + } +} diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDDefinitions.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDDefinitions.java new file mode 100644 index 00000000000..730953b041f --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDDefinitions.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.drools.ruleunits.api.SingletonStore; + +public record YaRDDefinitions( + Map> inputs, + List units, + Map> outputs) { + + public Map evaluate(Map context) { + Map results = new LinkedHashMap<>(context); + for (String inputKey : inputs.keySet()) { + if (!context.containsKey(inputKey)) { + throw new IllegalArgumentException("Missing input key in context: " + inputKey); + } + Object inputValue = context.get(inputKey); + inputs.get(inputKey).set(inputValue); + } + for (Firable unit : units) { + unit.fire(context, this); + } + for (Entry> outputSets : outputs.entrySet()) { + results.put(outputSets.getKey(), outputSets.getValue().get()); + } + reset(); + return results; + } + + private void reset() { + inputs.forEach((k, v) -> v.clear()); + outputs.forEach((k, v) -> v.clear()); + } +} diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java new file mode 100644 index 00000000000..9ebbc9752dc --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; + +import org.drools.ruleunits.api.DataSource; +import org.kie.yard.api.model.DecisionLogic; +import org.kie.yard.api.model.Element; +import org.kie.yard.api.model.Input; +import org.kie.yard.api.model.YaRD; +import org.kie.yard.api.model.YaRD_YamlMapperImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class YaRDParser { + + private static final Logger LOG = LoggerFactory.getLogger(YaRDParser.class); + private final YaRDDefinitions definitions = new YaRDDefinitions(new HashMap<>(), new ArrayList<>(), new HashMap<>()); + private final YaRD model; + private final String yaml; + + public YaRDParser(Reader reader) throws Exception { + yaml = read(reader); + model = getModel(yaml); + parse(yaml); + } + + public YaRDParser(String yaml) throws IOException { + this.yaml = yaml; + model = getModel(yaml); + parse(yaml); + } + + public YaRD getModel() { + return model; + } + + public String getYaml() { + return yaml; + } + + public YaRDDefinitions getDefinitions() { + return definitions; + } + + private String read(Reader reader) throws Exception { + final StringBuilder fileData = new StringBuilder(1000); + char[] buf = new char[1024]; + int numRead; + while ((numRead = reader.read(buf)) != -1) { + String readData = String.valueOf(buf, + 0, + numRead); + fileData.append(readData); + buf = new char[1024]; + } + reader.close(); + return fileData.toString(); + } + + private YaRD getModel(String yaml) throws IOException { + return new YaRD_YamlMapperImpl().read(yaml); + } + + private YaRDDefinitions parse(String yaml) throws IOException { + final YaRD sd = new YaRD_YamlMapperImpl().read(yaml); + if (!Objects.equals(sd.getExpressionLang(), "jshell")) { + throw new IllegalArgumentException("Only `jshell` is supported as an expression language"); + } + appendInputs(sd.getInputs()); + appendUnits(sd.getElements()); + return definitions; + } + + private void appendUnits(List list) { + for (Element hi : list) { + String nameString = hi.getName(); + LOG.debug("parsing {}", nameString); + Firable decisionLogic = createDecisionLogic(nameString, hi.getLogic()); + definitions.units().add(decisionLogic); + } + } + + private Firable createDecisionLogic(String nameString, DecisionLogic decisionLogic) { + if (decisionLogic instanceof org.kie.yard.api.model.DecisionTable decisionTable) { + return new SyntheticRuleUnitWrapper(new DTableUnitBuilder(definitions, nameString, decisionTable).build()); + } else if (decisionLogic instanceof org.kie.yard.api.model.LiteralExpression literalExpression) { + return new LiteralExpressionBuilder(definitions, nameString, literalExpression).build(); + } else { + throw new UnsupportedOperationException("Not implemented."); + } + } + + private void appendInputs(List list) { + for (Input hi : list) { + String nameString = hi.getName(); + @SuppressWarnings("unused") + Class typeRef = processType(hi.getType()); + definitions.inputs().put(nameString, DataSource.createSingleton()); + } + } + + private Class processType(String string) { + switch (string) { + case "string": + case "number": + case "boolean": + default: + return Object.class; + } + } +} diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDRunner.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDRunner.java new file mode 100644 index 00000000000..24b4bca1e31 --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDRunner.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.io.IOException; +import java.util.Map; + +import com.fasterxml.jackson.databind.json.JsonMapper; + +public class YaRDRunner { + + private final YaRDDefinitions units; + private final JsonMapper jsonMapper = JsonMapper.builder().build(); + private final String name; + + public YaRDRunner(final String yaml) throws IOException { + final YaRDParser parser = new YaRDParser(yaml); + name = parser.getModel().getName(); + units = parser.getDefinitions(); + } + + public String getName() { + return name; + } + + public Map evaluate(final Map map) { + return units.evaluate(map); + } + + public String evaluate(String jsonInputCxt) throws Exception { + final Map inputContext = readJSON(jsonInputCxt); + return jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(evaluate(inputContext)); + } + + @SuppressWarnings("unchecked") + private Map readJSON(final String json) { + try { + return jsonMapper.readValue(json, Map.class); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to read JSON " + json, e); + } + } +} diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DomesticPackagePricesTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DomesticPackagePricesTest.java new file mode 100644 index 00000000000..ba94c9449c9 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DomesticPackagePricesTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.util.Map; + +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DomesticPackagePricesTest + extends TestBase { + + private static final String FILE_NAME = "/domestic-package-prices.yml"; + + @Test + public void testMPackage() throws Exception { + final String CTX = """ + { + "Height":10, + "Width":10, + "Length": 10, + "Weight":10 + } + """; + Map outputJSONasMap = evaluate(CTX, FILE_NAME); + assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Package", "{ \"Size\": \"M\", \"Cost\": 6.90 }"); + } + + @Test + public void testLPackage() throws Exception { + final String CTX = """ + { + "Height":12, + "Width":10, + "Length": 10, + "Weight":10 + } + """; + Map outputJSONasMap = evaluate(CTX, FILE_NAME); + assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Package", "{ \"Size\": \"L\", \"Cost\": 8.90 }"); + } +} diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/ExtraCostsTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/ExtraCostsTest.java new file mode 100644 index 00000000000..82b32982549 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/ExtraCostsTest.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ExtraCostsTest + extends TestBase { + + private static final String FILE_NAME = "/extra-costs.yml"; + + @Test + public void testMPackage() throws Exception { + final String CTX = """ + { + "Fragile":true, + "Package Tracking":true, + "Insurance":true, + "Package Type":"M" + } + """; + Map outputJSONasMap = evaluate(CTX, FILE_NAME); + assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Total cost of premiums", 40); + } +} diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java new file mode 100644 index 00000000000..ed0195e239d --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InsuranceBasePriceTest + extends TestBase { + + private static final String FILE_NAME = "/insurance-base-price.yml"; + + @Test + public void testScenario1() throws Exception { + final String CTX = """ + { + "Age": 47, + "Previous incidents?": false + } + """; + Map outputJSONasMap = evaluate(CTX, FILE_NAME); + assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Base price", 500); + assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Downpayment", 50.0); + } + + @Test + public void testScenario2() throws Exception { + final String CTX = """ + { + "Age": 19, + "Previous incidents?": true + } + """; + Map outputJSONasMap = evaluate(CTX, FILE_NAME); + assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Base price", 1000); + assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Downpayment", 70.0); + } +} diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/TestBase.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/TestBase.java new file mode 100644 index 00000000000..e18ac182ba6 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/TestBase.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import java.io.IOException; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.drools.util.IoUtils; + +public class TestBase { + + private JsonMapper jsonMapper = JsonMapper.builder().build(); + + protected Map evaluate(String jsonInputCxt, String file) throws Exception { + final String yamlDecision = read(file); + final String OUTPUT_JSON = new YaRDRunner(yamlDecision).evaluate(jsonInputCxt); + return readJSON(OUTPUT_JSON); + } + + private String read(String file) throws IOException { + return new String(IoUtils.readBytesFromInputStream(this.getClass().getResourceAsStream(file), true)); + } + + @SuppressWarnings("unchecked") + private Map readJSON(final String CONTEXT) throws JsonProcessingException { + return jsonMapper.readValue(CONTEXT, Map.class); + } +} diff --git a/kie-yard/kie-yard-core/src/test/resources/domestic-package-prices.yml b/kie-yard/kie-yard-core/src/test/resources/domestic-package-prices.yml new file mode 100644 index 00000000000..8b921b676cd --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/domestic-package-prices.yml @@ -0,0 +1,30 @@ +specVersion: alpha +kind: YaRD +name: "Traffic Violation" +expressionLang: jshell +inputs: + - name: "Length" + type: integer + - name: "Width" + type: number + - name: "Height" + type: number + - name: "Weight" + type: number +elements: + - name: Package + type: Decision + logic: + type: DecisionTable + # First matching result will be picked + hitPolicy: FIRST + inputs: [ "Height", "Width", "Length", "Weight" ] + rules: + - when: [ '<= 3', '<= 25','<= 35', '<= 2' ] + then: '{ "Size": "S", "Cost": 5.90 }' + - when: [ '<= 11','<= 32','<= 42', '<=25' ] + then: '{ "Size": "M", "Cost": 6.90 }' + - when: [ '<= 19', '<= 36', '<= 60', '<= 25' ] + then: '{ "Size": "L", "Cost": 8.90 }' + - when: [ '<= 37', '<= 36', '<= 60', '<= 25' ] + then: '{ "Size": "XL", "Cost": 10.90}' \ No newline at end of file diff --git a/kie-yard/kie-yard-core/src/test/resources/extra-costs.yml b/kie-yard/kie-yard-core/src/test/resources/extra-costs.yml new file mode 100644 index 00000000000..6c16d351a3a --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/extra-costs.yml @@ -0,0 +1,47 @@ +specVersion: alpha +kind: YaRD +name: "Traffic Violation" +expressionLang: jshell +inputs: + - name: Fragile + type: boolean + - name: Package Tracking + type: boolean + - name: Insurance + type: boolean + - name: Package Type + type: string +elements: + - name: Selected premiums + type: Decision + logic: + type: DecisionTable + # Collect all costs + hitPolicy: COLLECT + inputs: [ Package Type, Fragile, Package Tracking, Insurance ] + rules: + # Insurance for all packages, based on size and if the content is fragile + - when: [ 'S', true, "-", true ] + then: '{ "Name": "Fragile insurance cost", "Price": 10}' + - when: [ 'M', true, "-", true ] + then: '{ "Name": "Fragile insurance cost", "Price": 20}' + - when: [ 'L', true, "-", true ] + then: '{ "Name": "Fragile insurance cost", "Price": 30}' + - when: [ 'XL', true, "-", true ] + then: '{ "Name": "Fragile insurance cost", "Price": 40}' + - when: [ "-", false, "-", true ] + then: '{ "Name": "Regular insurance cost", "Price": 5}' + # Tracking cost is same for all sizes + - when: [ "-", "-", true, "-" ] + then: '{ "Name": "Tracking cost", "Price": 5}' + # Fragile package extra care cost + - when: [ "-", true, "-", "-" ] + then: '{ "Name": "Fragile package shipping cost", "Price": 15}' + - name: "Total cost of premiums" + type: Decision + logic: + type: LiteralExpression + expression: | + ((java.util.List>)`Selected premiums`).stream().map(m -> m.get("Price")).mapToInt(Integer::valueOf).sum(); +# Feels filthy compared to FEEL below +# expression: 'sum( for item in Selected premiums return item.Price )' diff --git a/kie-yard/kie-yard-core/src/test/resources/insurance-base-price.yml b/kie-yard/kie-yard-core/src/test/resources/insurance-base-price.yml new file mode 100644 index 00000000000..b6efed66d95 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/insurance-base-price.yml @@ -0,0 +1,30 @@ +specVersion: alpha +kind: YaRD +name: 'BasePrice' +expressionLang: jshell +inputs: + - name: 'Age' + type: number + - name: 'Previous incidents?' + type: boolean +elements: + - name: 'Base price' + type: Decision + logic: + type: DecisionTable + inputs: [ 'Age', 'Previous incidents?' ] + rules: + - when: [ '<21', false ] + then: 800 + - when: [ '<21', true ] + then: 1000 + - when: [ '>=21', false ] + then: 500 + - when: [ '>=21', true ] + then: 600 + - name: 'Downpayment' + type: Decision + logic: + type: LiteralExpression + expression: | + Math.max(`Base price` * 0.07, 50) diff --git a/kie-yard/kie-yard-core/src/test/resources/logback-test.xml b/kie-yard/kie-yard-core/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..157ea2b895a --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/logback-test.xml @@ -0,0 +1,18 @@ + + + + + + %date{HH:mm:ss.SSS} [%thread] %-5level %class{36}.%method:%line - %msg%n + + + + + + + + + + + + diff --git a/kie-yard/kie-yard-core/src/test/resources/scorecards/README-health-card.yml b/kie-yard/kie-yard-core/src/test/resources/scorecards/README-health-card.yml new file mode 100644 index 00000000000..8f79686f2d6 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/scorecards/README-health-card.yml @@ -0,0 +1,47 @@ +specVersion: alpha +kind: YaRD +name: "Git Repository Completeness" +expressionLang: jshell +inputs: + - name: "README" + type: string + - name: "Web Page Link" + type: string + - name: "Documentation Link" + type: string +elements: + - name: Has README.md file + type: Decision + logic: + type: LiteralExpression + expression: | + `README` != null + - name: README has link to web page + type: Decision + logic: + type: LiteralExpression + expression: | + `README`.contains( `Web Page Link` ) + - name: README has a link to documentation + type: Decision + logic: + type: LiteralExpression + expression: | + `README`.contains( `Documentation Link` ) + - name: README has a title for 'How to contribute' + type: Decision + logic: + type: LiteralExpression + expression: | + `README`.contains( "#How to contribute" ) + - name: Score + type: Decision + logic: + type: DecisionTable + hitPolicy: COLLECT COUNT + inputs: [ "true == {0}" ] + rules: + - [ "Has README.md file" ] + - [ "README has link to web page" ] + - [ "README has a link to documentation" ] + - [ "README has a title for 'How to contribute'" ] diff --git a/kie-yard/kie-yard-core/src/test/resources/scorecards/README.md b/kie-yard/kie-yard-core/src/test/resources/scorecards/README.md new file mode 100644 index 00000000000..0bfe8deb30c --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/scorecards/README.md @@ -0,0 +1,8 @@ +Not used by any tests at this point. Used as drafts for upcoming features. + +Each YaRD yml file is a scorecard and has to return a value "Score". +Depending on the draft: + +* The Score is either a % between 0 and 100. +* The Score is calculated from previous expressions, each expression has to return true or it is considered a failure + diff --git a/kie-yard/kie-yard-core/src/test/resources/scorecards/branch-responsibilities.yml b/kie-yard/kie-yard-core/src/test/resources/scorecards/branch-responsibilities.yml new file mode 100644 index 00000000000..f0e9ee96f49 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/scorecards/branch-responsibilities.yml @@ -0,0 +1,38 @@ +specVersion: alpha +kind: YaRD +name: "Branch responsibilities" +expressionLang: jshell +inputs: + - name: "Main POM" + type: 'http://myapi.org/jsonSchema.json#POM' + - name: "Latest Tag POM" + type: 'http://myapi.org/jsonSchema.json#POM' +elements: + - name: Main is set on SNAPSHOT + type: Decision + logic: + type: LiteralExpression + expression: | + `Main POM`.version.endsWith("-SNAPSHOT") + - name: Main version is higher than latest Tag + type: Decision + logic: + type: LiteralExpression + expression: | + `Main POM`.version > `Latest Tag POM`.version + - name: Tag is not on SNAPSHOT + type: Decision + logic: + type: LiteralExpression + expression: | + !`Latest Tag POM`.version.endWith("-SNAPSHOT") + - name: Score + type: Decision + logic: + type: DecisionTable + hitPolicy: COLLECT COUNT + inputs: [ "true == {0}" ] # this is not in the current spec, neither is inserting the values below + rules: + - [ 'Main is set on SNAPSHOT' ] + - [ 'Main version is higher than latest Tag' ] + - [ 'Tag is not on SNAPSHOT' ] \ No newline at end of file diff --git a/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/branch-blocked-card.yml b/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/branch-blocked-card.yml new file mode 100644 index 00000000000..0b4a085bf85 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/branch-blocked-card.yml @@ -0,0 +1,21 @@ +specVersion: alpha +kind: YaRD +name: "Main broken" +expressionLang: jshell +inputs: + - name: "Creator" + type: string + - name: "Created" + type: datetime + - name: "Closed" + type: datetime + - name: "Severity" +# How long was branch broken +# Was the correct protocol followed, what ever that is +elements: + - name: Time Taken to Ack + type: Decision + logic: + type: LiteralExpression + expression: | + `Reported` -`Acked` diff --git a/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/closed-prs-card.yml b/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/closed-prs-card.yml new file mode 100644 index 00000000000..578330c2adb --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/closed-prs-card.yml @@ -0,0 +1,23 @@ +specVersion: alpha +kind: YaRD +name: "Closed PRs" +expressionLang: jshell +inputs: + - name: "Creator" + type: string + - name: "Created" + type: datetime + - name: "Closed" + type: datetime + - name: "Severity" +# Another one for closed PRs? +# Gap check +# Reviewers acked +# Code smells and so on were in check +elements: + - name: Time Taken to Ack + type: Decision + logic: + type: LiteralExpression + expression: | + `Reported` -`Acked` diff --git a/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/open-prs-card.yml b/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/open-prs-card.yml new file mode 100644 index 00000000000..b0eed257ea5 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/open-prs-card.yml @@ -0,0 +1,21 @@ +specVersion: alpha +kind: YaRD +name: "Open PRs" +expressionLang: jshell +inputs: + - name: "Creator" + type: string + - name: "Created" + type: datetime + - name: "Closed" + type: datetime + - name: "Severity" +# Check they have 2 reviewers set, open - close gap is scored based on length +# Tests are ran, code smells minimal +elements: + - name: Time Taken to Ack + type: Decision + logic: + type: LiteralExpression + expression: | + `Reported` -`Acked` diff --git a/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/weekly-branch-health-card.yml b/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/weekly-branch-health-card.yml new file mode 100644 index 00000000000..5bee4356941 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/scorecards/drafts/weekly-branch-health-card.yml @@ -0,0 +1,22 @@ +specVersion: alpha +kind: YaRD +name: "Main broken" +expressionLang: jshell +inputs: + - name: "Creator" + type: string + - name: "Created" + type: datetime + - name: "Closed" + type: datetime + - name: "Severity" +# Times the nightlies failed +# Amount of test failures +# Build failures +elements: + - name: Time Taken to Ack + type: Decision + logic: + type: LiteralExpression + expression: | + `Reported` -`Acked` diff --git a/kie-yard/kie-yard-core/src/test/resources/scorecards/git-repository-health-card.yml b/kie-yard/kie-yard-core/src/test/resources/scorecards/git-repository-health-card.yml new file mode 100644 index 00000000000..af056f36905 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/scorecards/git-repository-health-card.yml @@ -0,0 +1,56 @@ +specVersion: alpha +kind: YaRD +name: "Git Repository Completeness" +expressionLang: jshell +inputs: + - name: "Repository Data" + type: 'http://myapi.org/jsonSchema.json#GitRepositoryData' +elements: + - name: Has Owners + type: Decision + logic: + type: LiteralExpression + expression: | + `Git Data`.owners.size > 0 + - name: Has Description + type: Decision + logic: + type: LiteralExpression + expression: | + !`Git Data`.description.trim.isEmpty() + - name: Code of Conduct is Set + type: Decision + logic: + type: LiteralExpression + expression: | + `Git Data`.codeOfConduct != null + && !`Git Data`.codeOfConduct.trim.isEmpty() + - name: Security Information is Set + type: Decision + logic: + type: LiteralExpression + expression: | + `Git Data`.securityInformation != null + && !`Git Data`.securityInformation.trim.isEmpty() + - name: Weighted Score + type: Decision + logic: + type: DecisionTable + hitPolicy: COLLECT + inputs: [ "true == {0}" ] + outputComponents: [ "Line Score" ] + rules: + - when: [ 'Has Owners' ] + then: 4 + - when: [ 'Has Description' ] + then: 1 + - when: [ 'Code of Conduct is Set' ] + then: 4 + - when: [ 'Security Information is Set' ] + then: 2 + - name: Score + type: Decision + logic: + type: LiteralExpression + expression: | + `Weighted Score`.stream.mapToInt(Integer::intValue).sum(); \ No newline at end of file diff --git a/kie-yard/kie-yard-core/src/test/resources/scorecards/resource-limits-card.yml b/kie-yard/kie-yard-core/src/test/resources/scorecards/resource-limits-card.yml new file mode 100644 index 00000000000..984f4fcf319 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/scorecards/resource-limits-card.yml @@ -0,0 +1,40 @@ +specVersion: alpha +kind: YaRD +name: "Resource Limits" +expressionLang: jshell +inputs: + - name: "CPU Count" + type: number + - name: "Memory Use" + type: number + - name: "Run Started" + type: datetime + - name: "Run Ended" + type: datetime +elements: + - name: Run Time + type: Decision + logic: + type: LiteralExpression + # With JShell this does not work, but this can be done with a Java oneliner + expression: | + `Run Started` -`Run Ended` + - name: Fitting profiles + type: Decision + logic: + type: DecisionTable + hitPolicy: COLLECT + inputs: [ "Run Time", "Memory Use", "CPU Count" ] + # This assumes there are set profiles that include the above parameters + # How well we meet them is visualized in the score + rules: + - when: [ '<=30m', '<=20', '<=1' ] # Low profile + then: 4 + - when: [ '<=3h00m', '<=40', '<=1' ] # Medium + then: 3 + - when: [ '<=4h00m', '<=50', '<=1' ] # High + then: 2 + - when: [ '<=2h30m', '<=60', '<=1' ] # Ultra + then: 1 + - when: [ '-', '-', '-' ] # Unacceptable + then: 0 diff --git a/kie-yard/kie-yard-core/src/test/resources/ticket-score-cards.yml b/kie-yard/kie-yard-core/src/test/resources/ticket-score-cards.yml new file mode 100644 index 00000000000..65d4fb75597 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/ticket-score-cards.yml @@ -0,0 +1,46 @@ +specVersion: alpha +kind: YaRD +name: "Traffic Violation" +expressionLang: jshell +inputs: + - name: "Tickets" + type: List +elements: + - name: Incident was ack'd within 3 minutes + type: Decision + logic: + type: LiteralExpression + expression: | + `Tickets`.stream().allMatch(t -> TimeUnit.MINUTES.convert(t.reported.getTime() - t.acked.getTime(), TimeUnit.MILLISECONDS) <= 3 ); + - name: Incident was resolved within 5 minutes + type: Decision + logic: + type: LiteralExpression + expression: | + `Tickets`.stream().allMatch(t -> TimeUnit.MINUTES.convert(t.reported.getTime() - t.resolved.getTime(), TimeUnit.MILLISECONDS) <= 5 ); + - name: Bronze Level + type: Decision + logic: + type: DecisionTable + hitPolicy: COLLECT COUNT + inputs: [ "Incident was ack'd within 3 minutes", "Incident was resolved within 5 minutes" ] + rules: + - [ true, '-' ] + - [ '-', true ] + - name: Bronze Complete + type: Decision + logic: + type: LiteralExpression + expression: `Bronze Level` == 2 + - name: Level + type: Decision + logic: + type: DecisionTable + hitPolicy: ANY + inputs: [ "Bronze Complete", "Silver Complete", "Gold Complete" ] + outputComponents: [ "Level" ] + # To reach the next level, previous one has to be fully completed + rules: + - [ true, true, true, 'Gold' ] + - [ true, true, false, 'Silver' ] + - [ true, false, false, 'Bronze' ] diff --git a/kie-yard/pom.xml b/kie-yard/pom.xml new file mode 100644 index 00000000000..185b4a36d00 --- /dev/null +++ b/kie-yard/pom.xml @@ -0,0 +1,59 @@ + + + + + 4.0.0 + + + org.kie + drools-build-parent + 999-SNAPSHOT + ../build-parent/pom.xml + + + kie-yard + pom + + KIE :: Yet Another Rule Definition + + A simple way to describe declarative Decisions and Rules in YAML. + + + + kie-yard-api + kie-yard-core + + + + + + org.kie + kie-yard-bom + ${project.version} + pom + import + + + + + diff --git a/pom.xml b/pom.xml index 7dcdf4c004e..fa6b5bd7b7f 100644 --- a/pom.xml +++ b/pom.xml @@ -206,6 +206,7 @@ efesto kie-drl kie-dmn + kie-yard kie-pmml-trusty kie-maven-plugin kie-archetypes From 2424aab3eccd8fe6acd2aee58e67b96562c5d105 Mon Sep 17 00:00:00 2001 From: rikkola Date: Thu, 2 May 2024 14:57:33 +0300 Subject: [PATCH 2/4] set MVEL as default expression lang --- kie-yard/kie-yard-core/pom.xml | 9 +++ ...> JShellLiteralExpressionInterpreter.java} | 14 ++-- .../yard/core/LiteralExpressionBuilder.java | 18 ++++- .../MVELLiteralExpressionInterpreter.java | 43 ++++++++++ .../java/org/kie/yard/core/YaRDParser.java | 5 +- .../kie/yard/core/InsuranceBasePriceTest.java | 4 +- .../MVELLiteralExpressionInterpreterTest.java | 81 +++++++++++++++++++ .../resources/domestic-package-prices.yml | 1 - .../test/resources/insurance-base-price.yml | 1 - .../src/test/resources/logback-test.xml | 18 ----- .../src/test/resources/ticket-score-cards.yml | 1 - 11 files changed, 159 insertions(+), 36 deletions(-) rename kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/{LiteralExpressionInterpreter.java => JShellLiteralExpressionInterpreter.java} (82%) create mode 100644 kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/MVELLiteralExpressionInterpreter.java create mode 100644 kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELLiteralExpressionInterpreterTest.java delete mode 100644 kie-yard/kie-yard-core/src/test/resources/logback-test.xml diff --git a/kie-yard/kie-yard-core/pom.xml b/kie-yard/kie-yard-core/pom.xml index 5eeaa6596c7..4d6981620e3 100644 --- a/kie-yard/kie-yard-core/pom.xml +++ b/kie-yard/kie-yard-core/pom.xml @@ -44,6 +44,10 @@ ch.obermuhlner jshell-scriptengine + + org.mvel + mvel2 + com.fasterxml.jackson.dataformat jackson-dataformat-yaml @@ -58,6 +62,11 @@ assertj-core test + + org.mockito + mockito-core + test + ch.qos.logback logback-classic diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionInterpreter.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/JShellLiteralExpressionInterpreter.java similarity index 82% rename from kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionInterpreter.java rename to kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/JShellLiteralExpressionInterpreter.java index 31d65ece5b6..eca81a063df 100644 --- a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionInterpreter.java +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/JShellLiteralExpressionInterpreter.java @@ -28,20 +28,21 @@ import javax.script.ScriptEngineManager; import javax.script.ScriptException; -public class LiteralExpressionInterpreter implements Firable { +public class JShellLiteralExpressionInterpreter implements Firable { private final String name; private final QuotedExprParsed quoted; private final ScriptEngine engine; private final CompiledScript compiledScript; - public LiteralExpressionInterpreter(String nameString, QuotedExprParsed quotedExprParsed) { + public JShellLiteralExpressionInterpreter(final String nameString, + final QuotedExprParsed quotedExprParsed) { this.name = nameString; this.quoted = quotedExprParsed; try { - ScriptEngineManager manager = new ScriptEngineManager(); + final ScriptEngineManager manager = new ScriptEngineManager(); engine = manager.getEngineByName("jshell"); - Compilable compiler = (Compilable) engine; + final Compilable compiler = (Compilable) engine; compiledScript = compiler.compile(quoted.getRewrittenExpression()); } catch (Exception e) { throw new IllegalArgumentException("parse error", e); @@ -49,8 +50,9 @@ public LiteralExpressionInterpreter(String nameString, QuotedExprParsed quotedEx } @Override - public int fire(Map context, YaRDDefinitions units) { - Bindings bindings = engine.createBindings(); + public int fire(final Map context, + final YaRDDefinitions units) { + final Bindings bindings = engine.createBindings(); // deliberately escape all symbols; a normal symbol will // never be in the detected-by-unquoting set, so this // set can't be used to selectively put in scope diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionBuilder.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionBuilder.java index 6ac46f4e38b..f81eefbe1ff 100644 --- a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionBuilder.java +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/LiteralExpressionBuilder.java @@ -20,21 +20,33 @@ import org.kie.yard.api.model.LiteralExpression; +import java.util.Objects; + public class LiteralExpressionBuilder { + private final String expressionLang; private final YaRDDefinitions definitions; private final String name; private final LiteralExpression decisionLogic; - public LiteralExpressionBuilder(YaRDDefinitions definitions, String name, LiteralExpression decisionLogic) { + public LiteralExpressionBuilder(final String expressionLang, + final YaRDDefinitions definitions, + final String name, + final LiteralExpression decisionLogic) { + this.expressionLang = expressionLang; this.definitions = definitions; this.name = name; this.decisionLogic = decisionLogic; } public Firable build() { - String expr = decisionLogic.getExpression(); + final String expr = decisionLogic.getExpression(); definitions.outputs().put(name, StoreHandle.empty(Object.class)); - return new LiteralExpressionInterpreter(name, QuotedExprParsed.from(expr)); + if(Objects.equals(expressionLang, "jshell")){ + return new JShellLiteralExpressionInterpreter(name, QuotedExprParsed.from(expr)); + } + else { + return new MVELLiteralExpressionInterpreter(name,QuotedExprParsed.from(expr)); + } } } diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/MVELLiteralExpressionInterpreter.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/MVELLiteralExpressionInterpreter.java new file mode 100644 index 00000000000..70bd6e3a152 --- /dev/null +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/MVELLiteralExpressionInterpreter.java @@ -0,0 +1,43 @@ +package org.kie.yard.core; + +import org.drools.base.util.MVELExecutor; +import org.mvel2.MVEL; + +import javax.script.Bindings; +import java.util.HashMap; +import java.util.Map; + +public class MVELLiteralExpressionInterpreter implements Firable { + private final String name; + private final QuotedExprParsed expr; + + public MVELLiteralExpressionInterpreter(final String name, + final QuotedExprParsed expr) { + this.name = name; + this.expr = expr; + } + + @Override + public int fire(final Map context, + final YaRDDefinitions units) { + final Map internalContext = new HashMap<>(); + internalContext.putAll(context); + + for (Map.Entry> outKV : units.outputs().entrySet()) { + if (!outKV.getValue().isValuePresent()) { + continue; + } + internalContext.put(QuotedExprParsed.escapeIdentifier(outKV.getKey()), outKV.getValue().get()); + } + + try { + String rewrittenExpression = expr.getRewrittenExpression(); + final Object result = MVEL.eval(rewrittenExpression, internalContext); + units.outputs().get(name).set(result); + return 1; + } catch (Exception e) { + throw new RuntimeException("interpretation failed at runtime", e); + // TODO why throw and not return 0? + } + } +} diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java index 9ebbc9752dc..6f8076fd43d 100644 --- a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java @@ -86,9 +86,6 @@ private YaRD getModel(String yaml) throws IOException { private YaRDDefinitions parse(String yaml) throws IOException { final YaRD sd = new YaRD_YamlMapperImpl().read(yaml); - if (!Objects.equals(sd.getExpressionLang(), "jshell")) { - throw new IllegalArgumentException("Only `jshell` is supported as an expression language"); - } appendInputs(sd.getInputs()); appendUnits(sd.getElements()); return definitions; @@ -107,7 +104,7 @@ private Firable createDecisionLogic(String nameString, DecisionLogic decisionLog if (decisionLogic instanceof org.kie.yard.api.model.DecisionTable decisionTable) { return new SyntheticRuleUnitWrapper(new DTableUnitBuilder(definitions, nameString, decisionTable).build()); } else if (decisionLogic instanceof org.kie.yard.api.model.LiteralExpression literalExpression) { - return new LiteralExpressionBuilder(definitions, nameString, literalExpression).build(); + return new LiteralExpressionBuilder(model.getExpressionLang(), definitions, nameString, literalExpression).build(); } else { throw new UnsupportedOperationException("Not implemented."); } diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java index ed0195e239d..cfed2129397 100644 --- a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java @@ -39,7 +39,7 @@ public void testScenario1() throws Exception { """; Map outputJSONasMap = evaluate(CTX, FILE_NAME); assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Base price", 500); - assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Downpayment", 50.0); + assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Downpayment", 50); } @Test @@ -52,6 +52,6 @@ public void testScenario2() throws Exception { """; Map outputJSONasMap = evaluate(CTX, FILE_NAME); assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Base price", 1000); - assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Downpayment", 70.0); + assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Downpayment", 70); } } diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELLiteralExpressionInterpreterTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELLiteralExpressionInterpreterTest.java new file mode 100644 index 00000000000..63227755699 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELLiteralExpressionInterpreterTest.java @@ -0,0 +1,81 @@ +package org.kie.yard.core; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class MVELLiteralExpressionInterpreterTest { + + + private MVELLiteralExpressionInterpreter sum; + private YaRDDefinitions yardDefinitions; + + void setUp(final String expression) { + sum = new MVELLiteralExpressionInterpreter("sum", QuotedExprParsed.from(expression)); + + final Map> outputs = new HashMap<>(); + outputs.put("C", StoreHandle.of(5)); + outputs.put("sum", StoreHandle.empty(Object.class)); + yardDefinitions = new YaRDDefinitions(Collections.emptyMap(), Collections.EMPTY_LIST, outputs); + + } + + @Test + public void testSum() { + + setUp("1+1"); + + final int fire = sum.fire(Collections.emptyMap(), yardDefinitions); + + assertEquals(1, fire); + assertEquals(2, yardDefinitions.outputs().get("sum").get()); + } + + @Test + public void testOutputVar() { + + setUp("Math.max(C, 1)"); + + final int fire = sum.fire(Collections.emptyMap(), yardDefinitions); + + assertEquals(1, fire); + assertEquals(5, yardDefinitions.outputs().get("sum").get()); + } + + @Test + public void testContext() { + + setUp("A+B"); + + final Map context = new HashMap<>(); + context.put("A", 1); + context.put("B", 2); + final int fire = sum.fire(context, yardDefinitions); + + assertEquals(1, fire); + assertEquals(3, yardDefinitions.outputs().get("sum").get()); + } + + @Test + public void testMaps() { + + setUp("person.address.street + ' ' + person.address.number"); + + final Map context = new HashMap<>(); + final Map person = new HashMap<>(); + final Map address = new HashMap<>(); + address.put("street", "My Street"); + address.put("number", 12); + person.put("address", address); + context.put("person", person); + final int fire = sum.fire(context, yardDefinitions); + + assertEquals(1, fire); + assertEquals("My Street 12", yardDefinitions.outputs().get("sum").get()); + } +} \ No newline at end of file diff --git a/kie-yard/kie-yard-core/src/test/resources/domestic-package-prices.yml b/kie-yard/kie-yard-core/src/test/resources/domestic-package-prices.yml index 8b921b676cd..7f5257613b3 100644 --- a/kie-yard/kie-yard-core/src/test/resources/domestic-package-prices.yml +++ b/kie-yard/kie-yard-core/src/test/resources/domestic-package-prices.yml @@ -1,7 +1,6 @@ specVersion: alpha kind: YaRD name: "Traffic Violation" -expressionLang: jshell inputs: - name: "Length" type: integer diff --git a/kie-yard/kie-yard-core/src/test/resources/insurance-base-price.yml b/kie-yard/kie-yard-core/src/test/resources/insurance-base-price.yml index b6efed66d95..263fd92883d 100644 --- a/kie-yard/kie-yard-core/src/test/resources/insurance-base-price.yml +++ b/kie-yard/kie-yard-core/src/test/resources/insurance-base-price.yml @@ -1,7 +1,6 @@ specVersion: alpha kind: YaRD name: 'BasePrice' -expressionLang: jshell inputs: - name: 'Age' type: number diff --git a/kie-yard/kie-yard-core/src/test/resources/logback-test.xml b/kie-yard/kie-yard-core/src/test/resources/logback-test.xml deleted file mode 100644 index 157ea2b895a..00000000000 --- a/kie-yard/kie-yard-core/src/test/resources/logback-test.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - %date{HH:mm:ss.SSS} [%thread] %-5level %class{36}.%method:%line - %msg%n - - - - - - - - - - - - diff --git a/kie-yard/kie-yard-core/src/test/resources/ticket-score-cards.yml b/kie-yard/kie-yard-core/src/test/resources/ticket-score-cards.yml index 65d4fb75597..2dd17e66597 100644 --- a/kie-yard/kie-yard-core/src/test/resources/ticket-score-cards.yml +++ b/kie-yard/kie-yard-core/src/test/resources/ticket-score-cards.yml @@ -1,7 +1,6 @@ specVersion: alpha kind: YaRD name: "Traffic Violation" -expressionLang: jshell inputs: - name: "Tickets" type: List From d0bb0e348a90887e65f42454f7e8a4d0c1d4c4f1 Mon Sep 17 00:00:00 2001 From: rikkola Date: Tue, 7 May 2024 14:26:16 +0300 Subject: [PATCH 3/4] MVEL support --- .../org/kie/yard/core/DTableUnitBuilder.java | 14 +++-- .../MVELLiteralExpressionInterpreter.java | 18 +++++- .../yard/core/DomesticPackagePricesTest.java | 7 ++- .../kie/yard/core/InsuranceBasePriceTest.java | 1 + .../java/org/kie/yard/core/MVELJSONTest.java | 61 +++++++++++++++++++ .../test/resources/mvel-json-dot-access.yml | 57 +++++++++++++++++ 6 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELJSONTest.java create mode 100644 kie-yard/kie-yard-core/src/test/resources/mvel-json-dot-access.yml diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/DTableUnitBuilder.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/DTableUnitBuilder.java index 3bb1ba8628a..592d7e7a260 100644 --- a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/DTableUnitBuilder.java +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/DTableUnitBuilder.java @@ -85,13 +85,14 @@ private OnExecute getExecutionAction(String hitPolicy) { if (hitPolicy == null || Objects.equals("ANY", hitPolicy)) { return (ruleDefinition, storeHandle) -> { final RuleCell rc = parseGenericRuleThen(ruleDefinition); - storeHandle.set(rc.value); + Object value = resolveValue(rc.value); + storeHandle.set(value); }; } else if (Objects.equals("FIRST", hitPolicy)) { return (ruleDefinition, storeHandle) -> { if (!storeHandle.isValuePresent()) { final RuleCell rc = parseGenericRuleThen(ruleDefinition); - storeHandle.set(rc.value); + storeHandle.set(resolveValue(rc.value)); } }; } else if (Objects.equals("COLLECT", hitPolicy)) { @@ -102,22 +103,23 @@ private OnExecute getExecutionAction(String hitPolicy) { final RuleCell rc = parseGenericRuleThen(ruleDefinition); if (storeHandle.get() instanceof List list) { - list.add(resolveValue(rc)); + list.add(resolveValue(rc.value)); } + String remove = ""; }; } else { throw new UnsupportedOperationException("Not implemented "); } } - private Object resolveValue(final RuleCell rc) { + private Object resolveValue(Object value) { try { - if (rc.value instanceof String text) { + if (value instanceof String text) { return jsonMapper.readValue(text, Map.class); } } catch (JsonProcessingException ignored) { } - return rc.value; + return value; } private RuleCell parseGenericRuleThen(Rule rule) { diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/MVELLiteralExpressionInterpreter.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/MVELLiteralExpressionInterpreter.java index 70bd6e3a152..3e9971fe1fb 100644 --- a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/MVELLiteralExpressionInterpreter.java +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/MVELLiteralExpressionInterpreter.java @@ -1,9 +1,9 @@ package org.kie.yard.core; -import org.drools.base.util.MVELExecutor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.json.JsonMapper; import org.mvel2.MVEL; -import javax.script.Bindings; import java.util.HashMap; import java.util.Map; @@ -11,6 +11,8 @@ public class MVELLiteralExpressionInterpreter implements Firable { private final String name; private final QuotedExprParsed expr; + private final JsonMapper jsonMapper = JsonMapper.builder().build(); + public MVELLiteralExpressionInterpreter(final String name, final QuotedExprParsed expr) { this.name = name; @@ -33,11 +35,21 @@ public int fire(final Map context, try { String rewrittenExpression = expr.getRewrittenExpression(); final Object result = MVEL.eval(rewrittenExpression, internalContext); - units.outputs().get(name).set(result); + units.outputs().get(name).set(resolveValue(result)); return 1; } catch (Exception e) { throw new RuntimeException("interpretation failed at runtime", e); // TODO why throw and not return 0? } } + + private Object resolveValue(Object value) { + try { + if (value instanceof String text) { + return jsonMapper.readValue(text, Map.class); + } + } catch (JsonProcessingException ignored) { + } + return value; + } } diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DomesticPackagePricesTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DomesticPackagePricesTest.java index ba94c9449c9..a7a94255c7a 100644 --- a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DomesticPackagePricesTest.java +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DomesticPackagePricesTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class DomesticPackagePricesTest extends TestBase { @@ -41,7 +42,8 @@ public void testMPackage() throws Exception { } """; Map outputJSONasMap = evaluate(CTX, FILE_NAME); - assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Package", "{ \"Size\": \"M\", \"Cost\": 6.90 }"); + assertEquals(6.9, ((Map) outputJSONasMap.get("Package")).get("Cost")); + assertEquals("M", ((Map) outputJSONasMap.get("Package")).get("Size")); } @Test @@ -55,6 +57,7 @@ public void testLPackage() throws Exception { } """; Map outputJSONasMap = evaluate(CTX, FILE_NAME); - assertThat(outputJSONasMap).hasFieldOrPropertyWithValue("Package", "{ \"Size\": \"L\", \"Cost\": 8.90 }"); + assertEquals(8.9, ((Map) outputJSONasMap.get("Package")).get("Cost")); + assertEquals("L", ((Map) outputJSONasMap.get("Package")).get("Size")); } } diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java index cfed2129397..86cc261fae0 100644 --- a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/InsuranceBasePriceTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class InsuranceBasePriceTest extends TestBase { diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELJSONTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELJSONTest.java new file mode 100644 index 00000000000..c1921c2414f --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELJSONTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MVELJSONTest + extends TestBase { + + private static final String FILE_NAME = "/mvel-json-dot-access.yml"; + + @Test + public void testMVELManagesJSONMaps() throws Exception { + final String CTX = """ + { + "Work Address":true + } + """; + final Map output = evaluate(CTX, FILE_NAME); + + final Map mailingAddress = (Map) output.get("Mailing Address"); + assertEquals("Work Street", mailingAddress.get("Street")); + assertEquals(23, mailingAddress.get("Number")); + + final List deliveryItemNames = (List) output.get("Delivery Item Names"); + assertEquals(3, deliveryItemNames.size()); + assertTrue(deliveryItemNames.contains("Work Shoes")); + assertTrue(deliveryItemNames.contains("Work Hat")); + assertTrue(deliveryItemNames.contains("Work Shirt")); + + final Map JSONTest= (Map) output.get("JSON Test"); + assertEquals("Best Company LTD", JSONTest.get("Company")); + + final Map mapTest = (Map) output.get("Map Test"); + assertEquals("Hello", mapTest.get("Map")); + } + // TODO Validate two elements can not have the same name +} diff --git a/kie-yard/kie-yard-core/src/test/resources/mvel-json-dot-access.yml b/kie-yard/kie-yard-core/src/test/resources/mvel-json-dot-access.yml new file mode 100644 index 00000000000..3bd073033dc --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/mvel-json-dot-access.yml @@ -0,0 +1,57 @@ +specVersion: alpha +kind: YaRD +name: "Testing MVEL" +inputs: + - name: Work Address + type: boolean +elements: + - name: JSON Test + type: Decision + logic: + type: LiteralExpression + expression: | + '{ "Company": "Best Company LTD" }' + - name: Map Test + type: Decision + logic: + type: LiteralExpression + expression: | + [ "Map": "Hello" ] + - name: Delivery Items + type: Decision + logic: + type: DecisionTable + hitPolicy: COLLECT + inputs: [ 'Work Address'] + rules: + - when: [ true ] + then: '{ "Item": { "Name": "Work Shoes" } }' + - when: [ true ] + then: '{ "Item": { "Name": "Work Hat" } }' + - when: [ true ] + then: '{ "Item": { "Name": "Work Shirt" } }' + - when: [ false ] + then: '{ "Item": { "Name": "Holiday Hat" } }' + - name: Customer + type: Decision + logic: + type: DecisionTable + hitPolicy: ANY + inputs: [ 'Work Address'] + rules: + - when: [ true ] + then: '{ "Address": { "Street": "Work Street", "Number": 23 } }' + - when: [ false ] + then: '{ "Address": { "Street": "Free Time Path", "Number": 123 } }' + - name: Mailing Address + type: Decision + logic: + type: LiteralExpression + expression: | + Customer.Address + - name: Delivery Item Names + type: Decision + logic: + type: LiteralExpression + expression: | + ( Item.Name in `Delivery Items` ) From 840d8ba01563197d7b884ed91dc0c6bda1aea11c Mon Sep 17 00:00:00 2001 From: rikkola Date: Tue, 7 May 2024 16:39:44 +0300 Subject: [PATCH 4/4] Do not allow duplicate element names --- .../java/org/kie/yard/core/YaRDParser.java | 14 ++++-- .../java/org/kie/yard/core/YaRDRunner.java | 6 +-- .../yard/core/DublicateElementNameTest.java | 44 +++++++++++++++++++ .../java/org/kie/yard/core/MVELJSONTest.java | 1 - .../two-elements-with-duplicate-name.yml | 19 ++++++++ 5 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DublicateElementNameTest.java create mode 100644 kie-yard/kie-yard-core/src/test/resources/two-elements-with-duplicate-name.yml diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java index 6f8076fd43d..e1bc1e53507 100644 --- a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDParser.java @@ -71,8 +71,8 @@ private String read(Reader reader) throws Exception { int numRead; while ((numRead = reader.read(buf)) != -1) { String readData = String.valueOf(buf, - 0, - numRead); + 0, + numRead); fileData.append(readData); buf = new char[1024]; } @@ -92,10 +92,16 @@ private YaRDDefinitions parse(String yaml) throws IOException { } private void appendUnits(List list) { + final List existingNames = new ArrayList<>(); for (Element hi : list) { - String nameString = hi.getName(); + final String nameString = hi.getName(); + if (existingNames.contains(nameString)) { + throw new IllegalArgumentException("Two element definitions with the same name are not allowed."); + } else { + existingNames.add(nameString); + } LOG.debug("parsing {}", nameString); - Firable decisionLogic = createDecisionLogic(nameString, hi.getLogic()); + final Firable decisionLogic = createDecisionLogic(nameString, hi.getLogic()); definitions.units().add(decisionLogic); } } diff --git a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDRunner.java b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDRunner.java index 24b4bca1e31..8f616bf382f 100644 --- a/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDRunner.java +++ b/kie-yard/kie-yard-core/src/main/java/org/kie/yard/core/YaRDRunner.java @@ -25,14 +25,14 @@ public class YaRDRunner { - private final YaRDDefinitions units; + private final YaRDDefinitions definitions; private final JsonMapper jsonMapper = JsonMapper.builder().build(); private final String name; public YaRDRunner(final String yaml) throws IOException { final YaRDParser parser = new YaRDParser(yaml); name = parser.getModel().getName(); - units = parser.getDefinitions(); + definitions = parser.getDefinitions(); } public String getName() { @@ -40,7 +40,7 @@ public String getName() { } public Map evaluate(final Map map) { - return units.evaluate(map); + return definitions.evaluate(map); } public String evaluate(String jsonInputCxt) throws Exception { diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DublicateElementNameTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DublicateElementNameTest.java new file mode 100644 index 00000000000..3f1228f4275 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/DublicateElementNameTest.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.yard.core; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DublicateElementNameTest + extends TestBase { + + private static final String FILE_NAME = "/two-elements-with-duplicate-name.yml"; + + @Test + public void testNoDublicateNames() throws Exception { + final String CTX = """ + { + "Age": 10 + } + """; + assertThrows(IllegalArgumentException.class, () -> + evaluate(CTX, FILE_NAME) + ); + } +} diff --git a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELJSONTest.java b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELJSONTest.java index c1921c2414f..f69723e310c 100644 --- a/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELJSONTest.java +++ b/kie-yard/kie-yard-core/src/test/java/org/kie/yard/core/MVELJSONTest.java @@ -57,5 +57,4 @@ public void testMVELManagesJSONMaps() throws Exception { final Map mapTest = (Map) output.get("Map Test"); assertEquals("Hello", mapTest.get("Map")); } - // TODO Validate two elements can not have the same name } diff --git a/kie-yard/kie-yard-core/src/test/resources/two-elements-with-duplicate-name.yml b/kie-yard/kie-yard-core/src/test/resources/two-elements-with-duplicate-name.yml new file mode 100644 index 00000000000..7f954a64bb5 --- /dev/null +++ b/kie-yard/kie-yard-core/src/test/resources/two-elements-with-duplicate-name.yml @@ -0,0 +1,19 @@ +specVersion: alpha +kind: YaRD +name: 'BasePrice' +inputs: + - name: Age + type: number +elements: + - name: Ten + type: Decision + logic: + type: LiteralExpression + expression: | + 10 + - name: Ten + type: Decision + logic: + type: LiteralExpression + expression: | + 5 + 5