Repository: jdkandersson/OpenAlchemy

Issue:
Expose schema validation errors
As a user I want to know all validation errors for my schema so that I can fix them.

- [x] Create a data model that records information about each model and its properties.
- [x] Add a constructor that takes schemas and builds out the data model.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17a8499d..ec37dfe0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 - Add schemas pre-processor that extracts the required back references.
 - Add foreign key pre-processor that extracts the required back references.
 - Add schema validation pre-processor.
+- Add function that checks a specification.
 
 ## Version 1.3.0 - 2020-07-12
 
diff --git a/open_alchemy/helpers/schema.py b/open_alchemy/helpers/schema.py
index d1cb4de0..9afe830c 100644
--- a/open_alchemy/helpers/schema.py
+++ b/open_alchemy/helpers/schema.py
@@ -23,19 +23,22 @@ def constructable(*, schema: types.Schema, schemas: types.Schemas) -> bool:
         Whether the schema is constructable.
 
     """
+    if not isinstance(schema, dict):
+        return False
     # Check for reference only models
     ref = schema.get("$ref")
     if ref is not None and (not isinstance(ref, str) or ref.startswith("#")):
         return False
-    # Check for single item allOf
+    # Check allOf
     all_of = schema.get("allOf")
     if all_of is not None and (not isinstance(all_of, list) or len(all_of) < 2):
-        return False
-    # Check for tablename
-    if peek.tablename(schema=schema, schemas=schemas) is not None:
-        return True
-    # Check for inherits
-    if inherits(schema=schema, schemas=schemas) is True:
+        if not isinstance(all_of, list) or not all_of:
+            return False
+        return constructable(schema=all_of[0], schemas=schemas)
+    # Check for tablename and inherits
+    tablename = peek.tablename(schema=schema, schemas=schemas)
+    schema_inherits = inherits(schema=schema, schemas=schemas)
+    if tablename is not None or schema_inherits is True:
         return True
     return False
 
diff --git a/open_alchemy/schemas/validation/__init__.py b/open_alchemy/schemas/validation/__init__.py
index 96f62d44..aeb20cf9 100644
--- a/open_alchemy/schemas/validation/__init__.py
+++ b/open_alchemy/schemas/validation/__init__.py
@@ -1,13 +1,31 @@
 """Schema validation pre-processor."""
 
+import typing
+
 from ... import exceptions as _exceptions
-from ... import types as _types
+from ... import types as _oa_types
 from .. import helpers as _helpers
 from . import model
 from . import property_
+from . import types
 
 
-def process_model(schemas: _types.Schemas, schema_name, schema: _types.Schema) -> None:
+def _get_properties_results(
+    schemas: _oa_types.Schemas, schema: _oa_types.Schema
+) -> typing.Iterable[typing.Tuple[str, types.Result]]:
+    """Get an iterator with properties results."""
+    properties = _helpers.iterate.properties_items(
+        schema=schema, schemas=schemas, stay_within_model=True
+    )
+    return map(
+        lambda args: (args[0], property_.check(schemas, schema, args[0], args[1])),
+        properties,
+    )
+
+
+def _process_model(
+    schemas: _oa_types.Schemas, schema_name: str, schema: _oa_types.Schema
+) -> None:
     """
     Validate the model schema properties.
 
@@ -15,16 +33,11 @@ def process_model(schemas: _types.Schemas, schema_name, schema: _types.Schema) -
 
     Args:
         schemas: All defined schemas used to resolve any $ref.
+        schema_name: The name of the schema to validate.
         schema: The schema to validate.
 
     """
-    properties = _helpers.iterate.properties_items(
-        schema=schema, schemas=schemas, stay_within_model=True
-    )
-    properties_results = map(
-        lambda args: (args[0], property_.check(schemas, schema, args[0], args[1])),
-        properties,
-    )
+    properties_results = _get_properties_results(schemas, schema)
     invalid_properties_result = next(
         filter(lambda args: not args[1].valid, properties_results), None
     )
@@ -35,7 +48,7 @@ def process_model(schemas: _types.Schemas, schema_name, schema: _types.Schema) -
         )
 
 
-def process(*, schemas: _types.Schemas) -> None:
+def process(*, schemas: _oa_types.Schemas) -> None:
     """
     Validate schemas.
 
