Skip to content

Commit

Permalink
KOGITO-8410 Added support for GET method to Knative custom function (#…
Browse files Browse the repository at this point in the history
…3172)

* KOGITO-8410 Added support to GET method to Knative custom function

Signed-off-by: Helber Belmiro <[email protected]>

* KOGITO-8410 Added support for Number and Boolean to GetRequestKnativeParamsDecorator

Signed-off-by: Helber Belmiro <[email protected]>

* KOGITO-8410 Removed GetRequestKnativeParamsDecorator

Signed-off-by: Helber Belmiro <[email protected]>

* KOGITO-8410 Replaced PrefixParamsDecorator with CollectionParamsDecorator

Signed-off-by: Helber Belmiro <[email protected]>

---------

Signed-off-by: Helber Belmiro <[email protected]>
  • Loading branch information
hbelmiro committed Aug 23, 2023
1 parent 20e3213 commit 6bb3380
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@
import org.jbpm.ruleflow.core.RuleFlowNodeContainerFactory;
import org.jbpm.ruleflow.core.factory.NodeFactory;
import org.jbpm.ruleflow.core.factory.WorkItemNodeFactory;
import org.jetbrains.annotations.NotNull;
import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.CloudEventKnativeParamsDecorator;
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;
import org.kie.kogito.serverless.workflow.parser.ParserContext;
import org.kie.kogito.serverless.workflow.parser.VariableInfo;
import org.kie.kogito.serverless.workflow.parser.types.WorkItemTypeHandler;
import org.kie.kogito.serverless.workflow.suppliers.CollectionParamsDecoratorSupplier;
import org.kie.kogito.serverless.workflow.suppliers.ParamsRestBodyBuilderSupplier;
import org.kogito.workitem.rest.RestWorkItemHandler;

