Skip to content

Commit 52254cd

Browse files
committed
Stringify objects when posting a form-encoded body
1 parent d191292 commit 52254cd

6 files changed

Lines changed: 152 additions & 16 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed passing nested objects stringified in form-encoded body according to OAS3.1.

pulp-glue/src/pulp_glue/common/openapi.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@
2929
ValidationError,
3030
)
3131
from pulp_glue.common.i18n import get_translation
32-
from pulp_glue.common.schema import encode_json, encode_param, validate
32+
from pulp_glue.common.schema import (
33+
encode_json,
34+
encode_param,
35+
encode_stringify,
36+
validate,
37+
)
3338

3439
translation = get_translation(__package__)
3540
_ = translation.gettext
@@ -423,7 +428,10 @@ def _render_request_body(
423428
if content_type.startswith("application/json"):
424429
data = encode_json(body)
425430
elif content_type.startswith("application/x-www-form-urlencoded"):
426-
data = body
431+
if isinstance(body, dict):
432+
data = {k: encode_stringify(v) for k, v in body.items()}
433+
else:
434+
data = encode_param(body)
427435
elif content_type.startswith("multipart/form-data"):
428436
data = {}
429437
files = {}
@@ -438,7 +446,7 @@ def _render_request_body(
438446
"application/octet-stream",
439447
)
440448
else:
441-
data[key] = value
449+
data[key] = encode_stringify(value)
442450
break
443451
else:
444452
# No known content-type left

pulp-glue/src/pulp_glue/common/schema.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,46 @@ def encode_json(o: t.Any) -> str:
3030

3131

3232
def encode_param(value: t.Any) -> t.Any:
33-
if isinstance(value, list):
34-
return [encode_param(item) for item in value]
3533
if isinstance(value, datetime.datetime):
3634
return value.strftime(ISO_DATETIME_FORMAT)
3735
elif isinstance(value, datetime.date):
3836
return value.strftime(ISO_DATE_FORMAT)
3937
elif isinstance(value, bool):
4038
return "true" if value else "false"
39+
elif isinstance(value, (int, float)):
40+
return str(value)
41+
elif isinstance(value, list):
42+
return [encode_param(item) for item in value]
43+
elif isinstance(value, dict):
44+
return {k: encode_param(v) for k, v in value.items()}
4145
else:
4246
return value
4347

4448

49+
def encode_html(value: t.Any, *, key: str = "", sep: str = "") -> dict[str, str]:
50+
# This is how html forms __can__ supply nested representations.
51+
# According to openAPI 3.1 however they should default to json stringify.
52+
value = encode_param(value)
53+
res = {}
54+
if isinstance(value, list):
55+
for i, item in enumerate(value):
56+
res.update(encode_html(item, key=key + f"[{i}]"))
57+
elif isinstance(value, dict):
58+
for k, v in value.items():
59+
res.update(encode_html(v, key=key + sep + k, sep="."))
60+
else:
61+
res[key] = value
62+
return res
63+
64+
65+
def encode_stringify(value: str | dict[str, t.Any]) -> str:
66+
if isinstance(value, (dict, list)):
67+
value = encode_json(value)
68+
elif not isinstance(value, str):
69+
value = encode_param(value)
70+
return value
71+
72+
4573
def _assert_type(
4674
name: str,
4775
value: t.Any,

pulp-glue/tests/test_openapi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ def test_references_is_implemented(self, mock_openapi: OpenAPI) -> None:
344344

345345
res = mock_openapi._render_parameters(path_spec, method_spec, parameters)
346346

347-
assert res["query"] == {"limit": 2}
347+
assert res["query"] == {"limit": "2"}
348348

349349
def test_no_parameters_none_specified(self, mock_openapi: OpenAPI) -> None:
350350
parameters: dict[str, t.Any] = {}
@@ -368,7 +368,7 @@ def test_provided_parameters_are_rendered(self, mock_openapi: OpenAPI) -> None:
368368
assert res == {
369369
"query": {"query1": "asdf"},
370370
"header": {},
371-
"path": {"pk": 42},
371+
"path": {"pk": "42"},
372372
"cookie": {},
373373
}
374374
assert parameters == {"query1": "asdf", "pk": 42}

pulp-glue/tests/test_schema.py

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from pulp_glue.common import oas
99
from pulp_glue.common.exceptions import SchemaError, ValidationError
1010
from pulp_glue.common.schema import (
11+
encode_html,
1112
encode_json,
1213
encode_param,
14+
encode_stringify,
1315
validate,
1416
)
1517

@@ -414,10 +416,7 @@ def test_json_encoder_rejects_stream() -> None:
414416

415417
@pytest.mark.parametrize(
416418
"value",
417-
(
418-
pytest.param("asdf", id="string"),
419-
pytest.param(42, id="integer"),
420-
),
419+
(pytest.param("asdf", id="string"),),
421420
)
422421
def test_encode_param_keeps(value: t.Any) -> None:
423422
assert encode_param(value) == value
@@ -426,6 +425,7 @@ def test_encode_param_keeps(value: t.Any) -> None:
426425
@pytest.mark.parametrize(
427426
"value,expected",
428427
(
428+
pytest.param(42, "42", id="integer"),
429429
pytest.param(datetime.date(2000, 1, 1), "2000-01-01", id="date"),
430430
pytest.param(
431431
datetime.datetime(2000, 1, 1, 12, 30), "2000-01-01T12:30:00.000000Z", id="datetime"
@@ -434,3 +434,101 @@ def test_encode_param_keeps(value: t.Any) -> None:
434434
)
435435
def test_encode_param_transforms(value: t.Any, expected: t.Any) -> None:
436436
assert encode_param(value) == expected
437+
438+
439+
@pytest.mark.parametrize(
440+
"value,expected",
441+
(
442+
pytest.param(
443+
datetime.date(2000, 1, 1),
444+
{"": "2000-01-01"},
445+
id="date",
446+
),
447+
pytest.param(
448+
{"a": datetime.datetime(2000, 1, 1, 12, 30)},
449+
{"a": "2000-01-01T12:30:00.000000Z"},
450+
id="datetime",
451+
),
452+
pytest.param(
453+
[1, 1, 12, 30],
454+
{"[0]": "1", "[1]": "1", "[2]": "12", "[3]": "30"},
455+
id="list",
456+
),
457+
pytest.param(
458+
{"a": 1, "b": 2},
459+
{"a": "1", "b": "2"},
460+
id="dict",
461+
),
462+
pytest.param(
463+
{"o": {"a": 1, "b": 2}},
464+
{"o.a": "1", "o.b": "2"},
465+
id="nested_dict",
466+
),
467+
pytest.param(
468+
[{"a": 1, "b": 2}, {"c": 3}],
469+
{"[0]a": "1", "[0]b": "2", "[1]c": "3"},
470+
id="list_of_dicts",
471+
),
472+
pytest.param(
473+
{"a": [1, 2], "b": [3, 4]},
474+
{"a[0]": "1", "a[1]": "2", "b[0]": "3", "b[1]": "4"},
475+
id="dict_of_lists",
476+
),
477+
),
478+
)
479+
def test_encode_html(value: t.Any, expected: t.Any) -> None:
480+
assert encode_html(value) == expected
481+
482+
483+
@pytest.mark.parametrize(
484+
"value,expected",
485+
(
486+
pytest.param(
487+
"Hello, World!",
488+
"Hello, World!",
489+
id="string",
490+
),
491+
pytest.param(
492+
4,
493+
"4",
494+
id="integer",
495+
),
496+
pytest.param(
497+
3.14159,
498+
"3.14159",
499+
id="float",
500+
),
501+
pytest.param(
502+
False,
503+
"false",
504+
id="bool",
505+
),
506+
pytest.param(
507+
datetime.date(2000, 1, 1),
508+
"2000-01-01",
509+
id="date",
510+
),
511+
pytest.param(
512+
{"a": datetime.datetime(2000, 1, 1, 12, 30)},
513+
'{"a": "2000-01-01T12:30:00.000000Z"}',
514+
id="datetime",
515+
),
516+
pytest.param(
517+
{"a": 1, "b": 2},
518+
'{"a": 1, "b": 2}',
519+
id="dict",
520+
),
521+
pytest.param(
522+
{"o": {"a": 1, "b": 2}},
523+
'{"o": {"a": 1, "b": 2}}',
524+
id="nested_dict",
525+
),
526+
pytest.param(
527+
{"a": [1, 2], "b": [3, 4]},
528+
'{"a": [1, 2], "b": [3, 4]}',
529+
id="dict_of_lists",
530+
),
531+
),
532+
)
533+
def test_encode_stringify(value: t.Any, expected: str) -> None:
534+
assert encode_stringify(value) == expected

src/pulp_cli/generic.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -860,12 +860,13 @@ def option_group(
860860
require_all: bool = True,
861861
expose_value: bool = True,
862862
) -> t.Callable[[PulpCommand], PulpCommand]:
863+
"""
864+
Group a list of options into a group represented as a dictionary.
865+
This allows to add a `callback` function for further processing.
866+
`expose_value` allows to hide the value from the command callback.
867+
"""
868+
863869
def _group_callback(ctx: click.Context) -> None:
864-
"""
865-
Group a list of options into a group represented as a dictionary.
866-
This allows to add a `callback` function for further processing.
867-
`expose_value` allows to hide the value from the command callback.
868-
"""
869870
value = {k: v for k, v in ((k, ctx.params.pop(k, None)) for k in options) if v is not None}
870871
if value:
871872
if require_all and (missing_options := set(options) - set(value.keys())):

0 commit comments

Comments
 (0)