Skip to content

Commit

Permalink
new syntax for expressions
Browse files Browse the repository at this point in the history
Signed-off-by: aserkes <[email protected]>
  • Loading branch information
aserkes committed Mar 7, 2023
1 parent f1e4c6c commit 47049e8
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Value> 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("^`(?<expression>.*)`$"),
BRACE_VARS("^#\\{(?<expression>.*(\\$\\{)+.*}+.*)}$"),
NO_BRACE_VARS("^#\\{(?<expression>[^\\$\\{}]*)}$");

private final Pattern pattern;

ExpressionType(String regex) {
this.pattern = Pattern.compile(regex);
}
}

private enum Token {
SKIP("^\\s+"),
ARRAY("^\\[[^]\\[]*]"),
BOOLEAN("^(true|false)\\b"),
STRING("^['\"][^'\"]*['\"]"),
VARIABLE("^\\$\\{(?<varName>~?[\\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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,12 +40,26 @@
class ExpressionTest {

@Test
public void testTernaryExpression() {
public void testValue() {
Expression exp;
Map<String, Value> 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<String, Value> variables;

exp = Expression.parse("${shape} == 'circle' ? 'red' : 'blue'");
variables = Map.of("shape", Value.create("circle"));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Value> 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<String, Value> 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<String, Value> 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<String, Value> 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<String, Value> 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<String, Value> 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']"));
}
}

0 comments on commit 47049e8

Please sign in to comment.