Skip to content

[datatable] Add ability to register default transformers for table cell and entry #429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.cucumber.datatable;

import java.util.regex.Pattern;

class CamelCaseStringConverter {

private static final String WHITESPACE = " ";
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");

public String map(String string) {
String[] splitted = normalizeSpace(string).split(WHITESPACE);
splitted[0] = uncapitalize(splitted[0]);
for (int i = 1; i < splitted.length; i++) {
splitted[i] = capitalize(splitted[i]);
}
return join(splitted);
}

private String join(String[] splitted) {
StringBuilder sb = new StringBuilder();
for (String s : splitted) {
sb.append(s);
}
return sb.toString();
}

private String normalizeSpace(String originalHeaderName) {
return WHITESPACE_PATTERN.matcher(originalHeaderName.trim()).replaceAll(WHITESPACE);
}

private String capitalize(String string) {
return String.valueOf(Character.toTitleCase(string.charAt(0))) + string.substring(1);
}

private String uncapitalize(String string) {
if (string.isEmpty()) {
throw new CucumberDataTableException("Field name cannot be empty. Please check the table header.");
}
return String.valueOf(Character.toLowerCase(string.charAt(0))) + string.substring(1);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.cucumber.datatable;

import java.util.List;

import static io.cucumber.datatable.TypeFactory.aListOf;
import static java.util.Collections.singletonList;

class DataTableCellByTypeTransformer implements TableCellByTypeTransformer {

private DataTableTypeRegistry dataTableTypeRegistry;

DataTableCellByTypeTransformer(DataTableTypeRegistry dataTableTypeRegistry) {
this.dataTableTypeRegistry = dataTableTypeRegistry;
}

@Override
public <T> T transform(String value, Class<T> cellType) throws Throwable {
//noinspection unchecked
DataTableType typeByType = dataTableTypeRegistry.lookupTableTypeByType(aListOf(aListOf(cellType)));
if (typeByType == null) {
throw new CucumberDataTableException("There is no DataTableType registered for cell type "+ cellType);
}
//noinspection unchecked
return ((List<List<T>>)typeByType.transform(singletonList(singletonList(value)))).get(0).get(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,38 @@ public T transform(String cell) {
});
}

/**
* Creates a data table type for default cell transformer
* @param cellType class representing cell declared in {@code List<List<T>>}
* @param defaultDataTableTransformer default cell transformer registered in {@link DataTableTypeRegistry#setDefaultDataTableCellTransformer(TableCellByTypeTransformer)}
* @param <T> see {@code cellType}
* @return new DataTableType witch transforms {@code List<List<String>>} to {@code List<List<T>>}
*/
static <T> DataTableType defaultCell(final Class<T> cellType, final TableCellByTypeTransformer defaultDataTableTransformer) {
return new DataTableType(cellType, new TableCellTransformer<T>() {
@Override
public T transform(String cell) throws Throwable {
return defaultDataTableTransformer.transform(cell,cellType);
}
});
}

/**
* Creates a data table type for default entry transformer
* @param entryType class representing entry declared in {@code List<T>}
* @param defaultDataTableTransformer default entry transformer registered in {@link DataTableTypeRegistry#setDefaultDataTableEntryTransformer(TableEntryByTypeTransformer)}
* @param <T> see {@code entryType}
* @return new DataTableType witch transforms {@code List<List<String>>} to {@code List<T>}
*/
static <T> DataTableType defaultEntry(final Class<T> entryType, final TableEntryByTypeTransformer defaultDataTableTransformer, final TableCellByTypeTransformer tableCellByTypeTransformer) {
return new DataTableType(entryType, new TableEntryTransformer<T>() {
@Override
public T transform(Map<String, String> entry) throws Throwable {
return defaultDataTableTransformer.transform(entry, entryType,tableCellByTypeTransformer);
}
});
}

public Object transform(List<List<String>> raw) {
try {
return transformer.transform(raw);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
public final class DataTableTypeRegistry {

private final Map<JavaType, DataTableType> tableTypeByType = new HashMap<>();
private TableEntryByTypeTransformer defaultDataTableEntryTransformer;
private TableCellByTypeTransformer defaultDataTableCellTransformer;

public DataTableTypeRegistry(Locale locale) {
NumberFormat numberFormat = NumberFormat.getNumberInstance(locale);
Expand All @@ -35,47 +37,59 @@ public BigDecimal transform(String cell) {
}
}));

defineDataTableType(new DataTableType(Byte.class, new TableCellTransformer<Byte>() {
TableCellTransformer<Byte> byteTableCellTransformer = new TableCellTransformer<Byte>() {
@Override
public Byte transform(String cell) {
return Byte.decode(cell);
}
}));
};
defineDataTableType(new DataTableType(Byte.class, byteTableCellTransformer));
defineDataTableType(new DataTableType(byte.class, byteTableCellTransformer));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat!


defineDataTableType(new DataTableType(Short.class, new TableCellTransformer<Short>() {
TableCellTransformer<Short> shortTableCellTransformer = new TableCellTransformer<Short>() {
@Override
public Short transform(String cell) {
return Short.decode(cell);
}
}));
};
defineDataTableType(new DataTableType(Short.class, shortTableCellTransformer));
defineDataTableType(new DataTableType(short.class, shortTableCellTransformer));

defineDataTableType(new DataTableType(Integer.class, new TableCellTransformer<Integer>() {
TableCellTransformer<Integer> integerTableCellTransformer = new TableCellTransformer<Integer>() {
@Override
public Integer transform(String cell) {
return Integer.decode(cell);
}
}));
};
defineDataTableType(new DataTableType(Integer.class, integerTableCellTransformer));
defineDataTableType(new DataTableType(int.class, integerTableCellTransformer));

defineDataTableType(new DataTableType(Long.class, new TableCellTransformer<Long>() {
TableCellTransformer<Long> longTableCellTransformer = new TableCellTransformer<Long>() {
@Override
public Long transform(String cell) {
return Long.decode(cell);
}
}));
};
defineDataTableType(new DataTableType(Long.class, longTableCellTransformer));
defineDataTableType(new DataTableType(long.class, longTableCellTransformer));

defineDataTableType(new DataTableType(Float.class, new TableCellTransformer<Float>() {
TableCellTransformer<Float> floatTableCellTransformer = new TableCellTransformer<Float>() {
@Override
public Float transform(String cell) {
return numberParser.parseFloat(cell);
}
}));
};
defineDataTableType(new DataTableType(Float.class, floatTableCellTransformer));
defineDataTableType(new DataTableType(float.class, floatTableCellTransformer));

defineDataTableType(new DataTableType(Double.class, new TableCellTransformer<Double>() {
TableCellTransformer<Double> doubleTableCellTransformer = new TableCellTransformer<Double>() {
@Override
public Double transform(String cell) {
return numberParser.parseDouble(cell);
}
}));
};
defineDataTableType(new DataTableType(Double.class, doubleTableCellTransformer));
defineDataTableType(new DataTableType(double.class, doubleTableCellTransformer));

defineDataTableType(new DataTableType(String.class, new TableCellTransformer<String>() {
@Override
Expand All @@ -102,7 +116,36 @@ public void defineDataTableType(DataTableType dataTableType) {
}

public DataTableType lookupTableTypeByType(final Type tableType) {
return tableTypeByType.get(constructType(tableType));
JavaType targetType = constructType(tableType);
DataTableType dataTableType = tableTypeByType.get(targetType);

if(dataTableType != null){
return dataTableType;
}

if(!targetType.isCollectionLikeType()){
return null;
}

JavaType contentType = targetType.getContentType();
if(contentType.isCollectionLikeType()) {
if(defaultDataTableCellTransformer != null){
return DataTableType.defaultCell(contentType.getContentType().getRawClass(), defaultDataTableCellTransformer);
}
return null;
}
if (defaultDataTableEntryTransformer != null){
return DataTableType.defaultEntry(contentType.getRawClass(), defaultDataTableEntryTransformer, new DataTableCellByTypeTransformer(this));
}
return null;
}

public void setDefaultDataTableEntryTransformer(TableEntryByTypeTransformer defaultDataTableEntryTransformer) {
this.defaultDataTableEntryTransformer = defaultDataTableEntryTransformer;
}

public void setDefaultDataTableCellTransformer(TableCellByTypeTransformer defaultDataTableCellTransformer) {
this.defaultDataTableCellTransformer = defaultDataTableCellTransformer;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.cucumber.datatable;

import java.lang.reflect.Field;
import java.util.Map;

/**
* {@link TableEntryByTypeTransformer} based on reflection.
* <p>
* For example:
*
* <pre>
* | firstName | last name | age |
* | Annie M. G. | Schmidt | 20 |
* </pre>
* can be transformed to class:
* <pre>
*
* public class User {
*
* String firstName;
* String lastName;
* int age;
*
* public User() {
*
* }
* }
*
* </pre>
*/
public class SimpleTableEntryByTypeTransformer implements TableEntryByTypeTransformer {

public interface InstanceCreator {

<T> T createInstance(Class<T> type) throws Throwable;
}

public static class DefaultInstanceCreator implements InstanceCreator {

@Override
public <T> T createInstance(Class<T> type) throws Throwable {
return type.newInstance();
}
}

private CamelCaseStringConverter fieldNameConverter = new CamelCaseStringConverter();
private InstanceCreator instanceCreator;

public SimpleTableEntryByTypeTransformer(InstanceCreator instanceCreator) {
this.instanceCreator = instanceCreator;
}

@Override
public <T> T transform(Map<String, String> value, Class<T> type, TableCellByTypeTransformer cellTransformer) throws Throwable {

T instance = instanceCreator.createInstance(type);
Field[] fields = type.getDeclaredFields();
for (Map.Entry<String, String> entry : value.entrySet()) {
String key = entry.getKey();
String cell = entry.getValue();
for (Field field : fields) {
if (fieldNameConverter.map(key).equals(field.getName())) {
field.setAccessible(true);
field.set(instance, cellTransformer.transform(cell, field.getType()));
break;
}
}
}
return instance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.cucumber.datatable;

/**
* Transformer for single cell. Similar to {@link TableCellTransformer} but additionally it receives expected {@code Class<T>} of cell.
* @see TableCellTransformer
*/
public interface TableCellByTypeTransformer {

/**
* Transforms single cell to type {@code T}
* @param value cell
* @param cellType expected cell type
* @param <T> see {@code cellType}
* @return an instance of {@code T}
*/
<T> T transform(String value, Class<T> cellType) throws Throwable;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.cucumber.datatable;

import java.util.Map;

/**
* Default transformer for entries which don't have registered {@link DataTableType} in {@code TypeRegistry}. Similar to {@link TableEntryTransformer} but additionally it receives {@code Class<T>} of expected object and {@link TableCellByTypeTransformer} for transforming individual cells from {@code String} to arbitrary type.
* @see TableEntryTransformer
*/
public interface TableEntryByTypeTransformer {

/**
* This method should transform row represented by key-value map to object of type {@code type}
* @param entry table entry, key - column name, value - cell
* @param type type of an expected object to return
* @param cellTransformer cell transformer
* @param <T> see {@code type}
* @return new instance of {@code type}
*/
<T> T transform(Map<String, String> entry, Class<T> type, TableCellByTypeTransformer cellTransformer) throws Throwable;

}
Loading