Request Matching

Overview

API Simulator inspects the HTTP requests it receives by applying matching rules defined in the simlets. Multi-key matching by URI (route), URI path pattern, any URI part, HTTP Method, HTTP Header fields, and values from the body provide for flexible and extensive request matching.

Various operations defined in the SuchThat class exist to perform value matching:

SuchThat.isEqualTo(String)
SuchThat.isEqualToIgnoringCase(String)
SuchThat.startsWith(String)
SuchThat.endsWith(String)
SuchThat.isLike(String regEx)
SuchThat.contains(String)
SuchThat.not(SuchThat)
SuchThat.exists()
SuchThat.isMissing()

Perhaps obvious but just for completeness: isMissing() is equivalent to not(exists()).

Method Matching

Matching requests by the HTTP Method (verb) can be specified in several ways:

httpRequest("GET")
httpRequest().whereMethod(SuchThat.isEqualTo("GET"))

httpRequest(HttpMethod.GET)
httpRequest().whereMethod(HttpMethod.GET)

// These require 'import static com.apisimulator.http.HttpMethod.*;'
httpRequest(GET)
httpRequest().whereMethod(GET)

To not match request’s HTTP Method simply do not specify whereMethod and do not pass HTTP Method as argument to httpRequest().

URI Matching

Here are examples for matching elements of the URI:

// Assuming the URI looks like this:
// https://admin:[email protected]:8090/api/places/json?radius=5&types=food&types=cafe&checked#ref1

.when(httpRequest()
      .whereUri(contains("api/places/json?")) // match the whole URI
      .whereUriPath(isEqualTo("/api/places/json"))
      .whereUriPathMatchesPattern("/api/{collection}/{format}")
      .whereUriScheme(isEqualToIgnoringCase("https"))
      .whereUriUserInfo(startsWith("admin:"))
      .whereUriUserInfo(endsWith(":passW0rd"))
      .whereUriHost(isEqualToIgnoringCase("example.com"))
      .whereUriPort(isEqualTo("8090"))
      .whereQueryParameter("radius", isEqualTo("5"))
      .whereQueryParameter("types", isEqualTo("food"))
      .whereQueryParameter("types", isEqualTo("cafe")) // multi-value parameter
      .whereQueryParameter("checked", exists())
      .whereQueryParameter("blah", not(exists()))
      .whereUriFragment(isEqualTo("ref1"))
)

Header Matching

The following methods are for matching HTTP Header values:

// Specify standard or custom header name
// Header name matching is case insensitive
.whereHeader(String name, SuchThat)

// Standard HTTP/1.1 and HTTP/1.0 header names in the "usual" mixed casing
// Header name matching is case insensitive
.whereHeader(Http1Header headerName, SuchThat)

// Standard HTTP/2 header names in all small letters
// Header name matching is case insensitive
.whereHeader(Http2Header headerName, SuchThat)

HTTP requests may contain cookies in an HTTP Header field called Cookie. A single header field can have multiple cookies separated by semi-colon ;.

API Simulator supports matching individual cookies so you don’t have to extract them yourself from the Cookie header field.

.whereCookie(String cookieName, SuchThat)

// Examples
.whereCookie("JSESSIONID", exists())
.whereCookie("lang", isEqualTo("en-CA"))

Body Elements Matching

Currently there are two flavors of body matching methods: one using any SuchThat operation treating the body as unstructured content, and one that matches elements in structured text.

Unstructured Content

// The method signtature is 'whereBody(SuchThat)'
.whereBody(contains("\"id\":\"5678\""))

XML without Namespaces

Assuming the following XML body:

<ValidateAddress>
  <ValidateAddressInput>
    <Address>
      <Line1>123 Main Street</Line1>
        <City>Anycity</City>
        <StateProvinceCode>ZZ</StateProvinceCode>
        <PostalCode>12345</PostalCode>
        <CountryCode>USA</CountryCode>
    </Address>
  </ValidateAddressInput>
</ValidateAddress>

These can be used to match elements in the XML body:

.whereBody(
   element("ValidateAddress/ValidateAddressInput/Address/Line1"),
   isEqualTo("123 Main Street")
)
.whereBody(
   element("//Address/PostalCode"),
   isEqualTo("12345")
)

XML with Namespaces

Often time XML uses namespaces. This is an example of how to match XML elements when namespaces are in play:

Assuming the following XML body:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
   xmlns:ws="http://ws.v1_0.avs.example.com/">
<s:Header />
<s:Body>
  <ws:ValidateAddress>
    <ValidateAddressInput>
      <Address>
        <Line1>123 Main Street</Line1>
        <City>Anycity</City>
        <StateProvinceCode>ZZ</StateProvinceCode>
        <PostalCode>12345</PostalCode>
        <CountryCode>USA</CountryCode>
      </Address>
    </ValidateAddressInput>
  </ws:ValidateAddress>
