1
0
mirror of https://github.com/httpie/cli.git synced 2025-08-10 22:42:05 +02:00

Implement HTTPie Nested JSON v2

This commit is contained in:
Batuhan Taskaya
2022-01-04 12:04:20 +03:00
parent 21faddc4b9
commit 7bf373751d
7 changed files with 665 additions and 228 deletions

View File

@@ -5,12 +5,19 @@ import responses
from httpie.cli.constants import PRETTY_MAP
from httpie.cli.exceptions import ParseError
from httpie.cli.nested_json import HTTPieSyntaxError
from httpie.compat import is_windows
from httpie.output.formatters.colors import ColorFormatter
from httpie.utils import JsonDictPreservingDuplicateKeys
from .fixtures import JSON_WITH_DUPE_KEYS_FILE_PATH
from .utils import MockEnvironment, http, DUMMY_URL
from .fixtures import (
FILE_CONTENT,
FILE_PATH,
JSON_FILE_CONTENT,
JSON_FILE_PATH,
JSON_WITH_DUPE_KEYS_FILE_PATH,
)
from .utils import DUMMY_URL, MockEnvironment, http
TEST_JSON_XXSI_PREFIXES = [
r")]}',\n",
@@ -35,25 +42,27 @@ TEST_JSON_VALUES = [
TEST_PREFIX_TOKEN_COLOR = '\x1b[38;5;15m' if is_windows else '\x1b[04m\x1b[91m'
JSON_WITH_DUPES_RAW = '{"key": 15, "key": 15, "key": 3, "key": 7}'
JSON_WITH_DUPES_FORMATTED_SORTED = '''{
JSON_WITH_DUPES_FORMATTED_SORTED = """{
"key": 3,
"key": 7,
"key": 15,
"key": 15
}'''
JSON_WITH_DUPES_FORMATTED_UNSORTED = '''{
}"""
JSON_WITH_DUPES_FORMATTED_UNSORTED = """{
"key": 15,
"key": 15,
"key": 3,
"key": 7
}'''
}"""
@pytest.mark.parametrize('data_prefix', TEST_JSON_XXSI_PREFIXES)
@pytest.mark.parametrize('json_data', TEST_JSON_VALUES)
@pytest.mark.parametrize('pretty', PRETTY_MAP.keys())
@responses.activate
def test_json_formatter_with_body_preceded_by_non_json_data(data_prefix, json_data, pretty):
def test_json_formatter_with_body_preceded_by_non_json_data(
data_prefix, json_data, pretty
):
"""Test JSON bodies preceded by non-JSON data."""
body = data_prefix + json.dumps(json_data)
content_type = 'application/json;charset=utf8'
@@ -71,11 +80,15 @@ def test_json_formatter_with_body_preceded_by_non_json_data(data_prefix, json_da
indent = None if pretty in {'none', 'colors'} else 4
expected_body = data_prefix + json.dumps(json_data, indent=indent)
if colored_output:
fmt = ColorFormatter(env, format_options={'json': {'format': True, 'indent': 4}})
fmt = ColorFormatter(
env, format_options={'json': {'format': True, 'indent': 4}}
)
expected_body = fmt.format_body(expected_body, content_type)
# Check to ensure the non-JSON data prefix is colored only one time,
# meaning it was correctly handled as a whole.
assert TEST_PREFIX_TOKEN_COLOR + data_prefix in expected_body, expected_body
assert (
TEST_PREFIX_TOKEN_COLOR + data_prefix in expected_body
), expected_body
assert expected_body in r
@@ -119,12 +132,7 @@ def test_duplicate_keys_support_from_input_file():
assert JSON_WITH_DUPES_FORMATTED_UNSORTED in r
@pytest.mark.parametrize("value", [
1,
1.1,
True,
'some_value'
])
@pytest.mark.parametrize('value', [1, 1.1, True, 'some_value'])
def test_simple_json_arguments_with_non_json(httpbin, value):
r = http(
'--form',
@@ -134,15 +142,14 @@ def test_simple_json_arguments_with_non_json(httpbin, value):
assert r.json['form'] == {'option': str(value)}
@pytest.mark.parametrize("request_type", [
"--form",
"--multipart",
])
@pytest.mark.parametrize("value", [
[1, 2, 3],
{'a': 'b'},
None
])
@pytest.mark.parametrize(
'request_type',
[
'--form',
'--multipart',
],
)
@pytest.mark.parametrize('value', [[1, 2, 3], {'a': 'b'}, None])
def test_complex_json_arguments_with_non_json(httpbin, request_type, value):
with pytest.raises(ParseError) as cm:
http(
@@ -151,61 +158,322 @@ def test_complex_json_arguments_with_non_json(httpbin, request_type, value):
f'option:={json.dumps(value)}',
)
cm.match('Can\'t use complex JSON value types')
cm.match("Can't use complex JSON value types")
@pytest.mark.parametrize('input_json, expected_json', [
# Examples taken from https://www.w3.org/TR/html-json-forms/
(
['bottle-on-wall:=1', 'bottle-on-wall:=2', 'bottle-on-wall:=3'],
{'bottle-on-wall': [1, 2, 3]},
),
(
['pet[species]=Dahut', 'pet[name]:="Hypatia"', 'kids[1]=Thelma', 'kids[0]:="Ashley"'],
{'pet': {'species': 'Dahut', 'name': 'Hypatia'}, 'kids': ['Ashley', 'Thelma']},
),
(
['pet[0][species]=Dahut', 'pet[0][name]=Hypatia', 'pet[1][species]=Felis Stultus', 'pet[1][name]:="Billie"'],
{'pet': [{'species': 'Dahut', 'name': 'Hypatia'}, {'species': 'Felis Stultus', 'name': 'Billie'}]},
),
(
['wow[such][deep][3][much][power][!]=Amaze'],
{'wow': {'such': {'deep': [None, None, None, {'much': {'power': {'!': 'Amaze'}}}]}}},
),
(
['mix=scalar', 'mix[0]=array 1', 'mix[2]:="array 2"', 'mix[key]:="key key"', 'mix[car]=car key'],
{'mix': {'': 'scalar', '0': 'array 1', '2': 'array 2', 'key': 'key key', 'car': 'car key'}},
),
(
['highlander[]=one'],
{'highlander': ['one']},
),
(
['error[good]=BOOM!', 'error[bad:="BOOM BOOM!"'],
{'error': {'good': 'BOOM!'}, 'error[bad': 'BOOM BOOM!'},
),
(
['special[]:=true', 'special[]:=false', 'special[]:="true"', 'special[]:=null'],
{'special': [True, False, 'true', None]},
),
(
[r'\[\]:=1', r'escape\[d\]:=1', r'escaped\[\]:=1', r'e\[s\][c][a][p]\[ed\][]:=1'],
{'[]': 1, 'escape[d]': 1, 'escaped[]': 1, 'e[s]': {'c': {'a': {'p': {'[ed]': [1]}}}}},
),
(
['[]:=1', '[]=foo'],
[1, 'foo'],
),
(
[']:=1', '[]1:=1', '[1]]:=1'],
{']': 1, '[]1': 1, '[1]]': 1},
),
])
def test_nested_json_syntax(input_json, expected_json, httpbin_both):
r = http(httpbin_both + '/post', *input_json)
@pytest.mark.parametrize(
'input_json, expected_json',
[
# Examples taken from https://www.w3.org/TR/html-json-forms/
(
[
'bottle-on-wall[]:=1',
'bottle-on-wall[]:=2',
'bottle-on-wall[]:=3',
],
{'bottle-on-wall': [1, 2, 3]},
),
(
[
'pet[species]=Dahut',
'pet[name]:="Hypatia"',
'kids[1]=Thelma',
'kids[0]:="Ashley"',
],
{
'pet': {'species': 'Dahut', 'name': 'Hypatia'},
'kids': ['Ashley', 'Thelma'],
},
),
(
[
'pet[0][species]=Dahut',
'pet[0][name]=Hypatia',
'pet[1][species]=Felis Stultus',
'pet[1][name]:="Billie"',
],
{
'pet': [
{'species': 'Dahut', 'name': 'Hypatia'},
{'species': 'Felis Stultus', 'name': 'Billie'},
]
},
),
(
['wow[such][deep][3][much][power][!]=Amaze'],
{
'wow': {
'such': {
'deep': [
None,
None,
None,
{'much': {'power': {'!': 'Amaze'}}},
]
}
}
},
),
(
['mix[]=scalar', 'mix[2]=something', 'mix[4]:="something 2"'],
{'mix': ['scalar', None, 'something', None, 'something 2']},
),
(
['highlander[]=one'],
{'highlander': ['one']},
),
(
['error[good]=BOOM!', r'error\[bad:="BOOM BOOM!"'],
{'error': {'good': 'BOOM!'}, 'error[bad': 'BOOM BOOM!'},
),
(
[
'special[]:=true',
'special[]:=false',
'special[]:="true"',
'special[]:=null',
],
{'special': [True, False, 'true', None]},
),
(
[
r'\[\]:=1',
r'escape\[d\]:=1',
r'escaped\[\]:=1',
r'e\[s\][c][a][p][\[ed\]][]:=1',
],
{
'[]': 1,
'escape[d]': 1,
'escaped[]': 1,
'e[s]': {'c': {'a': {'p': {'[ed]': [1]}}}},
},
),
(
['[]:=1', '[]=foo'],
[1, 'foo'],
),
(
[r'\]:=1', r'\[\]1:=1', r'\[1\]\]:=1'],
{']': 1, '[]1': 1, '[1]]': 1},
),
(
[
r'foo\[bar\][baz]:=1',
r'foo\[bar\]\[baz\]:=3',
r'foo[bar][\[baz\]]:=4',
],
{
'foo[bar]': {'baz': 1},
'foo[bar][baz]': 3,
'foo': {'bar': {'[baz]': 4}},
},
),
(
['key[]:=1', 'key[][]:=2', 'key[][][]:=3', 'key[][][]:=4'],
{'key': [1, [2], [[3]], [[4]]]},
),
(
['x[0]:=1', 'x[]:=2', 'x[]:=3', 'x[][]:=4', 'x[][]:=5'],
{'x': [1, 2, 3, [4], [5]]},
),
(
[
f'x=@{FILE_PATH}',
f'y[z]=@{FILE_PATH}',
f'q[u][]:=@{JSON_FILE_PATH}',
],
{
'x': FILE_CONTENT,
'y': {'z': FILE_CONTENT},
'q': {'u': [json.loads(JSON_FILE_CONTENT)]},
},
),
(
[
'foo[bar][5][]:=5',
'foo[bar][]:=6',
'foo[bar][][]:=7',
'foo[bar][][x]=dfasfdas',
'foo[baz]:=[1, 2, 3]',
'foo[baz][]:=4',
],
{
'foo': {
'bar': [
None,
None,
None,
None,
None,
[5],
6,
[7],
{'x': 'dfasfdas'},
],
'baz': [1, 2, 3, 4],
}
},
),
(
[
'foo[]:=1',
'foo[]:=2',
'foo[][key]=value',
'foo[2][key 2]=value 2',
r'foo[2][key \[]=value 3',
r'[nesting][under][!][empty][?][\\key]:=4',
],
{
'foo': [
1,
2,
{'key': 'value', 'key 2': 'value 2', 'key [': 'value 3'},
],
'': {
'nesting': {'under': {'!': {'empty': {'?': {'\\key': 4}}}}}
},
},
),
(
[
r'foo\[key\]:=1',
r'bar\[1\]:=2',
r'baz\[\]:3',
r'quux[key\[escape\]]:=4',
r'quux[key 2][\\][\\\\][\\\[\]\\\]\\\[\n\\]:=5',
],
{
'foo[key]': 1,
'bar[1]': 2,
'quux': {
'key[escape]': 4,
'key 2': {'\\': {'\\\\': {'\\[]\\]\\[\\n\\': 5}}},
},
},
),
(
[r'A[B\\]=C', r'A[B\\\\]=C', r'A[\B\\]=C'],
{'A': {'B\\': 'C', 'B\\\\': 'C', '\\B\\': 'C'}},
),
(
[
'name=python',
'version:=3',
'date[year]:=2021',
'date[month]=December',
'systems[]=Linux',
'systems[]=Mac',
'systems[]=Windows',
'people[known_ids][1]:=1000',
'people[known_ids][5]:=5000',
],
{
'name': 'python',
'version': 3,
'date': {'year': 2021, 'month': 'December'},
'systems': ['Linux', 'Mac', 'Windows'],
'people': {'known_ids': [None, 1000, None, None, None, 5000]},
},
),
],
)
def test_nested_json_syntax(input_json, expected_json, httpbin):
r = http(httpbin + '/post', *input_json)
assert r.json['json'] == expected_json
@pytest.mark.parametrize(
'input_json, expected_error',
[
(
['A[:=1'],
"HTTPie Syntax Error: Expecting a text, a number or ']'\nA[\n ^",
),
(['A[1:=1'], "HTTPie Syntax Error: Expecting ']'\nA[1\n ^"),
(['A[text:=1'], "HTTPie Syntax Error: Expecting ']'\nA[text\n ^"),
(
['A[text][:=1'],
"HTTPie Syntax Error: Expecting a text, a number or ']'\nA[text][\n ^",
),
(
['A[key]=value', 'B[something]=u', 'A[text][:=1', 'C[key]=value'],
"HTTPie Syntax Error: Expecting a text, a number or ']'\nA[text][\n ^",
),
(
['A[text]1:=1'],
"HTTPie Syntax Error: Expecting '['\nA[text]1\n ^",
),
(['A\\[]:=1'], "HTTPie Syntax Error: Expecting '['\nA\\[]\n ^"),
(
['A[something\\]:=1'],
"HTTPie Syntax Error: Expecting ']'\nA[something\\]\n ^",
),
(
['foo\\[bar\\]\\\\[ bleh:=1'],
"HTTPie Syntax Error: Expecting ']'\nfoo\\[bar\\]\\\\[ bleh\n ^",
),
(
['foo\\[bar\\]\\\\[ bleh :=1'],
"HTTPie Syntax Error: Expecting ']'\nfoo\\[bar\\]\\\\[ bleh \n ^",
),
(
['foo[bar][1]][]:=2'],
"HTTPie Syntax Error: Expecting '['\nfoo[bar][1]][]\n ^",
),
(
['foo[bar][1]something[]:=2'],
"HTTPie Syntax Error: Expecting '['\nfoo[bar][1]something[]\n ^^^^^^^^^",
),
(
['foo[bar][1][142241[]:=2'],
"HTTPie Syntax Error: Expecting ']'\nfoo[bar][1][142241[]\n ^",
),
(
['foo[bar][1]\\[142241[]:=2'],
"HTTPie Syntax Error: Expecting '['\nfoo[bar][1]\\[142241[]\n ^^^^^^^^",
),
(
['foo=1', 'foo[key]:=2'],
"HTTPie Type Error: Can't perform 'key' based access on 'foo' which has a type of 'string' but this operation requires a type of 'object'.\nfoo[key]\n ^^^^^",
),
(
['foo=1', 'foo[0]:=2'],
"HTTPie Type Error: Can't perform 'index' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[0]\n ^^^",
),
(
['foo=1', 'foo[]:=2'],
"HTTPie Type Error: Can't perform 'append' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[]\n ^^",
),
(
['data[key]=value', 'data[key 2]=value 2', 'data[0]=value'],
"HTTPie Type Error: Can't perform 'index' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[0]\n ^^^",
),
(
['data[key]=value', 'data[key 2]=value 2', 'data[]=value'],
"HTTPie Type Error: Can't perform 'append' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[]\n ^^",
),
(
[
'foo[bar][baz][5]:=[1,2,3]',
'foo[bar][baz][5][]:=4',
'foo[bar][baz][key][]:=5',
],
"HTTPie Type Error: Can't perform 'key' based access on 'foo[bar][baz]' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[bar][baz][key][]\n ^^^^^",
),
(
['foo[-10]:=[1,2]'],
'HTTPie Value Error: Negative indexes are not supported.\nfoo[-10]\n ^^^',
),
],
)
def test_nested_json_errors(input_json, expected_error, httpbin):
with pytest.raises(HTTPieSyntaxError) as exc:
http(httpbin + '/post', *input_json)
assert str(exc.value) == expected_error
def test_nested_json_sparse_array(httpbin_both):
r = http(httpbin_both + '/post', 'test[0]:=1', 'test[100]:=1')
assert len(r.json['json']['test']) == 101