diff --git a/java/README.md b/java/README.md index 08d3552ccd..7ce31447ac 100644 --- a/java/README.md +++ b/java/README.md @@ -160,4 +160,47 @@ public class DataTableSteps { return objectMapper.convertValue(fromValue, objectMapper.constructType(toValueType)); } } -``` \ No newline at end of file +``` + +### Empty Cells + +Data tables in Gherkin can not represent null or the empty string unambiguously. +Cucumber will interpret empty cells as `null`. + +Empty string be represented using a replacement. For example `[empty]`. +The replacement can be configured by setting the `replaceWithEmptyString` +property of `DataTableType`, `DefaultDataTableCellTransformer` and +`DefaultDataTableEntryTransformerBody`. By default no replacement is configured. + +```gherkin +Given some authors + | name | first publication | + | Aspiring Author | | + | Ancient Author | [blank] | +``` + +```java +package com.example.app; + +import io.cucumber.java.DataTableType; +import io.cucumber.java.en.Given; + +import java.util.Map; +import java.util.List; + +public class DataTableSteps { + + @DataTableType(replaceWithEmptyString = "[blank]") + public Author convert(Map entry){ + return new Author( + entry.get("name"), + entry.get("first publication") + ); + } + + @Given("some authors") + public void given_some_authors(List authors){ + // authors = [Author(name="Aspiring Author", firstPublication=null), Author(name="Ancient Author", firstPublication=)] + } +} +``` diff --git a/java/src/main/java/io/cucumber/java/AbstractDatatableElementTransformerDefinition.java b/java/src/main/java/io/cucumber/java/AbstractDatatableElementTransformerDefinition.java new file mode 100644 index 0000000000..367b7dc5a1 --- /dev/null +++ b/java/src/main/java/io/cucumber/java/AbstractDatatableElementTransformerDefinition.java @@ -0,0 +1,67 @@ +package io.cucumber.java; + +import io.cucumber.core.backend.Lookup; +import io.cucumber.datatable.DataTable; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +public class AbstractDatatableElementTransformerDefinition extends AbstractGlueDefinition { + private final String[] emptyPatterns; + + AbstractDatatableElementTransformerDefinition(Method method, Lookup lookup, String[] emptyPatterns) { + super(method, lookup); + this.emptyPatterns = emptyPatterns; + } + + + List replaceEmptyPatternsWithEmptyString(List row) { + return row.stream() + .map(this::replaceEmptyPatternsWithEmptyString) + .collect(toList()); + } + + DataTable replaceEmptyPatternsWithEmptyString(DataTable table) { + List> rawWithEmptyStrings = table.cells().stream() + .map(this::replaceEmptyPatternsWithEmptyString) + .collect(toList()); + + return DataTable.create(rawWithEmptyStrings); //TODO: Add converter back in + } + + Map replaceEmptyPatternsWithEmptyString(Map fromValue) { + return fromValue.entrySet().stream() + .collect(toMap( + entry -> replaceEmptyPatternsWithEmptyString(entry.getKey()), + entry -> replaceEmptyPatternsWithEmptyString(entry.getValue()), + (s, s2) -> { + throw createDuplicateKeyAfterReplacement(fromValue); + } + )); + } + + private IllegalArgumentException createDuplicateKeyAfterReplacement(Map fromValue) { + List conflict = new ArrayList<>(2); + for (String emptyPattern : emptyPatterns) { + if (fromValue.containsKey(emptyPattern)) { + conflict.add(emptyPattern); + } + } + String msg = "After replacing %s and %s with empty strings the datatable entry contains duplicate keys: %s"; + return new IllegalArgumentException(String.format(msg, conflict.get(0), conflict.get(1), fromValue)); + } + + String replaceEmptyPatternsWithEmptyString(String t) { + for (String emptyPattern : emptyPatterns) { + if (t.equals(emptyPattern)) { + return ""; + } + } + return t; + } +} diff --git a/java/src/main/java/io/cucumber/java/DataTableType.java b/java/src/main/java/io/cucumber/java/DataTableType.java index 32bfb44b2f..4d10ef657a 100644 --- a/java/src/main/java/io/cucumber/java/DataTableType.java +++ b/java/src/main/java/io/cucumber/java/DataTableType.java @@ -27,4 +27,16 @@ @API(status = API.Status.STABLE) public @interface DataTableType { + /** + * Replace these strings in the Datatable with empty strings. + *

