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

Support oneOfs & anyOfs in operation request & response schema #701

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
13 changes: 12 additions & 1 deletion linodecli/baked/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
from openapi3.paths import Operation, Parameter

from linodecli.baked.parsing import simplify_description
from linodecli.baked.request import OpenAPIFilteringRequest, OpenAPIRequest
from linodecli.baked.request import (
OpenAPIFilteringRequest,
OpenAPIRequest,
OpenAPIRequestArg,
)
from linodecli.baked.response import OpenAPIResponse
from linodecli.exit_codes import ExitCodes
from linodecli.output.output_handler import OutputHandler
Expand Down Expand Up @@ -415,6 +419,13 @@ def args(self):
"""
return self.request.attrs if self.request else []

@property
def arg_routes(self) -> Dict[str, List[OpenAPIRequestArg]]:
"""
Return a list of attributes from the request schema
"""
return self.request.attr_routes if self.request else []

@staticmethod
def _flatten_url_path(tag: str) -> str:
"""
Expand Down
139 changes: 82 additions & 57 deletions linodecli/baked/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
Request details for a CLI Operation
"""

from openapi3.schemas import Schema

from linodecli.baked.parsing import simplify_description
from linodecli.baked.util import _aggregate_schema_properties


class OpenAPIRequestArg:
Expand Down Expand Up @@ -134,66 +137,70 @@ def _parse_request_model(schema, prefix=None, parent=None, depth=0):
"""
args = []

if schema.properties is not None:
for k, v in schema.properties.items():
if v.type == "object" and not v.readOnly and v.properties:
# nested objects receive a prefix and are otherwise parsed normally
pref = prefix + "." + k if prefix else k
properties = _aggregate_schema_properties(schema)

