From cc706e348f86c624d3875f3466bce69a5a082bf2 Mon Sep 17 00:00:00 2001 From: Dirk Bolte Date: Fri, 29 Mar 2024 09:07:21 +0100 Subject: [PATCH] feat: support for DSL-like configuration specification --- README.md | 2 +- build.gradle | 17 ++ settings.gradle | 3 + .../extensions/DeleteStateEventListener.java | 4 +- .../extensions/state/extensions/Dsl.java | 45 +++ .../state/extensions/StateRequestMatcher.java | 269 ++++++++++-------- .../DeleteStateEventListenerBuilder.java | 84 ++++++ .../builder/EventListenerBuilder.java | 16 ++ .../RecordStateEventListenerBuilder.java | 74 +++++ .../builder/StateRequestMatcherBuilder.java | 45 +++ .../state/internal/ContextManager.java | 4 +- .../internal/api/DeleteStateParameters.java | 7 + .../api/StateRequestMatcherParameters.java | 191 +++++++++++++ .../state/internal/model/Context.java | 6 +- .../internal/model/ContextTemplateModel.java | 3 +- .../DeleteStateEventListenerTest.java | 23 ++ .../StateRequestMatcherTest.java | 28 +- .../extensions/state/dsl/KotlinDslTest.kt | 24 ++ 18 files changed, 695 insertions(+), 150 deletions(-) create mode 100644 settings.gradle create mode 100644 src/main/java/org/wiremock/extensions/state/extensions/Dsl.java create mode 100644 src/main/java/org/wiremock/extensions/state/extensions/builder/DeleteStateEventListenerBuilder.java create mode 100644 src/main/java/org/wiremock/extensions/state/extensions/builder/EventListenerBuilder.java create mode 100644 src/main/java/org/wiremock/extensions/state/extensions/builder/RecordStateEventListenerBuilder.java create mode 100644 src/main/java/org/wiremock/extensions/state/extensions/builder/StateRequestMatcherBuilder.java create mode 100644 src/main/java/org/wiremock/extensions/state/internal/api/StateRequestMatcherParameters.java create mode 100644 src/test/kotlin/org/wiremock/extensions/state/dsl/KotlinDslTest.kt diff --git a/README.md b/README.md index 5fdbb6b..bba9630 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ The state is recorded in `serveEventListeners` of a stub. The following function - to delete a selective property, set it to `null` (as string). - `list` : stores a state in a list. Can be used to prepend/append new states to an existing list. List elements cannot be modified (only read/deleted). -`state` and `list` can be used in the same `ServeEventListener` (would count as ONE updates). Adding multiple `recordState` `ServeEventListener` is supported. +`state` and `list` can be used in the same `ServeEventListener` (would count as ONE update). Adding multiple `recordState` `ServeEventListener` is supported. The following parameters have to be provided: diff --git a/build.gradle b/build.gradle index 3f537fb..f6b439d 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ plugins { id 'jacoco' id 'com.diffplug.spotless' version '6.25.0' id 'org.wiremock.tools.gradle.wiremock-extension-convention' version '0.1.2' + id 'org.jetbrains.kotlin.jvm' version '1.9.22' } group 'org.wiremock.extensions' @@ -17,8 +18,15 @@ project.ext { ] } + +sourceSets { + test.java.srcDirs += 'src/test/java' + test.kotlin.srcDirs += 'src/test/kotlin' +} + dependencies { implementation("com.github.ben-manes.caffeine:caffeine:${versions.caffeine}") + implementation("org.wiremock:wiremock:3.3.3") implementation("com.github.jknack:handlebars-helpers:${versions.handlebars}") { exclude group: 'org.mozilla', module: 'rhino' } @@ -32,6 +40,15 @@ shadowJar { test { finalizedBy jacocoTestReport } + +kotlin { + jvmToolchain { + languageVersion = JavaLanguageVersion.of(11) + implementation = JvmImplementation.J9 + } +} + + jacocoTestReport { dependsOn test reports { diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..a9d2102 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} diff --git a/src/main/java/org/wiremock/extensions/state/extensions/DeleteStateEventListener.java b/src/main/java/org/wiremock/extensions/state/extensions/DeleteStateEventListener.java index dfcc1ec..8f7eca2 100644 --- a/src/main/java/org/wiremock/extensions/state/extensions/DeleteStateEventListener.java +++ b/src/main/java/org/wiremock/extensions/state/extensions/DeleteStateEventListener.java @@ -47,6 +47,8 @@ */ public class DeleteStateEventListener implements ServeEventListener, StateExtensionMixin { + public final static String NAME = "deleteState"; + private final TemplateEngine templateEngine; private final ContextManager contextManager; @@ -58,7 +60,7 @@ public DeleteStateEventListener(ContextManager contextManager, TemplateEngine te @Override public String getName() { - return "deleteState"; + return NAME; } @Override diff --git a/src/main/java/org/wiremock/extensions/state/extensions/Dsl.java b/src/main/java/org/wiremock/extensions/state/extensions/Dsl.java new file mode 100644 index 0000000..ed9bb1e --- /dev/null +++ b/src/main/java/org/wiremock/extensions/state/extensions/Dsl.java @@ -0,0 +1,45 @@ +package org.wiremock.extensions.state.extensions; + + +import org.jetbrains.annotations.NotNull; +import org.wiremock.extensions.state.extensions.builder.DeleteStateEventListenerBuilder; +import org.wiremock.extensions.state.extensions.builder.DeleteStateEventListenerBuilder.MultiContextBuilder; +import org.wiremock.extensions.state.extensions.builder.DeleteStateEventListenerBuilder.SingleContextBuilder; +import org.wiremock.extensions.state.extensions.builder.RecordStateEventListenerBuilder; +import org.wiremock.extensions.state.extensions.builder.StateRequestMatcherBuilder; +import org.wiremock.extensions.state.extensions.builder.StateRequestMatcherBuilder.HasContextBuilder; +import org.wiremock.extensions.state.extensions.builder.StateRequestMatcherBuilder.HasNotContextBuilder; + +import java.util.Arrays; +import java.util.Collection; + +public class Dsl { + + public static @NotNull RecordStateEventListenerBuilder recordContext(@NotNull String context) { + return RecordStateEventListenerBuilder.context(context); + } + + public static @NotNull SingleContextBuilder deleteContext(@NotNull String context) { + return DeleteStateEventListenerBuilder.context(context); + } + + public static @NotNull MultiContextBuilder deleteContexts(@NotNull String... contexts) { + return DeleteStateEventListenerBuilder.contexts(Arrays.asList(contexts)); + } + + public static @NotNull MultiContextBuilder deleteContexts(@NotNull Collection contexts) { + return DeleteStateEventListenerBuilder.contexts(contexts); + } + + public static @NotNull MultiContextBuilder deleteContextsMatching(@NotNull String contextMatching) { + return DeleteStateEventListenerBuilder.contextsMatching(contextMatching); + } + + public static @NotNull HasContextBuilder hasContext(@NotNull String context) { + return StateRequestMatcherBuilder.hasContext(context); + } + + public static @NotNull HasNotContextBuilder hasNotContext(@NotNull String context) { + return StateRequestMatcherBuilder.hasNotContext(context); + } +} diff --git a/src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java b/src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java index ba8648b..0c0bca8 100644 --- a/src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java +++ b/src/main/java/org/wiremock/extensions/state/extensions/StateRequestMatcher.java @@ -24,18 +24,25 @@ import com.github.tomakehurst.wiremock.matching.MatchResult; import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension; import com.github.tomakehurst.wiremock.matching.StringValuePattern; +import org.jetbrains.annotations.NotNull; import org.wiremock.extensions.state.internal.ContextManager; import org.wiremock.extensions.state.internal.StateExtensionMixin; +import org.wiremock.extensions.state.internal.api.StateRequestMatcherParameters; +import org.wiremock.extensions.state.internal.api.StateRequestMatcherParameters.HasContext; +import org.wiremock.extensions.state.internal.api.StateRequestMatcherParameters.HasNotContext; import org.wiremock.extensions.state.internal.model.Context; import org.wiremock.extensions.state.internal.model.ContextTemplateModel; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Objects; import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier; @@ -58,18 +65,13 @@ public StateRequestMatcher(ContextManager contextManager, TemplateEngine templat this.templateEngine = templateEngine; } - private static List> getMatchers(Parameters parameters) { - return parameters - .entrySet() - .stream() - .filter(it -> ContextMatcher.from(it.getKey()) != null) - .map(it -> Map.entry(ContextMatcher.from(it.getKey()), it.getValue())) - .collect(Collectors.toUnmodifiableList()); + private static List> getMatchers(HasContext parameters) { + return ContextMatcher.from(parameters); } - private static T mapToObject(Map map, Class klass) { + private static StringValuePattern mapToPatternMatcher(Map map) { try { - return Json.mapToObject(map, klass); + return Json.mapToObject(map, StringValuePattern.class); } catch (Exception ex) { var msg = String.format("Cannot create pattern matcher: %s", ex.getMessage()); var prefixed = String.format("%s: %s", "StateRequestMatcher", msg); @@ -80,7 +82,6 @@ private static T mapToObject(Map map, Class klass) { private static T cast(Object object, Class target) { try { - //noinspection unchecked return target.cast(object); } catch (ClassCastException ex) { var msg = String.format("Configuration has invalid type: %s", ex.getMessage()); @@ -97,39 +98,43 @@ public String getName() { @Override public MatchResult match(Request request, Parameters parameters) { - Map model = new HashMap<>(Map.of("request", RequestTemplateModel.from(request))); - return Optional - .ofNullable(parameters.getString("hasContext", null)) - .map(template -> hasContext(model, parameters, template)) - .or(() -> Optional.ofNullable(parameters.getString("hasNotContext", null)).map(template -> hasNotContext(model, template))) - .orElseThrow(() -> createConfigurationError("Parameters should only contain 'hasContext' or 'hasNotContext'")); + var parsedParameters = Json.mapToObject(parameters, StateRequestMatcherParameters.class); + var model = new HashMap(Map.of("request", RequestTemplateModel.from(request))); + if (parsedParameters instanceof HasContext) { + return hasContext((HasContext) parsedParameters, model); + } else if (parsedParameters instanceof HasNotContext) { + return hasNotContext((HasNotContext) parsedParameters, model); + } else { + throw createConfigurationError("Parameters should only contain 'hasContext' or 'hasNotContext'"); + } } - private MatchResult hasContext(Map model, Parameters parameters, String template) { - return contextManager.getContextCopy(renderTemplate(model, template)) + private MatchResult hasContext(HasContext parameters, Map model) { + return contextManager.getContextCopy(renderTemplate(model, parameters.getHasContext())) .map(context -> { - List> matchers = getMatchers(parameters); + model.put("context", ContextTemplateModel.from(context)); + @SuppressWarnings("unchecked") HasContext renderedParameters = Json.mapToObject((Map) renderTemplateRecursively(model, Json.objectToMap(parameters)), HasContext.class); + List> matchers = getMatchers(renderedParameters); if (matchers.isEmpty()) { logger().info(context, "hasContext matched"); return MatchResult.exactMatch(); } else { - return calculateMatch(model, context, matchers); + return calculateMatch(context, matchers); } }).orElseGet(MatchResult::noMatch); } - private MatchResult calculateMatch(Map model, Context context, List> matchers) { - model.put("context", ContextTemplateModel.from(context)); + private MatchResult calculateMatch(Context context, List> matchers) { var results = matchers .stream() - .map(it -> it.getKey().evaluate(context, renderTemplateRecursively(model, it.getValue()))) + .map(it -> it.getKey().evaluate(context, it.getValue())) .collect(Collectors.toList()); return MatchResult.aggregate(results); } - private MatchResult hasNotContext(Map model, String template) { - var context = renderTemplate(model, template); + private MatchResult hasNotContext(HasNotContext parameters, Map model) { + var context = renderTemplate(model, parameters.getHasNotContext()); if (contextManager.getContextCopy(context).isEmpty()) { logger().info(context, "hasNotContext matched"); return MatchResult.exactMatch(); @@ -144,11 +149,11 @@ String renderTemplate(Object context, String value) { Object renderTemplateRecursively(Object context, Object value) { if (value instanceof Collection) { - Collection castedCollection = cast(value, Collection.class); + @SuppressWarnings("unchecked") Collection castedCollection = cast(value, Collection.class); return castedCollection.stream().map(it -> renderTemplateRecursively(context, it)).collect(Collectors.toList()); } else if (value instanceof Map) { var newMap = new HashMap(); - Map castedMap = cast(value, Map.class); + @SuppressWarnings("unchecked") Map castedMap = cast(value, Map.class); castedMap.forEach((k, v) -> newMap.put( renderTemplate(context, k), renderTemplateRecursively(context, v) @@ -161,117 +166,129 @@ Object renderTemplateRecursively(Object context, Object value) { private enum ContextMatcher { - property((Context c, Object object) -> { - Map> mapValue = cast(object, Map.class); - var results = mapValue.entrySet().stream().map(entry -> { - var patterns = mapToObject(entry.getValue(), StringValuePattern.class); - var propertyValue = c.getProperties().get(entry.getKey()); - return patterns.match(propertyValue); - }).collect(Collectors.toList()); - if (results.isEmpty()) { - logger().info(c, "No interpretable matcher was found, defaulting to 'exactMatch'"); - return MatchResult.exactMatch(); - } else { - return MatchResult.aggregate(results); - } - }), + property( + HasContext::getProperty, + (Context c, Object object) -> { + @SuppressWarnings("unchecked") Map> mapValue = cast(object, Map.class); + var results = mapValue.entrySet().stream().map(entry -> { + var patterns = mapToPatternMatcher(entry.getValue()); + var propertyValue = c.getProperties().get(entry.getKey()); + return patterns.match(propertyValue); + }).collect(Collectors.toList()); + if (results.isEmpty()) { + logger().info(c, "No interpretable matcher was found, defaulting to 'exactMatch'"); + return MatchResult.exactMatch(); + } else { + return MatchResult.aggregate(results); + } + }), - list((Context c, Object object) -> { - Map>> mapValue = cast(object, Map.class); - var allResults = mapValue.entrySet().stream().map(listIndexEntry -> { - Map listEntry; - switch (listIndexEntry.getKey()) { - case "last": - case "-1": - listEntry = c.getList().getLast(); - break; - case "first": - listEntry = c.getList().getFirst(); - break; - default: - listEntry = withConvertedNumberGet(c, listIndexEntry.getKey(), (context, value) -> c.getList().get(value.intValue())); + list( + HasContext::getList, + (Context c, Object object) -> { + var mapValue = cast(object, HasContext.ContextList.class); + LinkedList allResults = new LinkedList<>(); + if (mapValue.getFirst() != null) { + allResults.push(evaluateListMatcher(c, mapValue.getFirst(), () -> c.getList().getFirst())); } - if (listEntry == null) { - return MatchResult.noMatch(); - } else { - List results = listIndexEntry.getValue().entrySet().stream().map(entry -> { - var patterns = mapToObject(entry.getValue(), StringValuePattern.class); - var propertyValue = listEntry.get(entry.getKey()); - return patterns.match(propertyValue); - }).collect(Collectors.toList()); - if (results.isEmpty()) { - logger().info(c, "No interpretable matcher was found, defaulting to 'exactMatch'"); - return MatchResult.exactMatch(); - } else { - return MatchResult.aggregate(results); - } + if (mapValue.getLast() != null) { + allResults.push(evaluateListMatcher(c, mapValue.getLast(), () -> c.getList().getLast())); + } + if (mapValue.getIndexed() != null) { + mapValue.getIndexed().forEach((key, value) -> allResults.push(evaluateListMatcher(c, value, () -> c.getList().get(Integer.parseInt(key))))); } - }).collect(Collectors.toList()); - return MatchResult.aggregate(allResults); - }), - hasProperty((Context c, Object object) -> { - String stringValue = cast(object, String.class); - return toMatchResult(c.getProperties().containsKey(stringValue)); - }), - hasNotProperty((Context c, Object object) -> { - String stringValue = cast(object, String.class); - return toMatchResult(!c.getProperties().containsKey(stringValue)); - }), - updateCountEqualTo((Context c, Object object) -> { - String stringValue = cast(object, String.class); - return toMatchResult(withConvertedNumber(c, stringValue, (context, value) -> context.getUpdateCount().equals(value))); - }), - updateCountLessThan((Context c, Object object) -> { - String stringValue = cast(object, String.class); - return toMatchResult(withConvertedNumber(c, stringValue, (context, value) -> context.getUpdateCount() < value)); - }), - updateCountMoreThan((Context c, Object object) -> { - String stringValue = cast(object, String.class); - return toMatchResult(withConvertedNumber(c, stringValue, (context, value) -> context.getUpdateCount() > value)); - }), - listSizeEqualTo((Context c, Object object) -> { - String stringValue = cast(object, String.class); - return toMatchResult(withConvertedNumber(c, stringValue, (context, value) -> context.getList().size() == value)); - }), - listSizeLessThan((Context c, Object object) -> { - String stringValue = cast(object, String.class); - return toMatchResult(withConvertedNumber(c, stringValue, (context, value) -> context.getList().size() < value)); - }), - listSizeMoreThan((Context c, Object object) -> { - String stringValue = cast(object, String.class); - return toMatchResult(withConvertedNumber(c, stringValue, (context, value) -> context.getList().size() > value)); - }); + return MatchResult.aggregate(allResults); + }), + hasProperty( + HasContext::getHasProperty, + (Context c, Object object) -> { + var stringValue = cast(object, String.class); + return toMatchResult(c.getProperties().containsKey(stringValue)); + }), + hasNotProperty( + HasContext::getHasNotProperty, + (Context c, Object object) -> { + var stringValue = cast(object, String.class); + return toMatchResult(!c.getProperties().containsKey(stringValue)); + }), + updateCountEqualTo( + HasContext::getUpdateCountEqualTo, + (Context c, Object object) -> { + var value = cast(object, Integer.class); + return toMatchResult(c.getUpdateCount().equals(value)); + }), + updateCountLessThan( + HasContext::getUpdateCountLessThan, + (Context c, Object object) -> { + var value = cast(object, Integer.class); + return toMatchResult(c.getUpdateCount() < value); + }), + updateCountMoreThan( + HasContext::getUpdateCountMoreThan, + (Context c, Object object) -> { + var value = cast(object, Integer.class); + return toMatchResult(c.getUpdateCount() > value); + }), + listSizeEqualTo( + HasContext::getListSizeEqualTo, + (Context c, Object object) -> { + var value = cast(object, Integer.class); + return toMatchResult(c.getList().size() == value); + }), + listSizeLessThan( + HasContext::getListSizeLessThan, + (Context c, Object object) -> { + var value = cast(object, Integer.class); + return toMatchResult(c.getList().size() < value); + }), + listSizeMoreThan( + HasContext::getListSizeMoreThan, + (Context c, Object object) -> { + var value = cast(object, Integer.class); + return toMatchResult(c.getList().size() > value); + }); + private final Function getConfiguration; private final BiFunction evaluator; - ContextMatcher(BiFunction evaluator) { + ContextMatcher(Function getConfiguration, BiFunction evaluator) { + this.getConfiguration = getConfiguration; this.evaluator = evaluator; } - private static MatchResult toMatchResult(boolean result) { - return result ? MatchResult.exactMatch() : MatchResult.noMatch(); - } - - public static ContextMatcher from(String from) { - return Arrays.stream(values()).filter(it -> it.name().equals(from)).findFirst().orElse(null); - } - - private static boolean withConvertedNumber(Context context, String stringValue, BiFunction evaluator) { + private static MatchResult evaluateListMatcher(Context c, Map> listIndexEntry, Supplier> listEntrySupplier) { try { - var longValue = Long.valueOf(stringValue); - return evaluator.apply(context, longValue); - } catch (NumberFormatException ex) { - return false; + var listEntry = listEntrySupplier.get(); + List results = listIndexEntry.entrySet().stream().map(entry -> { + var patterns = mapToPatternMatcher(entry.getValue()); + var propertyValue = listEntry.get(entry.getKey()); + return patterns.match(propertyValue); + }).collect(Collectors.toList()); + if (results.isEmpty()) { + logger().info(c, "No interpretable matcher was found, defaulting to 'exactMatch'"); + return MatchResult.exactMatch(); + } else { + return MatchResult.aggregate(results); + } + } catch (IndexOutOfBoundsException ex) { + logger().info(c, "List entry does not exist"); + return MatchResult.noMatch(); } } - private static T withConvertedNumberGet(Context context, String stringValue, BiFunction getter) { - try { - var longValue = Long.valueOf(stringValue); - return getter.apply(context, longValue); - } catch (NumberFormatException | IndexOutOfBoundsException ex) { - return null; - } + private static MatchResult toMatchResult(boolean result) { + return result ? MatchResult.exactMatch() : MatchResult.noMatch(); + } + + public static List> from(@NotNull HasContext hasContext) { + return Arrays.stream(values()) + .map(matcher -> { + var configuration = matcher.getConfiguration.apply(hasContext); + return configuration != null ? Map.entry(matcher, configuration) : null; + } + ) + .filter(Objects::nonNull) + .collect(Collectors.toList()); } public MatchResult evaluate(Context context, Object value) { diff --git a/src/main/java/org/wiremock/extensions/state/extensions/builder/DeleteStateEventListenerBuilder.java b/src/main/java/org/wiremock/extensions/state/extensions/builder/DeleteStateEventListenerBuilder.java new file mode 100644 index 0000000..e55a39b --- /dev/null +++ b/src/main/java/org/wiremock/extensions/state/extensions/builder/DeleteStateEventListenerBuilder.java @@ -0,0 +1,84 @@ +package org.wiremock.extensions.state.extensions.builder; + +import org.jetbrains.annotations.NotNull; +import org.wiremock.extensions.state.internal.api.DeleteStateParameters; +import org.wiremock.extensions.state.internal.api.DeleteStateParameters.ListParameters; + +import java.util.Collection; +import java.util.List; + +public abstract class DeleteStateEventListenerBuilder extends EventListenerBuilder { + + private DeleteStateEventListenerBuilder(DeleteStateParameters params) { + this.parameters = params; + } + + public static @NotNull SingleContextBuilder context(@NotNull String context) { + var params = new DeleteStateParameters(); + params.setContext(context); + return new SingleContextBuilder(params); + } + + public static @NotNull MultiContextBuilder contexts(@NotNull Collection contexts) { + var params = new DeleteStateParameters(); + params.setContexts(List.copyOf(contexts)); + return new MultiContextBuilder(params); + } + + public static @NotNull MultiContextBuilder contextsMatching(@NotNull String contextsMatching) { + var params = new DeleteStateParameters(); + params.setContextsMatching(contextsMatching); + return new MultiContextBuilder(params); + } + + + public static class SingleContextBuilder extends DeleteStateEventListenerBuilder { + private SingleContextBuilder(DeleteStateParameters params) { + super(params); + } + + public @NotNull ListBuilder list() { + return new ListBuilder(this); + } + } + + public static class ListBuilder { + private final SingleContextBuilder singleContextBuilder; + private final ListParameters listParams = new ListParameters(); + + private ListBuilder(SingleContextBuilder contextBuilder) { + this.singleContextBuilder = contextBuilder; + } + + public @NotNull SingleContextBuilder first() { + listParams.setDeleteFirst(true); + singleContextBuilder.parameters.setList(listParams); + return singleContextBuilder; + } + + public @NotNull SingleContextBuilder last() { + listParams.setDeleteLast(true); + singleContextBuilder.parameters.setList(listParams); + return singleContextBuilder; + } + + public @NotNull SingleContextBuilder index(int index) { + listParams.setDeleteIndex(String.valueOf(index)); + singleContextBuilder.parameters.setList(listParams); + return singleContextBuilder; + } + + public @NotNull SingleContextBuilder where(@NotNull String property, @NotNull String value) { + listParams.setDeleteWhere(new ListParameters.Where(property, value)); + singleContextBuilder.parameters.setList(listParams); + return singleContextBuilder; + } + } + + public static class MultiContextBuilder extends DeleteStateEventListenerBuilder { + private MultiContextBuilder(DeleteStateParameters params) { + super(params); + } + } + +} diff --git a/src/main/java/org/wiremock/extensions/state/extensions/builder/EventListenerBuilder.java b/src/main/java/org/wiremock/extensions/state/extensions/builder/EventListenerBuilder.java new file mode 100644 index 0000000..72b0bbe --- /dev/null +++ b/src/main/java/org/wiremock/extensions/state/extensions/builder/EventListenerBuilder.java @@ -0,0 +1,16 @@ +package org.wiremock.extensions.state.extensions.builder; + +import com.github.tomakehurst.wiremock.common.Json; +import com.github.tomakehurst.wiremock.extension.Parameters; +import com.github.tomakehurst.wiremock.extension.ServeEventListenerDefinition; +import org.wiremock.extensions.state.extensions.DeleteStateEventListener; + +public class EventListenerBuilder { + + protected T parameters; + + public ServeEventListenerDefinition build() { + var convertedParams = Parameters.from(Json.objectToMap(parameters)); + return new ServeEventListenerDefinition(DeleteStateEventListener.NAME, convertedParams); + } +} diff --git a/src/main/java/org/wiremock/extensions/state/extensions/builder/RecordStateEventListenerBuilder.java b/src/main/java/org/wiremock/extensions/state/extensions/builder/RecordStateEventListenerBuilder.java new file mode 100644 index 0000000..53391b5 --- /dev/null +++ b/src/main/java/org/wiremock/extensions/state/extensions/builder/RecordStateEventListenerBuilder.java @@ -0,0 +1,74 @@ +package org.wiremock.extensions.state.extensions.builder; + +import com.github.tomakehurst.wiremock.extension.Parameters; +import org.jetbrains.annotations.NotNull; +import org.wiremock.extensions.state.internal.api.RecordStateParameters; + +import java.util.Map; + +public class RecordStateEventListenerBuilder extends EventListenerBuilder { + + private RecordStateEventListenerBuilder(RecordStateParameters params) { + this.parameters = params; + } + + public static @NotNull RecordStateEventListenerBuilder context(@NotNull String context) { + var params = new RecordStateParameters(); + params.setContext(context); + return new RecordStateEventListenerBuilder(params); + } + + public @NotNull RecordStateEventListenerBuilder state(@NotNull Map state) { + parameters.setState(state); + return this; + } + + public @NotNull RecordStateEventListenerBuilder state(@NotNull Parameters state) { + //noinspection unchecked + parameters.setState(state.as(Map.class)); + return this; + } + + public ListBuilder list() { + return new ListBuilder(this); + } + + public static class ListBuilder { + private final RecordStateEventListenerBuilder recordStateBuilder; + private final RecordStateParameters.ListParameters listParameters = new RecordStateParameters.ListParameters(); + + private ListBuilder(RecordStateEventListenerBuilder recordStateBuilder) { + this.recordStateBuilder = recordStateBuilder; + } + + public @NotNull RecordStateEventListenerBuilder addFirst(@NotNull Map state) { + listParameters.setAddFirst(state); + recordStateBuilder.parameters.setList(listParameters); + return recordStateBuilder; + } + + public @NotNull RecordStateEventListenerBuilder addFirst(@NotNull Parameters state) { + //noinspection unchecked + listParameters.setAddFirst(state.as(Map.class)); + recordStateBuilder.parameters.setList(listParameters); + return recordStateBuilder; + } + + public @NotNull RecordStateEventListenerBuilder addLast(@NotNull Map state) { + listParameters.setAddLast(state); + recordStateBuilder.parameters.setList(listParameters); + return recordStateBuilder; + } + + public @NotNull RecordStateEventListenerBuilder addLast(@NotNull Parameters state) { + //noinspection unchecked + listParameters.setAddLast(state.as(Map.class)); + recordStateBuilder.parameters.setList(listParameters); + return recordStateBuilder; + } + + + } + + +} diff --git a/src/main/java/org/wiremock/extensions/state/extensions/builder/StateRequestMatcherBuilder.java b/src/main/java/org/wiremock/extensions/state/extensions/builder/StateRequestMatcherBuilder.java new file mode 100644 index 0000000..6629bf8 --- /dev/null +++ b/src/main/java/org/wiremock/extensions/state/extensions/builder/StateRequestMatcherBuilder.java @@ -0,0 +1,45 @@ +package org.wiremock.extensions.state.extensions.builder; + +import com.github.tomakehurst.wiremock.common.Json; +import com.github.tomakehurst.wiremock.extension.Parameters; +import com.github.tomakehurst.wiremock.matching.CustomMatcherDefinition; +import org.jetbrains.annotations.NotNull; +import org.wiremock.extensions.state.internal.api.StateRequestMatcherParameters; +import org.wiremock.extensions.state.internal.api.StateRequestMatcherParameters.HasContext; +import org.wiremock.extensions.state.internal.api.StateRequestMatcherParameters.HasNotContext; + +public abstract class StateRequestMatcherBuilder { + protected T parameters; + + public static @NotNull HasContextBuilder hasContext(@NotNull String context) { + return new HasContextBuilder(context); + } + + public static @NotNull HasNotContextBuilder hasNotContext(@NotNull String context) { + return new HasNotContextBuilder(context); + } + + public CustomMatcherDefinition build() { + var convertedParams = Parameters.from(Json.objectToMap(parameters)); + return new CustomMatcherDefinition("state-matcher", convertedParams); + } + + public static class HasContextBuilder extends StateRequestMatcherBuilder { + private HasContextBuilder(@NotNull String context) { + parameters = new HasContext(); + parameters.setHasContext(context); + } + + + + } + + public static class HasNotContextBuilder extends StateRequestMatcherBuilder { + private HasNotContextBuilder(@NotNull String context) { + parameters = new HasNotContext(); + parameters.setHasNotContext(context); + } + + } + +} diff --git a/src/main/java/org/wiremock/extensions/state/internal/ContextManager.java b/src/main/java/org/wiremock/extensions/state/internal/ContextManager.java index b8feae5..6e9f232 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/ContextManager.java +++ b/src/main/java/org/wiremock/extensions/state/internal/ContextManager.java @@ -135,8 +135,8 @@ public void createOrUpdateContextList(String requestId, String contextName, Cons }); } - public Long numUpdates(String contextName) { - return store.get(createContextKey(contextName)).map(it -> ((Context) it).getUpdateCount()).orElse(0L); + public Integer numUpdates(String contextName) { + return store.get(createContextKey(contextName)).map(it -> ((Context) it).getUpdateCount()).orElse(0); } private String getContextNameFromContextKey(String key) { diff --git a/src/main/java/org/wiremock/extensions/state/internal/api/DeleteStateParameters.java b/src/main/java/org/wiremock/extensions/state/internal/api/DeleteStateParameters.java index 406eba5..58d072c 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/api/DeleteStateParameters.java +++ b/src/main/java/org/wiremock/extensions/state/internal/api/DeleteStateParameters.java @@ -103,6 +103,13 @@ public static class Where { private String property; private String value; + public Where() { + } + public Where(String property, String value) { + this.property = property; + this.value = value; + } + public String getProperty() { return property; } diff --git a/src/main/java/org/wiremock/extensions/state/internal/api/StateRequestMatcherParameters.java b/src/main/java/org/wiremock/extensions/state/internal/api/StateRequestMatcherParameters.java new file mode 100644 index 0000000..7c6e44f --- /dev/null +++ b/src/main/java/org/wiremock/extensions/state/internal/api/StateRequestMatcherParameters.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 Dirk Bolte + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wiremock.extensions.state.internal.api; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +import static java.util.stream.Collectors.toMap; + +@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonSubTypes(value = { + @JsonSubTypes.Type(StateRequestMatcherParameters.HasContext.class), + @JsonSubTypes.Type(StateRequestMatcherParameters.HasNotContext.class) +}) +public abstract class StateRequestMatcherParameters { + + public static class HasContext extends StateRequestMatcherParameters { + private String hasContext; + private String hasProperty; + private String hasNotProperty; + private Map property; + private ContextList list; + private Integer updateCountEqualTo; + private Integer updateCountLessThan; + private Integer updateCountMoreThan; + private Integer listSizeEqualTo; + private Integer listSizeLessThan; + private Integer listSizeMoreThan; + + public @NotNull String getHasContext() { + return hasContext; + } + + public void setHasContext(@NotNull String hasContext) { + this.hasContext = hasContext; + } + + public @Nullable String getHasProperty() { + return hasProperty; + } + + public void setHasProperty(@NotNull String hasProperty) { + this.hasProperty = hasProperty; + } + public @Nullable String getHasNotProperty() { + return hasNotProperty; + } + + public void setHasNotProperty(@NotNull String hasNotProperty) { + this.hasNotProperty = hasNotProperty; + } + + public @Nullable Map getProperty() { + return property; + } + + public void setProperty(@NotNull Map property) { + this.property = property; + } + + public @Nullable ContextList getList() { + return list; + } + + public void setList(@NotNull ContextList list) { + this.list = list; + } + + public @Nullable Integer getUpdateCountEqualTo() { + return updateCountEqualTo; + } + + public void setUpdateCountEqualTo(@NotNull Integer updateCountEqualTo) { + this.updateCountEqualTo = updateCountEqualTo; + } + + public @Nullable Integer getUpdateCountLessThan() { + return updateCountLessThan; + } + + public void setUpdateCountLessThan(@NotNull Integer updateCountLessThan) { + this.updateCountLessThan = updateCountLessThan; + } + + public @Nullable Integer getUpdateCountMoreThan() { + return updateCountMoreThan; + } + + public void setUpdateCountMoreThan(@NotNull Integer updateCountMoreThan) { + this.updateCountMoreThan = updateCountMoreThan; + } + + public @Nullable Integer getListSizeEqualTo() { + return listSizeEqualTo; + } + + public void setListSizeEqualTo(@NotNull Integer listSizeEqualTo) { + this.listSizeEqualTo = listSizeEqualTo; + } + + public @Nullable Integer getListSizeLessThan() { + return listSizeLessThan; + } + + public void setListSizeLessThan(@NotNull Integer listSizeLessThan) { + this.listSizeLessThan = listSizeLessThan; + } + + public @Nullable Integer getListSizeMoreThan() { + return listSizeMoreThan; + } + + public void setListSizeMoreThan(@NotNull Integer listSizeMoreThan) { + this.listSizeMoreThan = listSizeMoreThan; + } + + public static class ContextList { + public Map> first; + public Map> last; + + @JsonIgnore + public Map>> indexed = new HashMap<>(); + + public @Nullable Map> getLast() { + return last; + } + + @JsonAlias("-1") + public void setLast(@NotNull Map> last) { + this.last = last; + } + + public @Nullable Map> getFirst() { + return first; + } + + public void setFirst(@NotNull Map> first) { + this.first = first; + } + + @JsonAnySetter + public void setIndexed(@NotNull String key, @NotNull Map> value) { + indexed.put(Integer.parseUnsignedInt(key), value); + } + + @JsonAnyGetter + public Map>> getIndexed() { + return indexed + .entrySet() + .stream() + .collect(toMap(entry -> entry.getKey().toString(), Map.Entry::getValue)); + } + } + } + + public static class HasNotContext extends StateRequestMatcherParameters { + private String hasNotContext; + + public @NotNull String getHasNotContext() { + return hasNotContext; + } + + public void setHasNotContext(@NotNull String hasNotContext) { + this.hasNotContext = hasNotContext; + } + } +} diff --git a/src/main/java/org/wiremock/extensions/state/internal/model/Context.java b/src/main/java/org/wiremock/extensions/state/internal/model/Context.java index e354012..212ca62 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/model/Context.java +++ b/src/main/java/org/wiremock/extensions/state/internal/model/Context.java @@ -26,7 +26,7 @@ public class Context { private final Map properties = new HashMap<>(); private final LinkedList> list = new LinkedList<>(); private final LinkedList requests = new LinkedList<>(); - private Long updateCount = 0L; + private Integer updateCount = 0; public Context(Context other) { this.contextName = other.contextName; @@ -44,11 +44,11 @@ public String getContextName() { return contextName; } - public Long getUpdateCount() { + public Integer getUpdateCount() { return updateCount; } - public Long incUpdateCount() { + public Integer incUpdateCount() { updateCount = updateCount + 1; return updateCount; } diff --git a/src/main/java/org/wiremock/extensions/state/internal/model/ContextTemplateModel.java b/src/main/java/org/wiremock/extensions/state/internal/model/ContextTemplateModel.java index 691e7a4..c3681cb 100644 --- a/src/main/java/org/wiremock/extensions/state/internal/model/ContextTemplateModel.java +++ b/src/main/java/org/wiremock/extensions/state/internal/model/ContextTemplateModel.java @@ -28,8 +28,7 @@ public static ContextTemplateModel from(Context context) { return new ContextTemplateModel(context); } - - public Long getUpdateCount() { + public Integer getUpdateCount() { return context.getUpdateCount(); } } diff --git a/src/test/java/org/wiremock/extensions/state/functionality/DeleteStateEventListenerTest.java b/src/test/java/org/wiremock/extensions/state/functionality/DeleteStateEventListenerTest.java index a31c54a..de0d694 100644 --- a/src/test/java/org/wiremock/extensions/state/functionality/DeleteStateEventListenerTest.java +++ b/src/test/java/org/wiremock/extensions/state/functionality/DeleteStateEventListenerTest.java @@ -17,6 +17,7 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.extension.Parameters; +import com.github.tomakehurst.wiremock.extension.ServeEventListenerDefinition; import io.restassured.http.ContentType; import org.apache.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.wiremock.extensions.state.extensions.builder.DeleteStateEventListenerBuilder; import java.net.URI; import java.util.HashMap; @@ -96,6 +98,17 @@ private void createGetStub(Map configuration) { ) ); } + private void createGetStub(ServeEventListenerDefinition listenerDefinition) { + wm.stubFor( + get(urlPathMatching("/state/[^/]+")) + .willReturn( + WireMock.ok() + .withHeader("content-type", "application/json") + .withBody("{}") + ) + .withServeEventListener(listenerDefinition) + ); + } private void createGetStubList(Map configuration) { wm.stubFor( @@ -549,6 +562,16 @@ void test_deleteContext() { assertThat(contextManager.getContextCopy(contextName)).isEmpty(); } + @DisplayName("deletes context with builder") + @Test + void test_deleteContext_builder() { + createGetStub(DeleteStateEventListenerBuilder.context( "{{request.pathSegments.[1]}}").build()); + getContext(contextName, HttpStatus.SC_OK, (result) -> assertThat(result).isEmpty()); + + assertThat(contextManager.getContextCopy(contextName)).isEmpty(); + } + + @DisplayName("double deletion does not cause an error") @Test void test_deleteContextTwice() { diff --git a/src/test/java/org/wiremock/extensions/state/functionality/StateRequestMatcherTest.java b/src/test/java/org/wiremock/extensions/state/functionality/StateRequestMatcherTest.java index da1314d..cf9b65d 100644 --- a/src/test/java/org/wiremock/extensions/state/functionality/StateRequestMatcherTest.java +++ b/src/test/java/org/wiremock/extensions/state/functionality/StateRequestMatcherTest.java @@ -17,7 +17,6 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.common.Json; -import com.github.tomakehurst.wiremock.common.Pair; import com.github.tomakehurst.wiremock.extension.Parameters; import io.restassured.http.ContentType; import io.restassured.response.ValidatableResponse; @@ -33,15 +32,14 @@ import java.net.URI; import java.util.HashMap; -import java.util.Map; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; -import static com.github.tomakehurst.wiremock.common.Pair.pair; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -546,12 +544,12 @@ void test_countMatches_ok() { getAndAssertContextMatcher(context, HttpStatus.SC_OK); } - @DisplayName("succeeds on invalid configuration") + @DisplayName("fails on invalid configuration") @Test void test_countInvalid_fail() { createGetStub("updateCountEqualTo", "invalid"); - getAndAssertContextMatcher(context, HttpStatus.SC_NOT_FOUND); + getAndAssertContextMatcher(context, HttpStatus.SC_INTERNAL_SERVER_ERROR); } @DisplayName("fails on non-matching count") @@ -575,12 +573,12 @@ void test_countMatches_ok() { getAndAssertContextMatcher(context, HttpStatus.SC_OK); } - @DisplayName("succeeds on invalid configuration") + @DisplayName("fails on invalid configuration") @Test void test_countInvalid_fail() { createGetStub("updateCountLessThan", "invalid"); - getAndAssertContextMatcher(context, HttpStatus.SC_NOT_FOUND); + getAndAssertContextMatcher(context, HttpStatus.SC_INTERNAL_SERVER_ERROR); } @DisplayName("fails on non-matching count") @@ -604,12 +602,12 @@ void test_countMatches_ok() { getAndAssertContextMatcher(context, HttpStatus.SC_OK); } - @DisplayName("succeeds on invalid configuration") + @DisplayName("fails on invalid configuration") @Test void test_countInvalid_fail() { createGetStub("updateCountMoreThan", "invalid"); - getAndAssertContextMatcher(context, HttpStatus.SC_NOT_FOUND); + getAndAssertContextMatcher(context, HttpStatus.SC_INTERNAL_SERVER_ERROR); } @DisplayName("fails on non-matching count") @@ -649,12 +647,12 @@ void test_countMatches_ok() { getAndAssertContextMatcher(context, HttpStatus.SC_OK); } - @DisplayName("succeeds on invalid configuration") + @DisplayName("fails on invalid configuration") @Test void test_countInvalid_fail() { createGetStub("listSizeEqualTo", "invalid"); - getAndAssertContextMatcher(context, HttpStatus.SC_NOT_FOUND); + getAndAssertContextMatcher(context, HttpStatus.SC_INTERNAL_SERVER_ERROR); } @DisplayName("fails on non-matching count") @@ -678,12 +676,12 @@ void test_countMatches_ok() { getAndAssertContextMatcher(context, HttpStatus.SC_OK); } - @DisplayName("succeeds on invalid configuration") + @DisplayName("fails on invalid configuration") @Test void test_countInvalid_fail() { createGetStub("listSizeLessThan", "invalid"); - getAndAssertContextMatcher(context, HttpStatus.SC_NOT_FOUND); + getAndAssertContextMatcher(context, HttpStatus.SC_INTERNAL_SERVER_ERROR); } @DisplayName("fails on non-matching count") @@ -707,12 +705,12 @@ void test_countMatches_ok() { getAndAssertContextMatcher(context, HttpStatus.SC_OK); } - @DisplayName("succeeds on invalid configuration") + @DisplayName("fails on invalid configuration") @Test void test_countInvalid_fail() { createGetStub("listSizeMoreThan", "invalid"); - getAndAssertContextMatcher(context, HttpStatus.SC_NOT_FOUND); + getAndAssertContextMatcher(context, HttpStatus.SC_INTERNAL_SERVER_ERROR); } @DisplayName("fails on non-matching count") diff --git a/src/test/kotlin/org/wiremock/extensions/state/dsl/KotlinDslTest.kt b/src/test/kotlin/org/wiremock/extensions/state/dsl/KotlinDslTest.kt new file mode 100644 index 0000000..a9c67a7 --- /dev/null +++ b/src/test/kotlin/org/wiremock/extensions/state/dsl/KotlinDslTest.kt @@ -0,0 +1,24 @@ +import com.github.tomakehurst.wiremock.client.WireMock +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching +import com.github.tomakehurst.wiremock.extension.Parameters +import org.junit.jupiter.api.Test +import org.wiremock.extensions.state.extensions.Dsl.deleteContext +import org.wiremock.extensions.state.extensions.Dsl.recordContext +import org.wiremock.extensions.state.functionality.AbstractTestBase + +class KotlinDslTest : AbstractTestBase() { + @Test + fun `delete state`() { + wm.stubFor( + get(urlPathMatching("/state/[^/]+")) + .willReturn( + WireMock.ok() + .withHeader("content-type", "application/json") + .withBody("{}") + ) + .withServeEventListener(deleteContext("something").list().last().build()) + .withServeEventListener(recordContext("something").list().addLast(Parameters()).build()) + ) + } +} \ No newline at end of file