Skip to content

Commit

Permalink
Introduce special property updateCount and corresponding matchers (#8)
Browse files Browse the repository at this point in the history
- 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
-->
  • Loading branch information
dirkbolte authored Jul 18, 2023
1 parent a5b8a2b commit 1d6d946
Show file tree
Hide file tree
Showing 11 changed files with 402 additions and 80 deletions.
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ int expiration=1024;
To have a WireMock stub only apply when there's actually a matching context, you can use the `StateRequestMatcher` . This helps to model different
behavior for requests with and without a matching context. The parameter supports templates.

### Positive match
### Positive context exists match

```json
{
Expand All @@ -300,7 +300,37 @@ behavior for requests with and without a matching context. The parameter support
}
```

### Negative match
### Context update count match

Whenever the serve event listener `recordState` is processed, the internal context update counter is increased. The number can be used
for request matching as well. The following matchers are available:

- `updateCountEqualTo`
- `updateCountLessThan`
- `updateCountMoreThan`

As for other matchers, templating is supported.

```json
{
"request": {
"method": "GET",
"urlPattern": "/test/[^\/]+",
"customMatcher": {
"name": "state-matcher",
"parameters": {
"hasContext": "{{request.pathSegments.[1]}}",
"updateCountEqualTo": "1"
}
}
},
"response": {
"status": 200
}
}
```

### Negative context exists match

```json
{
Expand All @@ -320,6 +350,8 @@ behavior for requests with and without a matching context. The parameter support
}
```



## Retrieve a state

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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,17 @@
import com.github.tomakehurst.wiremock.extension.ServeEventListener;
import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel;
import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine;
import com.github.tomakehurst.wiremock.store.Store;
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.ResponseTemplateModel;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;

/**
* Event listener to trigger state context deletion.
*
* <p>
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
*
* @see org.wiremock.extensions.state.StateExtension
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,23 @@
*/
package org.wiremock.extensions.state.extensions;

import org.wiremock.extensions.state.internal.ContextManager;
import com.github.tomakehurst.wiremock.core.ConfigurationException;
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.extension.ServeEventListener;
import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel;
import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine;
import com.github.tomakehurst.wiremock.store.Store;
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.ResponseTemplateModel;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

/**
* Event listener to trigger state context recording.
*
* <p>
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
*
* @see org.wiremock.extensions.state.StateExtension
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@
import org.apache.commons.lang3.StringUtils;
import org.wiremock.extensions.state.internal.ContextManager;

import java.util.Optional;

/**
* Response templating helper to access state.
*
* <p>
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
*
* @see org.wiremock.extensions.state.StateExtension
Expand All @@ -39,18 +37,22 @@ public StateHandlerbarHelper(ContextManager contextManager) {

@Override
public Object apply(Object o, Options options) {
String context = options.hash("context");
String contextName = options.hash("context");
String property = options.hash("property");
if (StringUtils.isEmpty(context)) {
return handleError("The context cannot be empty");
if (StringUtils.isEmpty(contextName)) {
return handleError("'context' cannot be empty");
}
if (StringUtils.isEmpty(property)) {
return handleError("The property cannot be empty");
return handleError("'property' cannot be empty");
}

return Optional.ofNullable(contextManager.getState(context, property))
.orElse(handleError(String.format("No state for context %s, property %s found", context, property)));
return contextManager.getContext(contextName)
.map(context -> {
if ("updateCount".equals(property)) {
return context.getUpdateCount();
} else {
return context.getProperties().get(property);
}
}
).orElse(handleError(String.format("No state for context %s, property %s found", contextName, property)));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@
import com.github.tomakehurst.wiremock.http.Request;
import com.github.tomakehurst.wiremock.matching.MatchResult;
import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension;
import org.wiremock.extensions.state.internal.Context;
import org.wiremock.extensions.state.internal.ContextManager;
import org.wiremock.extensions.state.internal.ContextTemplateModel;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

/**
* Request matcher for state.
*
* <p>
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
*
* @see org.wiremock.extensions.state.StateExtension
Expand All @@ -44,36 +51,56 @@ public StateRequestMatcher(ContextManager contextManager, TemplateEngine templat
this.templateEngine = templateEngine;
}

private static List<Map.Entry<CountMatcher, Object>> getMatches(Parameters parameters) {
return parameters
.entrySet()
.stream()
.filter(it -> CountMatcher.from(it.getKey()) != null)
.map(it -> Map.entry(CountMatcher.from(it.getKey()), it.getValue()))
.collect(Collectors.toUnmodifiableList());
}

@Override
public String getName() {
return "state-matcher";
}

@Override
public MatchResult match(Request request, Parameters parameters) {
if (parameters.size() != 1) {
throw new ConfigurationException("Parameters should only contain one entry ('hasContext' or 'hasNotContext'");
}
var model = Map.of("request", RequestTemplateModel.from(request));
Map<String, Object> model = new HashMap<>(Map.of("request", RequestTemplateModel.from(request)));
return Optional
.ofNullable(parameters.getString("hasContext", null))
.map(template -> hasContext(model, template))
.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'"));
}

private MatchResult hasContext(Map<String, RequestTemplateModel> model, String template) {
var context = renderTemplate(model, template);
if (contextManager.hasContext(context)) {
return MatchResult.exactMatch();
} else {
return MatchResult.noMatch();
}
private MatchResult hasContext(Map<String, Object> model, Parameters parameters, String template) {
return contextManager.getContext(renderTemplate(model, template))
.map(context -> {
List<Map.Entry<CountMatcher, Object>> matchers = getMatches(parameters);
if (matchers.isEmpty()) {
return MatchResult.exactMatch();
} else {
return calculateMatch(model, context, matchers);
}
}).orElseGet(MatchResult::noMatch);
}

private MatchResult hasNotContext(Map<String, RequestTemplateModel> model, String template) {
private MatchResult calculateMatch(Map<String, Object> model, Context context, List<Map.Entry<CountMatcher, Object>> matchers) {
model.put("context", ContextTemplateModel.from(context));
var result = matchers
.stream()
.map(it -> it.getKey().evaluate(context, Long.valueOf(renderTemplate(model, it.getValue().toString()))))
.filter(it -> !it)
.count();

return MatchResult.partialMatch((double) result / matchers.size());
}

private MatchResult hasNotContext(Map<String, Object> model, String template) {
var context = renderTemplate(model, template);
if (!contextManager.hasContext(context)) {
if (contextManager.getContext(context).isEmpty()) {
return MatchResult.exactMatch();
} else {
return MatchResult.noMatch();
Expand All @@ -83,4 +110,24 @@ private MatchResult hasNotContext(Map<String, RequestTemplateModel> model, Strin
String renderTemplate(Object context, String value) {
return templateEngine.getUncachedTemplate(value).apply(context);
}

private enum CountMatcher {
updateCountEqualTo((Context context, Long value) -> context.getUpdateCount().equals(value)),
updateCountLessThan((Context context, Long value) -> context.getUpdateCount() < value),
updateCountMoreThan((Context context, Long value) -> context.getUpdateCount() > value);

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

CountMatcher(BiFunction<Context, Long, Boolean> evaluator) {
this.evaluator = evaluator;
}

public static CountMatcher from(String from) {
return Arrays.stream(values()).filter(it -> it.name().equals(from)).findFirst().orElse(null);
}

public boolean evaluate(Context context, Long value) {
return this.evaluator.apply(context, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

/**
* Response template helper provider for state.
*
* <p>
* DO NOT REGISTER directly. Use {@link org.wiremock.extensions.state.StateExtension} instead.
*
* @see org.wiremock.extensions.state.StateExtension
Expand Down
37 changes: 29 additions & 8 deletions src/main/java/org/wiremock/extensions/state/internal/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
package org.wiremock.extensions.state.internal;

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

public class Context {
private final String contextName;
private Integer numUpdates = 1;

private static final int MAX_IDS = 10;
private final String contextName;
private final Map<String, String> properties = new HashMap<>();
private final LinkedList<String> requests = new LinkedList<>();
private Long updateCount = 1L;
private Long matchCount = 0L;

public Context(String contextName) {
this.contextName = contextName;
Expand All @@ -32,13 +36,30 @@ public String getContextName() {
return contextName;
}

public Integer getNumUpdates() {
return numUpdates;
public Long getUpdateCount() {
return updateCount;
}

public Long getMatchCount() {
return matchCount;
}

public Long incUpdateCount() {
updateCount = updateCount + 1;
return updateCount;
}

public Integer incUpdates() {
numUpdates = numUpdates + 1;
return numUpdates;
public Long incMatchCount(String requestId) {
if (requests.contains(requestId)) {
return matchCount;
} else {
requests.add(requestId);
if (requests.size() > MAX_IDS) {
requests.removeFirst();
}
matchCount = matchCount + 1;
return matchCount;
}
}

public Map<String, String> getProperties() {
Expand All @@ -49,7 +70,7 @@ public Map<String, String> getProperties() {
public String toString() {
return "Context{" +
"contextName='" + contextName + '\'' +
", numUpdates=" + numUpdates +
", updateCount=" + updateCount +
", properties=" + properties +
'}';
}
Expand Down
Loading

0 comments on commit 1d6d946

Please sign in to comment.