Skip to content

Commit 107fe5c

Browse files
committed
Adding logical operations and, or, not
1 parent 19b51b5 commit 107fe5c

File tree

13 files changed

+593
-57
lines changed

13 files changed

+593
-57
lines changed

README.md

Lines changed: 180 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,14 @@ dependencies {
190190
### Maven
191191

192192
```xml
193+
193194
<dependencies>
194-
<dependency>
195-
<groupId>org.wiremock.extensions</groupId>
196-
<artifactId>wiremock-state-extension</artifactId>
197-
<version>your-version</version>
198-
<scope>test</scope>
199-
</dependency>
195+
<dependency>
196+
<groupId>org.wiremock.extensions</groupId>
197+
<artifactId>wiremock-state-extension</artifactId>
198+
<version>your-version</version>
199+
<scope>test</scope>
200+
</dependency>
200201
</dependencies>
201202
```
202203

@@ -225,7 +226,7 @@ dependencies {
225226
```
226227

227228
</details>
228-
229+
229230
<details>
230231
<summary>
231232
Use GitHub Packages in Maven
@@ -590,7 +591,7 @@ An invalid regex results in an exception. If there are no matches, this is silen
590591
- `context` (string): the context to delete the list entry from
591592
- `list` (dictionary, see next column)
592593

593-
If `list` is specified and `context` is missing, an error is thrown.
594+
If `list` is specified and `context` is missing, an error is thrown.
594595
</td>
595596
<td>
596597
Dictionary - only one option is interpreted (top to bottom as listed here)
@@ -838,7 +839,7 @@ reported or logged.
838839

839840
### List size match
840841

841-
The list size (which is modified via `recordState` or `deleteState`) can be used
842+
The list size (which is modified via `recordState` or `deleteState`) can be used
842843
for request matching as well. The following matchers are available:
843844

844845
- `listSizeEqualTo`
@@ -874,14 +875,14 @@ The basic syntax:
874875

875876
```json
876877
"list": {
877-
<index-a>: {
878-
<property-a>: <matcher-a>,
879-
<property-b>: <matcher-b>
880-
},
881-
<index-b>: {
882-
<property-a>: <matcher-a>,
883-
<property-b>: <matcher-b>
884-
}
878+
<index-a>: {
879+
<property-a>: <matcher-a>,
880+
<property-b>: <matcher-b>
881+
},
882+
<index-b>: {
883+
<property-a>: <matcher-a>,
884+
<property-b>: <matcher-b>
885+
}
885886
}
886887
```
887888

@@ -917,7 +918,6 @@ The implementation makes use of WireMock's internal matching system and supports
917918
`after`,`equalToDateTime`,`anything`,`absent`,`and`,`or`,`matchesPathTemplate`.
918919
For documentation on using these matchers, check the [WireMock documentation](https://wiremock.org/docs/request-matching/)
919920

920-
921921
### Negative context exists match
922922

923923
```json
@@ -938,6 +938,120 @@ For documentation on using these matchers, check the [WireMock documentation](ht
938938
}
939939
```
940940

941+
### Logical matches
942+
943+
This extension supports logical matches. The following matchers are available:
944+
945+
- `and`
946+
- `or`
947+
- `not`
948+
949+
The matchers can be combined with `hasContext` and `hasNotContext` as well was themselves, so you can create a fully nested structure.
950+
951+
Example for `not`:
952+
953+
```json
954+
{
955+
"request": {
956+
"urlPathPattern": "/test/[^\/]+",
957+
"method": "GET",
958+
"customMatcher": {
959+
"name": "state-matcher",
960+
"parameters": {
961+
"not": {
962+
"hasContext": "{{request.pathSegments.[1]}}"
963+
}
964+
}
965+
}
966+
},
967+
"response": {
968+
"status": 400
969+
}
970+
}
971+
```
972+
973+
Example for `and`:
974+
975+
```json
976+
{
977+
"request": {
978+
"urlPathPattern": "/test/[^\/]+",
979+
"method": "GET",
980+
"customMatcher": {
981+
"name": "state-matcher",
982+
"parameters": {
983+
"and": [
984+
{
985+
"hasContext": "{{request.pathSegments.[1]}}"
986+
},
987+
{
988+
"hasNotContext": "anotherContext"
989+
}
990+
]
991+
}
992+
}
993+
},
994+
"response": {
995+
"status": 200
996+
}
997+
}
998+
```
999+
1000+
Example for `or`:
1001+
1002+
```json
1003+
{
1004+
"request": {
1005+
"urlPathPattern": "/test/[^\/]+",
1006+
"method": "GET",
1007+
"customMatcher": {
1008+
"name": "state-matcher",
1009+
"parameters": {
1010+
"or": [
1011+
{
1012+
"hasContext": "{{request.pathSegments.[1]}}"
1013+
},
1014+
{
1015+
"hasContext": "otherContext"
1016+
}
1017+
]
1018+
}
1019+
}
1020+
},
1021+
"response": {
1022+
"status": 200
1023+
}
1024+
}
1025+
```
1026+
1027+
Example for nesting:
1028+
1029+
```json
1030+
{
1031+
"request": {
1032+
"urlPathPattern": "/test/[^\/]+",
1033+
"method": "GET",
1034+
"customMatcher": {
1035+
"name": "state-matcher",
1036+
"parameters": {
1037+
"not": {
1038+
"or": [
1039+
{
1040+
"hasContext": "eitherContext"
1041+
},
1042+
{
1043+
"hasContext": "orContext"
1044+
}
1045+
]
1046+
}
1047+
}
1048+
},
1049+
"response": {
1050+
"status": 400
1051+
}
1052+
}
1053+
```
1054+
9411055
## Retrieve a state
9421056

9431057
A state can be retrieved using a handlebar helper. In the example above, the `StateHelper` is registered by the name `state`.
@@ -1016,13 +1130,50 @@ Example with bodyFileName:
10161130

10171131
```json
10181132
[
1019-
{{# each (state context='list' property='list' default='[]') }}
10201133
{
1021-
"id": {{id}},
1022-
"firstName": "{{firstName}}",
1023-
"lastName": "{{lastName}}"
1024-
}{{#unless @last}},{{/unless}}
1025-
{{/each}}
1134+
{
1135+
#
1136+
each
1137+
(state
1138+
context=
1139+
'list'
1140+
property=
1141+
'list'
1142+
default=
1143+
'[]'
1144+
)
1145+
}
1146+
}
1147+
{
1148+
"id": {
1149+
{
1150+
id
1151+
}
1152+
},
1153+
"firstName"
1154+
:
1155+
"{{firstName}}",
1156+
"lastName"
1157+
:
1158+
"{{lastName}}"
1159+
}
1160+
{
1161+
{
1162+
#
1163+
unless
1164+
@last
1165+
}
1166+
},
1167+
{
1168+
{
1169+
/unless
1170+
}
1171+
}
1172+
{
1173+
{
1174+
/each
1175+
}
1176+
}
10261177
]
10271178
```
10281179

@@ -1080,11 +1231,13 @@ This extension is at the moment not optimized for distributed setups or high deg
10801231
that should be held into account:
10811232

10821233
- The store used for storing the state is on instance-level only
1083-
- while it can be exchanged for a distributed store, any atomicity assurance on instance level is not replicated to the distributed setup. Thus concurrent operations on different instances might result in state overwrites
1234+
- while it can be exchanged for a distributed store, any atomicity assurance on instance level is not replicated to the distributed setup. Thus concurrent
1235+
operations on different instances might result in state overwrites
10841236
- Lock-level is basically the whole context store
1085-
- while the lock time is kept small, this can still impact measurements when being used in load tests
1237+
- while the lock time is kept small, this can still impact measurements when being used in load tests
10861238
- Single updates to contexts (property additions or changes, list entry additions or deletions) are atomic on instance level
1087-
- Concurrent requests are currently allowed to change the same context. Atomicity prevents overwrites but does not provide something like a transaction, so: the context can change while a request is performed
1239+
- Concurrent requests are currently allowed to change the same context. Atomicity prevents overwrites but does not provide something like a transaction, so: the
1240+
context can change while a request is performed
10881241

10891242
For any kind of usage with parallel write requests, it's recommended to use a different context for each parallel stream.
10901243

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ group 'org.wiremock.extensions'
1414
project.ext {
1515
versions = [
1616
caffeine : '3.2.2',
17-
handlebars : '4.5.0',
18-
wiremock : '3.13.1'
17+
handlebars: '4.3.1',
18+
wiremock : '3.13.1'
1919
]
2020
}
2121

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import com.github.tomakehurst.wiremock.store.Store;
2222
import org.wiremock.extensions.state.extensions.DeleteStateEventListener;
2323
import org.wiremock.extensions.state.extensions.RecordStateEventListener;
24-
import org.wiremock.extensions.state.extensions.StateRequestMatcher;
24+
import org.wiremock.extensions.state.extensions.requestmatcher.StateRequestMatcher;
2525
import org.wiremock.extensions.state.extensions.StateTemplateHelperProviderExtension;
2626
import org.wiremock.extensions.state.extensions.TransactionEventListener;
2727
import org.wiremock.extensions.state.internal.ContextManager;

src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java renamed to src/main/java/org/wiremock/extensions/state/extensions/requestmatcher/StateRequestMatcher.java

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,23 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.wiremock.extensions.state.extensions;
16+
package org.wiremock.extensions.state.extensions.requestmatcher;
1717

1818
import com.github.tomakehurst.wiremock.common.Json;
1919
import com.github.tomakehurst.wiremock.core.ConfigurationException;
2020
import com.github.tomakehurst.wiremock.extension.Parameters;
2121
import com.github.tomakehurst.wiremock.extension.WireMockServices;
22-
import com.github.tomakehurst.wiremock.extension.responsetemplating.RequestTemplateModel;
2322
import com.github.tomakehurst.wiremock.http.Request;
2423
import com.github.tomakehurst.wiremock.matching.MatchResult;
2524
import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension;
2625
import com.github.tomakehurst.wiremock.matching.StringValuePattern;
26+
import org.wiremock.extensions.state.extensions.requestmatcher.model.And;
27+
import org.wiremock.extensions.state.extensions.requestmatcher.model.BaseContextMatcher;
28+
import org.wiremock.extensions.state.extensions.requestmatcher.model.BaseRequestMatcher;
29+
import org.wiremock.extensions.state.extensions.requestmatcher.model.HasContext;
30+
import org.wiremock.extensions.state.extensions.requestmatcher.model.HasNotContext;
31+
import org.wiremock.extensions.state.extensions.requestmatcher.model.Not;
32+
import org.wiremock.extensions.state.extensions.requestmatcher.model.Or;
2733
import org.wiremock.extensions.state.internal.ContextManager;
2834
import org.wiremock.extensions.state.internal.StateExtensionMixin;
2935
import org.wiremock.extensions.state.internal.model.Context;
@@ -34,7 +40,6 @@
3440
import java.util.HashMap;
3541
import java.util.List;
3642
import java.util.Map;
37-
import java.util.Optional;
3843
import java.util.function.BiFunction;
3944
import java.util.stream.Collectors;
4045

@@ -98,11 +103,55 @@ public String getName() {
98103
@Override
99104
public MatchResult match(Request request, Parameters parameters) {
100105
var model = wireMockServices.getTemplateEngine().buildModelForRequest(request);
101-
return Optional
102-
.ofNullable(parameters.getString("hasContext", null))
103-
.map(template -> hasContext(model, parameters, template))
104-
.or(() -> Optional.ofNullable(parameters.getString("hasNotContext", null)).map(template -> hasNotContext(model, template)))
105-
.orElseThrow(() -> createConfigurationError("Parameters should only contain 'hasContext' or 'hasNotContext'"));
106+
try {
107+
var matcher = Json.mapToObject(parameters, BaseRequestMatcher.class);
108+
var validationMessage = matcher.assertValid();
109+
if (validationMessage != null) {
110+
throw createConfigurationError(validationMessage);
111+
}
112+
return matchContext(model, parameters, matcher);
113+
114+
} catch (IllegalArgumentException ex) {
115+
throw createConfigurationError("You have to specify 'hasContext' or 'hasNotContext'");
116+
}
117+
}
118+
119+
private MatchResult matchContext(Map<String, Object> model, Parameters parameters, BaseRequestMatcher matcher) {
120+
if (matcher instanceof BaseContextMatcher) {
121+
return matchContext(model, parameters, (BaseContextMatcher) matcher);
122+
} else if (matcher instanceof Not) {
123+
var matchResult = matchContext(model, parameters, ((Not) matcher).getBaseRequestMatcher());
124+
return MatchResult.partialMatch(1.0 - matchResult.getDistance());
125+
} else if (matcher instanceof And) {
126+
var containedMatcher = ((And) matcher).getBaseRequestMatcher();
127+
var matchResults = containedMatcher
128+
.stream()
129+
.map(it -> matchContext(model, parameters, it))
130+
.collect(Collectors.toList());
131+
return MatchResult.aggregate(matchResults);
132+
} else if (matcher instanceof Or) {
133+
var containedMatcher = ((Or) matcher).getBaseRequestMatcher();
134+
var matchResults = containedMatcher
135+
.stream()
136+
.map(it -> matchContext(model, parameters, it))
137+
.filter(MatchResult::isExactMatch)
138+
.collect(Collectors.toList());
139+
return matchResults.stream().findFirst().orElseGet(MatchResult::noMatch);
140+
} else {
141+
throw createConfigurationError("invalid request matcher configuration");
142+
}
143+
}
144+
145+
private MatchResult matchContext(Map<String, Object> model, Parameters parameters, BaseContextMatcher matcher) {
146+
var template = matcher.getContextTemplate();
147+
if (matcher instanceof HasContext) {
148+
return hasContext(model, parameters, template);
149+
} else if (matcher instanceof HasNotContext) {
150+
return hasNotContext(model, template);
151+
} else {
152+
throw createConfigurationError("invalid request matcher configuration");
153+
}
154+
106155
}
107156

108157
private MatchResult hasContext(Map<String, Object> model, Parameters parameters, String template) {

0 commit comments

Comments
 (0)