From 6eeeb0bb2a8b39a745c27c1b85ec60e0583d7260 Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Fri, 20 Jan 2017 13:47:15 -0800 Subject: [PATCH] Add support for validation of expected number of documents in a YamlFileAst Use it to produce some errors for manifest.yml files. --- .../ide/vscode/commons/util/IntegerRange.java | 113 ++++++++++++++++++ .../ide/vscode/commons/util/ValueParsers.java | 56 +++++++++ .../SchemaBasedYamlASTReconciler.java | 65 +++++++++- .../yaml/reconcile/YamlSchemaProblems.java | 7 ++ .../commons/yaml/schema/YTypeFactory.java | 41 ++++--- .../commons/yaml/schema/YTypedProperty.java | 1 + .../commons/yaml/schema/YamlSchema.java | 4 + ...arsers.java => ConcourseValueParsers.java} | 45 +------ .../vscode/concourse/PipelineYmlSchema.java | 7 +- .../vscode/concourse/DurationParserTest.java | 2 +- .../manifest/yaml/ManifestYmlSchema.java | 24 ++-- .../manifest/yaml/ManifestYamlEditorTest.java | 85 ++++++++++++- 12 files changed, 375 insertions(+), 75 deletions(-) create mode 100644 vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/IntegerRange.java create mode 100644 vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParsers.java rename vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/{ValueParsers.java => ConcourseValueParsers.java} (65%) diff --git a/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/IntegerRange.java b/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/IntegerRange.java new file mode 100644 index 000000000..a12f19318 --- /dev/null +++ b/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/IntegerRange.java @@ -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; + } + + +} diff --git a/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParsers.java b/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParsers.java new file mode 100644 index 000000000..2dde3ddb8 --- /dev/null +++ b/vscode-extensions/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParsers.java @@ -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 && valueupperBound) { + throw new NumberFormatException("Value must be at most "+upperBound); + } + return value; + } + }; + } + + +} diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java index c8436a4df..974c5a6bc 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/SchemaBasedYamlASTReconciler.java @@ -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 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. *

- * 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()); + } + + } diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java index 0473ac179..a5db4b2f7 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java @@ -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()); + } + } diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java index 173accd7e..3e594122d 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java @@ -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 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 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 builder = ImmutableMap.builder(); @@ -567,7 +569,7 @@ public class YTypeFactory { } return primaryProps; } - + @Override public YType inferMoreSpecificType(DynamicSchemaContext dc) { Set 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 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); } diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypedProperty.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypedProperty.java index a29908405..7f04d43ba 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypedProperty.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypedProperty.java @@ -19,4 +19,5 @@ public interface YTypedProperty { String getName(); YType getType(); Renderable getDescription(); + default boolean isRequired() { return false; } } diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YamlSchema.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YamlSchema.java index e05dcf7f3..11a0d42e7 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YamlSchema.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YamlSchema.java @@ -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(); } } diff --git a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ValueParsers.java b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java similarity index 65% rename from vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ValueParsers.java rename to vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java index a6f1b7ffa..13c9304c0 100644 --- a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ValueParsers.java +++ b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java @@ -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 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 && valueupperBound) { - 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'." ); - - + + } diff --git a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index 1fcc75a22..b93d2624f 100644 --- a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -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); diff --git a/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/DurationParserTest.java b/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/DurationParserTest.java index da382d586..6aea7c26c 100644 --- a/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/DurationParserTest.java +++ b/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/DurationParserTest.java @@ -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() { diff --git a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java index b792fd39b..c7b7bb31c 100644 --- a/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java +++ b/vscode-extensions/vscode-manifest-yaml/src/main/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchema.java @@ -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> buildpackProvider, Callable> 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), diff --git a/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java b/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java index dd13460c3..feb04ab1b 100644 --- a/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java +++ b/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYamlEditorTest.java @@ -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!"));