@@ -43,21 +56,9 @@ def process(*, schemas: _types.Schemas) -> None:
         schemas: The schemas to validate.
 
     """
-    if not isinstance(schemas, dict):
-        raise _exceptions.MalformedSchemaError("Schemas must be a dictionary.")
-
-    # Check keys are strings
-    any_key_not_string = any(
-        filter(lambda key: not isinstance(key, str), schemas.keys())
-    )
-    if any_key_not_string:
-        raise _exceptions.MalformedSchemaError("Schemas must have key strings.")
-    # Check values are dictionaries
-    any_values_not_string = any(
-        filter(lambda value: not isinstance(value, dict), schemas.values())
-    )
-    if any_values_not_string:
-        raise _exceptions.MalformedSchemaError("Schemas must have dictionary values.")
+    schemas_result = _check_schemas(schemas=schemas)
+    if not schemas_result.valid:
+        raise _exceptions.MalformedSchemaError(schemas_result.reason)
 
     # Check constructable schemas model
     constructables = _helpers.iterate.constructable(schemas=schemas)
@@ -75,4 +76,153 @@ def process(*, schemas: _types.Schemas) -> None:
     constructables = _helpers.iterate.constructable(schemas=schemas)
     for constructable in constructables:
         name, schema = constructable
-        process_model(schemas, name, schema)
+        _process_model(schemas, name, schema)
+
+
+def _check_schemas(*, schemas: _oa_types.Schemas) -> types.Result:
+    """
+    Validate the schemas.
+
+    Args:
+        schemas: The schemas to validate.
+
+    Returns:
+        Whether the schemas are valid.
+
+    """
+    if not isinstance(schemas, dict):
+        return types.Result(False, "schemas must be a dictionary")
+
+    # Check keys are strings
+    first_key_not_string = next(
+        filter(lambda key: not isinstance(key, str), schemas.keys()), None,
+    )
+    if first_key_not_string is not None:
+        return types.Result(
+            False, f"schemas keys must be strings, {first_key_not_string} is not"
+        )
+
+    # Check values are dictionaries
+    first_item_not_dict_value = next(
+        filter(lambda args: not isinstance(args[1], dict), schemas.items()), None
+    )
+    if first_item_not_dict_value is not None:
+        key, _ = first_item_not_dict_value
+        return types.Result(False, f"the value of {key} must be a dictionary")
+
+    # Check there is at least 1 constructable schema
+    constructables = _helpers.iterate.constructable(schemas=schemas)
+    if not any(constructables):
+        return types.Result(
+            False,
+            "specification must define at least 1 schema with the x-tablename key",
+        )
+
+    return types.Result(True, None)
+
+
+def _check_model_properties(
+    *, schemas: _oa_types.Schemas, schema: _oa_types.Schema
+) -> types.TProperties:
+    """
+    Check the properties of a model.
+
+    Args:
+        schemas: All defined schemas used to resolve any $ref.
+        schema_name: The name of the schema to validate.
+        schema: The schema to validate.
+
+    Returns:
+        Whether the properties are valid.
+
+    """
+    properties_results = _get_properties_results(schemas, schema)
+    properties_t_results: typing.Iterable[typing.Tuple[str, types.TProperty]] = map(
+        lambda args: (args[0], {"result": types.t_result_from_result(args[1])}),
+        properties_results,
+    )
+    return dict(properties_t_results)
+
+
+def _check_model(schemas: _oa_types.Schemas, schema: _oa_types.Schema) -> types.TModel:
+    """
+    Check a model.
+
+    Args:
+        schema: The schema of the model to check.
+
+    Returns:
+        Whether the model and its properties are valid with a reason if it is not.
+
+    """
+    model_result = model.check(schemas, schema)
+    if not model_result.valid:
+        return {"result": types.t_result_from_result(model_result)}
+
+    return {
+        "result": {"valid": True},
+        "properties": _check_model_properties(schemas=schemas, schema=schema),
+    }
+
+
+def check_models(*, schemas: _oa_types.Schemas) -> types.TModels:
+    """
+    Check the models of a schema.
+
+    Assume the schemas is valid although any of its models may not.
+
+    Args:
+        schemas: The schemas to check.
+
+    Returns:
+        The result for each model.
+
+    """
+    constructables = _helpers.iterate.constructable(schemas=schemas)
+    constructables_result = map(
+        lambda args: (args[0], _check_model(schemas, args[1])), constructables
+    )
+    return dict(constructables_result)
+
+
+def check(*, spec: typing.Any) -> types.TSpec:
+    """
+    Check a specification.
+
+    Args:
+        spec: The specification to check.
+
+    Returns:
+        Whether the specification is valid with a reason if it is not.
+
+    """
+    if not isinstance(spec, dict):
+        return {
+            "result": {"valid": False, "reason": "specification must be a dictionary"}
+        }
+
+    # Check components
+    components = spec.get("components")
+    if components is None:
+        return {
+            "result": {"valid": False, "reason": "specification must define components"}
+        }
+    if not isinstance(components, dict):
+        return {
+            "result": {
+                "valid": False,
+                "reason": "components value must be a dictionary",
+            }
+        }
+
+    # Check schemas
+    schemas = components.get("schemas")
+    if schemas is None:
+        return {
+            "result": {"valid": False, "reason": "specification must define schemas"}
+        }
+    schemas_result = _check_schemas(schemas=schemas)
+    if not schemas_result.valid:
+        return {"result": types.t_result_from_result(schemas_result)}
+
+    return {"result": {"valid": True}, "models": check_models(schemas=schemas)}
diff --git a/open_alchemy/schemas/validation/types.py b/open_alchemy/schemas/validation/types.py
index 12a689b6..c734ad8a 100644
--- a/open_alchemy/schemas/validation/types.py
+++ b/open_alchemy/schemas/validation/types.py
@@ -2,6 +2,8 @@
 
 import typing
 
+from ... import types
+
 
 class Result(typing.NamedTuple):
     """Result of checking a schema."""
@@ -13,3 +15,60 @@ class Result(typing.NamedTuple):
 
 
 OptResult = typing.Optional[Result]
+
+
+class _TResultBase(types.TypedDict, total=False):
+    """TResult base class for keys that might not be present."""
+
+    reason: str
+
+
+class TResult(_TResultBase, total=True):
+    """Record the result of validation."""
+
+    valid: bool
+
+
+def t_result_from_result(result: Result) -> TResult:
+    """Construct a result dictionary from a result."""
+    return_dict: "TResult" = {"valid": result.valid}
+    if result.reason is not None:
+        return_dict["reason"] = result.reason
+
+    return return_dict
+
+
+class TProperty(types.TypedDict, total=True):
+    """Record validation result of a property."""
+
+    result: TResult
+
+
+TProperties = typing.Dict[str, TProperty]
+
+
+class _TModelBase(types.TypedDict, total=False):
+    """TModel base class for keys that might not be present."""
+
+    properties: TProperties
+
+
+class TModel(_TModelBase, total=True):
+    """Record validation result of a model."""
+
+    result: TResult
+
+
+TModels = typing.Dict[str, TModel]
+
+
+class _TSpecBase(types.TypedDict, total=False):
+    """TSpec base class for keys that might not be presents."""
+
+    models: TModels
+
+
+class TSpec(_TSpecBase, total=True):
+    """Record validation result for a specification."""
+
+    result: TResult


Test patch:
diff --git a/tests/open_alchemy/helpers/test_schema.py b/tests/open_alchemy/helpers/test_schema.py
index c95fb2a4..2bcc59de 100644
--- a/tests/open_alchemy/helpers/test_schema.py
+++ b/tests/open_alchemy/helpers/test_schema.py
@@ -8,6 +8,7 @@ from open_alchemy import helpers
 @pytest.mark.parametrize(
     "schema, schemas, expected_result",
     [
+        pytest.param(True, {}, False, id="not dict",),
         pytest.param({}, {}, False, id="empty",),
         pytest.param({"x-tablename": "table 1"}, {}, True, id="x-tablename",),
         pytest.param({"x-inherits": "Schema1"}, {}, True, id="x-inherits string",),
@@ -38,6 +39,22 @@ from open_alchemy import helpers
             True,
             id="allOf x-tablename with additional",
         ),
+        pytest.param(
+            {
+                "allOf": [
+                    {
+                        "properties": {
+                            "column": {"type": "integer", "x-primary-key": True}
+                        },
+                        "x-tablename": "table",
+                        "type": "object",
+                    }
+                ]
+            },
+            {},
+            True,
+            id="allOf properties and tablename split",
+        ),
     ],
 )
 @pytest.mark.helper
diff --git a/tests/open_alchemy/integration/test_init.py b/tests/open_alchemy/integration/test_init.py
index f85af45a..374bb91c 100644
--- a/tests/open_alchemy/integration/test_init.py
+++ b/tests/open_alchemy/integration/test_init.py
@@ -118,7 +118,18 @@ def test_cache_diff(mocked_model_factory: mock.MagicMock):
         called.
     """
     model_factory = open_alchemy.init_model_factory(
-        base=mock.MagicMock, spec={"components": {"schemas": {}}}
+        base=mock.MagicMock,
+        spec={
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "x-tablename": "schema_1",
+                        "type": "object",
+                        "properties": {"id": {"type": "integer"}},
+                    }
+                }
+            }
+        },
     )
 
     model_factory(name="table 1")
