diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/AbstractYamlTraversal.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/AbstractYamlTraversal.java new file mode 100644 index 000000000..0e898d747 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/AbstractYamlTraversal.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.yaml.path; + +import java.util.stream.Stream; + +import org.springframework.ide.vscode.commons.yaml.util.Streams; + +/** + * Abstract superclass for implementing concrete {@link YamlTraversal}s. + *

+ * Note that allthoigh this class provides a default implementation for both + * `traverse` and `traverseAmbiguously`, at least one of these methods *must* + * be overridden by the subclass (otherwise the methods will just call eachother + * in a infinite recursion loop). + *

+ * To implement a non-ambiguous traversal, override the `traverse` method. + *

+ * To implement an ambguous traversal, override the `traverseAmbiguously` method + * instead. + * + * @author Kris De Volder + */ +public abstract class AbstractYamlTraversal implements YamlTraversal { + + @Override + public > Stream traverseAmbiguously(T start) { + return Streams.fromNullable(traverse(start)); + } + + @Override + public > T traverse(T startNode) { + return traverseAmbiguously(startNode).findFirst().orElse(null); + } + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/AlternativeYamlTraversal.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/AlternativeYamlTraversal.java new file mode 100644 index 000000000..d7e84d125 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/AlternativeYamlTraversal.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.yaml.path; + +import java.util.stream.Stream; + +import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.yaml.util.Streams; + +public class AlternativeYamlTraversal extends AbstractYamlTraversal { + + private YamlTraversal first; + private YamlTraversal second; + + public AlternativeYamlTraversal(YamlTraversal first, YamlTraversal second) { + Assert.isLegal(!first.isEmpty()); + Assert.isLegal(!second.isEmpty()); + this.first = first; + this.second = second; + } + + @Override + public > Stream traverseAmbiguously(T start) { + return Stream.concat( + first.traverseAmbiguously(start), + second.traverseAmbiguously(start) + ); + } + + @Override + public boolean canEmpty() { + return first.canEmpty() || second.canEmpty(); + } + + @Override + public String toString() { + return "Or("+first+", "+second+")"; + } + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/FilteringTraversal.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/FilteringTraversal.java new file mode 100644 index 000000000..3dcc39bd7 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/FilteringTraversal.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.yaml.path; + +import java.util.stream.Stream; + +/** + * @author Kris De Volder + */ +public class FilteringTraversal extends AbstractYamlTraversal { + + private YamlTraversal yamlTraversal; + private YamlTraversal check; + + public FilteringTraversal(YamlTraversal yamlTraversal, YamlTraversal check) { + this.yamlTraversal = yamlTraversal; + this.check = check; + } + + @Override + public > Stream traverseAmbiguously(T start) { + Stream x = yamlTraversal.traverseAmbiguously(start); + return x.filter(target -> + check.traverseAmbiguously(target) + .findAny() + .isPresent() + ); + } + + @Override + public String toString() { + return "Filter("+yamlTraversal + "has: " + check + ")"; + } + + @Override + public boolean canEmpty() { + return yamlTraversal.canEmpty(); + } + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/RepeatingYamlTraversal.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/RepeatingYamlTraversal.java new file mode 100644 index 000000000..b1b5be312 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/RepeatingYamlTraversal.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.yaml.path; + +import java.util.stream.Stream; + +import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.yaml.util.Streams; + +public class RepeatingYamlTraversal extends AbstractYamlTraversal { + + private YamlTraversal step; + + public RepeatingYamlTraversal(YamlTraversal step) { + Assert.isLegal(!step.canEmpty()); //This implementation is still too simplistic to handle that properly! + // If you hit this assert, then it may be time to make it more sophisticated. + this.step = step; + } + + @Override + public > Stream traverseAmbiguously(T start) { + if (start==null) { + return Stream.empty(); + } else { + return Stream.concat( + Streams.fromNullable(start), + step.traverseAmbiguously(start).flatMap(next -> { + return this.traverseAmbiguously(next); + }) + ); + } + } + + @Override + public String toString() { + return "Repeat("+step+")"; + } + + @Override + public YamlTraversal repeat() { + //don't make 'Repeat(Repeat(...))' + return this; + } + + @Override + public boolean canEmpty() { + return true; + } +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/SequencingYamlTraversal.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/SequencingYamlTraversal.java new file mode 100644 index 000000000..5b691ec68 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/SequencingYamlTraversal.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.yaml.path; + +import java.util.stream.Stream; + +public class SequencingYamlTraversal extends AbstractYamlTraversal { + + private YamlTraversal first; + private YamlTraversal second; + + public SequencingYamlTraversal(YamlTraversal first, YamlTraversal second) { + this.first = first; + this.second = second; + } + + @Override + public > Stream traverseAmbiguously(T start) { + return first.traverseAmbiguously(start) + .flatMap(second::traverseAmbiguously); + } + + @Override + public boolean canEmpty() { + return first.canEmpty() && second.canEmpty(); + } + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java index 7b265b188..ec29323a3 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPath.java @@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.yaml.path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.ide.vscode.commons.yaml.ast.NodeRef; @@ -29,7 +30,7 @@ import reactor.core.publisher.Flux; /** * @author Kris De Volder */ -public class YamlPath { +public class YamlPath extends AbstractYamlTraversal { public static final YamlPath EMPTY = new YamlPath(); private final YamlPathSegment[] segments; @@ -132,34 +133,7 @@ public class YamlPath { return new YamlPath(newPath); } - public Node traverseToNode(YamlFileAST root) { - ASTCursor cursor = traverse(new ASTRootCursor(root)); - if (cursor instanceof NodeCursor) { - return ((NodeCursor)cursor).getNode(); - } - return null; - } - - public > T traverse(T startNode) { - return traverseAmbiguously(startNode).findFirst().orElse(null); - } - - public Stream traverseAmbiguously(YamlFileAST ast) { - if (ast!=null) { - return traverseAmbiguously(new ASTRootCursor(ast)) - .map((ASTCursor cursor) -> (Node)cursor.getNode()); - } - return Stream.empty(); - } - - public Stream traverseAmbiguously(Node startNode) { - if (startNode!=null) { - return traverseAmbiguously(new NodeCursor(startNode)) - .map((ASTCursor cursor) -> (Node)cursor.getNode()); - } - return Stream.empty(); - } - + @Override public > Stream traverseAmbiguously(T startNode) { if (startNode!=null) { Stream result = Stream.of(startNode); @@ -206,6 +180,7 @@ public class YamlPath { } + @Override public boolean isEmpty() { return segments.length==0; } @@ -311,4 +286,31 @@ public class YamlPath { .block(); } + @Override + public YamlTraversal then(YamlTraversal _other) { + if (isEmpty()) { + return _other; + } else if (_other.isEmpty()) { + return this; + } else if (_other instanceof YamlPathSegment) { + return this.append((YamlPathSegment) _other); + } else if (_other instanceof YamlPath) { + YamlPath other = (YamlPath) _other; + return new YamlPath( + Stream.concat( + Arrays.stream(this.segments), + Arrays.stream(other.segments) + ).toArray(sz -> new YamlPathSegment[sz]) + ); + } else { + return new SequencingYamlTraversal(this, _other); + } + } + + @Override + public boolean canEmpty() { + //The empty path is the only one that 'canEmpty' since any step in path moves the cursor. + return isEmpty(); + } + } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java index e0c676077..b119c11c1 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlPathSegment.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.yaml.path; +import java.util.stream.Stream; + /** * A YamlPathSegment is a 'primitive' NodeNavigator operation. * More complex operations (i.e {@link YamlPath}) are composed as seqences @@ -17,7 +19,7 @@ package org.springframework.ide.vscode.commons.yaml.path; * * @author Kris De Volder */ -public abstract class YamlPathSegment { +public abstract class YamlPathSegment extends AbstractYamlTraversal { public static YamlPathSegment decode(String code) { switch (code.charAt(0)) { @@ -34,6 +36,11 @@ public abstract class YamlPathSegment { } } + @Override + public > Stream traverseAmbiguously(T start) { + return start.traverseAmbiguously(this); + } + public static enum YamlPathSegmentType { VAL_AT_KEY, //Go to value associate with given key in a map. KEY_AT_KEY, //Go to the key node associated with a given key in a map. @@ -43,7 +50,7 @@ public abstract class YamlPathSegment { public static class AnyChild extends YamlPathSegment { - private static AnyChild INSTANCE = new AnyChild(); + static AnyChild INSTANCE = new AnyChild(); private AnyChild() {} @@ -76,6 +83,7 @@ public abstract class YamlPathSegment { protected String getValueCode() { return ""; } + } public static class AtIndex extends YamlPathSegment { @@ -226,6 +234,12 @@ public abstract class YamlPathSegment { } + @Override + public boolean canEmpty() { + //All path segments implement a real 'one step' movement, + return false; + } + @Override public String toString() { return toNavString(); @@ -255,7 +269,23 @@ public abstract class YamlPathSegment { return getTypeCode() + getValueCode(); } + protected abstract String getValueCode(); protected abstract char getTypeCode(); + + @Override + public YamlTraversal then(YamlTraversal other) { + //Overriding the `then` method to try to compress sequences of segments into YamlPath instead of deeply nested + if (other.isEmpty()) { + return this; + } else if (other instanceof YamlPathSegment) { + return new YamlPath(this, (YamlPathSegment)other); + } else if (other instanceof YamlPath) { + return ((YamlPath) other).prepend(this); + } else { + return new SequencingYamlTraversal(this, other); + } + } + } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlTraversal.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlTraversal.java new file mode 100644 index 000000000..b806d0222 --- /dev/null +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/path/YamlTraversal.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.yaml.path; + +import java.util.stream.Stream; + +import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; +import org.yaml.snakeyaml.nodes.Node; + +public interface YamlTraversal { + + //////////////////////////////////////////////// + /// Methods for performing traversals : + + /** + * This is the essence of a traversal. Any concrete traversal must provide + * some way to be applied to a starting point and return a stream of + * endpoints. If a traversal is non-ambiguous the stream contains at + * most one element. + *

+ * If traversal is deterministic, the stream will contain at most one element. + *

+ * If traversal has potential ambiguity then the stream may contain more than + * one element. Each of the elements in the stream is an alternate + * place that this traversal could end-up in. + */ + > Stream traverseAmbiguously(T start); + + /** + * Performs a traversal and silently drops all but one of the endpoints. + *

+ * This is a convenience method for unambiguous traversals, or for when the caller + * doesn't care about precisely which one of the possible alternate end-points + * they might get. + */ + > T traverse(T startNode); + + default Node traverseToNode(YamlFileAST root) { + ASTCursor cursor = traverse(new ASTRootCursor(root)); + if (cursor instanceof NodeCursor) { + return ((NodeCursor)cursor).getNode(); + } + return null; + } + + default Stream traverseAmbiguously(YamlFileAST ast) { + if (ast!=null) { + return traverseAmbiguously(new ASTRootCursor(ast)) + .filter(cursor -> cursor.getNode() instanceof Node) + .map((ASTCursor cursor) -> (Node)cursor.getNode()); + } + return Stream.empty(); + } + + default Stream traverseAmbiguously(Node startNode) { + if (startNode!=null) { + return traverseAmbiguously(new NodeCursor(startNode)) + .map((ASTCursor cursor) -> (Node)cursor.getNode()); + } + return Stream.empty(); + } + + ///////////////////////////////////////////////// + // Creating/composing traversals + + YamlTraversal EMPTY = YamlPath.EMPTY; + + default YamlTraversal then(YamlTraversal other) { + if (this.isEmpty()) { + return other; + } else if (other.isEmpty()) { + return this; + } + return new SequencingYamlTraversal(this, other); + } + + default YamlTraversal thenValAt(int index) { + return then(YamlPathSegment.valueAt(index)); + } + default YamlTraversal thenValAt(String key) { + return then(YamlPathSegment.valueAt(key)); + } + default YamlTraversal thenKeyAt(String key) { + return then(YamlPathSegment.keyAt(key)); + } + default YamlTraversal thenAnyChild() { + return then(YamlPathSegment.anyChild()); + } + default YamlTraversal or(YamlTraversal other) { + if (this.isEmpty()) { + return other; + } else if (other.isEmpty()) { + return this; + } else { + return new AlternativeYamlTraversal(this, other); + } + } + + default YamlTraversal repeatAtLeast(int howMany) { + if (isEmpty()) { + return this; + } else if (howMany>0) { + return this.then(this.repeatAtLeast(howMany-1)); + } else { + return this.repeat(); + } + } + + default YamlTraversal repeat() { + return new RepeatingYamlTraversal(this); + } + + /** + * Filters the end-points of a traversal, retaining only those + * for which the `check` traversal starting at the end-point + * leads somewhere. + */ + default YamlTraversal has(YamlTraversal check) { + if (this.isEmpty()) { + return this; // don't bother filtering empty! + } + return new FilteringTraversal(this, check); + } + + + ////////////////////////////////////////////////////// + // Computing information about the traversal's nature + + /** + * Returns true if the traversal has only one possible end-point, equal + * to its starting point. In other words the traversal does nothing. + */ + default boolean isEmpty() { + return false; + } + + /** + * Returns true if the traversal might include the starting point in its end points. + *

+ * Note this is not the same as 'isEmpty', though 'isEmpty' implies 'canEmpty'. + *

+ * If this returns null, it should be taken to mean 'unknown'. This is used for + * cases where analyzing the traversal can not predict for certain if the traversal + * allows a 'no movement' step. + */ + boolean canEmpty(); + +} diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ITypeCollector.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ITypeCollector.java index b74c3b526..ec3e1301b 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ITypeCollector.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/ITypeCollector.java @@ -15,7 +15,7 @@ import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.yaml.snakeyaml.nodes.Node; /** - * A type collector can optionally be added to a {@link YamlASTReconciler}. + * A type collector can optionally be added to a {@link SchemaBasedYamlASTReconciler}. * It is notified of the types the reconciler infers for * any AST nodes it visits during reconciling. * diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java index ebd1b558f..0e4171994 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java @@ -124,6 +124,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { if (typeCollector!=null) { typeCollector.accept(node, type); } + checkConstraints(parent, node, type, schemaContext); switch (getNodeId(node)) { case mapping: MappingNode map = (MappingNode) node; @@ -250,12 +251,14 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { } problems.accept(YamlSchemaProblems.missingProperties(message, dc, missingProps, parent, map, quickfixes.MISSING_PROP_FIX)); } + } + } - //Check for other constraints attached to the type - for (Constraint constraint : typeUtil.getConstraints(type)) { - if (constraint!=null) { - constraint.verify(dc, parent, map, type, problems); - } + protected void checkConstraints(Node parent, Node node, YType type, DynamicSchemaContext dc) { + //Check for other constraints attached to the type + for (Constraint constraint : typeUtil.getConstraints(type)) { + if (constraint!=null) { + constraint.verify(dc, parent, node, type, problems); } } } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java index eabc17ef0..57b8a3cf5 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java @@ -33,6 +33,7 @@ import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.ValueParser; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraints; @@ -214,10 +215,9 @@ public class YTypeFactory { private SchemaContextAware parser; private List propertyList = new ArrayList<>(); - private final List hints = new ArrayList<>(); + private List hints = new ArrayList<>(); private Map cachedPropertyMap; private SchemaContextAware>> hintProvider; - private List constraints = new ArrayList<>(2); public boolean isSequenceable() { @@ -357,8 +357,9 @@ public class YTypeFactory { return parser == null ? null : parser.withContext(dc); } - public void require(Constraint dynamicConstraint) { + public AbstractType require(Constraint dynamicConstraint) { this.constraints.add(dynamicConstraint); + return this; } public void requireOneOf(String... properties) { @@ -645,7 +646,7 @@ public class YTypeFactory { return getPrimaryProps(); } - private synchronized Map typesByPrimary() { + public synchronized Map typesByPrimary() { if (typesByPrimary==null) { //To ensure that the map of 'typesByPrimary' is never stale, make the list of // types immutable at this point. The assumption here is that union can be diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java index 844a9f4b9..ceb2a86b8 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/structure/YamlStructureParser.java @@ -395,7 +395,7 @@ public class YamlStructureParser { if (index!=null) { List cs = getChildren(); if (index>=0 && index Stream of(T e) { + public static Stream fromNullable(T e) { return e==null ? Stream.empty() : Stream.of(e); } diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java index 3eb272045..ffb45787c 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseModel.java @@ -11,11 +11,13 @@ package org.springframework.ide.vscode.concourse; import static org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.anyChild; +import static org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.keyAt; import static org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.valueAt; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map.Entry; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -23,6 +25,7 @@ import java.util.stream.Collectors; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SnippetBuilder; +import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; @@ -33,14 +36,17 @@ import org.springframework.ide.vscode.commons.yaml.path.ASTRootCursor; import org.springframework.ide.vscode.commons.yaml.path.NodeCursor; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment; +import org.springframework.ide.vscode.commons.yaml.path.YamlTraversal; +import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.schema.BasicYValueHint; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanUnionType; import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; import org.springframework.ide.vscode.commons.yaml.schema.YValueHint; -import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; +import org.springframework.ide.vscode.commons.yaml.util.Streams; import org.springframework.ide.vscode.concourse.util.CollectorUtil; import org.springframework.ide.vscode.concourse.util.StaleFallbackCache; import org.yaml.snakeyaml.Yaml; @@ -60,13 +66,92 @@ import com.google.common.collect.Multiset; */ public class ConcourseModel { -// /** -// * Verification of contraint: a job used in the 'passed' attribute of a step -// * must interact with the resource in question. -// */ -// public Constraint jobHasInteractionWithResource() { -// return (IDocument doc, Node parent, Node node, YType type, Set foundProps, IProblemCollector problems) -> { -// } + /** + * Verification of contraint: a job used in the 'passed' attribute of a step + * must interact with the resource in question. + */ + public final void passedJobHasInteractionWithResource(DynamicSchemaContext dc, Node parent, Node node, YType type, IProblemCollector problems) { + YamlPath path = dc.getPath(); + // Expecting a path like this: YamlPath([0], .jobs, [1], .plan, [0], .passed, [0]) + path = path.dropLast(); + if (YamlPathSegment.valueAt("passed").equals(path.getLastSegment())) { + String jobName = NodeUtil.asScalar(node); + JobModel job = getJob(dc.getDocument(), jobName); + if (job!=null) { + //Only check if the job exists. Otherwise the extra checks will show 'redundant' errors (e.g. + // complaining that 'some-job' doesn't ineract with a resource (because the resource doesn't exist). + YamlFileAST root = this.getSafeAst(dc.getDocument()); + if (root!=null) { + Node stepNode = path.dropLast().traverseToNode(root); + if (stepNode!=null) { + StepModel step = newStep(stepNode); + String resourceName = step.getResourceName(); + if (resourceName!=null) { + Set interactions = job.getInteractedResources(); + if (interactions!=null && !interactions.contains(resourceName)) { + problems.accept(YamlSchemaProblems.schemaProblem("Job '"+jobName+"' does not interact with resource '"+resourceName+"'", node)); + } + } + } + } + } + } + } + + /** + * Get the job with given name. If there is no such job, or if there is more than one, this will return null. + */ + private JobModel getJob(IDocument doc, String jobName) { + List jobs = getFromAst(doc, ast -> + JOBS_PATH.traverseAmbiguously(ast) + .filter(node -> jobName.equals(NodeUtil.getScalarProperty(node, "name"))) + .map(JobModel::new) + ) + .limit(2) //We only need 2 elements at most to determine if there is more than one + .collect(Collectors.toList()); + return jobs.size()==1 + ? jobs.get(0) + : null; + } + + public StepModel newStep(Node _node) { + MappingNode node = (MappingNode) _node; + Set keys = NodeUtil.getScalarKeys(node); + for (Entry primary : stepType.typesByPrimary().entrySet()) { + String stepType = primary.getKey(); + if (keys.contains(primary.getKey())) { + return new StepModel(stepType, node); + } + } + throw new IllegalArgumentException("Node does not look like step node: "+node); + } + + private static final YamlTraversal JobModel_GET_PUT_STEP_PATH = new YamlPath() + .then(valueAt("plan")) + .then(anyChild().repeatAtLeast(1)) + .has(keyAt("get").or(keyAt("put"))); + + /** + * Wraps around a Node in the AST that represents a 'job' and + * provides methods for accessing information from the node. + */ + public class JobModel { + + private Node node; + + JobModel(Node node) { + this.node = node; + } + + public Set getInteractedResources() { + return JobModel_GET_PUT_STEP_PATH + .traverseAmbiguously(node) + .map(node -> newStep(node)) + .flatMap(step -> Streams.fromNullable(step.getResourceName())) + .collect(Collectors.toSet()); + } + + } /** * Wraps around a Node in the AST that represents a 'step' and @@ -83,6 +168,7 @@ public class ConcourseModel { } public Node getResourceNameNode() { + Assert.isLegal("put".equals(stepType) || "get".equals(stepType)); Node node = NodeUtil.getProperty(step, "resource"); return node!=null ? node : NodeUtil.getProperty(step, stepType); } @@ -111,6 +197,12 @@ public class ConcourseModel { } + public static final YamlPath JOBS_PATH = new YamlPath( + anyChild(), + valueAt("jobs"), + anyChild() + ); + public static final YamlPath JOB_NAMES_PATH = new YamlPath( anyChild(), valueAt("jobs"), @@ -144,6 +236,8 @@ public class ConcourseModel { private final Supplier snippetBuilderFactory; + private YBeanUnionType stepType; + public ConcourseModel(SimpleLanguageServer languageServer) { Yaml yaml = new Yaml(); this.parser = new YamlParser(yaml); @@ -326,12 +420,13 @@ public class ConcourseModel { return astTypes; } - public StepModel newStep(String stepType, MappingNode stepNode) { - return new StepModel(stepType, stepNode); - } - public void setResourceTypeRegistry(ResourceTypeRegistry resourceTypes) { this.resourceTypes = resourceTypes; } + public void setStepType(YBeanUnionType step) { + Assert.isNull("stepType already set", this.stepType); + this.stepType = step; + } + } diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index 6b8a99d96..18c396690 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -102,7 +102,7 @@ public class PipelineYmlSchema implements YamlSchema { .parseWith(ValueParsers.integerAtLeast(1)); public final YAtomicType t_resource_name; - public final YAtomicType t_job_name; + public final AbstractType t_job_name; public final YAtomicType t_resource_type_name; public final YType t_mime_type = f.yatomic("MimeType") .parseWith(ValueParsers.NE_STRING) @@ -189,7 +189,7 @@ public class PipelineYmlSchema implements YamlSchema { (DynamicSchemaContext dc) -> { return models.getJobNames(dc); } - ); + ).require(models::passedJobHasInteractionWithResource); YAtomicType resourceNameDef = f.yatomic("Resource Name"); resourceNameDef.parseWith(ConcourseValueParsers.resourceNameDef(models)); @@ -290,7 +290,7 @@ public class PipelineYmlSchema implements YamlSchema { putStep.require((DynamicSchemaContext dc, Node parent, Node _map, YType type, IProblemCollector problems) -> { if (_map instanceof MappingNode) { MappingNode map = (MappingNode) _map; - StepModel step = models.newStep("put", map); + StepModel step = models.newStep(map); String resourceName = step.getResourceName(); if (resourceName!=null) { ResourceModel resource = models.getResource(dc.getDocument(), resourceName); @@ -342,6 +342,7 @@ public class PipelineYmlSchema implements YamlSchema { addProp(step, subStep, "tags", t_strings); addProp(step, subStep, "timeout", t_duration); } + models.setStepType(step); AbstractType job = f.ybean("Job"); addProp(job, "name", jobNameDef).isRequired(true); diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index b0d234421..5685cc09c 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -651,8 +651,8 @@ public class ConcourseEditorTest { { List problems = editor.assertProblems( "config|Only one of [config, file]", - "config|[platform, run] are required", "config|One of [image_resource, image]", + "config|[platform, run] are required", "file|Only one of [config, file]" ); //All of the problems in this example are property contraint violations! So all should be warnings. @@ -1615,8 +1615,8 @@ public class ConcourseEditorTest { " access_key_id: the-key" ); editor.assertProblems( - "source|'bucket' is required", - "source|One of [regexp, versioned_file] is required" + "source|One of [regexp, versioned_file] is required", + "source|'bucket' is required" ); editor = harness.newEditor( @@ -3116,7 +3116,7 @@ public class ConcourseEditorTest { ); } - @Ignore @Test public void reconcilerJobFromPassedAttributeMustInteractWithResource() throws Exception { + @Test public void reconcilerJobFromPassedAttributeMustInteractWithResource() throws Exception { Editor editor; editor = harness.newEditor( @@ -3141,7 +3141,7 @@ public class ConcourseEditorTest { " - build-it # <- bad\n" ); editor.assertProblems( - "build-it^ # <- bad|Job 'build-it' doesn't interact with the resource 'version'" + "build-it^ # <- bad|Job 'build-it' does not interact with resource 'version'" ); editor = harness.newEditor( @@ -3166,7 +3166,7 @@ public class ConcourseEditorTest { " - build-it # <- good\n" ); editor.assertProblems( - "build-it^ # <- bad|Job 'build-it' doesn't interact with the resource 'source-repo'" + "build-it^ # <- bad|Job 'build-it' does not interact with resource 'source-repo'" ); //Check that we find interactions in steps that are at the top-level of the plan: