Skip to content
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

JBEHAVE-1580 Add ability to convert parameters to particular type #62

Merged
merged 1 commit into from
May 25, 2023
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
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