diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParsers.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParsers.java index ce5f6d68b..80d4a4233 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParsers.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/ValueParsers.java @@ -26,6 +26,8 @@ public class ValueParsers { }; public static final ValueParser POS_INTEGER = integerRange(0, null); + + public static final ValueParser STRICTLY_POS_INTEGER = integerAtLeast(1); public static ValueParser integerAtLeast(final Integer lowerBound) { return integerRange(lowerBound, null); diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java index 32985dc82..1d3c4554f 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/YTypeFactory.java @@ -15,7 +15,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -38,10 +37,8 @@ import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.SimpleGlob; import org.springframework.ide.vscode.commons.util.SimpleGlob.Match; import org.springframework.ide.vscode.commons.util.ValueParser; -import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; -import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractUnionType; -import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanAndSequenceUnion; +import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanType; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint; import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraints; import org.springframework.ide.vscode.commons.yaml.snippet.TypeBasedSnippetProvider; diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java index 08c9a0a02..ca2c3bc66 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseValueParsers.java @@ -16,6 +16,7 @@ import org.springframework.ide.vscode.commons.util.RegexpParser; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.ValueParseException; import org.springframework.ide.vscode.commons.util.ValueParser; +import org.springframework.ide.vscode.commons.util.ValueParsers; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; import org.springframework.ide.vscode.commons.yaml.schema.SchemaContextAware; @@ -96,6 +97,23 @@ public class ConcourseValueParsers { + "you should use location instead." ); + public static final ValueParser IDENTIFIER = new RegexpParser( + "(\\d|[\\p{L}&&[^\\p{Lu}]]|\\-|_|\\.)*", + "Identifier", + "An identifier is a string value comprised of Unicode lowercase letters, Decimal numbers, hyphens (`-`)" + + " underscores (`_`) and periods (`.`)"); + + public static final ValueParser MAX_IN_FLIGHT_OR_ALL = (str) -> { + if (str.equals("all")) { + return str; + } + try { + return ValueParsers.STRICTLY_POS_INTEGER.parse(str); + } catch (Exception e ) { + throw new IllegalArgumentException("Expecting either 'all' or an integer greater than 0"); + } + }; + private static String createTimeRegexp() { String hours = "([0-2]?[0-9])"; String minutes = "([0-6][0-9])"; diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java index d9c63ce3e..3a7a49ad1 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/PipelineYmlSchema.java @@ -94,6 +94,8 @@ public class PipelineYmlSchema implements YamlSchema { public final YType t_string = f.yatomic("String"); public final YType t_ne_string = f.yatomic("String") .parseWith(ValueParsers.NE_STRING); + public final YType t_identifier = f.yatomic("Identifier") + .parseWith(ConcourseValueParsers.IDENTIFIER); public final YType t_opt_string = f.yatomic("String") .parseWith(YamlSchemaValueParsers.OPT_STRING); @@ -381,7 +383,7 @@ public class PipelineYmlSchema implements YamlSchema { addProp(setPipelineStep, "var_files", t_strings); YBeanType loadVarStep = f.ybean("LoadVarStep"); - addProp(loadVarStep, "load_var", t_ne_string); //TODO: t_identifier: see https://concourse-ci.org/config-basics.html#schema.identifier + addProp(loadVarStep, "load_var", t_identifier); addProp(loadVarStep, "file", t_ne_string).isRequired(true); addProp(loadVarStep, "format", f.yenum("LoadVarFormat", "json", "yaml", "yml", "trim", "raw")); addProp(loadVarStep, "reveal", t_boolean); @@ -418,6 +420,12 @@ public class PipelineYmlSchema implements YamlSchema { addProp(doStep, "do", f.yseq(step)); addProp(tryStep, "try", step); + YBeanType acrossVar = f.ybean("AcrossVar"); + addProp(acrossVar, "var", t_identifier).isPrimary(true); + addProp(acrossVar, "values", f.yseq(t_any)).isRequired(true); + addProp(acrossVar, "max_in_flight", f.yatomic("AcrossVarMaxInFlight").parseWith(ConcourseValueParsers.MAX_IN_FLIGHT_OR_ALL)); + addProp(acrossVar, "fail_fast", t_boolean); + // shared properties applicable for any subtype of Step: for (AbstractType subStep : stepTypes) { addProp(step, subStep, "on_success", step); @@ -427,6 +435,9 @@ public class PipelineYmlSchema implements YamlSchema { addProp(step, subStep, "tags", t_strings); addProp(step, subStep, "timeout", t_duration); addProp(step, subStep, "attempts", t_strictly_pos_integer); + if (subStep!=getStep && subStep!=putStep) { // doc says 'across' doesn't work with put and get + addProp(step, subStep, "across", f.yseq(acrossVar)); //TODO: create proper schema + } } models.setStepType(step); diff --git a/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/fail_fast.md b/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/fail_fast.md new file mode 100644 index 000000000..8c842fd44 --- /dev/null +++ b/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/fail_fast.md @@ -0,0 +1 @@ +Default `false`. When enabled, the `across` step will fail fast by returning as soon as any sub-step fails. This means that running steps will be interrupted and pending steps will no longer be scheduled. \ No newline at end of file diff --git a/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/max_in_flight.md b/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/max_in_flight.md new file mode 100644 index 000000000..405351b4a --- /dev/null +++ b/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/max_in_flight.md @@ -0,0 +1,2 @@ +Default 1. If set to `all`, the substep will run with all combinations of the current var in parallel. If set to a positive integer, only that number of substeps may run in parallel. + diff --git a/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/values.md b/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/values.md new file mode 100644 index 000000000..0367bb7fd --- /dev/null +++ b/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/values.md @@ -0,0 +1,4 @@ +The list of values that the [var](https://concourse-ci.org/across-step.html#schema.across_var.var) will iterate over when running the substep. If multiple vars are configured, all combinations of values across all vars will run. + +The list of values may also be interpolated. For instance, you may use the [`load_var` step](https://concourse-ci.org/load-var-step.html) to first load a list of [*value* schema](https://concourse-ci.org/config-basics.html#schema.value) into a [local var](https://concourse-ci.org/vars.html#local-vars), and then iterate across that dynamic list of values. + diff --git a/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/var.md b/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/var.md new file mode 100644 index 000000000..e97741094 --- /dev/null +++ b/headless-services/concourse-language-server/src/main/resources/desc/AcrossVar/var.md @@ -0,0 +1,3 @@ +The name of the variable that will be added to the ["." var source](https://concourse-ci.org/vars.html#local-vars). This variable will only be accessible in the scope of the step - each iteration of the step gets its own scope. + +If a variable of the same name already exists in the parent scope, a warning will be printed. \ No newline at end of file diff --git a/headless-services/concourse-language-server/src/main/resources/desc/Step/across.md b/headless-services/concourse-language-server/src/main/resources/desc/Step/across.md new file mode 100644 index 000000000..de3745fd7 --- /dev/null +++ b/headless-services/concourse-language-server/src/main/resources/desc/Step/across.md @@ -0,0 +1,3 @@ +Run a step multiple times with different combinations of variable values. + +See [Across Step Modifier](https://concourse-ci.org/across-step.html#across-step) \ No newline at end of file diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index 66d5fb2c6..ef4593ae5 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -74,6 +74,42 @@ public class ConcourseEditorTest { serverInitializer.setMaxCompletions(100); } + @Test public void GH_752_accross_step_modifier() throws Exception { + //See: https://github.com/spring-projects/sts4/issues/752 + //See: https://concourse-ci.org/across-step.html#schema.across + Editor editor = harness.newEditor( + "jobs:\n" + + "- name: job\n" + + " plan:\n" + + " - across:\n" + + " - var: some-text\n" + + " values: some_values\n" + + " max_in_flight: bad_flight\n" + + " fail_fast: is_fail_fast\n" + + " task: running-((.:some-text))\n" + + " config:\n" + + " platform: linux\n" + + " image_resource:\n" + + " type: docker-image\n" + + " source:\n" + + " repository: ubuntu\n" + + " run:\n" + + " path: echo\n" + + " args: [\"((.:some-text))\"]" + ); + editor.assertProblems( + "some_values|Expecting a 'Sequence'", + "bad_flight|'all' or an integer greater than 0", + "is_fail_fast|boolean" + ); + + editor.assertHoverContains("across", "Run a step multiple times with different combinations of variable values"); + editor.assertHoverContains("var", "The name of the variable that will be added"); + editor.assertHoverContains("values", "will iterate over"); + editor.assertHoverContains("max_in_flight", "number of substeps may run in parallel"); + editor.assertHoverContains("fail_fast", "steps will be interrupted and pending steps"); + } + @Test public void GH_752_repo_mirror() throws Exception { //See: // - https://github.com/concourse/registry-image-resource#:~:text=registry_mirror%3A%20Optional. @@ -4569,6 +4605,7 @@ public class ConcourseEditorTest { //"plan", exists //"public", exists //Completions for nested context (i.e. task step) + "→ across", "→ attempts", "→ config", "→ ensure", diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/IdentifierParserTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/IdentifierParserTest.java new file mode 100644 index 000000000..186bd6afa --- /dev/null +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/IdentifierParserTest.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2022 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.concourse; + +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.springframework.ide.vscode.commons.util.ExceptionUtil; +import org.springframework.ide.vscode.commons.util.ValueParser; + +import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.*; + +public class IdentifierParserTest { + + private ValueParser parser = ConcourseValueParsers.IDENTIFIER; + + @Test + public void goodExamples() throws Exception { + parser.parse("identifier123.ha-boo_lalala"); + parser.parse("simple_ident"); + parser.parse("anything-with-dashes-123"); + } + + @Test + public void badExamples() { + does_not_parse("spaces are bad"); + does_not_parse("upperCaseisBad"); + does_not_parse("strange@symbols"); + does_not_parse("strange!symbols"); + does_not_parse("strange:symbols"); + } + + private void does_not_parse(String string) { + try { + parser.parse(string); + fail("Should have failed parsing!"); + } catch (Exception e) { + assertContains("Identifier", ExceptionUtil.getMessage(e)); + } + } + +} diff --git a/headless-services/concourse-language-server/src/test/resources/workspace/gh_752_pipeline.yml b/headless-services/concourse-language-server/src/test/resources/workspace/gh_752_pipeline.yml new file mode 100644 index 000000000..808e89341 --- /dev/null +++ b/headless-services/concourse-language-server/src/test/resources/workspace/gh_752_pipeline.yml @@ -0,0 +1,17 @@ +jobs: +- name: job + plan: + - across: + - var: some-text + max_in_flight: 1 + fail_fast: alala + task: running-((.:some-text)) + config: + platform: linux + image_resource: + type: docker-image + source: + repository: ubuntu + run: + path: echo + args: ["((.:some-text))"] \ No newline at end of file