Skip to content

Commit

Permalink
JBEHAVE-1580 Add ability to convert parameters to particular type
Browse files Browse the repository at this point in the history
  • Loading branch information
uarlouski committed May 25, 2023
1 parent e046041 commit 7f177f9
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 22 deletions.
47 changes: 47 additions & 0 deletions distribution/src/site/content/tabular-parameters.html
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,53 @@ <h2>Mapping parameters to custom types</h2>
}
</script>

<h2>Convert row to type using custom converter</h2>

<p>It may sometime be useful to map the whole row to a custom object using custom converter rather than relying on field to field mapping</p>

<p>Given the following class that we want to convert from <a
href="javadoc/core/org/jbehave/core/annotations/Parameter.html">Parameter</a></p>

<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
public class Person {
private int age;
private String name;

...
}
]]>
</script>

<p>Create parameter converter for the Person class, please do not forget to register converter in <a
href="javadoc/core/org/jbehave/core/steps/ParameterConverters.html">ParameterConverters</a></p>

<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
public class PersonConverter extends AbstractParameterConverter<Parameters, Person> {
@Override
public Person convertValue(Parameters value, Type type) {
Person person = new Person();
...
return person;
}
}
]]>
</script>

<p>Use <a href="javadoc/core/org/jbehave/core/steps/Parameters.html#as(java.lang.reflect.Type)">as(java.lang.reflect.Type)</a> method to perform the conversion</p>

<script type="syntaxhighlighter" class="brush: java">
<![CDATA[
@Then("the person age is greater than 18: $activityTable")
public void thePersonAge(ExamplesTable activityTable) {
Parameters row = activityTable.getRowAsParameters(0);
Person person = row.as(Person.class);
System.out.println("Age allowed: " + person.getAge() > 18);
}
]]>
</script>

<h2>Preserving whitespace</h2>

<p>By default, value in the table are trimmed, i.e. any preceding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public ConvertedParameters(Map<String, String> values, ParameterConverters param
this.parameterConverters = parameterConverters;
}

@Override
@SuppressWarnings("unchecked")
public <T> T as(Type type) {
return (T) parameterConverters.convert(this, Parameters.class, type);
}

