Skip to content
Open
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
2 changes: 2 additions & 0 deletions linodecli/baked/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

## Request Parsing Gotchas
- `_aggregate_schema_properties()` merges `oneOf`, `anyOf`, and `allOf`; a field is marked required only if every leaf schema that defines properties requires it. Schemas that only nest composition without their own `properties` do not increment the required-count denominator.
- Nested request objects are expanded when `_aggregate_schema_properties()` finds properties and the schema has `type: object` or no explicit type, unless the effective format is `json`. Explicit scalar and array types retain their dedicated parsing paths. A nested `oneOf`/`anyOf`/`allOf` without `type` normally becomes dotted CLI args (for example `--details.host`); `x-linode-cli-format: json` or `format: json` instead preserves a raw-JSON parent argument, with the CLI extension taking precedence.
- `OpenAPIRequest.attr_routes` stores per-`oneOf` option args keyed by schema `title`; oneOf entries without titles raise `ValueError`.
- `OpenAPIOperation.arg_routes` returns `self.request.attr_routes` (a dict) when a request exists, but `[]` when there is no request. Treat the empty case as a type inconsistency (list vs dict); callers that always call `.items()` will break on the no-request path.
- Request and response attributes marked `linode-cli-skip` are omitted from generated args/output.
Expand All @@ -48,6 +49,7 @@
- `--order-by` is restricted to filterable attrs and becomes `+order_by` in `X-Filter`; list filters serialize as `+and` entries.

## Response And Output Gotchas
- Nested response schemas with aggregated properties are expanded even without `type: object`; arbitrary-key dictionaries retain their existing leaf handling, and scalar compositions without properties are not expanded.
- Bake-time pagination detection is structural: top-level response schema must have exactly `pages`, `page`, `results`, and `data`; only `data.items` becomes display attrs (`is_paginated`).
- `OpenAPIResponse.fix_json()` handles `linode-cli-rows`, then `linode-cli-nested-list`, then unwraps via `"pages" in json` (not `self.is_paginated`), then wraps non-lists. Runtime unwrap is looser than bake-time pagination; do not tighten it to `is_paginated` without checking odd payloads that only include `pages`.
- `linode-cli-rows` paths are extracted from the raw API JSON and concatenated; list values extend the result, scalar/object values are appended.
Expand Down
9 changes: 7 additions & 2 deletions linodecli/baked/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,16 @@ def _parse_request_model(
if v.extensions.get("linode-cli-skip"):
continue

# Nested oneOf/anyOf/allOf schemas may omit type: object.
nested_props, _ = _aggregate_schema_properties(v)
effective_format = v.extensions.get("linode-cli-format") or v.format

# Handle nested objects which aren't read-only and have properties
if (
v.type == "object"
v.type in (None, "object")
and not v.readOnly
and len(_aggregate_schema_properties(v)[0]) > 0
and nested_props
and effective_format != "json"
):
# nested objects receive a prefix and are otherwise parsed normally
pref = prefix + "." + k if prefix else k
Expand Down
4 changes: 3 additions & 1 deletion linodecli/baked/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,9 @@ def _parse_response_model(schema, prefix=None, nested_list_depth=0):
k, v, prefix=prefix, nested_list_depth=nested_list_depth
)
)
elif v.type == "object":
elif v.type == "object" or (
v.type is None and bool(_aggregate_schema_properties(v)[0])
):
attrs += _parse_response_model(v, prefix=pref)
elif v.type == "array" and (
v.items.type == "object"
Expand Down
1 change: 0 additions & 1 deletion tests/fixtures/operation_with_one_ofs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ components:
description: Some foofoo.
barbar:
description: Some barbar.
type: object
anyOf:
- type: object
properties:
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test_request.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import pytest
from openapi3 import OpenAPI
from yaml import safe_load

from linodecli.baked.request import _parse_request_model
from tests.unit.conftest import open_fixture


class TestRequest:
"""
Unit tests for baked requests.
Expand All @@ -17,11 +25,54 @@ def test_handle_one_ofs(self, post_operation_with_one_ofs):
"barbar.baz": ("boolean", "Some baz.", False),
}

assert len(arg_map) == len(expected)

for k, v in expected.items():
assert arg_map[k].datatype == v[0]
assert arg_map[k].description == v[1]
assert arg_map[k].required == v[2]

@pytest.mark.parametrize("format_marker", ["x-linode-cli-format", "format"])
@pytest.mark.parametrize("schema_type", [None, "object"])
def test_json_format_composed_objects(self, format_marker, schema_type):
with open_fixture("operation_with_one_ofs.yaml") as fixture:
document = safe_load(fixture)

variant = document["components"]["schemas"]["Foo"]["oneOf"][1]
nested_schema = variant["properties"]["barbar"]
nested_schema[format_marker] = "json"
if schema_type is not None:
nested_schema["type"] = schema_type

schema = OpenAPI(document).components.schemas["Foo"]
arg_map = {arg.path: arg for arg in _parse_request_model(schema)}

assert set(arg_map) == {"foobar", "barfoo", "foofoo", "barbar"}
assert arg_map["barbar"].format == "json"
assert arg_map["barbar"].datatype == "object"

@pytest.mark.parametrize("schema_type", ["string", "integer", "array"])
def test_explicit_non_object_composed_schema(self, schema_type):
with open_fixture("operation_with_one_ofs.yaml") as fixture:
document = safe_load(fixture)

variant = document["components"]["schemas"]["Foo"]["oneOf"][1]
nested_schema = variant["properties"]["barbar"]
nested_schema["type"] = schema_type
for branch in nested_schema["anyOf"]:
branch.pop("type")
if schema_type == "array":
nested_schema["items"] = {"type": "string"}

schema = OpenAPI(document).components.schemas["Foo"]
arg_map = {arg.path: arg for arg in _parse_request_model(schema)}

assert set(arg_map) == {"foobar", "barfoo", "foofoo", "barbar"}
assert arg_map["barbar"].datatype == schema_type
assert arg_map["barbar"].format is None
if schema_type == "array":
assert arg_map["barbar"].item_type == "string"

def test_skip_request_attributes(self, post_skip_test_operation):
"""
Test that request attributes with x-linode-cli-skip extension are excluded.
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ def test_handle_one_ofs(self, post_operation_with_one_ofs):
"barbar.baz": ("boolean", "Some baz"),
}

assert len(attr_map) == len(expected)

for k, v in expected.items():
assert attr_map[k].datatype == v[0]
assert attr_map[k].description == v[1]
Expand Down
Loading