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
1 change: 1 addition & 0 deletions CHANGES/pulp-glue/+deep_objects.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed passing nested objects stringified in form-encoded body according to OAS3.1.
14 changes: 11 additions & 3 deletions pulp-glue/src/pulp_glue/common/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@
ValidationError,
)
from pulp_glue.common.i18n import get_translation
from pulp_glue.common.schema import encode_json, encode_param, validate
from pulp_glue.common.schema import (
encode_json,
encode_param,
encode_stringify,
validate,
)

translation = get_translation(__package__)
_ = translation.gettext
Expand Down Expand Up @@ -421,7 +426,10 @@ def _render_request_body(
if content_type.startswith("application/json"):
data = encode_json(body)
elif content_type.startswith("application/x-www-form-urlencoded"):
data = body
if isinstance(body, dict):
data = {k: encode_stringify(v) for k, v in body.items()}
else:
data = encode_param(body)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happens if body is a list of dicts? Is this possible?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not yet in our api. And I'd need to reread the specification in that case.

elif content_type.startswith("multipart/form-data"):
data = {}
files = {}
Expand All @@ -436,7 +444,7 @@ def _render_request_body(
"application/octet-stream",
)
else:
data[key] = value
data[key] = encode_stringify(value)
break
else:
# No known content-type left
Expand Down
30 changes: 28 additions & 2 deletions pulp-glue/src/pulp_glue/common/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,44 @@ def encode_json(o: t.Any) -> str:


def encode_param(value: t.Any) -> t.Any:
if isinstance(value, list):
return [encode_param(item) for item in value]
if isinstance(value, datetime.datetime):
return value.strftime(ISO_DATETIME_FORMAT)
elif isinstance(value, datetime.date):
return value.strftime(ISO_DATE_FORMAT)
elif isinstance(value, bool):
return "true" if value else "false"
elif isinstance(value, list):
return [encode_param(item) for item in value]
elif isinstance(value, dict):
return {k: encode_param(v) for k, v in value.items()}
else:
return value


def encode_html(value: t.Any, *, key: str = "", sep: str = "") -> dict[str, str]:
# This is how html forms __can__ supply nested representations.
# According to openAPI 3.1 however they should default to json stringify.
value = encode_param(value)
res = {}
if isinstance(value, list):
for i, item in enumerate(value):
res.update(encode_html(item, key=key + f"[{i}]"))
elif isinstance(value, dict):
for k, v in value.items():
res.update(encode_html(v, key=key + sep + k, sep="."))
else:
res[key] = value
return res


def encode_stringify(value: str | dict[str, t.Any]) -> str:
if isinstance(value, (dict, list)):
value = encode_json(value)
elif not isinstance(value, str):
value = encode_param(value)
return value


def _assert_type(
name: str,
value: t.Any,
Expand Down
45 changes: 45 additions & 0 deletions pulp-glue/tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pulp_glue.common import oas
from pulp_glue.common.exceptions import SchemaError, ValidationError
from pulp_glue.common.schema import (
encode_html,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are you going to add a test for encode_stringify?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can, I guess...

encode_json,
encode_param,
validate,
Expand Down Expand Up @@ -434,3 +435,47 @@ def test_encode_param_keeps(value: t.Any) -> None:
)
def test_encode_param_transforms(value: t.Any, expected: t.Any) -> None:
assert encode_param(value) == expected


@pytest.mark.parametrize(
"value,expected",
(
pytest.param(
datetime.date(2000, 1, 1),
{"": "2000-01-01"},
id="date",
),
pytest.param(
{"a": datetime.datetime(2000, 1, 1, 12, 30)},
{"a": "2000-01-01T12:30:00.000000Z"},
id="datetime",
),
pytest.param(
[1, 1, 12, 30],
{"[0]": 1, "[1]": 1, "[2]": 12, "[3]": 30},
id="list",
),
pytest.param(
{"a": 1, "b": 2},
{"a": 1, "b": 2},
id="dict",
),
pytest.param(
{"o": {"a": 1, "b": 2}},
{"o.a": 1, "o.b": 2},
id="nested_dict",
),
pytest.param(
[{"a": 1, "b": 2}, {"c": 3}],
{"[0]a": 1, "[0]b": 2, "[1]c": 3},
id="list_of_dicts",
),
pytest.param(
{"a": [1, 2], "b": [3, 4]},
{"a[0]": 1, "a[1]": 2, "b[0]": 3, "b[1]": 4},
id="dict_of_lists",
),
),
)
def test_encode_html(value: t.Any, expected: t.Any) -> None:
assert encode_html(value) == expected
11 changes: 6 additions & 5 deletions src/pulp_cli/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,12 +860,13 @@ def option_group(
require_all: bool = True,
expose_value: bool = True,
) -> t.Callable[[PulpCommand], PulpCommand]:
"""
Group a list of options into a group represented as a dictionary.
This allows to add a `callback` function for further processing.
`expose_value` allows to hide the value from the command callback.
"""

def _group_callback(ctx: click.Context) -> None:
"""
Group a list of options into a group represented as a dictionary.
This allows to add a `callback` function for further processing.
`expose_value` allows to hide the value from the command callback.
"""
value = {k: v for k, v in ((k, ctx.params.pop(k, None)) for k in options) if v is not None}
if value:
if require_all and (missing_options := set(options) - set(value.keys())):
Expand Down
Loading