Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions openapi_spec_tools/cli/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from openapi_spec_tools.layout.utils import file_to_tree
from openapi_spec_tools.layout.utils import operation_duplicates
from openapi_spec_tools.layout.utils import operation_order
from openapi_spec_tools.layout.utils import subcommand_extra_properties
from openapi_spec_tools.layout.utils import subcommand_missing_properties
from openapi_spec_tools.layout.utils import subcommand_order
from openapi_spec_tools.layout.utils import subcommand_references
Expand All @@ -55,6 +56,7 @@ def layout_check_format(
references: Annotated[bool, typer.Option(help="Check for missing and unused subcommands")] = True,
sub_order: Annotated[bool, typer.Option(help="Check the sub-command order")] = True,
missing_props: Annotated[bool, typer.Option(help="Check for missing properties")] = True,
extra_props: Annotated[bool, typer.Option(help="Check for any extra properties")] = False,
op_dups: Annotated[bool, typer.Option(help="Check for duplicate names in sub-commands")] = True,
op_order: Annotated[bool, typer.Option(help="Check the operations order within each sub-command")] = True,
pagination: Annotated[bool, typer.Option(help="Check the pagination parameters for issues")] = True,
Expand Down Expand Up @@ -90,6 +92,12 @@ def _dict_to_str(errors: dict[str, str], sep=SEP) -> str:
typer.echo(f"Sub-commands have missing properties:{_dict_to_str(errors)}")
result = 1

if extra_props:
errors = subcommand_extra_properties(data)
if errors:
typer.echo(f"Commands have extra properties:{_dict_to_str(errors)}")
result = 1

if op_dups:
errors = operation_duplicates(data)
if errors:
Expand Down
25 changes: 25 additions & 0 deletions openapi_spec_tools/layout/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,31 @@ def subcommand_missing_properties(data: dict[str, Any]) -> dict[str, str]:
return errors


def subcommand_extra_properties(data: dict[str, Any]) -> dict[str, str]:
"""Look for missing properties in the sub-commands."""
errors = {}
commands = CommandField.values()
operations = OperationField.values()
for sub_name, _sub_data in data.items():
sub_data = deepcopy(_sub_data or {})
extra = []

# check top-level fields
extra.extend([k for k in sub_data.keys() if k not in commands])

# check each operations
for index, op_data in enumerate(sub_data.get(CommandField.OPERATIONS, [])):
op_extra = [k for k in op_data.keys() if k not in operations]
if op_extra:
identifier = op_data.get(OperationField.NAME) or f"operation[{index}]"
extra.append(f"{identifier}: {', '.join(op_extra)}")

if extra:
errors[sub_name] = ", ".join(extra)

return errors


def operation_duplicates(data: dict[str, Any]) -> dict[str, Any]:
"""Look for command operations with redundant names (within each command)."""
errors = {}
Expand Down
19 changes: 19 additions & 0 deletions tests/assets/layout_bad.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ main:
subcommandId: veterinarians
- name: shows
subcommandId: dog_shows
- name: walkers
subcommandId: walkers

pets:
description: Manage your pets
Expand Down Expand Up @@ -61,3 +63,20 @@ veterinarians:
operations:
- name: add
- name: delete

walkers:
description: Manage dog walkers
foo: bar
operations:
- name: list
operationId: op_1
sna: foo
bugIds: abc
- name: show
another:
this: 1
that: one
thing:
- 1
- 2
operationId: op_2
36 changes: 30 additions & 6 deletions tests/cli/test_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,22 @@
Sub-commands are misordered:
owners < pets_health
"""
ERR_OPS_PROPS = """\
ERR_MISSING_PROPS = """\
Sub-commands have missing properties:
owners: description, operations
veterinarians: add operationId, subcommandId, or reference, delete operationId, subcommandId, or reference
"""
ERR_EXTRA_PROPS ="""\
Commands have extra properties:
walkers: foo, list: sna, show: another, thing
"""
ERR_OPS_DUPES = """\
Duplicate operations in sub-commands:
shelters: list at 0, 2
"""
ERR_OPS_ORDER = """\
Sub-command operation orders should be:
main: owners, pet, shows, vets
main: owners, pet, shows, vets, walkers
pets: create, delete, examine, health, update
shelters: list, list, rescue
"""
Expand Down Expand Up @@ -81,7 +85,22 @@ def args_disabled(updates: dict[str, Any]) -> dict[str, Any]:
ERR_SUB_MISSIING,
ERR_SUB_UNUSED,
ERR_SUB_ORDER,
ERR_OPS_PROPS,
ERR_MISSING_PROPS,
ERR_OPS_DUPES,
ERR_OPS_ORDER,
ERR_PAGINATION,
ERR_HARDCODED,
]),
id="default"
),
pytest.param(
{"filename":BAD_LAYOUT_FILE, "extra_props": True},
"".join([
ERR_SUB_MISSIING,
ERR_SUB_UNUSED,
ERR_SUB_ORDER,
ERR_MISSING_PROPS,
ERR_EXTRA_PROPS,
ERR_OPS_DUPES,
ERR_OPS_ORDER,
ERR_PAGINATION,
Expand All @@ -101,8 +120,13 @@ def args_disabled(updates: dict[str, Any]) -> dict[str, Any]:
),
pytest.param(
args_disabled({"missing_props": True}),
ERR_OPS_PROPS,
id="ops-props",
ERR_MISSING_PROPS,
id="ops-missing",
),
pytest.param(
args_disabled({"extra_props": True}),
ERR_EXTRA_PROPS,
id="ops-extra",
),
pytest.param(
args_disabled({"op_dups": True}),
Expand Down Expand Up @@ -142,7 +166,7 @@ def test_layout_check_format_success() -> None:
mock.patch('sys.stdout', new_callable=StringIo) as mock_stdout,
):
filename = asset_filename("layout_pets.yaml")
layout_check_format(filename=filename)
layout_check_format(filename=filename, extra_props=True)
output = mock_stdout.getvalue()
assert f"No errors found in {filename}\n" == output

Expand Down
31 changes: 31 additions & 0 deletions tests/layout/test_layout_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from openapi_spec_tools.layout.utils import parse_pagination
from openapi_spec_tools.layout.utils import parse_to_tree
from openapi_spec_tools.layout.utils import path_to_parts
from openapi_spec_tools.layout.utils import subcommand_extra_properties
from openapi_spec_tools.layout.utils import subcommand_missing_properties
from openapi_spec_tools.layout.utils import subcommand_order
from openapi_spec_tools.layout.utils import subcommand_references
Expand Down Expand Up @@ -78,6 +79,36 @@ def test_open_layout() -> None:
def test_missing_properties(data, expected) -> None:
assert expected == subcommand_missing_properties(data)

@pytest.mark.parametrize(
["data", "expected"],
[
pytest.param({}, {}, id="empty"),
pytest.param({"cmd": {DESC: "a"}}, {}, id="none"),
pytest.param({"cmd": {DESC: "a", "other": 1}}, {"cmd": "other"}, id="cmd-single"),
pytest.param(
{"cmd1": {DESC: "a", "other": 1}, "cmd2": {"more": "props"}},
{"cmd1": "other", "cmd2": "more"},
id="cmd-double",
),
pytest.param(
{"cmd": {OPS: [{NAME: "me", "other": 1}]}},
{"cmd": "me: other"},
id="op-single",
),
pytest.param(
{"cmd": {OPS: [{NAME: "me", "myself": 1, "eye": "blue"}]}},
{"cmd": "me: myself, eye"},
id="op-multi",
),
pytest.param(
{"sna": {OPS: [{"foo": "bar"}, {NAME: "a", "extra": "prop"}]}, "foo": {"this": "that"}},
{"sna": "operation[0]: foo, a: extra", "foo": "this"},
id="complex",
),
],
)
def test_extra_properties(data, expected) -> None:
assert expected == subcommand_extra_properties(data)

@pytest.mark.parametrize(
["data", "expected"],
Expand Down
Loading