Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate examples with jsf #1891

Merged
merged 9 commits into from
Mar 20, 2024
7 changes: 6 additions & 1 deletion connexion/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ def mock_operation(self, operation, *args, **kwargs):
resp, code = operation.example_response()
if resp is not None:
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,
)
28 changes: 4 additions & 24 deletions connexion/operations/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from connexion.datastructures import MediaTypeDict
from connexion.operations.abstract import AbstractOperation
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")

Expand Down Expand Up @@ -187,31 +187,11 @@ def example_response(self, status_code=None, content_type=None):
pass

try:
return (
self._nested_example(deep_get(self._responses, schema_path)),
status_code,
)
schema = deep_get(self._responses, schema_path)
except KeyError:
return (None, status_code)
return ("No example response or response schema defined.", status_code)

def _nested_example(self, schema):
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
return (build_example_from_schema(schema), status_code)

def get_path_parameter_types(self):
types = {}
Expand Down
28 changes: 4 additions & 24 deletions connexion/operations/swagger2.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from connexion.exceptions import InvalidSpecification
from connexion.operations.abstract import AbstractOperation
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")

Expand Down Expand Up @@ -209,31 +209,11 @@ def example_response(self, status_code=None, *args, **kwargs):
pass

try:
return (
self._nested_example(deep_get(self._responses, schema_path)),
status_code,
)
schema = deep_get(self._responses, schema_path)
except KeyError:
return (None, status_code)
return ("No example response or response schema defined.", status_code)

def _nested_example(self, schema):
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
return (build_example_from_schema(schema), status_code)

def body_name(self, content_type: str = None) -> str:
return self.body_definition(content_type).get("name", "body")
Expand Down
32 changes: 32 additions & 0 deletions connexion/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,3 +512,35 @@ def sort_apis_by_basepath(apis: t.List["API"]) -> t.List["API"]:
:return: List of APIs sorted by basepath
"""
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()
5 changes: 4 additions & 1 deletion docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ Running a mock server
---------------------

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``.

.. code-block:: bash
Expand Down
8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,26 @@ python = '^3.8'
asgiref = ">= 3.4"
httpx = ">= 0.23"
inflection = ">= 0.3.1"
jsonschema = ">= 4.0.1"
jsonschema = ">=4.17.3"
Jinja2 = ">= 3.0.0"
python-multipart = ">= 0.0.5"
PyYAML = ">= 5.1"
requests = ">= 2.27"
starlette = ">= 0.35"
typing-extensions = ">= 4"
typing-extensions = ">= 4.6.1"
werkzeug = ">= 2.2.1"

a2wsgi = { version = ">= 1.7", optional = true }
flask = { version = ">= 2.2", extras = ["async"], optional = true }
swagger-ui-bundle = { version = ">= 1.1.0", optional = true }
uvicorn = { version = ">= 0.17.6", extras = ["standard"], optional = true }
jsf = { version = ">=0.10.0", optional = true }

[tool.poetry.extras]
flask = ["a2wsgi", "flask"]
swagger-ui = ["swagger-ui-bundle"]
uvicorn = ["uvicorn"]
mock = ["jsf"]

[tool.poetry.group.tests.dependencies]
pre-commit = "~2.21.0"
Expand Down Expand Up @@ -106,4 +108,4 @@ exclude_lines = [

[[tool.mypy.overrides]]
module = "referencing.jsonschema.*"
follow_imports = "skip"
follow_imports = "skip"
13 changes: 9 additions & 4 deletions tests/test_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ def test_mock_resolver_no_example_nested_in_object():

response, status_code = resolver.mock_operation(operation)
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():
Expand Down Expand Up @@ -256,7 +257,8 @@ def test_mock_resolver_no_example_nested_in_list_openapi():

response, status_code = resolver.mock_operation(operation)
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():
Expand All @@ -278,7 +280,7 @@ def test_mock_resolver_no_examples():

response, status_code = resolver.mock_operation(operation)
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():
Expand Down Expand Up @@ -315,4 +317,7 @@ def test_mock_resolver_notimplemented():
)

# 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 changes: 162 additions & 0 deletions tests/test_mock2.py
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading