Concourse: reconcile checking for non-existent resources

This commit is contained in:
Kris De Volder
2016-12-20 16:44:25 -08:00
parent 998993cdaf
commit 7442db205d
24 changed files with 574 additions and 176 deletions

View File

@@ -11,7 +11,6 @@
package org.springframework.ide.vscode.commons.util;
import java.util.Collection;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
@@ -23,7 +22,8 @@ import com.google.common.collect.ImmutableSet;
public class EnumValueParser implements ValueParser {
private String typeName;
private Set<String> values;
private Collection<String> values;
public EnumValueParser(String typeName, String... values) {
this(typeName, ImmutableSet.copyOf(values));
@@ -31,15 +31,21 @@ public class EnumValueParser implements ValueParser {
public EnumValueParser(String typeName, Collection<String> values) {
this.typeName = typeName;
this.values = ImmutableSet.copyOf(values);
this.values = values;
}
public Object parse(String str) {
if (values.contains(str)) {
Collection<String> values = this.values;
//If values is not known (null) then just assume the str is acceptable.
if (values==null || values.contains(str)) {
return str;
} else {
throw new IllegalArgumentException("'"+str+"' is not valid for Enum '"+typeName+"'. Valid values are: "+values);
throw new IllegalArgumentException(createErrorMessage(str, values));
}
}
protected String createErrorMessage(String parseString, Collection<String> values2) {
return "'"+parseString+"' is not valid for Enum '"+typeName+"'. Valid values are: "+values;
}
}

View File

@@ -3,6 +3,7 @@ package org.springframework.ide.vscode.commons.util;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
/**
* Utility methods to convert exceptions into other types of exceptions, status
@@ -65,4 +66,14 @@ public class ExceptionUtil {
return dump.toString();
}
/**
* Convert throwables into exception try not to wrap if not needing to.
*/
public static Exception exception(Throwable cause) {
if (cause instanceof Exception) {
return (Exception)cause;
}
return new ExecutionException(cause);
}
}

View File

@@ -1,8 +1,10 @@
package org.springframework.ide.vscode.commons.yaml.ast;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeId;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.SequenceNode;
/**
* @author Kris De Volder
@@ -47,4 +49,18 @@ public class NodeUtil {
return null;
}
public static MappingNode asMapping(Node node) {
if (node!=null && node.getNodeId()==NodeId.mapping) {
return (MappingNode) node;
}
return null;
}
public static SequenceNode asSequence(Node node) {
if (node!=null && node.getNodeId()==NodeId.sequence) {
return (SequenceNode) node;
}
return null;
}
}

View File

@@ -9,6 +9,7 @@ import java.util.List;
import org.springframework.ide.vscode.commons.util.Collector;
import org.springframework.ide.vscode.commons.util.IRequestor;
import org.springframework.ide.vscode.commons.util.RememberLast;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.ast.NodeRef.RootRef;
import org.springframework.ide.vscode.commons.yaml.ast.NodeRef.SeqRef;
import org.springframework.ide.vscode.commons.yaml.ast.NodeRef.TupleKeyRef;
@@ -25,9 +26,11 @@ import org.yaml.snakeyaml.nodes.SequenceNode;
public class YamlFileAST {
private static final List<NodeRef<?>> NO_CHILDREN = Collections.emptyList();
private List<Node> nodes;
private final List<Node> nodes;
private final IDocument doc;
public YamlFileAST(Iterable<Node> iter) {
public YamlFileAST(IDocument doc, Iterable<Node> iter) {
this.doc = doc;
nodes = new ArrayList<Node>();
for (Node node : iter) {
nodes.add(node);
@@ -141,5 +144,9 @@ public class YamlFileAST {
nodes.set(index, value);
}
public IDocument getDocument() {
return doc;
}
}

View File

@@ -17,7 +17,7 @@ public class YamlParser implements YamlASTProvider {
public YamlFileAST getAST(IDocument doc) throws Exception {
CharSequenceReader reader = new CharSequenceReader();
reader.setInput(doc.get());
return new YamlFileAST(yaml.composeAll(reader));
return new YamlFileAST(doc, yaml.composeAll(reader));
}
}

View File

@@ -0,0 +1,94 @@
/*******************************************************************************
* Copyright (c) 2016 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.ast.NodeUtil;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.KeyAtKey;
import org.yaml.snakeyaml.nodes.CollectionNode;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.SequenceNode;
/**
* Pointer to a specific {@link Node} in Snake yaml parse tree. Supports navigation
* using {@link YamlPath}s, including support for 'ambiguous' steps like
* {@link YamlPathSegment}.anyChild()
*
* @author Kris De Volder
*/
public class NodeCursor implements YamlNavigable<NodeCursor> {
private final Node currentNode;
public NodeCursor(Node node) {
Assert.isNotNull(node);
this.currentNode = node;
}
@Override
public Stream<NodeCursor> traverseAmbiguously(YamlPathSegment s) {
switch (s.getType()) {
case KEY_AT_KEY: {
String key = s.toPropString();
MappingNode mappingNode = NodeUtil.asMapping(getNode());
if (mappingNode!=null) {
return mappingNode.getValue().stream()
.filter((c) -> key.equals(NodeUtil.asScalar(c.getKeyNode())))
.map((c) -> new NodeCursor(c.getKeyNode()));
}
return Stream.empty();
}
case ANY_CHILD: {
MappingNode mappingNode = NodeUtil.asMapping(getNode());
if (mappingNode!=null) {
return mappingNode.getValue().stream()
.map((c) -> new NodeCursor(c.getValueNode()));
}
SequenceNode sequenceNode = NodeUtil.asSequence(getNode());
if (sequenceNode!=null) {
return sequenceNode.getValue().stream().map(NodeCursor::new);
}
return Stream.empty();
}
case VAL_AT_INDEX: {
SequenceNode seq = NodeUtil.asSequence(getNode());
int index = s.toIndex();
int size = seq.getValue().size();
if (index<size && index >= 0) {
return Stream.of(new NodeCursor(seq.getValue().get(index)));
}
return Stream.empty();
}
case VAL_AT_KEY: {
MappingNode mappingNode = NodeUtil.asMapping(getNode());
if (mappingNode!=null) {
String key = s.toPropString();
return mappingNode.getValue().stream()
.filter((c) -> key.equals(NodeUtil.asScalar(c.getKeyNode())))
.map((c) -> new NodeCursor(c.getValueNode()));
}
return Stream.empty();
}
default:
Assert.isLegal(false, "Bug? Missing switch case?");
return Stream.empty();
}
}
public Node getNode() {
return currentNode;
}
}

View File

@@ -21,6 +21,7 @@ import org.springframework.ide.vscode.commons.yaml.ast.NodeRef.RootRef;
import org.springframework.ide.vscode.commons.yaml.ast.NodeRef.SeqRef;
import org.springframework.ide.vscode.commons.yaml.ast.NodeRef.TupleValueRef;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.YamlPathSegmentType;
import org.yaml.snakeyaml.nodes.Node;
/**
* @author Kris De Volder
@@ -125,6 +126,15 @@ public class YamlPath {
return traverseAmbiguously(startNode).findFirst().orElse(null);
}
public Stream<Node> traverseAmbiguously(Node startNode) {
if (startNode!=null) {
return traverseAmbiguously(new NodeCursor(startNode))
.map(NodeCursor::getNode);
}
return Stream.empty();
}
public <T extends YamlNavigable<T>> Stream<T> traverseAmbiguously(T startNode) {
if (startNode!=null) {
Stream<T> result = Stream.of(startNode);

View File

@@ -22,7 +22,35 @@ public abstract class YamlPathSegment {
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.
VAL_AT_INDEX //Go to value associate with given index in a sequence
VAL_AT_INDEX, //Go to value associate with given index in a sequence
ANY_CHILD // Go to any child (assumes you are using ambiguous traversal method, otherwise this is probably not very useful)
}
public static class AnyChild extends YamlPathSegment {
private static AnyChild INSTANCE = new AnyChild();
private AnyChild() {}
@Override
public String toNavString() {
return ".*";
}
@Override
public String toPropString() {
return "*";
}
@Override
public Integer toIndex() {
return null;
}
@Override
public YamlPathSegmentType getType() {
return YamlPathSegmentType.ANY_CHILD;
};
}
public static class AtIndex extends YamlPathSegment {
@@ -133,7 +161,7 @@ public abstract class YamlPathSegment {
}
}
private static class KeyAtKey extends ValAtKey {
public static class KeyAtKey extends ValAtKey {
public KeyAtKey(String key) {
super(key);
@@ -144,6 +172,7 @@ public abstract class YamlPathSegment {
return YamlPathSegmentType.KEY_AT_KEY;
}
}
public String toString() {
@@ -165,5 +194,9 @@ public abstract class YamlPathSegment {
public static YamlPathSegment keyAt(String key) {
return new KeyAtKey(key);
}
public static YamlPathSegment anyChild() {
return AnyChild.INSTANCE;
}
}

View File

@@ -8,6 +8,7 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemC
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.ValueParser;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
import org.springframework.ide.vscode.commons.yaml.schema.ASTDynamicSchemaContext;
@@ -39,23 +40,23 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
List<Node> nodes = ast.getNodes();
if (nodes!=null && !nodes.isEmpty()) {
for (Node node : nodes) {
reconcile(node, schema.getTopLevelType());
reconcile(ast.getDocument(), node, schema.getTopLevelType());
}
}
}
private void reconcile(Node node, YType type) {
private void reconcile(IDocument doc, Node node, YType type) {
if (type!=null) {
DynamicSchemaContext schemaContext = new ASTDynamicSchemaContext(doc, node);
switch (node.getNodeId()) {
case mapping:
MappingNode map = (MappingNode) node;
if (typeUtil.isMap(type)) {
for (NodeTuple entry : map.getValue()) {
reconcile(entry.getKeyNode(), typeUtil.getKeyType(type));
reconcile(entry.getValueNode(), typeUtil.getDomainType(type));
reconcile(doc, entry.getKeyNode(), typeUtil.getKeyType(type));
reconcile(doc, entry.getValueNode(), typeUtil.getDomainType(type));
}
} else if (typeUtil.isBean(type)) {
DynamicSchemaContext schemaContext = new ASTDynamicSchemaContext(map);
Map<String, YTypedProperty> beanProperties = typeUtil.getPropertiesMap(type, schemaContext);
for (NodeTuple entry : map.getValue()) {
Node keyNode = entry.getKeyNode();
@@ -68,7 +69,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
type = typeUtil.inferMoreSpecificType(type, schemaContext);
unknownBeanProperty(keyNode, type, key);
} else {
reconcile(entry.getValueNode(), prop.getType());
reconcile(doc, entry.getValueNode(), prop.getType());
}
}
}
@@ -80,7 +81,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
SequenceNode seq = (SequenceNode) node;
if (typeUtil.isSequencable(type)) {
for (Node el : seq.getValue()) {
reconcile(el, typeUtil.getDomainType(type));
reconcile(doc, el, typeUtil.getDomainType(type));
}
} else {
expectTypeButFoundSequence(type, node);
@@ -88,7 +89,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
break;
case scalar:
if (typeUtil.isAtomic(type)) {
ValueParser parser = typeUtil.getValueParser(type);
ValueParser parser = typeUtil.getValueParser(type, schemaContext);
if (parser!=null) {
try {
parser.parse(NodeUtil.asScalar(node));
@@ -108,11 +109,10 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
}
private void valueParseError(YType type, Node node, String parseErrorMsg) {
String msg= "Couldn't parse as '"+describe(type)+"'";
if (StringUtil.hasText(parseErrorMsg)) {
msg += " ("+parseErrorMsg+")";
if (!StringUtil.hasText(parseErrorMsg)) {
parseErrorMsg= "Couldn't parse as '"+describe(type)+"'";
}
problem(node, msg);
problem(node, parseErrorMsg);
}
private void unknownBeanProperty(Node keyNode, YType type, String name) {

View File

@@ -13,8 +13,10 @@ package org.springframework.ide.vscode.commons.yaml.schema;
import java.util.Collections;
import java.util.Set;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
import com.google.common.collect.ImmutableSet;
@@ -28,9 +30,19 @@ import com.google.common.collect.ImmutableSet;
public class ASTDynamicSchemaContext extends CachingSchemaContext {
private MappingNode mapNode;
private IDocument doc;
public ASTDynamicSchemaContext(MappingNode map) {
this.mapNode = map;
public ASTDynamicSchemaContext(IDocument doc, Node node) {
this.doc = doc;
this.mapNode = as(MappingNode.class, node);
}
@SuppressWarnings("unchecked")
private <T> T as(Class<T> klass, Node node) {
if (node!=null && klass.isInstance(node)) {
return (T) node;
}
return null;
}
@Override
@@ -47,4 +59,9 @@ public class ASTDynamicSchemaContext extends CachingSchemaContext {
}
return Collections.emptySet();
}
@Override
public IDocument getDocument() {
return doc;
}
}

View File

@@ -12,6 +12,8 @@ package org.springframework.ide.vscode.commons.yaml.schema;
import java.util.Set;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import com.google.common.collect.ImmutableSet;
/**
@@ -31,6 +33,11 @@ public interface DynamicSchemaContext {
public Set<String> getDefinedProperties() {
return ImmutableSet.of();
}
@Override
public IDocument getDocument() {
return null;
}
};
/**
@@ -45,6 +52,12 @@ public interface DynamicSchemaContext {
* properties are defined in the surrounding object.
*/
Set<String> getDefinedProperties();
/**
* Returns the IDocument the current context is in. This allows for some 'schemas' to have
* arbitrarily complex analysis of anyhting in the IDocument or even documents related
* to it based on its uri.
*/
IDocument getDocument();
}

View File

@@ -17,6 +17,7 @@ import java.util.Set;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SChildBearingNode;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SKeyNode;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode;
@@ -55,5 +56,10 @@ public class SNodeDynamicSchemaContext extends CachingSchemaContext {
return Collections.emptySet();
}
@Override
public IDocument getDocument() {
return contextNode.getDocument();
}
}

