diff --git a/services-api/src/main/java/io/scalecube/services/RequestContext.java b/services-api/src/main/java/io/scalecube/services/RequestContext.java index b82bea1aa..ca32ad761 100644 --- a/services-api/src/main/java/io/scalecube/services/RequestContext.java +++ b/services-api/src/main/java/io/scalecube/services/RequestContext.java @@ -9,9 +9,8 @@ import io.scalecube.services.auth.Principal; import io.scalecube.services.exceptions.ForbiddenException; import io.scalecube.services.methods.MethodInfo; -import java.math.BigDecimal; -import java.math.BigInteger; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -34,7 +33,7 @@ public class RequestContext implements Context { private static final Object REQUEST_KEY = new Object(); private static final Object PRINCIPAL_KEY = new Object(); private static final Object METHOD_INFO_KEY = new Object(); - private static final Object PATH_VARS_KEY = new Object(); + private static final Object PATH_PARAMS_KEY = new Object(); private final Context source; @@ -102,17 +101,40 @@ public Context delete(Object key) { /** * Returns request headers. * - * @return headers, or {@code null} if not set + * @return headers, or empty map if not set */ public Map headers() { return source.getOrDefault(HEADERS_KEY, Collections.emptyMap()); } + /** + * Returns typed access to all request headers. + * + * @return typed parameters for headers + */ + public TypedParameters headerParams() { + return new TypedParameters(headers()); + } + + /** + * Returns typed access to request headers filtered by the given prefix. Headers matching + * "{prefix}." are included with the prefix stripped from the key. + * + * @param prefix header prefix to filter by (if null or empty, returns all headers) + * @return typed parameters for filtered headers + */ + public TypedParameters headerParams(String prefix) { + if (prefix == null || prefix.isEmpty()) { + return headerParams(); + } + return new TypedParameters(filterByPrefix(headers(), prefix)); + } + /** * Puts request headers to the context. * * @param headers headers - * @return new {@code RequestContext} instance with updated headers + * @return new {@code RequestContext} */ public RequestContext headers(Map headers) { return put(HEADERS_KEY, headers); @@ -131,7 +153,7 @@ public Object request() { * Puts request to the context. * * @param request request - * @return new {@code RequestContext} instance with updated request + * @return new {@code RequestContext} */ public RequestContext request(Object request) { return put(REQUEST_KEY, request); @@ -187,7 +209,7 @@ public Principal principal() { * Puts principal to the context. * * @param principal principal - * @return new {@code RequestContext} instance with the updated principal + * @return new {@code RequestContext} */ public RequestContext principal(Principal principal) { return put(PRINCIPAL_KEY, principal); @@ -224,69 +246,21 @@ public RequestContext methodInfo(MethodInfo methodInfo) { } /** - * Returns path variables associated with the request. + * Returns path parameters associated with the request. * - * @return path variables, or {@code null} if not set + * @return path parameters, or empty map if not set */ - public Map pathVars() { - return source.getOrDefault(PATH_VARS_KEY, Collections.emptyMap()); + public TypedParameters pathParams() { + return new TypedParameters(source.getOrDefault(PATH_PARAMS_KEY, Collections.emptyMap())); } /** - * Puts path variables associated with the request. + * Puts path parameters associated with the request. * - * @return path variables, or {@code null} if not set + * @return new {@code RequestContext} */ - public RequestContext pathVars(Map pathVars) { - return put(PATH_VARS_KEY, pathVars); - } - - /** - * Returns specific path variable by name. - * - * @param name name of the path variable - * @return path variable value, or {@code null} if not found - */ - public String pathVar(String name) { - return pathVars().get(name); - } - - /** - * Returns specific path variable by name, and converts it to the specified type. - * - * @param name name of the path variable - * @param type expected type of the variable - * @param type parameter - * @return converted path variable, or {@code null} if not found - */ - public T pathVar(String name, Class type) { - final var s = pathVar(name); - if (s == null) { - return null; - } - - if (type == String.class) { - //noinspection unchecked - return (T) s; - } - if (type == Integer.class) { - //noinspection unchecked - return (T) Integer.valueOf(s); - } - if (type == Long.class) { - //noinspection unchecked - return (T) Long.valueOf(s); - } - if (type == BigDecimal.class) { - //noinspection unchecked - return (T) new BigDecimal(s); - } - if (type == BigInteger.class) { - //noinspection unchecked - return (T) new BigInteger(s); - } - - throw new IllegalArgumentException("Unsupported pathVar type: " + type); + public RequestContext pathParams(Map pathParams) { + return put(PATH_PARAMS_KEY, pathParams); } /** @@ -360,13 +334,28 @@ public static Mono deferSecured() { }); } + private static Map filterByPrefix(Map map, String prefix) { + if (map == null || map.isEmpty()) { + return Map.of(); + } + final var finalPrefix = prefix + "."; + final var result = new HashMap(); + map.forEach( + (k, v) -> { + if (k.startsWith(finalPrefix)) { + result.put(k.substring(finalPrefix.length()), v); + } + }); + return result; + } + @Override public String toString() { return new StringJoiner(", ", RequestContext.class.getSimpleName() + "[", "]") .add("principal=" + principal()) .add("methodInfo=" + methodInfo()) .add("headers=" + mask(headers())) - .add("pathVars=" + mask(pathVars())) + .add("pathParams=" + source.getOrDefault(PATH_PARAMS_KEY, Collections.emptyMap())) .add("sourceKeys=" + source.stream().map(Entry::getKey).toList()) .toString(); } diff --git a/services-api/src/main/java/io/scalecube/services/ServiceCall.java b/services-api/src/main/java/io/scalecube/services/ServiceCall.java index fa8d396a8..5e7cce338 100644 --- a/services-api/src/main/java/io/scalecube/services/ServiceCall.java +++ b/services-api/src/main/java/io/scalecube/services/ServiceCall.java @@ -425,13 +425,17 @@ private Optional toStringOrEqualsOrHashCode( private static Function, Flux> asFlux( boolean isReturnTypeServiceMessage) { return flux -> - isReturnTypeServiceMessage ? flux.cast(Object.class) : flux.map(ServiceMessage::data); + isReturnTypeServiceMessage + ? flux.cast(Object.class) + : flux.mapNotNull(ServiceMessage::data); } private static Function, Mono> asMono( boolean isReturnTypeServiceMessage) { return mono -> - isReturnTypeServiceMessage ? mono.cast(Object.class) : mono.map(ServiceMessage::data); + isReturnTypeServiceMessage + ? mono.cast(Object.class) + : mono.mapNotNull(ServiceMessage::data); } private ServiceMessage throwIfError(ServiceMessage message) { diff --git a/services-api/src/main/java/io/scalecube/services/TypedParameters.java b/services-api/src/main/java/io/scalecube/services/TypedParameters.java new file mode 100644 index 000000000..68bba22e4 --- /dev/null +++ b/services-api/src/main/java/io/scalecube/services/TypedParameters.java @@ -0,0 +1,53 @@ +package io.scalecube.services; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +public class TypedParameters { + + private final Map params; + + public TypedParameters(Map params) { + this.params = new LinkedHashMap<>(params != null ? params : Map.of()); + } + + public Integer getInt(String name) { + return get(name, Integer::parseInt); + } + + public Long getLong(String name) { + return get(name, Long::parseLong); + } + + public BigInteger getBigInteger(String name) { + return get(name, BigInteger::new); + } + + public Double getDouble(String name) { + return get(name, Double::parseDouble); + } + + public BigDecimal getBigDecimal(String name) { + return get(name, BigDecimal::new); + } + + public > T getEnum(String name, Function enumFunc) { + return get(name, enumFunc); + } + + public Boolean getBoolean(String name) { + return get(name, Boolean::parseBoolean); + } + + public String getString(String name) { + return get(name, s -> s); + } + + public T get(String name, Function converter) { + final var s = params.get(name); + return s != null ? converter.apply(s) : null; + } +} diff --git a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java index 36c5417de..fec655c69 100644 --- a/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java +++ b/services-api/src/main/java/io/scalecube/services/api/DynamicQualifier.java @@ -25,7 +25,7 @@ public final class DynamicQualifier { private final String qualifier; private final Pattern pattern; - private final List pathVariables; + private final List pathParams; private final int size; private DynamicQualifier(String qualifier) { @@ -34,9 +34,9 @@ private DynamicQualifier(String qualifier) { for (var s : qualifier.split("/")) { if (s.startsWith(":")) { - final var pathVar = s.substring(1); - builder.append("(?<").append(pathVar).append(">.+)"); - list.add(pathVar); + final var param = s.substring(1); + builder.append("(?<").append(param).append(">.+)"); + list.add(param); } else { builder.append(s); } @@ -46,7 +46,7 @@ private DynamicQualifier(String qualifier) { this.qualifier = qualifier; this.pattern = Pattern.compile(builder.toString()); - this.pathVariables = Collections.unmodifiableList(list); + this.pathParams = Collections.unmodifiableList(list); this.size = sizeOf(qualifier); } @@ -89,16 +89,16 @@ public Pattern pattern() { } /** - * Returns path variable names. + * Returns path parameter names. * - * @return path variable names + * @return path parameter names */ - public List pathVariables() { - return pathVariables; + public List pathParams() { + return pathParams; } /** - * Size of qualifier. This is a number of {@code /} symbols. + * Size of qualifier. A number of {@code /} symbols. * * @return result */ @@ -107,37 +107,39 @@ public int size() { } /** - * Matches input qualifier against this dynamic qualifier. + * Matches given qualifier string with this {@link DynamicQualifier}. * - * @param qualifier qualifier - * @return matched path variables key-value map, or null if no matching occurred + * @param qualifier qualifier string + * @return matched path parameters key-value map, or null if no matching occurred */ public Map matchQualifier(String qualifier) { - if (size != sizeOf(qualifier)) { + final var path = qualifier.split("\\?")[0]; + + if (size != sizeOf(path)) { return null; } - final var matcher = pattern.matcher(qualifier); + final var matcher = pattern.matcher(path); if (!matcher.matches()) { return null; } final var map = new LinkedHashMap(); - for (var pathVar : pathVariables) { - final var value = matcher.group(pathVar); + for (var param : pathParams) { + final var value = matcher.group(param); if (value == null || value.isEmpty()) { - throw new IllegalArgumentException("Wrong path variable: " + pathVar); + throw new IllegalArgumentException("Wrong path param: " + param); } - map.put(pathVar, value); + map.put(param, value); } return map; } - private static int sizeOf(String value) { + private static int sizeOf(String path) { int count = 0; - for (int i = 0, length = value.length(); i < length; i++) { - if (value.charAt(i) == '/') { + for (int i = 0, length = path.length(); i < length; i++) { + if (path.charAt(i) == '/') { count++; } } @@ -165,7 +167,7 @@ public String toString() { return new StringJoiner(", ", DynamicQualifier.class.getSimpleName() + "[", "]") .add("qualifier='" + qualifier + "'") .add("pattern=" + pattern) - .add("pathVariables=" + pathVariables) + .add("pathParams=" + pathParams) .add("size=" + size) .toString(); } diff --git a/services-api/src/main/java/io/scalecube/services/methods/ServiceMethodInvoker.java b/services-api/src/main/java/io/scalecube/services/methods/ServiceMethodInvoker.java index d9bbcdc14..1f56e2abf 100644 --- a/services-api/src/main/java/io/scalecube/services/methods/ServiceMethodInvoker.java +++ b/services-api/src/main/java/io/scalecube/services/methods/ServiceMethodInvoker.java @@ -255,15 +255,15 @@ private Context enhanceRequestContext( RequestContext context, Object request, Principal principal) { final var dynamicQualifier = methodInfo.dynamicQualifier(); - Map pathVars = null; + Map pathParams = null; if (dynamicQualifier != null) { - pathVars = dynamicQualifier.matchQualifier(context.requestQualifier()); + pathParams = dynamicQualifier.matchQualifier(context.requestQualifier()); } return new RequestContext(context) .request(request) .principal(principal) - .pathVars(pathVars) + .pathParams(pathParams) .methodInfo(methodInfo); } diff --git a/services-api/src/main/java/io/scalecube/services/routing/StaticAddressRouter.java b/services-api/src/main/java/io/scalecube/services/routing/StaticAddressRouter.java index 769aed4ed..d2425f6a5 100644 --- a/services-api/src/main/java/io/scalecube/services/routing/StaticAddressRouter.java +++ b/services-api/src/main/java/io/scalecube/services/routing/StaticAddressRouter.java @@ -81,7 +81,7 @@ public Builder address(Address address) { /** * Setter for whether to apply behavior of {@link CredentialsSupplier}, or not. If it is known * upfront that destination service is secured, then set this flag to {@code true}, in such case - * {@link CredentialsSupplier#credentials(String)} will be invoked. + * {@link CredentialsSupplier#credentials(String, String)}} will be invoked. * * @param secured secured flag * @return this @@ -93,7 +93,7 @@ public Builder secured(boolean secured) { /** * Setter for {@code serviceRole} property, will be used in the invocation of {@link - * CredentialsSupplier#credentials(String)}. + * CredentialsSupplier#credentials(String, String)}. * * @param serviceRole serviceRole * @return this @@ -105,7 +105,7 @@ public Builder serviceRole(String serviceRole) { /** * Setter for {@code serviceName} property, will be used in the invocation of {@link - * CredentialsSupplier#credentials(String)}. + * CredentialsSupplier#credentials(String, String)}. * * @param serviceName serviceName * @return this diff --git a/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java b/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java index 6a8bee1c3..5b9eb1bba 100644 --- a/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java +++ b/services-api/src/test/java/io/scalecube/services/api/DynamicQualifierTest.java @@ -82,7 +82,7 @@ void testEquality() { } @Test - void testMatchSinglePathVariable() { + void testMatchSinglePathParam() { final var userName = UUID.randomUUID().toString(); final var qualifier = DynamicQualifier.from("v1/this.is.namespace/foo/bar/:userName"); final var map = qualifier.matchQualifier("v1/this.is.namespace/foo/bar/" + userName); @@ -92,7 +92,7 @@ void testMatchSinglePathVariable() { } @Test - void testMatchMultiplePathVariables() { + void testMatchMultiplePathParams() { final var qualifier = DynamicQualifier.from("v1/this.is.namespace/foo/:foo/bar/:bar/baz/:baz"); final var map = qualifier.matchQualifier("v1/this.is.namespace/foo/123/bar/456/baz/678"); assertNotNull(map); @@ -101,4 +101,25 @@ void testMatchMultiplePathVariables() { assertEquals("456", map.get("bar")); assertEquals("678", map.get("baz")); } + + @Test + void testMatchWithQueryString() { + final var qualifier = DynamicQualifier.from("v1/this.is.namespace/foo/:foo/bar/:bar"); + final var map = qualifier.matchQualifier("v1/this.is.namespace/foo/123/bar/456?x=1&y=2"); + + assertNotNull(map); + assertEquals("123", map.get("foo")); + assertEquals("456", map.get("bar")); + } + + @Test + void testMatchMultipleWithQueryStringIgnored() { + final var qualifier = DynamicQualifier.from("v1/this.is.namespace/foo/:foo/bar/:bar"); + + final var map = qualifier.matchQualifier("v1/this.is.namespace/foo/123/bar/456?debug=true"); + + assertNotNull(map); + assertEquals("123", map.get("foo")); + assertEquals("456", map.get("bar")); + } } diff --git a/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java b/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java index 658911f08..bc99c9f4c 100644 --- a/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java +++ b/services-api/src/test/java/io/scalecube/services/methods/StubServiceImpl.java @@ -51,9 +51,10 @@ public Mono invokeDynamicQualifier() { context -> { assertNotNull(context.headers(), "headers"); assertNotNull(context.principal(), "principal"); - assertNotNull(context.pathVars(), "pathVars"); - assertNotNull(context.pathVar("foo"), "pathVar[foo]"); - assertNotNull(context.pathVar("bar"), "pathVar[bar]"); + final var pathParams = context.pathParams(); + assertNotNull(pathParams, "pathParams"); + assertNotNull(pathParams.getString("foo"), "pathParam[foo]"); + assertNotNull(pathParams.getString("bar"), "pathParam[bar]"); }) .then(); } diff --git a/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java b/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java index a78f0fc4d..6b89b5f1a 100644 --- a/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java +++ b/services-examples/src/main/java/io/scalecube/services/examples/GreetingServiceImpl.java @@ -107,6 +107,6 @@ public Mono emptyGreetingMessage(ServiceMessage request) { @Override public Mono helloDynamicQualifier(Long value) { return RequestContext.deferContextual() - .map(context -> context.pathVar("someVar") + "@" + value); + .map(context -> context.pathParams().getString("someVar") + "@" + value); } } diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java index f128cd556..6db709dc3 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java @@ -1,10 +1,17 @@ package io.scalecube.services.gateway.client.http; +import static io.netty.handler.codec.http.HttpMethod.DELETE; +import static io.netty.handler.codec.http.HttpMethod.GET; +import static io.netty.handler.codec.http.HttpMethod.HEAD; +import static io.netty.handler.codec.http.HttpMethod.OPTIONS; +import static io.netty.handler.codec.http.HttpMethod.TRACE; import static io.scalecube.services.gateway.client.ServiceMessageCodec.decodeData; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelOption; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.scalecube.services.Address; import io.scalecube.services.ServiceReference; @@ -14,14 +21,17 @@ import io.scalecube.services.transport.api.ClientTransport; import io.scalecube.services.transport.api.DataCodec; import java.lang.reflect.Type; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.UnaryOperator; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.NettyOutbound; @@ -33,12 +43,11 @@ public final class HttpGatewayClientTransport implements ClientChannel, ClientTransport { - private static final Logger LOGGER = LoggerFactory.getLogger(HttpGatewayClientTransport.class); - private static final String CONTENT_TYPE = "application/json"; private static final HttpGatewayClientCodec CLIENT_CODEC = new HttpGatewayClientCodec(DataCodec.getInstance(CONTENT_TYPE)); private static final int CONNECT_TIMEOUT_MILLIS = (int) Duration.ofSeconds(5).toMillis(); + private static final Set BODYLESS_METHODS = Set.of(GET, HEAD, DELETE, OPTIONS, TRACE); private final GatewayClientCodec clientCodec; private final LoopResources loopResources; @@ -80,28 +89,46 @@ public ClientChannel create(ServiceReference serviceReference) { } @Override - public Mono requestResponse(ServiceMessage request, Type responseType) { + public Mono requestResponse(ServiceMessage message, Type responseType) { return Mono.defer( () -> { - final HttpClient httpClient = httpClientReference.get(); + final var httpClient = httpClientReference.get(); + final var method = message.headers().getOrDefault("http.method", "POST"); + final var queryParams = headersByPrefix(message.headers(), "http.query"); + return httpClient - .post() - .uri("/" + request.qualifier()) - .send((clientRequest, outbound) -> send(request, clientRequest, outbound)) + .request(HttpMethod.valueOf(method)) + .uri(applyQueryParams("/" + message.qualifier(), queryParams)) + .send((request, outbound) -> send(message, request, outbound)) .responseSingle( (clientResponse, mono) -> - mono.map(ByteBuf::retain).map(data -> toMessage(clientResponse, data))) + mono.defaultIfEmpty(Unpooled.EMPTY_BUFFER) + .map(ByteBuf::retain) + .map(data -> toMessage(clientResponse, data))) .map(msg -> decodeData(msg, responseType)); }); } private Mono send( - ServiceMessage request, HttpClientRequest clientRequest, NettyOutbound outbound) { - LOGGER.debug("Sending request: {}", request); - // prepare request headers - request.headers().forEach(clientRequest::header); - // send with publisher (defer buffer cleanup to netty) - return outbound.sendObject(Mono.just(clientCodec.encode(request))).then(); + ServiceMessage message, HttpClientRequest request, NettyOutbound outbound) { + // Extract custom headers + final var messageHeaders = message.headers(); + final var httpHeaders = headersByPrefix(messageHeaders, "http.header"); + + // Apply HTTP headers first + httpHeaders.forEach(request::header); + + // Apply remaining message headers (skip http.*) + messageHeaders.entrySet().stream() + .filter(e -> !e.getKey().startsWith("http.")) + .forEach(e -> request.header(e.getKey(), e.getValue())); + + if (BODYLESS_METHODS.contains(request.method())) { + return outbound.then(); + } + + // Send with publisher (defer buffer cleanup to netty) + return outbound.sendObject(Mono.just(clientCodec.encode(message))).then(); } @Override @@ -116,29 +143,62 @@ public Flux requestChannel( } private static ServiceMessage toMessage(HttpClientResponse httpResponse, ByteBuf data) { - ServiceMessage.Builder builder = - ServiceMessage.builder().qualifier(httpResponse.uri()).data(data); + final var builder = + ServiceMessage.builder() + .qualifier(httpResponse.uri()) + .data(data != Unpooled.EMPTY_BUFFER ? data : null); - HttpResponseStatus status = httpResponse.status(); + final var status = httpResponse.status(); if (isError(status)) { builder.header(ServiceMessage.HEADER_ERROR_TYPE, status.code()); } - // prepare response headers + // Populate HTTP response headers httpResponse .responseHeaders() - .entries() .forEach(entry -> builder.header(entry.getKey(), entry.getValue())); - ServiceMessage message = builder.build(); - LOGGER.debug("Received response: {}", message); - return message; + return builder.build(); } private static boolean isError(HttpResponseStatus status) { return status.code() >= 400 && status.code() <= 599; } + private static String applyQueryParams(String uri, Map queryParams) { + if (queryParams != null && !queryParams.isEmpty()) { + final var queryString = + queryParams.entrySet().stream() + .map( + e -> { + final var key = e.getKey(); + final var value = e.getValue(); + final var charset = StandardCharsets.UTF_8; + return URLEncoder.encode(key, charset) + + "=" + + URLEncoder.encode(value, charset); + }) + .collect(Collectors.joining("&")); + uri += "?" + queryString; + } + return uri; + } + + private static Map headersByPrefix(Map headers, String prefix) { + if (headers == null || headers.isEmpty()) { + return Map.of(); + } + final var finalPrefix = prefix + "."; + final var result = new HashMap(); + headers.forEach( + (k, v) -> { + if (k.startsWith(finalPrefix)) { + result.put(k.substring(finalPrefix.length()), v); + } + }); + return result; + } + @Override public void close() { if (ownsLoopResources) { diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java index d243cc8b9..f146cfaa1 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java @@ -30,11 +30,15 @@ import io.scalecube.services.routing.StaticAddressRouter; import io.scalecube.services.transport.api.DataCodec; import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -214,17 +218,24 @@ private static ServiceMessage toMessage( HttpServerRequest httpRequest, Consumer consumer) { final var builder = ServiceMessage.builder(); - // Copy http headers to service message + // Copy HTTP headers to service message for (var httpHeader : httpRequest.requestHeaders()) { - builder.header(httpHeader.getKey(), httpHeader.getValue()); + builder.header("http.header." + httpHeader.getKey(), httpHeader.getValue()); } - // Add http method to service message (used by REST services) + // Copy HTTP query params to service message + + final var queryParams = matchQueryParams(httpRequest.uri()); + queryParams.forEach((param, value) -> builder.header("http.query." + param, value)); + + // Add HTTP method to service message (used by REST services) builder + .header("http.method", httpRequest.method().name()) .header(HEADER_REQUEST_METHOD, httpRequest.method().name()) .qualifier(httpRequest.uri().substring(1)); + if (consumer != null) { consumer.accept(builder); } @@ -232,6 +243,20 @@ private static ServiceMessage toMessage( return builder.build(); } + private static Map matchQueryParams(String uri) { + final var index = uri.indexOf('?'); + if (index < 0 || index == uri.length() - 1) { + return Collections.emptyMap(); // no query params + } + return Arrays.stream(uri.substring(index + 1).split("&")) + .map(s -> s.split("=", 2)) + .filter(parts -> parts.length == 2) + .collect( + Collectors.toMap( + parts -> URLDecoder.decode(parts[0], StandardCharsets.UTF_8), + parts -> URLDecoder.decode(parts[1], StandardCharsets.UTF_8))); + } + private static Mono emptyMessage(ServiceMessage message) { return Mono.just(ServiceMessage.builder().qualifier(message.qualifier()).build()); } @@ -255,7 +280,8 @@ private static Mono error(HttpServerResponse httpResponse, ServiceMessage ? encodeData(response.data(), response.dataFormatOrDefault()) : ((ByteBuf) response.data()); - // send with publisher (defer buffer cleanup to netty) + // Send with publisher (defer buffer cleanup to netty) + return httpResponse.status(status).send(Mono.just(content)).then(); } @@ -269,7 +295,8 @@ private static Mono ok(HttpServerResponse httpResponse, ServiceMessage res ? ((ByteBuf) response.data()) : encodeData(response.data(), response.dataFormatOrDefault()); - // send with publisher (defer buffer cleanup to netty) + // Send with publisher (defer buffer cleanup to netty) + return httpResponse.status(OK).send(Mono.just(content)).then(); } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java index 92ae3fba0..932c9c25d 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/files/ReportServiceImpl.java @@ -122,7 +122,8 @@ public Flux successfulDownload() { return RequestContext.deferContextual() .flatMapMany( context -> { - final var fileSize = context.pathVar("fileSize", Long.class); + final var pathParams = context.pathParams(); + final var fileSize = pathParams.getLong("fileSize"); final var headers = context.headers(); final File file; try { diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java index 6b0d3f9c6..d7ab4abdd 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestGatewayTest.java @@ -1,50 +1,46 @@ package io.scalecube.services.gateway.rest; +import static io.scalecube.services.api.ServiceMessage.HEADER_ERROR_TYPE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.collection.IsMapContaining.hasKey; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import io.netty.handler.codec.http.HttpResponseStatus; +import io.scalecube.services.Address; import io.scalecube.services.Microservices; import io.scalecube.services.Microservices.Context; import io.scalecube.services.ServiceCall; import io.scalecube.services.ServiceInfo; -import io.scalecube.services.api.ErrorData; +import io.scalecube.services.api.ServiceMessage; import io.scalecube.services.discovery.ScalecubeServiceDiscovery; import io.scalecube.services.examples.GreetingServiceImpl; import io.scalecube.services.exceptions.ServiceUnavailableException; +import io.scalecube.services.gateway.client.http.HttpGatewayClientTransport; import io.scalecube.services.gateway.client.websocket.WebsocketGatewayClientTransport; import io.scalecube.services.gateway.http.HttpGateway; import io.scalecube.services.gateway.websocket.WebsocketGateway; import io.scalecube.services.routing.StaticAddressRouter; import io.scalecube.services.transport.rsocket.RSocketServiceTransport; import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpRequest.BodyPublishers; -import java.net.http.HttpResponse.BodyHandlers; import java.time.Duration; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import reactor.test.StepVerifier; public class RestGatewayTest { private static Microservices gateway; private static Microservices microservices; - private static HttpClient httpClient; - private final ObjectMapper objectMapper = objectMapper(); - private static String httpGatewayAddress; + private static Address gatewayAddress; + private static StaticAddressRouter router; @BeforeAll static void beforeAll() { @@ -61,7 +57,8 @@ static void beforeAll() { .gateway(() -> HttpGateway.builder().id("HTTP").build()) .gateway(() -> WebsocketGateway.builder().id("WS").build())); - httpGatewayAddress = "http://localhost:" + gateway.gateway("HTTP").address().port(); + gatewayAddress = Address.from("localhost:" + gateway.gateway("HTTP").address().port()); + router = StaticAddressRouter.forService(gatewayAddress, "app-service").build(); microservices = Microservices.start( @@ -78,8 +75,6 @@ static void beforeAll() { .services(new GreetingServiceImpl()) .services(ServiceInfo.fromServiceInstance(new RestServiceImpl()).build()) .services(ServiceInfo.fromServiceInstance(new RoutingServiceImpl()).build())); - - httpClient = HttpClient.newHttpClient(); } @AfterAll @@ -92,207 +87,301 @@ static void afterAll() { } } - private static ObjectMapper objectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); - mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); - mapper.registerModule(new JavaTimeModule()); - return mapper; - } - @Nested + @TestInstance(Lifecycle.PER_CLASS) class GatewayTests { + private ServiceCall serviceCall; + + @BeforeAll + void beforeAll() { + serviceCall = + new ServiceCall() + .transport(HttpGatewayClientTransport.builder().address(gatewayAddress).build()) + .router(router); + } + + @AfterAll + void afterAll() { + if (serviceCall != null) { + serviceCall.close(); + } + } + @Test - void testOptions() throws Exception { - final var fooParam = "options" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder( - new URI(httpGatewayAddress + "/v1/restService/options/" + fooParam)) - .method("OPTIONS", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - fooParam, - objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), - "response"); + void testOptions() { + final var param = "options" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "OPTIONS") + .qualifier("v1/restService/options/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testGet() throws Exception { - final var fooParam = "get" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/get/" + fooParam)) - .method("GET", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - fooParam, - objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), - "response"); + void testGet() { + final var param = "get" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "GET") + .qualifier("v1/restService/get/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testHead() throws Exception { - final var fooParam = "head" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/head/" + fooParam)) - .method("HEAD", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals("", httpResponse.body(), "response"); + void testHead() { + final var param = "head123456"; + final var customHeader1 = "customHeader-" + System.currentTimeMillis(); + final var customHeader2 = "customHeader-" + System.currentTimeMillis(); + final var queryParam1 = "queryParam-" + System.currentTimeMillis(); + final var queryParam2 = "queryParam-" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "HEAD") + .header("http.header.X-Custom-Header-1", customHeader1) + .header("http.header.X-Custom-Header-2", customHeader2) + .header("http.query.param1", queryParam1) + .header("http.query.param2", queryParam2) + .qualifier("v1/restService/head/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + assertNull(message.data(), "data"); // this is HEAD + assertNotNull(message.headers(), "headers"); + assertThat(message.headers(), not(hasKey(HEADER_ERROR_TYPE))); + }) + .verifyComplete(); } @Test - void testPost() throws Exception { + void testPost() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "post" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/post/" + fooParam)) - .method( - "POST", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "post" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "POST") + .qualifier("v1/restService/post/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(name, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testPut() throws Exception { + void testPut() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "put" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/put/" + fooParam)) - .method( - "PUT", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "put" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "PUT") + .qualifier("v1/restService/put/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(name, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testPatch() throws Exception { + void testPatch() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "patch" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/patch/" + fooParam)) - .method( - "PATCH", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "patch" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "PATCH") + .qualifier("v1/restService/patch/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(name, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testDelete() throws Exception { + void testDelete() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "delete" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/delete/" + fooParam)) - .method( - "DELETE", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "delete" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "DELETE") + .qualifier("v1/restService/delete/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testTrace() throws Exception { - final var fooParam = "trace" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder(new URI(httpGatewayAddress + "/v1/restService/trace/" + fooParam)) - .method("TRACE", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - fooParam, - objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), - "response"); + void testTrace() { + final var param = "trace" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "TRACE") + .qualifier("v1/restService/trace/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); + } + + @Test + void testAttributesPropagation() { + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "GET") + .header("http.header.X-String-Header", "abc") + .header("http.header.X-Int-Header", "123456789") + .header("http.query.debug", "true") + .header("http.query.x", "1") + .header("http.query.y", "2") + .qualifier("v1/restService/propagate/123/bar456/baz789") + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertNotNull(someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } } @Nested + @TestInstance(Lifecycle.PER_CLASS) class RoutingTests { + private ServiceCall serviceCall; + + @BeforeAll + void beforeAll() { + serviceCall = + new ServiceCall() + .transport(HttpGatewayClientTransport.builder().address(gatewayAddress).build()) + .router(router); + } + + @AfterAll + void afterAll() { + if (serviceCall != null) { + serviceCall.close(); + } + } + @Test - void testMatchByGetMethod() throws Exception { - final var fooParam = "get" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder( - new URI(httpGatewayAddress + "/v1/routingService/find/" + fooParam)) - .method("GET", BodyPublishers.noBody()) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - fooParam, - objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), - "response"); + void testMatchByGetMethod() { + final var param = "get" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "GET") + .qualifier("v1/routingService/find/" + param) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(param, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testMatchByPostMethod() throws Exception { + void testMatchByPostMethod() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "post" + System.currentTimeMillis(); - final var httpRequest = - HttpRequest.newBuilder( - new URI(httpGatewayAddress + "/v1/routingService/update/" + fooParam)) - .method( - "POST", - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals(HttpResponseStatus.OK.code(), httpResponse.statusCode(), "statusCode"); - assertEquals( - name, objectMapper.readValue(httpResponse.body(), SomeResponse.class).name(), "response"); + final var param = "post" + System.currentTimeMillis(); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", "POST") + .qualifier("v1/routingService/update/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .assertNext( + message -> { + final var someResponse = message.data(); + assertNotNull(someResponse, "data"); + assertEquals(name, someResponse.name(), "someResponse.name"); + }) + .verifyComplete(); } @Test - void testNoMatchByRestMethod() throws Exception { + void testNoMatchByRestMethod() { final var name = "name" + System.currentTimeMillis(); - final var fooParam = "post" + System.currentTimeMillis(); + final var param = "post" + System.currentTimeMillis(); final var nonMatchedRestMethod = "PUT"; - final var httpRequest = - HttpRequest.newBuilder( - new URI(httpGatewayAddress + "/v1/routingService/update/" + fooParam)) - .method( - nonMatchedRestMethod, - BodyPublishers.ofString( - objectMapper.writeValueAsString(new SomeRequest().name(name)))) - .build(); - final var httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); - assertEquals( - HttpResponseStatus.SERVICE_UNAVAILABLE.code(), httpResponse.statusCode(), "statusCode"); - assertTrue( - objectMapper - .readValue(httpResponse.body(), ErrorData.class) - .getErrorMessage() - .startsWith("No reachable member with such service")); + StepVerifier.create( + serviceCall.requestOne( + ServiceMessage.builder() + .header("http.method", nonMatchedRestMethod) + .qualifier("v1/routingService/update/" + param) + .data(new SomeRequest().name(name)) + .build(), + SomeResponse.class)) + .verifyErrorSatisfies( + ex -> { + final var exception = (ServiceUnavailableException) ex; + assertEquals(503, exception.errorCode(), "errorCode"); + assertThat( + exception.getMessage(), + Matchers.startsWith("No reachable member with such service")); + }); } @Test @@ -308,9 +397,10 @@ void testNoMatchWithoutRestMethod() { .expectErrorSatisfies( ex -> { final var exception = (ServiceUnavailableException) ex; - final var errorMessage = exception.getMessage(); assertEquals(503, exception.errorCode()); - assertTrue(errorMessage.startsWith("No reachable member with such service")); + assertThat( + exception.getMessage(), + Matchers.startsWith("No reachable member with such service")); }) .verify(Duration.ofSeconds(3)); } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java index 73bdde976..56f174320 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestService.java @@ -1,5 +1,6 @@ package io.scalecube.services.gateway.rest; +import io.scalecube.services.annotations.RestMethod; import io.scalecube.services.annotations.Service; import io.scalecube.services.annotations.ServiceMethod; import reactor.core.publisher.Mono; @@ -26,8 +27,12 @@ public interface RestService { Mono patch(SomeRequest request); @ServiceMethod("delete/:foo") - Mono delete(SomeRequest request); + Mono delete(); @ServiceMethod("trace/:foo") Mono trace(); + + @RestMethod("GET") + @ServiceMethod("propagate/:foo/:bar/:baz") + Mono propagateRequestAttributes(); } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java index c8eab3e46..3838d5a73 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RestServiceImpl.java @@ -1,10 +1,14 @@ package io.scalecube.services.gateway.rest; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasKey; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import io.scalecube.services.RequestContext; +import java.util.UUID; import reactor.core.publisher.Mono; public class RestServiceImpl implements RestService { @@ -14,10 +18,11 @@ public Mono options() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("OPTIONS", context.requestMethod()); return new SomeResponse().name(foo); }); @@ -28,10 +33,11 @@ public Mono get() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("GET", context.requestMethod()); return new SomeResponse().name(foo); }); @@ -42,11 +48,23 @@ public Mono head() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); - assertNotNull(foo); - assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); + assertEquals("head123456", foo, "pathParam"); + final var headers = context.headers(); + assertNotNull(headers); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("HEAD", context.requestMethod()); + + assertThat( + headers, + allOf( + hasKey("http.method"), + hasKey("http.header.X-Custom-Header-1"), + hasKey("http.header.X-Custom-Header-2"), + hasKey("http.query.param1"), + hasKey("http.query.param2"))); + return new SomeResponse().name(foo); }); } @@ -56,9 +74,11 @@ public Mono post(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); + assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("POST", context.requestMethod()); return new SomeResponse().name(request.name()); }); @@ -69,9 +89,11 @@ public Mono put(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); + assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("PUT", context.requestMethod()); return new SomeResponse().name(request.name()); }); @@ -82,24 +104,28 @@ public Mono patch(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); + assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("PATCH", context.requestMethod()); return new SomeResponse().name(request.name()); }); } @Override - public Mono delete(SomeRequest request) { + public Mono delete() { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); + assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("DELETE", context.requestMethod()); - return new SomeResponse().name(request.name()); + return new SomeResponse().name(foo); }); } @@ -108,12 +134,44 @@ public Mono trace() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); - assertTrue(context.headers().size() > 0); + assertThat(context.headers().size(), greaterThan(0)); assertEquals("TRACE", context.requestMethod()); return new SomeResponse().name(foo); }); } + + @Override + public Mono propagateRequestAttributes() { + return RequestContext.deferContextual() + .map( + context -> { + final var pathParams = context.pathParams(); + assertEquals(123, pathParams.getInt("foo"), "foo"); + assertEquals("bar456", pathParams.getString("bar"), "bar"); + assertEquals("baz789", pathParams.getString("baz"), "baz"); + + final var headers = context.headers(); + assertEquals("GET", headers.get("http.method")); + assertEquals("abc", headers.get("http.header.X-String-Header")); + assertEquals("123456789", headers.get("http.header.X-Int-Header")); + assertEquals("true", headers.get("http.query.debug")); + assertEquals("1", headers.get("http.query.x")); + assertEquals("2", headers.get("http.query.y")); + + final var httpHeaders = context.headerParams("http.header"); + assertEquals("abc", httpHeaders.getString("X-String-Header")); + assertEquals(123456789, httpHeaders.getInt("X-Int-Header")); + + final var queryParams = context.headerParams("http.query"); + assertEquals(true, queryParams.getBoolean("debug")); + assertEquals(1, queryParams.getInt("x")); + assertEquals(2, queryParams.getInt("y")); + + return new SomeResponse().name(UUID.randomUUID().toString()); + }); + } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java index ea20ef61f..9c8950dc9 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/rest/RoutingServiceImpl.java @@ -14,7 +14,8 @@ public Mono find() { return RequestContext.deferContextual() .map( context -> { - final var foo = context.pathVar("foo"); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); assertNotNull(foo); assertNotNull(context.headers()); assertTrue(context.headers().size() > 0); @@ -28,7 +29,9 @@ public Mono update(SomeRequest request) { return RequestContext.deferContextual() .map( context -> { - assertNotNull(context.pathVar("foo")); + final var pathParams = context.pathParams(); + final var foo = pathParams.getString("foo"); + assertNotNull(foo); assertNotNull(context.headers()); assertTrue(context.headers().size() > 0); assertEquals("POST", context.requestMethod()); diff --git a/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java b/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java index e769bfe5f..9d6606a10 100644 --- a/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java +++ b/services/src/main/java/io/scalecube/services/files/FileServiceImpl.java @@ -96,7 +96,8 @@ public Flux streamFile() { .flatMapMany( context -> { final var headers = context.headers(); - final var filename = context.pathVar("filename"); + final var pathParams = context.pathParams(); + final var filename = pathParams.getString("filename"); final var path = baseDir.resolve(filename); if (!isPathValid(path)) { diff --git a/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java b/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java index 89aaf3127..3ed0c3b12 100644 --- a/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java +++ b/services/src/test/java/io/scalecube/services/PlaceholderQualifierTest.java @@ -86,7 +86,7 @@ void shouldRouteByPlaceholderQualifier() { } @Test - void shouldRouteByPlaceholderQualifierWithPathVar() { + void shouldRouteByPlaceholderQualifierWithPathParam() { final var foo1Id = providerFoo1.id(); final var name1 = "name1"; final String foo1Result = @@ -150,7 +150,7 @@ public interface FooService { Mono hello(); @ServiceMethod("hello/${microservices:id}/:name") - Mono helloWithPathVar(); + Mono helloWithPathParam(); } public static class FooServiceImpl implements FooService { @@ -168,8 +168,9 @@ public Mono hello() { } @Override - public Mono helloWithPathVar() { - return RequestContext.deferContextual().map(context -> id + "|" + context.pathVar("name")); + public Mono helloWithPathParam() { + return RequestContext.deferContextual() + .map(context -> id + "|" + context.pathParams().getString("name")); } } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/ServiceRegistrationTest.java b/services/src/test/java/io/scalecube/services/ServiceRegistrationTest.java similarity index 94% rename from services-gateway/src/test/java/io/scalecube/services/gateway/rest/ServiceRegistrationTest.java rename to services/src/test/java/io/scalecube/services/ServiceRegistrationTest.java index 379e99ce4..bbf4ec850 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/rest/ServiceRegistrationTest.java +++ b/services/src/test/java/io/scalecube/services/ServiceRegistrationTest.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.rest; +package io.scalecube.services; import static io.scalecube.services.api.ServiceMessage.HEADER_REQUEST_METHOD; import static org.hamcrest.MatcherAssert.assertThat; @@ -8,7 +8,6 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; -import io.scalecube.services.Microservices; import io.scalecube.services.Microservices.Context; import io.scalecube.services.annotations.RestMethod; import io.scalecube.services.annotations.Service; @@ -117,7 +116,7 @@ void registerMultipleValidRestServices() { interface EchoService { @ServiceMethod("get/:foo") - Mono echo(); + Mono echo(); } @Service("v1/service") @@ -125,11 +124,11 @@ interface BadRestService { @RestMethod("GET") @ServiceMethod("get/:foo") - Mono echo(); + Mono echo(); @RestMethod("GET") @ServiceMethod("get/:foo") - Mono ping(); + Mono ping(); } @Service("v1/service") @@ -137,11 +136,11 @@ interface GoodRestService { @RestMethod("GET") @ServiceMethod("echo/:foo") - Mono echo(); + Mono echo(); @RestMethod("POST") @ServiceMethod("echo/:foo") - Mono ping(); + Mono ping(); } @Service("v1/service") @@ -149,7 +148,7 @@ interface CreateRestService { @RestMethod("POST") @ServiceMethod("account/:foo") - Mono account(); + Mono account(); } @Service("v1/service") @@ -157,6 +156,6 @@ interface UpdateRestService { @RestMethod("PUT") @ServiceMethod("account/:foo") - Mono account(); + Mono account(); } } diff --git a/services/src/test/java/io/scalecube/services/registry/ServiceRegistryImplTest.java b/services/src/test/java/io/scalecube/services/registry/ServiceRegistryImplTest.java index d4200d701..2e1c466a9 100644 --- a/services/src/test/java/io/scalecube/services/registry/ServiceRegistryImplTest.java +++ b/services/src/test/java/io/scalecube/services/registry/ServiceRegistryImplTest.java @@ -78,7 +78,7 @@ void testRegisterThenUnregisterServiceEndpoint() { new HashMap<>(), List.of( ServiceMethodDefinition.fromAction("hello"), - ServiceMethodDefinition.fromAction("hello/:pathVar"))))) + ServiceMethodDefinition.fromAction("hello/:pathParam"))))) .build()); } @@ -136,7 +136,7 @@ void testLookupService() { new HashMap<>(), List.of( ServiceMethodDefinition.fromAction("hello"), - ServiceMethodDefinition.fromAction("hello/:pathVar"))))) + ServiceMethodDefinition.fromAction("hello/:pathParam"))))) .build()); } assertEquals( @@ -194,8 +194,8 @@ interface HelloTwo { String NAMESPACE = "greeting"; - @ServiceMethod("hello/:pathVar") - default Mono helloPathVar() { + @ServiceMethod("hello/:pathParam") + default Mono helloPathParam() { return Mono.just("" + System.currentTimeMillis()); } } @@ -210,14 +210,14 @@ interface RestServiceOne { String NAMESPACE = "v1/api"; @RestMethod("POST") - @ServiceMethod("foo/:pathVar") - default Mono updateWithPathVar() { + @ServiceMethod("foo/:pathParam") + default Mono updateWithPathParam() { return Mono.just("" + System.currentTimeMillis()); } @RestMethod("POST") @ServiceMethod("foo/update") - default Mono updateWithoutPathVar() { + default Mono updateWithoutPathParam() { return Mono.just("" + System.currentTimeMillis()); } } @@ -228,14 +228,14 @@ interface RestServiceTwo { String NAMESPACE = "v1/api"; @RestMethod("PUT") - @ServiceMethod("foo/:pathVar") - default Mono updateWithPathVar() { + @ServiceMethod("foo/:pathParam") + default Mono updateWithPathParam() { return Mono.just("" + System.currentTimeMillis()); } @RestMethod("PUT") @ServiceMethod("foo/update") - default Mono updateWithoutPathVar() { + default Mono updateWithoutPathParam() { return Mono.just("" + System.currentTimeMillis()); } } diff --git a/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java b/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java index fd6dc6ecb..1765c5ec4 100644 --- a/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java +++ b/services/src/test/java/io/scalecube/services/sut/GreetingServiceImpl.java @@ -205,6 +205,6 @@ public Flux manyStream(Long cnt) { @Override public Mono helloDynamicQualifier(Long value) { return RequestContext.deferContextual() - .map(context -> context.pathVar("someVar") + "@" + value); + .map(context -> context.pathParams().getString("someVar") + "@" + value); } }