httpie-cli/httpie/cli/json_form.py
Batuhan Taskaya df58ec683e
Add nested JSON syntax to the HTTPie DSL (#1224)
* Add support for nested JSON syntax (#1169)

Co-authored-by: Batuhan Taskaya <isidentical@gmail.com>
Co-authored-by: Jakub Roztocil <jakub@roztocil.co>

* minor improvements

* unpack top level lists

* Write more docs

* doc style changes

* fix double quotes

Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr>
Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-12-03 02:17:45 -08:00

151 lines
4.4 KiB
Python

"""
Routines for JSON form syntax, used to support nested JSON request items.
Highly inspired from the great jarg project <https://github.com/jdp/jarg/blob/master/jarg/jsonform.py>.
"""
import re
import operator
from typing import Optional
def step(value: str, is_escaped: bool) -> str:
if is_escaped:
value = value.replace(r'\[', '[').replace(r'\]', ']')
return value
def find_opening_bracket(
value: str,
search=re.compile(r'(?<!\\)\[').search
) -> Optional[int]:
match = search(value)
if not match:
return None
return match.start()
def find_closing_bracket(
value: str,
search=re.compile(r'(?<!\\)\]').search
) -> Optional[int]:
match = search(value)
if not match:
return None
return match.start()
def parse_path(path):
"""
Parse a string as a JSON path.
An implementation of 'steps to parse a JSON encoding path'.
<https://www.w3.org/TR/html-json-forms/#dfn-steps-to-parse-a-json-encoding-path>
"""
original = path
is_escaped = r'\[' in original
opening_bracket = find_opening_bracket(original)
last_step = [(step(path, is_escaped), {'last': True, 'type': 'object'})]
if opening_bracket is None:
return last_step
steps = [(step(original[:opening_bracket], is_escaped), {'type': 'object'})]
path = original[opening_bracket:]
while path:
if path.startswith('[]'):
steps[-1][1]['append'] = True
path = path[2:]
if path:
return last_step
elif path[0] == '[':
path = path[1:]
closing_bracket = find_closing_bracket(path)
if closing_bracket is None:
return last_step
key = path[:closing_bracket]
path = path[closing_bracket + 1:]
try:
steps.append((int(key), {'type': 'array'}))
except ValueError:
steps.append((key, {'type': 'object'}))
elif path[:2] == r'\[':
key = step(path[1:path.index(r'\]') + 2], is_escaped)
path = path[path.index(r'\]') + 2:]
steps.append((key, {'type': 'object'}))
else:
return last_step
for i in range(len(steps) - 1):
steps[i][1]['type'] = steps[i + 1][1]['type']
steps[-1][1]['last'] = True
return steps
def set_value(context, step, current_value, entry_value):
"""Apply a JSON value to a context object.
An implementation of 'steps to set a JSON encoding value'.
<https://www.w3.org/TR/html-json-forms/#dfn-steps-to-set-a-json-encoding-value>
"""
key, flags = step
if flags.get('last', False):
if current_value is None:
if flags.get('append', False):
context[key] = [entry_value]
else:
if isinstance(context, list) and len(context) <= key:
context.extend([None] * (key - len(context) + 1))
context[key] = entry_value
elif isinstance(current_value, list):
context[key].append(entry_value)
else:
context[key] = [current_value, entry_value]
return context
if current_value is None:
if flags.get('type') == 'array':
context[key] = []
else:
if isinstance(context, list) and len(context) <= key:
context.extend([None] * (key - len(context) + 1))
context[key] = {}
return context[key]
elif isinstance(current_value, dict):
return context[key]
elif isinstance(current_value, list):
if flags.get('type') == 'array':
return current_value
obj = {}
for i, item in enumerate(current_value):
if item is not None:
obj[i] = item
else:
context[key] = obj
return obj
else:
obj = {'': current_value}
context[key] = obj
return obj
def interpret_json_form(pairs):
"""The application/json form encoding algorithm.
<https://www.w3.org/TR/html-json-forms/#dfn-application-json-encoding-algorithm>
"""
result = {}
for key, value in pairs:
steps = parse_path(key)
context = result
for step in steps:
try:
current_value = operator.getitem(context, step[0])
except LookupError:
current_value = None
context = set_value(context, step, current_value, value)
return result