diff --git a/core/src/main/java/io/cucumber/core/backend/DataTableTypeTypeDefinition.java b/core/src/main/java/io/cucumber/core/backend/DataTableTypeTypeDefinition.java new file mode 100644 index 0000000000..42ee8b1234 --- /dev/null +++ b/core/src/main/java/io/cucumber/core/backend/DataTableTypeTypeDefinition.java @@ -0,0 +1,11 @@ +package io.cucumber.core.backend; + +import io.cucumber.datatable.DataTableType; +import org.apiguardian.api.API; + +@API(status = API.Status.EXPERIMENTAL) +public interface DataTableTypeTypeDefinition { + + DataTableType dataTableType(); + +} diff --git a/core/src/main/java/io/cucumber/core/backend/Glue.java b/core/src/main/java/io/cucumber/core/backend/Glue.java index c8dca44f92..a427dcf1df 100644 --- a/core/src/main/java/io/cucumber/core/backend/Glue.java +++ b/core/src/main/java/io/cucumber/core/backend/Glue.java @@ -1,6 +1,7 @@ package io.cucumber.core.backend; import io.cucumber.core.stepexpression.TypeRegistry; +import io.cucumber.datatable.DataTableType; import org.apiguardian.api.API; import java.util.function.Function; @@ -20,4 +21,6 @@ public interface Glue { void addParameterType(ParameterTypeDefinition parameterTypeDefinition); + void addDataTableType(DataTableTypeTypeDefinition dataTableTypeTypeDefinition); + } diff --git a/core/src/main/java/io/cucumber/core/runner/CachingGlue.java b/core/src/main/java/io/cucumber/core/runner/CachingGlue.java index 43290880d0..dc5dd35c86 100644 --- a/core/src/main/java/io/cucumber/core/runner/CachingGlue.java +++ b/core/src/main/java/io/cucumber/core/runner/CachingGlue.java @@ -1,5 +1,6 @@ package io.cucumber.core.runner; +import io.cucumber.core.backend.DataTableTypeTypeDefinition; import io.cucumber.core.backend.ParameterTypeDefinition; import io.cucumber.core.event.StepDefinedEvent; import io.cucumber.core.backend.DuplicateStepDefinitionException; @@ -86,6 +87,11 @@ public void addParameterType(ParameterTypeDefinition parameterTypeDefinition) { typeRegistry.defineParameterType(parameterTypeDefinition.parameterType()); } + @Override + public void addDataTableType(DataTableTypeTypeDefinition dataTableTypeTypeDefinition) { + typeRegistry.defineDataTableType(dataTableTypeTypeDefinition.dataTableType()); + } + List getBeforeHooks() { return new ArrayList<>(beforeHooks); } @@ -131,7 +137,7 @@ PickleStepDefinitionMatch stepDefinitionMatch(String featurePath, PickleStep ste } private List stepDefinitionMatches(String featurePath, PickleStep step) { - List result = new ArrayList(); + List result = new ArrayList<>(); for (StepDefinition stepDefinition : stepDefinitionsByPattern.values()) { List arguments = stepDefinition.matchedArguments(step); if (arguments != null) { diff --git a/core/src/main/java/io/cucumber/core/runner/Runner.java b/core/src/main/java/io/cucumber/core/runner/Runner.java index de3a5911f9..80e5c1848b 100644 --- a/core/src/main/java/io/cucumber/core/runner/Runner.java +++ b/core/src/main/java/io/cucumber/core/runner/Runner.java @@ -1,13 +1,13 @@ package io.cucumber.core.runner; -import io.cucumber.core.event.HookType; -import io.cucumber.core.event.SnippetsSuggestedEvent; import gherkin.events.PickleEvent; import gherkin.pickles.PickleStep; import gherkin.pickles.PickleTag; import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.ObjectFactory; +import io.cucumber.core.event.HookType; +import io.cucumber.core.event.SnippetsSuggestedEvent; import io.cucumber.core.eventbus.EventBus; import io.cucumber.core.logging.Logger; import io.cucumber.core.logging.LoggerFactory; @@ -57,10 +57,13 @@ public EventBus getBus() { } public void runPickle(PickleEvent pickle) { - buildBackendWorlds(); // Java8 step definitions will be added to the glue here - TestCase testCase = createTestCaseForPickle(pickle); - testCase.run(bus); - disposeBackendWorlds(); + try { + buildBackendWorlds(); // Java8 step definitions will be added to the glue here + TestCase testCase = createTestCaseForPickle(pickle); + testCase.run(bus); + } finally { + disposeBackendWorlds(); + } } private TestCase createTestCaseForPickle(PickleEvent pickleEvent) { diff --git a/java/src/main/java/io/cucumber/java/DataTableType.java b/java/src/main/java/io/cucumber/java/DataTableType.java new file mode 100644 index 0000000000..b705e00fd0 --- /dev/null +++ b/java/src/main/java/io/cucumber/java/DataTableType.java @@ -0,0 +1,23 @@ +package io.cucumber.java; + +import org.apiguardian.api.API; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Allows a DataTableType to be registered. + * + * Supports TableCellTransformer: String -> T + * Supports TableEntryTransformer: Map -> T + * Supports TableRowTransformer: List -> T + * Supports TableTransformer: DataTable -> T + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@API(status = API.Status.STABLE) +public @interface DataTableType { + +} diff --git a/java/src/main/java/io/cucumber/java/JavaBackend.java b/java/src/main/java/io/cucumber/java/JavaBackend.java index 7ba83ad2d0..c807a3f6a8 100644 --- a/java/src/main/java/io/cucumber/java/JavaBackend.java +++ b/java/src/main/java/io/cucumber/java/JavaBackend.java @@ -95,6 +95,8 @@ void addHook(Annotation annotation, Method method) { boolean useForSnippets = parameterType.useForSnippets(); boolean preferForRegexMatch = parameterType.preferForRegexMatch(); glue.addParameterType(new JavaParameterTypeDefinition(name, pattern, method, useForSnippets, preferForRegexMatch, lookup)); + } else if (annotation.annotationType().equals(DataTableType.class)) { + glue.addDataTableType(new JavaDataTableTypeDefinition(method, lookup)); } } } diff --git a/java/src/main/java/io/cucumber/java/JavaDataTableTypeDefinition.java b/java/src/main/java/io/cucumber/java/JavaDataTableTypeDefinition.java new file mode 100644 index 0000000000..d5d0e7b4d1 --- /dev/null +++ b/java/src/main/java/io/cucumber/java/JavaDataTableTypeDefinition.java @@ -0,0 +1,122 @@ +package io.cucumber.java; + +import io.cucumber.core.backend.DataTableTypeTypeDefinition; +import io.cucumber.core.backend.Lookup; +import io.cucumber.core.exception.CucumberException; +import io.cucumber.core.runtime.Invoker; +import io.cucumber.datatable.DataTable; +import io.cucumber.datatable.DataTableType; +import io.cucumber.datatable.TableCellTransformer; +import io.cucumber.datatable.TableEntryTransformer; +import io.cucumber.datatable.TableRowTransformer; +import io.cucumber.datatable.TableTransformer; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +class JavaDataTableTypeDefinition implements DataTableTypeTypeDefinition { + + private final Method method; + private final Lookup lookup; + private final DataTableType dataTableType; + + JavaDataTableTypeDefinition(Method method, Lookup lookup) { + this.method = method; + this.lookup = lookup; + this.dataTableType = createDataTableType(method); + } + + @SuppressWarnings("unchecked") + private DataTableType createDataTableType(Method method) { + Class returnType = requireValidReturnType(method); + Type parameterType = requireValidParameterType(method); + + if (DataTable.class.equals(parameterType)) { + return new DataTableType( + returnType, + (TableTransformer) this::execute + ); + } + + if (List.class.equals(parameterType)) { + return new DataTableType( + returnType, + (TableRowTransformer) this::execute + ); + } + + if (Map.class.equals(parameterType)) { + return new DataTableType( + returnType, + (TableEntryTransformer) this::execute + ); + } + + if (String.class.equals(parameterType)) { + return new DataTableType( + returnType, + (TableCellTransformer) this::execute + ); + } + + throw createInvalidSignatureException(); + + } + + private static CucumberException createInvalidSignatureException() { + return new CucumberException("" + + "A @DataTableType annotated method must have one of these signatures:\n" + + " * public Author author(DataTable table)\n" + + " * public Author author(List row)\n" + + " * public Author author(Map entry)\n" + + " * public Author author(String cell)\n" + + "Note: Author is an example of the class you want to convert the table to" + ); + } + + + private static Type requireValidParameterType(Method method) { + Type[] parameterTypes = method.getGenericParameterTypes(); + if (parameterTypes.length != 1) { + throw createInvalidSignatureException(); + } + + Type parameterType = parameterTypes[0]; + if (!(parameterType instanceof ParameterizedType)) { + return parameterType; + } + + ParameterizedType parameterizedType = (ParameterizedType) parameterType; + Type[] typeParameters = parameterizedType.getActualTypeArguments(); + for (Type typeParameter : typeParameters) { + if (!String.class.equals(typeParameter)) { + throw createInvalidSignatureException(); + } + } + + return parameterizedType.getRawType(); + } + + private static Class requireValidReturnType(Method method) { + Class returnType = method.getReturnType(); + if (Void.class.equals(returnType)) { + throw createInvalidSignatureException(); + } + return returnType; + } + + + @Override + public DataTableType dataTableType() { + return dataTableType; + } + + + private Object execute(Object arg) throws Throwable { + return Invoker.invoke(lookup.getInstance(method.getDeclaringClass()), method, 0, arg); + } + +} diff --git a/java/src/main/java/io/cucumber/java/JavaParameterTypeDefinition.java b/java/src/main/java/io/cucumber/java/JavaParameterTypeDefinition.java index 847ab8061b..08ad01a348 100644 --- a/java/src/main/java/io/cucumber/java/JavaParameterTypeDefinition.java +++ b/java/src/main/java/io/cucumber/java/JavaParameterTypeDefinition.java @@ -8,58 +8,60 @@ import java.lang.reflect.Method; import java.util.Collections; -import java.util.List; -public class JavaParameterTypeDefinition implements ParameterTypeDefinition { +class JavaParameterTypeDefinition implements ParameterTypeDefinition { - private final String name; - private final List patterns; private final Method method; private final Lookup lookup; - private final boolean preferForRegexpMatch; - private final boolean useForSnippets; + private final ParameterType parameterType; JavaParameterTypeDefinition(String name, String pattern, Method method, boolean useForSnippets, boolean preferForRegexpMatch, Lookup lookup) { - this.name = name.isEmpty() ? method.getName() : name; - this.patterns = Collections.singletonList(pattern); this.method = requireValidMethod(method); this.lookup = lookup; - this.useForSnippets = useForSnippets; - this.preferForRegexpMatch = preferForRegexpMatch; + this.parameterType = new ParameterType<>( + name.isEmpty() ? method.getName() : name, + Collections.singletonList(pattern), + this.method.getReturnType(), + this::execute, + useForSnippets, + preferForRegexpMatch + ); } private Method requireValidMethod(Method method) { Class returnType = method.getReturnType(); if (Void.class.equals(returnType)) { - throw new CucumberException("TODO"); + throw createInvalidSignatureException(); } Class[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length < 1) { - throw new CucumberException("TODO"); + throw createInvalidSignatureException(); } - for (int i = 0; i < parameterTypes.length; i++) { - Class parameterType = parameterTypes[i]; + for (Class parameterType : parameterTypes) { if (!String.class.equals(parameterType)) { - throw new CucumberException("TODO" + i); + throw createInvalidSignatureException(); } } return method; } + private CucumberException createInvalidSignatureException() { + return new CucumberException("" + + "A @ParameterType annotated method must have one of these signatures:\n" + + " * public Author parameterName(String all)\n" + + " * public Author parameterName(String captureGroup1, String captureGroup2, ...ect )\n" + + " * public Author parameterName(String... captureGroups)\n" + + "Note: Author is an example of the class you want to convert parameter name" + ); + } + @Override public ParameterType parameterType() { - return new ParameterType<>( - name, - patterns, - method.getReturnType(), - this::execute, - useForSnippets, - preferForRegexpMatch - ); + return parameterType; } private Object execute(Object[] args) throws Throwable { diff --git a/java/src/main/java/io/cucumber/java/MethodScanner.java b/java/src/main/java/io/cucumber/java/MethodScanner.java index 2f7522385b..6ff23e92f0 100644 --- a/java/src/main/java/io/cucumber/java/MethodScanner.java +++ b/java/src/main/java/io/cucumber/java/MethodScanner.java @@ -84,6 +84,7 @@ private boolean isHookAnnotation(Annotation annotation) { || annotationClass.equals(BeforeStep.class) || annotationClass.equals(AfterStep.class) || annotationClass.equals(ParameterType.class) + || annotationClass.equals(DataTableType.class) ; } diff --git a/java/src/main/java/io/cucumber/java/ParameterType.java b/java/src/main/java/io/cucumber/java/ParameterType.java index 4064f561a3..10846c038c 100644 --- a/java/src/main/java/io/cucumber/java/ParameterType.java +++ b/java/src/main/java/io/cucumber/java/ParameterType.java @@ -7,6 +7,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Defines a parameter type. + * + * Method signature must have a String argument for each capture group + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @API(status = API.Status.STABLE) diff --git a/java/src/test/java/io/cucumber/java/annotation/Authors.java b/java/src/test/java/io/cucumber/java/annotation/DataTableStepdefs.java similarity index 51% rename from java/src/test/java/io/cucumber/java/annotation/Authors.java rename to java/src/test/java/io/cucumber/java/annotation/DataTableStepdefs.java index 4023b23aea..88e348376b 100644 --- a/java/src/test/java/io/cucumber/java/annotation/Authors.java +++ b/java/src/test/java/io/cucumber/java/annotation/DataTableStepdefs.java @@ -1,35 +1,40 @@ package io.cucumber.java.annotation; +import io.cucumber.java.DataTableType; import io.cucumber.java.Transpose; import io.cucumber.java.en.Given; import java.util.List; +import java.util.Map; +import java.util.Objects; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -public class Authors { +public class DataTableStepdefs { - private final Author expected = new Author("Annie M. G.", "Schmidt", "1911-03-20"); + private final Author expectedAuthor = new Author("Annie M. G.", "Schmidt", "1911-03-20"); + private final Person expectedPerson = new Person("Astrid", "Lindgren"); @Given("a list of authors in a table") - public void aListOfAuthorsInATable(List authors) throws Throwable { - assertTrue(authors.contains(expected)); + public void aListOfAuthorsInATable(List authors) { + assertTrue(authors.contains(expectedAuthor)); } @Given("a list of authors in a transposed table") - public void aListOfAuthorsInATransposedTable(@Transpose List authors) throws Throwable { - assertTrue(authors.contains(expected)); + public void aListOfAuthorsInATransposedTable(@Transpose List authors) { + assertTrue(authors.contains(expectedAuthor)); } @Given("a single author in a table") - public void aSingleAuthorInATable(Author author) throws Throwable { - assertEquals(expected, author); + public void aSingleAuthorInATable(Author author) { + assertEquals(expectedAuthor, author); } @Given("a single author in a transposed table") - public void aSingleAuthorInATransposedTable(@Transpose Author author) throws Throwable { - assertEquals(expected, author); + public void aSingleAuthorInATransposedTable(@Transpose Author author) { + assertEquals(expectedAuthor, author); } public static class Author { @@ -72,4 +77,43 @@ public int hashCode() { return result; } } + + @Given("a list of people in a table") + public void this_table_of_authors(List persons) { + assertTrue(persons.contains(expectedPerson)); + } + + @DataTableType + public DataTableStepdefs.Person transform(Map tableEntry) { + return new Person(tableEntry.get("first"), tableEntry.get("last")); + } + + public static class Person { + private final String first; + private final String last; + + public Person(String first, String last) { + this.first = first; + this.last = last; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Person person = (Person) o; + return first.equals(person.first) && + last.equals(person.last); + } + + @Override + public int hashCode() { + return Objects.hash(first, last); + } + + + } + + } diff --git a/java/src/test/java/io/cucumber/java/annotation/ParameterTypeStepdefs.java b/java/src/test/java/io/cucumber/java/annotation/ParameterTypeStepdefs.java new file mode 100644 index 0000000000..461ddc5a97 --- /dev/null +++ b/java/src/test/java/io/cucumber/java/annotation/ParameterTypeStepdefs.java @@ -0,0 +1,28 @@ +package io.cucumber.java.annotation; + +import io.cucumber.java.ParameterType; +import io.cucumber.java.en.Given; +import org.junit.jupiter.api.Assertions; + +import java.time.LocalDate; + +public class ParameterTypeStepdefs { + + private final LocalDate expected = LocalDate.of(1907, 11, 14); + + @ParameterType("([0-9]{4})-([0-9]{2})-([0-9]{2})") + public LocalDate parameterTypeIso8601Date(String year, String month, String day) { + return LocalDate.of(Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day)); + } + + @Given("today is {parameterTypeIso8601Date}") + public void today_is(LocalDate date) { + + Assertions.assertEquals(expected, date); + } + + @Given("tomorrow is {parameterTypeRegistryIso8601Date}") + public void tomorrow_is(LocalDate date) { + Assertions.assertEquals(expected, date); + } +} diff --git a/java/src/test/java/io/cucumber/java/annotation/Stepdefs.java b/java/src/test/java/io/cucumber/java/annotation/Stepdefs.java index 6c5d99f38f..c9ccd045fb 100644 --- a/java/src/test/java/io/cucumber/java/annotation/Stepdefs.java +++ b/java/src/test/java/io/cucumber/java/annotation/Stepdefs.java @@ -5,16 +5,12 @@ import java.util.List; public class Stepdefs { + @Given("I have {int} cukes in the belly") public void I_have_cukes_in_the_belly(int arg1) { } - @Given("this data table:") - public void this_data_table(List people) throws Throwable { - } - public static class Person { - String first; - String last; - } + + } diff --git a/java/src/test/java/io/cucumber/java/annotation/TypeRegistryConfiguration.java b/java/src/test/java/io/cucumber/java/annotation/TypeRegistryConfiguration.java index fd7dce76cc..5654a53328 100644 --- a/java/src/test/java/io/cucumber/java/annotation/TypeRegistryConfiguration.java +++ b/java/src/test/java/io/cucumber/java/annotation/TypeRegistryConfiguration.java @@ -1,13 +1,14 @@ package io.cucumber.java.annotation; -import io.cucumber.core.api.TypeRegistryConfigurer; import io.cucumber.core.api.TypeRegistry; -import io.cucumber.datatable.DataTable; +import io.cucumber.core.api.TypeRegistryConfigurer; +import io.cucumber.cucumberexpressions.CaptureGroupTransformer; +import io.cucumber.cucumberexpressions.ParameterType; import io.cucumber.datatable.DataTableType; import io.cucumber.datatable.TableEntryTransformer; -import io.cucumber.java.annotation.Authors.Author; import io.cucumber.datatable.TableTransformer; +import java.time.LocalDate; import java.util.Locale; import java.util.Map; @@ -15,31 +16,24 @@ public class TypeRegistryConfiguration implements TypeRegistryConfigurer { - private final TableEntryTransformer personEntryTransformer = new TableEntryTransformer() { - @Override - public Stepdefs.Person transform(Map tableEntry) { - Stepdefs.Person person = new Stepdefs.Person(); - person.first = tableEntry.get("first"); - person.last = tableEntry.get("last"); - return person; - } - }; - private final TableEntryTransformer authorEntryTransformer = new TableEntryTransformer() { - @Override - public Author transform(Map tableEntry) { - return new Author( - tableEntry.get("firstName"), - tableEntry.get("lastName"), - tableEntry.get("birthDate")); - } - }; - private final TableTransformer singleAuthorTransformer = new TableTransformer() { - @Override - public Author transform(DataTable table) throws Throwable { + private final TableEntryTransformer authorEntryTransformer = + tableEntry -> new DataTableStepdefs.Author( + tableEntry.get("firstName"), + tableEntry.get("lastName"), + tableEntry.get("birthDate")); + + private final TableTransformer singleAuthorTransformer = + table -> { Map tableEntry = table.asMaps().get(0); return authorEntryTransformer.transform(tableEntry); - } - }; + }; + + private final CaptureGroupTransformer localDateParameterType = + (String[] args) -> LocalDate.of( + Integer.parseInt(args[0]), + Integer.parseInt(args[1]), + Integer.parseInt(args[2]) + ); @Override public Locale locale() { @@ -49,15 +43,19 @@ public Locale locale() { @Override public void configureTypeRegistry(TypeRegistry typeRegistry) { typeRegistry.defineDataTableType(new DataTableType( - Author.class, + DataTableStepdefs.Author.class, authorEntryTransformer)); typeRegistry.defineDataTableType(new DataTableType( - Author.class, + DataTableStepdefs.Author.class, singleAuthorTransformer)); - typeRegistry.defineDataTableType(new DataTableType( - Stepdefs.Person.class, - personEntryTransformer)); + + typeRegistry.defineParameterType(new ParameterType<>( + "parameterTypeRegistryIso8601Date", + "([0-9]{4})/([0-9]{2})/([0-9]{2})", + LocalDate.class, + localDateParameterType + )); } } diff --git a/java/src/test/resources/io/cucumber/java/annotation/authors.feature b/java/src/test/resources/io/cucumber/java/annotation/data-table.feature similarity index 63% rename from java/src/test/resources/io/cucumber/java/annotation/authors.feature rename to java/src/test/resources/io/cucumber/java/annotation/data-table.feature index 7b2c57d8d0..bcf1b280a6 100644 --- a/java/src/test/resources/io/cucumber/java/annotation/authors.feature +++ b/java/src/test/resources/io/cucumber/java/annotation/data-table.feature @@ -1,6 +1,6 @@ -Feature: Authors and tables +Feature: Datatable - Scenario: Some authors and tables + Scenario: Convert a table to a generic list via the ParameterTypeRegistry Given a list of authors in a table | firstName | lastName | birthDate | | Annie M. G. | Schmidt | 1911-03-20 | @@ -11,6 +11,8 @@ Feature: Authors and tables | lastName | Schmidt | Dahl | | birthDate | 1911-03-20 | 1916-09-13 | + Scenario: Convert a table to a single object via the ParameterTypeRegistry + Given a single author in a table | firstName | lastName | birthDate | | Annie M. G. | Schmidt | 1911-03-20 | @@ -20,3 +22,10 @@ Feature: Authors and tables | lastName | Schmidt | | birthDate | 1911-03-20 | + + Scenario: Convert a table to a generic list via the @DataTableType Annotation + + Given a list of people in a table + | first | last | + | Astrid | Lindgren | + | Roald | Dahl | \ No newline at end of file diff --git a/java/src/test/resources/io/cucumber/java/annotation/parameter-types.feature b/java/src/test/resources/io/cucumber/java/annotation/parameter-types.feature new file mode 100644 index 0000000000..84894790fd --- /dev/null +++ b/java/src/test/resources/io/cucumber/java/annotation/parameter-types.feature @@ -0,0 +1,7 @@ +Feature: ParameterTypes + + Scenario: Convert a parameter to date via the ParameterTypeRegistry + Given tomorrow is 1907/11/14 + + Scenario: Convert a parameter to date via the @ParameterType Annotation + Given today is 1907-11-14 diff --git a/java/src/test/resources/io/cucumber/java/annotation/table_conversion.feature b/java/src/test/resources/io/cucumber/java/annotation/table_conversion.feature deleted file mode 100644 index 7a4881ae4b..0000000000 --- a/java/src/test/resources/io/cucumber/java/annotation/table_conversion.feature +++ /dev/null @@ -1,7 +0,0 @@ -Feature: Table Conversion - - Scenario: use a table - Given this data table: - | first | last | - | Aslak | Hellesøy | - | Donald | Duck |