All Downloads are FREE. Search and download functionalities are using the official Maven repository.

au.com.dius.pact.consumer.dsl.PactDslJsonBody.kt Maven / Gradle / Ivy

Go to download

Pact consumer ============= Pact Consumer is used by projects that are consumers of an API. Most projects will want to use pact-consumer via one of the test framework specific projects. If your favourite framework is not implemented, this module should give you all the hooks you need. Provides a DSL for use with Java to build consumer pacts. ## Dependency The library is available on maven central using: * group-id = `au.com.dius.pact` * artifact-id = `consumer` * version-id = `4.2.x` ## DSL Usage Example in a JUnit test: ```java import au.com.dius.pact.model.MockProviderConfig; import au.com.dius.pact.model.RequestResponsePact; import org.apache.http.entity.ContentType; import org.jetbrains.annotations.NotNull; import org.junit.Test; import java.io.IOException; import java.util.HashMap; import java.util.Map; import static au.com.dius.pact.consumer.ConsumerPactRunnerKt.runConsumerTest; import static org.junit.Assert.assertEquals; public class PactTest { @Test public void testPact() { RequestResponsePact pact = ConsumerPactBuilder .consumer("Some Consumer") .hasPactWith("Some Provider") .uponReceiving("a request to say Hello") .path("/hello") .method("POST") .body("{\"name\": \"harry\"}") .willRespondWith() .status(200) .body("{\"hello\": \"harry\"}") .toPact(); MockProviderConfig config = MockProviderConfig.createDefault(); PactVerificationResult result = runConsumerTest(pact, config, new PactTestRun() { @Override public void run(@NotNull MockServer mockServer) throws IOException { Map expectedResponse = new HashMap(); expectedResponse.put("hello", "harry"); assertEquals(expectedResponse, new ConsumerClient(mockServer.getUrl()).post("/hello", "{\"name\": \"harry\"}", ContentType.APPLICATION_JSON)); } }); if (result instanceof PactVerificationResult.Error) { throw new RuntimeException(((PactVerificationResult.Error)result).getError()); } assertEquals(PactVerificationResult.Ok.INSTANCE, result); } } ``` The DSL has the following pattern: ```java .consumer("Some Consumer") .hasPactWith("Some Provider") .given("a certain state on the provider") .uponReceiving("a request for something") .path("/hello") .method("POST") .body("{\"name\": \"harry\"}") .willRespondWith() .status(200) .body("{\"hello\": \"harry\"}") .uponReceiving("another request for something") .path("/hello") .method("POST") .body("{\"name\": \"harry\"}") .willRespondWith() .status(200) .body("{\"hello\": \"harry\"}") . . . .toPact() ``` You can define as many interactions as required. Each interaction starts with `uponReceiving` followed by `willRespondWith`. The test state setup with `given` is a mechanism to describe what the state of the provider should be in before the provider is verified. It is only recorded in the consumer tests and used by the provider verification tasks. ### Building JSON bodies with PactDslJsonBody DSL The body method of the ConsumerPactBuilder can accept a PactDslJsonBody, which can construct a JSON body as well as define regex and type matchers. For example: ```java PactDslJsonBody body = new PactDslJsonBody() .stringType("name") .booleanType("happy") .hexValue("hexCode") .id() .ipAddress("localAddress") .numberValue("age", 100) .timestamp(); ``` #### DSL Matching methods The following matching methods are provided with the DSL. In most cases, they take an optional value parameter which will be used to generate example values (i.e. when returning a mock response). If no example value is given, a random one will be generated. | method | description | |--------|-------------| | string, stringValue | Match a string value (using string equality) | | number, numberValue | Match a number value (using Number.equals)\* | | booleanValue | Match a boolean value (using equality) | | stringType | Will match all Strings | | numberType | Will match all numbers\* | | integerType | Will match all numbers that are integers (both ints and longs)\* | | decimalType | Will match all real numbers (floating point and decimal)\* | | booleanType | Will match all boolean values (true and false) | | stringMatcher | Will match strings using the provided regular expression | | timestamp | Will match string containing timestamps. If a timestamp format is not given, will match an ISO timestamp format | | date | Will match string containing dates. If a date format is not given, will match an ISO date format | | time | Will match string containing times. If a time format is not given, will match an ISO time format | | ipAddress | Will match string containing IP4 formatted address. | | id | Will match all numbers by type | | hexValue | Will match all hexadecimal encoded strings | | uuid | Will match strings containing UUIDs | | includesStr | Will match strings containing the provided string | | equalsTo | Will match using equals | | matchUrl | Defines a matcher for URLs, given the base URL path and a sequence of path fragments. The path fragments could be strings or regular expression matchers | | nullValue | Matches the JSON Null value | _\* Note:_ JSON only supports double precision floating point values. Depending on the language implementation, they may be parsed as integer, floating point or decimal numbers. #### Ensuring all items in a list match an example Lots of the time you might not know the number of items that will be in a list, but you want to ensure that the list has a minimum or maximum size and that each item in the list matches a given example. You can do this with the `arrayLike`, `minArrayLike` and `maxArrayLike` functions. | function | description | |----------|-------------| | `eachLike` | Ensure that each item in the list matches the provided example | | `maxArrayLike` | Ensure that each item in the list matches the provided example and the list is no bigger than the provided max | | `minArrayLike` | Ensure that each item in the list matches the provided example and the list is no smaller than the provided min | For example: ```java DslPart body = new PactDslJsonBody() .minArrayLike("users") .id() .stringType("name") .closeObject() .closeArray(); ``` This will ensure that the users list is never empty and that each user has an identifier that is a number and a name that is a string. #### Ignoring the list order (V4 specification) If the order of the list items is not known, you can use the `unorderedArray` matcher functions. These will match the actual list against the expected one, except will match the items in any order. | function | description | |----------|-------------| | `unorderedArray` | Ensure that the list matches the provided example, ignoring the order | | `unorderedMinArray` | Ensure that the list matches the provided example and the list is not smaller than the provided min | | `unorderedMaxArray` | Ensure that the list matches the provided example and the list is no bigger than the provided max | | `unorderedMinMaxArray` | Ensure that the list matches the provided example and the list is constrained to the provided min and max | #### Array contains matcher (V4 specification) The array contains matcher functions allow you to match the actual list against a list of required variants. These work by matching each item against the variants, and the matching succeeds if each variant matches at least one item. Order of items in the list is not important. The variants can have a totally different structure, and can have their own matching rules to apply. For an example of how these can be used to match a hypermedia format like Siren, see [Example Pact + Siren project](https://github.com/pactflow/example-siren). | function | description | |----------|-------------| | `arrayContaining` | Matches the items in an array against a number of variants. Matching is successful if each variant occurs once in the array. Variants may be objects containing matching rules. | ```java .arrayContaining("actions") .object() .stringValue("name", "update") .stringValue("method", "PUT") .matchUrl("href", "http://localhost:9000", "orders", regex("\\d+", "1234")) .closeObject() .object() .stringValue("name", "delete") .stringValue("method", "DELETE") .matchUrl("href", "http://localhost:9000", "orders", regex("\\d+", "1234")) .closeObject() .closeArray() ``` #### Matching JSON values at the root For cases where you are expecting basic JSON values (strings, numbers, booleans and null) at the root level of the body and need to use matchers, you can use the `PactDslJsonRootValue` class. It has all the DSL matching methods for basic values that you can use. For example: ```java .consumer("Some Consumer") .hasPactWith("Some Provider") .uponReceiving("a request for a basic JSON value") .path("/hello") .willRespondWith() .status(200) .body(PactDslJsonRootValue.integerType()) ``` #### Root level arrays that match all items If the root of the body is an array, you can create PactDslJsonArray classes with the following methods: | function | description | |----------|-------------| | `arrayEachLike` | Ensure that each item in the list matches the provided example | | `arrayMaxLike` | Ensure that each item in the list matches the provided example and the list is no bigger than the provided max | | `arrayMinLike` | Ensure that each item in the list matches the provided example and the list is no smaller than the provided min | For example: ```java PactDslJsonArray.arrayEachLike() .date("clearedDate", "mm/dd/yyyy", date) .stringType("status", "STATUS") .decimalType("amount", 100.0) .closeObject() ``` This will then match a body like: ```json [ { "clearedDate" : "07/22/2015", "status" : "C", "amount" : 15.0 }, { "clearedDate" : "07/22/2015", "status" : "C", "amount" : 15.0 }, { "clearedDate" : "07/22/2015", "status" : "C", "amount" : 15.0 } ] ``` #### Matching arrays of arrays For the case where you have arrays of arrays (GeoJSON is an example), the following methods have been provided: | function | description | |----------|-------------| | `eachArrayLike` | Ensure that each item in the array is an array that matches the provided example | | `eachArrayWithMaxLike` | Ensure that each item in the array is an array that matches the provided example and the array is no bigger than the provided max | | `eachArrayWithMinLike` | Ensure that each item in the array is an array that matches the provided example and the array is no smaller than the provided min | For example (with GeoJSON structure): ```java new PactDslJsonBody() .stringType("type","FeatureCollection") .eachLike("features") .stringType("type","Feature") .object("geometry") .stringType("type","Point") .eachArrayLike("coordinates") // coordinates is an array of arrays .decimalType(-7.55717) .decimalType(49.766896) .closeArray() .closeArray() .closeObject() .object("properties") .stringType("prop0","value0") .closeObject() .closeObject() .closeArray() ``` This generated the following JSON: ```json { "features": [ { "geometry": { "coordinates": [[-7.55717, 49.766896]], "type": "Point" }, "type": "Feature", "properties": { "prop0": "value0" } } ], "type": "FeatureCollection" } ``` and will be able to match all coordinates regardless of the number of coordinates. #### Matching any key in a map The DSL has been extended for cases where the keys in a map are IDs. For an example of this, see [#313](https://github.com/DiUS/pact-jvm/issues/313). In this case you can use the `eachKeyLike` method, which takes an example key as a parameter. For example: ```java DslPart body = new PactDslJsonBody() .object("one") .eachKeyLike("001", PactDslJsonRootValue.id(12345L)) // key like an id mapped to a matcher .closeObject() .object("two") .eachKeyLike("001-A") // key like an id where the value is matched by the following example .stringType("description", "Some Description") .closeObject() .closeObject() .object("three") .eachKeyMappedToAnArrayLike("001") // key like an id mapped to an array where each item is matched by the following example .id("someId", 23456L) .closeObject() .closeArray() .closeObject(); ``` For an example, have a look at [ArticlesTest](https://github.com/pact-foundation/pact-jvm/blob/master/consumer/junit/src/test/java/au/com/dius/pact/consumer/junit/examples/ArticlesTest.java). ### Matching on paths You can use regular expressions to match incoming requests. The DSL has a `matchPath` method for this. You can provide a real path as a second value to use when generating requests, and if you leave it out it will generate a random one from the regular expression. For example: ```java .given("test state") .uponReceiving("a test interaction") .matchPath("/transaction/[0-9]+") // or .matchPath("/transaction/[0-9]+", "/transaction/1234567890") .method("POST") .body("{\"name\": \"harry\"}") .willRespondWith() .status(200) .body("{\"hello\": \"harry\"}") ``` ### Matching on headers You can use regular expressions to match request and response headers. The DSL has a `matchHeader` method for this. You can provide an example header value to use when generating requests and responses, and if you leave it out it will generate a random one from the regular expression. For example: ```java .given("test state") .uponReceiving("a test interaction") .path("/hello") .method("POST") .matchHeader("testreqheader", "test.*value") .body("{\"name\": \"harry\"}") .willRespondWith() .status(200) .body("{\"hello\": \"harry\"}") .matchHeader("Location", ".*/hello/[0-9]+", "/hello/1234") ``` ### Matching on query parameters You can use regular expressions to match request query parameters. The DSL has a `matchQuery` method for this. You can provide an example value to use when generating requests, and if you leave it out it will generate a random one from the regular expression. For example: ```java .given("test state") .uponReceiving("a test interaction") .path("/hello") .method("POST") .matchQuery("a", "\\d+", "100") .matchQuery("b", "[A-Z]", "X") .body("{\"name\": \"harry\"}") .willRespondWith() .status(200) .body("{\"hello\": \"harry\"}") ``` # Forcing pact files to be overwritten By default, when the pact file is written, it will be merged with any existing pact file. To force the file to be overwritten, set the Java system property `pact.writer.overwrite` to `true`. # Having values injected from provider state callbacks You can have values from the provider state callbacks be injected into most places (paths, query parameters, headers, bodies, etc.). This works by using the V3 spec generators with provider state callbacks that return values. One example of where this would be useful is API calls that require an ID which would be auto-generated by the database on the provider side, so there is no way to know what the ID would be beforehand. The following DSL methods allow you to set an expression that will be parsed with the values returned from the provider states: For JSON bodies, use `valueFromProviderState`.<br/> For headers, use `headerFromProviderState`.<br/> For query parameters, use `queryParameterFromProviderState`.<br/> For paths, use `pathFromProviderState`. For example, assume that an API call is made to get the details of a user by ID. A provider state can be defined that specifies that the user must be exist, but the ID will be created when the user is created. So we can then define an expression for the path where the ID will be replaced with the value returned from the provider state callback. ```java .pathFromProviderState("/api/users/${id}", "/api/users/100") ``` You can also just use the key instead of an expression: ```java .valueFromProviderState('userId', 'userId', 100) // will look value using userId as the key ``` # A Lambda DSL for Pact This is an extension for the pact DSL. The difference between the default pact DSL and this lambda DSL is, as the name suggests, the usage of lambdas. The use of lambdas makes the code much cleaner. ## Why a new DSL implementation? The lambda DSL solves the following two main issues. Both are visible in the following code sample: ```java new PactDslJsonArray() .array() # open an array .stringValue("a1") # choose the method that is valid for arrays .stringValue("a2") # choose the method that is valid for arrays .closeArray() # close the array .array() # open an array .numberValue(1) # choose the method that is valid for arrays .numberValue(2) # choose the method that is valid for arrays .closeArray() # close the array .array() # open an array .object() # now we work with an object .stringValue("foo", "Foo") # choose the method that is valid for objects .closeObject() # close the object and we're back in the array .closeArray() # close the array ``` ### The existing DSL is quite error-prone Methods may only be called in certain states. For example `object()` may only be called when you're currently working on an array whereas `object(name)` is only allowed to be called when working on an object. But both of the methods are available. You'll find out at runtime if you're using the correct method. Finally, the need for opening and closing objects and arrays makes usage cumbersome. The lambda DSL has no ambiguous methods and there's no need to close objects and arrays as all the work on such an object is wrapped in a lamda call. ### The existing DSL is hard to read When formatting your source code with an IDE the code becomes hard to read as there's no indentation possible. Of course, you could do it by hand but we want auto formatting! Auto formatting works great for the new DSL! ```java array.object((o) -> { o.stringValue("foo", "Foo"); # an attribute o.stringValue("bar", "Bar"); # an attribute o.object("tar", (tarObject) -> { # an attribute with a nested object tarObject.stringValue("a", "A"); # attribute of the nested object tarObject.stringValue("b", "B"); # attribute of the nested object }) }); ``` ## Usage Start with a static import of `LambdaDsl`. This class contains factory methods for the lambda dsl extension. When you come accross the `body()` method of `PactDslWithProvider` builder start using the new extensions. The call to `LambdaDsl` replaces the call to instance `new PactDslJsonArray()` and `new PactDslJsonBody()` of the pact library. ```java io.pactfoundation.consumer.dsl.LambdaDsl.* ``` ### Response body as json array ```java import static io.pactfoundation.consumer.dsl.LambdaDsl.newJsonArray; ... PactDslWithProvider builder = ... builder.given("some state") .uponReceiving("a request") .path("/my-app/my-service") .method("GET") .willRespondWith() .status(200) .body(newJsonArray((a) -> { a.stringValue("a1"); a.stringValue("a2"); }).build()); ``` ### Response body as json object ```java import static io.pactfoundation.consumer.dsl.LambdaDsl.newJsonBody; ... PactDslWithProvider builder = ... builder.given("some state") .uponReceiving("a request") .path("/my-app/my-service") .method("GET") .willRespondWith() .status(200) .body(newJsonBody((o) -> { o.stringValue("foo", "Foo"); o.stringValue("bar", "Bar"); }).build()); ``` ### Examples #### Simple Json object When creating simple json structures the difference between the two approaches isn't big. ##### JSON ```json { "bar": "Bar", "foo": "Foo" } ``` ##### Pact DSL ```java new PactDslJsonBody() .stringValue("foo", "Foo") .stringValue("bar", "Bar") ``` ##### Lambda DSL ```java newJsonBody((o) -> { o.stringValue("foo", "Foo"); o.stringValue("bar", "Bar"); }).build(); ``` #### An array of arrays When we come to more complex constructs with arrays and nested objects the beauty of lambdas become visible! ##### JSON ```json [ ["a1", "a2"], [1, 2], [{"foo": "Foo"}] ] ``` ##### Pact DSL ```java new PactDslJsonArray() .array() .stringValue("a1") .stringValue("a2") .closeArray() .array() .numberValue(1) .numberValue(2) .closeArray() .array() .object() .stringValue("foo", "Foo") .closeObject() .closeArray(); ``` ##### Lambda DSL ```java newJsonArray((rootArray) -> { rootArray.array((a) -> a.stringValue("a1").stringValue("a2")); rootArray.array((a) -> a.numberValue(1).numberValue(2)); rootArray.array((a) -> a.object((o) -> o.stringValue("foo", "Foo"))); }).build(); ``` ##### Kotlin Lambda DSL ```kotlin newJsonArray { newArray { stringValue("a1") stringValue("a2") } newArray { numberValue(1) numberValue(2) } newArray { newObject { stringValue("foo", "Foo") } } } ``` ## Dealing with persistent HTTP/1.1 connections (Keep Alive) As each test will get a new mock server, connections can not be persisted between tests. HTTP clients can cache connections with HTTP/1.1, and this can cause subsequent tests to fail. See [#342](https://github.com/pact-foundation/pact-jvm/issues/342) and [#1383](https://github.com/pact-foundation/pact-jvm/issues/1383). One option (if the HTTP client supports it, Apache HTTP Client does) is to set the system property `http.keepAlive` to `false` in the test JVM. The other option is to set `pact.mockserver.addCloseHeader` to `true` to force the mock server to send a `Connection: close` header with every response (supported with Pact-JVM 4.2.7+).