Expand Down Expand Up @@ -57,41 +59,53 @@ public class KnativeTypeHandler extends WorkItemTypeHandler {
WorkItemNodeFactory<?> node = buildWorkItem(embeddedSubProcess, context, varInfo.getInputVar(), varInfo.getOutputVar())
.name(functionDef.getName());

if (functionRef.getArguments() != null && !functionRef.getArguments().isEmpty()) {
List<String> payloadFields = new ArrayList<>();
functionRef.getArguments().fieldNames().forEachRemaining(payloadFields::add);
List<String> payloadFields = getPayloadFields(functionRef);

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

if (HttpMethod.GET.equals(operation.getHttpMethod())) {
node.workParameter(RestWorkItemHandler.PARAMS_DECORATOR, new CollectionParamsDecoratorSupplier(List.of(), payloadFields));
} else {
if (!payloadFields.isEmpty()) {
node.workParameter(PAYLOAD_FIELDS_PROPERTY_NAME, String.join(";", payloadFields));
node.workParameter(PAYLOAD_FIELDS_PROPERTY_NAME, payloadFields);
}
if (operation.isCloudEvent()) {
node.workParameter(RestWorkItemHandler.PARAMS_DECORATOR, CloudEventKnativeParamsDecorator.class.getName());
} else {
node.workParameter(RestWorkItemHandler.PARAMS_DECORATOR, PlainJsonKnativeParamsDecorator.class.getName());
}
}

node.workParameter(KnativeWorkItemHandler.SERVICE_PROPERTY_NAME, operation.getService())
.workParameter(KnativeWorkItemHandler.PATH_PROPERTY_NAME, operation.getPath())
.workParameter(RestWorkItemHandler.METHOD, operation.getHttpMethod());

return addFunctionArgs(workflow,
fillWorkItemHandler(workflow, context, node, functionDef),
functionRef);
}

@NotNull
private static List<String> getPayloadFields(FunctionRef functionRef) {
List<String> payloadFields = new ArrayList<>();

if (functionRef.getArguments() != null && !functionRef.getArguments().isEmpty()) {
functionRef.getArguments().fieldNames().forEachRemaining(payloadFields::add);
}
return payloadFields;
}

@Override
protected <T extends RuleFlowNodeContainerFactory<T, ?>> WorkItemNodeFactory<T> fillWorkItemHandler(
Workflow workflow, ParserContext context, WorkItemNodeFactory<T> node, FunctionDefinition functionDef) {
if (functionDef.getMetadata() != null) {
functionDef.getMetadata().forEach(node::metaData);
}

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)
return node.workParameter(RestWorkItemHandler.BODY_BUILDER, new ParamsRestBodyBuilderSupplier())
.workParameter(RestWorkItemHandler.REQUEST_TIMEOUT_IN_MILLIS, requestTimeout)
.metaData(TaskDescriptor.KEY_WORKITEM_TYPE, RestWorkItemHandler.REST_TASK_TYPE)
.workName(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
Expand Up @@ -15,13 +15,12 @@
*/
package org.kie.kogito.addons.quarkus.knative.serving.customfunctions;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

import static java.util.stream.Collectors.toMap;
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;

final class KnativeFunctionPayloadSupplier {
Expand All @@ -35,11 +34,8 @@ static Map<String, Object> getPayload(Map<String, Object> parameters) {
}

private static List<String> getPayloadFields(Map<String, Object> parameters) {
String payloadFields = (String) parameters.remove(PAYLOAD_FIELDS_PROPERTY_NAME);
if (payloadFields != null) {
return Arrays.asList(payloadFields.split(PAYLOAD_FIELDS_DELIMITER));
} else {
return List.of();
}
@SuppressWarnings("unchecked")
List<String> payloadFields = (List<String>) parameters.remove(PAYLOAD_FIELDS_PROPERTY_NAME);
return Objects.requireNonNullElseGet(payloadFields, List::of);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ public final class KnativeWorkItemHandler extends RestWorkItemHandler {

public static final String PAYLOAD_FIELDS_PROPERTY_NAME = "knative_function_payload_fields";

public static final String PAYLOAD_FIELDS_DELIMITER = ";";

public static final String CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE = "A Knative custom function argument cannot be a CloudEvent when the 'asCloudEvent' property are not set to 'true'";

private final KubernetesServiceCatalog kubernetesServiceCatalog;
Expand Down
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
Expand Up @@ -21,29 +21,55 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import io.vertx.core.http.HttpMethod;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.CLOUD_EVENT_PARAMETER_NAME;
import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.METHOD_PARAMETER_NAME;
import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.PATH_PARAMETER_NAME;

class OperationTest {

public static final String SERVICE = "service";

public static Stream<Arguments> parseSource() {
return Stream.of(
Arguments.of("service", Operation.builder().withService("service").build()),
Arguments.of(SERVICE, Operation.builder().withService(SERVICE).build()),

Arguments.of("service?", Operation.builder().withService(SERVICE).build()),

Arguments.of("service?", Operation.builder().withService("service").build()),
Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path", Operation.builder().withService(SERVICE).withPath("/my_path").build()),

Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path", Operation.builder().withService("service").withPath("/my_path").build()),
Arguments.of("service?" + CLOUD_EVENT_PARAMETER_NAME + "=true", Operation.builder().withService(SERVICE).withIsCloudEvent(true).build()),

Arguments.of("service?" + CLOUD_EVENT_PARAMETER_NAME + "=true", Operation.builder().withService("service").withIsCloudEvent(true).build()),
Arguments.of("service?" + METHOD_PARAMETER_NAME + "=GET", Operation.builder().withService(SERVICE).withMethod(HttpMethod.GET).build()),

Arguments.of("service?" + METHOD_PARAMETER_NAME + "=get", Operation.builder().withService(SERVICE).withMethod(HttpMethod.GET).build()),

Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path&" + CLOUD_EVENT_PARAMETER_NAME + "=true",
Operation.builder().withService("service").withPath("/my_path").withIsCloudEvent(true).build()));
Operation.builder().withService(SERVICE).withPath("/my_path").withIsCloudEvent(true).build()),

Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path&" + CLOUD_EVENT_PARAMETER_NAME + "=false&" + METHOD_PARAMETER_NAME + "=GET",
Operation.builder().withService(SERVICE).withPath("/my_path").withIsCloudEvent(false).withMethod(HttpMethod.GET).build()));
}

public static Stream<Arguments> invalidOperationSource() {
return Stream.of(
Arguments.of(Operation.builder().withService(SERVICE).withMethod(HttpMethod.DELETE)),
Arguments.of(Operation.builder().withService(SERVICE).withIsCloudEvent(true).withMethod(HttpMethod.GET)));
}

@ParameterizedTest
@MethodSource("parseSource")
void parse(String operationValue, Operation expectedOperation) {
assertThat(Operation.parse(operationValue)).isEqualTo(expectedOperation);
}
}

@ParameterizedTest
@MethodSource("invalidOperationSource")
void invalidOperation(Operation.Builder operationBuilder) {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(operationBuilder::build);
}
}
Loading

0 comments on commit 6bb3380

Please sign in to comment.