args += _parse_request_model(
v,
prefix=pref,
if properties is None:
return args

for k, v in properties.items():
if v.type == "object" and not v.readOnly and v.properties:
# nested objects receive a prefix and are otherwise parsed normally
pref = prefix + "." + k if prefix else k

args += _parse_request_model(
v,
prefix=pref,
parent=parent,
# NOTE: We do not increment the depth because dicts do not have
# parent arguments.
depth=depth,
)
elif (
v.type == "array"
and v.items
and v.items.type == "object"
and v.extensions.get("linode-cli-format") != "json"
):
# handle lists of objects as a special case, where each property
# of the object in the list is its own argument
pref = prefix + "." + k if prefix else k

# Support specifying this list as JSON
args.append(
OpenAPIRequestArg(
k,
v.items,
False,
prefix=prefix,
is_parent=True,
parent=parent,
# NOTE: We do not increment the depth because dicts do not have
# parent arguments.
depth=depth,
)
elif (
v.type == "array"
and v.items
and v.items.type == "object"
and v.extensions.get("linode-cli-format") != "json"
):
# handle lists of objects as a special case, where each property
# of the object in the list is its own argument
pref = prefix + "." + k if prefix else k

# Support specifying this list as JSON
args.append(
OpenAPIRequestArg(
k,
v.items,
False,
prefix=prefix,
is_parent=True,
parent=parent,
depth=depth,
)
)
)

args += _parse_request_model(
v.items,
prefix=pref,
parent=pref,
depth=depth + 1,
)
else:
# required fields are defined in the schema above the property, so
# we have to check here if required fields are defined/if this key
# is among them and pass it into the OpenAPIRequestArg class.
required = False
if schema.required:
required = k in schema.required
args.append(
OpenAPIRequestArg(
k,
v,
required,
prefix=prefix,
parent=parent,
depth=depth,
)
args += _parse_request_model(
v.items,
prefix=pref,
parent=pref,
depth=depth + 1,
)
else:
# required fields are defined in the schema above the property, so
# we have to check here if required fields are defined/if this key
# is among them and pass it into the OpenAPIRequestArg class.
required = False
if schema.required:
required = k in schema.required
args.append(
OpenAPIRequestArg(
k,
v,
required,
prefix=prefix,
parent=parent,
depth=depth,
)
)

Copy link
Contributor Author

@lgarber-akamai lgarber-akamai Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is all the same, I just replaced the wrapping if statement with a guard clause

return args

Expand All @@ -212,15 +219,30 @@ def __init__(self, request):
:type request: openapi3.MediaType
"""
self.required = request.schema.required

schema_override = request.extensions.get("linode-cli-use-schema")

schema = request.schema

if schema_override:
override = type(request)(
request.path, {"schema": schema_override}, request._root
)
override._resolve_references()
self.attrs = _parse_request_model(override.schema)
else:
self.attrs = _parse_request_model(request.schema)
schema = override.schema

self.attrs = _parse_request_model(schema)

# attr_routes stores all attribute routes defined using oneOf.
# For example, config-create uses one of to isolate HTTP, HTTPS, and TCP request attributes
self.attr_routes = {}

if schema.oneOf is not None:
for entry in schema.oneOf:
entry_schema = Schema(schema.path, entry, request._root)
self.attr_routes[entry_schema.title] = _parse_request_model(
entry_schema
)


class OpenAPIFilteringRequest:
Expand Down Expand Up @@ -249,3 +271,6 @@ def __init__(self, response_model):

# actually parse out what we can filter by
self.attrs = [c for c in response_model.attrs if c.filterable]

# This doesn't apply since we're building from the response model
self.attr_routes = {}
9 changes: 7 additions & 2 deletions linodecli/baked/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from openapi3.paths import MediaType

from linodecli.baked.util import _aggregate_schema_properties


def _is_paginated(response):
"""
Expand Down Expand Up @@ -169,10 +171,13 @@ def _parse_response_model(schema, prefix=None, nested_list_depth=0):
)

attrs = []
if schema.properties is None:

properties = _aggregate_schema_properties(schema)

if properties is None:
return attrs

for k, v in schema.properties.items():
for k, v in properties.items():
pref = prefix + "." + k if prefix else k

if v.type == "object":
Expand Down
29 changes: 29 additions & 0 deletions linodecli/baked/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Any, Dict

from openapi3.schemas import Schema


def _aggregate_schema_properties(schema: Schema) -> Dict[str, Any]:
"""
Aggregates all properties in the given schema, accounting properties
nested in oneOf and anyOf blocks.

:param schema: The schema to aggregate properties from.
:return: The aggregated properties.
"""

result = {}

if schema.properties is not None:
result.update(dict(schema.properties))

nested_schema = (schema.oneOf or []) + (schema.anyOf or [])

for entry in nested_schema:
entry_schema = Schema(schema.path, entry, schema._root)
if entry_schema.properties is None:
continue

result.update(dict(entry_schema.properties))

return result
15 changes: 11 additions & 4 deletions linodecli/help_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,13 @@ def print_help_action(
_help_action_print_filter_args(console, op)
return

if op.args:
_help_action_print_body_args(console, op)
if len(op.arg_routes) > 0:
# This operation uses oneOf so we need to render routes
# instead of the operation-level argument list.
for title, option in op.arg_routes.items():
_help_action_print_body_args(console, op, option, title=title)
elif op.args:
_help_action_print_body_args(console, op, op.args)


def _help_action_print_filter_args(console: Console, op: OpenAPIOperation):
Expand All @@ -250,13 +255,15 @@ def _help_action_print_filter_args(console: Console, op: OpenAPIOperation):
def _help_action_print_body_args(
console: Console,
op: OpenAPIOperation,
args: List[OpenAPIRequestArg],
title: Optional[str] = None,
):
"""
Pretty-prints all the body (POST/PUT) arguments for this operation.
"""
console.print("[bold]Arguments:[/]")
console.print(f"[bold]Arguments{f' ({title})' if title else ''}:[/]")

for group in _help_group_arguments(op.args):
for group in _help_group_arguments(args):
for arg in group:
metadata = []

Expand Down
Loading