Permalink
Browse files

Merge branch 'handlebars-response-templating'

  • Loading branch information...
2 parents 5d62f19 + a59cb61 commit e7882a046d50b2fd7809f2f971a5ea91a112a207 @tomakehurst committed Dec 21, 2016
View
@@ -62,6 +62,9 @@ dependencies {
}
compile 'org.apache.commons:commons-lang3:3.4'
compile 'com.flipkart.zjsonpatch:zjsonpatch:0.2.1'
+ compile 'com.github.jknack:handlebars:4.0.6', {
+ exclude group: 'org.mozilla', module: 'rhino'
+ }
testCompile "org.hamcrest:hamcrest-all:1.3"
testCompile("org.jmock:jmock:2.5.1") {
@@ -0,0 +1,110 @@
+---
+layout: docs
+title: Response Templating
+toc_rank: 71
+description: Generating dynamic responses using Handlebars templates
+---
+
+Response headers and bodies can optionally be rendered using [Handlebars templates](http://handlebarsjs.com/). This enables attributes of the request
+to be used in generating the response e.g. to pass the value of a request ID header as a response header or
+render an identifier from part of the URL in the response body.
+
+## Enabling response templating
+When starting WireMock programmatically, response templating can be enabled by adding `ResponseTemplateTransformer` as an extension e.g.
+
+```java
+@Rule
+public WireMockRule wm = new WireMockRule(options()
+ .extensions(new ResponseTemplateTransformer(false))
+);
+```
+
+
+The boolean constructor parameter indicates whether the extension should be applied globally. If true, all stub mapping responses will be rendered as templates prior
+to being served.
+
+Otherwise the transformer will need to be specified on each stub mapping by its name `response-template`:
+
+Command line parameters can be used to enable templating when running WireMock [standalone](/docs/running-standalone/#command-line-options).
+
+### Java
+
+{% raw %}
+```java
+wm.stubFor(get(urlPathEqualTo("/templated"))
+ .willReturn(aResponse()
+ .withBody("{{request.path.[0]}}")
+ .withTransformers("response-template")));
+```
+{% endraw %}
+
+
+{% raw %}
+### JSON
+```json
+{
+ "request": {
+ "urlPath": "/templated"
+ },
+ "response": {
+ "body": "{{request.path.[0]}}",
+ "transformers": ["response-template"]
+ }
+}
+```
+{% endraw %}
+
+## The request model
+The model of the request is supplied to the header and body templates. The following request attributes are available:
+
+`request.url` - URL path and query
+
+`request.path` - URL path
+
+`request.path.[<n>]`- URL path segment (zero indexed) e.g. `request.path.[2]`
+
+`request.query.<key>`- First value of a query parameter e.g. `request.query.search`
+
+`request.query.<key>.[<n>]`- nth value of a query parameter (zero indexed) e.g. `request.query.search.[5]`
+
+`request.headers.<key>`- First value of a request header e.g. `request.headers.X-Request-Id`
+
+`request.headers.[<key>]`- Header with awkward characters e.g. `request.headers.[$?blah]`
+
+`request.headers.<key>.[<n>]`- nth value of a header (zero indexed) e.g. `request.headers.ManyThings.[1]`
+
+`request.cookies.<key>` - Value of a request cookie e.g. `request.cookies.JSESSIONID`
+
+`request.body` - Request body text (avoid for non-text bodies)
+
+
+## Handlebars helpers
+All of the standard helpers (template functions) provided by the [Java Handlebars implementation by jknack](https://github.com/jknack/handlebars.java)
+plus all of the [string helpers](https://github.com/jknack/handlebars.java/blob/master/handlebars/src/main/java/com/github/jknack/handlebars/helper/StringHelpers.java)
+are available e.g.
+
+{% raw %}
+```
+{{capitalize request.query.search}}
+```
+{% endraw %}
+
+
+## Custom helpers
+Custom Handlebars helpers can be registered with the transformer on construction:
+
+```java
+Helper<String> stringLengthHelper = new Helper<String>() {
+ @Override
+ public Object apply(String context, Options options) throws IOException {
+ return context.length();
+ }
+};
+
+@Rule
+public WireMockRule wm = new WireMockRule(options()
+ .extensions(new ResponseTemplateTransformer(false), "string-length", stringLengthHelper)
+);
+```
+
+
@@ -101,6 +101,9 @@ com.mycorp.HeaderTransformer,com.mycorp.BodyTransformer. See extending-wiremock.
`--print-all-network-traffic`: Print all raw incoming and outgoing network traffic to console.
+`--global-response-templating`: Render all response definitions using Handlebars templates.
+`--local-response-templating`: Enable rendering of response definitions using Handlebars templates for specific stub mappings.
+
`--help`: Show command line help
## Configuring WireMock using the Java client
@@ -94,8 +94,8 @@ public ResponseDefinitionBuilder withStatus(int status) {
return this;
}
- public ResponseDefinitionBuilder withHeader(String key, String value) {
- headers.add(new HttpHeader(key, value));
+ public ResponseDefinitionBuilder withHeader(String key, String... values) {
+ headers.add(new HttpHeader(key, values));
return this;
}
@@ -0,0 +1,31 @@
+package com.github.tomakehurst.wiremock.extension.responsetemplating;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+public class ListOrSingle<T> extends ArrayList<T> {
+
+ public ListOrSingle(Collection<? extends T> c) {
+ super(c);
+ }
+
+ public ListOrSingle(T... items) {
+ this(asList(items));
+ }
+
+ @Override
+ public String toString() {
+ return size() > 0 ? get(0).toString() : "";
+ }
+
+ public static <T> ListOrSingle<T> of(T... items) {
+ return new ListOrSingle<>(items);
+ }
+
+ public static <T> ListOrSingle<T> of(List<T> items) {
+ return new ListOrSingle<>(items);
+ }
+}
@@ -0,0 +1,92 @@
+package com.github.tomakehurst.wiremock.extension.responsetemplating;
+
+import com.github.tomakehurst.wiremock.common.Urls;
+import com.github.tomakehurst.wiremock.http.Cookie;
+import com.github.tomakehurst.wiremock.http.MultiValue;
+import com.github.tomakehurst.wiremock.http.QueryParameter;
+import com.github.tomakehurst.wiremock.http.Request;
+import com.google.common.base.Function;
+import com.google.common.collect.Maps;
+
+import java.net.URI;
+import java.util.Map;
+
+public class RequestTemplateModel {
+
+ private final String url;
+ private final UrlPath path;
+ private final Map<String, ListOrSingle<String>> query;
+ private final Map<String, ListOrSingle<String>> headers;
+ private final Map<String, ListOrSingle<String>> cookies;
+ private final String body;
+
+
+ public RequestTemplateModel(String url, UrlPath path, Map<String, ListOrSingle<String>> query, Map<String, ListOrSingle<String>> headers, Map<String, ListOrSingle<String>> cookies, String body) {
+ this.url = url;
+ this.path = path;
+ this.query = query;
+ this.headers = headers;
+ this.cookies = cookies;
+ this.body = body;
+ }
+
+ public static RequestTemplateModel from(final Request request) {
+ URI url = URI.create(request.getUrl());
+ Map<String, QueryParameter> rawQuery = Urls.splitQuery(url);
+ Map<String, ListOrSingle<String>> adaptedQuery = Maps.transformValues(rawQuery, TO_TEMPLATE_MODEL);
+ Map<String, ListOrSingle<String>> adaptedHeaders = Maps.toMap(request.getAllHeaderKeys(), new Function<String, ListOrSingle<String>>() {
+ @Override
+ public ListOrSingle<String> apply(String input) {
+ return ListOrSingle.of(request.header(input).values());
+ }
+ });
+ Map<String, ListOrSingle<String>> adaptedCookies = Maps.transformValues(request.getCookies(), new Function<Cookie, ListOrSingle<String>>() {
+ @Override
+ public ListOrSingle<String> apply(Cookie input) {
+ return ListOrSingle.of(input.getValue());
+ }
+ });
+
+ UrlPath path = new UrlPath(request.getUrl());
+
+ return new RequestTemplateModel(
+ request.getUrl(),
+ path,
+ adaptedQuery,
+ adaptedHeaders,
+ adaptedCookies,
+ request.getBodyAsString()
+ );
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public UrlPath getPath() {
+ return path;
+ }
+
+ public Map<String, ListOrSingle<String>> getQuery() {
+ return query;
+ }
+
+ public Map<String, ListOrSingle<String>> getHeaders() {
+ return headers;
+ }
+
+ public Map<String, ListOrSingle<String>> getCookies() {
+ return cookies;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ private static final Function<MultiValue, ListOrSingle<String>> TO_TEMPLATE_MODEL = new Function<MultiValue, ListOrSingle<String>>() {
+ @Override
+ public ListOrSingle<String> apply(MultiValue input) {
+ return ListOrSingle.of(input.values());
+ }
+ };
+}
@@ -0,0 +1,111 @@
+package com.github.tomakehurst.wiremock.extension.responsetemplating;
+
+import com.github.jknack.handlebars.Handlebars;
+import com.github.jknack.handlebars.Helper;
+import com.github.jknack.handlebars.Template;
+import com.github.jknack.handlebars.helper.StringHelpers;
+import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
+import com.github.tomakehurst.wiremock.common.FileSource;
+import com.github.tomakehurst.wiremock.extension.Parameters;
+import com.github.tomakehurst.wiremock.extension.ResponseDefinitionTransformer;
+import com.github.tomakehurst.wiremock.http.HttpHeader;
+import com.github.tomakehurst.wiremock.http.HttpHeaders;
+import com.github.tomakehurst.wiremock.http.Request;
+import com.github.tomakehurst.wiremock.http.ResponseDefinition;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
+
+public class ResponseTemplateTransformer extends ResponseDefinitionTransformer {
+
+ private final boolean global;
+
+ private final Handlebars handlebars;
+
+ public ResponseTemplateTransformer(boolean global) {
+ this(global, Collections.<String, Helper>emptyMap());
+ }
+
+ public ResponseTemplateTransformer(boolean global, String helperName, Helper helper) {
+ this(global, ImmutableMap.of(helperName, helper));
+ }
+
+ public ResponseTemplateTransformer(boolean global, Map<String, Helper> helpers) {
+ this.global = global;
+ handlebars = new Handlebars();
+
+ for (StringHelpers helper: StringHelpers.values()) {
+ handlebars.registerHelper(helper.name(), helper);
+ }
+
+ for (Map.Entry<String, Helper> entry: helpers.entrySet()) {
+ handlebars.registerHelper(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public boolean applyGlobally() {
+ return global;
+ }
+
+ @Override
+ public String getName() {
+ return "response-template";
+ }
+
+ @Override
+ public ResponseDefinition transform(Request request, ResponseDefinition responseDefinition, FileSource files, Parameters parameters) {
+ ResponseDefinitionBuilder newResponseDefBuilder = ResponseDefinitionBuilder.like(responseDefinition);
+ final ImmutableMap<String, RequestTemplateModel> model = ImmutableMap.of("request", RequestTemplateModel.from(request));
+
+ if (responseDefinition.getBody() != null) {
+ Template bodyTemplate = uncheckedCompileTemplate(responseDefinition.getBody());
+ String newBody = uncheckedApplyTemplate(bodyTemplate, model);
+ newResponseDefBuilder.withBody(newBody);
+ }
+
+ if (responseDefinition.getHeaders() != null) {
+ Iterable<HttpHeader> newResponseHeaders = Iterables.transform(responseDefinition.getHeaders().all(), new Function<HttpHeader, HttpHeader>() {
+ @Override
+ public HttpHeader apply(HttpHeader input) {
+ List<String> newValues = Lists.transform(input.values(), new Function<String, String>() {
+ @Override
+ public String apply(String input) {
+ Template template = uncheckedCompileTemplate(input);
+ return uncheckedApplyTemplate(template, model);
+ }
+ });
+
+ return new HttpHeader(input.key(), newValues);
+ }
+ });
+ newResponseDefBuilder.withHeaders(new HttpHeaders(newResponseHeaders));
+ }
+
+ return newResponseDefBuilder.build();
+ }
+
+ private String uncheckedApplyTemplate(Template template, Object context) {
+ try {
+ return template.apply(context);
+ } catch (IOException e) {
+ return throwUnchecked(e, String.class);
+ }
+ }
+
+ private Template uncheckedCompileTemplate(String content) {
+ try {
+ return handlebars.compileInline(content);
+ } catch (IOException e) {
+ return throwUnchecked(e, Template.class);
+ }
+ }
+}
Oops, something went wrong.

0 comments on commit e7882a0

Please sign in to comment.