From 47049e87d2f90273bd14acee2969335d87860f0b Mon Sep 17 00:00:00 2001 From: aserkes Date: Tue, 7 Mar 2023 13:50:41 +0100 Subject: [PATCH] new syntax for expressions Signed-off-by: aserkes --- .../archetype/engine/v2/ast/Expression.java | 13 +- .../engine/v2/util/ValueHandler.java | 139 ++++++++++++++ .../engine/v2/ast/ExpressionTest.java | 21 ++- .../engine/v2/util/ValueHandlerTest.java | 175 ++++++++++++++++++ 4 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ValueHandler.java create mode 100644 archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/util/ValueHandlerTest.java diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/Expression.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/Expression.java index 858c35d32..2c49e9e62 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/Expression.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/Expression.java @@ -196,7 +196,12 @@ public String variable() { */ public static final class FormatException extends RuntimeException { - private FormatException(String message) { + /** + * Create new instance. + * + * @param message message + */ + public FormatException(String message) { super(message); } } @@ -293,6 +298,7 @@ public static Expression parse(String expression) { case BOOLEAN: case STRING: case ARRAY: + case INTEGER: case VARIABLE: stackSize += 1 - addToken(symbol, tokens); break; @@ -543,6 +549,8 @@ private static Token create(Symbol symbol) { return new Token(null, null, Value.create(Boolean.parseBoolean(symbol.value))); case STRING: return new Token(null, null, Value.create(symbol.value.substring(1, symbol.value.length() - 1))); + case INTEGER: + return new Token(null, null, Value.create(Integer.parseInt(symbol.value), ValueTypes.INT)); case ARRAY: return new Token(null, null, Value.create(parseArray(symbol.value))); case VARIABLE: @@ -578,7 +586,8 @@ enum Type { PARENTHESIS("^[()]"), COMMENT("#.*\\R"), TERNARY_IF_OPERATOR("^\\?"), - TERNARY_ELSE_OPERATOR("^:"); + TERNARY_ELSE_OPERATOR("^:"), + INTEGER("^[0-9]+"); private final Pattern pattern; diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ValueHandler.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ValueHandler.java new file mode 100644 index 000000000..eed0dc25a --- /dev/null +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ValueHandler.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.build.archetype.engine.v2.util; + +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.helidon.build.archetype.engine.v2.ast.Expression; +import io.helidon.build.archetype.engine.v2.ast.Value; + +/** + * Process string and if it represents an expression evaluate it and return {@link Value}. + * + * @see Expression + */ +public class ValueHandler { + + private static final Pattern VAR_NO_BRACE = Pattern.compile("^\\w+"); + + /** + * Process expression. + * + * @param expression expression + * @param resolver variable resolver + * @return {@link Value} object + */ + public static Value process(String expression, Function resolver) { + if (expression == null) { + return Value.NULL; + } + for (ExpressionType type : ExpressionType.values()) { + Matcher matcher = type.pattern.matcher(expression); + if (matcher.find()) { + String value = matcher.group("expression"); + if (type == ExpressionType.NO_BRACE_VARS) { + value = preprocessExpression(value); + } + return Expression.parse(value).eval(resolver); + } + + } + return Value.create(expression); + } + + /** + * Process expression. + * + * @param expression expression + * @return {@link Value} object + */ + public static Value process(String expression) { + return process(expression, s -> null); + } + + private static String preprocessExpression(String expression) { + StringBuilder output = new StringBuilder(); + int cursor = 0; + String current = expression.trim(); + while (current.length() > 0) { + current = current.substring(cursor); + cursor = 0; + boolean tokenFound = false; + for (Token token : Token.values()) { + Matcher matcher = token.pattern.matcher(current); + if (matcher.find()) { + String value = matcher.group(); + cursor += value.length(); + output.append(value); + tokenFound = true; + break; + } + } + if (tokenFound) { + continue; + } + Matcher matcherVar = VAR_NO_BRACE.matcher(current); + while (matcherVar.find()) { + var value = matcherVar.group(); + cursor += value.length(); + output.append("${").append(value).append("}"); + tokenFound = true; + } + if (!tokenFound && current.trim().length() > 0) { + throw new Expression.FormatException("Unexpected token - " + current); + } + } + return output.toString(); + } + + private enum ExpressionType { + + BACKTICK("^`(?.*)`$"), + BRACE_VARS("^#\\{(?.*(\\$\\{)+.*}+.*)}$"), + NO_BRACE_VARS("^#\\{(?[^\\$\\{}]*)}$"); + + private final Pattern pattern; + + ExpressionType(String regex) { + this.pattern = Pattern.compile(regex); + } + } + + private enum Token { + SKIP("^\\s+"), + ARRAY("^\\[[^]\\[]*]"), + BOOLEAN("^(true|false)\\b"), + STRING("^['\"][^'\"]*['\"]"), + VARIABLE("^\\$\\{(?~?[\\w.-]+)}"), + EQUALITY_OPERATOR("^(!=|==)"), + BINARY_LOGICAL_OPERATOR("^(\\|\\||&&)"), + UNARY_LOGICAL_OPERATOR("^[!]"), + CONTAINS_OPERATOR("^contains\\b"), + PARENTHESIS("^[()]"), + COMMENT("#.*\\R"), + TERNARY_IF_OPERATOR("^\\?"), + TERNARY_ELSE_OPERATOR("^:"), + INTEGER("^[0-9]+"); + + private final Pattern pattern; + + Token(String regex) { + this.pattern = Pattern.compile(regex); + } + } +} diff --git a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ast/ExpressionTest.java b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ast/ExpressionTest.java index cb0ef3d0b..89ec373f5 100644 --- a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ast/ExpressionTest.java +++ b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ast/ExpressionTest.java @@ -28,6 +28,7 @@ import static io.helidon.build.archetype.engine.v2.ast.Expression.parse; import static io.helidon.build.common.Maps.mapValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; @@ -39,12 +40,26 @@ class ExpressionTest { @Test - public void testTernaryExpression() { + public void testValue() { Expression exp; - Map variables; exp = Expression.parse("'circle'"); - assertThat(exp.eval().asText(), is("circle")); + assertThat(exp.eval().asString(), is("circle")); + + exp = Expression.parse("true"); + assertThat(exp.eval().asBoolean(), is(true)); + + exp = Expression.parse("1"); + assertThat(exp.eval().asInt(), is(1)); + + exp = Expression.parse("['', 'adc', 'def']"); + assertThat(exp.eval().asList(), containsInAnyOrder("", "adc", "def")); + } + + @Test + public void testTernaryExpression() { + Expression exp; + Map variables; exp = Expression.parse("${shape} == 'circle' ? 'red' : 'blue'"); variables = Map.of("shape", Value.create("circle")); diff --git a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/util/ValueHandlerTest.java b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/util/ValueHandlerTest.java new file mode 100644 index 000000000..330872642 --- /dev/null +++ b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/util/ValueHandlerTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.build.archetype.engine.v2.util; + +import java.util.List; +import java.util.Map; + +import io.helidon.build.archetype.engine.v2.ast.Expression; +import io.helidon.build.archetype.engine.v2.ast.Value; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for {@link ValueHandler} + */ +public class ValueHandlerTest { + + @Test + public void testBacktickExpression() { + Map variables; + Value result; + + variables = Map.of( + "shape", Value.create("circle"), + "var1", Value.create(List.of("a", "b", "c")), + "var2", Value.create("b"), + "var3", Value.create("c")); + result = ValueHandler.process("`${shape} == 'circle' ? ${var1} contains ${var2} ? 'circle_b' : 'not_circle_b'" + + " : ${var1} contains ${var3} ? 'circle_c' : 'not_circle_c'`", variables::get); + assertThat(result.asText(), is("circle_b")); + + variables = Map.of( + "shape", Value.create("circle"), + "var1", Value.create(List.of("a", "b", "c")), + "var2", Value.create("b")); + result = ValueHandler.process("`${shape} == 'circle' ? ${var1} contains ${var2} : false`", variables::get); + assertThat(result.asBoolean(), is(true)); + + variables = Map.of("shape", Value.create("circle")); + result = ValueHandler.process("`${shape} == 'circle' ? 'red' : 'blue'`", variables::get); + assertThat(result.asString(), is("red")); + + variables = Map.of("shape", Value.create("circle")); + result = ValueHandler.process("`${shape}`", variables::get); + assertThat(result.asString(), is("circle")); + + Map varMap = Map.of( + "shape", Value.create("circle"), + "var1", Value.create("circle")); + Expression.FormatException e = assertThrows(Expression.FormatException.class, + () -> ValueHandler.process("`${shape} == var1`", varMap::get)); + assertThat(e.getMessage(), containsString("Unexpected token - var1")); + + e = assertThrows(Expression.FormatException.class, + () -> ValueHandler.process("`shape`")); + assertThat(e.getMessage(), containsString("Unexpected token - shape")); + } + + @Test + public void testBraceVarExpression() { + Map variables; + Value result; + + variables = Map.of( + "shape", Value.create("circle"), + "var1", Value.create(List.of("a", "b", "c")), + "var2", Value.create("b"), + "var3", Value.create("c")); + result = ValueHandler.process("#{${shape} == 'circle' ? ${var1} contains ${var2} ? 'circle_b' : 'not_circle_b'" + + " : ${var1} contains ${var3} ? 'circle_c' : 'not_circle_c'}", variables::get); + assertThat(result.asText(), is("circle_b")); + + variables = Map.of( + "shape", Value.create("circle"), + "var1", Value.create(List.of("a", "b", "c")), + "var2", Value.create("b")); + result = ValueHandler.process("#{${shape} == 'circle' ? ${var1} contains ${var2} : false}", variables::get); + assertThat(result.asBoolean(), is(true)); + + variables = Map.of("shape", Value.create("circle")); + result = ValueHandler.process("#{${shape} == 'circle' ? 'red' : 'blue'}", variables::get); + assertThat(result.asString(), is("red")); + + variables = Map.of("shape", Value.create("circle")); + result = ValueHandler.process("#{${shape}}", variables::get); + assertThat(result.asString(), is("circle")); + + Map varMap = Map.of( + "shape", Value.create("circle"), + "var1", Value.create("circle")); + Expression.FormatException e = assertThrows(Expression.FormatException.class, + () -> ValueHandler.process("#{${shape} == var1}", varMap::get)); + assertThat(e.getMessage(), containsString("Unexpected token - var1")); + } + + @Test + public void testNoBraceVarExpression() { + Map variables; + Value result; + + variables = Map.of( + "shape", Value.create("circle"), + "var1", Value.create(List.of("a", "b", "c")), + "var2", Value.create("b"), + "var3", Value.create("c")); + result = ValueHandler.process("#{shape == 'circle' ? var1 contains var2 ? 'circle_b' : 'not_circle_b' : " + + "var1 contains var3 ? 'circle_c' : 'not_circle_c'}", variables::get); + assertThat(result.asText(), is("circle_b")); + + variables = Map.of( + "shape", Value.create("circle"), + "var1", Value.create(List.of("a", "b", "c")), + "var2", Value.create("b")); + result = ValueHandler.process("#{shape == 'circle' ? var1 contains var2 : false}", variables::get); + assertThat(result.asBoolean(), is(true)); + + variables = Map.of("shape", Value.create("circle")); + result = ValueHandler.process("#{shape == 'circle' ? 'red' : 'blue'}", variables::get); + assertThat(result.asString(), is("red")); + + variables = Map.of("shape", Value.create("circle")); + result = ValueHandler.process("#{shape}", variables::get); + assertThat(result.asString(), is("circle")); + + result = ValueHandler.process("`true`"); + assertThat(result.asBoolean(), is(true)); + + result = ValueHandler.process("#{1}"); + assertThat(result.asInt(), is(1)); + + result = ValueHandler.process("`['', 'adc', 'def']`"); + assertThat(result.asList(), containsInAnyOrder("", "adc", "def")); + + Map varMap = Map.of( + "shape", Value.create("circle"), + "var1", Value.create("circle")); + Expression.FormatException e = assertThrows(Expression.FormatException.class, + () -> ValueHandler.process("#{${shape} ^ var1}", varMap::get)); + assertThat(e.getMessage(), containsString("Unexpected token - ^")); + } + + @Test + public void testNoExpression() { + var value = ValueHandler.process("circle"); + assertThat(value.asString(), is("circle")); + + value = ValueHandler.process("true"); + assertThat(value.asString(), is("true")); + + value = ValueHandler.process("1"); + assertThat(value.asString(), is("1")); + + value = ValueHandler.process("['', 'adc', 'def']"); + assertThat(value.asString(), is("['', 'adc', 'def']")); + } +}