Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/json handler consumer factory #148

Merged
merged 10 commits into from
Apr 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
import io.vertx.core.json.JsonObject;

/**
* The {@link FragmentExecutionLogConsumer} factory interface that enables dynamic implementation binding
* using SPI.
* The {@link FragmentExecutionLogConsumer} factory interface that enables dynamic implementation
* binding using SPI.
*/
public interface FragmentExecutionLogConsumerFactory {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import io.knotx.fragments.handler.consumer.api.FragmentExecutionLogConsumerFactory;
import io.knotx.fragments.handler.consumer.api.model.FragmentExecutionLog;
import io.knotx.server.api.context.ClientRequest;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -96,14 +95,11 @@ private boolean isSupported(FragmentExecutionLog executionData) {
}

private Set<String> getSupportedTypes(JsonObject config) {
if (config.containsKey(FRAGMENT_TYPES_OPTIONS)) {
JsonArray fragmentTypes = config.getJsonArray(FRAGMENT_TYPES_OPTIONS);
return StreamSupport.stream(fragmentTypes.spliterator(), false)
.map(Object::toString)
.collect(Collectors.toSet());
} else {
return Collections.emptySet();
}
return Optional.ofNullable(config.getJsonArray(FRAGMENT_TYPES_OPTIONS))
.map(fragmentTypes -> StreamSupport.stream(fragmentTypes.spliterator(), false)
.map(Object::toString)
.collect(Collectors.toSet()))
.orElse(Collections.emptySet());
}

private String getConditionHeader(JsonObject config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,7 @@ void expectFragmentBodyContainsDebugScript() {
Fragment fragment = new Fragment("snippet", new JsonObject(), body);
FragmentEvent event = new FragmentEvent(fragment);

JsonObject expectedLog = new JsonObject()
.put("startTime", 0)
.put("finishTime", 0)
.put("status", "UNPROCESSED")
.put("fragment", new JsonObject()
.put("id", fragment.getId())
.put("type", "snippet"))
.put("graph", new JsonObject());
JsonObject expectedLog = FragmentExecutionLog.newInstance(event).toJson();

String scriptRegexp =
"<script data-knotx-debug=\"log\" data-knotx-id=\"" + event.getFragment().getId()
Expand All @@ -209,7 +202,8 @@ void expectFragmentBodyContainsDebugScript() {
// then
Matcher matcher = scriptPattern.matcher(event.getFragment().getBody());
assertTrue(matcher.find());
assertJsonEquals(expectedLog, new JsonObject(matcher.group("fragmentEventJson")));
assertJsonEquals(expectedLog,
new FragmentExecutionLog(new JsonObject(matcher.group("fragmentEventJson"))).toJson());
}

@Test
Expand Down
48 changes: 48 additions & 0 deletions handler/consumer/json/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Fragment JSON Consumer
When configured, it appends debug data to `Fragment` body under `_knotx_fragment` key, the existing body remains unchanged.
Appended entry will contain data provided by [FragmentExecutionLog](/~https://github.com/Knotx/knotx-fragments/blob/master/handler/consumer/api/src/main/java/io/knotx/fragments/handler/consumer/api/model/FragmentExecutionLog.java).
```
{
"user" {
"profile": "admin"
}
...
"_knotx_fragment": {
"fragment": {},
"status": {},
"graph": {}
}
}
```

## How to start?
- Configure consumer in handler
- add a [consumer factory configuration](#how-to-configure) to the [Fragments Handler options](/~https://github.com/Knotx/knotx-fragments/blob/master/handler/core/docs/asciidoc/dataobjects.adoc#fragmentshandleroptions)
- set `logLevel` to `INFO` for more fragments' processing details in the [Default Task Factory config](/~https://github.com/Knotx/knotx-fragments/blob/master/handler/core/docs/asciidoc/dataobjects.adoc#defaulttaskfactoryconfig)

Any issues? Please check the [functional](/~https://github.com/Knotx/knotx-stack/blob/master/src/functionalTest/java/io/knotx/stack/functional/KnotxFragmentsDebugDataWithHandlebarsTest.java) test configuration.

## How to configure?
It must be configured in `consumerFactories`
```hocon
consumerFactories = [
{
factory = fragmentJsonBodyWriter
config { CONSUMER_CONFIG }
}
]
```
where `CONSUMER_CONFIG` consists of:
```hocon
condition {
param = debug
# header = x-knotx-debug
}
fragmentTypes = [ "json" ]
```
It runs when any of the following conditions are met:
- `param` - an original request contains a parameter with the *given name* (e.g. by configuring
`param=debug`, requesting `{address}?debug=true` will meet the condition),
- `header` - condition is analogous, but the value comes from the request header.

If no condition is configured, the consumer is not triggered.
1 change: 1 addition & 0 deletions handler/consumer/json/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation(group = "org.apache.commons", name = "commons-lang3")
implementation(group = "com.google.guava", name = "guava")

testImplementation("org.junit.jupiter:junit-jupiter-params")
testImplementation(group = "org.mockito", name = "mockito-core")
testImplementation(group = "org.mockito", name = "mockito-junit-jupiter")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,96 @@
*/
package io.knotx.fragments.handler.consumer.json;

import static java.lang.Boolean.FALSE;

import io.knotx.fragments.handler.consumer.api.FragmentExecutionLogConsumer;
import io.knotx.fragments.handler.consumer.api.FragmentExecutionLogConsumerFactory;
import io.knotx.fragments.handler.consumer.api.model.FragmentExecutionLog;
import io.knotx.server.api.context.ClientRequest;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

// TODO fix #134
public class JsonFragmentsHandlerConsumerFactory implements FragmentExecutionLogConsumerFactory {

private static final String PARAM_OPTION = "param";
static final String KNOTX_FRAGMENT = "_knotx_fragment";
static final String FRAGMENT_TYPES_OPTIONS = "fragmentTypes";
static final String HEADER_OPTION = "header";
static final String CONDITION_OPTION = "condition";

@Override
public String getName() {
return "json";
return "fragmentJsonBodyWriter";
}

@Override
public FragmentExecutionLogConsumer create(JsonObject config) {
return null;
return new FragmentExecutionLogConsumer() {

private Set<String> supportedTypes = getSupportedTypes(config);
private String requestHeader = getConditionHeader(config);
private String requestParam = getConditionParam(config);

@Override
public void accept(ClientRequest request, List<FragmentExecutionLog> executions) {
if (containsHeader(request) || containsParam(request)) {
executions.stream()
.filter(this::isSupported)
.forEach(this::appendExecutionDataToFragmentBody);
}
}

private boolean isSupported(FragmentExecutionLog executionData) {
if (supportedTypes.contains(executionData.getFragment().getType())) {
try {
new JsonObject(executionData.getFragment().getBody());
return true;
} catch (DecodeException e) {
return false;
}
}
return false;
}

private boolean containsHeader(ClientRequest request) {
return Optional.ofNullable(requestHeader)
.map(header -> request.getHeaders().contains(header))
.orElse(FALSE);
}

private boolean containsParam(ClientRequest request) {
return Optional.ofNullable(requestParam)
.map(param -> request.getParams().contains(param))
.orElse(FALSE);
}

private void appendExecutionDataToFragmentBody(FragmentExecutionLog executionData) {
JsonObject fragmentBody = new JsonObject().put(KNOTX_FRAGMENT, executionData.toJson())
.mergeIn(new JsonObject(executionData.getFragment().getBody()));
executionData.getFragment().setBody(fragmentBody.encodePrettily());
}
};
}

private Set<String> getSupportedTypes(JsonObject config) {
return Optional.ofNullable(config.getJsonArray(FRAGMENT_TYPES_OPTIONS))
.map(fragmentTypes -> StreamSupport.stream(fragmentTypes.spliterator(), false)
.map(Object::toString)
.collect(Collectors.toSet()))
.orElse(Collections.emptySet());
}

private String getConditionHeader(JsonObject config) {
return config.getJsonObject(CONDITION_OPTION, new JsonObject()).getString(HEADER_OPTION);
}

private String getConditionParam(JsonObject config) {
return config.getJsonObject(CONDITION_OPTION, new JsonObject()).getString(PARAM_OPTION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,172 @@
*/
package io.knotx.fragments.handler.consumer.json;

import static org.junit.jupiter.api.Assertions.*;
import static io.knotx.fragments.engine.api.EventLogEntry.NodeStatus.UNPROCESSED;
import static io.knotx.fragments.handler.consumer.json.JsonFragmentsHandlerConsumerFactory.CONDITION_OPTION;
import static io.knotx.fragments.handler.consumer.json.JsonFragmentsHandlerConsumerFactory.FRAGMENT_TYPES_OPTIONS;
import static io.knotx.fragments.handler.consumer.json.JsonFragmentsHandlerConsumerFactory.HEADER_OPTION;
import static io.knotx.fragments.handler.consumer.json.JsonFragmentsHandlerConsumerFactory.KNOTX_FRAGMENT;
import static io.knotx.junit5.assertions.KnotxAssertions.assertJsonEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.common.collect.ImmutableList;
import io.knotx.fragments.api.Fragment;
import io.knotx.fragments.engine.api.FragmentEvent;
import io.knotx.fragments.handler.consumer.api.FragmentExecutionLogConsumer;
import io.knotx.fragments.handler.consumer.api.model.FragmentExecutionLog;
import io.knotx.server.api.context.ClientRequest;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.reactivex.core.MultiMap;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

// TODO fix #134
class JsonFragmentsHandlerConsumerFactoryTest {

private static final String EXPECTED_FRAGMENT_TYPE = "json";
private static final String EXPECTED_HEADER = "x-knotx-debug";
private static final String EXPECTED_PARAM = "debug";
private static final String PARAM_OPTION = "param";
private static final String OTHER_TYPE = "other";
private static final String FRAGMENT_BODY_JSON = "{\"user\": \"admin\"}";
private static final String FRAGMENT_BODY_OTHER = "\"simple text value\"";
private static final String USER_KEY = "user";
private static final String UNSUPPORTED = "unsupported";

private static Stream<Arguments> unfulfilledConditions() {
return Stream.of( //fragmentType, fragmentBody, supportedTypes
Arguments.of(EXPECTED_FRAGMENT_TYPE, FRAGMENT_BODY_JSON, new JsonArray()),
Arguments.of(OTHER_TYPE, FRAGMENT_BODY_OTHER, new JsonArray()),
Arguments.of(EXPECTED_FRAGMENT_TYPE, FRAGMENT_BODY_OTHER, new JsonArray()),
Arguments.of(OTHER_TYPE, FRAGMENT_BODY_JSON, new JsonArray()),
Arguments.of(OTHER_TYPE, FRAGMENT_BODY_OTHER, new JsonArray().add(EXPECTED_FRAGMENT_TYPE)),
Arguments.of(EXPECTED_FRAGMENT_TYPE, FRAGMENT_BODY_OTHER,
new JsonArray().add(EXPECTED_FRAGMENT_TYPE)),
Arguments.of(OTHER_TYPE, FRAGMENT_BODY_JSON, new JsonArray().add(EXPECTED_FRAGMENT_TYPE)),
Arguments.of(EXPECTED_FRAGMENT_TYPE, FRAGMENT_BODY_JSON, new JsonArray().add(UNSUPPORTED)),
Arguments.of(OTHER_TYPE, FRAGMENT_BODY_OTHER, new JsonArray().add(UNSUPPORTED)),
Arguments.of(EXPECTED_FRAGMENT_TYPE, FRAGMENT_BODY_OTHER, new JsonArray().add(UNSUPPORTED)),
Arguments.of(OTHER_TYPE, FRAGMENT_BODY_JSON, new JsonArray().add(UNSUPPORTED))
);
}

@ParameterizedTest
@MethodSource("unfulfilledConditions")
@DisplayName("Fragment body should not be modified when no supported methods specified in configuration")
void fragmentBodyShouldNotBeModifiedWhenInvalidConfigurationProvided(String fragmentType,
String fragmentBody, JsonArray supportedTypes) {
FragmentEvent original = new FragmentEvent(
new Fragment(fragmentType, new JsonObject(), fragmentBody));
FragmentEvent copy = new FragmentEvent(original.toJson());

FragmentExecutionLogConsumer tested = new JsonFragmentsHandlerConsumerFactory()
.create(new JsonObject()
.put(CONDITION_OPTION, new JsonObject().put(HEADER_OPTION, EXPECTED_HEADER))
.put(FRAGMENT_TYPES_OPTIONS, supportedTypes));
tested.accept(new ClientRequest()
.setHeaders(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_HEADER, "true")),
ImmutableList.of(FragmentExecutionLog.newInstance(original)));

assertEquals(copy, original);
}

@Test
@DisplayName("Fragment should be modified when header condition and fragment type match")
void expectFragmentModifiedWhenHeaderConditionAndSupportedTypedConfigured() {
//given
FragmentEvent original = new FragmentEvent(
new Fragment(EXPECTED_FRAGMENT_TYPE, new JsonObject(), FRAGMENT_BODY_JSON));
FragmentEvent copy = new FragmentEvent(original.toJson());

//when
FragmentExecutionLogConsumer tested = new JsonFragmentsHandlerConsumerFactory()
.create(new JsonObject()
.put(FRAGMENT_TYPES_OPTIONS, new JsonArray().add(EXPECTED_FRAGMENT_TYPE))
.put(CONDITION_OPTION, new JsonObject().put(HEADER_OPTION, EXPECTED_HEADER)));
tested.accept(new ClientRequest()
.setHeaders(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_HEADER, "true")),
ImmutableList.of(FragmentExecutionLog.newInstance(original)));

//then
assertNotEquals(copy, original);
}

@Test
@DisplayName("Fragment should be modified when param condition and fragment type match")
void expectFragmentModifiedWhenParamConditionAndSupportedTypesConfigured() {
//given
FragmentEvent original = new FragmentEvent(
new Fragment(EXPECTED_FRAGMENT_TYPE, new JsonObject(), FRAGMENT_BODY_JSON));
FragmentEvent copy = new FragmentEvent(original.toJson());

//when
FragmentExecutionLogConsumer tested = new JsonFragmentsHandlerConsumerFactory()
.create(new JsonObject()
.put(FRAGMENT_TYPES_OPTIONS, new JsonArray().add(EXPECTED_FRAGMENT_TYPE))
.put(CONDITION_OPTION, new JsonObject().put(PARAM_OPTION, EXPECTED_PARAM)));
tested.accept(new ClientRequest()
.setParams(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_PARAM, "true")),
ImmutableList.of(FragmentExecutionLog.newInstance(original)));

//then
assertNotEquals(copy, original);
}

@Test
@DisplayName("Execution log entry should be properly merged into existing fragment body")
void expectExecutionLogEntryProperlyMergedIntoFragmentBody() {
//given
FragmentEvent original = new FragmentEvent(
new Fragment(EXPECTED_FRAGMENT_TYPE, new JsonObject(), FRAGMENT_BODY_JSON));
FragmentEvent copy = new FragmentEvent(original.toJson());

//when
FragmentExecutionLogConsumer tested = new JsonFragmentsHandlerConsumerFactory()
.create(new JsonObject()
.put(FRAGMENT_TYPES_OPTIONS, new JsonArray().add(EXPECTED_FRAGMENT_TYPE))
.put(CONDITION_OPTION, new JsonObject().put(PARAM_OPTION, EXPECTED_PARAM)));
tested.accept(new ClientRequest()
.setParams(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_PARAM, "true")),
ImmutableList.of(FragmentExecutionLog.newInstance(original)));

//then
assertNotEquals(copy, original);
JsonObject fragmentBody = new JsonObject(original.getFragment().getBody());
assertTrue(fragmentBody.containsKey(USER_KEY));
assertTrue(fragmentBody.containsKey(KNOTX_FRAGMENT));
}

@Test
@DisplayName("Execution log entry should contain proper fragment details")
void expectFragmentDetailsInExecutionLogEntry() {
//given
FragmentEvent original = new FragmentEvent(
new Fragment(EXPECTED_FRAGMENT_TYPE, new JsonObject(), FRAGMENT_BODY_JSON));
FragmentEvent copy = new FragmentEvent(original.toJson());

JsonObject expectedLog = FragmentExecutionLog.newInstance(original).toJson();

//when
FragmentExecutionLogConsumer tested = new JsonFragmentsHandlerConsumerFactory()
.create(new JsonObject()
.put(FRAGMENT_TYPES_OPTIONS, new JsonArray().add(EXPECTED_FRAGMENT_TYPE))
.put(CONDITION_OPTION, new JsonObject().put(PARAM_OPTION, EXPECTED_PARAM)));
tested.accept(new ClientRequest()
.setParams(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_PARAM, "true")),
ImmutableList.of(FragmentExecutionLog.newInstance(original)));

//then
assertNotEquals(copy, original);
JsonObject fragmentBody = new JsonObject(original.getFragment().getBody());
assertJsonEquals(expectedLog,
new FragmentExecutionLog(fragmentBody.getJsonObject(KNOTX_FRAGMENT)).toJson());
assertEquals(new JsonObject(FRAGMENT_BODY_JSON).getString(USER_KEY),
fragmentBody.getString(USER_KEY));
}
}
Loading