The newest version!
package au.com.dius.pact.consumer.dsl

import au.com.dius.pact.consumer.InvalidMatcherException
import au.com.dius.pact.consumer.dsl.Dsl.matcherKey
import au.com.dius.pact.core.matchers.UrlMatcherSupport
import au.com.dius.pact.core.model.constructValidPath
import au.com.dius.pact.core.model.generators.Category
import au.com.dius.pact.core.model.generators.DateGenerator
import au.com.dius.pact.core.model.generators.DateTimeGenerator
import au.com.dius.pact.core.model.generators.MockServerURLGenerator
import au.com.dius.pact.core.model.generators.ProviderStateGenerator
import au.com.dius.pact.core.model.generators.RandomDecimalGenerator
import au.com.dius.pact.core.model.generators.RandomHexadecimalGenerator
import au.com.dius.pact.core.model.generators.RandomIntGenerator
import au.com.dius.pact.core.model.generators.RandomStringGenerator
import au.com.dius.pact.core.model.generators.RegexGenerator
import au.com.dius.pact.core.model.generators.TimeGenerator
import au.com.dius.pact.core.model.generators.UuidGenerator
import au.com.dius.pact.core.model.matchingrules.EachKeyMatcher
import au.com.dius.pact.core.model.matchingrules.EqualsIgnoreOrderMatcher
import au.com.dius.pact.core.model.matchingrules.EqualsMatcher
import au.com.dius.pact.core.model.matchingrules.MatchingRule
import au.com.dius.pact.core.model.matchingrules.MatchingRuleGroup
import au.com.dius.pact.core.model.matchingrules.MaxEqualsIgnoreOrderMatcher
import au.com.dius.pact.core.model.matchingrules.MinEqualsIgnoreOrderMatcher
import au.com.dius.pact.core.model.matchingrules.MinMaxEqualsIgnoreOrderMatcher
import au.com.dius.pact.core.model.matchingrules.NumberTypeMatcher
import au.com.dius.pact.core.model.matchingrules.RegexMatcher
import au.com.dius.pact.core.model.matchingrules.RuleLogic
import au.com.dius.pact.core.model.matchingrules.TypeMatcher
import au.com.dius.pact.core.model.matchingrules.ValuesMatcher
import au.com.dius.pact.core.model.matchingrules.expressions.MatchingRuleDefinition
import au.com.dius.pact.core.support.Json.toJson
import au.com.dius.pact.core.support.Random
import au.com.dius.pact.core.support.expressions.DataType.Companion.from
import au.com.dius.pact.core.support.json.JsonValue
import au.com.dius.pact.core.support.padTo
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.time.DateFormatUtils
import org.apache.commons.lang3.time.FastDateFormat
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.TimeZone
import java.util.UUID
import java.util.regex.Pattern

