Skip to content

Commit

Permalink
KOGITO-8410 Added support to GET method to Knative custom function
Browse files Browse the repository at this point in the history
Signed-off-by: Helber Belmiro <[email protected]>
  • Loading branch information
hbelmiro committed Aug 11, 2023
1 parent db2e6fc commit dad6ef0
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.jbpm.ruleflow.core.factory.NodeFactory;
import org.jbpm.ruleflow.core.factory.WorkItemNodeFactory;
import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.CloudEventKnativeParamsDecorator;
import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.GetRequestKnativeParamsDecorator;
import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler;
import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation;
import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.PlainJsonKnativeParamsDecorator;
Expand All @@ -32,6 +33,7 @@
import org.kie.kogito.serverless.workflow.parser.types.WorkItemTypeHandler;
import org.kie.kogito.serverless.workflow.suppliers.ParamsRestBodyBuilderSupplier;
import org.kogito.workitem.rest.RestWorkItemHandler;
import org.kogito.workitem.rest.decorators.ParamsDecorator;

import com.github.javaparser.ast.expr.Expression;

Expand Down Expand Up @@ -79,24 +81,29 @@ public class KnativeTypeHandler extends WorkItemTypeHandler {

Operation operation = Operation.parse(trimCustomOperation(functionDef));

if (operation.isCloudEvent()) {
node.workParameter(RestWorkItemHandler.PARAMS_DECORATOR, CloudEventKnativeParamsDecorator.class.getName());
} else {
node.workParameter(RestWorkItemHandler.PARAMS_DECORATOR, PlainJsonKnativeParamsDecorator.class.getName());
}

Supplier<Expression> requestTimeout = runtimeRestApi(functionDef, "timeout",
context.getContext(), String.class, DEFAULT_REQUEST_TIMEOUT_VALUE);

return node.workParameter(KnativeWorkItemHandler.SERVICE_PROPERTY_NAME, operation.getService())
.workParameter(KnativeWorkItemHandler.PATH_PROPERTY_NAME, operation.getPath())
.workParameter(RestWorkItemHandler.BODY_BUILDER, new ParamsRestBodyBuilderSupplier())
.workParameter(RestWorkItemHandler.METHOD, HttpMethod.POST)
.workParameter(RestWorkItemHandler.PARAMS_DECORATOR, getParamsDecorator(operation).getName())
.workParameter(RestWorkItemHandler.METHOD, operation.getHttpMethod())
.workParameter(RestWorkItemHandler.REQUEST_TIMEOUT_IN_MILLIS, requestTimeout)
.metaData(TaskDescriptor.KEY_WORKITEM_TYPE, RestWorkItemHandler.REST_TASK_TYPE)
.workName(KnativeWorkItemHandler.NAME);
}

private static Class<? extends ParamsDecorator> getParamsDecorator(Operation operation) {
if (operation.isCloudEvent()) {
return CloudEventKnativeParamsDecorator.class;
} else if (HttpMethod.GET.equals(operation.getHttpMethod())) {
return GetRequestKnativeParamsDecorator.class;
} else {
return PlainJsonKnativeParamsDecorator.class;
}
}

@Override
public String type() {
return KnativeWorkItemHandler.NAME;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"id": "getKnativeFunction",
"version": "1.0",
"name": "Test Knative function",
"description": "This workflow tests a Knative function",
"start": "invokeFunction",
"functions": [
{
"name": "greet",
"type": "custom",
"operation": "knative:services.v1.serving.knative.dev/default/serverless-workflow-greeting-quarkus?path=/plainJsonFunction&method=GET"
}
],
"states": [
{
"name": "invokeFunction",
"type": "operation",
"actions": [
{
"functionRef": {
"refName": "greet",
"arguments": {
"name": ".name"
}
}
}
],
"end": true
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
Expand Down Expand Up @@ -85,6 +87,22 @@ static void afterAll() {
}
}

@Test
void executeHttpGet() {
mockExecuteHttpGetEndpoint();

given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body("{\"name\": \"hbelmiro\" }").when()
.post("/getKnativeFunction")
.then()
.statusCode(HttpURLConnection.HTTP_CREATED)
.body("workflowdata.message", is("Hello"));

wireMockServer.verify(getRequestedFor(urlEqualTo("/plainJsonFunction?name=hbelmiro")));
}

@Test
void executeWithEmptyParameters() {
mockExecuteWithEmptyParametersEndpoint();
Expand Down Expand Up @@ -306,6 +324,15 @@ private void mockExecuteWithParametersEndpoint() {
.put("message", "Hello"))));
}

