mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-06 04:19:26 +00:00
Generate examples with jsf (#1891)
Fixes https://github.com/spec-first/connexion/issues/1719 . Builds on work of https://github.com/spec-first/connexion/pull/1718, pulling in external library to provide the fake data from JSON schema. Changes proposed in this pull request: * Use JSF library to generate sample data for mocking APIs without examples. * Add this as a new extra dependency "mock" See discussion on https://github.com/spec-first/connexion/pull/1870 --------- Co-authored-by: Robbe Sneyders <robbe.sneyders@gmail.com>
This commit is contained in:
@@ -51,4 +51,9 @@ class MockResolver(Resolver):
|
|||||||
resp, code = operation.example_response()
|
resp, code = operation.example_response()
|
||||||
if resp is not None:
|
if resp is not None:
|
||||||
return resp, code
|
return resp, code
|
||||||
return "No example response was defined.", code
|
return (
|
||||||
|
"No example response defined in the API, and response "
|
||||||
|
"auto-generation disabled. To enable response auto-generation, "
|
||||||
|
"install connexion using the mock extra (connexion[mock])",
|
||||||
|
501,
|
||||||
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import logging
|
|||||||
from connexion.datastructures import MediaTypeDict
|
from connexion.datastructures import MediaTypeDict
|
||||||
from connexion.operations.abstract import AbstractOperation
|
from connexion.operations.abstract import AbstractOperation
|
||||||
from connexion.uri_parsing import OpenAPIURIParser
|
from connexion.uri_parsing import OpenAPIURIParser
|
||||||
from connexion.utils import deep_get
|
from connexion.utils import build_example_from_schema, deep_get
|
||||||
|
|
||||||
logger = logging.getLogger("connexion.operations.openapi3")
|
logger = logging.getLogger("connexion.operations.openapi3")
|
||||||
|
|
||||||
@@ -187,31 +187,11 @@ class OpenAPIOperation(AbstractOperation):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return (
|
schema = deep_get(self._responses, schema_path)
|
||||||
self._nested_example(deep_get(self._responses, schema_path)),
|
|
||||||
status_code,
|
|
||||||
)
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return (None, status_code)
|
return ("No example response or response schema defined.", status_code)
|
||||||
|
|
||||||
def _nested_example(self, schema):
|
return (build_example_from_schema(schema), status_code)
|
||||||
try:
|
|
||||||
return schema["example"]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
# Recurse if schema is an object
|
|
||||||
return {
|
|
||||||
key: self._nested_example(value)
|
|
||||||
for (key, value) in schema["properties"].items()
|
|
||||||
}
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
# Recurse if schema is an array
|
|
||||||
return [self._nested_example(schema["items"])]
|
|
||||||
except KeyError:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def get_path_parameter_types(self):
|
def get_path_parameter_types(self):
|
||||||
types = {}
|
types = {}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import typing as t
|
|||||||
from connexion.exceptions import InvalidSpecification
|
from connexion.exceptions import InvalidSpecification
|
||||||
from connexion.operations.abstract import AbstractOperation
|
from connexion.operations.abstract import AbstractOperation
|
||||||
from connexion.uri_parsing import Swagger2URIParser
|
from connexion.uri_parsing import Swagger2URIParser
|
||||||
from connexion.utils import deep_get
|
from connexion.utils import build_example_from_schema, deep_get
|
||||||
|
|
||||||
logger = logging.getLogger("connexion.operations.swagger2")
|
logger = logging.getLogger("connexion.operations.swagger2")
|
||||||
|
|
||||||
@@ -209,31 +209,11 @@ class Swagger2Operation(AbstractOperation):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return (
|
schema = deep_get(self._responses, schema_path)
|
||||||
self._nested_example(deep_get(self._responses, schema_path)),
|
|
||||||
status_code,
|
|
||||||
)
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return (None, status_code)
|
return ("No example response or response schema defined.", status_code)
|
||||||
|
|
||||||
def _nested_example(self, schema):
|
return (build_example_from_schema(schema), status_code)
|
||||||
try:
|
|
||||||
return schema["example"]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
# Recurse if schema is an object
|
|
||||||
return {
|
|
||||||
key: self._nested_example(value)
|
|
||||||
for (key, value) in schema["properties"].items()
|
|
||||||
}
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
# Recurse if schema is an array
|
|
||||||
return [self._nested_example(schema["items"])]
|
|
||||||
except KeyError:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def body_name(self, content_type: str = None) -> str:
|
def body_name(self, content_type: str = None) -> str:
|
||||||
return self.body_definition(content_type).get("name", "body")
|
return self.body_definition(content_type).get("name", "body")
|
||||||
|
|||||||
@@ -512,3 +512,35 @@ def sort_apis_by_basepath(apis: t.List["API"]) -> t.List["API"]:
|
|||||||
:return: List of APIs sorted by basepath
|
:return: List of APIs sorted by basepath
|
||||||
"""
|
"""
|
||||||
return sort_routes(apis, key=lambda api: api.base_path or "/")
|
return sort_routes(apis, key=lambda api: api.base_path or "/")
|
||||||
|
|
||||||
|
|
||||||
|
def build_example_from_schema(schema):
|
||||||
|
if "example" in schema:
|
||||||
|
return schema["example"]
|
||||||
|
|
||||||
|
if "properties" in schema:
|
||||||
|
# Recurse if schema is an object
|
||||||
|
return {
|
||||||
|
key: build_example_from_schema(value)
|
||||||
|
for (key, value) in schema["properties"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
if "items" in schema:
|
||||||
|
# Recurse if schema is an array
|
||||||
|
min_item_count = schema.get("minItems", 0)
|
||||||
|
max_item_count = schema.get("maxItems")
|
||||||
|
|
||||||
|
if max_item_count is None or max_item_count >= min_item_count + 1:
|
||||||
|
item_count = min_item_count + 1
|
||||||
|
else:
|
||||||
|
item_count = min_item_count
|
||||||
|
|
||||||
|
return [build_example_from_schema(schema["items"]) for n in range(item_count)]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from jsf import JSF
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
faker = JSF(schema)
|
||||||
|
return faker.generate()
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ Running a mock server
|
|||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
You can run a simple server which returns example responses on every request.
|
You can run a simple server which returns example responses on every request.
|
||||||
The example responses must be defined in the ``examples`` response property of the OpenAPI specification.
|
|
||||||
|
The example responses can be defined in the ``examples`` response property of
|
||||||
|
the OpenAPI specification. If no examples are specified, and you have installed connexion with the `mock` extra (`pip install connexion[mock]`), an example is generated based on the provided schema.
|
||||||
|
|
||||||
Your API specification file is not required to have any ``operationId``.
|
Your API specification file is not required to have any ``operationId``.
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|||||||
@@ -48,24 +48,26 @@ python = '^3.8'
|
|||||||
asgiref = ">= 3.4"
|
asgiref = ">= 3.4"
|
||||||
httpx = ">= 0.23"
|
httpx = ">= 0.23"
|
||||||
inflection = ">= 0.3.1"
|
inflection = ">= 0.3.1"
|
||||||
jsonschema = ">= 4.0.1"
|
jsonschema = ">=4.17.3"
|
||||||
Jinja2 = ">= 3.0.0"
|
Jinja2 = ">= 3.0.0"
|
||||||
python-multipart = ">= 0.0.5"
|
python-multipart = ">= 0.0.5"
|
||||||
PyYAML = ">= 5.1"
|
PyYAML = ">= 5.1"
|
||||||
requests = ">= 2.27"
|
requests = ">= 2.27"
|
||||||
starlette = ">= 0.35"
|
starlette = ">= 0.35"
|
||||||
typing-extensions = ">= 4"
|
typing-extensions = ">= 4.6.1"
|
||||||
werkzeug = ">= 2.2.1"
|
werkzeug = ">= 2.2.1"
|
||||||
|
|
||||||
a2wsgi = { version = ">= 1.7", optional = true }
|
a2wsgi = { version = ">= 1.7", optional = true }
|
||||||
flask = { version = ">= 2.2", extras = ["async"], optional = true }
|
flask = { version = ">= 2.2", extras = ["async"], optional = true }
|
||||||
swagger-ui-bundle = { version = ">= 1.1.0", optional = true }
|
swagger-ui-bundle = { version = ">= 1.1.0", optional = true }
|
||||||
uvicorn = { version = ">= 0.17.6", extras = ["standard"], optional = true }
|
uvicorn = { version = ">= 0.17.6", extras = ["standard"], optional = true }
|
||||||
|
jsf = { version = ">=0.10.0", optional = true }
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
flask = ["a2wsgi", "flask"]
|
flask = ["a2wsgi", "flask"]
|
||||||
swagger-ui = ["swagger-ui-bundle"]
|
swagger-ui = ["swagger-ui-bundle"]
|
||||||
uvicorn = ["uvicorn"]
|
uvicorn = ["uvicorn"]
|
||||||
|
mock = ["jsf"]
|
||||||
|
|
||||||
[tool.poetry.group.tests.dependencies]
|
[tool.poetry.group.tests.dependencies]
|
||||||
pre-commit = "~2.21.0"
|
pre-commit = "~2.21.0"
|
||||||
@@ -106,4 +108,4 @@ exclude_lines = [
|
|||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "referencing.jsonschema.*"
|
module = "referencing.jsonschema.*"
|
||||||
follow_imports = "skip"
|
follow_imports = "skip"
|
||||||
|
|||||||
@@ -224,7 +224,8 @@ def test_mock_resolver_no_example_nested_in_object():
|
|||||||
|
|
||||||
response, status_code = resolver.mock_operation(operation)
|
response, status_code = resolver.mock_operation(operation)
|
||||||
assert status_code == 200
|
assert status_code == 200
|
||||||
assert response == "No example response was defined."
|
assert isinstance(response, dict)
|
||||||
|
assert isinstance(response["foo"], str)
|
||||||
|
|
||||||
|
|
||||||
def test_mock_resolver_no_example_nested_in_list_openapi():
|
def test_mock_resolver_no_example_nested_in_list_openapi():
|
||||||
@@ -256,7 +257,8 @@ def test_mock_resolver_no_example_nested_in_list_openapi():
|
|||||||
|
|
||||||
response, status_code = resolver.mock_operation(operation)
|
response, status_code = resolver.mock_operation(operation)
|
||||||
assert status_code == 202
|
assert status_code == 202
|
||||||
assert response == "No example response was defined."
|
assert isinstance(response, list)
|
||||||
|
assert all(isinstance(c, str) for c in response)
|
||||||
|
|
||||||
|
|
||||||
def test_mock_resolver_no_examples():
|
def test_mock_resolver_no_examples():
|
||||||
@@ -278,7 +280,7 @@ def test_mock_resolver_no_examples():
|
|||||||
|
|
||||||
response, status_code = resolver.mock_operation(operation)
|
response, status_code = resolver.mock_operation(operation)
|
||||||
assert status_code == 418
|
assert status_code == 418
|
||||||
assert response == "No example response was defined."
|
assert response == "No example response or response schema defined."
|
||||||
|
|
||||||
|
|
||||||
def test_mock_resolver_notimplemented():
|
def test_mock_resolver_notimplemented():
|
||||||
@@ -315,4 +317,7 @@ def test_mock_resolver_notimplemented():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# check if it is using the mock function
|
# check if it is using the mock function
|
||||||
assert operation._resolution.function() == ("No example response was defined.", 418)
|
assert operation._resolution.function() == (
|
||||||
|
"No example response or response schema defined.",
|
||||||
|
418,
|
||||||
|
)
|
||||||
|
|||||||
162
tests/test_mock2.py
Normal file
162
tests/test_mock2.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from re import fullmatch
|
||||||
|
|
||||||
|
from connexion.utils import build_example_from_schema
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_string():
|
||||||
|
schema = {
|
||||||
|
"type": "string",
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert isinstance(example, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_integer():
|
||||||
|
schema = {
|
||||||
|
"type": "integer",
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert isinstance(example, int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_number():
|
||||||
|
schema = {
|
||||||
|
"type": "number",
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert isinstance(example, float)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_boolean():
|
||||||
|
schema = {
|
||||||
|
"type": "boolean",
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert isinstance(example, bool)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_integer_minimum():
|
||||||
|
schema = {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 4,
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert example >= schema["minimum"] and isinstance(example, int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_integer_maximum():
|
||||||
|
schema = {
|
||||||
|
"type": "integer",
|
||||||
|
"maximum": 17,
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert example <= schema["maximum"] and isinstance(example, int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_integer_exclusive_minimum():
|
||||||
|
schema = {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 4,
|
||||||
|
"exclusiveMinimum": True,
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert example > schema["minimum"] and isinstance(example, int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_integer_exclusive_maximum():
|
||||||
|
schema = {
|
||||||
|
"type": "integer",
|
||||||
|
"maximum": 17,
|
||||||
|
"exclusiveMaximum": True,
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert example < schema["maximum"] and isinstance(example, int)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_string_regular_expression():
|
||||||
|
pattern = r"^\d{3}-\d{2}-\d{4}$"
|
||||||
|
schema = {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": pattern,
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert fullmatch(pattern, example) != None and isinstance(example, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_string_maximum():
|
||||||
|
schema = {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 20,
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert isinstance(example, str) and len(example) <= schema["maxLength"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_string_minimum():
|
||||||
|
schema = {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 20,
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert isinstance(example, str) and len(example) >= schema["minLength"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_schema_enum():
|
||||||
|
schema = {"type": "string", "enum": ["asc", "desc"]}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
assert isinstance(example, str)
|
||||||
|
assert example == "asc" or example == "desc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_example_from_complex_schema():
|
||||||
|
schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datetimeField": {"type": "string", "format": "date-time"},
|
||||||
|
"integerField": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 2,
|
||||||
|
"maximum": 5,
|
||||||
|
"exclusiveMinimum": True,
|
||||||
|
"multipleOf": 2,
|
||||||
|
},
|
||||||
|
"arrayOfNumbersField": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"maximum": 0.9,
|
||||||
|
"multipleOf": 0.1,
|
||||||
|
},
|
||||||
|
"minItems": 3,
|
||||||
|
"maxItems": 5,
|
||||||
|
},
|
||||||
|
"objectField": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"nestedBoolean": {"type": "boolean"},
|
||||||
|
"stringWithExample": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "example-string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
example = build_example_from_schema(schema)
|
||||||
|
|
||||||
|
# Check that ValueError is not raised on invalid datetime.
|
||||||
|
datetime.fromisoformat(example["datetimeField"])
|
||||||
|
assert example["integerField"] == 4
|
||||||
|
|
||||||
|
assert isinstance(example["arrayOfNumbersField"], list)
|
||||||
|
assert 3 <= len(example["arrayOfNumbersField"]) <= 5
|
||||||
|
assert all(0.1 <= num <= 0.9 for num in example["arrayOfNumbersField"])
|
||||||
|
|
||||||
|
example_boolean = example["objectField"]["nestedBoolean"]
|
||||||
|
assert example_boolean is True or example_boolean is False
|
||||||
|
|
||||||
|
# Check that if an example is provided then it is used directly.
|
||||||
|
assert example["objectField"]["stringWithExample"] == "example-string"
|
||||||
@@ -103,7 +103,7 @@ def test_mock_resolver_no_examples():
|
|||||||
|
|
||||||
response, status_code = resolver.mock_operation(operation)
|
response, status_code = resolver.mock_operation(operation)
|
||||||
assert status_code == 418
|
assert status_code == 418
|
||||||
assert response == "No example response was defined."
|
assert response == "No example response or response schema defined."
|
||||||
|
|
||||||
|
|
||||||
def test_mock_resolver_notimplemented():
|
def test_mock_resolver_notimplemented():
|
||||||
@@ -133,4 +133,7 @@ def test_mock_resolver_notimplemented():
|
|||||||
resolver=resolver,
|
resolver=resolver,
|
||||||
)
|
)
|
||||||
# check if it is using the mock function
|
# check if it is using the mock function
|
||||||
assert operation._resolution.function() == ("No example response was defined.", 418)
|
assert operation._resolution.function() == (
|
||||||
|
"No example response or response schema defined.",
|
||||||
|
418,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user