diff --git a/src/jsonparse/parser.py b/src/jsonparse/parser.py index 62815d2..7081c9c 100755 --- a/src/jsonparse/parser.py +++ b/src/jsonparse/parser.py @@ -51,10 +51,12 @@ def __init__(self, stack_trace=False, queue_trace=False): self.stack_ref = self._stack_init() self.queue_ref = self._queue_init() - def find_key(self, data, key): + def find_key(self, data, key, partial, case_sensitive): # type: (Union[dict, list, OrderedDict], str) -> list if not self._valid_key_input(data, key): raise + if partial and len(key)==0: + raise self.stack_ref = self._stack_init() # init a new queue every request self._stack_push(data) @@ -66,16 +68,19 @@ def find_key(self, data, key): elem = self._stack_pop() - if type(elem) is list: + if isinstance(elem,list): self._stack_push_list_elem(elem) elif isinstance(elem, (dict, OrderedDict)): - value = self._stack_all_key_values_in_dict(key, elem) + value = self._stack_all_key_values_in_dict(key, elem, partial, case_sensitive) if value: for v in value: - value_list.insert(0, v) + if isinstance(v,list): + value_list.extend([item for item in v]) + else: + value_list.insert(0, v) else: # according to RFC 7159, valid JSON can also contain a - # string, number, 'false', 'null', 'true' - pass # discard these other values as they don't have a key + # string, number, 'false', 'null', 'true' + pass # discard these other values as they don't have a key return value_list @@ -241,7 +246,7 @@ def _stack_push_list_elem(self, elem): self._stack_push(e) self._stack_trace() - def _stack_all_key_values_in_dict(self, key, elem): + def _stack_all_key_values_in_dict(self, key, elem, partial, case_sensitive): # type: (str, Union[dict, OrderedDict]) -> list value_list = [] @@ -253,18 +258,25 @@ def _stack_all_key_values_in_dict(self, key, elem): if len(elem) <= 0: # don't want an empty dict on the stack pass else: - for e in elem: - if e == key: - value_list.append(elem[e]) + value_list=[] + for actual_key, value in elem.items(): + match_key = actual_key if case_sensitive else actual_key.lower() + search_key = key if case_sensitive else key.lower() + if partial: + if search_key in match_key: + value_list.append(value) else: - self._stack_push(elem[e]) - self._stack_trace() + if match_key == search_key: + value_list.append(value) + if not (partial and search_key in match_key) and not (not partial and match_key == search_key): + if isinstance(value, (dict, list, OrderedDict)): + self._stack_push(value) + self._stack_trace() return value_list def _stack_all_keys_values_in_dict(self, keys, elem): # type: (list, Union[dict, OrderedDict]) -> list value_list = [] - if not isinstance(elem, (dict, OrderedDict)): raise TypeError elif type(keys) is not list: diff --git a/tests/test_parser_partial.py b/tests/test_parser_partial.py new file mode 100644 index 0000000..7548c09 --- /dev/null +++ b/tests/test_parser_partial.py @@ -0,0 +1,149 @@ +from jsonparse.parser import Parser +import pytest + +class TestParserComplexJSON: + + @pytest.fixture + def parser(self): + + return Parser(stack_trace=False, queue_trace=False) + + @pytest.fixture + def complex_json(self): + return [ + { + "id": "0001", + "type": "donut", + "exists": True, + "ppu": 0.55, + "batters": { + "batter": [ + {"id": "1001", "type": "Reg"}, + {"id": "1002", "type": "Chocolate"}, + {"id": "1003", "type": "Blueberry"}, + {"id": "1004", "type": "Devil's Food"}, + {"start": 5, "end": 8} + ] + }, + "topping": [ + {"id": "5001", "ty": "None"}, + {"id": "5002", "type": "Glazed"}, + {"id": "5003", "type": "Sugar"}, + {"id": "5004", "type": "Powdered Sugar"}, + {"id": "5005", "type": "Chocolate with Sprinkles"}, + {"id": "5006", "type": "Chocolate"}, + {"id": "5007", "type": "Maple"} + ], + "start": 22, + "end": 99 + }, + { + "id": "0002", + "type": "donut", + "exists": False, + "ppu": 42, + "batters": { + "batter": [{"id": "1001", "type": "Rul"}] + }, + "top_stuff": [ + {"id": "5001", "typ": "None"}, + {"id": "5002", "type": "Glazed"}, + {"id": "5003", "type": "Sugar"}, + {"id": "5004", "type": "Chocolate"}, + {"id": "5005", "type": "Maple"} + ], + "start": 1, + "end": 9 + }, + { + "id": "0003", + "type": "donut", + "exists": None, + "ppu": 7, + "batters": { + "batter": [ + {"id": "1001", "type": "Lar"}, + {"id": "1002", "type": "Chocolate"} + ] + }, + "on_top_thing": [ + {"id": "5001", "type": "None"}, + {"id": "5002", "type": "Glazed"}, + {"id": "5003", "type": "Chocolate"}, + {"id": "5004", "type": "Maple"} + ], + "start": 4, + "end": 7 + } + ] + + + def test_find_key_id(self, parser, complex_json): + """Find all 'id' keys.""" + result = parser.find_key(complex_json, "id", partial=False, case_sensitive=True) + assert result == [ + '1001', '1002', '1003', '1004', '5001', '5002', '5003', + '5004', '5005', '5006', '5007', '0001', '1001', '5001', + '5002', '5003', '5004', '5005', '0002', '1001', '1002', + '5001', '5002', '5003', '5004', '0003' + ] + + def test_find_key_type(self, parser, complex_json): + """Find all 'type' keys.""" + result = parser.find_key(complex_json, "type", partial=False, case_sensitive=True) + print(result) + assert result == [ + 'Reg', 'Chocolate', 'Blueberry', "Devil's Food", 'Glazed', 'Sugar', 'Powdered Sugar', + 'Chocolate with Sprinkles', 'Chocolate', 'Maple', 'donut', 'Rul', 'Glazed', 'Sugar', + 'Chocolate', 'Maple', 'donut', 'Lar', 'Chocolate', 'None', 'Glazed', 'Chocolate', 'Maple', 'donut' + ] + + def test_find_key_start(self, parser, complex_json): + """Find all 'start' keys.""" + result = parser.find_key(complex_json, "start", partial=False, case_sensitive=True) + assert result == [5, 22, 1, 4] + + def test_find_key_partial(self, parser, complex_json): + """Find keys with partial match 'ty' (should match 'type' and 'typ' and 'ty').""" + result = parser.find_key(complex_json, "ty", partial=True, case_sensitive=True) + assert result == [ + 'Reg', 'Chocolate', 'Blueberry', "Devil's Food", 'None', 'Glazed', 'Sugar', 'Powdered Sugar', + 'Chocolate with Sprinkles', 'Chocolate', 'Maple', 'donut', 'Rul', 'None', 'Glazed', + 'Sugar', 'Chocolate', 'Maple', 'donut', 'Lar', 'Chocolate', 'None', + 'Glazed', 'Chocolate', 'Maple', 'donut' + ] + + def test_find_key_case_insensitive(self, parser, complex_json): + """Find keys case-insensitively.""" + result = parser.find_key(complex_json, "TYPE", partial=False, case_sensitive=False) + assert all(isinstance(x,str) for x in result) + assert len(result)>10 + + def test_find_key_nonexistent(self, parser, complex_json): + """Try to find a non-existent key.""" + result = parser.find_key(complex_json, "unknown_key", partial=False, case_sensitive=True) + assert result == [] + + def test_invalid_data_type(self, parser, complex_json): + """Pass invalid data type.""" + try: + parser.find_key("invalid_data", "id", partial=False, case_sensitive=True) + except TypeError: + assert True + + def test_invalid_key_type(self, parser, complex_json): + """Pass non-string key.""" + try: + parser.find_key(complex_json, 1234, partial=False, case_sensitive=True) + except TypeError: + assert True + + def test_empty_key(self, parser, complex_json): + """Pass empty key.""" + try: + parser.find_key(complex_json, "", partial=False, case_sensitive=True) + except ValueError: + assert True + + +