Skip to content

Commit

Permalink
[Java] Support empty strings and null values in data tables
Browse files Browse the repository at this point in the history
Empty data table cells can either be considered null or empty strings.
For example table below can converted to a map as
`{name=Aspiring Author, first publication=}` or
`{name=Aspiring Author, first publication=null}`.

```gherkin
 | name            | first publication |
 | Aspiring Author |                   |
```

And as demonstrated #1617 there are good reasons to default the empty
table cell to null. However this does not cover all cases. There are
however good use cases to use the empty string.

By declaring a table transformer with a replacement string it becomes
possible to explicitly disambiguate between the two scenarios. For
example:

```gherkin
Given some authors
   | name            | first publication |
   | Aspiring Author |                   |
   | Ancient Author  | [blank]           |
```

```java
@DataTableType(replaceWithEmptyString = "[blank]")
public Author convert(Map<String, String> entry){
  return new Author(
     entry.get("name"),
     entry.get("first publication")
  );
}

@given("some authors")
public void given_some_authors(List<Author> authors){
  // authors = [Author(name="Aspiring Author", firstPublication=null), Author(name="Ancient Author", firstPublication=)]
}
```
  • Loading branch information
mpkorstanje committed Jan 10, 2020
1 parent d2c28f1 commit 6320426
Show file tree
Hide file tree
Showing 15 changed files with 284 additions and 63 deletions.
45 changes: 44 additions & 1 deletion java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,47 @@ public class DataTableSteps {
return objectMapper.convertValue(fromValue, objectMapper.constructType(toValueType));
}
}
```
```

### 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<String, String> entry){
return new Author(
entry.get("name"),
entry.get("first publication")
);
}

@Given("some authors")
public void given_some_authors(List<Author> authors){
// authors = [Author(name="Aspiring Author", firstPublication=null), Author(name="Ancient Author", firstPublication=)]
}
}
```
Original file line number Diff line number Diff line change
@@ -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<String> replaceEmptyPatternsWithEmptyString(List<String> row) {
return row.stream()
.map(this::replaceEmptyPatternsWithEmptyString)
.collect(toList());
}

DataTable replaceEmptyPatternsWithEmptyString(DataTable table) {
List<List<String>> rawWithEmptyStrings = table.cells().stream()
.map(this::replaceEmptyPatternsWithEmptyString)
.collect(toList());

return DataTable.create(rawWithEmptyStrings); //TODO: Add converter back in
}

Map<String, String> replaceEmptyPatternsWithEmptyString(Map<String, String> fromValue) {
return fromValue.entrySet().stream()
.collect(toMap(
entry -> replaceEmptyPatternsWithEmptyString(entry.getKey()),
entry -> replaceEmptyPatternsWithEmptyString(entry.getValue()),
(s, s2) -> {
throw createDuplicateKeyAfterReplacement(fromValue);
}
));
}

private IllegalArgumentException createDuplicateKeyAfterReplacement(Map<String, String> fromValue) {
List<String> 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;
}
}
12 changes: 12 additions & 0 deletions java/src/main/java/io/cucumber/java/DataTableType.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,16 @@
@API(status = API.Status.STABLE)
public @interface DataTableType {

/**
* Replace these strings in the Datatable with empty strings.
* <p>
* 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.
* <p>
* It is not recommended to use multiple replacements in the same table.
*
* @return strings to be replaced with empty strings.
*/
String[] replaceWithEmptyString() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,17 @@
@API(status = API.Status.STABLE)
public @interface DefaultDataTableCellTransformer {

/**
* Replace these strings in the Datatable with empty strings.
* <p>
* 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.
* <p>
* It is not recommended to use multiple replacements in the same table.
*
* @return strings to be replaced with empty strings.
*/
String[] replaceWithEmptyString() default {};

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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.
* <p>
* It is not recommended to use multiple replacements in the same table.
*
* @return strings to be replaced with empty strings.
*/
String[] replaceWithEmptyString() default {};

}
10 changes: 7 additions & 3 deletions java/src/main/java/io/cucumber/java/GlueAdaptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -75,33 +71,32 @@ private DataTableType createDataTableType(Method method) {
if (DataTable.class.equals(parameterType)) {
return new DataTableType(
returnType,
(TableTransformer<Object>) this::execute
(DataTable table) -> execute(replaceEmptyPatternsWithEmptyString(table))
);
}

if (List.class.equals(parameterType)) {
return new DataTableType(
returnType,
(TableRowTransformer<Object>) this::execute
(List<String> row) -> execute(replaceEmptyPatternsWithEmptyString(row))
);
}

if (Map.class.equals(parameterType)) {
return new DataTableType(
returnType,
(TableEntryTransformer<Object>) this::execute
(Map<String, String> entry) -> execute(replaceEmptyPatternsWithEmptyString(entry))
);
}

if (String.class.equals(parameterType)) {
return new DataTableType(
returnType,
(TableCellTransformer<Object>) this::execute
(String cell) -> execute(replaceEmptyPatternsWithEmptyString(cell))
);
}

throw createInvalidSignatureException(method);

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 6320426

Please sign in to comment.