diff --git a/config/accepted-api-changes.json b/config/accepted-api-changes.json index bc76b209e..b7531e72a 100644 --- a/config/accepted-api-changes.json +++ b/config/accepted-api-changes.json @@ -8,5 +8,15 @@ "type": "io.micronaut.testresources.client.DefaultTestResourcesClient", "member": "Constructor io.micronaut.testresources.client.DefaultTestResourcesClient(io.micronaut.http.client.HttpClient,java.lang.String)", "reason": "This type is internal API" + }, + { + "type": "io.micronaut.testresources.client.TestResourcesClient", + "member": "Implemented interface io.micronaut.testresources.core.TestResourcesResolver", + "reason": "With the custom codec the client can no longer directly implement the interface" + }, + { + "type": "io.micronaut.testresources.client.TestResourcesClient", + "member": "Implemented interface io.micronaut.core.order.Ordered", + "reason": "Was supplied by the test resources resolver" } ] diff --git a/settings.gradle b/settings.gradle index b90fe5c99..27743c08d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -58,6 +58,7 @@ def localstackModules = [ include 'test-resources-bom' include 'test-resources-build-tools' +include 'test-resources-codec' include 'test-resources-core' include 'test-resources-client' include 'test-resources-elasticsearch' diff --git a/test-resources-client/build.gradle b/test-resources-client/build.gradle index d3dc4b7e8..9e05866cc 100644 --- a/test-resources-client/build.gradle +++ b/test-resources-client/build.gradle @@ -9,8 +9,7 @@ and provide their value on demand. """ dependencies { - api(mn.micronaut.json.core) api(project(':micronaut-test-resources-core')) - - testRuntimeOnly(mn.micronaut.http.server.netty) + api(project(":micronaut-test-resources-codec")) + testImplementation(mn.micronaut.http.server.netty) } diff --git a/test-resources-client/src/main/java/io/micronaut/testresources/client/DefaultTestResourcesClient.java b/test-resources-client/src/main/java/io/micronaut/testresources/client/DefaultTestResourcesClient.java index d6f20dc12..ee36a68c6 100644 --- a/test-resources-client/src/main/java/io/micronaut/testresources/client/DefaultTestResourcesClient.java +++ b/test-resources-client/src/main/java/io/micronaut/testresources/client/DefaultTestResourcesClient.java @@ -18,8 +18,13 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; -import io.micronaut.json.JsonMapper; +import io.micronaut.testresources.codec.Result; +import io.micronaut.testresources.codec.TestResourcesCodec; +import io.micronaut.testresources.codec.TestResourcesMediaType; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -52,7 +57,6 @@ public class DefaultTestResourcesClient implements TestResourcesClient { private static final Argument STRING = Argument.STRING; private static final Argument BOOLEAN = Argument.BOOLEAN; - private final JsonMapper jsonMapper; private final String baseUri; private final HttpClient client; @@ -66,47 +70,46 @@ public DefaultTestResourcesClient(String baseUri, String accessToken, int client .connectTimeout(clientTimeout) .build(); this.accessToken = accessToken; - this.jsonMapper = JsonMapper.createDefault(); } @Override - public List getResolvableProperties(Map> propertyEntries, - Map testResourcesConfig) { + public Result> getResolvableProperties(Map> propertyEntries, + Map testResourcesConfig) { Map properties = new HashMap<>(); properties.put("propertyEntries", propertyEntries); properties.put("testResourcesConfig", testResourcesConfig); - return request(RESOLVABLE_PROPERTIES_URI, LIST_OF_STRING, + return Result.of(request(RESOLVABLE_PROPERTIES_URI, LIST_OF_STRING, r -> POST(r, properties) - ); + )); } @Override - public Optional resolve(String name, Map properties, Map testResourcesConfig) { + public Optional> resolve(String name, Map properties, Map testResourcesConfig) { Map params = new HashMap<>(); params.put("name", name); params.put("properties", properties); params.put("testResourcesConfig", testResourcesConfig); - return Optional.ofNullable(request(RESOLVE_URI, STRING, r -> POST(r, params))); + return Result.asOptional(request(RESOLVE_URI, STRING, r -> POST(r, params))); } @Override - public List getRequiredProperties(String expression) { - return request(REQUIRED_PROPERTIES_URI + "/" + expression, LIST_OF_STRING, this::GET); + public Result> getRequiredProperties(String expression) { + return Result.of(request(REQUIRED_PROPERTIES_URI + "/" + expression, LIST_OF_STRING, this::GET)); } @Override - public List getRequiredPropertyEntries() { - return request(REQUIRED_PROPERTY_ENTRIES_URI, LIST_OF_STRING, this::GET); + public Result> getRequiredPropertyEntries() { + return Result.of(request(REQUIRED_PROPERTY_ENTRIES_URI, LIST_OF_STRING, this::GET)); } @Override - public boolean closeAll() { - return request(CLOSE_ALL_URI, BOOLEAN, this::GET); + public Result closeAll() { + return Result.of(request(CLOSE_ALL_URI, BOOLEAN, this::GET)); } @Override - public boolean closeScope(@Nullable String id) { - return request(CLOSE_URI + "/" + id, BOOLEAN, this::GET); + public Result closeScope(@Nullable String id) { + return Result.of(request(CLOSE_URI + "/" + id, BOOLEAN, this::GET)); } @SuppressWarnings({"java:S100", "checkstyle:MethodName"}) @@ -124,20 +127,17 @@ private T request(String path, Argument type, Consumer getResolvableProperties() { + default Result> getResolvableProperties() { return getResolvableProperties(Collections.emptyMap(), Collections.emptyMap()); } - @Override - @Post("/list") - List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig); + Result> getResolvableProperties(Map> propertyEntries, Map testResourcesConfig); - @Override - @Post("/resolve") - Optional resolve(String name, Map properties, Map testResourcesConfig); + Optional> resolve(String name, Map properties, Map testResourcesConfig); - @Override - @Get("/requirements/expr/{expression}") - List getRequiredProperties(String expression); + Result> getRequiredProperties(String expression); - @Override - @Get("/requirements/entries") - List getRequiredPropertyEntries(); + Result> getRequiredPropertyEntries(); /** * Closes all test resources. */ - @Get("/close/all") - boolean closeAll(); + Result closeAll(); - @Get("/close/{id}") - boolean closeScope(@Nullable String id); + Result closeScope(@Nullable String id); } diff --git a/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertyExpressionResolver.java b/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertyExpressionResolver.java index cf39c203f..75ea4ac2a 100644 --- a/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertyExpressionResolver.java +++ b/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertyExpressionResolver.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.value.PropertyResolver; +import io.micronaut.testresources.codec.Result; import io.micronaut.testresources.core.LazyTestResourcesExpressionResolver; import io.micronaut.testresources.core.TestResourcesResolver; import org.slf4j.Logger; @@ -27,7 +28,6 @@ import java.net.URL; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -60,33 +60,33 @@ private static class NoOpClient implements TestResourcesClient { static final TestResourcesClient INSTANCE = new NoOpClient(); @Override - public List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { - return Collections.emptyList(); + public Result> getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { + return Result.emptyList(); } @Override - public Optional resolve(String name, Map properties, Map testResourcesConfig) { + public Optional> resolve(String name, Map properties, Map testResourcesConfig) { return Optional.empty(); } @Override - public List getRequiredProperties(String expression) { - return Collections.emptyList(); + public Result> getRequiredProperties(String expression) { + return Result.emptyList(); } @Override - public List getRequiredPropertyEntries() { - return Collections.emptyList(); + public Result> getRequiredPropertyEntries() { + return Result.emptyList(); } @Override - public boolean closeAll() { - return true; + public Result closeAll() { + return Result.TRUE; } @Override - public boolean closeScope(@Nullable String id) { - return true; + public Result closeScope(@Nullable String id) { + return Result.FALSE; } } @@ -100,7 +100,27 @@ public Optional resolve(PropertyResolver propertyResolver, Class requiredType) { if (propertyResolver instanceof Environment) { TestResourcesClient client = clients.computeIfAbsent((Environment) propertyResolver, TestResourcesClientPropertyExpressionResolver::createClient); - Map props = resolveRequiredProperties(expression, propertyResolver, client); + Map props = resolveRequiredProperties(expression, propertyResolver, new TestResourcesResolver() { + @Override + public List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { + return client.getResolvableProperties(propertyEntries, testResourcesConfig).value(); + } + + @Override + public Optional resolve(String propertyName, Map properties, Map testResourcesConfig) { + return client.resolve(propertyName, properties, testResourcesConfig).map(Result::value); + } + + @Override + public List getRequiredProperties(String expression) { + return client.getRequiredProperties(expression).value(); + } + + @Override + public List getRequiredPropertyEntries() { + return client.getRequiredPropertyEntries().value(); + } + }); Map properties = propertyResolver.getProperties(TestResourcesResolver.TEST_RESOURCES_PROPERTY); Optional resolved = callClient(expression, client, props, properties); if (resolved.isPresent()) { @@ -115,7 +135,7 @@ public Optional resolve(PropertyResolver propertyResolver, } private static Optional callClient(String expression, TestResourcesClient client, Map props, Map properties) { - return client.resolve(expression, props, properties); + return client.resolve(expression, props, properties).map(Result::value); } @Override diff --git a/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertySourceLoader.java b/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertySourceLoader.java index f30040a05..9f9d79f07 100644 --- a/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertySourceLoader.java +++ b/test-resources-client/src/main/java/io/micronaut/testresources/client/TestResourcesClientPropertySourceLoader.java @@ -16,6 +16,7 @@ package io.micronaut.testresources.client; import io.micronaut.core.io.ResourceLoader; +import io.micronaut.testresources.codec.Result; import io.micronaut.testresources.core.LazyTestResourcesPropertySourceLoader; import io.micronaut.testresources.core.PropertyExpressionProducer; @@ -51,6 +52,7 @@ private static class ClientTestResourcesResolver implements PropertyExpressionPr public List getPropertyEntries() { return findClient(null) .map(TestResourcesClient::getRequiredPropertyEntries) + .map(Result::value) .orElse(Collections.emptyList()); } @@ -58,6 +60,7 @@ public List getPropertyEntries() { public List produceKeys(ResourceLoader resourceLoader, Map> propertyEntries, Map testResourcesConfig) { return findClient(resourceLoader) .map(client -> client.getResolvableProperties(propertyEntries, testResourcesConfig)) + .map(Result::value) .orElse(Collections.emptyList()); } diff --git a/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestServer.groovy b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestServer.groovy index 14c04d8e6..7b1b33c2f 100644 --- a/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestServer.groovy +++ b/test-resources-client/src/test/groovy/io/micronaut/testresources/client/TestServer.groovy @@ -1,44 +1,45 @@ package io.micronaut.testresources.client import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Consumes import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post -import io.micronaut.testresources.core.TestResourcesResolver +import io.micronaut.http.annotation.Produces +import io.micronaut.testresources.codec.Result +import io.micronaut.testresources.codec.TestResourcesMediaType @Controller("/") @Requires(property = 'server', notEquals = 'false') -class TestServer implements TestResourcesResolver { +@Produces(TestResourcesMediaType.TEST_RESOURCES_BINARY) +@Consumes(TestResourcesMediaType.TEST_RESOURCES_BINARY) +class TestServer { - @Override @Post("/list") - List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { - ["dummy1", "dummy2", "missing"] + Result> getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { + Result.of(["dummy1", "dummy2", "missing"]) } - @Override @Get("/requirements/expr/{expression}") - List getRequiredProperties(String expression) { - [] + Result> getRequiredProperties(String expression) { + Result.of([]) } - @Override @Get("/requirements/entries") - List getRequiredPropertyEntries() { - [] + Result> getRequiredPropertyEntries() { + Result.of([]) } - @Override @Post('/resolve') - Optional resolve(String name, Map properties, Map testResourcesConfig) { + Optional> resolve(String name, Map properties, Map testResourcesConfig) { if ("missing" == name) { return Optional.empty() } - Optional.of("value for $name".toString()) + Result.asOptional("value for $name".toString()) } @Get("/close/all") - void closeAll() { - + Result closeAll() { + Result.TRUE } } diff --git a/test-resources-codec/build.gradle b/test-resources-codec/build.gradle new file mode 100644 index 000000000..8de65467c --- /dev/null +++ b/test-resources-codec/build.gradle @@ -0,0 +1,16 @@ +plugins { + id("io.micronaut.build.internal.test-resources-module") +} + +dependencies { + implementation(mn.micronaut.http) +} + +micronautBuild { + def simpleVersion = version + if (version.contains('-')) { + simpleVersion = version.substring(0, version.indexOf('-')) + } + def (String major, String minor, String patch) = simpleVersion.split("[.]") + binaryCompatibility.enabled = major.toInteger() >=2 && minor.toInteger() >= 0 && patch.toInteger() > 0 +} diff --git a/test-resources-codec/src/main/java/io/micronaut/testresources/codec/Result.java b/test-resources-codec/src/main/java/io/micronaut/testresources/codec/Result.java new file mode 100644 index 000000000..2676ce6bb --- /dev/null +++ b/test-resources-codec/src/main/java/io/micronaut/testresources/codec/Result.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.testresources.codec; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * A wrapper type which is used for all messages between the client and + * the server as return types. This is done because types like String are + * otherwise treated differently by Micronaut, bypassing our codec. + * + * Note that it is possible to skip this type for types like `List` + * or `Map` but it makes the code less understandable since in some + * cases the codec would be called and some others not. + * + * Therefore, for consistency, all argument types are wrapped in that + * result type. + * + * @param value the value to be wrapped + * @param the type of the result + */ +public record Result(T value) { + public static final Result TRUE = new Result<>(true); + public static final Result FALSE = new Result<>(false); + + private static final Result EMPTY_LIST = new Result<>(Collections.emptyList()); + + /** + * Wraps a value into a result type. + * @param value the value + * @return the wrapped value + * @param the type of the value + */ + public static Result of(@NonNull V value) { + return new Result<>(value); + } + + /** + * Wraps a potentially null value into an optional. + * If the value is null, then it returns an empty optional. + * If the value is not null, then it returns an optional + * which value is a wrapped result. + * @param value the value + * @return an optional of wrapped value + * @param the type of the value + */ + public static Optional> asOptional(@Nullable V value) { + if (value == null) { + return Optional.empty(); + } + return Optional.of(new Result<>(value)); + } + + @SuppressWarnings("unchecked") + public static Result> emptyList() { + return (Result>) EMPTY_LIST; + } +} diff --git a/test-resources-codec/src/main/java/io/micronaut/testresources/codec/TestResourcesBodyHandler.java b/test-resources-codec/src/main/java/io/micronaut/testresources/codec/TestResourcesBodyHandler.java new file mode 100644 index 000000000..6225611cb --- /dev/null +++ b/test-resources-codec/src/main/java/io/micronaut/testresources/codec/TestResourcesBodyHandler.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.testresources.codec; + +import io.micronaut.context.annotation.BootstrapContextCompatible; +import io.micronaut.core.convert.value.ConvertibleValues; +import io.micronaut.core.type.Argument; +import io.micronaut.core.type.Headers; +import io.micronaut.core.type.MutableHeaders; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Consumes; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.body.MessageBodyHandler; +import io.micronaut.http.codec.CodecException; +import jakarta.inject.Singleton; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; + +import static io.micronaut.testresources.codec.TestResourcesMediaType.TEST_RESOURCES_BINARY; +import static io.micronaut.testresources.codec.TestResourcesMediaType.TEST_RESOURCES_BINARY_MEDIA_TYPE; + +/** + * The test resources binary protocol body handler. + * + * @param the type of the arguments + * @since 2.0.0 + */ +@Singleton +@Consumes(TEST_RESOURCES_BINARY) +@Produces(TEST_RESOURCES_BINARY) +@BootstrapContextCompatible +public class TestResourcesBodyHandler implements MessageBodyHandler { + @Override + public boolean isReadable(Argument type, MediaType mediaType) { + return mediaType.matches(TEST_RESOURCES_BINARY_MEDIA_TYPE); + } + + @Override + public boolean isWriteable(Argument type, MediaType mediaType) { + return mediaType.matches(TEST_RESOURCES_BINARY_MEDIA_TYPE); + } + + @Override + @SuppressWarnings("unchecked") + public T read(Argument type, MediaType mediaType, Headers httpHeaders, InputStream inputStream) throws CodecException { + try { + var dis = new DataInputStream(inputStream); + var result = TestResourcesCodec.readObject(dis); + if (result instanceof Map map && type.getType().equals(ConvertibleValues.class)) { + return (T) ConvertibleValues.of(map); + } + if (Result.class.equals(type.getType())) { + return (T) Result.of(result); + } + return (T) result; + } catch (IOException e) { + throw new CodecException("Test resources wasn't able to decode response", e); + } + } + + @Override + public void writeTo(Argument type, MediaType mediaType, T object, MutableHeaders outgoingHeaders, OutputStream outputStream) throws CodecException { + try { + var dos = new DataOutputStream(outputStream); + outgoingHeaders.set(HttpHeaders.CONTENT_TYPE, TEST_RESOURCES_BINARY_MEDIA_TYPE); + TestResourcesCodec.writeObject(object, dos); + } catch (IOException e) { + throw new CodecException("Test resources wasn't able to encode response", e); + } + } + +} diff --git a/test-resources-codec/src/main/java/io/micronaut/testresources/codec/TestResourcesCodec.java b/test-resources-codec/src/main/java/io/micronaut/testresources/codec/TestResourcesCodec.java new file mode 100644 index 000000000..03c07f3b0 --- /dev/null +++ b/test-resources-codec/src/main/java/io/micronaut/testresources/codec/TestResourcesCodec.java @@ -0,0 +1,148 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.testresources.codec; + +import io.micronaut.http.codec.CodecException; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This codec is responsible for (de)serializing the binary stream + * between the test resources client and server. It is used by the + * message body handler in the controller, but also directly by + * the test resources client to decode messages. + * + * @since 2.0.0 + */ +public final class TestResourcesCodec { + private TestResourcesCodec() { + + } + + public static V readObject(DataInputStream dis) throws IOException { + var kind = SupportedType.of(dis.readByte()); + return switch (kind) { + case NULL -> null; + case RESULT -> readObject(dis); + case BOOLEAN -> cast(dis.readBoolean()); + case INTEGER -> cast(dis.readInt()); + case STRING -> cast(dis.readUTF()); + case LIST -> { + int count = dis.readInt(); + List list = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + list.add(readObject(dis)); + } + yield cast(Collections.unmodifiableList(list)); + } + case MAP -> { + int count = dis.readInt(); + Map map = new HashMap<>(); + for (int i = 0; i < count; i++) { + Object key = readObject(dis); + Object value = readObject(dis); + map.put(key, value); + } + yield cast(Collections.unmodifiableMap(map)); + } + }; + } + + public static void writeObject(Object object, DataOutputStream dos) throws IOException { + var kind = SupportedType.kindOf(object); + if (SupportedType.RESULT == kind) { + writeObject(((Result) object).value(), dos); + return; + } + dos.write(kind.asByte()); + switch (kind) { + case BOOLEAN -> dos.writeBoolean((Boolean) object); + case INTEGER -> dos.writeInt((Integer) object); + case STRING -> dos.writeUTF((String) object); + case LIST -> { + List list = cast(object); + dos.writeInt(list.size()); + for (Object o : list) { + writeObject(o, dos); + } + } + case MAP -> { + Map map = cast(object); + dos.writeInt(map.size()); + for (Map.Entry entry : map.entrySet()) { + writeObject(entry.getKey(), dos); + writeObject(entry.getValue(), dos); + } + } + default -> { + } + } + } + + @SuppressWarnings("unchecked") + private static V cast(Object o) { + return (V) o; + } + + enum SupportedType { + NULL, + RESULT, + BOOLEAN, + INTEGER, + STRING, + LIST, + MAP; + + byte asByte() { + return (byte) ordinal(); + } + + static SupportedType of(byte b) { + return SupportedType.values()[b]; + } + + public static SupportedType kindOf(Object o) { + if (o == null) { + return NULL; + } + var clazz = o.getClass(); + if (Result.class.equals(clazz)) { + return RESULT; + } + if (Boolean.TYPE.equals(clazz) || Boolean.class.equals(clazz)) { + return BOOLEAN; + } + if (String.class.equals(clazz)) { + return STRING; + } + if (List.class.isAssignableFrom(clazz)) { + return LIST; + } + if (Map.class.isAssignableFrom(clazz)) { + return MAP; + } + throw new CodecException("Unsupported type " + clazz); + } + + } +} diff --git a/test-resources-codec/src/main/java/io/micronaut/testresources/codec/TestResourcesMediaType.java b/test-resources-codec/src/main/java/io/micronaut/testresources/codec/TestResourcesMediaType.java new file mode 100644 index 000000000..35da2b7ab --- /dev/null +++ b/test-resources-codec/src/main/java/io/micronaut/testresources/codec/TestResourcesMediaType.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.testresources.codec; + +import io.micronaut.http.MediaType; + +/** + * Provides constants used to declare the test resources binary + * protocol media type. + * + * @since 2.0.0 + */ +public final class TestResourcesMediaType { + public static final String TEST_RESOURCES_BINARY = "application/x-test-resources+binary"; + public static final MediaType TEST_RESOURCES_BINARY_MEDIA_TYPE = MediaType.of(TEST_RESOURCES_BINARY); + + private TestResourcesMediaType() { + + } +} diff --git a/test-resources-extensions/test-resources-extensions-core/build.gradle b/test-resources-extensions/test-resources-extensions-core/build.gradle index f17e6c62c..bdfc7fef3 100644 --- a/test-resources-extensions/test-resources-extensions-core/build.gradle +++ b/test-resources-extensions/test-resources-extensions-core/build.gradle @@ -12,6 +12,7 @@ dependencies { api(platform(mnTest.micronaut.test.bom)) api(mnTest.micronaut.test.core) implementation(projects.micronautTestResourcesClient) + implementation(projects.micronautTestResourcesCodec) testAnnotationProcessor(mn.micronaut.inject.java) testImplementation(mnTest.micronaut.test.junit5) diff --git a/test-resources-extensions/test-resources-extensions-core/src/main/java/io/micronaut/test/extensions/testresources/TestResourcesClientHolder.java b/test-resources-extensions/test-resources-extensions-core/src/main/java/io/micronaut/test/extensions/testresources/TestResourcesClientHolder.java index fcbca11a9..c138eddf4 100644 --- a/test-resources-extensions/test-resources-extensions-core/src/main/java/io/micronaut/test/extensions/testresources/TestResourcesClientHolder.java +++ b/test-resources-extensions/test-resources-extensions-core/src/main/java/io/micronaut/test/extensions/testresources/TestResourcesClientHolder.java @@ -17,6 +17,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.testresources.client.TestResourcesClient; +import io.micronaut.testresources.codec.Result; import java.util.Collection; import java.util.List; @@ -58,37 +59,37 @@ private static T nullSafe(Supplier value) { } @Override - public List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { + public Result> getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { return nullSafe(CLIENT::getResolvableProperties); } @Override - public Optional resolve(String name, Map properties, Map testResourcesConfig) { + public Optional> resolve(String name, Map properties, Map testResourcesConfig) { return nullSafe(() -> CLIENT.resolve(name, properties, testResourcesConfig)); } @Override - public List getRequiredProperties(String expression) { + public Result> getRequiredProperties(String expression) { return nullSafe(() -> CLIENT.getRequiredProperties(expression)); } @Override - public List getRequiredPropertyEntries() { + public Result> getRequiredPropertyEntries() { return nullSafe(CLIENT::getRequiredPropertyEntries); } @Override - public boolean closeAll() { + public Result closeAll() { return nullSafe(CLIENT::closeAll); } @Override - public boolean closeScope(String id) { + public Result closeScope(String id) { return nullSafe(() -> CLIENT.closeScope(id)); } @Override - public List getResolvableProperties() { + public Result> getResolvableProperties() { return nullSafe(CLIENT::getResolvableProperties); } diff --git a/test-resources-extensions/test-resources-extensions-core/src/main/java/io/micronaut/test/extensions/testresources/TestResourcesPropertiesFactory.java b/test-resources-extensions/test-resources-extensions-core/src/main/java/io/micronaut/test/extensions/testresources/TestResourcesPropertiesFactory.java index 81b389dfe..b2fb4ec7a 100644 --- a/test-resources-extensions/test-resources-extensions-core/src/main/java/io/micronaut/test/extensions/testresources/TestResourcesPropertiesFactory.java +++ b/test-resources-extensions/test-resources-extensions-core/src/main/java/io/micronaut/test/extensions/testresources/TestResourcesPropertiesFactory.java @@ -19,6 +19,7 @@ import io.micronaut.test.support.TestPropertyProvider; import io.micronaut.test.support.TestPropertyProviderFactory; import io.micronaut.testresources.client.TestResourcesClientFactory; +import io.micronaut.testresources.codec.Result; import java.lang.reflect.InvocationTargetException; import java.util.Collections; @@ -67,7 +68,7 @@ public Map getProperties() { Map resolvedProperties = Stream.of(requestedProperties) .map(v -> new Object() { private final String key = v; - private final String value = client.resolve(v, Map.of(), testResourcesConfig).orElse(null); + private final String value = client.resolve(v, Map.of(), testResourcesConfig).map(Result::value).orElse(null); }) .filter(o -> o.value != null) .collect(Collectors.toMap(e -> e.key, e -> e.value)); diff --git a/test-resources-extensions/test-resources-extensions-core/src/testFixtures/java/io/micronaut/test/extensions/testresources/FakeTestResourcesClient.java b/test-resources-extensions/test-resources-extensions-core/src/testFixtures/java/io/micronaut/test/extensions/testresources/FakeTestResourcesClient.java index 409518eb3..3defbdeb9 100644 --- a/test-resources-extensions/test-resources-extensions-core/src/testFixtures/java/io/micronaut/test/extensions/testresources/FakeTestResourcesClient.java +++ b/test-resources-extensions/test-resources-extensions-core/src/testFixtures/java/io/micronaut/test/extensions/testresources/FakeTestResourcesClient.java @@ -16,6 +16,7 @@ package io.micronaut.test.extensions.testresources; import io.micronaut.testresources.client.TestResourcesClient; +import io.micronaut.testresources.codec.Result; import java.util.Collection; import java.util.List; @@ -30,32 +31,32 @@ public class FakeTestResourcesClient implements TestResourcesClient { ); @Override - public List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { - return MOCK_PROPERTIES.keySet().stream().toList(); + public Result> getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { + return new Result<>(MOCK_PROPERTIES.keySet().stream().toList()); } @Override - public Optional resolve(String name, Map properties, Map testResourcesConfig) { - return Optional.ofNullable(MOCK_PROPERTIES.get(name)); + public Optional> resolve(String name, Map properties, Map testResourcesConfig) { + return Result.asOptional(MOCK_PROPERTIES.get(name)); } @Override - public List getRequiredProperties(String expression) { - return List.of(); + public Result> getRequiredProperties(String expression) { + return Result.emptyList(); } @Override - public List getRequiredPropertyEntries() { - return List.of(); + public Result> getRequiredPropertyEntries() { + return Result.emptyList(); } @Override - public boolean closeAll() { - return true; + public Result closeAll() { + return Result.TRUE; } @Override - public boolean closeScope(String id) { - return true; + public Result closeScope(String id) { + return Result.TRUE; } } diff --git a/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClient.java b/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClient.java index a30953301..019de51f8 100644 --- a/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClient.java +++ b/test-resources-extensions/test-resources-extensions-junit-platform/src/testFixtures/java/io/micronaut/test/extensions/testresources/junit5/FakeTestResourcesClient.java @@ -16,6 +16,7 @@ package io.micronaut.test.extensions.testresources.junit5; import io.micronaut.testresources.client.TestResourcesClient; +import io.micronaut.testresources.codec.Result; import java.util.Collection; import java.util.HashSet; @@ -32,34 +33,34 @@ public class FakeTestResourcesClient implements TestResourcesClient { private static final ThreadLocal> CLOSED_SCOPES = ThreadLocal.withInitial(HashSet::new); @Override - public List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { - return MOCK_PROPERTIES.keySet().stream().toList(); + public Result> getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { + return Result.of(MOCK_PROPERTIES.keySet().stream().toList()); } @Override - public Optional resolve(String name, Map properties, Map testResourcesConfig) { - return Optional.ofNullable(MOCK_PROPERTIES.get(name)); + public Optional> resolve(String name, Map properties, Map testResourcesConfig) { + return Result.asOptional(MOCK_PROPERTIES.get(name)); } @Override - public List getRequiredProperties(String expression) { - return List.of(); + public Result> getRequiredProperties(String expression) { + return Result.emptyList(); } @Override - public List getRequiredPropertyEntries() { - return List.of(); + public Result> getRequiredPropertyEntries() { + return Result.emptyList(); } @Override - public boolean closeAll() { - return true; + public Result closeAll() { + return Result.TRUE; } @Override - public boolean closeScope(String id) { + public Result closeScope(String id) { CLOSED_SCOPES.get().add(id); - return true; + return Result.TRUE; } public static Set closedScopes() { diff --git a/test-resources-server/build.gradle b/test-resources-server/build.gradle index f960d89e6..4a073c1c5 100644 --- a/test-resources-server/build.gradle +++ b/test-resources-server/build.gradle @@ -19,9 +19,9 @@ dependencies { implementation(project(':micronaut-test-resources-core')) implementation(project(':micronaut-test-resources-embedded')) implementation(project(':micronaut-test-resources-testcontainers')) + implementation(project(':micronaut-test-resources-codec')) runtimeOnly(mnLogging.logback.classic) runtimeOnly(mn.micronaut.management) - runtimeOnly(mnSerde.micronaut.serde.jackson) testImplementation(mn.micronaut.http.client) testImplementation(project(':micronaut-test-resources-client')) diff --git a/test-resources-server/src/main/java/io/micronaut/testresources/server/TestContainer.java b/test-resources-server/src/main/java/io/micronaut/testresources/server/TestContainer.java index dc725ba18..440496a29 100644 --- a/test-resources-server/src/main/java/io/micronaut/testresources/server/TestContainer.java +++ b/test-resources-server/src/main/java/io/micronaut/testresources/server/TestContainer.java @@ -18,6 +18,8 @@ import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.Nullable; +import java.util.Map; + /** * Stores metadata about a running test container. */ @@ -69,6 +71,15 @@ public String getScope() { return scope; } + public Map asMap() { + return Map.of( + "name", name, + "imageName", imageName, + "id", id, + "scope", scope + ); + } + @Override public String toString() { return "TestContainer{" + diff --git a/test-resources-server/src/main/java/io/micronaut/testresources/server/TestResourcesController.java b/test-resources-server/src/main/java/io/micronaut/testresources/server/TestResourcesController.java index e33df3690..be9f82b7d 100644 --- a/test-resources-server/src/main/java/io/micronaut/testresources/server/TestResourcesController.java +++ b/test-resources-server/src/main/java/io/micronaut/testresources/server/TestResourcesController.java @@ -16,9 +16,13 @@ package io.micronaut.testresources.server; import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Consumes; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; +import io.micronaut.testresources.codec.Result; +import io.micronaut.testresources.codec.TestResourcesMediaType; import io.micronaut.testresources.core.TestResourcesResolver; import io.micronaut.testresources.embedded.TestResourcesResolverLoader; import io.micronaut.testresources.testcontainers.TestContainers; @@ -30,92 +34,89 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; /** * A client responsible for connecting to a test resources * server. */ @Controller("/") -public final class TestResourcesController implements TestResourcesResolver { +@Produces(TestResourcesMediaType.TEST_RESOURCES_BINARY) +@Consumes(TestResourcesMediaType.TEST_RESOURCES_BINARY) +public final class TestResourcesController { private static final Logger LOGGER = LoggerFactory.getLogger(TestResourcesController.class); private final TestResourcesResolverLoader loader = new TestResourcesResolverLoader(); @Get("/list") - public List getResolvableProperties() { + public Result> getResolvableProperties() { return getResolvableProperties(Collections.emptyMap(), Collections.emptyMap()); } - @Override @Post("/list") - public List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { - return loader.getResolvers() + public Result> getResolvableProperties(Map> propertyEntries, Map testResourcesConfig) { + return Result.of(loader.getResolvers() .stream() .map(r -> r.getResolvableProperties(propertyEntries, testResourcesConfig)) .flatMap(Collection::stream) .distinct() .peek(p -> LOGGER.debug("For configuration {} and property entries {} , resolvable property: {}", testResourcesConfig, propertyEntries, p)) - .collect(Collectors.toList()); + .toList()); } - @Override @Get("/requirements/expr/{expression}") - public List getRequiredProperties(String expression) { - return loader.getResolvers() + public Result> getRequiredProperties(String expression) { + return Result.of(loader.getResolvers() .stream() .map(testResourcesResolver -> testResourcesResolver.getRequiredProperties(expression)) .flatMap(Collection::stream) .distinct() - .collect(Collectors.toList()); + .toList()); } - @Override @Get("/requirements/entries") - public List getRequiredPropertyEntries() { - return loader.getResolvers() + public Result> getRequiredPropertyEntries() { + return Result.of(loader.getResolvers() .stream() .map(TestResourcesResolver::getRequiredPropertyEntries) .flatMap(Collection::stream) .distinct() - .collect(Collectors.toList()); + .toList()); } @Post("/resolve") - public Optional resolve(String name, - Map properties, - Map testResourcesConfig) { - Optional result = Optional.empty(); + public Optional> resolve(String name, + Map properties, + Map testResourcesConfig) { for (TestResourcesResolver resolver : loader.getResolvers()) { - result = resolver.resolve(name, properties, testResourcesConfig); + Optional result = resolver.resolve(name, properties, testResourcesConfig); LOGGER.debug("Attempt to resolve {} with resolver {}, properties {} and test resources configuration {} : {}", name, resolver.getClass(), properties, testResourcesConfig, result.isPresent() ? result.get() : "\uD83D\uDEAB"); if (result.isPresent()) { - return result; + return Result.asOptional(result.get()); } } - return result; + return Optional.empty(); } @Get("/close/all") - public boolean closeAll() { + public Result closeAll() { LOGGER.debug("Closing all test resources"); - return TestContainers.closeAll(); + return Result.of(TestContainers.closeAll()); } @Get("/close/{id}") - public boolean closeScope(@Nullable String id) { + public Result closeScope(@Nullable String id) { LOGGER.info("Closing test resources of scope {}", id); - return TestContainers.closeScope(id); + return Result.of(TestContainers.closeScope(id)); } @Get("/testcontainers") - public List listContainers() { + public Result>> listContainers() { return listContainersByScope(null); } @Get("/testcontainers/{scope}") - public List listContainersByScope(@Nullable String scope) { - return TestContainers.listByScope(scope) + public Result>> listContainersByScope(@Nullable String scope) { + return Result.of(TestContainers.listByScope(scope) .entrySet() .stream() .flatMap(entry -> entry.getValue().stream() @@ -123,9 +124,9 @@ public List listContainersByScope(@Nullable String scope) { c.getContainerName(), c.getDockerImageName(), c.getContainerId(), - entry.getKey().toString()) + entry.getKey().toString()).asMap() )) - .collect(Collectors.toList()); + .toList()); } } diff --git a/test-resources-server/src/test/groovy/io/micronaut/testresources/server/TestResourcesControllerTest.groovy b/test-resources-server/src/test/groovy/io/micronaut/testresources/server/TestResourcesControllerTest.groovy index fc704fb15..c9a70aea5 100644 --- a/test-resources-server/src/test/groovy/io/micronaut/testresources/server/TestResourcesControllerTest.groovy +++ b/test-resources-server/src/test/groovy/io/micronaut/testresources/server/TestResourcesControllerTest.groovy @@ -2,11 +2,15 @@ package io.micronaut.testresources.server import io.micronaut.context.annotation.Property import io.micronaut.core.annotation.Nullable +import io.micronaut.http.annotation.Consumes import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Produces import io.micronaut.http.client.annotation.Client import io.micronaut.test.extensions.spock.annotation.MicronautTest import io.micronaut.testresources.client.TestResourcesClient +import io.micronaut.testresources.codec.Result +import io.micronaut.testresources.codec.TestResourcesMediaType import jakarta.inject.Inject import spock.lang.Specification @@ -20,14 +24,15 @@ class TestResourcesControllerTest extends Specification { def "verifies that server can instantiate container"() { expect: - client.resolvableProperties == ['kafka.bootstrap.servers'] - client.listContainers().empty + client.resolvableProperties.value() == ['kafka.bootstrap.servers'] + client.listContainers().value().empty when: - client.resolve("kafka.bootstrap.servers", [:], [:]) + Optional> servers = client.resolve("kafka.bootstrap.servers", [:], [:]) then: - def containers = client.listContainers() + servers.present + def containers = client.listContainers().value() containers.size() == 1 containers[0].imageName.startsWith 'confluentinc/cp-kafka' @@ -35,37 +40,39 @@ class TestResourcesControllerTest extends Specification { client.closeAll() then: - client.listContainers().empty + client.listContainers().value().empty } @Client("/") + @Produces(TestResourcesMediaType.TEST_RESOURCES_BINARY) + @Consumes(TestResourcesMediaType.TEST_RESOURCES_BINARY) static interface DiagnosticsClient extends TestResourcesClient { @Get("/testcontainers") - List listContainers(); + Result> listContainers(); @Override @Post("/list") - List getResolvableProperties(Map> propertyEntries, Map testResourcesConfig); + Result> getResolvableProperties(Map> propertyEntries, Map testResourcesConfig); @Override @Post("/resolve") - Optional resolve(String name, Map properties, Map testResourcesConfig); + Optional> resolve(String name, Map properties, Map testResourcesConfig); @Override @Get("/requirements/expr/{expression}") - List getRequiredProperties(String expression); + Result> getRequiredProperties(String expression); @Override @Get("/requirements/entries") - List getRequiredPropertyEntries(); + Result> getRequiredPropertyEntries(); /** * Closes all test resources. */ @Get("/close/all") - boolean closeAll(); + Result closeAll(); @Get("/close/{id}") - boolean closeScope(@Nullable String id); + Result closeScope(@Nullable String id); } }