/**
 * DSL to define a JSON Object
 */
@Suppress("LargeClass", "TooManyFunctions", "SpreadOperator")
open class PactDslJsonBody : DslPart {
  override var body: JsonValue = JsonValue.Object()

  /**
   * Constructs a new body as a root
   */
  constructor() : super(".", "") {
    body = JsonValue.Object()
  }

  /**
   * Constructs a new body as a child
   * @param rootPath Path to prefix to this child
   * @param rootName Name to associate this object as in the parent
   * @param parent Parent to attach to
   */
  constructor(rootPath: String, rootName: String, parent: DslPart?) : super(parent, rootPath, rootName) {
    body = JsonValue.Object()
  }

  /**
   * Constructs a new body as a child as a copy of an existing one
   * @param rootPath Path to prefix to this child
   * @param rootName Name to associate this object as in the parent
   * @param parent Parent to attach to
   * @param body Body to copy values from
   */
  constructor(rootPath: String, rootName: String, parent: DslPart?, body: PactDslJsonBody)
    : super(parent, rootPath, rootName) {
    this.body = body.body
    matchers = body.matchers.copyWithUpdatedMatcherRootPrefix(rootPath)
    generators = body.generators.copyWithUpdatedMatcherRootPrefix(rootPath)
  }

  /**
   * Constructs a new body as a child of an array
   * @param rootPath Path to prefix to this child
   * @param rootName Name to associate this object as in the parent
   * @param parent Parent to attach to
   * @param examples Number of examples to generate
   */
  constructor(rootPath: String, rootName: String, parent: PactDslJsonArray, examples: Int)
    : super(parent, rootPath, rootName) {
    this.body = JsonValue.Array(1.rangeTo(examples).map { JsonValue.Object() }.toMutableList())
  }

  override fun toString(): String {
    return body.toString()
  }

  override fun putObjectPrivate(obj: DslPart) {
    for (matcherName in obj.matchers.matchingRules.keys) {
      matchers.setRules(matcherName, obj.matchers.matchingRules[matcherName]!!)
    }
    generators.addGenerators(obj.generators)

    val elementBase = StringUtils.difference(rootPath, obj.rootPath)
    when (val body = body) {
      is JsonValue.Object -> {
        if (StringUtils.isNotEmpty(obj.rootName)) {
          body.add(obj.rootName, obj.body)
        } else {
          val name = StringUtils.strip(elementBase, ".")
          val p = Pattern.compile("\\['(.+)'\\]")
          val matcher = p.matcher(name)
          if (matcher.matches()) {
            body.add(matcher.group(1), obj.body)
          } else {
            body.add(name, obj.body)
          }
        }
      }
      is JsonValue.Array -> body.values.forEach { v ->
        if (StringUtils.isNotEmpty(obj.rootName)) {
          v.asObject()!!.add(obj.rootName, obj.body)
        } else {
          val name = StringUtils.strip(elementBase, ".")
          val p = Pattern.compile("\\['(.+)'\\]")
          val matcher = p.matcher(name)
          if (matcher.matches()) {
            v.asObject()!!.add(matcher.group(1), obj.body)
          } else {
            v.asObject()!!.add(name, obj.body)
          }
        }
      }
      else -> {}
    }
  }

  override fun putArrayPrivate(obj: DslPart) {
    for (matcherName in obj.matchers.matchingRules.keys) {
      matchers.setRules(matcherName, obj.matchers.matchingRules[matcherName]!!)
    }
    generators.addGenerators(obj.generators)

    when (val body = body) {
      is JsonValue.Object -> {
        if (StringUtils.isNotEmpty(obj.rootName)) {
          body.add(obj.rootName, obj.body)
        } else {
          body.add(StringUtils.difference(rootPath, obj.rootPath), obj.body)
        }
      }
      is JsonValue.Array -> body.values.forEach { v ->
        if (StringUtils.isNotEmpty(obj.rootName)) {
          v.asObject()!!.add(obj.rootName, obj.body)
        } else {
          v.asObject()!!.add(StringUtils.difference(rootPath, obj.rootPath), obj.body)
        }
      }
      else -> {}
    }
  }

  /**
   * Attribute that must be the specified value
   * @param name attribute name
   * @param value string value
   */
  fun stringValue(name: String, vararg values: String?): PactDslJsonBody {
    require(values.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(values.size == 1) {
        "You provided multiple example values (${values.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= values.size) {
        "You provided ${values.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> {
        if (values[0] == null) {
          body.add(name, JsonValue.Null)
        } else {
          body.add(name, JsonValue.StringValue(values[0]!!.toCharArray()))
        }
      }
      is JsonValue.Array -> {
        values.padTo(body.size()).forEachIndexed { i, value ->
          if (value == null) {
            body[i].asObject()!!.add(name, JsonValue.Null)
          } else {
            body[i].asObject()!!.add(name, JsonValue.StringValue(value.toCharArray()))
          }
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must be the specified number
   * @param name attribute name
   * @param value number value
   */
  fun numberValue(name: String, vararg values: Number): PactDslJsonBody {
    require(values.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(values.size == 1) {
        "You provided multiple example values (${values.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= values.size) {
        "You provided ${values.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Decimal(values[0].toString().toCharArray()))
      is JsonValue.Array -> {
        values.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.Decimal(value.toString().toCharArray()))
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must be the specified boolean
   * @param name attribute name
   * @param value boolean value
   */
  fun booleanValue(name: String, vararg values: Boolean?): PactDslJsonBody {
    require(values.isNotEmpty()) {
      "At least one example value is required"
    }
    require(values.none { it == null }) {
      "Example values can not be null"
    }
    if (body is JsonValue.Object) {
      require(values.size == 1) {
        "You provided multiple example values (${values.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= values.size) {
        "You provided ${values.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, if (values[0]!!) JsonValue.True else JsonValue.False)
      is JsonValue.Array -> {
        values.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, if (value!!) JsonValue.True else JsonValue.False)
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must be the same type as the example
   * @param name attribute name
   */
  open fun like(name: String, vararg examples: Any?): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, toJson(examples[0]))
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, toJson(value))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), TypeMatcher)

    return this
  }

  /**
   * Attribute that can be any string
   * @param name attribute name
   */
  fun stringType(name: String): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath), RandomStringGenerator(20))
    return stringType(name, *examples("string"))
  }

  private fun examples(example: String): Array {
    return when (val body = body) {
      is JsonValue.Array -> 1.rangeTo(body.size).map { example }.toTypedArray()
      else -> arrayOf(example)
    }
  }

  /**
   * Attributes that can be any string
   * @param names attribute names
   */
  fun stringTypes(vararg names: String?): PactDslJsonBody {
    require(names.none { it == null }) {
      "Attribute names can not be null"
    }
    for (name in names) {
      stringType(name!!)
    }
    return this
  }

  /**
   * Attribute that can be any string
   * @param name attribute name
   * @param example example value to use for generated bodies
   */
  fun stringType(name: String, vararg examples: String?): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    require(examples.none { it == null }) {
      "Example values can not be null"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(examples[0]!!.toCharArray()))
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.StringValue(value!!.toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), TypeMatcher)

    return this
  }

