Skip to content

Commit 4e40006

Browse files
authored
state lists feature (#17)
- 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 - adding request matchers `listSizeEqualTo`, `listSizeMoreThan`, `listSizeLessThan` - adding explicit property `listSize` to use in response templates - extended error reporting - create common base for all tests - extending documentation to cover new feature - made sample test cases more "complete" <!-- Please describe your pull request here. --> ## References - #16 <!-- References to relevant GitHub issues and pull requests, esp. upstream and downstream changes --> ## Submitter checklist - [x] The PR request is well described and justified, including the body and the references - [x] The PR title represents the desired changelog entry - [x] The repository's code style is followed (see the contributing guide) - [x] Test coverage that demonstrates that the change works as expected - [x] For new features, there's necessary documentation in this pull request or in a subsequent PR to [wiremock.org](https://github.com/wiremock/wiremock.org) <!-- Put an `x` into the [ ] to show you have filled the information. The template comes from https://github.com/wiremock/.github/blob/main/.github/pull_request_template.md You can override it by creating .github/pull_request_template.md in your own repository -->
1 parent 8ec682a commit 4e40006

19 files changed

+2092
-530
lines changed

README.md

Lines changed: 629 additions & 81 deletions
Large diffs are not rendered by default.

src/main/java/org/wiremock/extensions/state/extensions/DeleteStateEventListener.java

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.wiremock.extensions.state.extensions;
1717

18+
import com.github.tomakehurst.wiremock.common.Json;
1819
import com.github.tomakehurst.wiremock.core.ConfigurationException;
1920
import com.github.tomakehurst.wiremock.extension.Parameters;
2021
import com.github.tomakehurst.wiremock.extension.ServeEventListener;
@@ -23,9 +24,13 @@
2324
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
2425
import org.apache.commons.lang3.StringUtils;
2526
import org.wiremock.extensions.state.internal.ContextManager;
27+
import org.wiremock.extensions.state.internal.DeleteStateParameters;
2628
import org.wiremock.extensions.state.internal.ResponseTemplateModel;
29+
import org.wiremock.extensions.state.internal.StateExtensionMixin;
2730

31+
import java.util.LinkedList;
2832
import java.util.Map;
33+
import java.util.Objects;
2934
import java.util.Optional;
3035

3136
/**
@@ -35,7 +40,7 @@
3540
*
3641
* @see org.wiremock.extensions.state.StateExtension
3742
*/
38-
public class DeleteStateEventListener implements ServeEventListener {
43+
public class DeleteStateEventListener implements ServeEventListener, StateExtensionMixin {
3944

4045
private final TemplateEngine templateEngine;
4146
private final ContextManager contextManager;
@@ -46,15 +51,6 @@ public DeleteStateEventListener(ContextManager contextManager, TemplateEngine te
4651
this.templateEngine = templateEngine;
4752
}
4853

49-
public void afterComplete(ServeEvent serveEvent, Parameters parameters) {
50-
var model = Map.of(
51-
"request", RequestTemplateModel.from(serveEvent.getRequest()),
52-
"response", ResponseTemplateModel.from(serveEvent.getResponse())
53-
);
54-
var contextName = createContextName(model, parameters);
55-
contextManager.deleteContext(contextName);
56-
}
57-
5854
@Override
5955
public String getName() {
6056
return "deleteState";
@@ -65,11 +61,57 @@ public boolean applyGlobally() {
6561
return false;
6662
}
6763

68-
private String createContextName(Map<String, Object> model, Parameters parameters) {
69-
var rawContext = Optional.ofNullable(parameters.getString("context")).filter(StringUtils::isNotBlank).orElseThrow(() -> new ConfigurationException("no context specified"));
64+
public void afterComplete(ServeEvent serveEvent, Parameters parameters) {
65+
var model = Map.of(
66+
"request", RequestTemplateModel.from(serveEvent.getRequest()),
67+
"response", ResponseTemplateModel.from(serveEvent.getResponse())
68+
);
69+
var configuration = Json.mapToObject(parameters, DeleteStateParameters.class);
70+
var contextName = createContextName(model, configuration);
71+
Optional.ofNullable(configuration.getList()).ifPresentOrElse(
72+
listConfig -> handleListDeletion(listConfig, contextName, model),
73+
() -> contextManager.deleteContext(contextName)
74+
);
75+
}
76+
77+
private void handleListDeletion(DeleteStateParameters.ListParameters listConfig, String contextName, Map<String, Object> model) {
78+
if (Boolean.TRUE.equals(listConfig.getDeleteFirst())) {
79+
contextManager.createOrUpdateContextList(contextName, LinkedList::removeFirst);
80+
} else if (Boolean.TRUE.equals(listConfig.getDeleteLast())) {
81+
contextManager.createOrUpdateContextList(contextName, LinkedList::removeLast);
82+
} else if (StringUtils.isNotBlank(listConfig.getDeleteIndex())) {
83+
try {
84+
var index = Integer.parseInt(renderTemplate(model, listConfig.getDeleteIndex()));
85+
contextManager.createOrUpdateContextList(contextName, list -> list.remove(index));
86+
} catch (IndexOutOfBoundsException | NumberFormatException e) {
87+
throw createConfigurationError("List index '%s' does not exist or cannot be parsed: %s", listConfig.getDeleteIndex(), e.getMessage());
88+
}
89+
} else if (listConfig.getDeleteWhere() != null &&
90+
listConfig.getDeleteWhere().getProperty() != null &&
91+
listConfig.getDeleteWhere().getValue() != null
92+
) {
93+
var property = renderTemplate(model, listConfig.getDeleteWhere().getProperty());
94+
var value = renderTemplate(model, listConfig.getDeleteWhere().getValue());
95+
contextManager.createOrUpdateContextList(contextName, list -> {
96+
var iterator = list.iterator();
97+
while (iterator.hasNext()) {
98+
var element = iterator.next();
99+
if (Objects.equals(element.getOrDefault(property, null), value)) {
100+
iterator.remove();
101+
break;
102+
}
103+
}
104+
});
105+
} else {
106+
throw createConfigurationError("Missing/invalid configuration for list");
107+
}
108+
}
109+
110+
private String createContextName(Map<String, Object> model, DeleteStateParameters parameters) {
111+
var rawContext = Optional.ofNullable(parameters.getContext()).filter(StringUtils::isNotBlank).orElseThrow(() -> new ConfigurationException("no context specified"));
70112
String context = renderTemplate(model, rawContext);
71113
if (StringUtils.isBlank(context)) {
72-
throw new ConfigurationException("context is blank");
114+
throw createConfigurationError("Context cannot be blank");
73115
}
74116
return context;
75117
}

src/main/java/org/wiremock/extensions/state/extensions/RecordStateEventListener.java

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.wiremock.extensions.state.extensions;
1717

18+
import com.github.tomakehurst.wiremock.common.Json;
1819
import com.github.tomakehurst.wiremock.core.ConfigurationException;
1920
import com.github.tomakehurst.wiremock.extension.Parameters;
2021
import com.github.tomakehurst.wiremock.extension.ServeEventListener;
@@ -23,7 +24,9 @@
2324
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
2425
import org.apache.commons.lang3.StringUtils;
2526
import org.wiremock.extensions.state.internal.ContextManager;
27+
import org.wiremock.extensions.state.internal.RecordStateParameters;
2628
import org.wiremock.extensions.state.internal.ResponseTemplateModel;
29+
import org.wiremock.extensions.state.internal.StateExtensionMixin;
2730

2831
import java.util.Map;
2932
import java.util.Optional;
@@ -36,7 +39,7 @@
3639
*
3740
* @see org.wiremock.extensions.state.StateExtension
3841
*/
39-
public class RecordStateEventListener implements ServeEventListener {
42+
public class RecordStateEventListener implements ServeEventListener, StateExtensionMixin {
4043

4144
private final TemplateEngine templateEngine;
4245
private final ContextManager contextManager;
@@ -51,8 +54,10 @@ public void afterComplete(ServeEvent serveEvent, Parameters parameters) {
5154
"request", RequestTemplateModel.from(serveEvent.getRequest()),
5255
"response", ResponseTemplateModel.from(serveEvent.getResponse())
5356
);
57+
var configuration = Json.mapToObject(parameters, RecordStateParameters.class);
5458
var contextName = createContextName(model, parameters);
55-
storeContextAndState(contextName, model, parameters);
59+
handleState(contextName, model, configuration);
60+
handleList(contextName, model, configuration);
5661
}
5762

5863
@Override
@@ -65,29 +70,43 @@ public boolean applyGlobally() {
6570
return false;
6671
}
6772

68-
private void storeContextAndState(String context, Map<String, Object> model, Parameters parameters) {
69-
@SuppressWarnings("unchecked") Map<String, Object> state = Optional.ofNullable(parameters.get("state"))
70-
.filter(it -> it instanceof Map)
71-
.map(Map.class::cast)
72-
.orElseThrow(() -> new ConfigurationException("no state specified"));
73-
var properties = state.entrySet()
73+
private void handleState(String contextName, Map<String, Object> model, RecordStateParameters parameters) {
74+
Optional.ofNullable(parameters.getState())
75+
.ifPresent(configuration ->
76+
contextManager.createOrUpdateContextState(contextName, getPropertiesFromConfiguration(model, configuration))
77+
);
78+
}
79+
80+
private Map<String, String> getPropertiesFromConfiguration(Map<String, Object> model, Map<String, String> configuration) {
81+
return configuration.entrySet()
7482
.stream()
75-
.map(entry -> Map.entry(entry.getKey(), renderTemplate(model, entry.getValue().toString())))
83+
.map(entry -> Map.entry(entry.getKey(), renderTemplate(model, entry.getValue())))
7684
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
85+
}
7786

78-
contextManager.createOrUpdateContext(context, properties);
87+
private void handleList(String contextName, Map<String, Object> model, RecordStateParameters parameters) {
88+
Optional.ofNullable(parameters.getList())
89+
.ifPresent(listConfiguration -> {
90+
Optional.ofNullable(listConfiguration.getAddFirst())
91+
.ifPresent(configuration ->
92+
contextManager.createOrUpdateContextList(contextName, list -> list.addFirst(getPropertiesFromConfiguration(model, configuration))));
93+
Optional.ofNullable(listConfiguration.getAddLast()).ifPresent(configuration ->
94+
contextManager.createOrUpdateContextList(contextName, list -> list.addLast(getPropertiesFromConfiguration(model, configuration))));
95+
}
96+
);
7997
}
8098

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

90108
private String renderTemplate(Object context, String value) {
91109
return templateEngine.getUncachedTemplate(value).apply(context);
92110
}
111+
93112
}

src/main/java/org/wiremock/extensions/state/extensions/StateHandlerbarHelper.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
*/
1616
package org.wiremock.extensions.state.extensions;
1717

18+
import com.fasterxml.jackson.core.JsonParser;
1819
import com.github.jknack.handlebars.Options;
1920
import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.HandlebarsHelper;
21+
import com.jayway.jsonpath.JsonPath;
2022
import org.apache.commons.lang3.StringUtils;
2123
import org.wiremock.extensions.state.internal.ContextManager;
2224

@@ -39,20 +41,39 @@ public StateHandlerbarHelper(ContextManager contextManager) {
3941
public Object apply(Object o, Options options) {
4042
String contextName = options.hash("context");
4143
String property = options.hash("property");
44+
String list = options.hash("list");
4245
if (StringUtils.isEmpty(contextName)) {
4346
return handleError("'context' cannot be empty");
4447
}
45-
if (StringUtils.isEmpty(property)) {
46-
return handleError("'property' cannot be empty");
48+
if (StringUtils.isBlank(property) && StringUtils.isBlank(list)) {
49+
return handleError("Either 'property' or 'list' has to be set");
4750
}
51+
if (StringUtils.isNotBlank(property) && StringUtils.isNotBlank(list)) {
52+
return handleError("Either 'property' or 'list' has to be set");
53+
}
54+
if (StringUtils.isNotBlank(property)) {
55+
return getProperty(contextName, property);
56+
} else {
57+
return getList(contextName, list);
58+
}
59+
}
60+
61+
private Object getProperty(String contextName, String property) {
4862
return contextManager.getContext(contextName)
4963
.map(context -> {
5064
if ("updateCount".equals(property)) {
5165
return context.getUpdateCount();
66+
} else if ("listSize".equals(property)) {
67+
return context.getList().size();
5268
} else {
5369
return context.getProperties().get(property);
5470
}
5571
}
5672
).orElse(handleError(String.format("No state for context %s, property %s found", contextName, property)));
5773
}
74+
private Object getList(String contextName, String list) {
75+
return contextManager.getContext(contextName)
76+
.map(context -> JsonPath.read(context.getList(), list))
77+
.orElse(handleError(String.format("No state for context %s, list %s found", contextName, list)));
78+
}
5879
}

src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package org.wiremock.extensions.state.extensions;
1717

18-
import com.github.tomakehurst.wiremock.core.ConfigurationException;
1918
import com.github.tomakehurst.wiremock.extension.Parameters;
2019
import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel;
2120
import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine;
@@ -25,6 +24,7 @@
2524
import org.wiremock.extensions.state.internal.Context;
2625
import org.wiremock.extensions.state.internal.ContextManager;
2726
import org.wiremock.extensions.state.internal.ContextTemplateModel;
27+
import org.wiremock.extensions.state.internal.StateExtensionMixin;
2828

2929
import java.util.Arrays;
3030
import java.util.HashMap;
@@ -41,7 +41,7 @@
4141
*
4242
* @see org.wiremock.extensions.state.StateExtension
4343
*/
44-
public class StateRequestMatcher extends RequestMatcherExtension {
44+
public class StateRequestMatcher extends RequestMatcherExtension implements StateExtensionMixin {
4545

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

7878
private MatchResult hasContext(Map<String, Object> model, Parameters parameters, String template) {
@@ -114,7 +114,10 @@ String renderTemplate(Object context, String value) {
114114
private enum CountMatcher {
115115
updateCountEqualTo((Context context, Long value) -> context.getUpdateCount().equals(value)),
116116
updateCountLessThan((Context context, Long value) -> context.getUpdateCount() < value),
117-
updateCountMoreThan((Context context, Long value) -> context.getUpdateCount() > value);
117+
updateCountMoreThan((Context context, Long value) -> context.getUpdateCount() > value),
118+
listSizeEqualTo((Context context, Long value) -> context.getList().size() == value),
119+
listSizeLessThan((Context context, Long value) -> context.getList().size() < value),
120+
listSizeMoreThan((Context context, Long value) -> context.getList().size() > value);
118121

119122
private final BiFunction<Context, Long, Boolean> evaluator;
120123

src/main/java/org/wiremock/extensions/state/internal/Context.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717

1818
import java.util.HashMap;
1919
import java.util.LinkedList;
20+
import java.util.List;
2021
import java.util.Map;
2122

2223
public class Context {
2324

2425
private static final int MAX_IDS = 10;
2526
private final String contextName;
2627
private final Map<String, String> properties = new HashMap<>();
28+
private final LinkedList<Map<String, String>> list = new LinkedList<>();
2729
private final LinkedList<String> requests = new LinkedList<>();
2830
private Long updateCount = 1L;
2931
private Long matchCount = 0L;
@@ -66,12 +68,17 @@ public Map<String, String> getProperties() {
6668
return properties;
6769
}
6870

71+
public LinkedList<Map<String, String>> getList() {
72+
return list;
73+
}
74+
6975
@Override
7076
public String toString() {
7177
return "Context{" +
7278
"contextName='" + contextName + '\'' +
73-
", updateCount=" + updateCount +
7479
", properties=" + properties +
80+
", list=" + list +
81+
", updateCount=" + updateCount +
7582
'}';
7683
}
7784
}

src/main/java/org/wiremock/extensions/state/internal/ContextManager.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

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

20+
import java.util.LinkedList;
2021
import java.util.Map;
2122
import java.util.Optional;
23+
import java.util.function.Consumer;
2224

2325
public class ContextManager {
2426

@@ -46,7 +48,7 @@ public void deleteContext(String contextName) {
4648
}
4749
}
4850

49-
public Long createOrUpdateContext(String contextName, Map<String, String> properties) {
51+
public Long createOrUpdateContextState(String contextName, Map<String, String> properties) {
5052
synchronized (store) {
5153
var context = store.get(contextName)
5254
.map(it -> (Context) it)
@@ -60,6 +62,20 @@ public Long createOrUpdateContext(String contextName, Map<String, String> proper
6062
}
6163
}
6264

65+
public Long createOrUpdateContextList(String contextName, Consumer<LinkedList<Map<String, String>>> consumer) {
66+
synchronized (store) {
67+
var context = store.get(contextName)
68+
.map(it -> (Context) it)
69+
.map(it -> {
70+
it.incUpdateCount();
71+
return it;
72+
}).orElseGet(() -> new Context(contextName));
73+
consumer.accept(context.getList());
74+
store.put(contextName, context);
75+
return context.getUpdateCount();
76+
}
77+
}
78+
6379
public Long numUpdates(String contextName) {
6480
synchronized (store) {
6581
return store.get(contextName).map(it -> ((Context) it).getUpdateCount()).orElse(0L);

0 commit comments

Comments
 (0)