@@ -135,7 +146,18 @@ def test_cache_same(mocked_model_factory: mock.MagicMock):
     THEN mocked model_factory is called once.
     """
     model_factory = open_alchemy.init_model_factory(
-        base=mock.MagicMock, spec={"components": {"schemas": {}}}
+        base=mock.MagicMock,
+        spec={
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "x-tablename": "schema_1",
+                        "type": "object",
+                        "properties": {"id": {"type": "integer"}},
+                    }
+                }
+            }
+        },
     )
 
     model_factory(name="table 1")
diff --git a/tests/open_alchemy/schemas/helpers/test_properties.py b/tests/open_alchemy/schemas/helpers/test_properties.py
index 8b6785d1..11a27d27 100644
--- a/tests/open_alchemy/schemas/helpers/test_properties.py
+++ b/tests/open_alchemy/schemas/helpers/test_properties.py
@@ -112,7 +112,6 @@ CHECK_PROPS_ITEMS_TEST = [
 
 @pytest.mark.parametrize("schema, schemas, expected_result", CHECK_PROPS_ITEMS_TEST)
 @pytest.mark.schemas
-@pytest.mark.only_this
 def test_check_properties_items(schema, schemas, expected_result):
     """
     GIVEN schema, schemas and expected result
diff --git a/tests/open_alchemy/schemas/validation/test_validation.py b/tests/open_alchemy/schemas/validation/test_validation.py
index 9285fc06..ac4ea013 100644
--- a/tests/open_alchemy/schemas/validation/test_validation.py
+++ b/tests/open_alchemy/schemas/validation/test_validation.py
@@ -7,10 +7,10 @@ from open_alchemy.schemas import validation
 
 PROCESS_TESTS = [
     pytest.param(True, True, id="not dictionary"),
-    pytest.param({}, False, id="empty"),
+    pytest.param({}, True, id="empty"),
     pytest.param({True: {}}, True, id="key not string"),
     pytest.param({"Schema1": True}, True, id="value not dict"),
-    pytest.param({"Schema1": {}}, False, id="model not constructable"),
+    pytest.param({"Schema1": {}}, True, id="model not constructable"),
     pytest.param(
         {True: {}, "Schema2": {}}, True, id="multiple model first key not string"
     ),
@@ -26,7 +26,7 @@ PROCESS_TESTS = [
         id="multiple model second value not dict",
     ),
     pytest.param(
-        {"Schema1": {}, "Schema2": {}}, False, id="multiple model not constructable"
+        {"Schema1": {}, "Schema2": {}}, True, id="multiple model not constructable"
     ),
     pytest.param({"Schema1": {"x-tablename": "schema_1"}}, True, id="model not valid"),
     pytest.param(
@@ -135,6 +135,23 @@ PROCESS_TESTS = [
         False,
         id="multiple model valid",
     ),
+    pytest.param(
+        {
+            "Schema1": {
+                "allOf": [
+                    {
+                        "properties": {
+                            "column": {"type": "integer", "x-primary-key": True}
+                        },
+                        "x-tablename": "table",
+                        "type": "object",
+                    }
+                ]
+            },
+        },
+        False,
+        id="model allOf",
+    ),
 ]
 
 
@@ -151,3 +168,447 @@ def test_process(schemas, raises):
             validation.process(schemas=schemas)
     else:
         validation.process(schemas=schemas)
+
+
+CHECK_TESTS = [
+    pytest.param(
+        True,
+        {"result": {"valid": False, "reason": "specification must be a dictionary"}},
+        id="spec not dict",
+    ),
+    pytest.param(
+        {},
+        {"result": {"valid": False, "reason": "specification must define components"}},
+        id="no components key",
+    ),
+    pytest.param(
+        {"components": True},
+        {"result": {"valid": False, "reason": "components value must be a dictionary"}},
+        id="components value not dict",
+    ),
+    pytest.param(
+        {"components": {}},
+        {"result": {"valid": False, "reason": "specification must define schemas"}},
+        id="no schemas",
+    ),
+    pytest.param(
+        {"components": {"schemas": True}},
+        {"result": {"valid": False, "reason": "schemas must be a dictionary"}},
+        id="schemas not dict",
+    ),
+    pytest.param(
+        {"components": {"schemas": {}}},
+        {
+            "result": {
+                "valid": False,
+                "reason": "specification must define at least 1 schema with the "
+                "x-tablename key",
+            }
+        },
+        id="schemas empty",
+    ),
+    pytest.param(
+        {"components": {"schemas": {True: {}}}},
+        {
+            "result": {
+                "valid": False,
+                "reason": "schemas keys must be strings, True is not",
+            }
+        },
+        id="schemas key not string",
+    ),
+    pytest.param(
+        {"components": {"schemas": {"Schema1": True}}},
+        {
+            "result": {
+                "valid": False,
+                "reason": "the value of Schema1 must be a dictionary",
+            }
+        },
+        id="schemas values not dict",
+    ),
+    pytest.param(
+        {"components": {"schemas": {"Schema1": {}}}},
+        {
+            "result": {
+                "valid": False,
+                "reason": "specification must define at least 1 schema with the "
+                "x-tablename key",
+            }
+        },
+        id="single model model not constructable",
+    ),
+    pytest.param(
+        {"components": {"schemas": {"Schema1": {"x-tablename": "schema_1"}}}},
+        {
+            "result": {"valid": True},
+            "models": {
+                "Schema1": {
+                    "result": {
+                        "valid": False,
+                        "reason": "malformed schema :: Every property requires a "
+                        "type. ",
+                    }
+                }
+            },
+        },
+        id="single model model not valid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {"prop_1": {}},
+                    }
+                }
+            }
+        },
+        {
+            "result": {"valid": True},
+            "models": {
+                "Schema1": {
+                    "result": {"valid": True},
+                    "properties": {
+                        "prop_1": {
+                            "result": {
+                                "valid": False,
+                                "reason": "malformed schema :: Every property requires "
+                                "a type. ",
+                            }
+                        }
+                    },
+                }
+            },
+        },
+        id="single model properties not valid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    }
+                }
+            }
+        },
+        {
+            "result": {"valid": True},
+            "models": {
+                "Schema1": {
+                    "result": {"valid": True},
+                    "properties": {"prop_1": {"result": {"valid": True}}},
+                }
+            },
+        },
+        id="single model valid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {"prop_1": {}, "prop_2": {"type": "integer"}},
+                    }
+                }
+            }
+        },
+        {
+            "result": {"valid": True},
+            "models": {
+                "Schema1": {
+                    "result": {"valid": True},
+                    "properties": {
+                        "prop_1": {
+                            "result": {
+                                "valid": False,
+                                "reason": "malformed schema :: Every property requires "
+                                "a type. ",
+                            }
+                        },
+                        "prop_2": {"result": {"valid": True}},
+                    },
+                }
+            },
+        },
+        id="single model multiple properties first invalid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {"prop_1": {"type": "integer"}, "prop_2": {}},
+                    }
+                }
+            }
+        },
+        {
+            "result": {"valid": True},
+            "models": {
+                "Schema1": {
+                    "result": {"valid": True},
+                    "properties": {
+                        "prop_1": {"result": {"valid": True}},
+                        "prop_2": {
+                            "result": {
+                                "valid": False,
+                                "reason": "malformed schema :: Every property requires "
+                                "a type. ",
+                            }
+                        },
+                    },
+                }
+            },
+        },
+        id="single model multiple properties second invalid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {
+                            "prop_1": {"type": "integer"},
+                            "prop_2": {"type": "integer"},
+                        },
+                    }
+                }
+            }
+        },
+        {
+            "result": {"valid": True},
+            "models": {
+                "Schema1": {
+                    "result": {"valid": True},
+                    "properties": {
+                        "prop_1": {"result": {"valid": True}},
+                        "prop_2": {"result": {"valid": True}},
+                    },
+                }
+            },
+        },
+        id="single model multiple properties valid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    True: {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                    "Schema2": {
+                        "type": "object",
+                        "x-tablename": "schema_2",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                }
+            }
+        },
+        {
+            "result": {
+                "valid": False,
+                "reason": "schemas keys must be strings, True is not",
+            }
+        },
+        id="multiple model first key invalid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                    True: {
+                        "type": "object",
+                        "x-tablename": "schema_2",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                }
+            }
+        },
+        {
+            "result": {
+                "valid": False,
+                "reason": "schemas keys must be strings, True is not",
+            }
+        },
+        id="multiple model second key invalid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": True,
+                    "Schema2": {
+                        "type": "object",
+                        "x-tablename": "schema_2",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                }
+            }
+        },
+        {
+            "result": {
+                "valid": False,
+                "reason": "the value of Schema1 must be a dictionary",
+            }
+        },
+        id="multiple model first value invalid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                    "Schema2": True,
+                }
+            }
+        },
+        {
+            "result": {
+                "valid": False,
+                "reason": "the value of Schema2 must be a dictionary",
+            }
+        },
+        id="multiple model second value invalid",
+    ),
+    pytest.param(
+        {"components": {"schemas": {"Schema1": {}, "Schema2": {}}}},
+        {
+            "result": {
+                "valid": False,
+                "reason": "specification must define at least 1 schema with the "
+                "x-tablename key",
+            }
+        },
+        id="multiple model not constructable",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {"x-tablename": "schema_1"},
+                    "Schema2": {
+                        "type": "object",
+                        "x-tablename": "schema_2",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                }
+            }
+        },
+        {
+            "result": {"valid": True},
+            "models": {
+                "Schema1": {
+                    "result": {
+                        "valid": False,
+                        "reason": "malformed schema :: Every property requires a "
+                        "type. ",
+                    }
+                },
+                "Schema2": {
+                    "result": {"valid": True},
+                    "properties": {"prop_1": {"result": {"valid": True}}},
+                },
+            },
+        },
+        id="multiple model first model invalid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                    "Schema2": {"x-tablename": "schema_2"},
+                }
+            }
+        },
+        {
+            "result": {"valid": True},
+            "models": {
+                "Schema1": {
+                    "result": {"valid": True},
+                    "properties": {"prop_1": {"result": {"valid": True}}},
+                },
+                "Schema2": {
+                    "result": {
+                        "valid": False,
+                        "reason": "malformed schema :: Every property requires a "
+                        "type. ",
+                    }
+                },
+            },
+        },
+        id="multiple model second model invalid",
+    ),
+    pytest.param(
+        {
+            "components": {
+                "schemas": {
+                    "Schema1": {
+                        "type": "object",
+                        "x-tablename": "schema_1",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                    "Schema2": {
+                        "type": "object",
+                        "x-tablename": "schema_2",
+                        "properties": {"prop_1": {"type": "integer"}},
+                    },
+                }
+            }
+        },
+        {
+            "result": {"valid": True},
+            "models": {
+                "Schema1": {
+                    "result": {"valid": True},
+                    "properties": {"prop_1": {"result": {"valid": True}}},
+                },
+                "Schema2": {
+                    "result": {"valid": True},
+                    "properties": {"prop_1": {"result": {"valid": True}}},
+                },
+            },
+        },
+        id="multiple model valid",
+    ),
+]
+
+
+@pytest.mark.parametrize("spec, expected_result", CHECK_TESTS)
+@pytest.mark.schemas
+def test_check(spec, expected_result):
+    """
+    GIVEN spec and the expected result
+    WHEN check is called with the spec
+    THEN the expected result is returned.
+    """
+    returned_result = validation.check(spec=spec)
+
+    assert returned_result == expected_result