</s:Body>
</s:Envelope>

These can be used to match elements in the XML body:

.whereBody(
   element("s:Envelope/s:Body/ws:ValidateAddress/ValidateAddressInput/Address/Line1",
      namespace("s", "http://schemas.xmlsoap.org/soap/envelope/"),
      namespace("ws", "http://ws.v1_0.avs.example.com/")
   ),
   isEqualTo("123 Main Street")
)

// The namespace prefixes in the path (ns1, ns2) do not have to match the
// prefixes in the request (s, ws) as longs as they refer to the same URI
.whereBody(
   element("ns1:Envelope/ns1:Body/ns2:ValidateAddress/ValidateAddressInput/Address/PostalCode",
      namespace("ns1", "http://schemas.xmlsoap.org/soap/envelope/"),
      namespace("ns2", "http://ws.v1_0.avs.example.com/")
   ),
   isEqualTo("12345")
)

JSON Elements

In the absence of a standard for JSON similar to XPath, we developed an extension to the JSON parsing that, internally, safely converts JSON documents to XML documents. Among the things it deals with is JSON field names with spaces, for example. Elements from the original JSON document can then be matched using the same powerful XPath like for XML.

Need not to worry - for the majority of cases there is little to learn beyond how to navigate the structure of the JSON document.

We did look at JSONPath and some of its implementations like this one, but after analyzing its syntax we decided that it is too different (and, arguably, even a bit weird) compared to XPath when a single implementation can handle both JSON and XML types of data.

Please see json.org for the complete JSON syntax. Here are some of the definitions to help understand the conversion we do to XML:

object
    {}
    { members }
members
    pair
    pair , members
pair
    string : value
array
    []
    [ elements ]
elements
    value
    value , elements
value
    string
    number
    object
    array
    true
    false
    null

...see http://json.org/ for the complete JSON syntax.

These are few important conversion rules for converting JSON to XML:

  • We represent { - the start of an unnamed object - with <object> XML tag and the end - } - with </object>.

  • Simple elements of an array are stored between XML tags <value> and </value>.

  • JSON field names with spaces and other characters invalid in an XML tag name become attributes called name of a <field> tag. For example, <field name="country code">US</field>.

The following example should help understand how it works.

Assuming this is the JSON document:

{
  "product":
  {
    "id":"1234",
    "name":"The Jumpers",
    "category":
    [
      "Shoes",
      "Kids"
    ],
    "subCategory":"Basketball",
    "color":"white"
  }
}

…​then this is the XML representation of the JSON document:

<object>
    <product>
        <id>1234</id>
        <category>
            <value>Shoes</value>
            <value>Kids</value>
        </category>
        <color>white</color>
        <subCategory>Basketball</subCategory>
        <name>The Jumpers</name>
    </product>
</object>

…​and these can be used to match elements in the JSON body:

.whereBody(element("object/product/id"), isEqualTo("1234"))
.whereBody(element("/object/product/id"), isEqualTo("1234"))
.whereBody(element("//product/id"), isEqualTo("1234"))

// The first array element
.whereBody(element("object/product/category/value"), isEqualToIgnoringCase("shoes"))
.whereBody(element("object/product/category/value[1]"), isEqualToIgnoringCase("shoes"))

// The second array element
.whereBody(element("object/product/category/value[2]"), isEqualToIgnoringCase("kids"))

// Any of the array elements' value is equal to Kids' (case-sensitive matching)
.whereBody(element("object/product/category[value='Kids']"), exists())

.whereBody(element("object/product/color"), exists())
.whereBody(element("object/product/size"), not(exists()))

Matching Rank

Sometimes, simlet matching has to be applied in a certain order. This is where matching rank comes into play. Here is an example:

apiSim.add(simlet("access-forbidden").withRank(0)
   .when(httpRequest(GET)
      .whereUriPath(isLike("/admin/.*"))
      .whereHeader(AUTHORIZATION, not(exists()))
   )
   .then(httpResponse(FORBIDDEN))
);

apiSim.add(simlet("get-health").withRank(100)
   .when(httpRequest(GET)
      .whereUriPath(isEqualTo("/admin/healthz"))
   )
   .then(httpResponse(OK))
);

API Simulator tries to match simlets with higher matching ranks before simlets with lower ranks; simlets with the same matching rank are tried in no particular order.

The default matching rank is 0 (zero).


We would love to hear your feedback! Shoot us a quick email to [feedback at APISimulator.com] to let us know what you think.

Happy API Simulating!