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: