Skip to content

Commit 47049e8

Browse files
committed
new syntax for expressions
Signed-off-by: aserkes <[email protected]>
1 parent f1e4c6c commit 47049e8

File tree

4 files changed

+343
-5
lines changed

4 files changed

+343
-5
lines changed

archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/Expression.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,12 @@ public String variable() {
196196
*/
197197
public static final class FormatException extends RuntimeException {
198198

199-
private FormatException(String message) {
199+
/**
200+
* Create new instance.
201+
*
202+
* @param message message
203+
*/
204+
public FormatException(String message) {
200205
super(message);
201206
}
202207
}
@@ -293,6 +298,7 @@ public static Expression parse(String expression) {
293298
case BOOLEAN:
294299
case STRING:
295300
case ARRAY:
301+
case INTEGER:
296302
case VARIABLE:
297303
stackSize += 1 - addToken(symbol, tokens);
298304
break;
@@ -543,6 +549,8 @@ private static Token create(Symbol symbol) {
543549
return new Token(null, null, Value.create(Boolean.parseBoolean(symbol.value)));
544550
case STRING:
545551
return new Token(null, null, Value.create(symbol.value.substring(1, symbol.value.length() - 1)));
552+
case INTEGER:
553+
return new Token(null, null, Value.create(Integer.parseInt(symbol.value), ValueTypes.INT));
546554
case ARRAY:
547555
return new Token(null, null, Value.create(parseArray(symbol.value)));
548556
case VARIABLE:
@@ -578,7 +586,8 @@ enum Type {
578586
PARENTHESIS("^[()]"),
579587
COMMENT("#.*\\R"),
580588
TERNARY_IF_OPERATOR("^\\?"),
581-
TERNARY_ELSE_OPERATOR("^:");
589+
TERNARY_ELSE_OPERATOR("^:"),
590+
INTEGER("^[0-9]+");
582591

583592
private final Pattern pattern;
584593

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright (c) 2023 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.build.archetype.engine.v2.util;
17+
18+
import java.util.function.Function;
19+
import java.util.regex.Matcher;
20+
import java.util.regex.Pattern;
21+
22+
import io.helidon.build.archetype.engine.v2.ast.Expression;
23+
import io.helidon.build.archetype.engine.v2.ast.Value;
24+
25+
/**
26+
* Process string and if it represents an expression evaluate it and return {@link Value}.
27+
*
28+
* @see Expression
29+
*/
30+
public class ValueHandler {
31+
32+
private static final Pattern VAR_NO_BRACE = Pattern.compile("^\\w+");
33+
34+
/**
35+
* Process expression.
36+
*
37+
* @param expression expression
38+
* @param resolver variable resolver
39+
* @return {@link Value} object
40+
*/
41+
public static Value process(String expression, Function<String, Value> resolver) {
42+
if (expression == null) {
43+
return Value.NULL;
44+
}
45+
for (ExpressionType type : ExpressionType.values()) {
46+
Matcher matcher = type.pattern.matcher(expression);
47+
if (matcher.find()) {
48+
String value = matcher.group("expression");
49+
if (type == ExpressionType.NO_BRACE_VARS) {
50+
value = preprocessExpression(value);
51+
}
52+
return Expression.parse(value).eval(resolver);
53+
}
54+
55+
}
56+
return Value.create(expression);
57+
}
58+
59+
/**
60+
* Process expression.
61+
*
62+
* @param expression expression
63+
* @return {@link Value} object
64+
*/
65+
public static Value process(String expression) {
66+
return process(expression, s -> null);
67+
}
68+
69+
private static String preprocessExpression(String expression) {
70+
StringBuilder output = new StringBuilder();
71+
int cursor = 0;
72+
String current = expression.trim();
73+
while (current.length() > 0) {
74+
current = current.substring(cursor);
75+
cursor = 0;
76+
boolean tokenFound = false;
77+
for (Token token : Token.values()) {
78+
Matcher matcher = token.pattern.matcher(current);
79+
if (matcher.find()) {
80+
String value = matcher.group();
81+
cursor += value.length();
82+
output.append(value);
83+
tokenFound = true;
84+
break;
85+
}
86+
}
87+
if (tokenFound) {
88+
continue;
89+
}
90+
Matcher matcherVar = VAR_NO_BRACE.matcher(current);
91+
while (matcherVar.find()) {
92+
var value = matcherVar.group();
93+
cursor += value.length();
94+
output.append("${").append(value).append("}");
95+
tokenFound = true;
96+
}
97+
if (!tokenFound && current.trim().length() > 0) {
98+
throw new Expression.FormatException("Unexpected token - " + current);
99+
}
100+
}
101+
return output.toString();
102+
}
103+
104+
private enum ExpressionType {
105+
106+
BACKTICK("^`(?<expression>.*)`$"),
107+
BRACE_VARS("^#\\{(?<expression>.*(\\$\\{)+.*}+.*)}$"),
108+
NO_BRACE_VARS("^#\\{(?<expression>[^\\$\\{}]*)}$");
109+
110+
private final Pattern pattern;
111+
112+
ExpressionType(String regex) {
113+
this.pattern = Pattern.compile(regex);
114+
}
115+
}
116+
117+
private enum Token {
118+
SKIP("^\\s+"),
119+
ARRAY("^\\[[^]\\[]*]"),
120+
BOOLEAN("^(true|false)\\b"),
121+
STRING("^['\"][^'\"]*['\"]"),
122+
VARIABLE("^\\$\\{(?<varName>~?[\\w.-]+)}"),
123+
EQUALITY_OPERATOR("^(!=|==)"),
124+
BINARY_LOGICAL_OPERATOR("^(\\|\\||&&)"),
125+
UNARY_LOGICAL_OPERATOR("^[!]"),
126+
CONTAINS_OPERATOR("^contains\\b"),
127+
PARENTHESIS("^[()]"),
128+
COMMENT("#.*\\R"),
129+
TERNARY_IF_OPERATOR("^\\?"),
130+
TERNARY_ELSE_OPERATOR("^:"),
131+
INTEGER("^[0-9]+");
132+
133+
private final Pattern pattern;
134+
135+
Token(String regex) {
136+
this.pattern = Pattern.compile(regex);
137+
}
138+
}
139+
}

archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ast/ExpressionTest.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import static io.helidon.build.archetype.engine.v2.ast.Expression.parse;
2929
import static io.helidon.build.common.Maps.mapValue;
3030
import static org.hamcrest.MatcherAssert.assertThat;
31+
import static org.hamcrest.Matchers.containsInAnyOrder;
3132
import static org.hamcrest.Matchers.containsString;
3233
import static org.hamcrest.Matchers.is;
3334
import static org.hamcrest.Matchers.startsWith;
@@ -39,12 +40,26 @@
3940
class ExpressionTest {
4041

4142
@Test
42-
public void testTernaryExpression() {
43+
public void testValue() {
4344
Expression exp;
44-
Map<String, Value> variables;
4545

4646
exp = Expression.parse("'circle'");
47-
assertThat(exp.eval().asText(), is("circle"));
47+
assertThat(exp.eval().asString(), is("circle"));
48+
49+
exp = Expression.parse("true");
50+
assertThat(exp.eval().asBoolean(), is(true));
51+
52+
exp = Expression.parse("1");
53+
assertThat(exp.eval().asInt(), is(1));
54+
55+
exp = Expression.parse("['', 'adc', 'def']");
56+
assertThat(exp.eval().asList(), containsInAnyOrder("", "adc", "def"));
57+
}
58+
59+
@Test
60+
public void testTernaryExpression() {
61+
Expression exp;
62+
Map<String, Value> variables;
4863

4964
exp = Expression.parse("${shape} == 'circle' ? 'red' : 'blue'");
5065
variables = Map.of("shape", Value.create("circle"));
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Copyright (c) 2023 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.build.archetype.engine.v2.util;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
21+
import io.helidon.build.archetype.engine.v2.ast.Expression;
22+
import io.helidon.build.archetype.engine.v2.ast.Value;
23+
24+
import org.junit.jupiter.api.Test;
25+
26+
import static org.hamcrest.MatcherAssert.assertThat;
27+
import static org.hamcrest.Matchers.containsInAnyOrder;
28+
import static org.hamcrest.Matchers.containsString;
29+
import static org.hamcrest.Matchers.is;
30+
import static org.junit.jupiter.api.Assertions.assertThrows;
31+
32+
/**
33+
* Tests for {@link ValueHandler}
34+
*/
35+
public class ValueHandlerTest {
36+
37+
@Test
38+
public void testBacktickExpression() {
39+
Map<String, Value> variables;
40+
Value result;
41+
42+
variables = Map.of(
43+
"shape", Value.create("circle"),
44+
"var1", Value.create(List.of("a", "b", "c")),
45+
"var2", Value.create("b"),
46+
"var3", Value.create("c"));
47+
result = ValueHandler.process("`${shape} == 'circle' ? ${var1} contains ${var2} ? 'circle_b' : 'not_circle_b'"
48+
+ " : ${var1} contains ${var3} ? 'circle_c' : 'not_circle_c'`", variables::get);
49+
assertThat(result.asText(), is("circle_b"));
50+
51+
variables = Map.of(
52+
"shape", Value.create("circle"),
53+
"var1", Value.create(List.of("a", "b", "c")),
54+
"var2", Value.create("b"));
55+
result = ValueHandler.process("`${shape} == 'circle' ? ${var1} contains ${var2} : false`", variables::get);
56+
assertThat(result.asBoolean(), is(true));
57+
58+
variables = Map.of("shape", Value.create("circle"));
59+
result = ValueHandler.process("`${shape} == 'circle' ? 'red' : 'blue'`", variables::get);
60+
assertThat(result.asString(), is("red"));
61+
62+
variables = Map.of("shape", Value.create("circle"));
63+
result = ValueHandler.process("`${shape}`", variables::get);
64+
assertThat(result.asString(), is("circle"));
65+
66+
Map<String, Value> varMap = Map.of(
67+
"shape", Value.create("circle"),
68+
"var1", Value.create("circle"));
69+
Expression.FormatException e = assertThrows(Expression.FormatException.class,
70+
() -> ValueHandler.process("`${shape} == var1`", varMap::get));
71+
assertThat(e.getMessage(), containsString("Unexpected token - var1"));
72+
73+
e = assertThrows(Expression.FormatException.class,
74+
() -> ValueHandler.process("`shape`"));
75+
assertThat(e.getMessage(), containsString("Unexpected token - shape"));
76+
}
77+
78+
@Test
79+
public void testBraceVarExpression() {
80+
Map<String, Value> variables;
81+
Value result;
82+
83+
variables = Map.of(
84+
"shape", Value.create("circle"),
85+
"var1", Value.create(List.of("a", "b", "c")),
86+
"var2", Value.create("b"),
87+
"var3", Value.create("c"));
88+
result = ValueHandler.process("#{${shape} == 'circle' ? ${var1} contains ${var2} ? 'circle_b' : 'not_circle_b'"
89+
+ " : ${var1} contains ${var3} ? 'circle_c' : 'not_circle_c'}", variables::get);
90+
assertThat(result.asText(), is("circle_b"));
91+
92+
variables = Map.of(
93+
"shape", Value.create("circle"),
94+
"var1", Value.create(List.of("a", "b", "c")),
95+
"var2", Value.create("b"));
96+
result = ValueHandler.process("#{${shape} == 'circle' ? ${var1} contains ${var2} : false}", variables::get);
97+
assertThat(result.asBoolean(), is(true));
98+
99+
variables = Map.of("shape", Value.create("circle"));
100+
result = ValueHandler.process("#{${shape} == 'circle' ? 'red' : 'blue'}", variables::get);
101+
assertThat(result.asString(), is("red"));
102+
103+
variables = Map.of("shape", Value.create("circle"));
104+
result = ValueHandler.process("#{${shape}}", variables::get);
105+
assertThat(result.asString(), is("circle"));
106+
107+
Map<String, Value> varMap = Map.of(
108+
"shape", Value.create("circle"),
109+
"var1", Value.create("circle"));
110+
Expression.FormatException e = assertThrows(Expression.FormatException.class,
111+
() -> ValueHandler.process("#{${shape} == var1}", varMap::get));
112+
assertThat(e.getMessage(), containsString("Unexpected token - var1"));
113+
}
114+
115+
@Test
116+
public void testNoBraceVarExpression() {
117+
Map<String, Value> variables;
118+
Value result;
119+
120+
variables = Map.of(
121+
"shape", Value.create("circle"),
122+
"var1", Value.create(List.of("a", "b", "c")),
123+
"var2", Value.create("b"),
124+
"var3", Value.create("c"));
125+
result = ValueHandler.process("#{shape == 'circle' ? var1 contains var2 ? 'circle_b' : 'not_circle_b' : "
126+
+ "var1 contains var3 ? 'circle_c' : 'not_circle_c'}", variables::get);
127+
assertThat(result.asText(), is("circle_b"));
128+
129+
variables = Map.of(
130+
"shape", Value.create("circle"),
131+
"var1", Value.create(List.of("a", "b", "c")),
132+
"var2", Value.create("b"));
133+
result = ValueHandler.process("#{shape == 'circle' ? var1 contains var2 : false}", variables::get);
134+
assertThat(result.asBoolean(), is(true));
135+
136+
variables = Map.of("shape", Value.create("circle"));
137+
result = ValueHandler.process("#{shape == 'circle' ? 'red' : 'blue'}", variables::get);
138+
assertThat(result.asString(), is("red"));
139+
140+
variables = Map.of("shape", Value.create("circle"));
141+
result = ValueHandler.process("#{shape}", variables::get);
142+
assertThat(result.asString(), is("circle"));
143+
144+
result = ValueHandler.process("`true`");
145+
assertThat(result.asBoolean(), is(true));
146+
147+
result = ValueHandler.process("#{1}");
148+
assertThat(result.asInt(), is(1));
149+
150+
result = ValueHandler.process("`['', 'adc', 'def']`");
151+
assertThat(result.asList(), containsInAnyOrder("", "adc", "def"));
152+
153+
Map<String, Value> varMap = Map.of(
154+
"shape", Value.create("circle"),
155+
"var1", Value.create("circle"));
156+
Expression.FormatException e = assertThrows(Expression.FormatException.class,
157+
() -> ValueHandler.process("#{${shape} ^ var1}", varMap::get));
158+
assertThat(e.getMessage(), containsString("Unexpected token - ^"));
159+
}
160+
161+
@Test
162+
public void testNoExpression() {
163+
var value = ValueHandler.process("circle");
164+
assertThat(value.asString(), is("circle"));
165+
166+
value = ValueHandler.process("true");
167+
assertThat(value.asString(), is("true"));
168+
169+
value = ValueHandler.process("1");
170+
assertThat(value.asString(), is("1"));
171+
172+
value = ValueHandler.process("['', 'adc', 'def']");
173+
assertThat(value.asString(), is("['', 'adc', 'def']"));
174+
}
175+
}

0 commit comments

Comments
 (0)