Skip to content

Commit

Permalink
feat: add matchers for ISO 8601 date format
Browse files Browse the repository at this point in the history
This introduces `pact.Format.iso_8601_datetime()` method to match a
string for a full ISO 8601 Date.

This method does not do any sort of date validation, only checks if
the string is according to the ISO 8601 spec.

It differs from `pact.Format.timestamp`, `pact.Format.date` and
`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_millis` parameter 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

This change aims to bring the capabilities of the python library into
alignment with pact-foundation/docs.pact.io#88, since the existing
functionality is a bit liberal and allows tests to pass even in cases
where the dates do not conform to the ISO 8601 spec.
  • Loading branch information
sergeyklay committed Mar 12, 2023
1 parent 48b7f37 commit 96bac1d
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 13 deletions.
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_mills` | 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

Expand Down
52 changes: 52 additions & 0 deletions pact/matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_mills = self.iso_8601_datetime(with_millis=True)

def integer_or_identifier(self):
"""
Expand Down Expand Up @@ -360,6 +362,52 @@ def time(self):
).time().isoformat()
)

def iso_8601_datetime(self, with_millis=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_millis` 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_millis: Enforcing millisecond precision.
:type with_millis: bool
:return: a Term object with a date regex.
:rtype: Term
"""
date = [1991, 2, 20, 6, 35, 26]
if with_millis:
matcher = self.Regexes.iso_8601_datetime_mils.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."""

Expand Down Expand Up @@ -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_mils = 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)?$'
42 changes: 42 additions & 0 deletions tests/test_matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, 79043,
tzinfo=datetime.timezone.utc
).isoformat(),
},
},
)

def test_iso_8601_datetime_mills(self):
date = self.formatter.iso_datetime_mills.generate()
self.assertEqual(
date,
{
"json_class": "Pact::Term",
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.iso_8601_datetime_mils.value,
"o": 0,
},
"generate": datetime.datetime(
1991, 2, 20, 6, 35, 26, 79043,
tzinfo=datetime.timezone.utc
).isoformat(),
},
},
)

0 comments on commit 96bac1d

Please sign in to comment.