+ * A data table can only represent absent and non-empty strings. By replacing + * a known value (for example [empty]) a data table can also represent + * empty strings. + *

+ * It is not recommended to use multiple replacements in the same table. + * + * @return strings to be replaced with empty strings. + */ + String[] replaceWithEmptyString() default {}; } diff --git a/java/src/main/java/io/cucumber/java/DefaultDataTableCellTransformer.java b/java/src/main/java/io/cucumber/java/DefaultDataTableCellTransformer.java index f8fc865081..857f6429d1 100644 --- a/java/src/main/java/io/cucumber/java/DefaultDataTableCellTransformer.java +++ b/java/src/main/java/io/cucumber/java/DefaultDataTableCellTransformer.java @@ -24,4 +24,17 @@ @API(status = API.Status.STABLE) public @interface DefaultDataTableCellTransformer { + /** + * Replace these strings in the Datatable with empty strings. + *

+ * A data table can only represent absent and non-empty strings. By replacing + * a known value (for example [empty]) a data table can also represent + * empty strings. + *

+ * It is not recommended to use multiple replacements in the same table. + * + * @return strings to be replaced with empty strings. + */ + String[] replaceWithEmptyString() default {}; + } diff --git a/java/src/main/java/io/cucumber/java/DefaultDataTableEntryTransformer.java b/java/src/main/java/io/cucumber/java/DefaultDataTableEntryTransformer.java index 219f4d5780..fbe4d23a31 100644 --- a/java/src/main/java/io/cucumber/java/DefaultDataTableEntryTransformer.java +++ b/java/src/main/java/io/cucumber/java/DefaultDataTableEntryTransformer.java @@ -33,4 +33,18 @@ * @return true if conversion should be be applied, true by default. */ boolean headersToProperties() default true; + + /** + * Replace these strings in the Datatable with empty strings. + *

+ * A data table can only represent absent and non-empty strings. By replacing + * a known value (for example [empty]) a data table can also represent + * empty strings. + *

+ * It is not recommended to use multiple replacements in the same table. + * + * @return strings to be replaced with empty strings. + */ + String[] replaceWithEmptyString() default {}; + } diff --git a/java/src/main/java/io/cucumber/java/GlueAdaptor.java b/java/src/main/java/io/cucumber/java/GlueAdaptor.java index e9084cd704..49a970ae80 100644 --- a/java/src/main/java/io/cucumber/java/GlueAdaptor.java +++ b/java/src/main/java/io/cucumber/java/GlueAdaptor.java @@ -45,15 +45,19 @@ void addDefinition(Method method, Annotation annotation) { boolean preferForRegexMatch = parameterType.preferForRegexMatch(); glue.addParameterType(new JavaParameterTypeDefinition(name, pattern, method, useForSnippets, preferForRegexMatch, lookup)); } else if (annotationType.equals(DataTableType.class)) { - glue.addDataTableType(new JavaDataTableTypeDefinition(method, lookup)); + DataTableType dataTableType = (DataTableType) annotation; + glue.addDataTableType(new JavaDataTableTypeDefinition(method, lookup, dataTableType.replaceWithEmptyString())); } else if (annotationType.equals(DefaultParameterTransformer.class)) { glue.addDefaultParameterTransformer(new JavaDefaultParameterTransformerDefinition(method, lookup)); } else if (annotationType.equals(DefaultDataTableEntryTransformer.class)) { DefaultDataTableEntryTransformer transformer = (DefaultDataTableEntryTransformer) annotation; boolean headersToProperties = transformer.headersToProperties(); - glue.addDefaultDataTableEntryTransformer(new JavaDefaultDataTableEntryTransformerDefinition(method, lookup, headersToProperties)); + String[] replaceWithEmptyString = transformer.replaceWithEmptyString(); + glue.addDefaultDataTableEntryTransformer(new JavaDefaultDataTableEntryTransformerDefinition(method, lookup, headersToProperties, replaceWithEmptyString)); } else if (annotationType.equals(DefaultDataTableCellTransformer.class)) { - glue.addDefaultDataTableCellTransformer(new JavaDefaultDataTableCellTransformerDefinition(method, lookup)); + DefaultDataTableCellTransformer cellTransformer = (DefaultDataTableCellTransformer) annotation; + String[] emptyPatterns = cellTransformer.replaceWithEmptyString(); + glue.addDefaultDataTableCellTransformer(new JavaDefaultDataTableCellTransformerDefinition(method, lookup, emptyPatterns)); } else if (annotationType.equals(DocStringType.class)){ DocStringType docStringType = (DocStringType) annotation; String contentType = docStringType.contentType(); diff --git a/java/src/main/java/io/cucumber/java/JavaDataTableTypeDefinition.java b/java/src/main/java/io/cucumber/java/JavaDataTableTypeDefinition.java index 189584ae92..c5fc8aec36 100644 --- a/java/src/main/java/io/cucumber/java/JavaDataTableTypeDefinition.java +++ b/java/src/main/java/io/cucumber/java/JavaDataTableTypeDefinition.java @@ -4,10 +4,6 @@ import io.cucumber.core.backend.Lookup; 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; @@ -17,12 +13,12 @@ import static io.cucumber.java.InvalidMethodSignatureException.builder; -class JavaDataTableTypeDefinition extends AbstractGlueDefinition implements DataTableTypeDefinition { +class JavaDataTableTypeDefinition extends AbstractDatatableElementTransformerDefinition implements DataTableTypeDefinition { private final DataTableType dataTableType; - JavaDataTableTypeDefinition(Method method, Lookup lookup) { - super(method, lookup); + JavaDataTableTypeDefinition(Method method, Lookup lookup, String[] emptyPatterns) { + super(method, lookup, emptyPatterns); this.dataTableType = createDataTableType(method); } @@ -75,33 +71,32 @@ private DataTableType createDataTableType(Method method) { if (DataTable.class.equals(parameterType)) { return new DataTableType( returnType, - (TableTransformer) this::execute + (DataTable table) -> execute(replaceEmptyPatternsWithEmptyString(table)) ); } if (List.class.equals(parameterType)) { return new DataTableType( returnType, - (TableRowTransformer) this::execute + (List row) -> execute(replaceEmptyPatternsWithEmptyString(row)) ); } if (Map.class.equals(parameterType)) { return new DataTableType( returnType, - (TableEntryTransformer) this::execute + (Map entry) -> execute(replaceEmptyPatternsWithEmptyString(entry)) ); } if (String.class.equals(parameterType)) { return new DataTableType( returnType, - (TableCellTransformer) this::execute + (String cell) -> execute(replaceEmptyPatternsWithEmptyString(cell)) ); } throw createInvalidSignatureException(method); - } @Override diff --git a/java/src/main/java/io/cucumber/java/JavaDefaultDataTableCellTransformerDefinition.java b/java/src/main/java/io/cucumber/java/JavaDefaultDataTableCellTransformerDefinition.java index 2d67c2d32a..abe5a28ff6 100644 --- a/java/src/main/java/io/cucumber/java/JavaDefaultDataTableCellTransformerDefinition.java +++ b/java/src/main/java/io/cucumber/java/JavaDefaultDataTableCellTransformerDefinition.java @@ -9,13 +9,14 @@ import static io.cucumber.java.InvalidMethodSignatureException.builder; -class JavaDefaultDataTableCellTransformerDefinition extends AbstractGlueDefinition implements DefaultDataTableCellTransformerDefinition { +class JavaDefaultDataTableCellTransformerDefinition extends AbstractDatatableElementTransformerDefinition implements DefaultDataTableCellTransformerDefinition { private final TableCellByTypeTransformer transformer; - JavaDefaultDataTableCellTransformerDefinition(Method method, Lookup lookup) { - super(requireValidMethod(method), lookup); - this.transformer = this::execute; + JavaDefaultDataTableCellTransformerDefinition(Method method, Lookup lookup, String[] emptyPatterns) { + super(requireValidMethod(method), lookup, emptyPatterns); + this.transformer = (cellValue, toValueType) -> + execute(replaceEmptyPatternsWithEmptyString(cellValue), toValueType); } private static Method requireValidMethod(Method method) { diff --git a/java/src/main/java/io/cucumber/java/JavaDefaultDataTableEntryTransformerDefinition.java b/java/src/main/java/io/cucumber/java/JavaDefaultDataTableEntryTransformerDefinition.java index 1d68ca6fbf..b8e331a57f 100644 --- a/java/src/main/java/io/cucumber/java/JavaDefaultDataTableEntryTransformerDefinition.java +++ b/java/src/main/java/io/cucumber/java/JavaDefaultDataTableEntryTransformerDefinition.java @@ -12,21 +12,20 @@ import static io.cucumber.java.InvalidMethodSignatureException.builder; -class JavaDefaultDataTableEntryTransformerDefinition extends AbstractGlueDefinition implements DefaultDataTableEntryTransformerDefinition { +class JavaDefaultDataTableEntryTransformerDefinition extends AbstractDatatableElementTransformerDefinition implements DefaultDataTableEntryTransformerDefinition { private final TableEntryByTypeTransformer transformer; private final boolean headersToProperties; JavaDefaultDataTableEntryTransformerDefinition(Method method, Lookup lookup) { - super(requireValidMethod(method), lookup); - this.headersToProperties = false; - this.transformer = this::execute; + this(method, lookup, false, new String[0]); } - JavaDefaultDataTableEntryTransformerDefinition(Method method, Lookup lookup, boolean headersToProperties) { - super(requireValidMethod(method), lookup); + JavaDefaultDataTableEntryTransformerDefinition(Method method, Lookup lookup, boolean headersToProperties, String[] emptyPatterns) { + super(requireValidMethod(method), lookup, emptyPatterns); this.headersToProperties = headersToProperties; - this.transformer = this::execute; + this.transformer = (entryValue, toValueType, cellTransformer) -> + execute(replaceEmptyPatternsWithEmptyString(entryValue), toValueType, cellTransformer); } private static Method requireValidMethod(Method method) { diff --git a/java/src/test/java/io/cucumber/java/JavaDataTableTypeDefinitionTest.java b/java/src/test/java/io/cucumber/java/JavaDataTableTypeDefinitionTest.java index c8d5f33051..b2c7a48081 100644 --- a/java/src/test/java/io/cucumber/java/JavaDataTableTypeDefinitionTest.java +++ b/java/src/test/java/io/cucumber/java/JavaDataTableTypeDefinitionTest.java @@ -30,59 +30,95 @@ public T getInstance(Class glueClass) { asList("c", "d") )); + private final DataTable emptyTable = DataTable.create(asList( + asList("a", "[empty]"), + asList("[empty]", "d") + )); + @Test void can_define_data_table_converter() throws NoSuchMethodException { Method method = JavaDataTableTypeDefinitionTest.class.getMethod("convert_data_table_to_string", DataTable.class); - JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup); - assertThat(definition.dataTableType().transform(dataTable.asLists()), is("convert_data_table_to_string")); + JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup, new String[0]); + assertThat(definition.dataTableType().transform(dataTable.asLists()), is("convert_data_table_to_string=[[a, b], [c, d]]")); + } + + @Test + void can_define_data_table_converter_with_empty_pattern() throws NoSuchMethodException { + Method method = JavaDataTableTypeDefinitionTest.class.getMethod("convert_data_table_to_string", DataTable.class); + JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup, new String[]{"[empty]"}); + assertThat(definition.dataTableType().transform(emptyTable.asLists()), is("convert_data_table_to_string=[[a, ], [, d]]")); } public String convert_data_table_to_string(DataTable table) { - return "convert_data_table_to_string"; + return "convert_data_table_to_string=" + table.cells(); } @Test void can_define_table_row_transformer() throws NoSuchMethodException { Method method = JavaDataTableTypeDefinitionTest.class.getMethod("convert_table_row_to_string", List.class); - JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup); + JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup, new String[0]); assertThat(definition.dataTableType().transform(dataTable.asLists()), - is(asList("convert_table_row_to_string", "convert_table_row_to_string"))); + is(asList("convert_table_row_to_string=[a, b]", "convert_table_row_to_string=[c, d]"))); + } + @Test + void can_define_table_row_transformer_with_empty_pattern() throws NoSuchMethodException { + Method method = JavaDataTableTypeDefinitionTest.class.getMethod("convert_table_row_to_string", List.class); + JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup, new String[]{"[empty]"}); + assertThat(definition.dataTableType().transform(emptyTable.asLists()), + is(asList("convert_table_row_to_string=[a, ]", "convert_table_row_to_string=[, d]"))); } public String convert_table_row_to_string(List row) { - return "convert_table_row_to_string"; + return "convert_table_row_to_string=" + row; } @Test void can_define_table_entry_transformer() throws NoSuchMethodException { Method method = JavaDataTableTypeDefinitionTest.class.getMethod("converts_table_entry_to_string", Map.class); - JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup); + JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup, new String[0]); assertThat(definition.dataTableType().transform(dataTable.asLists()), - is(singletonList("converts_table_entry_to_string"))); + is(singletonList("converts_table_entry_to_string={a=c, b=d}"))); + } + @Test + void can_define_table_entry_transformer_with_empty_pattern() throws NoSuchMethodException { + Method method = JavaDataTableTypeDefinitionTest.class.getMethod("converts_table_entry_to_string", Map.class); + JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup, new String[]{"[empty]"}); + assertThat(definition.dataTableType().transform(emptyTable.asLists()), + is(singletonList("converts_table_entry_to_string={=d, a=}"))); } public String converts_table_entry_to_string(Map entry) { - return "converts_table_entry_to_string"; + return "converts_table_entry_to_string=" + entry; } @Test void can_define_table_cell_transformer() throws NoSuchMethodException { Method method = JavaDataTableTypeDefinitionTest.class.getMethod("converts_table_cell_to_string", String.class); - JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup); + JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup, new String[0]); assertThat(definition.dataTableType().transform(dataTable.asLists()), is(asList( - asList("converts_table_cell_to_string", "converts_table_cell_to_string"), - asList("converts_table_cell_to_string", "converts_table_cell_to_string")) + asList("converts_table_cell_to_string=a", "converts_table_cell_to_string=b"), + asList("converts_table_cell_to_string=c", "converts_table_cell_to_string=d")) + )); + } + + @Test + void can_define_table_cell_transformer_with_empty_pattern() throws NoSuchMethodException { + Method method = JavaDataTableTypeDefinitionTest.class.getMethod("converts_table_cell_to_string", String.class); + JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup, new String[0]); + assertThat(definition.dataTableType().transform(emptyTable.asLists()), is(asList( + asList("converts_table_cell_to_string=a", "converts_table_cell_to_string=[empty]"), + asList("converts_table_cell_to_string=[empty]", "converts_table_cell_to_string=d")) )); } public String converts_table_cell_to_string(String cell) { - return "converts_table_cell_to_string"; + return "converts_table_cell_to_string=" + cell; } @Test void target_type_must_class_type() throws NoSuchMethodException { Method method = JavaDataTableTypeDefinitionTest.class.getMethod("converts_datatable_to_optional_string", DataTable.class); - JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup); + JavaDataTableTypeDefinition definition = new JavaDataTableTypeDefinition(method, lookup, new String[0]); assertThat(definition.dataTableType().transform(dataTable.asLists()), is(Optional.of("converts_datatable_to_optional_string"))); } @@ -94,7 +130,7 @@ public Optional converts_datatable_to_optional_string(DataTable table) { @Test void target_type_must_not_be_void() throws NoSuchMethodException { Method method = JavaDataTableTypeDefinitionTest.class.getMethod("converts_data_table_to_void", DataTable.class); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(method, lookup)); + assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(method, lookup, new String[0])); } public void converts_data_table_to_void(DataTable table) { @@ -103,9 +139,9 @@ public void converts_data_table_to_void(DataTable table) { @Test void must_have_exactly_one_argument() throws NoSuchMethodException { Method noArgs = JavaDataTableTypeDefinitionTest.class.getMethod("converts_nothing_to_string"); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(noArgs, lookup)); + assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(noArgs, lookup, new String[0])); Method twoArgs = JavaDataTableTypeDefinitionTest.class.getMethod("converts_two_strings_to_string", String.class, String.class); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(twoArgs, lookup)); + assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(twoArgs, lookup, new String[0])); } public String converts_nothing_to_string() { @@ -113,27 +149,27 @@ public String converts_nothing_to_string() { } public String converts_two_strings_to_string(String arg1, String arg2) { - return "converts_two_strings_to_string"; + return "converts_two_strings_to_string=" + arg1 + "+" + arg2; } @Test void argument_must_match_existing_transformer() throws NoSuchMethodException { Method method = JavaDataTableTypeDefinitionTest.class.getMethod("converts_object_to_string", Object.class); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(method, lookup)); + assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(method, lookup, new String[0])); } public String converts_object_to_string(Object string) { - return "converts_object_to_string"; + return "converts_object_to_string=" + string; } @Test void table_entry_transformer_must_have_map_of_strings() throws NoSuchMethodException { Method method = JavaDataTableTypeDefinitionTest.class.getMethod("converts_map_of_objects_to_string", Map.class); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(method, lookup)); + assertThrows(InvalidMethodSignatureException.class, () -> new JavaDataTableTypeDefinition(method, lookup, new String[0])); } public String converts_map_of_objects_to_string(Map entry) { - return "converts_map_of_objects_to_string"; + return "converts_map_of_objects_to_string=" + entry; } } diff --git a/java/src/test/java/io/cucumber/java/JavaDefaultDataTableCellTransformerDefinitionTest.java b/java/src/test/java/io/cucumber/java/JavaDefaultDataTableCellTransformerDefinitionTest.java index 8d8edb957e..af1d10badb 100644 --- a/java/src/test/java/io/cucumber/java/JavaDefaultDataTableCellTransformerDefinitionTest.java +++ b/java/src/test/java/io/cucumber/java/JavaDefaultDataTableCellTransformerDefinitionTest.java @@ -26,19 +26,27 @@ public T getInstance(Class glueClass) { @Test void can_transform_string_to_type() throws Throwable { Method method = JavaDefaultDataTableCellTransformerDefinitionTest.class.getMethod("transform_string_to_type", String.class, Type.class); - JavaDefaultDataTableCellTransformerDefinition definition = new JavaDefaultDataTableCellTransformerDefinition(method, lookup); + JavaDefaultDataTableCellTransformerDefinition definition = new JavaDefaultDataTableCellTransformerDefinition(method, lookup, new String[0]); Object transformed = definition.tableCellByTypeTransformer().transform("something", String.class); - assertThat(transformed, is("transform_string_to_type")); + assertThat(transformed, is("transform_string_to_type=something")); + } + + @Test + void can_transform_string_to_empty() throws Throwable { + Method method = JavaDefaultDataTableCellTransformerDefinitionTest.class.getMethod("transform_string_to_type", String.class, Type.class); + JavaDefaultDataTableCellTransformerDefinition definition = new JavaDefaultDataTableCellTransformerDefinition(method, lookup, new String[]{"[empty]"}); + Object transformed = definition.tableCellByTypeTransformer().transform("[empty]", String.class); + assertThat(transformed, is("transform_string_to_type=")); } public Object transform_string_to_type(String fromValue, Type toValueType) { - return "transform_string_to_type"; + return "transform_string_to_type=" + fromValue; } @Test void can_transform_object_to_type() throws Throwable { Method method = JavaDefaultDataTableCellTransformerDefinitionTest.class.getMethod("transform_object_to_type", Object.class, Type.class); - JavaDefaultDataTableCellTransformerDefinition definition = new JavaDefaultDataTableCellTransformerDefinition(method, lookup); + JavaDefaultDataTableCellTransformerDefinition definition = new JavaDefaultDataTableCellTransformerDefinition(method, lookup, new String[0]); Object transformed = definition.tableCellByTypeTransformer().transform("something", String.class); assertThat(transformed, is("transform_object_to_type")); } @@ -50,7 +58,7 @@ public Object transform_object_to_type(Object fromValue, Type toValueType) { @Test void must_have_non_void_return() throws Throwable { Method method = JavaDefaultDataTableCellTransformerDefinitionTest.class.getMethod("transforms_string_to_void", String.class, Type.class); - InvalidMethodSignatureException exception = assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(method, lookup)); + InvalidMethodSignatureException exception = assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(method, lookup , new String[0])); assertThat(exception.getMessage(), startsWith("" + "A @DefaultDataTableCellTransformer annotated method must have one of these signatures:\n" + " * public Object defaultDataTableCell(String fromValue, Type toValueType)\n" + @@ -65,9 +73,9 @@ public void transforms_string_to_void(String fromValue, Type toValueType) { @Test void must_have_two_arguments() throws Throwable { Method oneArg = JavaDefaultDataTableCellTransformerDefinitionTest.class.getMethod("one_argument", String.class); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(oneArg, lookup)); + assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(oneArg, lookup , new String[0])); Method threeArg = JavaDefaultDataTableCellTransformerDefinitionTest.class.getMethod("three_arguments", String.class, Type.class, Object.class); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(threeArg, lookup)); + assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(threeArg, lookup , new String[0])); } public Object one_argument(String fromValue) { @@ -81,7 +89,7 @@ public Object three_arguments(String fromValue, Type toValueType, Object extra) @Test void must_have_string_or_object_as_from_value() throws Throwable { Method threeArg = JavaDefaultDataTableCellTransformerDefinitionTest.class.getMethod("map_as_from_value", Map.class, Type.class); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(threeArg, lookup)); + assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(threeArg, lookup , new String[0])); } @@ -92,7 +100,7 @@ public Object map_as_from_value(Map fromValue, Type toValueType) @Test void must_have_type_as_to_value_type() throws Throwable { Method threeArg = JavaDefaultDataTableCellTransformerDefinitionTest.class.getMethod("object_as_to_value_type", String.class, Object.class); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(threeArg, lookup)); + assertThrows(InvalidMethodSignatureException.class, () -> new JavaDefaultDataTableCellTransformerDefinition(threeArg, lookup , new String[0])); } public Object object_as_to_value_type(String fromValue, Object toValueType) { diff --git a/java/src/test/java/io/cucumber/java/JavaDefaultDataTableEntryTransformerDefinitionTest.java b/java/src/test/java/io/cucumber/java/JavaDefaultDataTableEntryTransformerDefinitionTest.java index b91af706e4..d555f563f6 100644 --- a/java/src/test/java/io/cucumber/java/JavaDefaultDataTableEntryTransformerDefinitionTest.java +++ b/java/src/test/java/io/cucumber/java/JavaDefaultDataTableEntryTransformerDefinitionTest.java @@ -6,6 +6,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -39,14 +40,40 @@ void transforms_with_correct_method() throws Throwable { assertThat(definition.tableEntryByTypeTransformer() .transform(fromValue, String.class, cellTransformer), is("key=value")); + } + + @Test + void transforms_empties_with_correct_method() throws Throwable { + Map fromValue = singletonMap("key", "[empty]"); + Method method = JavaDefaultDataTableEntryTransformerDefinitionTest.class.getMethod("correct_method", Map.class, Type.class); + JavaDefaultDataTableEntryTransformerDefinition definition = + new JavaDefaultDataTableEntryTransformerDefinition(method, lookup, false, new String[]{"[empty]"}); + assertThat(definition.tableEntryByTypeTransformer() + .transform(fromValue, String.class, cellTransformer), is("key=")); + } + + @Test + void throws_for_multiple_empties_with_correct_method() throws Throwable { + Map fromValue = new HashMap<>(); + fromValue.put("[empty]", "a"); + fromValue.put("[blank]", "b"); + Method method = JavaDefaultDataTableEntryTransformerDefinitionTest.class.getMethod("correct_method", Map.class, Type.class); + JavaDefaultDataTableEntryTransformerDefinition definition = + new JavaDefaultDataTableEntryTransformerDefinition(method, lookup, false, new String[]{"[empty]", "[blank]"}); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> definition.tableEntryByTypeTransformer().transform(fromValue, String.class, cellTransformer) + ); + + assertThat(exception.getMessage(), is("After replacing [empty] and [blank] with empty strings the datatable entry contains duplicate keys: {[blank]=b, [empty]=a}")); } public T correct_method(Map fromValue, Type toValueType) { return join(fromValue); } - @Test void transforms_with_correct_method_with_cell_transformer() throws Throwable { Method method = JavaDefaultDataTableEntryTransformerDefinitionTest.class.getMethod("correct_method_with_cell_transformer", Map.class, Type.class, TableCellByTypeTransformer.class); @@ -55,7 +82,6 @@ void transforms_with_correct_method_with_cell_transformer() throws Throwable { assertThat(definition.tableEntryByTypeTransformer() .transform(fromValue, String.class, cellTransformer), is("key=value")); - } diff --git a/java/src/test/java/io/cucumber/java/annotation/DataTableSteps.java b/java/src/test/java/io/cucumber/java/annotation/DataTableSteps.java index f0922d0e81..dd9a8bcfc3 100644 --- a/java/src/test/java/io/cucumber/java/annotation/DataTableSteps.java +++ b/java/src/test/java/io/cucumber/java/annotation/DataTableSteps.java @@ -16,6 +16,7 @@ public class DataTableSteps { private final Author expectedAuthor = new Author("Annie M. G.", "Schmidt", "1911-03-20"); private final Person expectedPerson = new Person("Astrid", "Lindgren"); + private final Person mononymousPerson = new Person("Plato", ""); @DataTableType public Author authorEntryTransformer(Map entry) { @@ -94,9 +95,10 @@ public int hashCode() { @Given("a list of people in a table") public void this_table_of_authors(List persons) { assertTrue(persons.contains(expectedPerson)); + assertTrue(persons.contains(mononymousPerson)); } - @DataTableType + @DataTableType(replaceWithEmptyString = "[blank]") public DataTableSteps.Person transform(Map tableEntry) { return new Person(tableEntry.get("first"), tableEntry.get("last")); } diff --git a/java/src/test/resources/io/cucumber/java/annotation/data-table.feature b/java/src/test/resources/io/cucumber/java/annotation/data-table.feature index bcf1b280a6..a31584b6c1 100644 --- a/java/src/test/resources/io/cucumber/java/annotation/data-table.feature +++ b/java/src/test/resources/io/cucumber/java/annotation/data-table.feature @@ -28,4 +28,5 @@ Feature: Datatable Given a list of people in a table | first | last | | Astrid | Lindgren | - | Roald | Dahl | \ No newline at end of file + | Roald | Dahl | + | Plato | [blank] | \ No newline at end of file