  /**
   * Attribute that can be any number
   * @param name attribute name
   */
  fun numberType(name: String): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath), RandomIntGenerator(0, Int.MAX_VALUE))
    return numberType(name, 100)
  }

  /**
   * Attributes that can be any number
   * @param names attribute names
   */
  fun numberTypes(vararg names: String?): PactDslJsonBody {
    require(names.isNotEmpty()) {
      "At least one attribute name is required"
    }
    require(names.none { it == null }) {
      "Attribute names can not be null"
    }
    for (name in names) {
      numberType(name!!)
    }
    return this
  }

  /**
   * Attribute that can be any number
   * @param name attribute name
   * @param number example number to use for generated bodies
   */
  fun numberType(name: String, vararg numbers: Number?): PactDslJsonBody {
    require(numbers.isNotEmpty()) {
      "At least one example value is required"
    }
    require(numbers.none { it == null }) {
      "Example values can not be null"
    }
    if (body is JsonValue.Object) {
      require(numbers.size == 1) {
        "You provided multiple example values (${numbers.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= numbers.size) {
        "You provided ${numbers.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Decimal(numbers[0]!!.toString().toCharArray()))
      is JsonValue.Array -> {
        numbers.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.Decimal(value!!.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER))

    return this
  }

  /**
   * Attribute that must be an integer
   * @param name attribute name
   */
  fun integerType(name: String): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name!!, rootPath), RandomIntGenerator(0, Int.MAX_VALUE))
    return integerType(name, 100 as Int)
  }

  /**
   * Attributes that must be an integer
   * @param names attribute names
   */
  fun integerTypes(vararg names: String?): PactDslJsonBody {
    require(names.isNotEmpty()) {
      "At least one attribute name is required"
    }
    require(names.none { it == null }) {
      "Attribute names can not be null"
    }
    for (name in names) {
      integerType(name!!)
    }
    return this
  }

  /**
   * Attribute that must be an integer
   * @param name attribute name
   * @param number example integer value to use for generated bodies
   */
  fun integerType(name: String, vararg numbers: Long?): PactDslJsonBody {
    require(numbers.isNotEmpty()) {
      "At least one example value is required"
    }
    require(numbers.none { it == null }) {
      "Example values can not be null"
    }
    if (body is JsonValue.Object) {
      require(numbers.size == 1) {
        "You provided multiple example values (${numbers.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= numbers.size) {
        "You provided ${numbers.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Integer(numbers[0]!!.toString().toCharArray()))
      is JsonValue.Array -> {
        numbers.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.Integer(value!!.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER))

    return this
  }

  /**
   * Attribute that must be an integer
   * @param name attribute name
   * @param number example integer value to use for generated bodies
   */
  fun integerType(name: String, vararg numbers: Int?): PactDslJsonBody {
    require(numbers.isNotEmpty()) {
      "At least one example value is required"
    }
    require(numbers.none { it == null }) {
      "Example values can not be null"
    }
    if (body is JsonValue.Object) {
      require(numbers.size == 1) {
        "You provided multiple example values (${numbers.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= numbers.size) {
        "You provided ${numbers.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Integer(numbers[0]!!.toString().toCharArray()))
      is JsonValue.Array -> {
        numbers.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.Integer(value!!.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER))

    return this
  }

  /**
   * Attribute that must be a decimal value (has significant digits after the decimal point)
   * @param name attribute name
   */
  fun decimalType(name: String): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath), RandomDecimalGenerator(10))
    return decimalType(name, 100.0)
  }

  /**
   * Attributes that must be a decimal values (have significant digits after the decimal point)
   * @param names attribute names
   */
  fun decimalTypes(vararg names: String?): PactDslJsonBody {
    require(names.isNotEmpty()) {
      "At least one attribute name is required"
    }
    require(names.none { it == null }) {
      "Attribute names can not be null"
    }
    for (name in names) {
      decimalType(name!!)
    }
    return this
  }

  /**
   * Attribute that must be a decimalType value (has significant digits after the decimal point)
   * @param name attribute name
   * @param number example decimalType value
   */
  fun decimalType(name: String, vararg numbers: BigDecimal?): PactDslJsonBody {
    require(numbers.isNotEmpty()) {
      "At least one example value is required"
    }
    require(numbers.none { it == null }) {
      "Example values can not be null"
    }
    if (body is JsonValue.Object) {
      require(numbers.size == 1) {
        "You provided multiple example values (${numbers.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= numbers.size) {
        "You provided ${numbers.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Decimal(numbers[0]!!.toString().toCharArray()))
      is JsonValue.Array -> {
        numbers.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.Decimal(value!!.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL))

    return this
  }

  /**
   * Attribute that must be a decimalType value (has significant digits after the decimal point)
   * @param name attribute name
   * @param number example decimalType value
   */
  fun decimalType(name: String, vararg numbers: Double?): PactDslJsonBody {
    require(numbers.isNotEmpty()) {
      "At least one example value is required"
    }
    require(numbers.none { it == null }) {
      "Example values can not be null"
    }
    if (body is JsonValue.Object) {
      require(numbers.size == 1) {
        "You provided multiple example values (${numbers.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= numbers.size) {
        "You provided ${numbers.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Decimal(numbers[0]!!.toString().toCharArray()))
      is JsonValue.Array -> {
        numbers.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.Decimal(value!!.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL))

    return this
  }

  /**
   * Attribute that can be any number and which must match the provided regular expression
   * @param name attribute name
   * @param regex Regular expression that the numbers string form must match
   * @param example example number to use for generated bodies
   */
  fun numberMatching(name: String, regex: String, example: Number): PactDslJsonBody {
    require(example.toString().matches(Regex(regex))) {
      "Example value $example does not match the provided regular expression '$regex'"
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Decimal(example.toString().toCharArray()))
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, JsonValue.Decimal(example.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRules(constructValidPath(name, rootPath), listOf(
      NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER),
      RegexMatcher(regex, example.toString())
    ))

    return this
  }

  /**
   * Attribute that can be any number decimal number (has significant digits after the decimal point) and which must
   * match the provided regular expression
   * @param name attribute name
   * @param regex Regular expression that the numbers string form must match
   * @param example example number to use for generated bodies
   */
  fun decimalMatching(name: String, regex: String, example: Double): PactDslJsonBody {
    require(example.toString().matches(Regex(regex))) {
      "Example value $example does not match the provided regular expression '$regex'"
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Decimal(example.toString().toCharArray()))
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, JsonValue.Decimal(example.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRules(constructValidPath(name, rootPath), listOf(
      NumberTypeMatcher(NumberTypeMatcher.NumberType.DECIMAL),
      RegexMatcher(regex, example.toString())
    ))

    return this
  }

  /**
   * Attribute that can be any integer and which must match the provided regular expression
   * @param name attribute name
   * @param regex Regular expression that the numbers string form must match
   * @param example example integer to use for generated bodies
   */
  fun integerMatching(name: String, regex: String, example: Int): PactDslJsonBody {
    require(example.toString().matches(Regex(regex))) {
      "Example value $example does not match the provided regular expression $regex"
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Integer(example.toString().toCharArray()))
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, JsonValue.Integer(example.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRules(constructValidPath(name, rootPath), listOf(
            NumberTypeMatcher(NumberTypeMatcher.NumberType.INTEGER),
            RegexMatcher(regex, example.toString())
    ))

    return this
  }

  /**
   * Attributes that must be a boolean
   * @param names attribute names
   */
  fun booleanTypes(vararg names: String?): PactDslJsonBody {
    require(names.isNotEmpty()) {
      "At least one attribute name is required"
    }
    require(names.none { it == null }) {
      "Attribute names can not be null"
    }
    for (name in names) {
      booleanType(name!!)
    }
    return this
  }

  /**
   * Attribute that must be a boolean
   * @param name attribute name
   * @param example example boolean to use for generated bodies
   */
  @JvmOverloads
  fun booleanType(name: String, vararg examples: Boolean? = arrayOf(true)): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    require(examples.none { it == null }) {
      "Example values can not be null"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, if (examples[0]!!) JsonValue.True else JsonValue.False)
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, if (value!!) JsonValue.True else JsonValue.False)
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), TypeMatcher)

    return this
  }

  /**
   * Attribute that must match the regular expression
   * @param name attribute name
   * @param regex regular expression
   * @param value example value to use for generated bodies
   */
  @Suppress("ThrowsCount")
  fun stringMatcher(name: String, regex: String, vararg values: String?): PactDslJsonBody {
    require(values.isNotEmpty()) {
      "At least one example value is required"
    }
    require(values.none { it == null }) {
      "Example values can not be null"
    }
    if (body is JsonValue.Object) {
      require(values.size == 1) {
        "You provided multiple example values (${values.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= values.size) {
        "You provided ${values.size} example values but ${body.size()} was expected"
      }
    }

    val re = Regex(regex)
    when (val body = body) {
      is JsonValue.Object -> {
        if (!values[0]!!.matches(re)) {
          throw InvalidMatcherException("Example \"${values[0]}\" does not match regular expression \"$regex\"")
        }
        body.add(name, JsonValue.StringValue(values[0]!!.toCharArray()))
      }
      is JsonValue.Array -> {
        values.padTo(body.size()).forEachIndexed { i, value ->
          if (!value!!.matches(re)) {
            throw InvalidMatcherException("Example \"$value\" does not match regular expression \"$regex\"")
          }
          body[i].asObject()!!.add(name, JsonValue.StringValue(value.toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), regexp(regex))

    return this
  }

  /**
   * Attribute that must match the regular expression
   * @param name attribute name
   * @param regex regular expression
   */
  fun stringMatcher(name: String, regex: String): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath), RegexGenerator(regex))
    stringMatcher(name, regex, *examples(Random.generateRandomString(regex)))
    return this
  }

  /**
   * Attribute that must be an ISO formatted datetime
   * @param name
   */
  fun datetime(name: String): PactDslJsonBody {
    val pattern = DateFormatUtils.ISO_DATETIME_FORMAT.pattern
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath), DateTimeGenerator(pattern, null))
    matchers.addRule(matcherKey(name, rootPath), matchTimestamp(pattern))

    val stringValue = JsonValue.StringValue(DateFormatUtils.ISO_DATETIME_FORMAT.format(Date(DATE_2000)).toCharArray())
    when (val body = body) {
      is JsonValue.Object -> body.add(name, stringValue)
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, stringValue)
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must match the given datetime format
   * @param name attribute name
   * @param format datetime format
   */
  fun datetime(name: String, format: String): PactDslJsonBody {
    val path = constructValidPath(name, rootPath)
    generators.addGenerator(Category.BODY, path, DateTimeGenerator(format, null))
    val formatter = DateTimeFormatter.ofPattern(format).withZone(ZoneId.systemDefault())
    matchers.addRule(path, matchTimestamp(format))

    val stringValue = JsonValue.StringValue(formatter.format(Date(DATE_2000).toInstant()).toCharArray())
    when (val body = body) {
      is JsonValue.Object -> body.add(name, stringValue)
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, stringValue)
        }
      }
      else -> {}
    }

    return this
  }


  /**
   * Attribute that must match the given datetime format
   * @param name attribute name
   * @param format datetime format
   * @param example example date and time to use for generated bodies
   * @param timeZone time zone used for formatting of example date and time
   */
  @JvmOverloads
  fun datetime(
    name: String,
    format: String,
    example: Date,
    timeZone: TimeZone = TimeZone.getDefault()
  ) = datetime(name, format, timeZone, example)

  /**
   * Attribute that must match the given datetime format
   * @param name attribute name
   * @param format datetime format
   * @param example example date and time to use for generated bodies
   * @param timeZone time zone used for formatting of example date and time
   */
  @JvmOverloads
  fun datetime(
    name: String,
    format: String,
    timeZone: TimeZone = TimeZone.getDefault(),
    vararg examples: Date
  ): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    val formatter = DateTimeFormatter.ofPattern(format).withZone(timeZone.toZoneId())
    matchers.addRule(constructValidPath(name, rootPath), matchTimestamp(format))

    when (val body = body) {
      is JsonValue.Object -> body.add(name,
        JsonValue.StringValue(formatter.format(examples[0].toInstant()).toCharArray()))
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.StringValue(formatter.format(value.toInstant()).toCharArray()))
        }
      }
      else -> {}
    }

    return this
  }


  /**
   * Attribute that must match the given datetime format
   * @param name attribute name
   * @param format timestamp format
   * @param example example date and time to use for generated bodies
   * @param timeZone time zone used for formatting of example date and time
   */
  @JvmOverloads
  fun datetime(
    name: String,
    format: String,
    example: Instant,
    timeZone: TimeZone = TimeZone.getDefault()
  ) = datetime(name, format, timeZone, example)

  /**
   * Attribute that must match the given datetime format
   * @param name attribute name
   * @param format timestamp format
   * @param examples example dates and times to use for generated bodies
   * @param timeZone time zone used for formatting of example date and time
   */
  @JvmOverloads
  fun datetime(
    name: String,
    format: String,
    timeZone: TimeZone = TimeZone.getDefault(),
    vararg examples: Instant
  ): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    val formatter = DateTimeFormatter.ofPattern(format).withZone(timeZone.toZoneId())
    matchers.addRule(constructValidPath(name, rootPath), matchTimestamp(format))

    when (val body = body) {
      is JsonValue.Object -> body.add(name,
        JsonValue.StringValue(formatter.format(examples[0]).toCharArray()))
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.StringValue(formatter.format(value).toCharArray()))
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must be formatted as an ISO date
   * @param name attribute name
   */
  @JvmOverloads
  fun date(name: String = "date"): PactDslJsonBody {
    val pattern = DateFormatUtils.ISO_DATE_FORMAT.pattern
    val path = constructValidPath(name, rootPath)
    generators.addGenerator(Category.BODY, path, DateGenerator(pattern, null))
    matchers.addRule(path, matchDate(pattern))

    val stringValue = JsonValue.StringValue(DateFormatUtils.ISO_DATE_FORMAT.format(Date(DATE_2000)).toCharArray())
    when (val body = body) {
      is JsonValue.Object -> body.add(name, stringValue)
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, stringValue)
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must match the provided date format
   * @param name attribute date
   * @param format date format to match
   */
  fun date(name: String, format: String): PactDslJsonBody {
    val path = constructValidPath(name, rootPath)
    generators.addGenerator(Category.BODY, path, DateGenerator(format, null))
    val instance = FastDateFormat.getInstance(format)
    matchers.addRule(path, matchDate(format))

    val stringValue = JsonValue.StringValue(instance.format(Date(DATE_2000)).toCharArray())
    when (val body = body) {
      is JsonValue.Object -> body.add(name, stringValue)
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, stringValue)
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must match the provided date format
   * @param name attribute date
   * @param format date format to match
   * @param example example date to use for generated values
   * @param timeZone time zone used for formatting of example date
   */
  @JvmOverloads
  fun date(name: String, format: String, example: Date, timeZone: TimeZone = TimeZone.getDefault()) =
    date(name, format, timeZone, example)


  /**
   * Attribute that must match the provided date format
   * @param name attribute date
   * @param format date format to match
   * @param examples example dates to use for generated values
   * @param timeZone time zone used for formatting of example date
   */
  @JvmOverloads
  fun date(
    name: String,
    format: String,
    timeZone: TimeZone = TimeZone.getDefault(),
    vararg examples: Date
  ): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    val instance = FastDateFormat.getInstance(format, timeZone)
    matchers.addRule(constructValidPath(name, rootPath), matchDate(format))

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(instance.format(examples[0]).toCharArray()))
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.StringValue(instance.format(value).toCharArray()))
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must match the provided date format
   * @param name attribute date
   * @param format date format to match
   * @param example example date to use for generated values
   */
  fun localDate(name: String, format: String, vararg examples: LocalDate): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    val formatter = DateTimeFormatter.ofPattern(format)
    matchers.addRule(constructValidPath(name, rootPath), matchDate(format))

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(formatter.format(examples[0]).toCharArray()))
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.StringValue(formatter.format(value).toCharArray()))
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must be an ISO formatted time
   * @param name attribute name
   */
  /**
   * Attribute named 'time' that must be an ISO formatted time
   */
  @JvmOverloads
  fun time(name: String = "time"): PactDslJsonBody {
    val pattern = DateFormatUtils.ISO_TIME_FORMAT.pattern
    val path = constructValidPath(name, rootPath)
    generators.addGenerator(Category.BODY, path, TimeGenerator(pattern, null))
    matchers.addRule(path, matchTime(pattern))

    val stringValue = JsonValue.StringValue(DateFormatUtils.ISO_TIME_FORMAT.format(Date(DATE_2000)).toCharArray())
    when (val body = body) {
      is JsonValue.Object -> body.add(name, stringValue)
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, stringValue)
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must match the given time format
   * @param name attribute name
   * @param format time format to match
   */
  fun time(name: String, format: String): PactDslJsonBody {
    val path = constructValidPath(name, rootPath)
    generators.addGenerator(Category.BODY, path, TimeGenerator(format, null))
    matchers.addRule(path, matchTime(format))

    val instance = FastDateFormat.getInstance(format)
    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(instance.format(Date(DATE_2000)).toCharArray()))
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, JsonValue.StringValue(instance.format(Date(DATE_2000)).toCharArray()))
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must match the given time format
   * @param name attribute name
   * @param format time format to match
   * @param example example time to use for generated bodies
   * @param timeZone time zone used for formatting of example time
   */
  @JvmOverloads
  fun time(
    name: String,
    format: String,
    example: Date,
    timeZone: TimeZone = TimeZone.getDefault()
  ) = time(name, format, timeZone, example)

  /**
   * Attribute that must match the given time format
   * @param name attribute name
   * @param format time format to match
   * @param examples example times to use for generated bodies
   * @param timeZone time zone used for formatting of example time
   */
  @JvmOverloads
  fun time(
    name: String,
    format: String,
    timeZone: TimeZone = TimeZone.getDefault(),
    vararg examples: Date
  ): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    val instance = FastDateFormat.getInstance(format, timeZone)
    matchers.addRule(constructValidPath(name, rootPath), matchTime(format))

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(instance.format(examples[0]).toCharArray()))
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.StringValue(instance.format(value).toCharArray()))
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must be an IP4 address
   * @param name attribute name
   */
  fun ipAddress(name: String): PactDslJsonBody {
    matchers.addRule(constructValidPath(name, rootPath), regexp("(\\d{1,3}\\.)+\\d{1,3}"))

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue("127.0.0.1".toCharArray()))
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, JsonValue.StringValue("127.0.0.1".toCharArray()))
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that is a JSON object
   * @param name field name
   */
  override fun `object`(name: String): PactDslJsonBody {
    return PactDslJsonBody(constructValidPath(name, rootPath) + ".", name, this)
  }

  override fun `object`(): PactDslJsonBody {
    throw UnsupportedOperationException("use the object(String name) form")
  }

  /**
   * Attribute that is a JSON object defined from a DSL part
   * @param name field name
   * @param value DSL Part to set the value as
   */
  fun `object`(name: String, value: DslPart): PactDslJsonBody {
    val base = constructValidPath(name, rootPath)
    if (value is PactDslJsonBody) {
      val obj = PactDslJsonBody(base, name, this, value)
      putObjectPrivate(obj)
    } else if (value is PactDslJsonArray) {
      val obj = PactDslJsonArray(base, name, this, (value as PactDslJsonArray?)!!)
      putArrayPrivate(obj)
    }
    return this
  }

  /**
   * Closes the current JSON object
   */
  override fun closeObject(): DslPart? {
    if (parent != null) {
      parent.putObjectPrivate(this)
    } else {
      matchers.applyMatcherRootPrefix("$")
      generators.applyRootPrefix("$")
    }
    closed = true
    return parent
  }

  override fun close(): DslPart? {
    var parentToReturn: DslPart? = this
    if (!closed) {
      var parent = closeObject()
      while (parent != null) {
        parentToReturn = parent
        parent = if (parent is PactDslJsonArray) {
          parent.closeArray()
        } else {
          parent.closeObject()
        }
      }
    }
    return parentToReturn
  }

  /**
   * Attribute that is an array
   * @param name field name
   */
  override fun array(name: String): PactDslJsonArray {
    return PactDslJsonArray(matcherKey(name, rootPath), name, this)
  }

  override fun array(): PactDslJsonArray {
    throw UnsupportedOperationException("use the array(String name) form")
  }

  override fun unorderedArray(name: String): PactDslJsonArray {
    matchers.addRule(matcherKey(name, rootPath), EqualsIgnoreOrderMatcher)
    return this.array(name)
  }

  override fun unorderedArray(): PactDslJsonArray {
    throw UnsupportedOperationException("use the unorderedArray(String name) form")
  }

  override fun unorderedMinArray(name: String, size: Int): PactDslJsonArray {
    matchers.addRule(matcherKey(name, rootPath), MinEqualsIgnoreOrderMatcher(size))
    return this.array(name)
  }

  override fun unorderedMinArray(size: Int): PactDslJsonArray {
    throw UnsupportedOperationException("use the unorderedMinArray(String name, int size) form")
  }

  override fun unorderedMaxArray(name: String, size: Int): PactDslJsonArray {
    matchers.addRule(matcherKey(name, rootPath), MaxEqualsIgnoreOrderMatcher(size))
    return this.array(name)
  }

  override fun unorderedMaxArray(size: Int): PactDslJsonArray {
    throw UnsupportedOperationException("use the unorderedMaxArray(String name, int size) form")
  }

  override fun unorderedMinMaxArray(name: String, minSize: Int, maxSize: Int): PactDslJsonArray {
    require(minSize <= maxSize) {
      String.format("The minimum size of %d is greater than the maximum of %d",
        minSize, maxSize)
    }
    matchers.addRule(matcherKey(name, rootPath), MinMaxEqualsIgnoreOrderMatcher(minSize, maxSize))
    return this.array(name)
  }

  override fun unorderedMinMaxArray(minSize: Int, maxSize: Int): PactDslJsonArray {
    throw UnsupportedOperationException("use the unorderedMinMaxArray(String name, int minSize, int maxSize) form")
  }

  /**
   * Closes the current array
   */
  override fun closeArray(): DslPart? {
    return if (parent is PactDslJsonArray) {
      closeObject()
      parent.closeArray()
    } else {
      throw UnsupportedOperationException("can't call closeArray on an Object")
    }
  }

  /**
   * Attribute that is an array where each item must match the following example
   * @param name field name
   */
  override fun eachLike(name: String): PactDslJsonBody {
    return eachLike(name, 1)
  }

  override fun eachLike(name: String, obj: DslPart): PactDslJsonBody {
    val base = constructValidPath(name, rootPath)
    matchers.addRule(base, TypeMatcher)
    val parent = PactDslJsonArray(base, name, this, true)
    if (obj is PactDslJsonBody) {
      parent.putObjectPrivate(obj)
    } else if (obj is PactDslJsonArray) {
      parent.putArrayPrivate(obj)
    }
    return parent.closeArray() as PactDslJsonBody
  }

  override fun eachLike(): PactDslJsonBody {
    throw UnsupportedOperationException("use the eachLike(String name) form")
  }

  override fun eachLike(obj: DslPart): PactDslJsonArray {
    throw UnsupportedOperationException("use the eachLike(String name, DslPart object) form")
  }

  /**
   * Attribute that is an array where each item must match the following example
   * @param name field name
   * @param numberExamples number of examples to generate
   */
  override fun eachLike(name: String, numberExamples: Int): PactDslJsonBody {
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, TypeMatcher)
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    return PactDslJsonBody(".", ".", parent, numberExamples)
  }

  override fun eachLike(numberExamples: Int): PactDslJsonBody {
    throw UnsupportedOperationException("use the eachLike(String name, int numberExamples) form")
  }

  /**
   * Attribute that is an array of values that are not objects where each item must match the following example
   * @param name field name
   * @param value Value to use to match each item
   * @param numberExamples number of examples to generate
   */
  @JvmOverloads
  fun eachLike(name: String, value: PactDslJsonRootValue, numberExamples: Int = 1): PactDslJsonBody {
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, TypeMatcher)
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    parent.putObjectPrivate(value)
    return parent.closeArray() as PactDslJsonBody
  }

  /**
   * Attribute that is an array with a minimum size where each item must match the following example
   * @param name field name
   * @param size minimum size of the array
   */
  override fun minArrayLike(name: String, size: Int): PactDslJsonBody {
    return minArrayLike(name, size, size)
  }

  override fun minArrayLike(size: Int): PactDslJsonBody {
    throw UnsupportedOperationException("use the minArrayLike(String name, Integer size) form")
  }

  override fun minArrayLike(name: String, size: Int, obj: DslPart): PactDslJsonBody {
    val base = constructValidPath(name, rootPath)
    matchers.addRule(base, matchMin(size))
    val parent = PactDslJsonArray(base, name, this, true)
    if (obj is PactDslJsonBody) {
      parent.putObjectPrivate(obj)
    } else if (obj is PactDslJsonArray) {
      parent.putArrayPrivate(obj)
    }
    return parent.closeArray() as PactDslJsonBody
  }

  override fun minArrayLike(size: Int, obj: DslPart): PactDslJsonArray {
    throw UnsupportedOperationException("use the minArrayLike(String name, Integer size, DslPart object) form")
  }

  /**
   * Attribute that is an array with a minimum size where each item must match the following example
   * @param name field name
   * @param size minimum size of the array
   * @param numberExamples number of examples to generate
   */
  override fun minArrayLike(name: String, size: Int, numberExamples: Int): PactDslJsonBody {
    require(numberExamples >= size) {
      String.format("Number of example %d is less than the minimum size of %d",
        numberExamples, size)
    }
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, matchMin(size))
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    return PactDslJsonBody(".", "", parent, numberExamples)
  }

  override fun minArrayLike(size: Int, numberExamples: Int): PactDslJsonBody {
    throw UnsupportedOperationException("use the minArrayLike(String name, Integer size, int numberExamples) form")
  }

  /**
   * Attribute that is an array of values with a minimum size that are not objects where each item must match
   * the following example
   * @param name field name
   * @param size minimum size of the array
   * @param value Value to use to match each item
   * @param numberExamples number of examples to generate
   */
  @JvmOverloads
  fun minArrayLike(name: String, size: Int, value: PactDslJsonRootValue, numberExamples: Int = 2): PactDslJsonBody {
    return minArrayLike(name, size, value as DslPart, numberExamples)
  }

  /**
   * Attribute that is an array of values with a minimum size that are not objects where each item must match
   * the following example
   * @param name field name
   * @param size minimum size of the array
   * @param value Value to use to match each item
   * @param numberExamples number of examples to generate
   */
  fun minArrayLike(name: String, size: Int, value: DslPart, numberExamples: Int): PactDslJsonBody {
    require(numberExamples >= size) {
      String.format("Number of example %d is less than the minimum size of %d",
        numberExamples, size)
    }
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, matchMin(size))
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    parent.putObjectPrivate(value)
    return parent.closeArray() as PactDslJsonBody
  }

  /**
   * Attribute that is an array with a maximum size where each item must match the following example
   * @param name field name
   * @param size maximum size of the array
   */
  override fun maxArrayLike(name: String, size: Int): PactDslJsonBody {
    return maxArrayLike(name, size, 1)
  }

  override fun maxArrayLike(size: Int): PactDslJsonBody {
    throw UnsupportedOperationException("use the maxArrayLike(String name, Integer size) form")
  }

  override fun maxArrayLike(name: String, size: Int, obj: DslPart): PactDslJsonBody {
    val base = constructValidPath(name, rootPath)
    matchers.addRule(base, matchMax(size))
    val parent = PactDslJsonArray(base, name, this, true)
    if (obj is PactDslJsonBody) {
      parent.putObjectPrivate(obj)
    } else if (obj is PactDslJsonArray) {
      parent.putArrayPrivate(obj)
    }
    return parent.closeArray() as PactDslJsonBody
  }

  override fun maxArrayLike(size: Int, obj: DslPart): PactDslJsonArray {
    throw UnsupportedOperationException("use the maxArrayLike(String name, Integer size, DslPart object) form")
  }

  /**
   * Attribute that is an array with a maximum size where each item must match the following example
   * @param name field name
   * @param size maximum size of the array
   * @param numberExamples number of examples to generate
   */
  override fun maxArrayLike(name: String, size: Int, numberExamples: Int): PactDslJsonBody {
    require(numberExamples <= size) {
      String.format("Number of example %d is more than the maximum size of %d",
        numberExamples, size)
    }
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, matchMax(size))
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    return PactDslJsonBody(".", "", parent, numberExamples)
  }

  override fun maxArrayLike(size: Int, numberExamples: Int): PactDslJsonBody {
    throw UnsupportedOperationException("use the maxArrayLike(String name, Integer size, int numberExamples) form")
  }

  /**
   * Attribute that is an array of values with a maximum size that are not objects where each item must match the
   * following example
   * @param name field name
   * @param size maximum size of the array
   * @param value Value to use to match each item
   * @param numberExamples number of examples to generate
   */
  @JvmOverloads
  fun maxArrayLike(name: String, size: Int, value: PactDslJsonRootValue, numberExamples: Int = 1): PactDslJsonBody {
    return maxArrayLike(name, size, value as DslPart, numberExamples)
  }

  /**
   * Attribute that is an array of values with a maximum size that are not objects where each item must match the
   * following example
   * @param name field name
   * @param size maximum size of the array
   * @param value Value to use to match each item
   * @param numberExamples number of examples to generate
   */
  fun maxArrayLike(name: String, size: Int, value: DslPart, numberExamples: Int): PactDslJsonBody {
    require(numberExamples <= size) {
      String.format("Number of example %d is more than the maximum size of %d",
        numberExamples, size)
    }
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, matchMax(size))
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    parent.putObjectPrivate(value)
    return parent.closeArray() as PactDslJsonBody
  }

  /**
   * Attribute that must be a numeric identifier
   * @param name attribute name, defaults to 'id', that must be a numeric identifier
   */
  @JvmOverloads
  fun id(name: String = "id"): PactDslJsonBody {
    val path = constructValidPath(name, rootPath)
    generators.addGenerator(Category.BODY, path, RandomIntGenerator(0, Int.MAX_VALUE))
    matchers.addRule(path, TypeMatcher)

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Integer("1234567890".toCharArray()))
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, JsonValue.Integer("1234567890".toCharArray()))
        }
      }
      else -> {}
    }

    return this
  }

  /**
   * Attribute that must be a numeric identifier
   * @param name attribute name
   * @param examples example ids to use for generated bodies
   */
  fun id(name: String, vararg examples: Long): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.Integer(examples[0].toString().toCharArray()))
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, JsonValue.Integer(value.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), TypeMatcher)

    return this
  }

  /**
   * Attribute that must be encoded as a hexadecimal value
   * @param name attribute name
   */
  fun hexValue(name: String): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name!!, rootPath), RandomHexadecimalGenerator(10))
    return hexValue(name, "1234a")
  }

  /**
   * Attribute that must be encoded as a hexadecimal value
   * @param name attribute name
   * @param hexValue example value to use for generated bodies
   */
  fun hexValue(name: String, vararg examples: String): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> {
        if (!examples[0].matches(HEXADECIMAL)) {
          throw InvalidMatcherException("Example \"${examples[0]}\" is not a valid hexadecimal value")
        }
        body.add(name, JsonValue.StringValue(examples[0].toCharArray()))
      }
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          if (!examples[0].matches(HEXADECIMAL)) {
            throw InvalidMatcherException("Example \"$value\" is not a valid hexadecimal value")
          }
          body[i].asObject()!!.add(name, JsonValue.StringValue(value.toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), regexp("[0-9a-fA-F]+"))

    return this
  }

  /**
   * Attribute that must be encoded as an UUID
   * @param name attribute name
   */
  fun uuid(name: String): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath), UuidGenerator())
    return uuid(name, "e2490de5-5bd3-43d5-b7c4-526e33f71304")
  }

  /**
   * Attribute that must be encoded as an UUID
   * @param name attribute name
   * @param uuid example UUID to use for generated bodies
   */
  fun uuid(name: String, vararg uuids: UUID): PactDslJsonBody {
    val ids = uuids.map { it.toString() }.toTypedArray()
    return uuid(name, *ids)
  }

  /**
   * Attribute that must be encoded as an UUID
   * @param name attribute name
   * @param uuid example UUID to use for generated bodies
   */
  @Suppress("ThrowsCount")
  fun uuid(name: String, vararg examples: String): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> {
        if (!examples[0].matches(UUID_REGEX)) {
          throw InvalidMatcherException("Example \"${examples[0]}\" is not a valid UUID value")
        }
        body.add(name, JsonValue.StringValue(examples[0].toCharArray()))
      }
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          if (!value.matches(UUID_REGEX)) {
            throw InvalidMatcherException("Example \"$value\" is not a valid UUID value")
          }
          body[i].asObject()!!.add(name, JsonValue.StringValue(value.toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), regexp(UUID_REGEX.pattern))

    return this
  }

  /**
   * Sets the field to a null value
   * @param fieldName field name
   */
  fun nullValue(fieldName: String): PactDslJsonBody {
    when (val body = body) {
      is JsonValue.Object -> body.add(fieldName, JsonValue.Null)
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(fieldName, JsonValue.Null)
        }
      }
      else -> {}
    }

    return this
  }

  override fun eachArrayLike(name: String): PactDslJsonArray {
    return eachArrayLike(name, 1)
  }

  override fun eachArrayLike(): PactDslJsonArray {
    throw UnsupportedOperationException("use the eachArrayLike(String name) form")
  }

  override fun eachArrayLike(name: String, numberExamples: Int): PactDslJsonArray {
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, TypeMatcher)
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    return PactDslJsonArray("", "", parent)
  }

  override fun eachArrayLike(numberExamples: Int): PactDslJsonArray {
    throw UnsupportedOperationException("use the eachArrayLike(String name, int numberExamples) form")
  }

  override fun eachArrayWithMaxLike(name: String, size: Int): PactDslJsonArray {
    return eachArrayWithMaxLike(name, 1, size)
  }

  override fun eachArrayWithMaxLike(size: Int): PactDslJsonArray {
    throw UnsupportedOperationException("use the eachArrayWithMaxLike(String name, Integer size) form")
  }

  override fun eachArrayWithMaxLike(name: String, numberExamples: Int, size: Int): PactDslJsonArray {
    require(numberExamples <= size) {
      String.format("Number of example %d is more than the maximum size of %d",
        numberExamples, size)
    }
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, matchMax(size))
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    return PactDslJsonArray("", "", parent)
  }

  override fun eachArrayWithMaxLike(numberExamples: Int, size: Int): PactDslJsonArray {
    throw UnsupportedOperationException(
      "use the eachArrayWithMaxLike(String name, int numberExamples, Integer size) form")
  }

  override fun eachArrayWithMinLike(name: String, size: Int): PactDslJsonArray {
    return eachArrayWithMinLike(name, size, size)
  }

  override fun eachArrayWithMinLike(size: Int): PactDslJsonArray {
    throw UnsupportedOperationException("use the eachArrayWithMinLike(String name, Integer size) form")
  }

  override fun eachArrayWithMinLike(name: String, numberExamples: Int, size: Int): PactDslJsonArray {
    require(numberExamples >= size) {
      String.format("Number of example %d is less than the minimum size of %d",
        numberExamples, size)
    }
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, matchMin(size))
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    return PactDslJsonArray("", "", parent)
  }

  override fun eachArrayWithMinLike(numberExamples: Int, size: Int): PactDslJsonArray {
    throw UnsupportedOperationException(
      "use the eachArrayWithMinLike(String name, int numberExamples, Integer size) form")
  }

  /**
   * Accepts any key, and each key is mapped to a list of items that must match the following object definition
   * @param exampleKey Example key to use for generating bodies
   */
  fun eachKeyMappedToAnArrayLike(exampleKey: String): PactDslJsonBody {
    matchers.addRule(
      if (rootPath.endsWith(".")) rootPath.substring(0, rootPath.length - 1) else rootPath, ValuesMatcher
    )
    val parent = PactDslJsonArray("$rootPath*", exampleKey, this, true)
    return PactDslJsonBody(".", "", parent)
  }

  /**
   * Accepts any key, and each key is mapped to a map that must match the following object definition
   * @param exampleKey Example key to use for generating bodies
   */
  @Deprecated("Use eachValueLike instead", ReplaceWith("eachValueLike(exampleKey)"))
  fun eachKeyLike(exampleKey: String): PactDslJsonBody {
    return eachValueLike(exampleKey)
  }

  /**
   * Accepts any key, and each key is mapped to a map that must match the provided object definition
   * @param exampleKey Example key to use for generating bodies
   * @param value Value to use for matching and generated bodies
   */
  @Deprecated("Use eachValueLike instead", ReplaceWith("eachValueLike(exampleKey, value)"))
  fun eachKeyLike(exampleKey: String, value: PactDslJsonRootValue): PactDslJsonBody {
    return eachValueLike(exampleKey, value)
  }

  /**
   * Accepts any key, and each key is mapped to a value that must match the following object definition
   * @param exampleKey Example key to use for generating bodies
   */
  fun eachValueLike(exampleKey: String): PactDslJsonBody {
    matchers.addRule(
      if (rootPath.endsWith(".")) rootPath.substring(0, rootPath.length - 1) else rootPath, ValuesMatcher
    )
    return PactDslJsonBody("$rootPath*.", exampleKey, this)
  }

  /**
   * Accepts any key, and each key is mapped to a map that must match the provided object definition
   * @param exampleKey Example key to use for generating bodies
   * @param value Value to use for matching and generated bodies
   */
  fun eachValueLike(exampleKey: String, value: PactDslJsonRootValue): PactDslJsonBody {
    when (val body = body) {
      is JsonValue.Object -> body.add(exampleKey, value.body)
      is JsonValue.Array -> {
        body.values.forEach { v ->
          v.asObject()!!.add(exampleKey, value.body)
        }
      }
      else -> {}
    }

    matchers.addRule(
      if (rootPath.endsWith(".")) rootPath.substring(0, rootPath.length - 1) else rootPath, ValuesMatcher
    )
    for (matcherName in value.matchers.matchingRules.keys) {
      matchers.addRules("$rootPath*$matcherName", value.matchers.matchingRules[matcherName]!!.rules)
    }
    return this
  }

  /**
   * Attribute that must include the provided string value
   * @param name attribute name
   * @param value Value that must be included
   */
  fun includesStr(name: String, value: String): PactDslJsonBody {
    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(value.toCharArray()))
      is JsonValue.Array -> {
        body.values.forEach { value ->
          value.asObject()!!.add(name, JsonValue.StringValue(value.toString().toCharArray()))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), includesMatcher(value))

    return this
  }

  /**
   * Attribute that must be equal to the provided value.
   * @param name attribute name
   * @param value Value that will be used for comparisons
   */
  fun equalTo(name: String, vararg examples: Any?): PactDslJsonBody {
    require(examples.isNotEmpty()) {
      "At least one example value is required"
    }
    if (body is JsonValue.Object) {
      require(examples.size == 1) {
        "You provided multiple example values (${examples.size}) but only one was expected"
      }
    } else if (body is JsonValue.Array) {
      require(body.size() >= examples.size) {
        "You provided ${examples.size} example values but ${body.size()} was expected"
      }
    }

    when (val body = body) {
      is JsonValue.Object -> body.add(name, toJson(examples[0]))
      is JsonValue.Array -> {
        examples.padTo(body.size()).forEachIndexed { i, value ->
          body[i].asObject()!!.add(name, toJson(value))
        }
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), EqualsMatcher)

    return this
  }

  /**
   * Combine all the matchers using AND
   * @param name Attribute name
   * @param value Attribute example value
   * @param rules Matching rules to apply
   */
  fun and(name: String, value: Any?, vararg rules: MatchingRule): PactDslJsonBody {
    when (val body = body) {
      is JsonValue.Object -> body.add(name, toJson(value))
      is JsonValue.Array -> body.values.forEach { v -> v.asObject()!!.add(name, toJson(value)) }
      else -> {}
    }

    matchers.setRules(matcherKey(name, rootPath), MatchingRuleGroup(mutableListOf(*rules), RuleLogic.AND))

    return this
  }

  /**
   * Combine all the matchers using OR
   * @param name Attribute name
   * @param value Attribute example value
   * @param rules Matching rules to apply
   */
  fun or(name: String, value: Any?, vararg rules: MatchingRule): PactDslJsonBody {
    when (val body = body) {
      is JsonValue.Object -> body.add(name, toJson(value))
      is JsonValue.Array -> body.values.forEach { v -> v.asObject()!!.add(name, toJson(value)) }
      else -> {}
    }

    matchers.setRules(matcherKey(name, rootPath), MatchingRuleGroup(mutableListOf(*rules), RuleLogic.OR))

    return this
  }

  /**
   * Matches a URL that is composed of a base path and a sequence of path expressions
   * @param name Attribute name
   * @param basePath The base path for the URL (like "http://localhost:8080/") which will be excluded from the matching
   * @param pathFragments Series of path fragments to match on. These can be strings or regular expressions.
   */
  override fun matchUrl(name: String, basePath: String?, vararg pathFragments: Any): PactDslJsonBody {
    val urlMatcher = UrlMatcherSupport(basePath, listOf(*pathFragments))
    val exampleValue = urlMatcher.getExampleValue()

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(exampleValue.toCharArray()))
      is JsonValue.Array -> body.values.forEach { v ->
        v.asObject()!!.add(name, JsonValue.StringValue(exampleValue.toCharArray()))
      }
      else -> {}
    }

    val regexExpression = urlMatcher.getRegexExpression()
    matchers.addRule(matcherKey(name, rootPath), regexp(regexExpression))
    if (StringUtils.isEmpty(basePath)) {
      generators.addGenerator(Category.BODY, matcherKey(name, rootPath),
        MockServerURLGenerator(exampleValue, regexExpression))
    }
    return this
  }

  override fun matchUrl(basePath: String?, vararg pathFragments: Any): DslPart {
    throw UnsupportedOperationException(
      "URL matcher without an attribute name is not supported for objects. " +
        "Use matchUrl(String name, String basePath, Object... pathFragments)")
  }

  /**
   * Matches a URL that is composed of a base path and a sequence of path expressions. Base path from the mock server
   * will be used.
   * @param name Attribute name
   * @param pathFragments Series of path fragments to match on. These can be strings or regular expressions.
   */
  override fun matchUrl2(name: String, vararg pathFragments: Any): PactDslJsonBody {
    return matchUrl(name, null, *pathFragments)
  }

  override fun matchUrl2(vararg pathFragments: Any): DslPart {
    throw UnsupportedOperationException(
      "URL matcher without an attribute name is not supported for objects. " +
        "Use matchUrl2(Object... pathFragments)")
  }

  override fun minMaxArrayLike(name: String, minSize: Int, maxSize: Int): PactDslJsonBody {
    return minMaxArrayLike(name, minSize, maxSize, minSize)
  }

  override fun minMaxArrayLike(name: String, minSize: Int, maxSize: Int, obj: DslPart): PactDslJsonBody {
    validateMinAndMaxAndExamples(minSize, maxSize, minSize)
    val base = constructValidPath(name, rootPath)
    matchers.addRule(base, matchMinMax(minSize, maxSize))
    val parent = PactDslJsonArray(base, name, this, true)
    if (obj is PactDslJsonBody) {
      parent.putObjectPrivate(obj)
    } else if (obj is PactDslJsonArray) {
      parent.putArrayPrivate(obj)
    }
    return parent.closeArray() as PactDslJsonBody
  }

  override fun minMaxArrayLike(minSize: Int, maxSize: Int): PactDslJsonBody {
    throw UnsupportedOperationException("use the minMaxArrayLike(String name, Integer minSize, Integer maxSize) form")
  }

  override fun minMaxArrayLike(minSize: Int, maxSize: Int, obj: DslPart): PactDslJsonArray {
    throw UnsupportedOperationException(
      "use the minMaxArrayLike(String name, Integer minSize, Integer maxSize, DslPart object) form")
  }

  override fun minMaxArrayLike(name: String, minSize: Int, maxSize: Int, numberExamples: Int): PactDslJsonBody {
    validateMinAndMaxAndExamples(minSize, maxSize, numberExamples)
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, matchMinMax(minSize, maxSize))
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    return PactDslJsonBody(".", "", parent, numberExamples)
  }

  private fun validateMinAndMaxAndExamples(minSize: Int, maxSize: Int, numberExamples: Int) {
    require(minSize <= maxSize) {
      String.format("The minimum size %d is more than the maximum size of %d",
        minSize, maxSize)
    }
    require(numberExamples >= minSize) {
      String.format("Number of example %d is less than the minimum size of %d",
        numberExamples, minSize)
    }
    require(numberExamples <= maxSize) {
      String.format("Number of example %d is greater than the maximum size of %d",
        numberExamples, maxSize)
    }
  }

  override fun minMaxArrayLike(minSize: Int, maxSize: Int, numberExamples: Int): PactDslJsonBody {
    throw UnsupportedOperationException(
      "use the minMaxArrayLike(String name, Integer minSize, Integer maxSize, int numberExamples) form")
  }

  override fun eachArrayWithMinMaxLike(name: String, minSize: Int, maxSize: Int): PactDslJsonArray {
    return eachArrayWithMinMaxLike(name, minSize, minSize, maxSize)
  }

  override fun eachArrayWithMinMaxLike(minSize: Int, maxSize: Int): PactDslJsonArray {
    throw UnsupportedOperationException(
      "use the eachArrayWithMinMaxLike(String name, Integer minSize, Integer maxSize) form")
  }

  override fun eachArrayWithMinMaxLike(
    name: String,
    numberExamples: Int,
    minSize: Int,
    maxSize: Int
  ): PactDslJsonArray {
    validateMinAndMaxAndExamples(minSize, maxSize, numberExamples)
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, matchMinMax(minSize, maxSize))
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    return PactDslJsonArray("", "", parent)
  }

  override fun eachArrayWithMinMaxLike(numberExamples: Int, minSize: Int, maxSize: Int): PactDslJsonArray {
    throw UnsupportedOperationException(
      "use the eachArrayWithMinMaxLike(String name, int numberExamples, Integer minSize, Integer maxSize) form")
  }

  /**
   * Attribute that is an array of values with a minimum and maximum size that are not objects where each item must
   * match the following example
   * @param name field name
   * @param minSize minimum size
   * @param maxSize maximum size
   * @param value Value to use to match each item
   * @param numberExamples number of examples to generate
   */
  fun minMaxArrayLike(
    name: String,
    minSize: Int,
    maxSize: Int,
    value: PactDslJsonRootValue,
    numberExamples: Int
  ): PactDslJsonBody {
    return minMaxArrayLike(name, minSize, maxSize, value as DslPart, numberExamples)
  }

  /**
   * Attribute that is an array of values with a minimum and maximum size that are not objects where each item must
   * match the following example
   * @param name field name
   * @param minSize minimum size
   * @param maxSize maximum size
   * @param value Value to use to match each item
   * @param numberExamples number of examples to generate
   */
  fun minMaxArrayLike(
    name: String,
    minSize: Int,
    maxSize: Int,
    value: DslPart,
    numberExamples: Int
  ): PactDslJsonBody {
    validateMinAndMaxAndExamples(minSize, maxSize, numberExamples)
    val path = constructValidPath(name, rootPath)
    matchers.addRule(path, matchMinMax(minSize, maxSize))
    val parent = PactDslJsonArray(path, name, this, true)
    parent.numberExamples = numberExamples
    parent.putObjectPrivate(value)
    return parent.closeArray() as PactDslJsonBody
  }

  /**
   * Adds an attribute that will have its value injected from the provider state
   * @param name Attribute name
   * @param expression Expression to be evaluated from the provider state
   * @param example Example value to be used in the consumer test
   */
  fun valueFromProviderState(name: String, expression: String, example: Any?): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath),
      ProviderStateGenerator(expression, from(example)))

    when (val body = body) {
      is JsonValue.Object -> body.add(name, toJson(example))
      is JsonValue.Array -> body.values.forEach { v -> v.asObject()!!.add(name, toJson(example)) }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), TypeMatcher)
    return this
  }

  /**
   * Adds a date attribute with the value generated by the date expression
   * @param name Attribute name
   * @param expression Date expression to use to generate the values
   * @param format Date format to use
   */
  @JvmOverloads
  fun dateExpression(
    name: String,
    expression: String,
    format: String = DateFormatUtils.ISO_DATE_FORMAT.pattern
  ): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath), DateGenerator(format, expression))
    val instance = FastDateFormat.getInstance(format)

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(instance.format(Date(DATE_2000)).toCharArray()))
      is JsonValue.Array -> body.values.forEach { v ->
        v.asObject()!!.add(name, JsonValue.StringValue(instance.format(Date(DATE_2000)).toCharArray()))
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), matchDate(format))

    return this
  }

  /**
   * Adds a time attribute with the value generated by the time expression
   * @param name Attribute name
   * @param expression Time expression to use to generate the values
   * @param format Time format to use
   */
  @JvmOverloads
  fun timeExpression(
    name: String,
    expression: String,
    format: String = DateFormatUtils.ISO_TIME_NO_T_FORMAT.pattern
  ): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath), TimeGenerator(format, expression))
    val instance = FastDateFormat.getInstance(format)

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(instance.format(Date(DATE_2000)).toCharArray()))
      is JsonValue.Array -> body.values.forEach { v ->
        v.asObject()!!.add(name, JsonValue.StringValue(instance.format(Date(DATE_2000)).toCharArray()))
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), matchTime(format))

    return this
  }

  /**
   * Adds a datetime attribute with the value generated by the expression
   * @param name Attribute name
   * @param expression Datetime expression to use to generate the values
   * @param format Datetime format to use
   */
  @JvmOverloads
  fun datetimeExpression(
    name: String,
    expression: String,
    format: String = DateFormatUtils.ISO_DATETIME_FORMAT.pattern
  ): PactDslJsonBody {
    generators.addGenerator(Category.BODY, matcherKey(name, rootPath), DateTimeGenerator(format, expression))
    val instance = FastDateFormat.getInstance(format)

    when (val body = body) {
      is JsonValue.Object -> body.add(name, JsonValue.StringValue(instance.format(Date(DATE_2000)).toCharArray()))
      is JsonValue.Array -> body.values.forEach { v ->
        v.asObject()!!.add(name, JsonValue.StringValue(instance.format(Date(DATE_2000)).toCharArray()))
      }
      else -> {}
    }

    matchers.addRule(matcherKey(name, rootPath), matchTimestamp(format))

    return this
  }

  override fun arrayContaining(name: String): DslPart {
    return PactDslJsonArrayContaining(rootPath, name, this)
  }

  /**
   * Extends this JSON object from a base template.
   */
  fun extendFrom(baseTemplate: PactDslJsonBody) {
    this.body = copyBody(baseTemplate.body)
    matchers = baseTemplate.matchers.copyWithUpdatedMatcherRootPrefix("")
    generators = baseTemplate.generators.copyWithUpdatedMatcherRootPrefix("")
  }

  // TODO: Replace this with JsonValue.copy in the next major version
  private fun copyBody(body: JsonValue): JsonValue {
    return when (body) {
      is JsonValue.Array -> JsonValue.Array(body.values.map { it.copy() }.toMutableList())
      is JsonValue.Decimal -> JsonValue.Decimal(body.value.chars)
      is JsonValue.Integer -> JsonValue.Integer(body.value.chars)
      is JsonValue.Object -> JsonValue.Object(body.entries.mapValues { it.value.copy() }.toMutableMap())
      is JsonValue.StringValue -> JsonValue.StringValue(body.value.chars)
      else -> body
    }
  }

  /**
   * Applies a matching rule to each key in the object, ignoring the values.
   */
  fun eachKeyMatching(matcher: Matcher): PactDslJsonBody {
    val path = if (rootPath.endsWith(".")) rootPath.substring(0, rootPath.length - 1) else rootPath
    val value = matcher.value.toString()
    if (matcher.matcher != null) {
      matchers.addRule(path, EachKeyMatcher(MatchingRuleDefinition(value, matcher.matcher!!, matcher.generator)))
    }
    if (!body.has(value)) {
      when (val body = body) {
        is JsonValue.Object -> body.add(value, JsonValue.Null)
        else -> {}
      }
    }
    return this
  }

  /**
   * Applies matching rules to each value in the object, ignoring the keys.
   */
  fun eachValueMatching(exampleKey: String): PactDslJsonBody {
    val path = constructValidPath("*", rootPath)
    return PactDslJsonBody("$path.", exampleKey, this)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy