diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py index 4e17aa2e..44ed1ad2 100644 --- a/src/apispec/ext/marshmallow/field_converter.py +++ b/src/apispec/ext/marshmallow/field_converter.py @@ -113,6 +113,7 @@ def init_attribute_functions(self): self.list2properties, self.dict2properties, self.timedelta2properties, + self.date2properties, self.datetime2properties, self.field2nullable, ] @@ -544,9 +545,7 @@ def datetime2properties(self, field, **kwargs: typing.Any) -> dict: :rtype: dict """ ret = {} - if isinstance(field, marshmallow.fields.DateTime) and not isinstance( - field, marshmallow.fields.Date - ): + if isinstance(field, marshmallow.fields.DateTime): if field.format == "iso" or field.format is None: # Will return { "type": "string", "format": "date-time" } # as specified inside DEFAULT_FIELD_MAPPING @@ -574,16 +573,62 @@ def datetime2properties(self, field, **kwargs: typing.Any) -> dict: "example": "1676451277514.654", "min": "0", } + elif field.format is not None: + ret = { + "type": "string", + "format": None, + "pattern": ( + field.metadata["pattern"] + if field.metadata.get("pattern") + else None + ), + } else: ret = { "type": "string", "format": None, - "pattern": field.metadata["pattern"] - if field.metadata.get("pattern") - else None, + "pattern": ( + field.metadata["pattern"] + if field.metadata.get("pattern") + else None + ), } return ret + def date2properties( + self, field: marshmallow.fields.Date, **kwargs: typing.Any + ) -> dict: + """Return a dictionary of OpenAPI properties for a Date field. + + :param Field field: A marshmallow Date field. + :rtype: dict + """ + ret = {} + + if isinstance(field, marshmallow.fields.Date): + if field.format == "iso" or field.format is None: + ret = { + "type": "string", + "format": "date", + } + elif field.format: + ret = { + "type": "string", + "format": None, + } + else: + ret = { + "type": "string", + "format": None, + "pattern": ( + field.metadata["pattern"] + if field.metadata.get("pattern") + else None + ), + } + + return ret + def make_type_list(types): """Return a list of types from a type attribute diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py index 20e079d2..6a31553e 100644 --- a/tests/test_ext_marshmallow_field.py +++ b/tests/test_ext_marshmallow_field.py @@ -457,63 +457,59 @@ def test_nested_field_with_property(spec_fixture): } -def test_datetime2property_iso(spec_fixture): - field = fields.DateTime(format="iso") - res = spec_fixture.openapi.field2property(field) - assert res == { - "type": "string", - "format": "date-time", - } - - -def test_datetime2property_rfc(spec_fixture): - field = fields.DateTime(format="rfc") - res = spec_fixture.openapi.field2property(field) - assert res == { - "type": "string", - "format": None, - "example": "Wed, 02 Oct 2002 13:00:00 GMT", - "pattern": r"((Mon|Tue|Wed|Thu|Fri|Sat|Sun), ){0,1}\d{2} " - + r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} " - + r"(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|(Z|A|M|N)|(\+|-)\d{4})", - } - - -def test_datetime2property_timestamp(spec_fixture): - field = fields.DateTime(format="timestamp") - res = spec_fixture.openapi.field2property(field) - assert res == { - "type": "number", - "format": "float", - "min": "0", - "example": "1676451245.596", - } - - -def test_datetime2property_timestamp_ms(spec_fixture): - field = fields.DateTime(format="timestamp_ms") - res = spec_fixture.openapi.field2property(field) - assert res == { - "type": "number", - "format": "float", - "min": "0", - "example": "1676451277514.654", - } - - -def test_datetime2property_custom_format(spec_fixture): - field = fields.DateTime( - format="%d-%m%Y %H:%M:%S", - metadata={ - "pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$" - }, - ) +@pytest.mark.parametrize( + ("format", "expected"), + [ + ( + "iso", + { + "type": "string", + "format": "date-time", + }, + ), + ( + "rfc", + { + "type": "string", + "format": None, + "example": "Wed, 02 Oct 2002 13:00:00 GMT", + "pattern": r"((Mon|Tue|Wed|Thu|Fri|Sat|Sun), ){0,1}\d{2} " + + r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} " + + r"(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|(Z|A|M|N)|(\+|-)\d{4})", + }, + ), + ( + "timestamp", + { + "type": "number", + "format": "float", + "min": "0", + "example": "1676451245.596", + }, + ), + ( + "timestamp_ms", + { + "type": "number", + "format": "float", + "min": "0", + "example": "1676451277514.654", + }, + ), + ( + "%d-%m%Y %H:%M:%S", + { + "type": "string", + "format": None, + "pattern": None, + }, + ), + ], +) +def test_datetime2property(spec_fixture, format, expected): + field = fields.DateTime(format=format) res = spec_fixture.openapi.field2property(field) - assert res == { - "type": "string", - "format": None, - "pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$", - } + assert res == expected def test_datetime2property_custom_format_missing_regex(spec_fixture): @@ -601,3 +597,36 @@ class _DesertSentinel: field.metadata[_DesertSentinel()] = "to be ignored" result = spec_fixture.openapi.field2property(field) assert result == {"description": "A description", "type": "boolean"} + + +@pytest.mark.parametrize( + ("format", "expected"), + [ + ( + None, + { + "type": "string", + "format": "date", + }, + ), + ( + "iso", + { + "type": "string", + "format": "date", + }, + ), + ( + "%d-%m-%Y", + { + "type": "string", + "format": None, + "pattern": None, + }, + ), + ], +) +def test_date2property(spec_fixture, format, expected): + field = fields.Date(format=format) + res = spec_fixture.openapi.field2property(field) + assert res == expected