Skip to content

Commit 1d6d946

Browse files
authored
Introduce special property updateCount and corresponding matchers (#8)
- adding state context matchers `updateCountEqualTo`, `updateCountLessThan`, `updateCountMoreThan` - extending documentation <!-- Please describe your pull request here. --> ## References - TODO <!-- References to relevant GitHub issues and pull requests, esp. upstream and downstream changes --> ## Submitter checklist - [ ] The PR request is well described and justified, including the body and the references - [ ] The PR title represents the desired changelog entry - [ ] The repository's code style is followed (see the contributing guide) - [ ] Test coverage that demonstrates that the change works as expected - [ ] 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 a5b8a2b commit 1d6d946

File tree

11 files changed

+402
-80
lines changed

11 files changed

+402
-80
lines changed

README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ int expiration=1024;
280280
To have a WireMock stub only apply when there's actually a matching context, you can use the `StateRequestMatcher` . This helps to model different
281281
behavior for requests with and without a matching context. The parameter supports templates.
282282

283-
### Positive match
283+
### Positive context exists match
284284

285285
```json
286286
{
@@ -300,7 +300,37 @@ behavior for requests with and without a matching context. The parameter support
300300
}
301301
```
302302

303-
### Negative match
303+
### Context update count match
304+
305+
Whenever the serve event listener `recordState` is processed, the internal context update counter is increased. The number can be used
306+
for request matching as well. The following matchers are available:
307+
308+
- `updateCountEqualTo`
309+
- `updateCountLessThan`
310+
- `updateCountMoreThan`
311+
312+
As for other matchers, templating is supported.
313+
314+
```json
315+
{
316+
"request": {
317+
"method": "GET",
318+
"urlPattern": "/test/[^\/]+",
319+
"customMatcher": {
320+
"name": "state-matcher",
321+
"parameters": {
322+
"hasContext": "{{request.pathSegments.[1]}}",
323+
"updateCountEqualTo": "1"
324+
}
325+
}
326+
},
327+
"response": {
328+
"status": 200
329+
}
330+
}
331+
```
332+
333+
### Negative context exists match
304334

305335
```json
306336
{
@@ -320,6 +350,8 @@ behavior for requests with and without a matching context. The parameter support
320350
}
321351
```
322352

353+
354+
323355
## Retrieve a state
324356

325357
A state can be retrieved using a handlebar helper. In the example above, the `StateHelper` is registered by the name `state`.
@@ -329,6 +361,8 @@ The handler has two parameters:
329361

330362
- `context`: has to match the context data was registered with
331363
- `property`: the property of the state context to retrieve, so e.g. `firstName`
364+
- `property='updateCount` retrieves the number of updates to a certain state.
365+
The number matches the one described in [Context update count match](#context-update-count-match)
332366

333367
To retrieve a full body, use: `{{{state context=request.pathSegments.[1] property='fullBody'}}}` .
334368

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,17 @@
2020
import com.github.tomakehurst.wiremock.extension.ServeEventListener;
2121
import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel;
2222
import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine;
23-
import com.github.tomakehurst.wiremock.store.Store;
2423
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
2524
import org.apache.commons.lang3.StringUtils;
2625
import org.wiremock.extensions.state.internal.ContextManager;
2726
import org.wiremock.extensions.state.internal.ResponseTemplateModel;
2827

29-
import java.util.Collections;
3028
import java.util.Map;
3129
import java.util.Optional;
3230

3331
/**
3432
* Event listener to trigger state context deletion.
35-
*
33+
* <p>
3634
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
3735
*
3836
* @see org.wiremock.extensions.state.StateExtension

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

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

18-
import org.wiremock.extensions.state.internal.ContextManager;
1918
import com.github.tomakehurst.wiremock.core.ConfigurationException;
2019
import com.github.tomakehurst.wiremock.extension.Parameters;
2120
import com.github.tomakehurst.wiremock.extension.ServeEventListener;
2221
import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel;
2322
import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine;
24-
import com.github.tomakehurst.wiremock.store.Store;
2523
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
2624
import org.apache.commons.lang3.StringUtils;
25+
import org.wiremock.extensions.state.internal.ContextManager;
2726
import org.wiremock.extensions.state.internal.ResponseTemplateModel;
2827

29-
import java.util.Collections;
3028
import java.util.Map;
3129
import java.util.Optional;
3230
import java.util.stream.Collectors;
3331

3432
/**
3533
* Event listener to trigger state context recording.
36-
*
34+
* <p>
3735
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
3836
*
3937
* @see org.wiremock.extensions.state.StateExtension

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@
2020
import org.apache.commons.lang3.StringUtils;
2121
import org.wiremock.extensions.state.internal.ContextManager;
2222

23-
import java.util.Optional;
24-
2523
/**
2624
* Response templating helper to access state.
27-
*
25+
* <p>
2826
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
2927
*
3028
* @see org.wiremock.extensions.state.StateExtension
@@ -39,18 +37,22 @@ public StateHandlerbarHelper(ContextManager contextManager) {
3937

4038
@Override
4139
public Object apply(Object o, Options options) {
42-
String context = options.hash("context");
40+
String contextName = options.hash("context");
4341
String property = options.hash("property");
44-
if (StringUtils.isEmpty(context)) {
45-
return handleError("The context cannot be empty");
42+
if (StringUtils.isEmpty(contextName)) {
43+
return handleError("'context' cannot be empty");
4644
}
4745
if (StringUtils.isEmpty(property)) {
48-
return handleError("The property cannot be empty");
46+
return handleError("'property' cannot be empty");
4947
}
50-
51-
return Optional.ofNullable(contextManager.getState(context, property))
52-
.orElse(handleError(String.format("No state for context %s, property %s found", context, property)));
48+
return contextManager.getContext(contextName)
49+
.map(context -> {
50+
if ("updateCount".equals(property)) {
51+
return context.getUpdateCount();
52+
} else {
53+
return context.getProperties().get(property);
54+
}
55+
}
56+
).orElse(handleError(String.format("No state for context %s, property %s found", contextName, property)));
5357
}
54-
55-
5658
}

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

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,21 @@
2222
import com.github.tomakehurst.wiremock.http.Request;
2323
import com.github.tomakehurst.wiremock.matching.MatchResult;
2424
import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension;
25+
import org.wiremock.extensions.state.internal.Context;
2526
import org.wiremock.extensions.state.internal.ContextManager;
27+
import org.wiremock.extensions.state.internal.ContextTemplateModel;
2628

29+
import java.util.Arrays;
30+
import java.util.HashMap;
31+
import java.util.List;
2732
import java.util.Map;
2833
import java.util.Optional;
34+
import java.util.function.BiFunction;
35+
import java.util.stream.Collectors;
2936

3037
/**
3138
* Request matcher for state.
32-
*
39+
* <p>
3340
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
3441
*
3542
* @see org.wiremock.extensions.state.StateExtension
@@ -44,36 +51,56 @@ public StateRequestMatcher(ContextManager contextManager, TemplateEngine templat
4451
this.templateEngine = templateEngine;
4552
}
4653

54+
private static List<Map.Entry<CountMatcher, Object>> getMatches(Parameters parameters) {
55+
return parameters
56+
.entrySet()
57+
.stream()
58+
.filter(it -> CountMatcher.from(it.getKey()) != null)
59+
.map(it -> Map.entry(CountMatcher.from(it.getKey()), it.getValue()))
60+
.collect(Collectors.toUnmodifiableList());
61+
}
62+
4763
@Override
4864
public String getName() {
4965
return "state-matcher";
5066
}
5167

5268
@Override
5369
public MatchResult match(Request request, Parameters parameters) {
54-
if (parameters.size() != 1) {
55-
throw new ConfigurationException("Parameters should only contain one entry ('hasContext' or 'hasNotContext'");
56-
}
57-
var model = Map.of("request", RequestTemplateModel.from(request));
70+
Map<String, Object> model = new HashMap<>(Map.of("request", RequestTemplateModel.from(request)));
5871
return Optional
5972
.ofNullable(parameters.getString("hasContext", null))
60-
.map(template -> hasContext(model, template))
73+
.map(template -> hasContext(model, parameters, template))
6174
.or(() -> Optional.ofNullable(parameters.getString("hasNotContext", null)).map(template -> hasNotContext(model, template)))
6275
.orElseThrow(() -> new ConfigurationException("Parameters should only contain 'hasContext' or 'hasNotContext'"));
6376
}
6477

65-
private MatchResult hasContext(Map<String, RequestTemplateModel> model, String template) {
66-
var context = renderTemplate(model, template);
67-
if (contextManager.hasContext(context)) {
68-
return MatchResult.exactMatch();
69-
} else {
70-
return MatchResult.noMatch();
71-
}
78+
private MatchResult hasContext(Map<String, Object> model, Parameters parameters, String template) {
79+
return contextManager.getContext(renderTemplate(model, template))
80+
.map(context -> {
81+
List<Map.Entry<CountMatcher, Object>> matchers = getMatches(parameters);
82+
if (matchers.isEmpty()) {
83+
return MatchResult.exactMatch();
84+
} else {
85+
return calculateMatch(model, context, matchers);
86+
}
87+
}).orElseGet(MatchResult::noMatch);
7288
}
7389

74-
private MatchResult hasNotContext(Map<String, RequestTemplateModel> model, String template) {
90+
private MatchResult calculateMatch(Map<String, Object> model, Context context, List<Map.Entry<CountMatcher, Object>> matchers) {
91+
model.put("context", ContextTemplateModel.from(context));
92+
var result = matchers
93+
.stream()
94+
.map(it -> it.getKey().evaluate(context, Long.valueOf(renderTemplate(model, it.getValue().toString()))))
95+
.filter(it -> !it)
96+
.count();
97+
98+
return MatchResult.partialMatch((double) result / matchers.size());
99+
}
100+
101+
private MatchResult hasNotContext(Map<String, Object> model, String template) {
75102
var context = renderTemplate(model, template);
76-
if (!contextManager.hasContext(context)) {
103+
if (contextManager.getContext(context).isEmpty()) {
77104
return MatchResult.exactMatch();
78105
} else {
79106
return MatchResult.noMatch();
@@ -83,4 +110,24 @@ private MatchResult hasNotContext(Map<String, RequestTemplateModel> model, Strin
83110
String renderTemplate(Object context, String value) {
84111
return templateEngine.getUncachedTemplate(value).apply(context);
85112
}
113+
114+
private enum CountMatcher {
115+
updateCountEqualTo((Context context, Long value) -> context.getUpdateCount().equals(value)),
116+
updateCountLessThan((Context context, Long value) -> context.getUpdateCount() < value),
117+
updateCountMoreThan((Context context, Long value) -> context.getUpdateCount() > value);
118+
119+
private final BiFunction<Context, Long, Boolean> evaluator;
120+
121+
CountMatcher(BiFunction<Context, Long, Boolean> evaluator) {
122+
this.evaluator = evaluator;
123+
}
124+
125+
public static CountMatcher from(String from) {
126+
return Arrays.stream(values()).filter(it -> it.name().equals(from)).findFirst().orElse(null);
127+
}
128+
129+
public boolean evaluate(Context context, Long value) {
130+
return this.evaluator.apply(context, value);
131+
}
132+
}
86133
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
/**
2626
* Response template helper provider for state.
27-
*
27+
* <p>
2828
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
2929
*
3030
* @see org.wiremock.extensions.state.StateExtension

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
package org.wiremock.extensions.state.internal;
1717

1818
import java.util.HashMap;
19+
import java.util.LinkedList;
1920
import java.util.Map;
2021

2122
public class Context {
22-
private final String contextName;
23-
private Integer numUpdates = 1;
2423

24+
private static final int MAX_IDS = 10;
25+
private final String contextName;
2526
private final Map<String, String> properties = new HashMap<>();
27+
private final LinkedList<String> requests = new LinkedList<>();
28+
private Long updateCount = 1L;
29+
private Long matchCount = 0L;
2630

2731
public Context(String contextName) {
2832
this.contextName = contextName;
@@ -32,13 +36,30 @@ public String getContextName() {
3236
return contextName;
3337
}
3438

35-
public Integer getNumUpdates() {
36-
return numUpdates;
39+
public Long getUpdateCount() {
40+
return updateCount;
41+
}
42+
43+
public Long getMatchCount() {
44+
return matchCount;
45+
}
46+
47+
public Long incUpdateCount() {
48+
updateCount = updateCount + 1;
49+
return updateCount;
3750
}
3851

39-
public Integer incUpdates() {
40-
numUpdates = numUpdates + 1;
41-
return numUpdates;
52+
public Long incMatchCount(String requestId) {
53+
if (requests.contains(requestId)) {
54+
return matchCount;
55+
} else {
56+
requests.add(requestId);
57+
if (requests.size() > MAX_IDS) {
58+
requests.removeFirst();
59+
}
60+
matchCount = matchCount + 1;
61+
return matchCount;
62+
}
4263
}
4364

4465
public Map<String, String> getProperties() {
@@ -49,7 +70,7 @@ public Map<String, String> getProperties() {
4970
public String toString() {
5071
return "Context{" +
5172
"contextName='" + contextName + '\'' +
52-
", numUpdates=" + numUpdates +
73+
", updateCount=" + updateCount +
5374
", properties=" + properties +
5475
'}';
5576
}

0 commit comments

Comments
 (0)