Repository: Dorthu/openapi3

Issue:
openapi3.errors.SpecError when minimum/maximum validation is float
The following file::

<details><summary><code>openapi.json</code></summary>

```json
{
    "openapi": "3.0.3",
    "info": {
        "title": "Foo",
        "version": "1.0.0"
    },
    "paths": {
        "/foo": {
            "post": {
                "responses": {
                    "200": {
                        "description": "Foo.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "value": {
                                            "type": "number",
                                            "format": "double",
                                            "minimum": 0.0,
                                            "maximum": 1.0
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```

</details>

Fails to load with the following error:


<details><summary>Traceback</summary>

```
Traceback (most recent call last):
  File "<censored>.py", line 5, in <module>
    api = OpenAPI(json.load(fd))
  File "<censored>/openapi.py", line 44, in __init__
    super(OpenAPI, self).__init__([], raw_document, self)
  File "<censored>/object_base.py", line 56, in __init__
    self._parse_data()
  File "<censored>/openapi.py", line 134, in _parse_data
    self.paths        = self._get('paths', ['Path'], is_map=True)
  File "<censored>/object_base.py", line 193, in _get
    ret = Map(self.path + [field], ret, object_types, self._root)
  File "<censored>/object_base.py", line 516, in __init__
    dct[k] = t(path + [k], v, self._root)
  File "<censored>/object_base.py", line 56, in __init__
    self._parse_data()
  File "<censored>/paths.py", line 36, in _parse_data
    self.post        = self._get('post', 'Operation')
  File "<censored>/object_base.py", line 209, in _get
    ret = python_type(self.path + [field], ret, self._root)
  File "<censored>/object_base.py", line 56, in __init__
    self._parse_data()
  File "<censored>/paths.py", line 102, in _parse_data
    self.responses    = self._get('responses', ['Response', 'Reference'], is_map=True)
  File "<censored>/object_base.py", line 193, in _get
    ret = Map(self.path + [field], ret, object_types, self._root)
  File "<censored>/object_base.py", line 516, in __init__
    dct[k] = t(path + [k], v, self._root)
  File "<censored>/object_base.py", line 56, in __init__
    self._parse_data()
  File "<censored>/paths.py", line 385, in _parse_data
    self.content     = self._get('content', ['MediaType'], is_map=True)
  File "<censored>/object_base.py", line 193, in _get
    ret = Map(self.path + [field], ret, object_types, self._root)
  File "<censored>/object_base.py", line 516, in __init__
    dct[k] = t(path + [k], v, self._root)
  File "<censored>/object_base.py", line 56, in __init__
    self._parse_data()
  File "<censored>/paths.py", line 365, in _parse_data
    self.schema   = self._get('schema', ['Schema', 'Reference'])
  File "<censored>/object_base.py", line 209, in _get
    ret = python_type(self.path + [field], ret, self._root)
  File "<censored>/object_base.py", line 56, in __init__
    self._parse_data()
  File "<censored>/schemas.py", line 50, in _parse_data
    self.properties           = self._get('properties', ['Schema', 'Reference'], is_map=True)
  File "<censored>/object_base.py", line 193, in _get
    ret = Map(self.path + [field], ret, object_types, self._root)
  File "<censored>/object_base.py", line 516, in __init__
    dct[k] = t(path + [k], v, self._root)
  File "<censored>/object_base.py", line 56, in __init__
    self._parse_data()
  File "<censored>/schemas.py", line 36, in _parse_data
    self.maximum              = self._get('maximum', int)
  File "<censored>/object_base.py", line 222, in _get
    raise SpecError('Expected {}.{} to be one of [{}], got {}'.format(
openapi3.errors.SpecError: Expected paths./foo.post.responses.200.content.application/json.schema.properties.value.maximum to be one of [<class 'int'>], got <class 'float'>
```

</details>

The specification reads ([section 5.2, validation schema](https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.2)):

> The value of "maximum" MUST be a number, representing an upper limit
> for a numeric instance.
> 
> If the instance is a number, then this keyword validates if
> "exclusiveMaximum" is true and instance is less than the provided
> value, or else if the instance is less than or exactly equal to the
> provided value.

The same happens with `minimum` (and probably the `exclusive*` variants, didn't bother to check).

A number is ([section 4.2, core schema](https://tools.ietf.org/html/draft-wright-json-schema-00#section-4.2)):

> number  An arbitrary-precision, base-10 decimal number value, from
> the JSON "number" production

Thus, the code should not be checking for `int` but `float`.

Discussion:

Patch:
diff --git a/openapi3/schemas.py b/openapi3/schemas.py
index 78ab0c3..081b28a 100644
--- a/openapi3/schemas.py
+++ b/openapi3/schemas.py
@@ -33,8 +33,8 @@ class Schema(ObjectBase):
         Implementation of :any:`ObjectBase._parse_data`
         """
         self.title                = self._get('title', str)
-        self.maximum              = self._get('maximum', int)
-        self.minimum              = self._get('minimum', int)
+        self.maximum              = self._get('maximum', [int, float])
+        self.minimum              = self._get('minimum', [int, float])
         self.maxLength            = self._get('maxLength', int)
         self.minLength            = self._get('minLength', int)
         self.pattern              = self._get('pattern', str)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 44d4a16..b071006 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -75,7 +75,6 @@ def broken_reference():
     yield _get_parsed_yaml("broken-ref.yaml")
 
 
-@pytest.fixture
 def has_bad_parameter_name():
     """
     Provides the parsed yaml for a spec with a bad parameter name
@@ -97,3 +96,11 @@ def parameter_with_underscores():
     A valid spec with underscores in a path parameter
     """
     yield _get_parsed_yaml("parameter-with-underscores.yaml")
+
+
+@pytest.fixture
+def float_validation_expanded():
+    """
+    Provides the float-validation.yaml spec
+    """
+    yield _get_parsed_yaml("float-validation.yaml")
diff --git a/tests/fixtures/float-validation.yaml b/tests/fixtures/float-validation.yaml
new file mode 100644
index 0000000..c9478aa
--- /dev/null
+++ b/tests/fixtures/float-validation.yaml
@@ -0,0 +1,25 @@
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Testing float validations
+paths:
+  /foo:
+    get:
+      responses:
+        '200':
+          description: foo
+          content:
+            'application/json':
+              schema:
+                type: object
+                properties:
+                  integer:
+                    type: integer
+                    format: int32
+                    minimum: 0
+                    maximum: 1
+                  real:
+                    type: number
+                    format: double
+                    minimum: 0.0
+                    maximum: 1.0
diff --git a/tests/parsing_test.py b/tests/parsing_test.py
index 16be56d..0e3118e 100644
--- a/tests/parsing_test.py
+++ b/tests/parsing_test.py
@@ -51,3 +51,16 @@ def test_parsing_parameter_name_with_underscores(parameter_with_underscores):
     Tests that path parameters with underscores in them are accepted
     """
     spec = OpenAPI(parameter_with_underscores)
+
+
+def test_parsing_float_validation(float_validation_expanded):
+    """
+    Tests that `minimum` and similar validators work with floats.
+    """
+    spec = OpenAPI(float_validation_expanded)
+    properties = spec.paths['/foo'].get.responses['200'].content['application/json'].schema.properties
+
+    assert isinstance(properties['integer'].minimum, int)
+    assert isinstance(properties['integer'].maximum, int)
+    assert isinstance(properties['real'].minimum, float)
+    assert isinstance(properties['real'].maximum, float)