diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index a625251..10d7567 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -48,7 +48,7 @@ jobs: run: timeout 10s poetry run pip --version || rm -rf .venv - name: Install dependencies - run: poetry install -E rfc3339-validator -E strict-rfc3339 -E isodate + run: poetry install -E rfc3339-validator - name: Test env: diff --git a/openapi_schema_validator/_format.py b/openapi_schema_validator/_format.py index 273c8b5..11fd863 100644 --- a/openapi_schema_validator/_format.py +++ b/openapi_schema_validator/_format.py @@ -1,43 +1,10 @@ import binascii from base64 import b64decode from base64 import b64encode -from datetime import datetime from typing import Any -from typing import Tuple from typing import Union -from uuid import UUID from jsonschema._format import FormatChecker -from jsonschema.exceptions import FormatError - -DATETIME_HAS_RFC3339_VALIDATOR = False -DATETIME_HAS_STRICT_RFC3339 = False -DATETIME_HAS_ISODATE = False -DATETIME_RAISES: Tuple[Exception, ...] = () - -try: - import isodate -except ImportError: - pass -else: - DATETIME_HAS_ISODATE = True - DATETIME_RAISES += (ValueError, isodate.ISO8601Error) - -try: - from rfc3339_validator import validate_rfc3339 -except ImportError: - pass -else: - DATETIME_HAS_RFC3339_VALIDATOR = True - DATETIME_RAISES += (ValueError, TypeError) - -try: - import strict_rfc3339 -except ImportError: - pass -else: - DATETIME_HAS_STRICT_RFC3339 = True - DATETIME_RAISES += (ValueError, TypeError) def is_int32(instance: Any) -> bool: @@ -65,91 +32,32 @@ def is_binary(instance: Any) -> bool: def is_byte(instance: Union[str, bytes]) -> bool: if isinstance(instance, str): instance = instance.encode() - - try: - encoded = b64encode(b64decode(instance)) - except TypeError: - return False - else: - return encoded == instance - - -def is_datetime(instance: str) -> bool: - if not isinstance(instance, (bytes, str)): - return False - - if DATETIME_HAS_RFC3339_VALIDATOR: - return bool(validate_rfc3339(instance)) - - if DATETIME_HAS_STRICT_RFC3339: - return bool(strict_rfc3339.validate_rfc3339(instance)) - - if DATETIME_HAS_ISODATE: - return bool(isodate.parse_datetime(instance)) - - return True - - -def is_date(instance: Any) -> bool: - if not isinstance(instance, (bytes, str)): + if not isinstance(instance, bytes): return False - if isinstance(instance, bytes): - instance = instance.decode() - - return bool(datetime.strptime(instance, "%Y-%m-%d")) + encoded = b64encode(b64decode(instance)) + return encoded == instance -def is_uuid(instance: Any) -> bool: +def is_password(instance: Any) -> bool: if not isinstance(instance, (bytes, str)): return False - if isinstance(instance, bytes): - instance = instance.decode() - - return str(UUID(instance)).lower() == instance.lower() - - -def is_password(instance: Any) -> bool: return True -class OASFormatChecker(FormatChecker): # type: ignore - - checkers = { - "int32": (is_int32, ()), - "int64": (is_int64, ()), - "float": (is_float, ()), - "double": (is_double, ()), - "byte": (is_byte, (binascii.Error, TypeError)), - "binary": (is_binary, ()), - "date": (is_date, (ValueError,)), - "date-time": (is_datetime, DATETIME_RAISES), - "password": (is_password, ()), - # non standard - "uuid": (is_uuid, (AttributeError, ValueError)), - } - - def check(self, instance: Any, format: str) -> Any: - if format not in self.checkers: - raise FormatError( - f"Format checker for {format!r} format not found" - ) - - func, raises = self.checkers[format] - result, cause = None, None - try: - result = func(instance) - except raises as e: # type: ignore - cause = e - - if not result: - raise FormatError( - f"{instance!r} is not a {format!r}", - cause=cause, - ) - return result - - -oas30_format_checker = OASFormatChecker() -oas31_format_checker = oas30_format_checker +oas30_format_checker = FormatChecker() +oas30_format_checker.checks("int32")(is_int32) +oas30_format_checker.checks("int64")(is_int64) +oas30_format_checker.checks("float")(is_float) +oas30_format_checker.checks("double")(is_double) +oas30_format_checker.checks("binary")(is_binary) +oas30_format_checker.checks("byte", (binascii.Error, TypeError))(is_byte) +oas30_format_checker.checks("password")(is_password) + +oas31_format_checker = FormatChecker() +oas31_format_checker.checks("int32")(is_int32) +oas31_format_checker.checks("int64")(is_int64) +oas31_format_checker.checks("float")(is_float) +oas31_format_checker.checks("double")(is_double) +oas31_format_checker.checks("password")(is_password) diff --git a/openapi_schema_validator/validators.py b/openapi_schema_validator/validators.py index 7a57d9b..5a39f86 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -10,6 +10,7 @@ from jsonschema.validators import create from jsonschema.validators import extend +from openapi_schema_validator import _format as oas_format from openapi_schema_validator import _types as oas_types from openapi_schema_validator import _validators as oas_validators from openapi_schema_validator._types import oas31_type_checker @@ -55,6 +56,7 @@ "deprecated": oas_validators.not_implemented, }, type_checker=oas_types.oas30_type_checker, + format_checker=oas_format.oas30_format_checker, # NOTE: version causes conflict with global jsonschema validator # See https://github.com/p1c2u/openapi-schema-validator/pull/12 # version="oas30", @@ -94,6 +96,7 @@ "example": oas_validators.not_implemented, }, type_checker=oas31_type_checker, + format_checker=oas_format.oas31_format_checker, ) diff --git a/pyproject.toml b/pyproject.toml index 0b898f2..d41af63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,18 +17,10 @@ strict = true module = "jsonschema.*" ignore_missing_imports = true -[[tool.mypy.overrides]] -module = "isodate" -ignore_missing_imports = true - [[tool.mypy.overrides]] module = "rfc3339_validator" ignore_missing_imports = true -[[tool.mypy.overrides]] -module = "strict_rfc3339" -ignore_missing_imports = true - [tool.poetry] name = "openapi-schema-validator" version = "0.4.1" @@ -54,13 +46,9 @@ classifiers = [ python = "^3.7.0" jsonschema = "^4.0.0" rfc3339-validator = {version = "*", optional = true} -strict-rfc3339 = {version = "*", optional = true} -isodate = {version = "*", optional = true} [tool.poetry.extras] rfc3339-validator = ["rfc3339-validator"] -strict-rfc3339 = ["strict-rfc3339"] -isodate = ["isodate"] [tool.poetry.dev-dependencies] black = "^22.0.0" diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index df6b160..908caf6 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -1,3 +1,5 @@ +from base64 import b64encode + import pytest from jsonschema import ValidationError @@ -8,10 +10,18 @@ from openapi_schema_validator import oas30_format_checker from openapi_schema_validator import oas31_format_checker -try: - from unittest import mock -except ImportError: - from unittest import mock + +class TestOAS30Validator: + + def test_format_checkers(self): + assert set(OAS30Validator.FORMAT_CHECKER.checkers.keys()) == set([ + # standard formats + "int32", "int64", "float", "double", "byte", "binary", + "date", "date-time", "password", + # extra formats + "uuid", "regex", + "ipv4", "ipv6", "email", "idn-email", "time" + ]) class TestOAS30ValidatorValidate: @@ -82,148 +92,28 @@ def test_nullable_enum_with_none(self): @pytest.mark.parametrize( "value", [ - "1989-01-02T00:00:00Z", - "2018-01-02T23:59:59Z", - ], - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_RFC3339_VALIDATOR", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_STRICT_RFC3339", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_ISODATE", False - ) - def test_string_format_no_datetime_validator(self, value): - schema = {"type": "string", "format": "date-time"} - validator = OAS30Validator(schema, format_checker=oas30_format_checker) - - result = validator.validate(value) - - assert result is None - - @pytest.mark.parametrize( - "value", - [ - "1989-01-02T00:00:00Z", - "2018-01-02T23:59:59Z", - ], - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_RFC3339_VALIDATOR", - True, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_STRICT_RFC3339", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_ISODATE", False - ) - def test_string_format_datetime_rfc3339_validator(self, value): - schema = {"type": "string", "format": "date-time"} - validator = OAS30Validator(schema, format_checker=oas30_format_checker) - - result = validator.validate(value) - - assert result is None - - @pytest.mark.parametrize( - "value", - [ - "1989-01-02T00:00:00Z", - "2018-01-02T23:59:59Z", - ], - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_RFC3339_VALIDATOR", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_STRICT_RFC3339", True - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_ISODATE", False - ) - def test_string_format_datetime_strict_rfc3339(self, value): - schema = {"type": "string", "format": "date-time"} - validator = OAS30Validator(schema, format_checker=oas30_format_checker) - - result = validator.validate(value) - - assert result is None - - @pytest.mark.parametrize( - "value", - [ - "1989-01-02T00:00:00Z", - "2018-01-02T23:59:59Z", - ], - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_RFC3339_VALIDATOR", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_STRICT_RFC3339", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_ISODATE", True + b64encode(b"string"), + b64encode(b"string").decode(), + ] ) - def test_string_format_datetime_isodate(self, value): - schema = {"type": "string", "format": "date-time"} + def test_string_format_byte_valid(self, value): + schema = {"type": "string", "format": "byte"} validator = OAS30Validator(schema, format_checker=oas30_format_checker) result = validator.validate(value) assert result is None - @pytest.mark.parametrize( - "value", - [ - "1989-01-00Z", - "2018", - ], - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_RFC3339_VALIDATOR", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_STRICT_RFC3339", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_ISODATE", True - ) - def test_string_format_datetime_invalid_isodate(self, value): - schema = {"type": "string", "format": "date-time"} + @pytest.mark.parametrize("value", ["string", b"string"]) + def test_string_format_byte_invalid(self, value): + schema = {"type": "string", "format": "byte"} validator = OAS30Validator(schema, format_checker=oas30_format_checker) with pytest.raises( - ValidationError, match=f"'{value}' is not a 'date-time'" + ValidationError, match="is not a 'byte'" ): validator.validate(value) - @pytest.mark.parametrize( - "value", - [ - "f50ec0b7-f960-400d-91f0-c42a6d44e3d0", - "F50EC0B7-F960-400D-91F0-C42A6D44E3D0", - ], - ) - def test_string_uuid(self, value): - schema = {"type": "string", "format": "uuid"} - validator = OAS30Validator(schema, format_checker=oas30_format_checker) - - result = validator.validate(value) - - assert result is None - def test_allof_required(self): schema = { "allOf": [ @@ -578,6 +468,18 @@ def test_nullable_schema_combos(self, is_nullable, schema_type, not_nullable_reg assert False +class TestOAS31Validator: + + def test_format_checkers(self): + assert set(OAS31Validator.FORMAT_CHECKER.checkers.keys()) == set([ + # standard formats + "int32", "int64", "float", "double", "password", + # extra formats + "date", "date-time", "uuid", "regex", + "ipv4", "ipv6", "email", "idn-email", "time" + ]) + + class TestOAS30ReadWriteValidatorValidate: def test_read_only(self): @@ -693,139 +595,6 @@ def test_nullable(self, schema_type): assert result is None - @pytest.mark.parametrize( - "value", - [ - "1989-01-02T00:00:00Z", - "2018-01-02T23:59:59Z", - ], - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_RFC3339_VALIDATOR", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_STRICT_RFC3339", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_ISODATE", False - ) - def test_string_format_no_datetime_validator(self, value): - schema = {"type": "string", "format": "date-time"} - validator = OAS31Validator( - schema, - format_checker=oas31_format_checker, - ) - - result = validator.validate(value) - - assert result is None - - @pytest.mark.parametrize( - "value", - [ - "1989-01-02T00:00:00Z", - "2018-01-02T23:59:59Z", - ], - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_RFC3339_VALIDATOR", - True, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_STRICT_RFC3339", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_ISODATE", False - ) - def test_string_format_datetime_rfc3339_validator(self, value): - schema = {"type": "string", "format": "date-time"} - validator = OAS31Validator( - schema, - format_checker=oas31_format_checker, - ) - - result = validator.validate(value) - - assert result is None - - @pytest.mark.parametrize( - "value", - [ - "1989-01-02T00:00:00Z", - "2018-01-02T23:59:59Z", - ], - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_RFC3339_VALIDATOR", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_STRICT_RFC3339", True - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_ISODATE", False - ) - def test_string_format_datetime_strict_rfc3339(self, value): - schema = {"type": "string", "format": "date-time"} - validator = OAS31Validator( - schema, - format_checker=oas31_format_checker, - ) - - result = validator.validate(value) - - assert result is None - - @pytest.mark.parametrize( - "value", - [ - "1989-01-02T00:00:00Z", - "2018-01-02T23:59:59Z", - ], - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_RFC3339_VALIDATOR", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_STRICT_RFC3339", - False, - ) - @mock.patch( - "openapi_schema_validator._format." "DATETIME_HAS_ISODATE", True - ) - def test_string_format_datetime_isodate(self, value): - schema = {"type": "string", "format": "date-time"} - validator = OAS31Validator( - schema, - format_checker=oas31_format_checker, - ) - - result = validator.validate(value) - - assert result is None - - @pytest.mark.parametrize( - "value", - [ - "f50ec0b7-f960-400d-91f0-c42a6d44e3d0", - "F50EC0B7-F960-400D-91F0-C42A6D44E3D0", - ], - ) - def test_string_uuid(self, value): - schema = {"type": "string", "format": "uuid"} - validator = OAS31Validator( - schema, - format_checker=oas31_format_checker, - ) - - result = validator.validate(value) - - assert result is None - def test_schema_validation(self): schema = { "type": "object",