From f07d6e4e28cd1ad6673880a35b6bb706113a9869 Mon Sep 17 00:00:00 2001 From: tvallin Date: Wed, 18 Oct 2023 15:45:14 +0200 Subject: [PATCH 1/2] Conditional Block Signed-off-by: tvallin --- .../archetype/v2/json/ScriptSerializer.java | 14 +- .../build/archetype/engine/v2/Controller.java | 19 +- .../archetype/engine/v2/ScriptLoader.java | 36 +- .../build/archetype/engine/v2/ast/Block.java | 39 +- .../engine/v2/ast/ConditionBlock.java | 86 +++ .../engine/v2/util/ArchetypeValidator.java | 20 +- .../engine-v2/src/main/schema/archetype.xsd | 627 ++++++++++++------ .../archetype/engine/v2/ControllerTest.java | 67 ++ .../archetype/engine/v2/ScriptLoaderTest.java | 128 +++- .../controller/conditional-block.xml | 131 ++++ .../controller/conditional-inputs.xml | 90 +++ .../resources/loader/conditional-block.xml | 120 ++++ 12 files changed, 1149 insertions(+), 228 deletions(-) create mode 100644 archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/ConditionBlock.java create mode 100644 archetype/engine-v2/src/test/resources/controller/conditional-block.xml create mode 100644 archetype/engine-v2/src/test/resources/controller/conditional-inputs.xml create mode 100644 archetype/engine-v2/src/test/resources/loader/conditional-block.xml diff --git a/archetype/engine-v2-json/src/main/java/io/helidon/build/archetype/v2/json/ScriptSerializer.java b/archetype/engine-v2-json/src/main/java/io/helidon/build/archetype/v2/json/ScriptSerializer.java index f8c3c0af7..a0fe53a2c 100644 --- a/archetype/engine-v2-json/src/main/java/io/helidon/build/archetype/v2/json/ScriptSerializer.java +++ b/archetype/engine-v2-json/src/main/java/io/helidon/build/archetype/v2/json/ScriptSerializer.java @@ -35,6 +35,7 @@ import io.helidon.build.archetype.engine.v2.Walker; import io.helidon.build.archetype.engine.v2.ast.Block; import io.helidon.build.archetype.engine.v2.ast.Condition; +import io.helidon.build.archetype.engine.v2.ast.ConditionBlock; import io.helidon.build.archetype.engine.v2.ast.DeclaredBlock; import io.helidon.build.archetype.engine.v2.ast.Expression; import io.helidon.build.archetype.engine.v2.ast.Expression.Token; @@ -195,8 +196,12 @@ public VisitResult visitInvocation(Invocation invocation, Script script) { @Override public VisitResult visitCondition(Condition cond, Script script) { - exprId = exprIds.computeIfAbsent(cond.expression(), this::exprId); - return VisitResult.CONTINUE; + return evaluate(cond.expression()); + } + + @Override + public VisitResult visitConditionBlock(ConditionBlock condition, JsonObjectBuilder arg) { + return evaluate(condition.expression()); } @Override @@ -369,6 +374,11 @@ private String exprId(Expression expr) { return exprId; } + private VisitResult evaluate(Expression expression) { + exprId = exprIds.computeIfAbsent(expression, this::exprId); + return VisitResult.CONTINUE; + } + private static final class TokenVisitor implements Token.Visitor { @Override diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/Controller.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/Controller.java index fd008366b..b2c1e3105 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/Controller.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/Controller.java @@ -21,6 +21,8 @@ import io.helidon.build.archetype.engine.v2.ast.Block; import io.helidon.build.archetype.engine.v2.ast.Condition; +import io.helidon.build.archetype.engine.v2.ast.ConditionBlock; +import io.helidon.build.archetype.engine.v2.ast.Expression; import io.helidon.build.archetype.engine.v2.ast.Model; import io.helidon.build.archetype.engine.v2.ast.Node.VisitResult; import io.helidon.build.archetype.engine.v2.ast.Output; @@ -92,10 +94,12 @@ public VisitResult postVisitBlock(Block block, Context ctx) { @Override public VisitResult visitCondition(Condition condition, Context ctx) { - if (condition.expression().eval(ctx::getValue)) { - return VisitResult.CONTINUE; - } - return VisitResult.SKIP_SUBTREE; + return evaluate(condition.expression(), ctx); + } + + @Override + public VisitResult visitConditionBlock(ConditionBlock condition, Context ctx) { + return evaluate(condition.expression(), ctx); } /** @@ -212,4 +216,11 @@ public static void walk(InputResolver resolver, throw new IllegalStateException("Invalid scope after walking block"); } } + + private VisitResult evaluate(Expression expression, Context ctx) { + if (expression.eval(ctx::getValue)) { + return VisitResult.CONTINUE; + } + return VisitResult.SKIP_SUBTREE; + } } diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ScriptLoader.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ScriptLoader.java index db3fb7be7..04b6153c0 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ScriptLoader.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ScriptLoader.java @@ -30,6 +30,7 @@ import io.helidon.build.archetype.engine.v2.ast.Block; import io.helidon.build.archetype.engine.v2.ast.Condition; +import io.helidon.build.archetype.engine.v2.ast.ConditionBlock; import io.helidon.build.archetype.engine.v2.ast.DynamicValue; import io.helidon.build.archetype.engine.v2.ast.Input; import io.helidon.build.archetype.engine.v2.ast.Invocation; @@ -178,6 +179,7 @@ private static final class ReaderImpl implements SimpleXMLParser.Reader { private String qName; private Map attrs; private LinkedList stack; + private LinkedList expressions; private Context ctx; private Script.Builder scriptBuilder; @@ -188,6 +190,7 @@ private static final class ReaderImpl implements SimpleXMLParser.Reader { Script read(InputStream is, Path path) throws IOException { this.path = Objects.requireNonNull(path, "path is null"); stack = new LinkedList<>(); + expressions = new LinkedList<>(); parser = SimpleXMLParser.create(is, this); parser.parse(); if (scriptBuilder == null) { @@ -198,7 +201,7 @@ Script read(InputStream is, Path path) throws IOException { @Override public void startElement(String qName, Map attrs) { - this.qName = qName; + this.qName = qName.replace("-", ""); this.attrs = Maps.mapValue(attrs, DynamicValue::create); Location location = Location.of(path, parser.lineNumber(), parser.charNumber()); info = BuilderInfo.of(loader, path, location); @@ -241,6 +244,11 @@ public void endElement(String name) { void processElement() { switch (qName) { + case "if": + case "elseif": + case "else": + processCondition(); + return; case "directory": case "help": stack.push(new Context(ctx.state, new ValueBuilder(ctx.builder, qName))); @@ -338,6 +346,32 @@ void processValidation() { } } + void processCondition() { + String expression; + ConditionBlock.Builder builder; + Block.Kind kind = blockKind(); + switch (kind) { + case IF: + expression = processXmlEscapes(attrs.get("expr").asString()); + builder = ConditionBlock.builder(info, kind).expression(expression); + expressions.push(expression); + break; + case ELSEIF: + expression = String.format("!(%s) && %s", expressions.pop(), processXmlEscapes(attrs.get("expr").asString())); + builder = ConditionBlock.builder(info, kind).expression(expression); + expressions.push(expression); + break; + case ELSE: + expression = expressions.pop(); + builder = ConditionBlock.builder(info, kind).expression(String.format("!(%s)", expression)); + break; + default: + throw new XMLReaderException(String.format( + "Invalid input block: %s. { element=%s }", kind, qName)); + } + addChild(ctx.state, builder); + } + void processPreset() { Block.Builder builder; Block.Kind kind = blockKind(); diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/Block.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/Block.java index 30415a98d..a3dce9e10 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/Block.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/Block.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 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. @@ -277,6 +277,28 @@ default VisitResult postVisitModel(Model model, A arg) { return postVisitAny(model, arg); } + /** + * Visit a condition block. + * + * @param condition condition block + * @param arg visitor argument + * @return result + */ + default VisitResult visitConditionBlock(ConditionBlock condition, A arg) { + return visitAny(condition, arg); + } + + /** + * Visit a condition block after traversing the nested nodes. + * + * @param condition condition block + * @param arg visitor argument + * @return result + */ + default VisitResult postVisitConditionBlock(ConditionBlock condition, A arg) { + return postVisitAny(condition, arg); + } + /** * Visit any block. * @@ -504,6 +526,21 @@ public enum Kind { * Invoke. */ INVOKE, + + /** + * If. + */ + IF, + + /** + * Else. + */ + ELSE, + + /** + * Else-if. + */ + ELSEIF } /** diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/ConditionBlock.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/ConditionBlock.java new file mode 100644 index 000000000..784eaefa5 --- /dev/null +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/ConditionBlock.java @@ -0,0 +1,86 @@ +/* + * 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.ast; + +/** + * ConditionBlock. + */ +public final class ConditionBlock extends Block { + private final Expression expression; + + private ConditionBlock(Builder builder) { + super(builder); + this.expression = builder.expression; + } + + /** + * Create a new Builder instance. + * + * @param info builder info + * @param kind kind + * @return builder instance + */ + public static Builder builder(BuilderInfo info, Kind kind) { + return new Builder(info, kind); + } + + /** + * Get the expression. + * + * @return expression + */ + public Expression expression() { + return expression; + } + + @Override + public VisitResult accept(Block.Visitor visitor, A arg) { + return visitor.visitConditionBlock(this, arg); + } + + @Override + public VisitResult acceptAfter(Visitor visitor, A arg) { + return visitor.postVisitConditionBlock(this, arg); + } + + /** + * ConditionBlock builder. + */ + public static final class Builder extends Block.Builder { + private Expression expression; + + private Builder(BuilderInfo info, Kind kind) { + super(info, kind); + } + + /** + * Set the expression. + * + * @param expression expression + * @return this builder + */ + public Builder expression(String expression) { + this.expression = Expression.create(expression); + return this; + } + + @Override + protected ConditionBlock doBuild() { + return new ConditionBlock(this); + } + } +} diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ArchetypeValidator.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ArchetypeValidator.java index ed2b0f05f..9621b1ab0 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ArchetypeValidator.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ArchetypeValidator.java @@ -29,6 +29,7 @@ import io.helidon.build.archetype.engine.v2.Walker; import io.helidon.build.archetype.engine.v2.ast.Block; import io.helidon.build.archetype.engine.v2.ast.Condition; +import io.helidon.build.archetype.engine.v2.ast.ConditionBlock; import io.helidon.build.archetype.engine.v2.ast.Expression; import io.helidon.build.archetype.engine.v2.ast.Input; import io.helidon.build.archetype.engine.v2.ast.Input.DeclaredInput; @@ -149,10 +150,9 @@ private List refs(String path, Context ctx) { return null; } - @Override - public VisitResult visitCondition(Condition condition, Context ctx) { + private VisitResult evaluate(Node node, Expression expression, Context ctx) { try { - condition.expression().eval(variable -> { + expression.eval(variable -> { List refs = refs(variable, ctx); if (refs == null || refs.isEmpty()) { return null; @@ -174,13 +174,13 @@ public VisitResult visitCondition(Condition condition, Context ctx) { }); } catch (Expression.UnresolvedVariableException ex) { errors.add(String.format("%s %s: '%s'", - condition.location(), + node.location(), EXPR_UNRESOLVED_VARIABLE, ex.variable())); } catch (IllegalStateException ex) { errors.add(String.format( "%s %s: '%s'", - condition.location(), + node.location(), EXPR_EVAL_ERROR, ex.getMessage())); } @@ -188,6 +188,16 @@ public VisitResult visitCondition(Condition condition, Context ctx) { return VisitResult.CONTINUE; } + @Override + public VisitResult visitCondition(Condition condition, Context ctx) { + return evaluate(condition, condition.expression(), ctx); + } + + @Override + public VisitResult visitConditionBlock(ConditionBlock condition, Context ctx) { + return evaluate(condition, condition.expression(), ctx); + } + @Override public VisitResult visitPreset(Preset preset, Context ctx) { ctx.scope().putValue(preset.path(), preset.value(), preset.isModel(), ValueKind.LOCAL_VAR); diff --git a/archetype/engine-v2/src/main/schema/archetype.xsd b/archetype/engine-v2/src/main/schema/archetype.xsd index 14562cf56..b7aa6abe1 100644 --- a/archetype/engine-v2/src/main/schema/archetype.xsd +++ b/archetype/engine-v2/src/main/schema/archetype.xsd @@ -109,6 +109,17 @@ + + + + 2.0 + + Expression to guard nested blocks. + + + + + @@ -400,13 +411,33 @@ Map data type. + + + + + + + + + + + + + + + + + + + + + + - - @@ -424,13 +455,33 @@ List data type. Nested elements do not require a key attribute. + + + + + + + + + + + + + + + + + + + + + + - - @@ -523,75 +574,94 @@ Define a variable. - - - - - 2.0 - - Define a boolean value. - - - - - - - - - - - - - - 2.0 - - Define a text value. - - - - - - - - - - - - - - 2.0 - - Define an enum value. - - - - - - - - - - - - - - 2.0 - - Define a list value. - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + 2.0 + + Define a boolean value. + + + + + + + + + + + + + + 2.0 + + Define a text value. + + + + + + + + + + + + + + 2.0 + + Define an enum value. + + + + + + + + + + + + + + 2.0 + + Define a list value. + + + + + + + + + + + + + + + + + + @@ -601,71 +671,90 @@ Define a read-only value for an input. - - - - - 2.0 - - Define a boolean value. - - - - - - - - - - - - - 2.0 - - Define a text value. - - - - - - - - - - - - - 2.0 - - Define an enum value. - - - - - - - - - - - - - 2.0 - - Define a list value. - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + 2.0 + + Define a boolean value. + + + + + + + + + + + + + 2.0 + + Define a text value. + + + + + + + + + + + + + 2.0 + + Define an enum value. + + + + + + + + + + + + + 2.0 + + Define a list value. + + + + + + + + + + + + + + + + + @@ -840,50 +929,51 @@ + + + + + + + + + + 2.0 + + Default boolean input value. + + + + + + + + + + + + + + + + + + + + + - - - - - - - - 2.0 - - Default boolean input value. - - - - - - - - - - - - - 2.0 - - Enum constant value. - - - - - - - - + @@ -901,30 +991,50 @@ + + + + + + 2.0 + + Enum/List constant value. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - 2.0 - - List constant value. - - - - - - - - + @@ -996,12 +1106,29 @@ A user input. - + + + + + + + + + + + + + + + + + + @@ -1015,21 +1142,41 @@ Step. A logical group of inputs to be presented together like a pane or a window. + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + - - - @@ -1040,12 +1187,32 @@ or <templates>. - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1055,6 +1222,22 @@ Configuration of files, template and data model for the current scope. + + + + + + + + + + + + + + + + @@ -1314,12 +1497,14 @@ + + + - @@ -1356,20 +1541,34 @@ - + 2.0 The <archetype-script> element is the root of the descriptor. - - + + + + + + + + + + + + + + + + - - - + + + diff --git a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ControllerTest.java b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ControllerTest.java index 87d83e448..5751d6978 100644 --- a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ControllerTest.java +++ b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ControllerTest.java @@ -16,6 +16,7 @@ package io.helidon.build.archetype.engine.v2; +import java.io.ByteArrayInputStream; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -24,6 +25,7 @@ import io.helidon.build.archetype.engine.v2.ast.Model; import io.helidon.build.archetype.engine.v2.ast.Node.VisitResult; import io.helidon.build.archetype.engine.v2.ast.Script; +import io.helidon.build.archetype.engine.v2.ast.Step; import io.helidon.build.archetype.engine.v2.ast.Value; import io.helidon.build.archetype.engine.v2.ast.ValueTypes; import io.helidon.build.archetype.engine.v2.context.Context; @@ -36,6 +38,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; /** @@ -143,6 +146,63 @@ void testInvocationCondition() { assertThat(values.size(), is(0)); } + @Test + void testConditionalScript() { + Script script = load("controller/conditional-block.xml"); + Context context = createConditionalContext(); + + Controller.walk(script, context); + + assertThat(context.getValue("success1"), is(notNullValue())); + assertThat(context.getValue("success2"), is(notNullValue())); + assertThat(context.getValue("success3"), is(notNullValue())); + assertThat(context.getValue("outside1"), is(notNullValue())); + assertThat(context.getValue("outside2"), is(notNullValue())); + assertThat(context.getValue("outside3"), is(notNullValue())); + assertThat(context.getValue("failure1"), is(nullValue())); + assertThat(context.getValue("failure2"), is(nullValue())); + assertThat(context.getValue("failure3"), is(nullValue())); + } + + @Test + void testConditionModel() { + Script script = load("controller/conditional-block.xml"); + Context context = createConditionalContext(); + + Controller.walk(script, context); + + List values = modelValues(script, context); + assertThat(values, contains("value1", "value2")); + } + + @Test + void testConditionalStep() { + Script script = load("controller/conditional-block.xml"); + Context context = createConditionalContext(); + List steps = new LinkedList<>(); + Controller.walk(new TerminalInputResolver(new ByteArrayInputStream("yes".getBytes())) { + @Override + protected void onVisitStep(Step step, Context context) { + steps.add(step.name()); + super.onVisitStep(step, context); + } + }, script, context); + + assertThat(steps, contains("step", "my-step")); + } + + @Test + void testConditionalInput() { + Script script = load("controller/conditional-inputs.xml"); + Context context = Context.create(); + context.putValue("enum1", Value.create("option1"), ContextValue.ValueKind.EXTERNAL); + context.putValue("list1", Value.create(List.of("option1", "option3")), ContextValue.ValueKind.EXTERNAL); + + List values = modelValues(script, context); + + assertThat(values, contains("value1", "value2", "value3", "value4", "value5")); + } + private static List modelValues(Block block, Context context) { List values = new LinkedList<>(); Controller.walk(new Model.Visitor<>() { @@ -154,4 +214,11 @@ public VisitResult visitValue(Model.Value value, Context arg) { }, block, context); return values; } + + private Context createConditionalContext() { + Context context = Context.create(); + context.putValue("foo", Value.create(true), ContextValue.ValueKind.EXTERNAL); + context.putValue("bar", Value.create(false), ContextValue.ValueKind.EXTERNAL); + return context; + } } diff --git a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ScriptLoaderTest.java b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ScriptLoaderTest.java index 1fcf19f7a..ea330757e 100644 --- a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ScriptLoaderTest.java +++ b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ScriptLoaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 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. @@ -23,6 +23,8 @@ import io.helidon.build.archetype.engine.v2.ast.Block; import io.helidon.build.archetype.engine.v2.ast.Condition; +import io.helidon.build.archetype.engine.v2.ast.ConditionBlock; +import io.helidon.build.archetype.engine.v2.ast.Expression; import io.helidon.build.archetype.engine.v2.ast.Input; import io.helidon.build.archetype.engine.v2.ast.Invocation; import io.helidon.build.archetype.engine.v2.ast.Model; @@ -33,6 +35,7 @@ import io.helidon.build.archetype.engine.v2.ast.Script; import io.helidon.build.archetype.engine.v2.ast.Step; import io.helidon.build.archetype.engine.v2.ast.Validation; +import io.helidon.build.archetype.engine.v2.ast.Value; import io.helidon.build.archetype.engine.v2.ast.ValueTypes; import io.helidon.build.archetype.engine.v2.ast.Variable; import io.helidon.build.common.test.utils.TestFiles; @@ -727,4 +730,127 @@ public VisitResult visitValue(Model.Value value, Void arg) { }, script); assertThat(colors, contains("yellow", "green", "red", "blue")); } + + @Test + void testConditionalBlock() { + Script script = load("loader/conditional-block.xml"); + int[] index = new int[]{0}; + + Model.Visitor modelVisitor = new Model.Visitor<>() { + @Override + public VisitResult visitValue(Model.Value value, Void arg) { + switch (++index[0]) { + case 3: + assertThat(value.value(), is("value1")); + break; + case 4: + assertThat(value.value(), is("value2")); + break; + case 5: + assertThat(value.value(), is("value3")); + break; + default: + Assertions.fail(); + } + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitList(Model.List list, Void arg) { + assertThat(list.key(), is("list")); + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitMap(Model.Map map, Void arg) { + assertThat(map.key(), is("map")); + return VisitResult.CONTINUE; + } + }; + + walk(new VisitorAdapter<>(null, null, modelVisitor, null) { + + @Override + public VisitResult visitValidation(Validation validation, Void arg) { + switch (++index[0]) { + case 1: + assertThat(validation.id(), is("validation1")); + break; + case 2: + assertThat(validation.id(), is("validation2")); + break; + default: + Assertions.fail(); + } + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitPreset(Preset preset, Void arg) { + assertThat(preset.path(), is("path")); + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitVariable(Variable variable, Void arg) { + assertThat(variable.path(), is("path")); + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitCondition(Condition condition, Void arg) { + return evaluate(condition.expression()); + } + + @Override + public VisitResult visitConditionBlock(ConditionBlock condition, Void arg) { + return evaluate(condition.expression()); + } + + @Override + public VisitResult visitStep(Step step, Void arg) { + assertThat(step.name(), is("step")); + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitInput(Input input, Void arg) { + return input.accept(new Input.Visitor<>() { + @Override + public VisitResult visitBoolean(Input.Boolean input, Void arg) { + assertThat(input.id(), is("bool1")); + assertThat(input.name(), is("Bool 1")); + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitOption(Input.Option input, Void arg) { + assertThat(input.value(), is("option1")); + assertThat(input.name(), is("Option 1")); + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitEnum(Input.Enum input, Void arg) { + assertThat(input.id(), is("enum1")); + assertThat(input.name(), is("Enum 1")); + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitList(Input.List input, Void arg) { + assertThat(input.id(), is("list1")); + assertThat(input.name(), is("List 1")); + return VisitResult.CONTINUE; + } + }, arg); + } + private VisitResult evaluate(Expression expression) { + if (expression.eval()) { + return VisitResult.CONTINUE; + } + return VisitResult.SKIP_SUBTREE; + } + }, script); + } } diff --git a/archetype/engine-v2/src/test/resources/controller/conditional-block.xml b/archetype/engine-v2/src/test/resources/controller/conditional-block.xml new file mode 100644 index 000000000..c23579c5b --- /dev/null +++ b/archetype/engine-v2/src/test/resources/controller/conditional-block.xml @@ -0,0 +1,131 @@ + + + + + + + + success + + + + + success + + + + failure + + + + + success + + + + success + + + + failure + + + + + failure + + + + + success + + + + success + + + my help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dummy + + + + + dummy + + + value1 + + + + + + + + + + dummy + + + + + + dummy + + + value2 + + + + + + + + + diff --git a/archetype/engine-v2/src/test/resources/controller/conditional-inputs.xml b/archetype/engine-v2/src/test/resources/controller/conditional-inputs.xml new file mode 100644 index 000000000..6df8d7faa --- /dev/null +++ b/archetype/engine-v2/src/test/resources/controller/conditional-inputs.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + value1 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archetype/engine-v2/src/test/resources/loader/conditional-block.xml b/archetype/engine-v2/src/test/resources/loader/conditional-block.xml new file mode 100644 index 000000000..622e2b5ea --- /dev/null +++ b/archetype/engine-v2/src/test/resources/loader/conditional-block.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + regex + + + + + + + + + + + + + + + + + + + + + + + + + + + + value1 + + + + + value2 + + + failure + + + + + + + failure + + + failure + + + value3 + + + + + + + + + + From 06670ff480579c667d01af80efad2f6c2166c32c Mon Sep 17 00:00:00 2001 From: tvallin Date: Mon, 23 Oct 2023 10:08:38 +0200 Subject: [PATCH 2/2] review changes Signed-off-by: tvallin --- .../archetype/v2/json/ScriptSerializer.java | 8 ++- .../v2/json/ScriptSerializerTest.java | 11 ++++ .../test/resources/expected/filtering3.json | 61 +++++++++++++++++++ .../src/test/resources/filtering3/main.xml | 49 +++++++++++++++ .../engine/v2/ast/ConditionBlock.java | 13 ++++ .../engine/v2/util/ClientCompiler.java | 7 +++ .../engine/v2/util/ClientPredicate.java | 16 +++++ .../archetype/engine/v2/ControllerTest.java | 2 +- .../archetype/engine/v2/ScriptLoaderTest.java | 5 +- .../engine/v2/util/ClientCompilerTest.java | 61 +++++++++++++++++++ .../compiler/conditions/condition.xml | 45 ++++++++++++++ .../controller/conditional-block.xml | 20 +++--- .../resources/loader/conditional-block.xml | 6 +- 13 files changed, 288 insertions(+), 16 deletions(-) create mode 100644 archetype/engine-v2-json/src/test/resources/expected/filtering3.json create mode 100644 archetype/engine-v2-json/src/test/resources/filtering3/main.xml create mode 100644 archetype/engine-v2/src/test/resources/compiler/conditions/condition.xml diff --git a/archetype/engine-v2-json/src/main/java/io/helidon/build/archetype/v2/json/ScriptSerializer.java b/archetype/engine-v2-json/src/main/java/io/helidon/build/archetype/v2/json/ScriptSerializer.java index a0fe53a2c..4ac264166 100644 --- a/archetype/engine-v2-json/src/main/java/io/helidon/build/archetype/v2/json/ScriptSerializer.java +++ b/archetype/engine-v2-json/src/main/java/io/helidon/build/archetype/v2/json/ScriptSerializer.java @@ -201,7 +201,13 @@ public VisitResult visitCondition(Condition cond, Script script) { @Override public VisitResult visitConditionBlock(ConditionBlock condition, JsonObjectBuilder arg) { - return evaluate(condition.expression()); + VisitResult result = evaluate(condition.expression()); + BuilderContext builder = stack.peek(); + if (builder != null) { + builder.block.add("if", exprId); + } + exprId = null; + return result; } @Override diff --git a/archetype/engine-v2-json/src/test/java/io/helidon/build/archetype/v2/json/ScriptSerializerTest.java b/archetype/engine-v2-json/src/test/java/io/helidon/build/archetype/v2/json/ScriptSerializerTest.java index f9c3f4064..700318743 100644 --- a/archetype/engine-v2-json/src/test/java/io/helidon/build/archetype/v2/json/ScriptSerializerTest.java +++ b/archetype/engine-v2-json/src/test/java/io/helidon/build/archetype/v2/json/ScriptSerializerTest.java @@ -143,6 +143,17 @@ void testFiltering2() throws IOException { assertThat(jsonDiff(archetypeJson, readJson(expected)), is(EMPTY_JSON_ARRAY)); } + @Test + void testFiltering3() throws IOException { + Path targetDir = targetDir(this.getClass()); + Path sourceDir = targetDir.resolve("test-classes/filtering3"); + Path expected = targetDir.resolve("test-classes/expected/filtering3.json"); + FileSystem fs = VirtualFileSystem.create(sourceDir); + + JsonObject archetypeJson = ScriptSerializer.serialize(fs); + assertThat(jsonDiff(archetypeJson, readJson(expected)), is(EMPTY_JSON_ARRAY)); + } + @Test void testConvertExpression() throws IOException { int id = 1; diff --git a/archetype/engine-v2-json/src/test/resources/expected/filtering3.json b/archetype/engine-v2-json/src/test/resources/expected/filtering3.json new file mode 100644 index 000000000..db010af6b --- /dev/null +++ b/archetype/engine-v2-json/src/test/resources/expected/filtering3.json @@ -0,0 +1,61 @@ +{ + "expressions": { + "1": [ + { + "kind": "literal", + "value": true + } + ], + "2": [ + { + "kind": "literal", + "value": false + }, + { + "kind": "operator", + "value": "!" + }, + { + "kind": "literal", + "value": true + }, + { + "kind": "operator", + "value": "&&" + } + ] + }, + "methods": {}, + "children": [ + { + "kind": "if", + "if": "1", + "children": [ + { + "kind": "step", + "name": "Step1", + "id": "1", + "children": [ + { + "kind": "elseif", + "if": "2", + "children": [ + { + "kind": "inputs", + "children": [ + { + "kind": "boolean", + "name": "Boolean1", + "id": "boolean1", + "default": false + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/archetype/engine-v2-json/src/test/resources/filtering3/main.xml b/archetype/engine-v2-json/src/test/resources/filtering3/main.xml new file mode 100644 index 000000000..78384021d --- /dev/null +++ b/archetype/engine-v2-json/src/test/resources/filtering3/main.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + bar + + + + + + + + + + diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/ConditionBlock.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/ConditionBlock.java index 784eaefa5..edbbe8a43 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/ConditionBlock.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/ast/ConditionBlock.java @@ -21,10 +21,12 @@ */ public final class ConditionBlock extends Block { private final Expression expression; + private final String rawExpression; private ConditionBlock(Builder builder) { super(builder); this.expression = builder.expression; + this.rawExpression = builder.rawExpression; } /** @@ -47,6 +49,15 @@ public Expression expression() { return expression; } + /** + * Get the raw expression. + * + * @return raw expression + */ + public String rawExpression() { + return rawExpression; + } + @Override public VisitResult accept(Block.Visitor visitor, A arg) { return visitor.visitConditionBlock(this, arg); @@ -62,6 +73,7 @@ public VisitResult acceptAfter(Visitor visitor, A arg) { */ public static final class Builder extends Block.Builder { private Expression expression; + private String rawExpression; private Builder(BuilderInfo info, Kind kind) { super(info, kind); @@ -74,6 +86,7 @@ private Builder(BuilderInfo info, Kind kind) { * @return this builder */ public Builder expression(String expression) { + this.rawExpression = expression; this.expression = Expression.create(expression); return this; } diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ClientCompiler.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ClientCompiler.java index aaf8e775f..38a83d0b2 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ClientCompiler.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ClientCompiler.java @@ -23,6 +23,7 @@ import io.helidon.build.archetype.engine.v2.ScriptLoader; import io.helidon.build.archetype.engine.v2.ast.Block; import io.helidon.build.archetype.engine.v2.ast.Condition; +import io.helidon.build.archetype.engine.v2.ast.ConditionBlock; import io.helidon.build.archetype.engine.v2.ast.DeclaredBlock; import io.helidon.build.archetype.engine.v2.ast.DeclaredValue; import io.helidon.build.archetype.engine.v2.ast.Input; @@ -137,6 +138,9 @@ public Node.VisitResult visitBlock(Block block, Script script) { builder = Step.builder(builderInfo(block)); } else if (block instanceof Input) { builder = Input.builder(builderInfo, kind); + } else if (block instanceof ConditionBlock) { + builder = ConditionBlock.builder(builderInfo(block), kind) + .expression(((ConditionBlock) block).rawExpression()); } else { builder = Block.builder(builderInfo, kind); } @@ -159,6 +163,9 @@ public Node.VisitResult postVisitBlock(Block block, Script arg) { case VARIABLES: case INVOKE: case INVOKE_DIR: + case IF: + case ELSEIF: + case ELSE: Node.Builder parentBuilder = stack.peek(); if (parentBuilder != null) { parentBuilder.nestedBuilders().remove(builder); diff --git a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ClientPredicate.java b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ClientPredicate.java index 9a88a6146..bc1d500ea 100644 --- a/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ClientPredicate.java +++ b/archetype/engine-v2/src/main/java/io/helidon/build/archetype/engine/v2/util/ClientPredicate.java @@ -17,6 +17,7 @@ package io.helidon.build.archetype.engine.v2.util; import io.helidon.build.archetype.engine.v2.ast.Block; +import io.helidon.build.archetype.engine.v2.ast.ConditionBlock; import io.helidon.build.archetype.engine.v2.ast.Node; import io.helidon.build.archetype.engine.v2.ast.Variable; @@ -52,4 +53,19 @@ public Node.VisitResult visitVariable(Variable variable, Void arg) { } return Node.VisitResult.CONTINUE; } + + @Override + public Node.VisitResult visitConditionBlock(ConditionBlock condition, Void arg) { + if (condition.children().isEmpty()) { + return Node.VisitResult.SKIP_SUBTREE; + } + for (Node node : condition.children()) { + if (node instanceof Block) { + if (((Block) node).kind() != Block.Kind.OUTPUT) { + return Node.VisitResult.CONTINUE; + } + } + } + return Node.VisitResult.SKIP_SUBTREE; + } } diff --git a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ControllerTest.java b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ControllerTest.java index 5751d6978..ddd5394bb 100644 --- a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ControllerTest.java +++ b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ControllerTest.java @@ -188,7 +188,7 @@ protected void onVisitStep(Step step, Context context) { } }, script, context); - assertThat(steps, contains("step", "my-step")); + assertThat(steps, contains("step1", "step2")); } @Test diff --git a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ScriptLoaderTest.java b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ScriptLoaderTest.java index ea330757e..cd527a450 100644 --- a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ScriptLoaderTest.java +++ b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/ScriptLoaderTest.java @@ -846,10 +846,7 @@ public VisitResult visitList(Input.List input, Void arg) { }, arg); } private VisitResult evaluate(Expression expression) { - if (expression.eval()) { - return VisitResult.CONTINUE; - } - return VisitResult.SKIP_SUBTREE; + return expression.eval() ? VisitResult.CONTINUE : VisitResult.SKIP_SUBTREE; } }, script); } diff --git a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/util/ClientCompilerTest.java b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/util/ClientCompilerTest.java index f498b9a86..424895e69 100644 --- a/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/util/ClientCompilerTest.java +++ b/archetype/engine-v2/src/test/java/io/helidon/build/archetype/engine/v2/util/ClientCompilerTest.java @@ -16,9 +16,11 @@ package io.helidon.build.archetype.engine.v2.util; import io.helidon.build.archetype.engine.v2.ast.Block; +import io.helidon.build.archetype.engine.v2.ast.ConditionBlock; import io.helidon.build.archetype.engine.v2.ast.Input; import io.helidon.build.archetype.engine.v2.ast.Node; import io.helidon.build.archetype.engine.v2.ast.Node.VisitResult; +import io.helidon.build.archetype.engine.v2.ast.Preset; import io.helidon.build.archetype.engine.v2.ast.Script; import io.helidon.build.archetype.engine.v2.ast.Step; @@ -618,4 +620,63 @@ public VisitResult visitAny(Input input, Void arg) { }, compiledScript); assertThat(index[0], is(12)); } + + @Test + void testCondition() { + Script script = load("compiler/conditions/condition.xml"); + Script compiledScript = ClientCompiler.compile(script, false); + int[] index = new int[]{0}; + walk(new Node.Visitor<>() { + @Override + public VisitResult visitBlock(Block block, Void arg) { + return block.accept(new Block.Visitor<>() { + @Override + public VisitResult visitStep(Step step, Void arg) { + assertThat(++index[0], is(1)); + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitAny(Block block, Void arg) { + switch (block.kind()) { + case SCRIPT: + break; + case INPUTS: + assertThat(++index[0], is(3)); + break; + default: + fail(String.format("Unexpected block: %s, index=%d", block, index[0])); + } + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitInput(Input input, Void arg) { + return input.accept(new Input.Visitor<>() { + @Override + public VisitResult visitBoolean(Input.Boolean input, Void arg) { + assertThat(++index[0], is(4)); + assertThat(input.id(), is("boolean1")); + assertThat(input.name(), is("Boolean1")); + return VisitResult.CONTINUE; + } + + @Override + public VisitResult visitAny(Input input, Void arg) { + fail(String.format("Unexpected input: %s, index=%d", block, index[0])); + return VisitResult.CONTINUE; + } + }, arg); + } + + @Override + public VisitResult visitConditionBlock(ConditionBlock condition, Void arg) { + assertThat(++index[0], is(2)); + return VisitResult.CONTINUE; + } + }, arg); + } + }, compiledScript); + assertThat(index[0], is(4)); + } } diff --git a/archetype/engine-v2/src/test/resources/compiler/conditions/condition.xml b/archetype/engine-v2/src/test/resources/compiler/conditions/condition.xml new file mode 100644 index 000000000..0c2a47cf6 --- /dev/null +++ b/archetype/engine-v2/src/test/resources/compiler/conditions/condition.xml @@ -0,0 +1,45 @@ + + + + + + true + + + + + + + bar + + + + + + + + + + + + + + diff --git a/archetype/engine-v2/src/test/resources/controller/conditional-block.xml b/archetype/engine-v2/src/test/resources/controller/conditional-block.xml index c23579c5b..686f84202 100644 --- a/archetype/engine-v2/src/test/resources/controller/conditional-block.xml +++ b/archetype/engine-v2/src/test/resources/controller/conditional-block.xml @@ -61,20 +61,26 @@ success - + my help - + + + + + + + - + @@ -92,12 +98,12 @@ - dummy + bar - dummy + bar value1 @@ -110,13 +116,13 @@ - dummy + foo - dummy + bar value2 diff --git a/archetype/engine-v2/src/test/resources/loader/conditional-block.xml b/archetype/engine-v2/src/test/resources/loader/conditional-block.xml index 622e2b5ea..94ed8ab8e 100644 --- a/archetype/engine-v2/src/test/resources/loader/conditional-block.xml +++ b/archetype/engine-v2/src/test/resources/loader/conditional-block.xml @@ -94,17 +94,17 @@ value2 - failure + foo - failure + foo - failure + foo value3