Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add matchers.Format for matching common types #109

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pactman/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Python methods for interactive with a Pact Mock Service."""
from .mock.consumer import Consumer
from .mock.matchers import EachLike, Equals, Includes, Like, SomethingLike, Term
from .mock.matchers import EachLike, Equals, Format, Includes, Like, SomethingLike, Term
from .mock.pact import Pact
from .mock.provider import Provider

__all__ = (
"Consumer",
"EachLike",
"Equals",
"Format",
"Includes",
"Like",
"Pact",
Expand Down
174 changes: 174 additions & 0 deletions pactman/mock/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 datetime
from enum import Enum


class Matcher(object):
"""Base class for defining complex contract expectations."""
Expand Down Expand Up @@ -448,3 +451,174 @@ def get_matching_rules_v3(input, path):
rules = MatchingRuleV3()
rules.generate(input, path)
return rules


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
).isoformat()
)

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().isoformat()
)

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().isoformat()
)

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)?)?$'
141 changes: 141 additions & 0 deletions pactman/test/mock_matchers/test_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from datetime import datetime
from unittest import TestCase

from pactman import Format


class FormatTestCase(TestCase):
@classmethod
def setUpClass(cls):
cls.formatter = Format()

def test_identifier(self):
identifier = self.formatter.identifier.ruby_protocol()
self.assertEqual(identifier, {"json_class": "Pact::SomethingLike", "contents": 1})

def test_integer(self):
integer = self.formatter.integer.ruby_protocol()
self.assertEqual(integer, {"json_class": "Pact::SomethingLike", "contents": 1})

def test_decimal(self):
decimal = self.formatter.integer.ruby_protocol()
self.assertEqual(decimal, {"json_class": "Pact::SomethingLike", "contents": 1.0})

def test_ip_address(self):
ip_address = self.formatter.ip_address.ruby_protocol()
self.assertEqual(
ip_address,
{
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.ip_address.value,
"o": 0,
},
"generate": "127.0.0.1",
},
},
)

def test_hexadecimal(self):
hexadecimal = self.formatter.hexadecimal.ruby_protocol()
self.assertEqual(
hexadecimal,
{
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.hexadecimal.value,
"o": 0,
},
"generate": "3F",
},
},
)

def test_ipv6_address(self):
ipv6_address = self.formatter.ipv6_address.ruby_protocol()
self.assertEqual(
ipv6_address,
{
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.ipv6_address.value,
"o": 0,
},
"generate": "::ffff:192.0.2.128",
},
},
)

def test_uuid(self):
uuid = self.formatter.uuid.ruby_protocol()
self.assertEqual(
uuid,
{
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.uuid.value,
"o": 0,
},
"generate": "fc763eba-0905-41c5-a27f-3934ab26786c",
},
},
)

def test_timestamp(self):
timestamp = self.formatter.timestamp.ruby_protocol()
self.assertEqual(
timestamp,
{
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.timestamp.value,
"o": 0,
},
"generate": datetime(2000, 2, 1, 12, 30, 0, 0).isoformat(),
},
},
)

def test_date(self):
date = self.formatter.date.ruby_protocol()
self.assertEqual(
date,
{
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.date.value,
"o": 0,
},
"generate": datetime(2000, 2, 1, 12, 30, 0, 0).date().isoformat(),
},
},
)

def test_time(self):
time = self.formatter.time.ruby_protocol()
self.assertEqual(
time,
{
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.time_regex.value,
"o": 0,
},
"generate": datetime(2000, 2, 1, 12, 30, 0, 0).time().isoformat(),
},
},
)