diff --git a/.revapi/api-changes.json b/.revapi/api-changes.json index 1f306f01db..68debace37 100644 --- a/.revapi/api-changes.json +++ b/.revapi/api-changes.json @@ -21,6 +21,31 @@ } } ], + "7.2.0": [ + { + "extension": "revapi.differences", + "id": "intentional-api-changes", + "ignore": true, + "configuration": { + "differences": [ + { + "code": "java.generics.elementNowParameterized", + "old": "method void io.cucumber.java8.LambdaGlue::DocStringType(java.lang.String, io.cucumber.java8.DocStringDefinitionBody)", + "new": "method void io.cucumber.java8.LambdaGlue::DocStringType(java.lang.String, io.cucumber.java8.DocStringDefinitionBody)", + "justification": "Should not impact the normal use case of the java8 API" + + }, + { + "code": "java.generics.formalTypeParameterAdded", + "old": "method void io.cucumber.java8.LambdaGlue::DocStringType(java.lang.String, io.cucumber.java8.DocStringDefinitionBody)", + "new": "method void io.cucumber.java8.LambdaGlue::DocStringType(java.lang.String, io.cucumber.java8.DocStringDefinitionBody)", + "typeParameter": "T", + "justification": "Should not impact the normal use case of the java8 API" + } + ] + } + } + ], "internal": [ { "extension": "revapi.differences", diff --git a/CHANGELOG.md b/CHANGELOG.md index 073d337839..83cd6b7dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] (In Git) ### Added +* [Core] Support multiple doc strings types with same content type ([#2421](https://github.com/cucumber/cucumber-jvm/pull/2421) Postelnicu George) + - When transforming doc strings in addition to the content type Cucumber will + also look at the type used in the step definition to disambiguate between + doc string types. ### Changed diff --git a/core/src/test/java/io/cucumber/core/plugin/JsonFormatterTest.java b/core/src/test/java/io/cucumber/core/plugin/JsonFormatterTest.java index 0b0d460603..5b8e2a11fd 100644 --- a/core/src/test/java/io/cucumber/core/plugin/JsonFormatterTest.java +++ b/core/src/test/java/io/cucumber/core/plugin/JsonFormatterTest.java @@ -12,6 +12,7 @@ import io.cucumber.core.runtime.StubFeatureSupplier; import io.cucumber.core.runtime.TimeServiceEventBus; import io.cucumber.datatable.DataTable; +import io.cucumber.docstring.DocString; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; @@ -1251,7 +1252,7 @@ void should_format_scenario_with_a_step_with_a_doc_string_and_content_type() { "\n" + " Scenario: Monkey eats bananas\n" + " Given there are bananas\n" + - " \"\"\"doc\n" + + " \"\"\"text/plain\n" + " doc string content\n" + " \"\"\"\n"); @@ -1262,7 +1263,7 @@ void should_format_scenario_with_a_step_with_a_doc_string_and_content_type() { .withAdditionalPlugins(timeService, new JsonFormatter(out)) .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID)) .withBackendSupplier(new StubBackendSupplier( - new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()", String.class))) + new StubStepDefinition("there are bananas", "StepDefs.there_are_bananas()", DocString.class))) .build() .run(); @@ -1290,7 +1291,7 @@ void should_format_scenario_with_a_step_with_a_doc_string_and_content_type() { " \"name\": \"there are bananas\",\n" + " \"line\": 4,\n" + " \"doc_string\": {\n" + - " \"content_type\": \"doc\",\n" + + " \"content_type\": \"text/plain\",\n" + " \"value\": \"doc string content\",\n" + " \"line\": 5\n" + " },\n" + diff --git a/core/src/test/java/io/cucumber/core/stepexpression/StepExpressionFactoryTest.java b/core/src/test/java/io/cucumber/core/stepexpression/StepExpressionFactoryTest.java index e677aa51f3..141028df39 100644 --- a/core/src/test/java/io/cucumber/core/stepexpression/StepExpressionFactoryTest.java +++ b/core/src/test/java/io/cucumber/core/stepexpression/StepExpressionFactoryTest.java @@ -158,15 +158,25 @@ void unknown_target_type_transform_doc_string_to_doc_string() { } @Test - void docstring_expression_transform_doc_string_with_content_type_to_string() { + void docstring_expression_transform_doc_string_to_string() { String docString = "A rather long and boring string of documentation"; - String contentType = "doc"; StepDefinition stepDefinition = new StubStepDefinition("Given some stuff:", String.class); StepExpression expression = stepExpressionFactory.createExpression(stepDefinition); - List match = expression.match("Given some stuff:", docString, contentType); + List match = expression.match("Given some stuff:", docString, null); assertThat(match.get(0).getValue(), is(equalTo(docString))); } + @Test + void docstring_and_datatable_match_same_step_definition() { + String docString = "A rather long and boring string of documentation"; + StepDefinition stepDefinition = new StubStepDefinition("Given some stuff:", UNKNOWN_TYPE); + StepExpression expression = stepExpressionFactory.createExpression(stepDefinition); + List match = expression.match("Given some stuff:", docString, null); + assertThat(match.get(0).getValue(), is(equalTo(DocString.create(docString)))); + match = expression.match("Given some stuff:", table); + assertThat(match.get(0).getValue(), is(equalTo(DataTable.create(table)))); + } + @Test void docstring_expression_transform_doc_string_to_json_node() { String docString = "{\"hello\": \"world\"}"; diff --git a/docstring/src/main/java/io/cucumber/docstring/DocStringTypeRegistry.java b/docstring/src/main/java/io/cucumber/docstring/DocStringTypeRegistry.java index e0e09aadda..b846d6b4ad 100644 --- a/docstring/src/main/java/io/cucumber/docstring/DocStringTypeRegistry.java +++ b/docstring/src/main/java/io/cucumber/docstring/DocStringTypeRegistry.java @@ -3,33 +3,34 @@ import org.apiguardian.api.API; import java.lang.reflect.Type; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import static java.lang.String.format; @API(status = API.Status.STABLE) public final class DocStringTypeRegistry { - private final Map docStringTypesByContentType = new HashMap<>(); - private final Map docStringTypesByType = new HashMap<>(); + private static final Class DEFAULT_TYPE = String.class; + private static final String DEFAULT_CONTENT_TYPE = ""; + private final Map> docStringTypes = new HashMap<>(); public DocStringTypeRegistry() { - defineDocStringType(new DocStringType(String.class, "", (String docString) -> docString)); + defineDocStringType(new DocStringType(DEFAULT_TYPE, DEFAULT_CONTENT_TYPE, (String docString) -> docString)); } public void defineDocStringType(DocStringType docStringType) { - DocStringType byContentType = docStringTypesByContentType.get(docStringType.getContentType()); - if (byContentType != null) { - throw createDuplicateTypeException(byContentType, docStringType); + DocStringType existing = lookupByContentTypeAndType(docStringType.getContentType(), docStringType.getType()); + if (existing != null) { + throw createDuplicateTypeException(existing, docStringType); } - DocStringType byClass = docStringTypesByType.get(docStringType.getType()); - if (byClass != null) { - throw createDuplicateTypeException(byClass, docStringType); - - } - docStringTypesByContentType.put(docStringType.getContentType(), docStringType); - docStringTypesByType.put(docStringType.getType(), docStringType); + Map map = docStringTypes.computeIfAbsent(docStringType.getContentType(), + s -> new HashMap<>()); + map.put(docStringType.getType(), docStringType); + docStringTypes.put(docStringType.getContentType(), map); } private static CucumberDocStringException createDuplicateTypeException( @@ -49,12 +50,31 @@ private static String emptyToAnonymous(String contentType) { return contentType.isEmpty() ? "[anonymous]" : contentType; } - DocStringType lookupByContentType(String contentType) { - return docStringTypesByContentType.get(contentType); + List lookup(String contentType, Type type) { + if (contentType == null) { + return lookUpByType(type); + } + + DocStringType docStringType = lookupByContentTypeAndType(contentType, type); + if (docStringType == null) { + return Collections.emptyList(); + } + return Collections.singletonList(docStringType); } - DocStringType lookupByType(Type type) { - return docStringTypesByType.get(type); + private List lookUpByType(Type type) { + return docStringTypes.values().stream() + .flatMap(typeDocStringTypeMap -> typeDocStringTypeMap.entrySet().stream() + .filter(entry -> entry.getKey().equals(type)) + .map(Map.Entry::getValue)) + .collect(Collectors.toList()); } + private DocStringType lookupByContentTypeAndType(String contentType, Type type) { + Map docStringTypesByType = docStringTypes.get(contentType); + if (docStringTypesByType == null) { + return null; + } + return docStringTypesByType.get(type); + } } diff --git a/docstring/src/main/java/io/cucumber/docstring/DocStringTypeRegistryDocStringConverter.java b/docstring/src/main/java/io/cucumber/docstring/DocStringTypeRegistryDocStringConverter.java index b7bf89ccb6..463a4f2c76 100644 --- a/docstring/src/main/java/io/cucumber/docstring/DocStringTypeRegistryDocStringConverter.java +++ b/docstring/src/main/java/io/cucumber/docstring/DocStringTypeRegistryDocStringConverter.java @@ -4,6 +4,8 @@ import org.apiguardian.api.API; import java.lang.reflect.Type; +import java.util.List; +import java.util.stream.Collectors; import static java.lang.String.format; import static java.util.Objects.requireNonNull; @@ -23,26 +25,38 @@ public T convert(DocString docString, Type targetType) { return (T) docString; } - DocStringType docStringType = docStringTypeRegistry.lookupByContentType(docString.getContentType()); - if (docStringType != null) { - return (T) docStringType.transform(docString.getContent()); - } - - docStringType = docStringTypeRegistry.lookupByType(targetType); - if (docStringType != null) { - return (T) docStringType.transform(docString.getContent()); - } + List docStringTypes = docStringTypeRegistry.lookup(docString.getContentType(), targetType); - if (docString.getContentType() == null) { + if (docStringTypes.isEmpty()) { + if (docString.getContentType() == null) { + throw new CucumberDocStringException(format( + "It appears you did not register docstring type for %s", + targetType.getTypeName())); + } throw new CucumberDocStringException(format( - "It appears you did not register docstring type for %s", + "It appears you did not register docstring type for '%s' or %s", + docString.getContentType(), targetType.getTypeName())); } + if (docStringTypes.size() > 1) { + throw new CucumberDocStringException(format( + "Multiple converters found for type %s, add one of the following content types to your docstring %s", + targetType.getTypeName(), + suggestedContentTypes(docStringTypes))); + } + + return (T) docStringTypes.get(0).transform(docString.getContent()); + } - throw new CucumberDocStringException(format( - "It appears you did not register docstring type for '%s' or %s", - docString.getContentType(), - targetType.getTypeName())); + private List suggestedContentTypes(List docStringTypes) { + return docStringTypes.stream() + .map(DocStringType::getContentType) + // Can't use the anonymous content type to resolve + // the ambiguity. + .filter(contentType -> !contentType.isEmpty()) + .sorted() + .distinct() + .collect(Collectors.toList()); } } diff --git a/docstring/src/test/java/io/cucumber/docstring/DocStringTypeRegistryDocStringConverterTest.java b/docstring/src/test/java/io/cucumber/docstring/DocStringTypeRegistryDocStringConverterTest.java index c1e186dde4..cb164e0168 100644 --- a/docstring/src/test/java/io/cucumber/docstring/DocStringTypeRegistryDocStringConverterTest.java +++ b/docstring/src/test/java/io/cucumber/docstring/DocStringTypeRegistryDocStringConverterTest.java @@ -4,8 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; +import java.util.Objects; + import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalToCompressingWhiteSpace; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -16,7 +19,7 @@ class DocStringTypeRegistryDocStringConverterTest { registry); @Test - void uses_doc_string_type_when_available() { + void throws_when_uses_doc_string_type_but_downcast_conversion() { registry.defineDocStringType(new DocStringType( JsonNode.class, "json", @@ -26,8 +29,12 @@ void uses_doc_string_type_when_available() { "{\"hello\":\"world\"}", "json"); - JsonNode converted = converter.convert(docString, Object.class); - assertThat(converted.get("hello").textValue(), is("world")); + CucumberDocStringException exception = assertThrows( + CucumberDocStringException.class, + () -> converter.convert(docString, Object.class)); + + assertThat(exception.getMessage(), is("" + + "It appears you did not register docstring type for 'json' or java.lang.Object")); } @Test @@ -48,6 +55,7 @@ void uses_target_type_when_available() { void target_type_to_string_is_predefined() { DocString docString = DocString.create( "hello world"); + String converted = converter.convert(docString, String.class); assertThat(converted, is("hello world")); } @@ -112,4 +120,201 @@ void throws_when_conversion_fails() { " \"\"\""))); } + @Test + void converts_no_content_type_doc_string_to_registered_matching_convertor() { + DocString docString = DocString.create( + "{\"hello\":\"world\"}"); + + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "json", + (String s) -> new ObjectMapper().convertValue(s, JsonNode.class))); + + JsonNode converted = converter.convert(docString, JsonNode.class); + assertThat(converted.asText(), equalTo(docString.getContent())); + } + + @Test + void throws_when_multiple_convertors_available() { + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "json", + (String s) -> new ObjectMapper().readTree(s))); + + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "xml", + (String s) -> new ObjectMapper().readTree(s))); + + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "", + (String s) -> new ObjectMapper().readTree(s))); + + DocString docString = DocString.create( + "{\"hello\":\"world\"}"); + + CucumberDocStringException exception = assertThrows( + CucumberDocStringException.class, + () -> converter.convert(docString, JsonNode.class)); + + assertThat(exception.getMessage(), is("" + + "Multiple converters found for type com.fasterxml.jackson.databind.JsonNode, " + + "add one of the following content types to your docstring [json, xml]")); + } + + @Test + void different_docstring_content_types_convert_to_matching_doc_string_types() { + registry.defineDocStringType(new DocStringType( + String.class, + "json", + (String s) -> s)); + + registry.defineDocStringType(new DocStringType( + String.class, + "xml", + (String s) -> s)); + + registry.defineDocStringType(new DocStringType( + String.class, + "yml", + (String s) -> s)); + + DocString docStringJson = DocString.create( + "{\"content\":\"hello world\"}", "json"); + DocString docStringXml = DocString.create( + "hello world}", "xml"); + DocString docStringYml = DocString.create( + " content: hello world", "yml"); + + String convertJson = converter.convert(docStringJson, String.class); + String convertXml = converter.convert(docStringXml, String.class); + String convertYml = converter.convert(docStringYml, String.class); + + assertThat(docStringJson.getContent(), equalTo(convertJson)); + assertThat(docStringXml.getContent(), equalTo(convertXml)); + assertThat(docStringYml.getContent(), equalTo(convertYml)); + } + + @Test + void same_docstring_content_type_can_convert_to_different_registered_doc_string_types() { + registry.defineDocStringType(new DocStringType( + Greet.class, + "text", + Greet::new)); + + registry.defineDocStringType(new DocStringType( + Meet.class, + "text", + Meet::new)); + + registry.defineDocStringType(new DocStringType( + Leave.class, + "text", + Leave::new)); + + DocString docStringGreet = DocString.create( + "hello world", "text"); + DocString docStringMeet = DocString.create( + "nice to meet", "text"); + DocString docStringLeave = DocString.create( + "goodbye", "text"); + + Greet actualGreet = converter.convert(docStringGreet, Greet.class); + Meet actualMeet = converter.convert(docStringMeet, Meet.class); + Leave actualLeave = converter.convert(docStringLeave, Leave.class); + + Greet expectedGreet = new Greet(docStringGreet.getContent()); + Meet expectedMeet = new Meet(docStringMeet.getContent()); + Leave expectedLeave = new Leave(docStringLeave.getContent()); + + assertThat(actualGreet, equalTo(expectedGreet)); + assertThat(actualMeet, equalTo(expectedMeet)); + assertThat(actualLeave, equalTo(expectedLeave)); + } + + private static class Greet { + private final String message; + + Greet(String message) { + this.message = message; + } + + @Override + public String toString() { + return message; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Greet greet = (Greet) o; + return Objects.equals(message, greet.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } + } + + private static class Meet { + private final String message; + + Meet(String message) { + this.message = message; + } + + @Override + public String toString() { + return message; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Meet meet = (Meet) o; + return Objects.equals(message, meet.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } + } + + private static class Leave { + private final String message; + + Leave(String message) { + this.message = message; + } + + @Override + public String toString() { + return message; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Leave leave = (Leave) o; + return Objects.equals(message, leave.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } + } + } diff --git a/docstring/src/test/java/io/cucumber/docstring/DocStringTypeRegistryTest.java b/docstring/src/test/java/io/cucumber/docstring/DocStringTypeRegistryTest.java index cf0019b6d3..e8273f535c 100644 --- a/docstring/src/test/java/io/cucumber/docstring/DocStringTypeRegistryTest.java +++ b/docstring/src/test/java/io/cucumber/docstring/DocStringTypeRegistryTest.java @@ -1,22 +1,26 @@ package io.cucumber.docstring; +import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.JsonNode; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; class DocStringTypeRegistryTest { + public static final String DEFAULT_CONTENT_TYPE = ""; private final DocStringTypeRegistry registry = new DocStringTypeRegistry(); @Test void anonymous_doc_string_is_predefined() { DocStringType docStringType = new DocStringType( String.class, - "", + DEFAULT_CONTENT_TYPE, (String s) -> s); CucumberDocStringException actualThrown = assertThrows( @@ -27,10 +31,10 @@ void anonymous_doc_string_is_predefined() { } @Test - void doc_string_types_must_be_unique() { + void doc_string_types_of_same_content_type_must_have_unique_return_type() { registry.defineDocStringType(new DocStringType( JsonNode.class, - "json", + "application/json", (String s) -> null)); DocStringType duplicate = new DocStringType( @@ -42,8 +46,81 @@ void doc_string_types_must_be_unique() { CucumberDocStringException.class, () -> registry.defineDocStringType(duplicate)); assertThat(exception.getMessage(), is("" + - "There is already docstring type registered for 'json' and com.fasterxml.jackson.databind.JsonNode.\n" + + "There is already docstring type registered for 'application/json' and com.fasterxml.jackson.databind.JsonNode.\n" + + "You are trying to add 'application/json' and com.fasterxml.jackson.databind.JsonNode")); } + @Test + void can_register_multiple_doc_string_with_different_content_type_but_same_return_type() { + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "application/json", + (String s) -> null)); + + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "application/xml", + (String s) -> null)); + + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "application/yml", + (String s) -> null)); + + assertThat(registry.lookup(null, JsonNode.class), hasSize(3)); + } + + @Test + void no_content_type_association_is_made() { + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "application/json", + (String s) -> null)); + + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "json", + (String s) -> null)); + + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "json/application", + (String s) -> null)); + + assertThat(registry.lookup(null, JsonNode.class), hasSize(3)); + } + + @Test + void can_add_multiple_default_content_types_with_different_return_types() { + registry.defineDocStringType(new DocStringType( + JsonNode.class, + DEFAULT_CONTENT_TYPE, + (String s) -> null)); + + registry.defineDocStringType(new DocStringType( + TreeNode.class, + DEFAULT_CONTENT_TYPE, + (String s) -> null)); + + assertThat(registry.lookup(DEFAULT_CONTENT_TYPE, JsonNode.class), hasSize(1)); + assertThat(registry.lookup(DEFAULT_CONTENT_TYPE, TreeNode.class), hasSize(1)); + } + + @Test + void can_add_same_content_type_with_different_return_types() { + registry.defineDocStringType(new DocStringType( + JsonNode.class, + "application/json", + (String s) -> null)); + + registry.defineDocStringType(new DocStringType( + TreeNode.class, + "application/json", + (String s) -> null)); + + assertThat(registry.lookup("application/json", JsonNode.class), hasSize(1)); + assertThat(registry.lookup("application/json", TreeNode.class), hasSize(1)); + } + } diff --git a/java/README.md b/java/README.md index 62c0806b66..459ac45796 100644 --- a/java/README.md +++ b/java/README.md @@ -455,10 +455,25 @@ Using `@DocStringType` annotation, it is possible to define transformations to o ```gherkin Given some more information """json - { - "produce": "Cucumbers", - "weight": "5 Kilo", - "price": "1€/Kilo" + [ + { + "produce": "Cucumbers", + "weight": "5 Kilo", + "price": "1€/Kilo" + }, + { + "produce": "Gherkins", + "weight": "1 Kilo", + "price": "5€/Kilo" + } + ] + """ +Then some conclusion is drawn + """json + { + "size" : "XL", + "taste": "delicious", + "type" : "cucumber salad" } """ ``` @@ -466,22 +481,40 @@ Given some more information ```java package com.example; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.cucumber.java.DocStringType; import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; + +import java.io.IOException; +import java.util.List; public class StepDefinitions { + private final ObjectMapper objectMapper = new ObjectMapper(); + @DocStringType - public JsonNode json(String docString) throws IOException { - return objectMapper.readTree(docString); + public List json(String docString) throws IOException { + return objectMapper.readValue(docString, new TypeReference>() { + }); } - + + @DocStringType(contentType = "json") + public FoodItem convertFoodItem(String docString) throws IOException { + return objectMapper.readValue(docString, new TypeReference() { + }); + } + @Given("some more information") - public void some_more_information(JsonNode json){ - + public void some_more_information(List groceries) { + + } + + @Then("some conclusion is drawn") + public void some_conclusion_is_drawn(FoodItem foodItem) { + } } ``` diff --git a/java/src/main/java/io/cucumber/java/JavaDocStringTypeDefinition.java b/java/src/main/java/io/cucumber/java/JavaDocStringTypeDefinition.java index 08b1e96ab0..443f94494d 100644 --- a/java/src/main/java/io/cucumber/java/JavaDocStringTypeDefinition.java +++ b/java/src/main/java/io/cucumber/java/JavaDocStringTypeDefinition.java @@ -16,7 +16,7 @@ class JavaDocStringTypeDefinition extends AbstractGlueDefinition implements DocS JavaDocStringTypeDefinition(String contentType, Method method, Lookup lookup) { super(requireValidMethod(method), lookup); this.docStringType = new DocStringType( - this.method.getReturnType(), + this.method.getGenericReturnType(), contentType.isEmpty() ? method.getName() : contentType, this::invokeMethod); } diff --git a/java/src/test/java/io/cucumber/java/JavaDocStringTypeDefinitionTest.java b/java/src/test/java/io/cucumber/java/JavaDocStringTypeDefinitionTest.java index e78f1b766b..3670bd6164 100644 --- a/java/src/test/java/io/cucumber/java/JavaDocStringTypeDefinitionTest.java +++ b/java/src/test/java/io/cucumber/java/JavaDocStringTypeDefinitionTest.java @@ -1,5 +1,6 @@ package io.cucumber.java; +import com.fasterxml.jackson.core.type.TypeReference; import io.cucumber.core.backend.Lookup; import io.cucumber.docstring.DocString; import io.cucumber.docstring.DocStringTypeRegistry; @@ -7,10 +8,15 @@ import org.junit.jupiter.api.Test; import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Map; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsMapContaining.hasEntry; import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; class JavaDocStringTypeDefinitionTest { @@ -30,14 +36,23 @@ public T getInstance(Class glueClass) { @Test void can_define_doc_string_converter() throws NoSuchMethodException { + Method method = JavaDocStringTypeDefinitionTest.class.getMethod("convert_doc_string_to_string", String.class); + JavaDocStringTypeDefinition definition = new JavaDocStringTypeDefinition("text/plain", method, lookup); + registry.defineDocStringType(definition.docStringType()); + assertThat(converter.convert(docString, Object.class), is("some_desired_string")); + } + + @Test + void can_define_doc_string_without_content_types_converter() throws NoSuchMethodException { Method method = JavaDocStringTypeDefinitionTest.class.getMethod("convert_doc_string_to_string", String.class); JavaDocStringTypeDefinition definition = new JavaDocStringTypeDefinition("", method, lookup); registry.defineDocStringType(definition.docStringType()); - assertThat(converter.convert(docString, Object.class), is("convert_doc_string_to_string")); + assertThat(converter.convert(DocString.create("some doc string"), Object.class), + is("some_desired_string")); } public Object convert_doc_string_to_string(String docString) { - return "convert_doc_string_to_string"; + return "some_desired_string"; } @Test @@ -75,11 +90,52 @@ public Object converts_object_to_string(Object object) { @Test void must_return_something() throws NoSuchMethodException { - Method method = JavaDocStringTypeDefinitionTest.class.getMethod("converts_string_to_void", String.class); - assertThrows(InvalidMethodSignatureException.class, () -> new JavaDocStringTypeDefinition("", method, lookup)); + Method voidMethod = JavaDocStringTypeDefinitionTest.class.getMethod("converts_string_to_void", String.class); + Method voidObjectMethod = JavaDocStringTypeDefinitionTest.class.getMethod("converts_string_to_void_object", + String.class); + + assertAll( + () -> assertThrows(InvalidMethodSignatureException.class, + () -> new JavaDocStringTypeDefinition("", voidMethod, lookup)), + () -> assertThrows(InvalidMethodSignatureException.class, + () -> new JavaDocStringTypeDefinition("", voidObjectMethod, lookup))); } public void converts_string_to_void(String docString) { } + public Void converts_string_to_void_object(String docString) { + return null; + } + + @Test + public void correct_conversion_is_used_for_simple_and_complex_return_types() throws NoSuchMethodException { + Method simpleMethod = JavaDocStringTypeDefinitionTest.class.getMethod("converts_string_to_simple_type", + String.class); + JavaDocStringTypeDefinition simpleDefinition = new JavaDocStringTypeDefinition("text/plain", simpleMethod, + lookup); + registry.defineDocStringType(simpleDefinition.docStringType()); + + Method complexMethod = JavaDocStringTypeDefinitionTest.class.getMethod("converts_string_to_complex_type", + String.class); + JavaDocStringTypeDefinition complexDefinition = new JavaDocStringTypeDefinition("text/plain", complexMethod, + lookup); + registry.defineDocStringType(complexDefinition.docStringType()); + + Type simpleType = Map.class; + assertThat(converter.convert(docString, simpleType), hasEntry("some_simple_type", Collections.emptyMap())); + Type complexType = new TypeReference>>() { + }.getType(); + assertThat(converter.convert(docString, complexType), hasEntry("some_complex_type", Collections.emptyMap())); + } + + @SuppressWarnings("rawtypes") + public Map converts_string_to_simple_type(String docString) { + return Collections.singletonMap("some_simple_type", Collections.emptyMap()); + } + + public Map> converts_string_to_complex_type(String docString) { + return Collections.singletonMap("some_complex_type", Collections.emptyMap()); + } + } diff --git a/java8/src/main/java/io/cucumber/java8/Java8DocStringTypeDefinition.java b/java8/src/main/java/io/cucumber/java8/Java8DocStringTypeDefinition.java index cddff4c84b..f5a38fd009 100644 --- a/java8/src/main/java/io/cucumber/java8/Java8DocStringTypeDefinition.java +++ b/java8/src/main/java/io/cucumber/java8/Java8DocStringTypeDefinition.java @@ -8,7 +8,7 @@ final class Java8DocStringTypeDefinition extends AbstractGlueDefinition implemen private final DocStringType docStringType; - Java8DocStringTypeDefinition(String contentType, DocStringDefinitionBody body) { + Java8DocStringTypeDefinition(String contentType, DocStringDefinitionBody body) { super(body, new Exception().getStackTrace()[3]); if (contentType == null) { throw new CucumberException("Docstring content type couldn't be null, define docstring content type"); diff --git a/java8/src/main/java/io/cucumber/java8/LambdaGlue.java b/java8/src/main/java/io/cucumber/java8/LambdaGlue.java index 5b2f70522b..7b41c7b931 100644 --- a/java8/src/main/java/io/cucumber/java8/LambdaGlue.java +++ b/java8/src/main/java/io/cucumber/java8/LambdaGlue.java @@ -406,7 +406,7 @@ default void AfterStep(String tagExpression, int order, final HookNoArgsBody bod * type from the doc string * @see io.cucumber.docstring.DocStringType */ - default void DocStringType(String contentType, DocStringDefinitionBody body) { + default void DocStringType(String contentType, DocStringDefinitionBody body) { LambdaGlueRegistry.INSTANCE.get().addDocStringType(new Java8DocStringTypeDefinition(contentType, body)); } diff --git a/pom.xml b/pom.xml index 5fdaa603ba..8474e41e69 100644 --- a/pom.xml +++ b/pom.xml @@ -402,6 +402,7 @@ ${main.basedir}/.revapi/api-changes.json 7.0.0 + 7.2.0 internal testng