@Override
public <T> T valueAs(String name, Type type) {
return convert(valueFor(name), type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -344,8 +345,9 @@ public ParameterConverters addConverters(List<? extends ParameterConverter> conv
return this;
}

private static boolean isChainComplete(Queue<ParameterConverter> convertersChain) {
return !convertersChain.isEmpty() && isBaseType(convertersChain.peek().getSourceType());
private static boolean isChainComplete(Queue<ParameterConverter> convertersChain,
Predicate<Type> sourceTypePredicate) {
return !convertersChain.isEmpty() && sourceTypePredicate.test(convertersChain.peek().getSourceType());
}

private static Object applyConverters(Object value, Type basicType, Queue<ParameterConverter> convertersChain) {
Expand All @@ -354,34 +356,40 @@ private static Object applyConverters(Object value, Type basicType, Queue<Parame
(v, c) -> c.convertValue(v, c.getTargetType()), (l, r) -> l);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
public Object convert(String value, Type type) {
Queue<ParameterConverter> converters = findConverters(type);
if (isChainComplete(converters)) {
return convert(value, String.class, type);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
public Object convert(Object value, Class<?> source, Type type) {
Predicate<Type> sourceTypePredicate = t -> source.isAssignableFrom((Class<?>) t);
Queue<ParameterConverter> converters = findConverters(type, sourceTypePredicate);
if (isChainComplete(converters, sourceTypePredicate)) {
Object converted = applyConverters(value, type, converters);
Queue<Class<?>> classes = converters.stream().map(ParameterConverter::getClass)
.collect(Collectors.toCollection(LinkedList::new));
monitor.convertedValueOfType(value, type, converted, classes);
monitor.convertedValueOfType(String.valueOf(value), type, converted, classes);
return converted;
}

if (isAssignableFromRawType(Optional.class, type)) {
Type elementType = argumentType(type);
return Optional.of(convert(value, elementType));
return Optional.of(convert(value, source, elementType));
}

if (isAssignableFromRawType(Collection.class, type)) {
Type elementType = argumentType(type);
Collection collection = createCollection(rawClass(type));

if (collection != null) {
Queue<ParameterConverter> typeConverters = findConverters(elementType);
Queue<ParameterConverter> typeConverters = findConverters(elementType, sourceTypePredicate);

if (!typeConverters.isEmpty()) {
Type sourceType = typeConverters.peek().getSourceType();

if (isBaseType(sourceType)) {
fillCollection(value, escapedCollectionSeparator, typeConverters, elementType, collection);
if (String.class.isAssignableFrom((Class<?>) sourceType)) {
fillCollection((String) value, escapedCollectionSeparator, typeConverters, elementType,
collection);
} else if (isAssignableFrom(Parameters.class, sourceType)) {
ExamplesTable table = (ExamplesTable) findBaseConverter(ExamplesTable.class).convertValue(value,
String.class);
Expand All @@ -396,7 +404,7 @@ public Object convert(String value, Type type) {
if (type instanceof Class) {
Class clazz = (Class) type;
if (clazz.isArray()) {
String[] elements = parseElements(value, escapedCollectionSeparator);
String[] elements = parseElements((String) value, escapedCollectionSeparator);
Class elementType = clazz.getComponentType();
ParameterConverter elementConverter = findBaseConverter(elementType);
Object array = createArray(elementType, elements.length);
Expand All @@ -420,29 +428,26 @@ private ParameterConverter findBaseConverter(Type type) {
return null;
}

private Queue<ParameterConverter> findConverters(Type type) {
private Queue<ParameterConverter> findConverters(Type type, Predicate<Type> sourceTypePredicate) {
LinkedList<ParameterConverter> convertersChain = new LinkedList<>();
putConverters(type, convertersChain);
putConverters(type, convertersChain, sourceTypePredicate);
return convertersChain;
}

private void putConverters(Type type, LinkedList<ParameterConverter> container) {
private void putConverters(Type type, LinkedList<ParameterConverter> container,
Predicate<Type> sourceTypePredicate) {
for (ParameterConverter converter : converters) {
if (converter.canConvertTo(type)) {
container.addFirst(converter);
Type sourceType = converter.getSourceType();
if (isBaseType(sourceType)) {
if (sourceTypePredicate.test(sourceType)) {
break;
}
putConverters(sourceType, container);
putConverters(sourceType, container, sourceTypePredicate);
}
}
}

private static boolean isBaseType(Type type) {
return String.class.isAssignableFrom((Class<?>) type);
}

private static boolean isAssignableFrom(Class<?> clazz, Type type) {
return type instanceof Class<?> && clazz.isAssignableFrom((Class<?>) type);
}
Expand Down
12 changes: 10 additions & 2 deletions jbehave-core/src/main/java/org/jbehave/core/steps/Parameters.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
*/
public interface Parameters extends Row {

/**
* Converts the parameters object to the specified type
*
* @param type the Type or Class of type &lt;T&gt; to convert to
* @return The value of type &lt;T&gt;
*/
<T> T as(Type type);

/**
* Returns the value of a named parameter as a given type
*
Expand All @@ -29,15 +37,15 @@ public interface Parameters extends Row {
<T> T valueAs(String name, Type type, T defaultValue);

/**
* Maps parameters to the specified type
* Maps parameters fields to the fields of specified type
*
* @param type The target type
* @return The object of type &lt;T&gt;
*/
<T> T mapTo(Class<T> type);

/**
* Maps parameters to the specified type
* Maps parameters fields to the fields of specified type
*
* @param type The target type
* @param fieldNameMapping The field mapping between parameters and target type fields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -13,6 +14,7 @@
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.model.TableTransformers;
import org.jbehave.core.steps.ConvertedParameters.ParametersNotMappableToType;
import org.jbehave.core.steps.ParameterConverters.AbstractParameterConverter;
import org.junit.jupiter.api.Test;

class ConvertedParametersBehaviour {
Expand Down Expand Up @@ -111,6 +113,23 @@ void shouldThrowExceptionOnUnknownFieldWhileConversion() {
+ "org.jbehave.core.steps.ConvertedParametersBehaviour$Identifier"));
}

@Test
void shouldConvertParametersToPersonType() {
Map<String, String> row = new HashMap<>();
row.put("years", "38");
row.put("firstName", "Din");
row.put("l_name", "Djarin");

converters.addConverters(new ParametersToPersonConverter());
Parameters parameters = new ConvertedParameters(row, converters);

Person person = parameters.as(Person.class);

assertThat(person.getAge(), is(38));
assertThat(person.getFirstName(), is("Din"));
assertThat(person.getLastName(), is("Djarin"));
}

public static class Identifier {

private String identifier;
Expand All @@ -121,6 +140,19 @@ public String getIdentifier() {

}

public static final class ParametersToPersonConverter extends AbstractParameterConverter<Parameters, Person> {

@Override
public Person convertValue(Parameters value, Type type) {
Person person = new Person();
person.setAge(value.valueAs("years", int.class));
person.setFirstName(value.valueAs("firstName", String.class));
person.setLastName(value.valueAs("l_name", String.class));
return person;
}

}

public static final class Person extends Identifier {

private int age;
Expand All @@ -132,13 +164,25 @@ public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,19 @@ void shouldConvertStringViaChainOfConverters() {
assertThat(output.getOutput(), is(input + "\nfirstsecondthird"));
}

@Test
void shouldConvertFromExampleTableToTargetObject() {
ParameterConverters converters = new ParameterConverters();
converters.addConverters(new FirstParameterConverter());

String input = "|key|\n|value|";
ExamplesTable table = (ExamplesTable) converters.convert(input, ExamplesTable.class);

FirstConverterOutput output = (FirstConverterOutput) converters.convert(table, ExamplesTable.class,
FirstConverterOutput.class);
assertThat(output.getOutput(), is(input + "\nfirst"));
}

@Test
void shouldConvertToOptionalViaChainOfConverters() {
ParameterConverters converters = new ParameterConverters();
Expand Down

0 comments on commit 7f177f9

Please sign in to comment.