View File

@@ -0,0 +1,22 @@
/*******************************************************************************
* Copyright (c) 2016 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.schema;
/**
* Interface that can be implemented by something producing another
* component (of some type `T`) where the returned component needs to
* configured with a {@link DynamicSchemaContext}.
*
* @author Kris De Volder
*/
public interface SchemaContextAware<T> {
T withContext(DynamicSchemaContext dc);
}

View File

@@ -20,6 +20,8 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import javax.inject.Provider;
@@ -120,8 +122,8 @@ public class YTypeFactory {
}
@Override
public ValueParser getValueParser(YType type) {
return ((AbstractType)type).getParser();
public ValueParser getValueParser(YType type, DynamicSchemaContext dc) {
return ((AbstractType)type).getParser(dc);
}
@Override
@@ -137,7 +139,7 @@ public class YTypeFactory {
*/
public static abstract class AbstractType implements YType {
private ValueParser parser;
private SchemaContextAware<ValueParser> parser;
private List<YTypedProperty> propertyList = new ArrayList<>();
private final List<YValueHint> hints = new ArrayList<>();
private Map<String, YTypedProperty> cachedPropertyMap;
@@ -246,11 +248,15 @@ public class YTypeFactory {
}
}
public void parseWith(ValueParser parser) {
public void parseWith(SchemaContextAware<ValueParser> parser) {
this.parser = parser;
}
public ValueParser getParser() {
return parser;
public void parseWith(ValueParser parser) {
parseWith((DynamicSchemaContext dc) -> parser);
}
private ValueParser getParser(DynamicSchemaContext dc) {
return parser == null ? null : parser.withContext(dc);
}
}
@@ -577,6 +583,28 @@ public class YTypeFactory {
return new YTypedPropertyImpl(name, type);
}
public YAtomicType yenum(String name, BiFunction<String, Collection<String>, String> errorMessageFormatter, SchemaContextAware<Collection<String>> values) {
YAtomicType t = yatomic(name);
t.addHintProvider(() -> {
Collection<String> strings = values.withContext(DynamicSchemaContext.NULL); //TODO: make this really context aware!
return strings==null
? null
: strings.stream()
.map((s) -> new BasicYValueHint(s))
.collect(Collectors.toSet());
});
t.parseWith((DynamicSchemaContext dc) -> {
EnumValueParser enumParser = new EnumValueParser(name, values.withContext(dc)) {
@Override
protected String createErrorMessage(String parseString, Collection<String> values) {
return errorMessageFormatter.apply(parseString, values);
}
};
return enumParser;
});
return t;
}
public YAtomicType yenum(String name, String... values) {
YAtomicType t = yatomic(name);
t.addHints(values);

View File

@@ -32,7 +32,7 @@ public interface YTypeUtil {
YValueHint[] getHintValues(YType yType);
String niceTypeName(YType type);
YType getKeyType(YType type);
ValueParser getValueParser(YType type);
ValueParser getValueParser(YType type, DynamicSchemaContext dc);
//TODO: only one of these two should be enough?
List<YTypedProperty> getProperties(YType type, DynamicSchemaContext dc);
@@ -44,5 +44,5 @@ public interface YTypeUtil {
* present in the context to narrow the type, then the type itself
* should be returned.
*/
YType inferMoreSpecificType(YType type, DynamicSchemaContext schemaContext);
YType inferMoreSpecificType(YType type, DynamicSchemaContext dc);
}

View File

@@ -15,6 +15,7 @@ import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.Log;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.util.text.IRegion;
import org.springframework.ide.vscode.commons.yaml.path.KeyAliases;
import org.springframework.ide.vscode.commons.yaml.path.YamlNavigable;
@@ -257,6 +258,10 @@ public class YamlStructureParser {
return getStart()<=offset && offset<=getTreeEnd();
}
public IDocument getDocument() {
return doc.getDocument();
}
public String getText() throws Exception {
return doc.textBetween(start, end);
}

View File

@@ -1,3 +1,13 @@
/*******************************************************************************
* Copyright (c) 2016 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.concourse;
import org.eclipse.lsp4j.CompletionOptions;
@@ -25,22 +35,20 @@ import org.yaml.snakeyaml.Yaml;
public class ConcourseLanguageServer extends SimpleLanguageServer {
private Yaml yaml = new Yaml();
private YamlSchema schema = new PipelineYmlSchema();
public ConcourseLanguageServer() {
SimpleTextDocumentService documents = getTextDocumentService();
YamlASTProvider parser = new YamlParser(yaml);
ConcourseModel models = new ConcourseModel(documents);
YamlASTProvider currentAsts = models.getAstProvider(false);
YamlStructureProvider structureProvider = YamlStructureProvider.DEFAULT;
YamlSchema schema = new PipelineYmlSchema(models);
YamlAssistContextProvider contextProvider = new SchemaBasedYamlAssistContextProvider(schema);
YamlCompletionEngine yamlCompletionEngine = new YamlCompletionEngine(structureProvider, contextProvider);
VscodeCompletionEngine completionEngine = new VscodeCompletionEngineAdapter(this, yamlCompletionEngine);
HoverInfoProvider infoProvider = new YamlHoverInfoProvider(parser, structureProvider, contextProvider);
HoverInfoProvider infoProvider = new YamlHoverInfoProvider(currentAsts, structureProvider, contextProvider);
VscodeHoverEngine hoverEngine = new VscodeHoverEngineAdapter(this, infoProvider);
IReconcileEngine engine = new YamlSchemaBasedReconcileEngine(parser, schema);
IReconcileEngine engine = new YamlSchemaBasedReconcileEngine(currentAsts, schema);
// SimpleWorkspaceService workspace = getWorkspaceService();
documents.onDidChangeContent(params -> {

View File

@@ -0,0 +1,109 @@
/*******************************************************************************
* Copyright (c) 2016 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.concourse;
import static org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.anyChild;
import static org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.valueAt;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.languageserver.util.TextDocumentContentChange;
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;
import org.springframework.ide.vscode.commons.yaml.ast.YamlASTProvider;
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
import org.springframework.ide.vscode.commons.yaml.ast.YamlParser;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.concourse.util.StaleFallbackCache;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.error.YAMLException;
import org.yaml.snakeyaml.nodes.Node;
/**
* ConcourseModels is responsible for extracting various bits of information
* out of .yml documents and caching them for use by various tools (reconcile engine
* and completion engine).
*/
public class ConcourseModel {
private static final YamlPath RESOURCE_NAMES_PATH = new YamlPath(
valueAt("resources"),
anyChild(),
valueAt("name")
);
private final YamlParser parser;
private StaleFallbackCache<String, YamlFileAST> asts = new StaleFallbackCache<>();
public ConcourseModel(SimpleTextDocumentService documents) {
Yaml yaml = new Yaml();
this.parser = new YamlParser(yaml);
documents.onDidChangeContent(this::documentChanged);
}
private void documentChanged(TextDocumentContentChange changeEvent) {
String uri = changeEvent.getDocument().getUri();
if (uri!=null) {
asts.invalidate(uri);
}
}
/**
* Returns the resource names that are defined by given IDocument. If the contents
* of IDocument is not currently parseable then this may return stale information
* retained from a previous successful parse.
* <p>
* It may also return null if its not currently possible to obtain the list of resource
* names (e.g. because there hasn't been a successful parse yet and current document contents
* can not be parsed).
*/
public Set<String> getResourceNames(IDocument doc) {
try {
if (doc!=null) {
String uri = doc.getUri();
if (uri!=null) {
YamlFileAST ast = getAst(doc);
Node root = ast.get(0);
return RESOURCE_NAMES_PATH
.traverseAmbiguously(root)
.map(NodeUtil::asScalar)
.filter((string) -> string!=null)
.collect(Collectors.toSet());
}
}
} catch (YAMLException e) {
// ignore: garbage in the doc. Can't compute stuff and that's to be expected.
} catch (Exception e) {
Log.log(e);
}
return null;
}
private YamlFileAST getAst(IDocument doc) throws Exception {
return getAstProvider(true).getAST(doc);
}
public YamlASTProvider getAstProvider(boolean allowStaleAsts) {
return (IDocument doc) -> {
String uri = doc.getUri();
if (uri!=null) {
return asts.get(uri, allowStaleAsts, () -> {
return parser.getAST(doc);
});
}
return null;
};
}
}

View File

@@ -12,6 +12,7 @@ package org.springframework.ide.vscode.concourse;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
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;
@@ -31,7 +32,7 @@ public class PipelineYmlSchema implements YamlSchema {
private final YTypeFactory f = new YTypeFactory();
public PipelineYmlSchema() {
public PipelineYmlSchema(ConcourseModel models) {
TYPE_UTIL = f.TYPE_UTIL;
// define schema types
@@ -81,9 +82,18 @@ public class PipelineYmlSchema implements YamlSchema {
//
// The vagrant-cloud r
);
YType resourceName = f.yenum("ResourceName",
(parseString, validValues) -> {
return "The '"+parseString+"' resource does not exist. Existing resources: "+validValues;
},
(DynamicSchemaContext dc) -> {
return models.getResourceNames(dc.getDocument());
}
);
YBeanType getStep = f.ybean("GetStep");
prop(getStep, "get", t_ne_string);
prop(getStep, "get", resourceName);
prop(getStep, "resource", t_string);
prop(getStep, "version", t_version);
prop(getStep, "passed", t_strings);
@@ -91,7 +101,7 @@ public class PipelineYmlSchema implements YamlSchema {
prop(getStep, "trigger", t_boolean);
YBeanType putStep = f.ybean("PutStep");
prop(putStep, "put", t_ne_string);
prop(putStep, "put", resourceName);
prop(putStep, "resource", t_string);
prop(putStep, "params", t_params);
prop(putStep, "get_params", t_params);

View File

@@ -0,0 +1,91 @@
/*******************************************************************************
* Copyright (c) 2016 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.concourse.util;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
/**
* Simple cache implementation that falls back on 'stale' cache entry if
* a new entry can not be computed. The api is loosely modeled after
* guava's Cache interface (but only the subset we use is implemented to reduce the
* complexity of its implementation).
*/
public class StaleFallbackCache<K, V>{
Map<K, V> staleEntries = new HashMap<>();
Cache<K, CompletableFuture<V>> validEntries = CacheBuilder.newBuilder().build();
public synchronized V get(K key, boolean allowStaleEntries, Callable<? extends V> valueLoader) throws Exception {
CompletableFuture<V> valid = validEntries.get(key, () -> load(valueLoader));
if (!allowStaleEntries) {
return future_get(valid);
} else {
if (valid.isCompletedExceptionally()) {
V staleValue = staleEntries.get(key);
if (staleValue!=null) {
return staleValue;
}
}
return future_get(valid);
}
}
public synchronized void invalidate(K key) {
CompletableFuture<V> staleEntry = validEntries.getIfPresent(key);
if (staleEntry!=null) {
validEntries.invalidate(key);
try {
staleEntries.put(key, future_get(staleEntry));
} catch (Exception e) {
//ignore. Don't overwrite stale entry if current entry represents an error.
// We only keep 'good quality' stale entries not failed attempts to compute a value.
// as it is kind of the point to fall back on a 'good' old entry when the current
// entry is unavailable because of a problem (e.g. problems parsing the AST).
}
}
}
private V future_get(CompletableFuture<V> f) throws Exception {
try {
return f.get();
} catch (InterruptedException e) {
throw e;
} catch (ExecutionException e) {
throw ExceptionUtil.exception(e.getCause());
}
}
private CompletableFuture<V> load(Callable<? extends V> valueLoader) {
CompletableFuture<V> future = new CompletableFuture<V>();
try {
V value = valueLoader.call();
Assert.isNotNull(value);
future.complete(value);
} catch (Throwable e) {
future.completeExceptionally(e);
}
return future;
}
}

View File

@@ -1,4 +1,4 @@
package org.springframework.ide.vscode.manifest.yaml;
package org.springframework.ide.vscode.concourse;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -8,7 +8,7 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.manifest.yaml;
package org.springframework.ide.vscode.concourse;
import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains;
@@ -259,12 +259,15 @@ public class PipelineYamlEditorTest {
);
editor.assertProblems(
"boohoo|boolean",
"-1|Positive Integer",
"-1|must be positive",
"git|resource does not exist",
"yohoho|boolean"
);
//check that correct values are indeed accepted
editor = harness.newEditor(
"resources:\n" +
"- name: git\n" +
"jobs:\n" +
"- name: foo\n" +
" serial: true\n" +
@@ -379,6 +382,33 @@ public class PipelineYamlEditorTest {
editor.assertHoverContains("jobs", "At a high level, a job describes some actions to perform");
}
@Test
public void reconcileResourceReferences() throws Exception {
Editor editor = harness.newEditor(
"resources:\n" +
"- name: sts4\n" +
" type: git\n" +
" source:\n" +
" repository: https://github.com/kdvolder/somestuff\n" +
"jobs:\n" +
"- name: job1\n" +
" plan:\n" +
" - get: sts4\n" +
" - get: bogus-get\n" +
" - put: bogus-put\n"
);
editor.assertProblems(
"bogus-get|resource does not exist",
"bogus-put|resource does not exist"
);
editor.assertProblems(
"bogus-get|[sts4]",
"bogus-put|[sts4]"
);
}
//////////////////////////////////////////////////////////////////////////////
private void assertContextualCompletions(String conText, String textBefore, String... textAfter) throws Exception {

View File

@@ -1,129 +0,0 @@
/*******************************************************************************
* Copyright (c) 2015, 2016 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.manifest.yaml;
import static org.junit.Assert.fail;
import org.junit.Test;
import org.springframework.ide.vscode.concourse.PipelineYmlSchema;
/**
* @author Kris De Volder
*/
public class PipelineYmlSchemaTest {
// @Test
// public void shouldMakeSomeTests() {
// fail("We should make some tests for this");
// }
//
// private static final String[] NESTED_PROP_NAMES = {
//// "applications",
// "buildpack",
// "command",
// "disk_quota",
// "domain",
// "domains",
// "env",
// "health-check-type",
// "host",
// "hosts",
//// "inherit",
// "instances",
// "memory",
// "name",
// "no-hostname",
// "no-route",
// "path",
// "random-route",
// "services",
// "stack",
// "timeout"
// };
//
private static final String[] TOPLEVEL_PROP_NAMES = {
"resources",
"jobs",
"resource-types"
//groups
};
PipelineYmlSchema schema = new PipelineYmlSchema();
//
// @Test
// public void toplevelProperties() throws Exception {
// assertPropNames(schema.getTopLevelType().getProperties(DynamicSchemaContext.NULL), TOPLEVEL_PROP_NAMES);
// assertPropNames(schema.getTopLevelType().getPropertiesMap(DynamicSchemaContext.NULL), TOPLEVEL_PROP_NAMES);
// }
//
// @Test
// public void nestedProperties() throws Exception {
// assertPropNames(getNestedProps(), NESTED_PROP_NAMES);
// }
//
// @Test
// public void toplevelPropertiesHaveDescriptions() {
// for (YTypedProperty p : schema.getTopLevelType().getProperties(DynamicSchemaContext.NULL)) {
// if (!p.getName().equals("applications")) {
// assertHasRealDescription(p);
// }
// }
// }
//
// @Test
// public void nestedPropertiesHaveDescriptions() {
// for (YTypedProperty p : getNestedProps()) {
// assertHasRealDescription(p);
// }
// }
//
// //////////////////////////////////////////////////////////////////////////////
//
// private void assertHasRealDescription(YTypedProperty p) {
// {
// String noDescriptionText = Renderables.NO_DESCRIPTION.toHtml();
// String actual = p.getDescription().toHtml();
// String msg = "Description missing for '"+p.getName()+"'";
// assertTrue(msg, StringUtil.hasText(actual));
// assertFalse(msg, noDescriptionText.equals(actual));
// }
// {
// String noDescriptionText = Renderables.NO_DESCRIPTION.toMarkdown();
// String actual = p.getDescription().toMarkdown();
// String msg = "Description missing for '"+p.getName()+"'";
// assertTrue(msg, StringUtil.hasText(actual));
// assertFalse(msg, noDescriptionText.equals(actual));
// }
// }
//
// private List<YTypedProperty> getNestedProps() {
// YSeqType applications = (YSeqType) schema.getTopLevelType().getPropertiesMap().get("applications").getType();
// YBeanType application = (YBeanType) applications.getDomainType();
// return application.getProperties();
// }
//
// private void assertPropNames(List<YTypedProperty> properties, String... expectedNames) {
// assertEquals(ImmutableSet.copyOf(expectedNames), getNames(properties));
// }
//
// private void assertPropNames(Map<String, YTypedProperty> propertiesMap, String[] toplevelPropNames) {
// assertEquals(ImmutableSet.copyOf(toplevelPropNames), ImmutableSet.copyOf(propertiesMap.keySet()));
// }
//
// private ImmutableSet<String> getNames(Iterable<YTypedProperty> properties) {
// Builder<String> builder = ImmutableSet.builder();
// for (YTypedProperty p : properties) {
// builder.add(p.getName());
// }
// return builder.build();
// }
}

View File

@@ -0,0 +1,11 @@
resources:
- name: sts4
type: git
source:
repository: https://github.com/kdvolder/somestuff
jobs:
- name: job1
plan:
- get: sts4
- get: bogus-get
- put: bogus-put