Skip to content

Commit

Permalink
Merge pull request #138 from pyasi/pyasi_add_matcher_regexes
Browse files Browse the repository at this point in the history
Pyasi add Matcher support for common regex formats
  • Loading branch information
elliottmurray authored May 15, 2020
2 parents 48f0f51 + db39d87 commit 562e047
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 10 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,43 @@ from pact.matchers import get_generated_values
self.assertEqual(result, get_generated_values(expected))
```

### Match common formats
Often times, you find yourself having to re-write regular expressions for common formats.

```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
```

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 |

These can be used to replace other matchers

```python
from pact import Like, Format
Like({
'id': Format().integer, # integer
'lastUpdated': Format().timestamp, # timestamp
'location': { # dictionary
'host': Format().ip_address # ip address
}
})
```

For more information see [Matching](https://docs.pact.io/getting_started/matching)

## Verifying Pacts Against a Service
Expand Down
2 changes: 2 additions & 0 deletions examples/e2e/pact_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ def setup_no_user_a():
def setup_user_a_nonadmin():
id = '00000000-0000-4000-a000-000000000000'
some_date = '2016-12-15T20:16:01'
ip_address = '198.0.0.1'

fakedb['UserA'] = {
'name': "UserA",
'id': id,
'created_on': some_date,
'ip_address': ip_address,
'admin': False
}

Expand Down
3 changes: 2 additions & 1 deletion examples/e2e/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ attrs==19.3.0
certifi==2019.11.28
chardet==3.0.4
click==7.1.1
enum34==1.1.10
Flask==1.1.1
idna==2.9
importlib-metadata==1.6.0
Expand All @@ -13,7 +14,7 @@ packaging==20.3
pluggy==0.13.1
psutil==5.7.0
py==1.8.1
pyparsing==2.4.6
pyparsing==2.4.6f
pytest==5.4.1
requests==2.23.0
six==1.14.0
Expand Down
10 changes: 4 additions & 6 deletions examples/e2e/tests/test_user_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
from requests.auth import HTTPBasicAuth

import pytest
from pact import Consumer, Like, Provider, Term
from pact import Consumer, Like, Provider, Term, Format

from ..src.consumer import UserConsumer

log = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

print(Format().__dict__)

PACT_UPLOAD_URL = (
"http://127.0.0.1/pacts/provider/UserService/consumer"
Expand Down Expand Up @@ -76,14 +76,12 @@ def push_to_broker(version):
def test_get_user_non_admin(pact, consumer):
expected = {
'name': 'UserA',
'id': Term(
r'^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}\Z', # noqa: E501
'00000000-0000-4000-a000-000000000000'
),
'id': Format().uuid,
'created_on': Term(
r'\d+-\d+-\d+T\d+:\d+:\d+',
'2016-12-15T20:16:01'
),
'ip_address': Format().ip_address,
'admin': False
}

Expand Down
4 changes: 2 additions & 2 deletions pact/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Python methods for interactive with a Pact Mock Service."""
from .consumer import Consumer
from .matchers import EachLike, Like, SomethingLike, Term
from .matchers import EachLike, Like, SomethingLike, Term, Format
from .pact import Pact
from .provider import Provider
from .__version__ import __version__ # noqa: F401

__all__ = ('Consumer', 'EachLike', 'Like', 'Pact', 'Provider', 'SomethingLike',
'Term')
'Term', 'Format')
174 changes: 174 additions & 0 deletions pact/matchers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Classes for defining request and response data that is variable."""
import six
import datetime

from enum import Enum


class Matcher(object):
Expand Down Expand Up @@ -225,3 +228,174 @@ def get_generated_values(input):
return input.generate()['data']['generate']
else:
raise ValueError('Unknown type: %s' % type(input))


class Format:
"""
Class of regular expressions for common formats.
Example:
>>> from pact import Consumer, Provider
>>> from pact.matchers import Format
>>> pact = Consumer('consumer').has_pact_with(Provider('provider'))
>>> (pact.given('the current user is logged in as `tester`')
... .upon_receiving('a request for the user profile')
... .with_request('get', '/profile')
... .will_respond_with(200, body={
... 'id': Format().identifier,
... 'lastUpdated': Format().time
... }))
Would expect `id` to be any valid int and `lastUpdated` to be a valid time.
When the consumer runs this contract, the value of that will be returned
is the second value passed to Term in the given function, for the time
example it would be datetime.datetime(2000, 2, 1, 12, 30, 0, 0).time()
"""

def __init__(self):
"""Create a new Formatter."""
self.identifier = self.integer_or_identifier()
self.integer = self.integer_or_identifier()
self.decimal = self.decimal()
self.ip_address = self.ip_address()
self.hexadecimal = self.hexadecimal()
self.ipv6_address = self.ipv6_address()
self.uuid = self.uuid()
self.timestamp = self.timestamp()
self.date = self.date()
self.time = self.time()

def integer_or_identifier(self):
"""
Match any integer.
:return: a Like object with an integer.
:rtype: Like
"""
return Like(1)

def decimal(self):
"""
Match any decimal.
:return: a Like object with a decimal.
:rtype: Like
"""
return Like(1.0)

def ip_address(self):
"""
Match any ip address.
:return: a Term object with an ip address regex.
:rtype: Term
"""
return Term(self.Regexes.ip_address.value, '127.0.0.1')

def hexadecimal(self):
"""
Match any hexadecimal.
:return: a Term object with a hexdecimal regex.
:rtype: Term
"""
return Term(self.Regexes.hexadecimal.value, '3F')

def ipv6_address(self):
"""
Match any ipv6 address.
:return: a Term object with an ipv6 address regex.
:rtype: Term
"""
return Term(self.Regexes.ipv6_address.value, '::ffff:192.0.2.128')

def uuid(self):
"""
Match any uuid.
:return: a Term object with a uuid regex.
:rtype: Term
"""
return Term(
self.Regexes.uuid.value, 'fc763eba-0905-41c5-a27f-3934ab26786c'
)

def timestamp(self):
"""
Match any timestamp.
:return: a Term object with a timestamp regex.
:rtype: Term
"""
return Term(
self.Regexes.timestamp.value, datetime.datetime(
2000, 2, 1, 12, 30, 0, 0
)
)

def date(self):
"""
Match any date.
:return: a Term object with a date regex.
:rtype: Term
"""
return Term(
self.Regexes.date.value, datetime.datetime(
2000, 2, 1, 12, 30, 0, 0
).date()
)

def time(self):
"""
Match any time.
:return: a Term object with a time regex.
:rtype: Term
"""
return Term(
self.Regexes.time_regex.value, datetime.datetime(
2000, 2, 1, 12, 30, 0, 0
).time()
)

class Regexes(Enum):
"""Regex Enum for common formats."""

ip_address = r'(\d{1,3}\.)+\d{1,3}'
hexadecimal = r'[0-9a-fA-F]+'
ipv6_address = r'(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,6}\Z)|' \
r'(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}\Z)|(\A([0-9a-f]' \
r'{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}\Z)|(\A([0-9a-f]{1,4}:)' \
r'{1,4}(:[0-9a-f]{1,4}){1,3}\Z)|(\A([0-9a-f]{1,4}:){1,5}(:[0-' \
r'9a-f]{1,4}){1,2}\Z)|(\A([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4})' \
r'{1,1}\Z)|(\A(([0-9a-f]{1,4}:){1,7}|:):\Z)|(\A:(:[0-9a-f]{1,4})' \
r'{1,7}\Z)|(\A((([0-9a-f]{1,4}:){6})(25[0-5]|2[0-4]\d|[0-1]' \
r'?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A(([0-9a-f]' \
r'{1,4}:){5}[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25' \
r'[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A([0-9a-f]{1,4}:){5}:[' \
r'0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4' \
r']\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]' \
r'{1,4}){1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]' \
r'\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}' \
r'){1,3}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0' \
r'-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,' \
r'2}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]' \
r'?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,1}:' \
r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?' \
r'\d)){3}\Z)|(\A(([0-9a-f]{1,4}:){1,5}|:):(25[0-5]|2[0-4]\d|[0' \
r'-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A:(:[' \
r'0-9a-f]{1,4}){1,5}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]' \
r'|2[0-4]\d|[0-1]?\d?\d)){3}\Z)'
uuid = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
timestamp = r'^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3(' \
r'[12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-' \
r'9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2' \
r'[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d' \
r'([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$'
date = r'^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|' \
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)?)?$'
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def read(filename):

if sys.version_info.major == 2:
dependencies.append('subprocess32')
dependencies.append('enum34')

if __name__ == '__main__':
setup(
Expand Down
Loading

0 comments on commit 562e047

Please sign in to comment.