private void mockExecuteHttpGetEndpoint() {
wireMockServer.stubFor(get(urlEqualTo("/plainJsonFunction?name=hbelmiro"))
.willReturn(aResponse()
.withStatus(HttpURLConnection.HTTP_OK)
.withHeader("Content-Type", "application/json")
.withJsonBody(JsonNodeFactory.instance.objectNode()
.put("message", "Hello"))));
}

private void mockExecuteWithArrayEndpoint() {
wireMockServer.stubFor(post(urlEqualTo("/arrayFunction"))
.withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates.
*
* 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
*
* http://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 org.kie.kogito.addons.quarkus.knative.serving.customfunctions;

import java.text.MessageFormat;
import java.util.Map;

import org.kie.kogito.internal.process.runtime.KogitoWorkItem;
import org.kogito.workitem.rest.decorators.ParamsDecorator;

import io.vertx.mutiny.ext.web.client.HttpRequest;

public final class GetRequestKnativeParamsDecorator implements ParamsDecorator {

@Override
public void decorate(KogitoWorkItem workItem, Map<String, Object> parameters, HttpRequest<?> request) {
KnativeFunctionPayloadSupplier.getPayload(parameters).forEach((key, value) -> {
if (value instanceof String) {
request.addQueryParam(key, (String) value);
} else {
String message = "Knative functions support only GET requests with String attributes. {0} has a {1} value.";
throw new IllegalArgumentException(MessageFormat.format(message, key, value.getClass()));
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,51 @@
*/
package org.kie.kogito.addons.quarkus.knative.serving.customfunctions;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import io.vertx.core.http.HttpMethod;

public final class Operation {

private static final Set<HttpMethod> SUPPORTED_METHODS = Set.of(HttpMethod.POST, HttpMethod.GET);

private static final HttpMethod DEFAULT_HTTP_METHOD = HttpMethod.POST;

static final String CLOUD_EVENT_PARAMETER_NAME = "asCloudEvent";

static final String PATH_PARAMETER_NAME = "path";

static final String METHOD_PARAMETER_NAME = "method";

private final String service;

private final String path;

private final boolean isCloudEvent;

private final HttpMethod httpMethod;

private Operation(Builder builder) {
this.service = Objects.requireNonNull(builder.service);
this.path = builder.path != null ? builder.path : "/";
this.isCloudEvent = builder.isCloudEvent;
this.httpMethod = builder.httpMethod;
validate(this);
}

private static void validate(Operation operation) {
if (!SUPPORTED_METHODS.contains(operation.getHttpMethod())) {
throw new UnsupportedOperationException(
MessageFormat.format("Knative custom function doesn''t support the {0} HTTP method. Supported methods are: {1}.", operation.getHttpMethod(), SUPPORTED_METHODS));
}

if (operation.isCloudEvent() && !operation.getHttpMethod().equals(HttpMethod.POST)) {
throw new UnsupportedOperationException(MessageFormat.format("Knative custom function can only send CloudEvents through POST method. Method used: {0}", operation.getHttpMethod()));
}
}

public String getService() {
Expand All @@ -49,6 +74,10 @@ public boolean isCloudEvent() {
return isCloudEvent;
}

public HttpMethod getHttpMethod() {
return httpMethod;
}

public static Operation parse(String value) {
String[] parts = value.split("\\?", 2);

Expand All @@ -61,8 +90,9 @@ public static Operation parse(String value) {

return builder()
.withService(parts[0])
.withPath(params.get("path"))
.withPath(params.get(PATH_PARAMETER_NAME))
.withIsCloudEvent(Boolean.parseBoolean(params.get(CLOUD_EVENT_PARAMETER_NAME)))
.withMethod(HttpMethod.valueOf(params.getOrDefault(METHOD_PARAMETER_NAME, DEFAULT_HTTP_METHOD.name()).toUpperCase()))
.build();
}

Expand All @@ -81,12 +111,23 @@ public boolean equals(Object o) {
Operation operation = (Operation) o;
return isCloudEvent == operation.isCloudEvent
&& Objects.equals(service, operation.service)
&& Objects.equals(path, operation.path);
&& Objects.equals(path, operation.path)
&& Objects.equals(httpMethod, operation.httpMethod);
}

@Override
public String toString() {
return "Operation{" +
"service='" + service + '\'' +
", path='" + path + '\'' +
", isCloudEvent=" + isCloudEvent +
", httpMethod=" + httpMethod +
'}';
}

@Override
public int hashCode() {
return Objects.hash(service, path, isCloudEvent);
return Objects.hash(service, path, isCloudEvent, httpMethod);
}

public static class Builder {
Expand All @@ -97,6 +138,8 @@ public static class Builder {

private boolean isCloudEvent;

private HttpMethod httpMethod = DEFAULT_HTTP_METHOD;

private Builder() {
}

Expand All @@ -115,6 +158,11 @@ public Builder withIsCloudEvent(boolean isCloudEvent) {
return this;
}

public Builder withMethod(HttpMethod httpMethod) {
this.httpMethod = httpMethod;
return this;
}

public Operation build() {
return new Operation(this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates.
*
* 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
*
* http://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 org.kie.kogito.addons.quarkus.knative.serving.customfunctions;

import java.util.HashMap;
import java.util.Map;

import javax.inject.Inject;

import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.vertx.core.http.HttpMethod;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
import io.vertx.mutiny.ext.web.client.WebClient;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.PAYLOAD_FIELDS_DELIMITER;
import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.PAYLOAD_FIELDS_PROPERTY_NAME;

@QuarkusTest
class GetRequestKnativeParamsDecoratorTest {

@Inject
WebClient webClient;

final GetRequestKnativeParamsDecorator decorator = new GetRequestKnativeParamsDecorator();

@Test
void decorate() {
Map<String, String> expectedParams = Map.of(
"key1", "value1",
"key2", "value2");

HttpRequest<?> request = createRequest();

HashMap<String, Object> parameters = new HashMap<>(expectedParams);
parameters.put(PAYLOAD_FIELDS_PROPERTY_NAME, String.join(PAYLOAD_FIELDS_DELIMITER, expectedParams.keySet()));

decorator.decorate(null, parameters, request);

assertThat(request.queryParams()).hasSize(2);
expectedParams.forEach((k, v) -> assertThat(request.queryParams().get(k)).isEqualTo(v));
}

@Test
void decorateNonStringValuesShouldThrowException() {
Map<String, Object> expectedParams = Map.of(
"key1", "value1",
"key2", new Object());

HttpRequest<?> request = createRequest();

HashMap<String, Object> parameters = new HashMap<>(expectedParams);
parameters.put(PAYLOAD_FIELDS_PROPERTY_NAME, String.join(PAYLOAD_FIELDS_DELIMITER, expectedParams.keySet()));

assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> decorator.decorate(null, parameters, request));
}

private HttpRequest<Buffer> createRequest() {
return webClient.request(HttpMethod.GET, 8080, "localhost", "/path");
}
}
Loading

0 comments on commit dad6ef0

Please sign in to comment.