Repository: marshmallow-code/apispec
Pull request #364 (2019-01-28 20:28:54)

Add support for outputting field patterns


Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 126d2ba..fc2dc21 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -53,3 +53,4 @@ Contributors (chronological)
 - Laura Beaufort `@lbeaufort <https://github.com/lbeaufort>`_
 - Marcin Lulek `@ergo <https://github.com/ergo>`_
 - Jonathan Beezley `@jbeezley <https://github.com/jbeezley>`_
+- David Stapleton `@dstape <https://github.com/DStape>`_
diff --git a/apispec/compat.py b/apispec/compat.py
index 8114081..82d248b 100644
--- a/apispec/compat.py
+++ b/apispec/compat.py
@@ -1,7 +1,10 @@
 # -*- coding: utf-8 -*-
 # flake8: noqa
+import re
 import sys
 
+RegexType = type(re.compile(''))
+
 PY2 = int(sys.version[0]) == 2
 
 if PY2:
diff --git a/apispec/ext/marshmallow/openapi.py b/apispec/ext/marshmallow/openapi.py
index 334cb9f..fdd6ce3 100644
--- a/apispec/ext/marshmallow/openapi.py
+++ b/apispec/ext/marshmallow/openapi.py
@@ -7,6 +7,7 @@ OpenAPI 2.0 spec: https://github.com/OAI/OpenAPI-Specification/blob/master/versi
 from __future__ import absolute_import, unicode_literals
 import operator
 import functools
+import warnings
 from collections import OrderedDict
 
 import marshmallow
@@ -14,6 +15,7 @@ from marshmallow.utils import is_collection
 from marshmallow.compat import iteritems
 from marshmallow.orderedset import OrderedSet
 
+from apispec.compat import RegexType
 from apispec.utils import OpenAPIVersion
 from .common import (
     resolve_schema_cls, get_fields, make_schema_key, resolve_schema_instance,
@@ -329,6 +331,27 @@ class OpenAPIConverter(object):
                 attributes[max_attr] = validator.equal
         return attributes
 
+    def field2pattern(self, field, **kwargs):
+        """Return the dictionary of OpenAPI field attributes for a set of
+        :class:`Range <marshmallow.validators.Regexp>` validators.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        regex_validators = (v for v in field.validators
+                            if isinstance(getattr(v, 'regex', None), RegexType))
+        v = next(regex_validators, None)
+        attributes = {} if v is None else {'pattern': v.regex.pattern}
+
+        if next(regex_validators, None) is not None:
+            warnings.warn(
+                'More than one regex validator defined on {} field. Only the '
+                'first one will be used in the output spec.'
+                .format(type(field)), UserWarning,
+            )
+
+        return attributes
+
     def metadata2properties(self, field):
         """Return a dictionary of properties extracted from field Metadata
 
@@ -384,6 +407,7 @@ class OpenAPIConverter(object):
                 self.field2nullable,
                 self.field2range,
                 self.field2length,
+                self.field2pattern,
                 self.metadata2properties,
         ):
             ret.update(attr_func(field))


Test patch:
diff --git a/tests/test_openapi.py b/tests/test_openapi.py
index e0a688a..14dd18a 100644
--- a/tests/test_openapi.py
+++ b/tests/test_openapi.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 
+import re
 import warnings
 
 import pytest
@@ -235,6 +236,33 @@ class TestMarshmallowFieldToOpenAPI:
         else:
             assert res['nullable'] is True
 
+    def test_field_with_str_regex(self, openapi):
+        regex_str = '^[a-zA-Z0-9]$'
+        field = fields.Str(validate=validate.Regexp(regex_str))
+        ret = openapi.field2property(field)
+        assert ret['pattern'] == regex_str
+
+    def test_field_with_pattern_obj_regex(self, openapi):
+        regex_str = '^[a-zA-Z0-9]$'
+        field = fields.Str(validate=validate.Regexp(re.compile(regex_str)))
+        ret = openapi.field2property(field)
+        assert ret['pattern'] == regex_str
+
+    def test_field_with_no_pattern(self, openapi):
+        field = fields.Str()
+        ret = openapi.field2property(field)
+        assert 'pattern' not in ret
+
+    def test_field_with_multiple_patterns(self, recwarn, openapi):
+        regex_validators = [
+            validate.Regexp('winner'),
+            validate.Regexp('loser'),
+        ]
+        field = fields.Str(validate=regex_validators)
+        with pytest.warns(UserWarning, match='More than one regex validator'):
+            ret = openapi.field2property(field)
+        assert ret['pattern'] == 'winner'
+
 class TestMarshmallowSchemaToModelDefinition:
 
     def test_invalid_schema(self, openapi):