Skip to content

Commit

Permalink
Adding support for state lists
Browse files Browse the repository at this point in the history
- adding additional `list` handling:
 - add items to list (first or last)
 - delete items from list (first, last, index, by property)
 - return selective entries from list
- extended error reporting
- create common base for all tests
  • Loading branch information
dirkbolte committed Jul 27, 2023
1 parent dbba8ed commit acf1cb8
Show file tree
Hide file tree
Showing 14 changed files with 1,079 additions and 372 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.wiremock.extensions.state.extensions;

import com.github.tomakehurst.wiremock.common.Json;
import com.github.tomakehurst.wiremock.core.ConfigurationException;
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.ServeEventListener;
Expand All @@ -23,9 +24,13 @@
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
import org.apache.commons.lang3.StringUtils;
import org.wiremock.extensions.state.internal.ContextManager;
import org.wiremock.extensions.state.internal.DeleteStateParameters;
import org.wiremock.extensions.state.internal.ResponseTemplateModel;
import org.wiremock.extensions.state.internal.StateExtensionMixin;

import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
Expand All @@ -35,7 +40,7 @@
*
* @see org.wiremock.extensions.state.StateExtension
*/
public class DeleteStateEventListener implements ServeEventListener {
public class DeleteStateEventListener implements ServeEventListener, StateExtensionMixin {

private final TemplateEngine templateEngine;
private final ContextManager contextManager;
Expand All @@ -46,15 +51,6 @@ public DeleteStateEventListener(ContextManager contextManager, TemplateEngine te
this.templateEngine = templateEngine;
}

public void afterComplete(ServeEvent serveEvent, Parameters parameters) {
var model = Map.of(
"request", RequestTemplateModel.from(serveEvent.getRequest()),
"response", ResponseTemplateModel.from(serveEvent.getResponse())
);
var contextName = createContextName(model, parameters);
contextManager.deleteContext(contextName);
}

@Override
public String getName() {
return "deleteState";
Expand All @@ -65,11 +61,57 @@ public boolean applyGlobally() {
return false;
}

private String createContextName(Map<String, Object> model, Parameters parameters) {
var rawContext = Optional.ofNullable(parameters.getString("context")).filter(StringUtils::isNotBlank).orElseThrow(() -> new ConfigurationException("no context specified"));
public void afterComplete(ServeEvent serveEvent, Parameters parameters) {
var model = Map.of(
"request", RequestTemplateModel.from(serveEvent.getRequest()),
"response", ResponseTemplateModel.from(serveEvent.getResponse())
);
var configuration = Json.mapToObject(parameters, DeleteStateParameters.class);
var contextName = createContextName(model, configuration);
Optional.ofNullable(configuration.getList()).ifPresentOrElse(
listConfig -> handleListDeletion(listConfig, contextName, model),
() -> contextManager.deleteContext(contextName)
);
}

private void handleListDeletion(DeleteStateParameters.ListParameters listConfig, String contextName, Map<String, Object> model) {
if (Boolean.TRUE.equals(listConfig.getDeleteFirst())) {
contextManager.createOrUpdateContextList(contextName, LinkedList::removeFirst);
} else if (Boolean.TRUE.equals(listConfig.getDeleteLast())) {
contextManager.createOrUpdateContextList(contextName, LinkedList::removeLast);
} else if (StringUtils.isNotBlank(listConfig.getDeleteIndex())) {
try {
var index = Integer.parseInt(renderTemplate(model, listConfig.getDeleteIndex()));
contextManager.createOrUpdateContextList(contextName, list -> list.remove(index));
} catch (IndexOutOfBoundsException | NumberFormatException e) {
throw createConfigurationError("List index '%s' does not exist or cannot be parsed: %s", listConfig.getDeleteIndex(), e.getMessage());
}
} else if (listConfig.getDeleteWhere() != null &&
listConfig.getDeleteWhere().getProperty() != null &&
listConfig.getDeleteWhere().getValue() != null
) {
var property = renderTemplate(model, listConfig.getDeleteWhere().getProperty());
var value = renderTemplate(model, listConfig.getDeleteWhere().getValue());
contextManager.createOrUpdateContextList(contextName, list -> {
var iterator = list.iterator();
while (iterator.hasNext()) {
var element = iterator.next();
if (Objects.equals(element.getOrDefault(property, null), value)) {
iterator.remove();
break;
}
}
});
} else {
throw createConfigurationError("Missing/invalid configuration for list");
}
}

private String createContextName(Map<String, Object> model, DeleteStateParameters parameters) {
var rawContext = Optional.ofNullable(parameters.getContext()).filter(StringUtils::isNotBlank).orElseThrow(() -> new ConfigurationException("no context specified"));
String context = renderTemplate(model, rawContext);
if (StringUtils.isBlank(context)) {
throw new ConfigurationException("context is blank");
throw createConfigurationError("Context cannot be blank");
}
return context;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.wiremock.extensions.state.extensions;

import com.github.tomakehurst.wiremock.common.Json;
import com.github.tomakehurst.wiremock.core.ConfigurationException;
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.ServeEventListener;
Expand All @@ -23,7 +24,9 @@
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
import org.apache.commons.lang3.StringUtils;
import org.wiremock.extensions.state.internal.ContextManager;
import org.wiremock.extensions.state.internal.RecordStateParameters;
import org.wiremock.extensions.state.internal.ResponseTemplateModel;
import org.wiremock.extensions.state.internal.StateExtensionMixin;

import java.util.Map;
import java.util.Optional;
Expand All @@ -36,7 +39,7 @@
*
* @see org.wiremock.extensions.state.StateExtension
*/
public class RecordStateEventListener implements ServeEventListener {
public class RecordStateEventListener implements ServeEventListener, StateExtensionMixin {

private final TemplateEngine templateEngine;
private final ContextManager contextManager;
Expand All @@ -51,8 +54,10 @@ public void afterComplete(ServeEvent serveEvent, Parameters parameters) {
"request", RequestTemplateModel.from(serveEvent.getRequest()),
"response", ResponseTemplateModel.from(serveEvent.getResponse())
);
var configuration = Json.mapToObject(parameters, RecordStateParameters.class);
var contextName = createContextName(model, parameters);
storeContextAndState(contextName, model, parameters);
handleState(contextName, model, configuration);
handleList(contextName, model, configuration);
}

@Override
Expand All @@ -65,29 +70,42 @@ public boolean applyGlobally() {
return false;
}

private void storeContextAndState(String context, Map<String, Object> model, Parameters parameters) {
@SuppressWarnings("unchecked") Map<String, Object> state = Optional.ofNullable(parameters.get("state"))
.filter(it -> it instanceof Map)
.map(Map.class::cast)
.orElseThrow(() -> new ConfigurationException("no state specified"));
var properties = state.entrySet()
private void handleState(String contextName, Map<String, Object> model, RecordStateParameters parameters) {
Optional.ofNullable(parameters.getState())
.ifPresent(configuration ->
contextManager.createOrUpdateContextState(contextName, getPropertiesFromConfiguration(model, configuration))
);
}

private Map<String, String> getPropertiesFromConfiguration(Map<String, Object> model, Map<String, String> configuration) {
return configuration.entrySet()
.stream()
.map(entry -> Map.entry(entry.getKey(), renderTemplate(model, entry.getValue().toString())))
.map(entry -> Map.entry(entry.getKey(), renderTemplate(model, entry.getValue())))
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
}

contextManager.createOrUpdateContext(context, properties);
private void handleList(String contextName, Map<String, Object> model, RecordStateParameters parameters) {
Optional.ofNullable(parameters.getList())
.ifPresent(listConfiguration -> {
Optional.ofNullable(listConfiguration.getAddFirst()).ifPresent(configuration ->
contextManager.createOrUpdateContextList(contextName, list -> list.addFirst(getPropertiesFromConfiguration(model, configuration))));
Optional.ofNullable(listConfiguration.getAddLast()).ifPresent(configuration ->
contextManager.createOrUpdateContextList(contextName, list -> list.addLast(getPropertiesFromConfiguration(model, configuration))));
}
);
}

private String createContextName(Map<String, Object> model, Parameters parameters) {
var rawContext = Optional.ofNullable(parameters.getString("context")).filter(StringUtils::isNotBlank).orElseThrow(() -> new ConfigurationException("no context specified"));
String context = renderTemplate(model, rawContext);
if (StringUtils.isBlank(context)) {
throw new ConfigurationException("context is blank");
throw createConfigurationError("context cannot be blank");
}
return context;
}

private String renderTemplate(Object context, String value) {
return templateEngine.getUncachedTemplate(value).apply(context);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
*/
package org.wiremock.extensions.state.extensions;

import com.fasterxml.jackson.core.JsonParser;
import com.github.jknack.handlebars.Options;
import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.HandlebarsHelper;
import com.jayway.jsonpath.JsonPath;
import org.apache.commons.lang3.StringUtils;
import org.wiremock.extensions.state.internal.ContextManager;

Expand All @@ -39,12 +41,24 @@ public StateHandlerbarHelper(ContextManager contextManager) {
public Object apply(Object o, Options options) {
String contextName = options.hash("context");
String property = options.hash("property");
String list = options.hash("list");
if (StringUtils.isEmpty(contextName)) {
return handleError("'context' cannot be empty");
}
if (StringUtils.isEmpty(property)) {
return handleError("'property' cannot be empty");
if (StringUtils.isBlank(property) && StringUtils.isBlank(list)) {
return handleError("Either 'property' or 'list' has to be set");
}
if (StringUtils.isNotBlank(property) && StringUtils.isNotBlank(list)) {
return handleError("Either 'property' or 'list' has to be set");
}
if (StringUtils.isNotBlank(property)) {
return getProperty(contextName, property);
} else {
return getList(contextName, list);
}
}

private Object getProperty(String contextName, String property) {
return contextManager.getContext(contextName)
.map(context -> {
if ("updateCount".equals(property)) {
Expand All @@ -55,4 +69,9 @@ public Object apply(Object o, Options options) {
}
).orElse(handleError(String.format("No state for context %s, property %s found", contextName, property)));
}
private Object getList(String contextName, String list) {
return contextManager.getContext(contextName)
.map(context -> JsonPath.read(context.getList(), list))
.orElse(handleError(String.format("No state for context %s, list %s found", contextName, list)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package org.wiremock.extensions.state.extensions;

import com.github.tomakehurst.wiremock.core.ConfigurationException;
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel;
import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine;
Expand All @@ -25,6 +24,7 @@
import org.wiremock.extensions.state.internal.Context;
import org.wiremock.extensions.state.internal.ContextManager;
import org.wiremock.extensions.state.internal.ContextTemplateModel;
import org.wiremock.extensions.state.internal.StateExtensionMixin;

import java.util.Arrays;
import java.util.HashMap;
Expand All @@ -41,7 +41,7 @@
*
* @see org.wiremock.extensions.state.StateExtension
*/
public class StateRequestMatcher extends RequestMatcherExtension {
public class StateRequestMatcher extends RequestMatcherExtension implements StateExtensionMixin {

private final TemplateEngine templateEngine;
private final ContextManager contextManager;
Expand Down Expand Up @@ -72,7 +72,7 @@ public MatchResult match(Request request, Parameters parameters) {
.ofNullable(parameters.getString("hasContext", null))
.map(template -> hasContext(model, parameters, template))
.or(() -> Optional.ofNullable(parameters.getString("hasNotContext", null)).map(template -> hasNotContext(model, template)))
.orElseThrow(() -> new ConfigurationException("Parameters should only contain 'hasContext' or 'hasNotContext'"));
.orElseThrow(() -> createConfigurationError("Parameters should only contain 'hasContext' or 'hasNotContext'"));
}

private MatchResult hasContext(Map<String, Object> model, Parameters parameters, String template) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class Context {

private static final int MAX_IDS = 10;
private final String contextName;
private final Map<String, String> properties = new HashMap<>();
private final LinkedList<Map<String, String>> list = new LinkedList<>();
private final LinkedList<String> requests = new LinkedList<>();
private Long updateCount = 1L;
private Long matchCount = 0L;
Expand Down Expand Up @@ -66,12 +68,17 @@ public Map<String, String> getProperties() {
return properties;
}

public LinkedList<Map<String, String>> getList() {
return list;
}

@Override
public String toString() {
return "Context{" +
"contextName='" + contextName + '\'' +
", updateCount=" + updateCount +
", properties=" + properties +
", list=" + list +
", updateCount=" + updateCount +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

import com.github.tomakehurst.wiremock.store.Store;

import java.util.LinkedList;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;

public class ContextManager {

Expand Down Expand Up @@ -46,7 +48,7 @@ public void deleteContext(String contextName) {
}
}

public Long createOrUpdateContext(String contextName, Map<String, String> properties) {
public Long createOrUpdateContextState(String contextName, Map<String, String> properties) {
synchronized (store) {
var context = store.get(contextName)
.map(it -> (Context) it)
Expand All @@ -60,6 +62,20 @@ public Long createOrUpdateContext(String contextName, Map<String, String> proper
}
}

public Long createOrUpdateContextList(String contextName, Consumer<LinkedList<Map<String, String>>> consumer) {
synchronized (store) {
var context = store.get(contextName)
.map(it -> (Context) it)
.map(it -> {
it.incUpdateCount();
return it;
}).orElseGet(() -> new Context(contextName));
consumer.accept(context.getList());
store.put(contextName, context);
return context.getUpdateCount();
}
}

public Long numUpdates(String contextName) {
synchronized (store) {
return store.get(contextName).map(it -> ((Context) it).getUpdateCount()).orElse(0L);
Expand Down
Loading

0 comments on commit acf1cb8

Please sign in to comment.