From c1ddb93ba81942e33e6d0352c3da1546430d657c Mon Sep 17 00:00:00 2001 From: Kyle Winkelman Date: Tue, 4 Jun 2024 11:29:54 -0500 Subject: [PATCH] Add custom content patterns. --- _data/doc-categories.yml | 3 +- _docs/extending-wiremock.md | 65 +++++++- .../extensibility/custom-content-matching.md | 150 ++++++++++++++++++ ...matching.md => custom-request-matching.md} | 2 +- 4 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 _docs/extensibility/custom-content-matching.md rename _docs/extensibility/{custom-matching.md => custom-request-matching.md} (99%) diff --git a/_data/doc-categories.yml b/_data/doc-categories.yml index abbae640..222a0dd6 100644 --- a/_data/doc-categories.yml +++ b/_data/doc-categories.yml @@ -78,7 +78,8 @@ extensibility: - extending-wiremock - extensibility/filtering-requests - extensibility/transforming-responses - - extensibility/custom-matching + - extensibility/custom-request-matching + - extensibility/custom-content-matching - extensibility/listening-for-serve-events - extensibility/extending-the-admin-api - extensibility/adding-template-helpers diff --git a/_docs/extending-wiremock.md b/_docs/extending-wiremock.md index be464785..32b05311 100644 --- a/_docs/extending-wiremock.md +++ b/_docs/extending-wiremock.md @@ -19,7 +19,8 @@ At present, the following extension interfaces are available: * `ResponseTransformerV2`: Modify the response served to the client. See [Transforming responses](../extensibility/transforming-responses/). * `ServeEventListener`: Listen for events at various points in the request processing lifecycle. See [Listening for Serve Events](../extensibility/listening-for-serve-events/). * `AdminApiExtension`: Add admin API functions. See [Admin API Extensions](../extensibility/extending-the-admin-api/). -* `RequestMatcherExtension`: Implement custom request matching logic. See [Custom matching](../extensibility/custom-matching/). +* `RequestMatcherExtension`: Implement custom request matching logic. See [Custom Request Matching](../extensibility/custom-request-matching/). +* `ContentPatternExtension`: Implement custom content matching logic. See [Custom Content Matching](../extensibility/custom-content-matching/). * `GlobalSettingsListener`: Listen for changes to the settings object. See [Listening for Settings Changes](../extensibility/listening-for-settings-changes/). * `StubLifecycleListener`: Listen for changes to the stub mappings. See [Listening for Stub Changes](../extensibility/listening-for-stub-changes/). * `TemplateHelperProviderExtension`: Provide custom Handlebars helpers to the template engine. See [Adding Template Helpers](../extensibility/adding-template-helpers/). @@ -35,18 +36,35 @@ initialisation or cleanup tasks. ## Registering Extensions -You can directly register the extension programmatically via its class name, -class or an instance: +You can directly register the extension programmatically via its class name, class or an instance. +Server: ```java new WireMockServer(wireMockConfig() - .extensions("com.mycorp.BodyContentTransformer", "com.mycorp.HeaderMangler")); + .extensions("com.mycorp.ClassNameOne", "com.mycorp.ClassNameTwo") + .extensions(ClassOne.class, ClassTwo.class) + .extensions(new InstanceOne(), new InstanceTwo())); +``` -new WireMockServer(wireMockConfig() - .extensions(BodyContentTransformer.class, HeaderMangler.class)); +Client: +```java +// Only need to register extensions that change how a mapping is parsed/written (i.e. ContentPatternExtension). +WireMock.create() + .extensions("com.mycorp.ClassNameOne", "com.mycorp.ClassNameTwo") + .extensions(ClassOne.class, ClassTwo.class) + .extensions(new InstanceOne(), new InstanceTwo()) + .build(); +``` -new WireMockServer(wireMockConfig() - .extensions(new BodyContentTransformer(), new HeaderMangler())); +Extension: +```java +@RegisterExtension +static WireMockExtension wm = WireMockExtension.newInstance() + .options(wireMockConfig() + .extensions("com.mycorp.ClassNameOne", "com.mycorp.ClassNameTwo") + .extensions(ClassOne.class, ClassTwo.class) + .extensions(new InstanceOne(), new InstanceTwo())) + .build(); ``` See [Running as a Standalone Process](../running-standalone/) for details on running with extensions from the command line. @@ -76,11 +94,42 @@ Services currently available to extension factories are: * `Extensions`: the service for creating and providing extension implementations. * `TemplateEngine`: the Handlebars template engine. +For factories that register extensions that change how a mapping is parsed/written (i.e. ContentPatternExtension), must implement `ExtensionFactory#createForClient()` +to return those extension. + ## Extension registration via service loading Extensions that are packaged with the relevant [Java service loader framework](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) metadata will be loaded automatically if they are placed on the classpath. +Server: +```java +new WireMockServer(wireMockConfig().extensionScanningEnabled(true)); +``` + +Client: +```java +WireMock.create() + .extensionScanningEnabled(true) + .build(); +``` + +Extension: +```java +@RegisterExtension +static WireMockExtension wm = WireMockExtension.newInstance() + .options(wireMockConfig() + .extensionScanningEnabled(true)) + .build(); + +// or + +@WireMockTest(extensionScanningEnabled = true) +public class MyTest { + ... +} +``` + See [https://github.com/wiremock/wiremock/tree/master/test-extension](https://github.com/wiremock/wiremock/tree/master/test-extension) for an example of such an extension. diff --git a/_docs/extensibility/custom-content-matching.md b/_docs/extensibility/custom-content-matching.md new file mode 100644 index 00000000..bea92da4 --- /dev/null +++ b/_docs/extensibility/custom-content-matching.md @@ -0,0 +1,150 @@ +--- +layout: docs +title: Custom Content Matching +meta_title: Adding custom content matchers +description: Adding custom content matchers via extensions +--- + +If WireMock's standard set of content matching strategies isn't sufficient, you can register one or more content +matcher classes containing your own logic. + +Custom content matchers can be attached directly to stubs via the Java API when using the local admin interface (by +calling `stubFor(...)` on `WireMockServer`, `WireMockExtension`, or `WireMockRule`). They can also be added via the +extension mechanism on a remote admin interface (by calling `extensions(...)` on `WireMockBuilder`). + +To create a custom content matcher extend `ContentPattern` or `StringValuePattern` (ensuring `Json.write(...)` +appropriately serializes your matcher and a `@JsonCreator` is defined): +```java +public class MagicBytesPattern extends ContentPattern { + + public enum Format { + GIF("47 49 46 38 37 61", "47 49 46 38 39 61"), + PNG("89 50 4E 47 0D 0A 1A 0A"), + ZIP("50 4B 03 04", "50 4B 05 06", "50 4B 07 08"); + + private final Set magicBytes; + + Format(String... magicBytes) { + HexFormat hexFormat = HexFormat.ofDelimiter(" "); + this.magicBytes = Stream.of(magicBytes).map(hexFormat::parseHex).collect(toSet()); + } + } + + private final Format format; + + @JsonCreator + public MagicBytesPattern(@JsonProperty("magicBytes") Format format) { + super(format.magicBytes.iterator().next()); + this.format = format; + } + + @Override + public String getName() { + return "magicBytes"; + } + + @Override + public String getExpected() { + return format.toString(); + } + + @Override + public MatchResult match(byte[] value) { + for (byte[] magicByte : format.magicBytes) { + if (value.length >= magicByte.length) { + boolean matches = true; + for (int i = 0; i < magicByte.length; i++) { + if (value[i] != magicByte[i]) { + matches = false; + break; + } + } + if (matches) { + return MatchResult.exactMatch(); + } + } + } + return MatchResult.noMatch(); + } + + public Format getMagicBytes() { + return format; + } +} +``` +```java +public class StartsWithMatcher extends StringValuePattern { + + @JsonCreator + public StartsWithMatcher(@JsonProperty("startsWith") String startsWith) { + super(startsWith); + } + + @Override + public MatchResult match(String value) { + return MatchResult.of(value.startsWith(expectedValue)); + } + + public String getStartsWith() { + return expectedValue; + } +} +``` + +Then, implement `ContentPatternExtension` giving the extension a unique name and identifying the class of your matcher: +```java +public class StartsWithPatternExtension implements ContentPatternExtension { + + @Override + public Class> getContentPatternClass() { + return StartsWithPattern.class; + } + + @Override + public String getName() { + return "starts-with-pattern"; + } +} +``` + +After the extension is properly registered, you can define a stub with it: + +{% codetabs %} + +{% codetab Java %} + +```java +stubFor( + post("/gif") + .withHeader("X-FileName", new StartsWithMatcher("gif")) + .withRequestBody(new MagicBytesPattern(MagicBytesPattern.Format.GIF)) + .willReturn(ok())); +``` + +{% endcodetab %} + +{% codetab JSON %} + +```json +{ + "request": { + "url": "/gif", + "method": "POST", + "headers": { + "X-FileName": { + "startsWith": "gif" + } + }, + "bodyPatterns": [{ + "magicBytes": "GIF" + }] + }, + "response": { + "status": 200 + } +} +``` + +{% endcodetab %} + +{% endcodetabs %} \ No newline at end of file diff --git a/_docs/extensibility/custom-matching.md b/_docs/extensibility/custom-request-matching.md similarity index 99% rename from _docs/extensibility/custom-matching.md rename to _docs/extensibility/custom-request-matching.md index 5099bde6..8f9bacd2 100644 --- a/_docs/extensibility/custom-matching.md +++ b/_docs/extensibility/custom-request-matching.md @@ -1,6 +1,6 @@ --- layout: docs -title: Custom Matching +title: Custom Request Matching meta_title: Adding custom request matchers description: Adding custom request matchers via extensions ---