diff --git a/README.md b/README.md index d679649d0f..54048deca0 100644 --- a/README.md +++ b/README.md @@ -257,23 +257,25 @@ Often times, you find yourself having to re-write regular expressions for common ```python from pact import Format Format().integer # Matches if the value is an integer -Format().ip_address # Matches if the value is a ip address +Format().ip_address # Matches if the value is an ip address ``` We've created a number of them for you to save you the time: -| matcher | description | -|-----------------|-------------------------------------------------------------------------------------------------| -| `identifier` | Match an ID (e.g. 42) | -| `integer` | Match all numbers that are integers (both ints and longs) | -| `decimal` | Match all real numbers (floating point and decimal) | -| `hexadecimal` | Match all hexadecimal encoded strings | -| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) | -| `timestamp` | Match a string containing an RFC3339 formatted timestapm (e.g. Mon, 31 Oct 2016 15:21:41 -0400) | -| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) | -| `ip_address` | Match string containing IP4 formatted address | -| `ipv6_address` | Match string containing IP6 formatted address | -| `uuid` | Match strings containing UUIDs | +| matcher | description | +|-------------------|-------------------------------------------------------------------------------------------------------------------------| +| `identifier` | Match an ID (e.g. 42) | +| `integer` | Match all numbers that are integers (both ints and longs) | +| `decimal` | Match all real numbers (floating point and decimal) | +| `hexadecimal` | Match all hexadecimal encoded strings | +| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) | +| `timestamp` | Match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400) | +| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) | +| `iso_datetime` | Match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00) | +| `iso_datetime_ms` | Match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00) | +| `ip_address` | Match string containing IP4 formatted address | +| `ipv6_address` | Match string containing IP6 formatted address | +| `uuid` | Match strings containing UUIDs | These can be used to replace other matchers diff --git a/pact/matchers.py b/pact/matchers.py index 52a4148045..fd929f6b68 100644 --- a/pact/matchers.py +++ b/pact/matchers.py @@ -264,6 +264,8 @@ def __init__(self): self.timestamp = self.timestamp() self.date = self.date() self.time = self.time() + self.iso_datetime = self.iso_8601_datetime() + self.iso_datetime_ms = self.iso_8601_datetime(with_ms=True) def integer_or_identifier(self): """ @@ -360,6 +362,52 @@ def time(self): ).time().isoformat() ) + def iso_8601_datetime(self, with_ms=False): + """ + Match a string for a full ISO 8601 Date. + + Does not do any sort of date validation, only checks if the string is + according to the ISO 8601 spec. + + This method differs from :func:`~pact.Format.timestamp`, + :func:`~pact.Format.date` and :func:`~pact.Format.time` implementations + in that it is more stringent and tests the string for exact match to + the ISO 8601 dates format. + + Without `with_ms` will match string containing ISO 8601 formatted dates + as stated bellow: + + * 2016-12-15T20:16:01 + * 2010-05-01T01:14:31.876 + * 2016-05-24T15:54:14.00000Z + * 1994-11-05T08:15:30-05:00 + * 2002-01-31T23:00:00.1234-02:00 + * 1991-02-20T06:35:26.079043+00:00 + + Otherwise, ONLY dates with milliseconds will match the pattern: + + * 2010-05-01T01:14:31.876 + * 2016-05-24T15:54:14.00000Z + * 2002-01-31T23:00:00.1234-02:00 + * 1991-02-20T06:35:26.079043+00:00 + + :param with_ms: Enforcing millisecond precision. + :type with_ms: bool + :return: a Term object with a date regex. + :rtype: Term + """ + date = [1991, 2, 20, 6, 35, 26] + if with_ms: + matcher = self.Regexes.iso_8601_datetime_ms.value + date.append(79043) + else: + matcher = self.Regexes.iso_8601_datetime.value + + return Term( + matcher, + datetime.datetime(*date, tzinfo=datetime.timezone.utc).isoformat() + ) + class Regexes(Enum): """Regex Enum for common formats.""" @@ -398,3 +446,7 @@ class Regexes(Enum): r'0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|' \ r'[12]\d{2}|3([0-5]\d|6[1-6])))?)' time_regex = r'^(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?$' + iso_8601_datetime = r'^\d{4}-[01]\d-[0-3]\d\x54[0-2]\d:[0-6]\d:' \ + r'[0-6]\d(?:\.\d+)?(?:(?:[+-]\d\d:\d\d)|\x5A)?$' + iso_8601_datetime_ms = r'^\d{4}-[01]\d-[0-3]\d\x54[0-2]\d:[0-6]\d:' \ + r'[0-6]\d\.\d+(?:(?:[+-]\d\d:\d\d)|\x5A)?$' diff --git a/tests/test_matchers.py b/tests/test_matchers.py index 1cc446f3be..241c965be3 100644 --- a/tests/test_matchers.py +++ b/tests/test_matchers.py @@ -407,3 +407,45 @@ def test_time(self): }, }, ) + + def test_iso_8601_datetime(self): + date = self.formatter.iso_datetime.generate() + self.assertEqual( + date, + { + "json_class": "Pact::Term", + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.iso_8601_datetime.value, + "o": 0, + }, + "generate": datetime.datetime( + 1991, 2, 20, 6, 35, 26, + tzinfo=datetime.timezone.utc + ).isoformat(), + }, + }, + ) + + def test_iso_8601_datetime_mills(self): + date = self.formatter.iso_datetime_ms.generate() + self.assertEqual( + date, + { + "json_class": "Pact::Term", + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.iso_8601_datetime_ms.value, + "o": 0, + }, + "generate": datetime.datetime( + 1991, 2, 20, 6, 35, 26, 79043, + tzinfo=datetime.timezone.utc + ).isoformat(), + }, + }, + )