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 10776e050..6ff139519 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 @@ -15,6 +15,7 @@ import static org.springframework.ide.vscode.commons.util.ExceptionUtil.getSimpl import static org.springframework.ide.vscode.commons.yaml.ast.NodeUtil.asScalar; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -52,6 +53,8 @@ import org.yaml.snakeyaml.nodes.NodeTuple; import org.yaml.snakeyaml.nodes.ScalarNode; import org.yaml.snakeyaml.nodes.SequenceNode; +import com.google.gson.internal.Streams; + public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { private final IProblemCollector problems; @@ -217,6 +220,7 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { boolean allPropertiesKnown = beanProperties.keySet().containsAll(foundProps); //Don't check for missing properties if some properties look like they might be spelled incorrectly. if (allPropertiesKnown) { + //Check for missing required properties: Set missingProps = beanProperties.values().stream() .filter(YTypedProperty::isRequired) .map(YTypedProperty::getName) @@ -233,6 +237,25 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler { } problem(map, message); } + + //Check for missing/extra 'one-of' constrained properties + for (String[] _requiredProps : typeUtil.getOneOfConstraints(type)) { + List requiredProps = Arrays.asList(_requiredProps); + long foundPropsCount = requiredProps.stream() + .filter(foundProps::contains) + .count(); + if (foundPropsCount==0) { + problem(map, "One of "+requiredProps+" is required for '"+type+"'"); + } else if (foundPropsCount>1) { + //Mark each of the found keys as a violation: + for (NodeTuple entry : map.getValue()) { + String key = NodeUtil.asScalar(entry.getKeyNode()); + if (key!=null && requiredProps.contains(key)) { + problem(entry.getKeyNode(), "Only one of "+requiredProps+" should be defined for '"+type+"'"); + } + } + } + } } } 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 acebcddff..c78ccf32b 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 @@ -23,20 +23,16 @@ import java.util.concurrent.Callable; import java.util.function.BiFunction; import java.util.stream.Collectors; -import javax.inject.Provider; - import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.EnumValueParser; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.ValueParser; -import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YAtomicType; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Multiset; /** * Static utility method for creating YType objects representing either @@ -137,6 +133,11 @@ public class YTypeFactory { public YType inferMoreSpecificType(YType type, DynamicSchemaContext schemaContext) { return ((AbstractType)type).inferMoreSpecificType(schemaContext); } + + @Override + public List getOneOfConstraints(YType type) { + return ((AbstractType)type).getOneOfConstraints(); + } }; ///////////////////////////////////////////////////////////////////////////////////// @@ -152,6 +153,8 @@ public class YTypeFactory { private Map cachedPropertyMap; private SchemaContextAware>> hintProvider; + private List oneOfConstraints = new ArrayList<>(1); + public boolean isSequenceable() { return false; } @@ -212,6 +215,10 @@ public class YTypeFactory { return ImmutableList.of(); } + public List getOneOfConstraints() { + return ImmutableList.copyOf(oneOfConstraints); + } + public List getProperties() { return Collections.unmodifiableList(propertyList); } @@ -282,6 +289,11 @@ public class YTypeFactory { private ValueParser getParser(DynamicSchemaContext dc) { return parser == null ? null : parser.withContext(dc); } + + public void requireOneOf(String... properties) { + Assert.isLegal(properties.length>1); + this.oneOfConstraints.add(properties); + } } /** @@ -418,11 +430,6 @@ public class YTypeFactory { return true; } - public void requireOneOf(String... properties) { - Assert.isLegal(properties.length>1); - //TODO: implement support for this. - } - } public static class YAtomicType extends AbstractType { @@ -479,7 +486,7 @@ public class YTypeFactory { return null; //unreachable, but compiler doesn't know. } private boolean isUniqueFor(String name, AbstractType t, List types) { - for (YBeanType other : types) { + for (AbstractType other : types) { if (other!=t) { //Note: passing null dynamic context below is okay, assuming the properties in YBeanType // do not care about dynamic context. diff --git a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java index ef4e518bd..783819d31 100644 --- a/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java +++ b/vscode-extensions/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeUtil.java @@ -45,4 +45,5 @@ public interface YTypeUtil { * should be returned. */ YType inferMoreSpecificType(YType type, DynamicSchemaContext dc); + List getOneOfConstraints(YType type); } 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 d759624c3..3ab97fb30 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 @@ -68,7 +68,7 @@ public class PipelineYmlSchema implements YamlSchema { .collect(Collectors.toSet()) .block(); - private final YBeanType TOPLEVEL_TYPE; + private final AbstractType TOPLEVEL_TYPE; private final YTypeUtil TYPE_UTIL; public final YTypeFactory f = new YTypeFactory(); @@ -99,7 +99,7 @@ public class PipelineYmlSchema implements YamlSchema { .parseWith(ValueParsers.NE_STRING) .addHints(MimeTypes.getKnownMimeTypes()); - public final YBeanType task; + public final AbstractType task; private final ResourceTypeRegistry resourceTypes = new ResourceTypeRegistry(); @@ -156,13 +156,13 @@ public class PipelineYmlSchema implements YamlSchema { resourceTypes.getSourceType(getResourceTypeTag(models, dc)) ); - YBeanType resource = f.ybean("Resource"); + AbstractType resource = f.ybean("Resource"); addProp(resource, "name", resourceNameDef).isRequired(true); addProp(resource, "type", t_resource_type_name).isRequired(true); addProp(resource, "source", resourceSource); addProp(resource, "check_every", t_duration); - YBeanType t_image_resource = f.ybean("ImageResource"); + AbstractType t_image_resource = f.ybean("ImageResource"); for (YTypedProperty p : resource.getProperties()) { if (!"name".equals(p.getName())) { t_image_resource.addProperty(p); @@ -172,15 +172,15 @@ public class PipelineYmlSchema implements YamlSchema { YAtomicType t_platform = f.yenum("Platform", "windows", "linux", "darwin"); t_platform.parseWith(ValueParsers.NE_STRING); //no errors because in theory platform are just strings. - YBeanType t_input = f.ybean("TaskInput"); + AbstractType t_input = f.ybean("TaskInput"); addProp(t_input, "name", t_ne_string).isRequired(true); addProp(t_input, "path", t_ne_string); - YBeanType t_output = f.ybean("TaskOutput"); + AbstractType t_output = f.ybean("TaskOutput"); addProp(t_output, "name", t_ne_string).isRequired(true); addProp(t_output, "path", t_ne_string); - YBeanType t_command = f.ybean("Command"); + AbstractType t_command = f.ybean("Command"); addProp(t_command, "path", t_ne_string).isRequired(true); addProp(t_command, "args", t_strings); addProp(t_command, "dir", t_ne_string); @@ -242,7 +242,7 @@ public class PipelineYmlSchema implements YamlSchema { addProp(tryStep, "try", step); // shared properties applicable for any subtype of Step: - for (YBeanType subStep : stepTypes) { + for (AbstractType subStep : stepTypes) { addProp(step, subStep, "on_success", step); addProp(step, subStep, "on_failure", step); addProp(step, subStep, "ensure", step); @@ -251,7 +251,7 @@ public class PipelineYmlSchema implements YamlSchema { addProp(step, subStep, "timeout", t_duration); } - YBeanType job = f.ybean("Job"); + AbstractType job = f.ybean("Job"); addProp(job, "name", jobNameDef).isRequired(true); addProp(job, "plan", f.yseq(step)).isRequired(true); addProp(job, "serial", t_boolean); @@ -261,12 +261,12 @@ public class PipelineYmlSchema implements YamlSchema { addProp(job, "public", t_boolean); addProp(job, "disable_manual_trigger", t_boolean); - YBeanType resourceType = f.ybean("ResourceType"); + AbstractType resourceType = f.ybean("ResourceType"); addProp(resourceType, "name", resourceTypeNameDef).isRequired(true); addProp(resourceType, "type", t_image_type).isRequired(true); addProp(resourceType, "source", resourceSource); - YBeanType group = f.ybean("Group"); + AbstractType group = f.ybean("Group"); addProp(group, "name", t_ne_string).isRequired(true); addProp(group, "resources", f.yseq(t_resource_name)); addProp(group, "jobs", f.yseq(t_job_name)); @@ -286,7 +286,7 @@ public class PipelineYmlSchema implements YamlSchema { private void initializeDefaultResourceTypes() { // git : { - YBeanType source = f.ybean("GitSource"); + AbstractType source = f.ybean("GitSource"); addProp(source, "uri", t_string).isRequired(true); addProp(source, "branch", t_string).isRequired(true); addProp(source, "private_key", t_string); @@ -302,12 +302,12 @@ public class PipelineYmlSchema implements YamlSchema { addProp(source, "commit_verification_key_ids", t_strings); addProp(source, "gpg_keyserver", t_string); - YBeanType get = f.ybean("GitGetParams"); + AbstractType get = f.ybean("GitGetParams"); addProp(get, "depth", t_pos_integer); addProp(get, "submodules", f.yany("GitSubmodules").addHints("all", "none")); addProp(get, "disable_git_lfs", t_boolean); - YBeanType put = f.ybean("GitPutParams"); + AbstractType put = f.ybean("GitPutParams"); addProp(put, "repository", t_ne_string).isRequired(true); addProp(put, "rebase", t_boolean); addProp(put, "tag", t_ne_string); @@ -320,7 +320,7 @@ public class PipelineYmlSchema implements YamlSchema { } //docker-image: { - YBeanType source = f.ybean("DockerImageSource"); + AbstractType source = f.ybean("DockerImageSource"); addProp(source, "repository", t_ne_string).isRequired(true); addProp(source, "tag", t_ne_string); addProp(source, "username", t_ne_string); @@ -339,12 +339,12 @@ public class PipelineYmlSchema implements YamlSchema { f.yprop("cert", t_ne_string) ))); - YBeanType get = f.ybean("DockerImageGetParams"); + AbstractType get = f.ybean("DockerImageGetParams"); addProp(get, "save", t_boolean); addProp(get, "rootfs", t_boolean); addProp(get, "skip_download", t_boolean); - YBeanType put = f.ybean("DockerImagePutParams"); + AbstractType put = f.ybean("DockerImagePutParams"); addProp(put, "build", t_ne_string); addProp(put, "load", t_ne_string); addProp(put, "dockerfile", t_ne_string); @@ -386,7 +386,7 @@ public class PipelineYmlSchema implements YamlSchema { "log-delivery-write" ); - YBeanType source = f.ybean("S3Source"); + AbstractType source = f.ybean("S3Source"); addProp(source, "bucket", t_ne_string).isRequired(true); addProp(source, "access_key_id", t_ne_string); addProp(source, "secret_access_key", t_ne_string); @@ -402,10 +402,10 @@ public class PipelineYmlSchema implements YamlSchema { addProp(source, "versioned_file", t_ne_string); source.requireOneOf("regexp", "versioned_file"); - YBeanType get = f.ybean("S3GetParams"); + AbstractType get = f.ybean("S3GetParams"); //Note: S3GetParams intentionally has no properties since no params are expected according to the docs. - YBeanType put = f.ybean("S3PutParams"); + AbstractType put = f.ybean("S3PutParams"); addProp(put, "file", t_ne_string).isRequired(true); addProp(put, "acl", t_canned_acl); addProp(put, "content_type", t_mime_type); @@ -459,7 +459,7 @@ public class PipelineYmlSchema implements YamlSchema { } @Override - public YBeanType getTopLevelType() { + public AbstractType getTopLevelType() { return TOPLEVEL_TYPE; } diff --git a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ResourceTypeRegistry.java b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ResourceTypeRegistry.java index 1f67c1abd..84ca62795 100644 --- a/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ResourceTypeRegistry.java +++ b/vscode-extensions/vscode-concourse/src/main/java/org/springframework/ide/vscode/concourse/ResourceTypeRegistry.java @@ -15,7 +15,7 @@ import java.util.Map; import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.yaml.schema.YType; -import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; /** * Keeps track of known resource types. @@ -25,26 +25,26 @@ import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanType public class ResourceTypeRegistry { private static class ResourceTypeInfo { - private final YBeanType source; - private final YBeanType in; - private final YBeanType out; + private final AbstractType source; + private final AbstractType in; + private final AbstractType out; - public ResourceTypeInfo(YBeanType source, YBeanType in, YBeanType out) { + public ResourceTypeInfo(AbstractType source, AbstractType in, AbstractType out) { super(); this.source = source; this.in = in; this.out = out; } - public YBeanType getSource() { + public AbstractType getSource() { return source; } - public YBeanType getIn() { + public AbstractType getIn() { return in; } - public YBeanType getOut() { + public AbstractType getOut() { return out; } } @@ -55,7 +55,7 @@ public class ResourceTypeRegistry { public ResourceTypeRegistry() { } - public void def(String resourceTypeName, YBeanType source, YBeanType in, YBeanType out) { + public void def(String resourceTypeName, AbstractType source, AbstractType in, AbstractType out) { Assert.isLegal(!resourceTypes.containsKey(resourceTypeName), "Multiple definitions for '"+resourceTypeName+"'"); resourceTypes.put(resourceTypeName, new ResourceTypeInfo(source, in, out)); } diff --git a/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index 869eab516..c90cef941 100644 --- a/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/vscode-extensions/vscode-concourse/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -1314,7 +1314,10 @@ public class ConcourseEditorTest { " source:\n" + " access_key_id: the-key" ); - editor.assertProblems("access_key_id: the-key|'bucket' is required"); + editor.assertProblems( + "access_key_id: the-key|'bucket' is required", + "access_key_id: the-key|One of [regexp, versioned_file] is required" + ); editor = harness.newEditor( "resources:\n" + @@ -1339,7 +1342,9 @@ public class ConcourseEditorTest { "bogus-region|unknown 'S3Region'", "is-private|'boolean'", "no_ssl_checking|'boolean'", - "should-use-v2|'boolean'" + "should-use-v2|'boolean'", + "regexp|Only one of [regexp, versioned_file] should be defined", + "versioned_file|Only one of [regexp, versioned_file] should be defined" ); editor.assertHoverContains("bucket", "The name of the bucket"); 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 d1848cfc9..86d2eb6f1 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 @@ -20,8 +20,8 @@ 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.AbstractType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YAtomicType; -import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YTypedPropertyImpl; import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil; import org.springframework.ide.vscode.commons.yaml.schema.YValueHint; @@ -34,7 +34,7 @@ import com.google.common.collect.ImmutableSet; */ public class ManifestYmlSchema implements YamlSchema { - private final YBeanType TOPLEVEL_TYPE; + private final AbstractType TOPLEVEL_TYPE; private final YTypeUtil TYPE_UTIL; private final Callable> buildpackProvider; @@ -55,7 +55,7 @@ public class ManifestYmlSchema implements YamlSchema { // define schema types TOPLEVEL_TYPE = f.ybean("Cloudfoundry Manifest"); - YBeanType application = f.ybean("Application"); + AbstractType application = f.ybean("Application"); YAtomicType t_path = f.yatomic("Path"); YAtomicType t_buildpack = f.yatomic("Buildpack"); @@ -136,7 +136,7 @@ public class ManifestYmlSchema implements YamlSchema { } @Override - public YBeanType getTopLevelType() { + public AbstractType getTopLevelType() { return TOPLEVEL_TYPE; } diff --git a/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchemaTest.java b/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchemaTest.java index 1ef978b52..5d7af42c8 100644 --- a/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchemaTest.java +++ b/vscode-extensions/vscode-manifest-yaml/src/test/java/org/springframework/ide/vscode/manifest/yaml/ManifestYmlSchemaTest.java @@ -21,7 +21,7 @@ import org.junit.Test; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty; -import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanType; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YSeqType; import org.springframework.ide.vscode.manifest.yaml.ManifestYmlSchema; @@ -131,7 +131,7 @@ public class ManifestYmlSchemaTest { private List getNestedProps() { YSeqType applications = (YSeqType) schema.getTopLevelType().getPropertiesMap().get("applications").getType(); - YBeanType application = (YBeanType) applications.getDomainType(); + AbstractType application = (AbstractType) applications.getDomainType(); return application.getProperties(); }