Add support for validation of expected number of documents in a YamlFileAst
Use it to produce some errors for manifest.yml files.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
/*******************************************************************************
|
||||
* 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.util;
|
||||
|
||||
/**
|
||||
* A range of integers between (inclusive) an optional lower and upper bound.
|
||||
*
|
||||
* @author Kris De Volder
|
||||
*/
|
||||
public class IntegerRange {
|
||||
|
||||
public static final IntegerRange ANY = new IntegerRange(null, null);
|
||||
|
||||
private final Integer lowerBound;
|
||||
private final Integer upperBound;
|
||||
|
||||
public boolean isInRange(int x) {
|
||||
return !isTooSmall(x) && !isTooLarge(x);
|
||||
}
|
||||
|
||||
public boolean isTooLarge(int x) {
|
||||
return upperBound==null || x > upperBound;
|
||||
}
|
||||
|
||||
public boolean isTooSmall(int x) {
|
||||
return lowerBound==null || x < lowerBound;
|
||||
}
|
||||
|
||||
private IntegerRange(Integer lowerBound, Integer upperBound) {
|
||||
super();
|
||||
this.lowerBound = lowerBound;
|
||||
this.upperBound = upperBound;
|
||||
}
|
||||
|
||||
|
||||
public static IntegerRange atLeast(int lowerBound) {
|
||||
return new IntegerRange(lowerBound, null);
|
||||
}
|
||||
|
||||
public static IntegerRange atMost(int upperBound) {
|
||||
return new IntegerRange(null, upperBound);
|
||||
}
|
||||
|
||||
public static IntegerRange exactly(int x) {
|
||||
return new IntegerRange(x, x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return
|
||||
"IntegerRange(" +
|
||||
maybeStr(lowerBound) +
|
||||
".." +
|
||||
maybeStr(upperBound) +
|
||||
")";
|
||||
}
|
||||
|
||||
private String maybeStr(Integer bound) {
|
||||
if (bound!=null) {
|
||||
return bound.toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((lowerBound == null) ? 0 : lowerBound.hashCode());
|
||||
result = prime * result + ((upperBound == null) ? 0 : upperBound.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
IntegerRange other = (IntegerRange) obj;
|
||||
if (lowerBound == null) {
|
||||
if (other.lowerBound != null)
|
||||
return false;
|
||||
} else if (!lowerBound.equals(other.lowerBound))
|
||||
return false;
|
||||
if (upperBound == null) {
|
||||
if (other.upperBound != null)
|
||||
return false;
|
||||
} else if (!upperBound.equals(other.upperBound))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public Integer getUpperBound() {
|
||||
return upperBound;
|
||||
}
|
||||
|
||||
public Integer getLowerBound() {
|
||||
return lowerBound;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*******************************************************************************
|
||||
* 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.util;
|
||||
|
||||
/**
|
||||
* Constants and static methods to create generally useful value parsers.
|
||||
*
|
||||
* @author Kris De Volder
|
||||
*/
|
||||
public class ValueParsers {
|
||||
|
||||
public static final ValueParser NE_STRING = (s) -> {
|
||||
if (StringUtil.hasText(s)) {
|
||||
return s;
|
||||
} else {
|
||||
throw new IllegalArgumentException("String should not be empty");
|
||||
}
|
||||
};
|
||||
|
||||
public static final ValueParser POS_INTEGER = integerRange(0, null);
|
||||
|
||||
public static ValueParser integerAtLeast(final Integer lowerBound) {
|
||||
return integerRange(lowerBound, null);
|
||||
}
|
||||
|
||||
public static ValueParser integerRange(final Integer lowerBound, final Integer upperBound) {
|
||||
Assert.isLegal(lowerBound==null || upperBound==null || lowerBound <= upperBound);
|
||||
return new ValueParser() {
|
||||
@Override
|
||||
public Object parse(String str) {
|
||||
int value = Integer.parseInt(str);
|
||||
if (lowerBound!=null && value<lowerBound) {
|
||||
if (lowerBound==0) {
|
||||
throw new NumberFormatException("Value must be positive");
|
||||
} else {
|
||||
throw new NumberFormatException("Value must be at least "+lowerBound);
|
||||
}
|
||||
}
|
||||
if (upperBound!=null && value>upperBound) {
|
||||
throw new NumberFormatException("Value must be at most "+upperBound);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -18,9 +18,13 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
|
||||
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
|
||||
import org.springframework.ide.vscode.commons.util.IntegerRange;
|
||||
import org.springframework.ide.vscode.commons.util.Log;
|
||||
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;
|
||||
@@ -56,14 +60,47 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
@Override
|
||||
public void reconcile(YamlFileAST ast) {
|
||||
List<Node> nodes = ast.getNodes();
|
||||
if (nodes!=null && !nodes.isEmpty()) {
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
Node node = nodes.get(i);
|
||||
reconcile(ast.getDocument(), new YamlPath(YamlPathSegment.valueAt(i)), node, schema.getTopLevelType());
|
||||
IntegerRange expectedDocs = schema.expectedNumberOfDocuments();
|
||||
if (expectedDocs.isInRange(nodes.size())) {
|
||||
if (nodes!=null && !nodes.isEmpty()) {
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
Node node = nodes.get(i);
|
||||
reconcile(ast.getDocument(), new YamlPath(YamlPathSegment.valueAt(i)), node, schema.getTopLevelType());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//wrong number of documents in the file. Figure out a good error message.
|
||||
if (nodes.isEmpty()) {
|
||||
problem(allOf(ast.getDocument()), "'"+schema.getName()+"' must have at least some Yaml content");
|
||||
} else if (expectedDocs.isTooLarge(nodes.size())) {
|
||||
int upperBound = expectedDocs.getUpperBound();
|
||||
Node extraNode = nodes.get(upperBound);
|
||||
problem(dashesAtStartOf(ast, extraNode), "'"+schema.getName()+"' should not have more than "+upperBound+" Yaml Documents");
|
||||
} else if (expectedDocs.isTooSmall(nodes.size())) {
|
||||
int lowerBound = expectedDocs.getLowerBound();
|
||||
problem(endOf(ast.getDocument()), "'"+schema.getName()+"' should have at least "+lowerBound+" Yaml Documents");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DocumentRegion dashesAtStartOf(YamlFileAST ast, Node node) {
|
||||
try {
|
||||
int start = node.getStartMark().getIndex();
|
||||
int end = node.getEndMark().getIndex();
|
||||
String text = ast.getDocument().textBetween(start, end);
|
||||
DocumentRegion textBefore = new DocumentRegion(ast.getDocument(), 0, start)
|
||||
.trimEnd(Pattern.compile("\\s*"));
|
||||
DocumentRegion dashes = textBefore.subSequence(textBefore.getLength()-3);
|
||||
if (dashes.toString().equals("---")) {
|
||||
return dashes;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.log(e);
|
||||
}
|
||||
//something unexpected... we couldn't find the '---'. So just mark the entire node.
|
||||
return allOf(ast, node);
|
||||
}
|
||||
|
||||
private void reconcile(IDocument doc, YamlPath path, Node node, YType type) {
|
||||
if (type!=null) {
|
||||
DynamicSchemaContext schemaContext = new ASTDynamicSchemaContext(doc, path, node);
|
||||
@@ -135,7 +172,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
|
||||
protected NodeId getNodeId(Node node) {
|
||||
NodeId id = node.getNodeId();
|
||||
if (id==NodeId.mapping && isMoustacheVar((MappingNode)node)) {
|
||||
if (id==NodeId.mapping && isMoustacheVar(node)) {
|
||||
return NodeId.scalar;
|
||||
}
|
||||
return id;
|
||||
@@ -146,7 +183,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
* parsed these will parse as a kind of map. But since these vars are meant to be replaced
|
||||
* with some kind of string we should treat them as scalar instead.
|
||||
* <p>
|
||||
* This function recognizes a mapping node that actually is moustache var pattern.
|
||||
* This function recognizes a mapping node that actually is moustache var pattern.
|
||||
*/
|
||||
private boolean isMoustacheVar(Node node) {
|
||||
return NodeUtil.asScalar(debrace(debrace(node))) != null;
|
||||
@@ -266,4 +303,20 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
|
||||
problems.accept(YamlSchemaProblems.schemaProblem(msg, node));
|
||||
}
|
||||
|
||||
private void problem(DocumentRegion region, String msg) {
|
||||
problems.accept(YamlSchemaProblems.schemaProblem(msg, region));
|
||||
}
|
||||
|
||||
private DocumentRegion endOf(IDocument document) {
|
||||
return new DocumentRegion(document, document.getLength(), document.getLength());
|
||||
}
|
||||
|
||||
private DocumentRegion allOf(IDocument doc) {
|
||||
return new DocumentRegion(doc, 0, doc.getLength());
|
||||
}
|
||||
private DocumentRegion allOf(YamlFileAST ast, Node node) {
|
||||
return new DocumentRegion(ast.getDocument(), node.getStartMark().getIndex(), node.getEndMark().getIndex());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSe
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion;
|
||||
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
|
||||
import org.yaml.snakeyaml.nodes.Node;
|
||||
|
||||
/**
|
||||
@@ -53,4 +55,9 @@ public class YamlSchemaProblems {
|
||||
int end = node.getEndMark().getIndex();
|
||||
return new ReconcileProblemImpl(SCHEMA_PROBLEM, msg, start, end-start);
|
||||
}
|
||||
|
||||
public static ReconcileProblem schemaProblem(String msg, DocumentRegion node) {
|
||||
return new ReconcileProblemImpl(SCHEMA_PROBLEM, msg, node.getStart(), node.getLength());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class YTypeFactory {
|
||||
Assert.isLegal(types.length>1);
|
||||
return new YBeanUnionType(name, types);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* YTypeUtil instances capable of 'interpreting' the YType objects created by this
|
||||
* YTypeFactory
|
||||
@@ -177,7 +177,7 @@ public class YTypeFactory {
|
||||
|
||||
public YValueHint[] getHintValues(DynamicSchemaContext dc) throws Exception {
|
||||
Collection<YValueHint> providerHints=getProviderHints(dc);
|
||||
|
||||
|
||||
if (providerHints == null || providerHints.isEmpty()) {
|
||||
return hints.toArray(new YValueHint[hints.size()]);
|
||||
} else {
|
||||
@@ -229,6 +229,7 @@ public class YTypeFactory {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract String toString(); // force each sublcass to implement a (nice) toString method.
|
||||
|
||||
public void addProperty(YTypedProperty p) {
|
||||
@@ -255,7 +256,7 @@ public class YTypeFactory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addHints(YValueHint... extraHints) {
|
||||
for (YValueHint h : extraHints) {
|
||||
if (!hints.contains(h)) {
|
||||
@@ -299,10 +300,10 @@ public class YTypeFactory {
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Represents a type that is completely unconstrained. Anything goes: A map, a sequence or some
|
||||
* atomic value.
|
||||
@@ -406,6 +407,7 @@ public class YTypeFactory {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBean() {
|
||||
return true;
|
||||
}
|
||||
@@ -445,7 +447,7 @@ public class YTypeFactory {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a union of several bean types. It is assumed one primary property
|
||||
* exists in each of the the sub-bean types that can be used to identify the
|
||||
@@ -467,11 +469,11 @@ public class YTypeFactory {
|
||||
addUnionMember(t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void addUnionMember(YBeanType type) {
|
||||
types.add(type);
|
||||
}
|
||||
|
||||
|
||||
private String findPrimary(AbstractType t, List<YBeanType> types) {
|
||||
//Note: passing null dynamic context below is okay, assuming the properties in YBeanType
|
||||
// do not care about dynamic context.
|
||||
@@ -544,7 +546,7 @@ public class YTypeFactory {
|
||||
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
|
||||
// built up flexibly using mutation ops during initialization, but once it
|
||||
// built up flexibly using mutation ops during initialization, but once it
|
||||
// starts being used it becomes immutable.
|
||||
types = ImmutableList.copyOf(types);
|
||||
ImmutableMap.Builder<String, AbstractType> builder = ImmutableMap.builder();
|
||||
@@ -567,7 +569,7 @@ public class YTypeFactory {
|
||||
}
|
||||
return primaryProps;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public YType inferMoreSpecificType(DynamicSchemaContext dc) {
|
||||
Set<String> existingProps = dc.getDefinedProperties();
|
||||
@@ -589,6 +591,7 @@ public class YTypeFactory {
|
||||
final private String name;
|
||||
final private YType type;
|
||||
private Renderable description = Renderables.NO_DESCRIPTION;
|
||||
private boolean isRequired;
|
||||
|
||||
private YTypedPropertyImpl(String name, YType type) {
|
||||
this.name = name;
|
||||
@@ -619,6 +622,16 @@ public class YTypeFactory {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public YTypedPropertyImpl isRequired(boolean b) {
|
||||
this.isRequired = b;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequired() {
|
||||
return isRequired;
|
||||
}
|
||||
}
|
||||
|
||||
public YAtomicType yatomic(String name) {
|
||||
@@ -633,8 +646,8 @@ public class YTypeFactory {
|
||||
YAtomicType t = yatomic(name);
|
||||
t.addHintProvider((dc) -> {
|
||||
Collection<String> strings = values.withContext(dc);
|
||||
return strings==null
|
||||
? null
|
||||
return strings==null
|
||||
? null
|
||||
: () -> strings.stream()
|
||||
.map((s) -> new BasicYValueHint(s))
|
||||
.collect(Collectors.toSet());
|
||||
@@ -646,7 +659,7 @@ public class YTypeFactory {
|
||||
return errorMessageFormatter.apply(parseString, values);
|
||||
}
|
||||
};
|
||||
return enumParser;
|
||||
return enumParser;
|
||||
});
|
||||
return t;
|
||||
}
|
||||
@@ -657,7 +670,7 @@ public class YTypeFactory {
|
||||
t.parseWith(new EnumValueParser(name, values));
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
public YValueHint hint(String value, String label) {
|
||||
return new BasicYValueHint(value, label);
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ public interface YTypedProperty {
|
||||
String getName();
|
||||
YType getType();
|
||||
Renderable getDescription();
|
||||
default boolean isRequired() { return false; }
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.commons.yaml.schema;
|
||||
|
||||
import org.springframework.ide.vscode.commons.util.IntegerRange;
|
||||
|
||||
/**
|
||||
* A 'schema' provides a toplevel type, which dictates the valid structure of a
|
||||
* YamlDocument and a {@link YTypeUtil} which provides the means to 'interpret'
|
||||
@@ -19,7 +21,9 @@ package org.springframework.ide.vscode.commons.yaml.schema;
|
||||
*/
|
||||
public interface YamlSchema {
|
||||
|
||||
default IntegerRange expectedNumberOfDocuments() { return IntegerRange.ANY; };
|
||||
YType getTopLevelType();
|
||||
YTypeUtil getTypeUtil();
|
||||
default String getName() { return getTopLevelType().toString(); }
|
||||
|
||||
}
|
||||
|
||||
@@ -27,19 +27,7 @@ import com.google.common.collect.Multiset;
|
||||
*
|
||||
* @author Kris De Volder
|
||||
*/
|
||||
public class ValueParsers {
|
||||
|
||||
//TODO: some of the parsers here are pretty general purpose and could be moved to commons.
|
||||
|
||||
public static final ValueParser NE_STRING = (s) -> {
|
||||
if (StringUtil.hasText(s)) {
|
||||
return s;
|
||||
} else {
|
||||
throw new IllegalArgumentException("String should not be empty");
|
||||
}
|
||||
};
|
||||
|
||||
public static final ValueParser POS_INTEGER = integerRange(0, null);
|
||||
public class ConcourseValueParsers {
|
||||
|
||||
public static final SchemaContextAware<ValueParser> resourceNameDef(ConcourseModel models) {
|
||||
return acceptOnlyUniqueNames(models::getResourceNames, "resource name");
|
||||
@@ -58,38 +46,13 @@ public class ValueParsers {
|
||||
return (String input) -> {
|
||||
if (resourceNames.count(input)<=1) {
|
||||
//okay
|
||||
return resourceNames;
|
||||
return resourceNames;
|
||||
}
|
||||
throw new IllegalArgumentException("Duplicate "+typeName+" '"+input+"'");
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
public static ValueParser integerAtLeast(final Integer lowerBound) {
|
||||
return integerRange(lowerBound, null);
|
||||
}
|
||||
|
||||
public static ValueParser integerRange(final Integer lowerBound, final Integer upperBound) {
|
||||
Assert.isLegal(lowerBound==null || upperBound==null || lowerBound <= upperBound);
|
||||
return new ValueParser() {
|
||||
@Override
|
||||
public Object parse(String str) {
|
||||
int value = Integer.parseInt(str);
|
||||
if (lowerBound!=null && value<lowerBound) {
|
||||
if (lowerBound==0) {
|
||||
throw new NumberFormatException("Value must be positive");
|
||||
} else {
|
||||
throw new NumberFormatException("Value must be at least "+lowerBound);
|
||||
}
|
||||
}
|
||||
if (upperBound!=null && value>upperBound) {
|
||||
throw new NumberFormatException("Value must be at most "+upperBound);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ValueParser DURATION = new RegexpParser(
|
||||
"^(([0-9]+(.[0-9]+)?)(ns|us|µs|ms|s|h|m))+$",
|
||||
"Duration",
|
||||
@@ -99,7 +62,7 @@ public class ValueParsers {
|
||||
+ "'m', 'h'."
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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.util.ValueParsers;
|
||||
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.path.YamlPath;
|
||||
@@ -66,7 +67,7 @@ public class PipelineYmlSchema implements YamlSchema {
|
||||
t_strictly_pos_integer.parseWith(ValueParsers.integerAtLeast(1));
|
||||
|
||||
YAtomicType t_duration = f.yatomic("Duration");
|
||||
t_duration.parseWith(ValueParsers.DURATION);
|
||||
t_duration.parseWith(ConcourseValueParsers.DURATION);
|
||||
|
||||
YAtomicType t_version = f.yatomic("Version");
|
||||
t_version.addHints("latest", "every");
|
||||
@@ -121,9 +122,9 @@ public class PipelineYmlSchema implements YamlSchema {
|
||||
);
|
||||
|
||||
YAtomicType resourceNameDef = f.yatomic("Resource Name");
|
||||
resourceNameDef.parseWith(ValueParsers.resourceNameDef(models));
|
||||
resourceNameDef.parseWith(ConcourseValueParsers.resourceNameDef(models));
|
||||
YAtomicType jobNameDef = f.yatomic("Job Name");
|
||||
jobNameDef.parseWith(ValueParsers.jobNameDef(models));
|
||||
jobNameDef.parseWith(ConcourseValueParsers.jobNameDef(models));
|
||||
|
||||
YBeanType getStep = f.ybean("GetStep");
|
||||
addProp(getStep, "get", resourceName);
|
||||
|
||||
@@ -20,7 +20,7 @@ import static org.springframework.ide.vscode.languageserver.testharness.TestAsse
|
||||
|
||||
public class DurationParserTest {
|
||||
|
||||
private ValueParser parser = ValueParsers.DURATION;
|
||||
private ValueParser parser = ConcourseValueParsers.DURATION;
|
||||
|
||||
@Test
|
||||
public void goodExamples() {
|
||||
|
||||
@@ -14,8 +14,10 @@ import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.springframework.ide.vscode.commons.util.IntegerRange;
|
||||
import org.springframework.ide.vscode.commons.util.Renderable;
|
||||
import org.springframework.ide.vscode.commons.util.Renderables;
|
||||
import org.springframework.ide.vscode.commons.util.ValueParsers;
|
||||
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.YAtomicType;
|
||||
@@ -40,23 +42,28 @@ public class ManifestYmlSchema implements YamlSchema {
|
||||
"name", "host", "hosts"
|
||||
);
|
||||
|
||||
@Override
|
||||
public IntegerRange expectedNumberOfDocuments() {
|
||||
return IntegerRange.exactly(1);
|
||||
}
|
||||
|
||||
public ManifestYmlSchema(Callable<Collection<YValueHint>> buildpackProvider, Callable<Collection<YValueHint>> servicesProvider) {
|
||||
this.buildpackProvider = buildpackProvider;
|
||||
YTypeFactory f = new YTypeFactory();
|
||||
TYPE_UTIL = f.TYPE_UTIL;
|
||||
|
||||
// define schema types
|
||||
TOPLEVEL_TYPE = f.ybean("manifest.yml schema");
|
||||
TOPLEVEL_TYPE = f.ybean("Cloudfoundry Manifest");
|
||||
|
||||
YBeanType application = f.ybean("Application");
|
||||
YAtomicType t_path = f.yatomic("Path");
|
||||
|
||||
|
||||
YAtomicType t_buildpack = f.yatomic("Buildpack");
|
||||
if (this.buildpackProvider != null) {
|
||||
t_buildpack.addHintProvider(this.buildpackProvider);
|
||||
// t_buildpack.parseWith(ManifestYmlValueParsers.fromHints(t_buildpack.toString(), buildpackProvider));
|
||||
}
|
||||
|
||||
|
||||
YAtomicType t_service_string = f.yatomic("Service");
|
||||
if (servicesProvider != null) {
|
||||
t_service_string.addHintProvider(servicesProvider);
|
||||
@@ -65,13 +72,15 @@ public class ManifestYmlSchema implements YamlSchema {
|
||||
YType t_services = f.yseq(t_service_string);
|
||||
|
||||
YAtomicType t_boolean = f.yenum("boolean", "true", "false");
|
||||
YAtomicType t_ne_string = f.yatomic("String");
|
||||
t_ne_string.parseWith(ValueParsers.NE_STRING);
|
||||
YType t_string = f.yatomic("String");
|
||||
YType t_strings = f.yseq(t_string);
|
||||
|
||||
|
||||
YAtomicType t_memory = f.yatomic("Memory");
|
||||
t_memory.addHints("256M", "512M", "1024M");
|
||||
t_memory.parseWith(ManifestYmlValueParsers.MEMORY);
|
||||
|
||||
|
||||
YAtomicType t_health_check_type = f.yenum("Health Check Type", "none", "port");
|
||||
|
||||
YAtomicType t_strictly_pos_integer = f.yatomic("Strictly Positive Integer");
|
||||
@@ -83,7 +92,8 @@ public class ManifestYmlSchema implements YamlSchema {
|
||||
YType t_env = f.ymap(t_string, t_string);
|
||||
|
||||
// define schema structure...
|
||||
TOPLEVEL_TYPE.addProperty("applications", f.yseq(application));
|
||||
TOPLEVEL_TYPE.addProperty(f.yprop("applications", f.yseq(application))
|
||||
.isRequired(true));
|
||||
TOPLEVEL_TYPE.addProperty("inherit", t_string, descriptionFor("inherit"));
|
||||
|
||||
YTypedPropertyImpl[] props = {
|
||||
@@ -97,7 +107,7 @@ public class ManifestYmlSchema implements YamlSchema {
|
||||
f.yprop("hosts", t_strings),
|
||||
f.yprop("instances", t_strictly_pos_integer),
|
||||
f.yprop("memory", t_memory),
|
||||
f.yprop("name", t_string),
|
||||
f.yprop("name", t_ne_string).isRequired(true),
|
||||
f.yprop("no-hostname", t_boolean),
|
||||
f.yprop("no-route", t_boolean),
|
||||
f.yprop("path", t_path),
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.manifest.yaml;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.lsp4j.Diagnostic;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.ide.vscode.commons.cloudfoundry.client.ClientRequests;
|
||||
@@ -681,6 +682,84 @@ public class ManifestYamlEditorTest {
|
||||
|
||||
}
|
||||
|
||||
@Test public void numberOfYamlDocumentsShouldBeExactlyOne() throws Exception {
|
||||
Editor editor;
|
||||
|
||||
{
|
||||
//when the file is empty (there is no AST at all)
|
||||
editor = harness.newEditor("#Emptyfile");
|
||||
editor.assertProblems("#Emptyfile|'Cloudfoundry Manifest' must have at least some Yaml content");
|
||||
}
|
||||
|
||||
{
|
||||
//when the file has too many documents... then highlight the '---' marker introducing the first document
|
||||
//exceeding the range.
|
||||
editor = harness.newEditor(
|
||||
"---\n" +
|
||||
"applications:\n"+
|
||||
"- name: foo\n" +
|
||||
"---\n" +
|
||||
"applications:\n"+
|
||||
"- name: foo\n"
|
||||
);
|
||||
editor.assertProblems(
|
||||
"---|'Cloudfoundry Manifest' should not have more than 1 Yaml Document"
|
||||
);
|
||||
//also check the location of the marker since there are two occurrences of '---' in the editor text.
|
||||
Diagnostic problem = editor.assertProblem("---");
|
||||
assertTrue(problem.getRange().getStart().getLine()>1);
|
||||
}
|
||||
|
||||
{
|
||||
// Also check that looking for the '---' isn't confused by extra whitespace
|
||||
editor = harness.newEditor(
|
||||
"---\n" +
|
||||
"applications:\n"+
|
||||
"- name: foo\n" +
|
||||
" \n"+
|
||||
"---\n" +
|
||||
" \n"+
|
||||
"applications:\n"+
|
||||
"- name: foo\n"
|
||||
);
|
||||
editor.assertProblems(
|
||||
"---|'Cloudfoundry Manifest' should not have more than 1 Yaml Document"
|
||||
);
|
||||
//also check the location of the marker since there are two occurrences of '---' in the editor text.
|
||||
Diagnostic problem = editor.assertProblem("---");
|
||||
assertTrue(problem.getRange().getStart().getLine()>1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test public void applicationsPropertyIsRequired() throws Exception {
|
||||
Editor editor;
|
||||
|
||||
//when the file is empty (there is no AST at all)
|
||||
editor = harness.newEditor(
|
||||
"foo: v1\n"
|
||||
);
|
||||
editor.assertProblems(
|
||||
|
||||
"foo|Unkown property",
|
||||
"foo: v1|'applications' is required"
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@Test public void namePropertyIsRequired() throws Exception {
|
||||
Editor editor = harness.newEditor(
|
||||
"applications:\n" +
|
||||
"- name: this-is-good\n" +
|
||||
"- memory: 1G\n" +
|
||||
"- name:\n"
|
||||
);
|
||||
editor.assertProblems(
|
||||
"memory: 1G|Property 'name' is required",
|
||||
":|should not be empty"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noReconcileErrorsWhenCFFactoryThrows() throws Exception {
|
||||
cfClientFactory.throwException(new IOException("Can't create a client!"));
|
||||
|
||||
Reference in New Issue
Block a user