From cefe25f013169946cbada7233f5ad30e984584f8 Mon Sep 17 00:00:00 2001 From: killeroonie Date: Fri, 27 Jun 2025 22:16:36 -0700 Subject: [PATCH 1/9] Tweaked __testrep__() output for a SLICE. Previously it used a colon between the SLICE token name and the slice parameters, which caused confusion since slice uses colons for delimiting fields. Now enclosing the slice parameters in brackets []. --- killerbunny/lexing/tokens.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/killerbunny/lexing/tokens.py b/killerbunny/lexing/tokens.py index 76d6727..a5f3593 100644 --- a/killerbunny/lexing/tokens.py +++ b/killerbunny/lexing/tokens.py @@ -286,7 +286,9 @@ def __str__(self) -> str: def __testrepr__(self) -> str: """Representation of Token for testing purposes. If you change this, you may break unit tests. """ - if self.token_type.is_literal() or self.token_type.is_identifier(): + if self.token_type == TokenType.SLICE: + string = f"SLICE[{self.value}]" + elif self.token_type.is_literal() or self.token_type.is_identifier(): string = f"{self.token_type}:{self.value}" elif self.token_type.is_keyword(): string =f"{self.token_type.category.name}:{self.value}" From 038fe1fc1d82e74b55ea00515e61dc37d6609b78 Mon Sep 17 00:00:00 2001 From: killeroonie Date: Fri, 27 Jun 2025 22:18:48 -0700 Subject: [PATCH 2/9] Uncommented paths which included function names. They were commented out before function expressions were implemented. These paths can now be used to test function expressions. --- tests/jpath/rfc9535_examples/table_14.jpathl | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/jpath/rfc9535_examples/table_14.jpathl b/tests/jpath/rfc9535_examples/table_14.jpathl index fe1b7b9..6f0a274 100644 --- a/tests/jpath/rfc9535_examples/table_14.jpathl +++ b/tests/jpath/rfc9535_examples/table_14.jpathl @@ -1,14 +1,14 @@ -#$[?length(@) < 3] -#$[?length(@.*) < 3] -#$[?count(@.*) == 1] -#$[?count(1) == 1] -#$[?count(foo(@.*)) == 1] -#$[?match(@.timezone, 'Europe/.*')] -#$[?match(@.timezone,'Europe/.*') == true] -#$[?value(@..color) == "red"] -#$[?value(@..color)] -#$[?bar(@.a)] -#$[?bnl(@.*)] -#$[?blt(1==1)] -#$[?blt(1)] -#$[?bal(1)] \ No newline at end of file +$[?length(@) < 3] +$[?length(@.*) < 3] +$[?count(@.*) == 1] +$[?count(1) == 1] +$[?count(foo(@.*)) == 1] +$[?match(@.timezone, 'Europe/.*')] +$[?match(@.timezone,'Europe/.*') == true] +$[?value(@..color) == "red"] +$[?value(@..color)] +$[?bar(@.a)] +$[?bnl(@.*)] +$[?blt(1==1)] +$[?blt(1)] +$[?bal(1)] \ No newline at end of file From b5192824bb21e217067349d8a95603331bf644f5 Mon Sep 17 00:00:00 2001 From: killeroonie Date: Fri, 27 Jun 2025 22:19:44 -0700 Subject: [PATCH 3/9] Deleted unused, commented-out code. --- tests/jpath/evaluating/cts/test_cts.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/jpath/evaluating/cts/test_cts.py b/tests/jpath/evaluating/cts/test_cts.py index 67e9d88..ab4b16d 100644 --- a/tests/jpath/evaluating/cts/test_cts.py +++ b/tests/jpath/evaluating/cts/test_cts.py @@ -99,11 +99,6 @@ def from_dict(cls, data: dict[str, Any]) -> 'CTSTestData': _MODULE_DIR = Path(__file__).parent _CTS_FILE_PATH = _MODULE_DIR / "cts.json" -# todo when code-complete we can removed _FILE_LIST or just use cts.json which has all the tests. -# they are separated here for the moment as some features have not been implemented yet and the entire file of test -# cases fails, like functions. -# _FILE_LIST = ["basic.json", "index_selector.json", "name_selector.json", "slice_selector.json", "filter.json"] -# _FILE_LIST = [ "functions/count.json", "functions/length.json", "functions/match.json", "functions/search.json", 'functions/value.json'] _FILE_LIST = [ "cts.json",] def data_loader() -> list[CTSTestData]: From 2bb8e37fc1150436e9c2178f28ae08ccc1e88516 Mon Sep 17 00:00:00 2001 From: killeroonie Date: Fri, 27 Jun 2025 22:23:25 -0700 Subject: [PATCH 4/9] Refactored lexer tests. Refactored lexer test case generation to output a json file. Converted previous multiple text files into a single json test file (lexer_tests.json). Implemented new tests in test_lexer_rfc9535_tables.py. --- killerbunny/shared/testgen.py | 117 +-- .../jpath_token_files/table_02.jpath_tokens | 14 - .../jpath_token_files/table_03.jpath_tokens | 2 - .../jpath_token_files/table_05.jpath_tokens | 6 - .../jpath_token_files/table_06.jpath_tokens | 6 - .../jpath_token_files/table_07.jpath_tokens | 3 - .../jpath_token_files/table_09.jpath_tokens | 6 - .../jpath_token_files/table_11.jpath_tokens | 29 - .../jpath_token_files/table_12.jpath_tokens | 16 - .../jpath_token_files/table_14.jpath_tokens | 15 - .../jpath_token_files/table_15.jpath_tokens | 4 - .../jpath_token_files/table_16.jpath_tokens | 9 - .../jpath_token_files/table_17.jpath_tokens | 10 - .../jpath_token_files/table_18.jpath_tokens | 7 - tests/jpath/lexing/lexer_tests.json | 789 ++++++++++++++++++ tests/jpath/lexing/test_lex_rfc_examples.py | 57 -- .../jpath/lexing/test_lexer_rfc9535_tables.py | 77 ++ 17 files changed, 936 insertions(+), 231 deletions(-) delete mode 100644 tests/jpath/lexing/jpath_token_files/table_02.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_03.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_05.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_06.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_07.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_09.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_11.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_12.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_14.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_15.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_16.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_17.jpath_tokens delete mode 100644 tests/jpath/lexing/jpath_token_files/table_18.jpath_tokens create mode 100644 tests/jpath/lexing/lexer_tests.json delete mode 100644 tests/jpath/lexing/test_lex_rfc_examples.py create mode 100644 tests/jpath/lexing/test_lexer_rfc9535_tables.py diff --git a/killerbunny/shared/testgen.py b/killerbunny/shared/testgen.py index dd3095c..54da608 100644 --- a/killerbunny/shared/testgen.py +++ b/killerbunny/shared/testgen.py @@ -6,12 +6,12 @@ # # -# testgen.py """Functions for creating test data files for the Lexer, Parser, and Evaluator unit tests""" import json +from dataclasses import dataclass, asdict from pathlib import Path -from typing import cast +from typing import cast, Any from killerbunny.evaluating.evaluator import JPathEvaluator from killerbunny.evaluating.runtime_result import RuntimeResult @@ -45,8 +45,8 @@ _MODULE_DIR = Path(__file__).parent def load_obj_from_json_file(input_file: Path) -> JSON_ValueType: - """Return the json object from the json file in the argument. - Intended for a json file with a single object for testing and debugging. """ + """Return the JSON object from the JSON file in the argument. + Intended for a JSON file with a single object for testing and debugging. """ with open(input_file, "rb", buffering=ONE_MEBIBYTE) as in_file: json_str = in_file.read() return cast(JSON_ValueType, json.loads(json_str)) @@ -55,6 +55,15 @@ def load_obj_from_json_file(input_file: Path) -> JSON_ValueType: # LEXER TEST GENERATION #################################################################### +@dataclass(frozen=True, slots=True) +class LexerTestCase: + test_name : str + json_path : str + lexer_tokens : str + source_file_name : str + is_invalid : bool = False + + def tokenize_jpath_str(file_name:str, jpath_query_str: str) -> str: """ Run the `jpath_query_str` through the lexer and return a string representation of the generated Token list. """ lexer = JPathLexer(file_name, jpath_query_str) @@ -68,26 +77,18 @@ def tokenize_jpath_str(file_name:str, jpath_query_str: str) -> str: return result_str -# Given an input file path, return a list of tokens generated by the lexer for each line in the input file. -# Each line of the input file is a json path query string. This method will pass each string to the lexer and -# receive from it a list of scanned tokens corresponding to the input string. This method returns a list of two-item -# tuples. The first tuple item is the original json path query string. The second tuple item is a string representation -# of the list of generated tokens. The string representation of each token is generated by calling __testrepr__() on -# each token. Comments in the input file are ignored. If a line starts with #, it is ignored. Note that # is a valid -# character that may be part of a member name or seach string, so inline comments are not supported. -def generate_lexer_path_values(input_path: Path) -> list[ tuple[ str, str] ]: - """For each line in the input file of json path query strings, return a list of tokens generated by the lexer. +def generate_lexer_path_tokens(input_path: Path) -> list[ tuple[ str, str]]: + """For each line in the input file of JSON Path query strings, return a list of tokens generated by the lexer. - Each line of the input file is a json path query string. This method will pass each string to the lexer and + Each line of the input file is a JSON Path query string. This method will pass each string to the lexer and receive from it a list of scanned tokens corresponding to the input string, or an error message if the lexer failed to scan the input string. - This method returns a list of two-item tuples. The first tuple item is the original json path query string. - The second tuple item is a string representation of the list of generated tokens, or a lexer error message. + This method returns a list of two-item tuples. The first tuple item is the original JSON Path query string. + The second tuple item is a string representation for the list of generated tokens, or a lexer error message. The string representation of each token is generated by calling __testrepr__() on each token. Lines that start with a # (line comment) are ignored, as well as blank lines. Note that # is a valid character that may be part of a member name or search string, so end-of-line comments are not supported. - """ result_list: list[ tuple[ str, str] ] = [] file_name: str = input_path.name @@ -100,39 +101,52 @@ def generate_lexer_path_values(input_path: Path) -> list[ tuple[ str, str] ]: result_list.append( (line_stripped, result_str) ) return result_list -FRAGILE_TEST_DIR = _MODULE_DIR / '../../../../tests/test_jpath_interpreter' +def generate_lexer_test_cases(path_tokens: list[ tuple[ str, str] ], + source_file_name: str, + ) -> list[LexerTestCase]: + + result_list: list[LexerTestCase] = [] + for path, tokens in path_tokens: + test_name = f"{source_file_name}-{path}" + result_list.append( LexerTestCase(test_name, path, tokens, source_file_name) ) + return result_list + + +FRAGILE_TEST_DIR = _MODULE_DIR / '../../tests/' FRAGILE_TEST_DIR_PATH = Path(FRAGILE_TEST_DIR) -def process_lexer_paths(input_dir: Path, suffix: str, generate_test_files: bool = False, quiet:bool = False)-> None: - """Given a directory path and a file suffix, run generate_lexer_path_values() for each file in the directory - that ends with `suffix`. No directory recursion is done. If `generate_test_files` is True, will create a test file - for each matching input file and save it in the tests.../.../lexing/jpath_token_files dir. Test files contain a - json path query string followed by JPATH_DATA_SEPARATOR and the string representation of the list of generated tokens. - If `quiet` is True, omits writing most output to the consoled, except for warning or error messages. +def process_lexer_paths(input_dir: Path, suffix: str, generate_test_file: bool = False, quiet:bool = False)-> None: + """Given a directory path and a file suffix, run generate_lexer_path_tokens() for each file in the directory + that ends with `suffix`. No directory recursion is done. This is a utility function to help create test data for lexer unit testing. This should be used with caution to avoid accidentally ovewriting existing test files. However, the open() call for creating the test file uses the 'x' mode - flag, so it will report an error if the file alredy exists. + flag, so it will report an error if the file already exists. + + If `generate_test_file` is True, create a JSON test file containing each matching input file's test data + and save it as: tests/jpath/lexing/lexer_tests.json. + Test files contain multiple test cases containing a JSON Path query string and the string representation + for the list of generated tokens. + If `quiet` is True, omits writing most output to the console, except for warning or error messages. """ if not quiet: print(f"*** Processing jpathl files for the Lexer") if not input_dir.is_dir(): raise FileNotFoundError(f'Input directory {input_dir} does not exist') - output_dir = FRAGILE_TEST_DIR_PATH / "incubator/jpath" / "lexing" / "jpath_token_files" - if generate_test_files: + output_dir = FRAGILE_TEST_DIR_PATH / "jpath/lexing" + if generate_test_file: # ensure test directory exists if not quiet: - print() - print(f"Generating test files in {output_dir}") + print(f"\nGenerating test files in {output_dir}") output_dir.mkdir(parents=True, exist_ok=True) generated_files: list[str] = [] - for file in input_dir.iterdir(): + test_cases: list[LexerTestCase] = [] + for file in sorted(input_dir.iterdir()): if file.name.endswith(suffix): if not quiet: - print() - print(f"Processing '{file}'") - file_results: list[ tuple[ str, str ]] = generate_lexer_path_values(file) + print(f"\nProcessing '{file}'") + file_results: list[ tuple[ str, str ]] = generate_lexer_path_tokens(file) if not file_results: print(f"Warning: file '{file}' produced no results. Is it empty?") continue @@ -143,18 +157,26 @@ def process_lexer_paths(input_dir: Path, suffix: str, generate_test_files: bool print(f"{jpath}{JPATH_DATA_SEPARATOR}{tokens}") print("-" * 40) - if generate_test_files: - outfile_path = output_dir / ( file.stem + ".jpath_tokens" ) - if not quiet: print(f"Generating test file '{outfile_path}'") - generated_files.append( outfile_path.name ) - with open(outfile_path, "x", encoding=UTF8, buffering=ONE_MEBIBYTE) as outfile: - outfile.write(f"# file_name: {outfile_path.name}\n") - for jpath, tokens in file_results: - outfile.write(f"{jpath}{JPATH_DATA_SEPARATOR}{tokens}\n") + if generate_test_file: + test_cases.extend(generate_lexer_test_cases(file_results, str(file.stem))) + + + if generate_test_file and test_cases: + outfile_path = output_dir / "lexer_tests.json" + if not quiet: print(f"Generating test file '{outfile_path}'") + generated_files.append( outfile_path.name ) + tests_list: list[dict[str, Any]] = [] + test_file_dict = {"description":"Lexer tests of example paths in RFC 9535 tables 2-18. This file is autogenerated, do not edit.", + "tests": tests_list } + for test_case in test_cases: + tests_list.append(asdict(test_case)) + + with open(outfile_path, "x", encoding=UTF8, buffering=ONE_MEBIBYTE) as outfile: + json.dump(test_file_dict, outfile, ensure_ascii=False, indent=4) + if generated_files and not quiet: - print() - print(f"Generated a total of {len(generated_files)} lexer test files:") + print(f"\nGenerated a total of {len(generated_files)} lexer test files:") for line in sorted(generated_files): print(f" {line}" ) @@ -164,7 +186,7 @@ def process_lexer_paths(input_dir: Path, suffix: str, generate_test_files: bool #################################################################### def parse_jpath_str(file_name:str, jpath_query_str: str) -> str: - """Lex and parse the json path query string and return a string representation of the generated AST from the parser.""" + """Lex and parse the JSON Path query string and return a string representation of the generated AST from the parser.""" lexer = JPathLexer(file_name, jpath_query_str) tokens, error = lexer.tokenize() if error: @@ -255,7 +277,7 @@ def process_parser_paths(input_dir: Path, #################################################################### def evaluate_jpath_str(file_name:str, jpath_query_str: str, json_value:JSON_ValueType) -> VNodeList: - """Lex and parse the json path query string and evalutate it with the JSON value in the argument. + """Lex and parse the JSON Path query string and evalutate it with the JSON value in the argument. Return a VNodeList""" lexer = JPathLexer(file_name, jpath_query_str) tokens, error = lexer.tokenize() @@ -322,7 +344,7 @@ def rename_file_suffix(input_dir: Path, from_suffix: str, to_suffix: str)-> None def create_lexer_test_files() -> None: """PRODUCTION CODE""" input_dir = Path(FRAGILE_TEST_DIR) / "jpath/rfc9535_examples/" - process_lexer_paths(input_dir, "jpathl", generate_test_files=True) + process_lexer_paths(input_dir, "jpathl", generate_test_file=True) def create_parser_test_files() -> None: """PRODUCTION CODE""" @@ -336,7 +358,8 @@ def create_all_test_files() -> None: create_parser_test_files() def main() -> None: - t1() + # t1() + create_lexer_test_files() #create_parser_test_files() if __name__ == '__main__': diff --git a/tests/jpath/lexing/jpath_token_files/table_02.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_02.jpath_tokens deleted file mode 100644 index eb518ce..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_02.jpath_tokens +++ /dev/null @@ -1,14 +0,0 @@ -# file_name: table_02.jpath_tokens -$.store.book[*].author : DOLLAR, DOT, ID:store, DOT, ID:book, LBRACKET, STAR, RBRACKET, DOT, ID:author, EOF -$..author : DOLLAR, DOUBLE_DOT, ID:author, EOF -$.store.* : DOLLAR, DOT, ID:store, DOT, STAR, EOF -$.store..price : DOLLAR, DOT, ID:store, DOUBLE_DOT, ID:price, EOF -$..book[2] : DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:2, RBRACKET, EOF -$..book[2].author : DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:2, RBRACKET, DOT, ID:author, EOF -$..book[2].publisher : DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:2, RBRACKET, DOT, ID:publisher, EOF -$..book[-1] : DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:-1, RBRACKET, EOF -$..book[0,1] : DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:0, COMMA, INT:1, RBRACKET, EOF -$..book[:2] : DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, SLICE::2, RBRACKET, EOF -$..book[?@.isbn] : DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, QMARK, AT, DOT, ID:isbn, RBRACKET, EOF -$..book[?@.price<10] : DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, QMARK, AT, DOT, ID:price, LT, INT:10, RBRACKET, EOF -$..* : DOLLAR, DOUBLE_DOT, STAR, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_03.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_03.jpath_tokens deleted file mode 100644 index 4719a49..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_03.jpath_tokens +++ /dev/null @@ -1,2 +0,0 @@ -# file_name: table_03.jpath_tokens -$ : DOLLAR, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_05.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_05.jpath_tokens deleted file mode 100644 index 4e51984..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_05.jpath_tokens +++ /dev/null @@ -1,6 +0,0 @@ -# file_name: table_05.jpath_tokens -$.o['j j'] : DOLLAR, DOT, ID:o, LBRACKET, STRING:'j j', RBRACKET, EOF -$.o['j j']['k.k'] : DOLLAR, DOT, ID:o, LBRACKET, STRING:'j j', RBRACKET, LBRACKET, STRING:'k.k', RBRACKET, EOF -$.o["j j"]["k.k"] : DOLLAR, DOT, ID:o, LBRACKET, STRING:"j j", RBRACKET, LBRACKET, STRING:"k.k", RBRACKET, EOF -$['o']['j j']['k.k'] : DOLLAR, LBRACKET, STRING:'o', RBRACKET, LBRACKET, STRING:'j j', RBRACKET, LBRACKET, STRING:'k.k', RBRACKET, EOF -$["'"]["@"] : DOLLAR, LBRACKET, STRING:"'", RBRACKET, LBRACKET, STRING:"@", RBRACKET, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_06.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_06.jpath_tokens deleted file mode 100644 index 6a87c74..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_06.jpath_tokens +++ /dev/null @@ -1,6 +0,0 @@ -# file_name: table_06.jpath_tokens -$[*] : DOLLAR, LBRACKET, STAR, RBRACKET, EOF -$.o[*] : DOLLAR, DOT, ID:o, LBRACKET, STAR, RBRACKET, EOF -$.o[*] : DOLLAR, DOT, ID:o, LBRACKET, STAR, RBRACKET, EOF -$.o[*, *] : DOLLAR, DOT, ID:o, LBRACKET, STAR, COMMA, STAR, RBRACKET, EOF -$.a[*] : DOLLAR, DOT, ID:a, LBRACKET, STAR, RBRACKET, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_07.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_07.jpath_tokens deleted file mode 100644 index b0e1363..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_07.jpath_tokens +++ /dev/null @@ -1,3 +0,0 @@ -# file_name: table_07.jpath_tokens -$[1] : DOLLAR, LBRACKET, INT:1, RBRACKET, EOF -$[-2] : DOLLAR, LBRACKET, INT:-2, RBRACKET, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_09.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_09.jpath_tokens deleted file mode 100644 index e5c1820..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_09.jpath_tokens +++ /dev/null @@ -1,6 +0,0 @@ -# file_name: table_09.jpath_tokens -$[1:3] : DOLLAR, LBRACKET, SLICE:1:3, RBRACKET, EOF -$[5:] : DOLLAR, LBRACKET, SLICE:5:, RBRACKET, EOF -$[1:5:2] : DOLLAR, LBRACKET, SLICE:1:5:2, RBRACKET, EOF -$[5:1:-2] : DOLLAR, LBRACKET, SLICE:5:1:-2, RBRACKET, EOF -$[::-1] : DOLLAR, LBRACKET, SLICE:::-1, RBRACKET, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_11.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_11.jpath_tokens deleted file mode 100644 index 848cc5b..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_11.jpath_tokens +++ /dev/null @@ -1,29 +0,0 @@ -# file_name: table_11.jpath_tokens -$.absent1 == $.absent2 : DOLLAR, DOT, ID:absent1, EQUAL, DOLLAR, DOT, ID:absent2, EOF -$.absent1 <= $.absent2 : DOLLAR, DOT, ID:absent1, LTE, DOLLAR, DOT, ID:absent2, EOF -$.absent == 'g' : DOLLAR, DOT, ID:absent, EQUAL, STRING:'g', EOF -$.absent1 != $.absent2 : DOLLAR, DOT, ID:absent1, NOT_EQUAL, DOLLAR, DOT, ID:absent2, EOF -$.absent != 'g' : DOLLAR, DOT, ID:absent, NOT_EQUAL, STRING:'g', EOF -1 <= 2 : INT:1, LTE, INT:2, EOF -1> 2 : INT:1, GT, INT:2, EOF -13 == '13' : INT:13, EQUAL, STRING:'13', EOF -'a' <= 'b' : STRING:'a', LTE, STRING:'b', EOF -'a' > 'b' : STRING:'a', GT, STRING:'b', EOF -$.obj == $.arr : DOLLAR, DOT, ID:obj, EQUAL, DOLLAR, DOT, ID:arr, EOF -$.obj != $.arr : DOLLAR, DOT, ID:obj, NOT_EQUAL, DOLLAR, DOT, ID:arr, EOF -$.obj == $.obj : DOLLAR, DOT, ID:obj, EQUAL, DOLLAR, DOT, ID:obj, EOF -$.obj != $.obj : DOLLAR, DOT, ID:obj, NOT_EQUAL, DOLLAR, DOT, ID:obj, EOF -$.arr == $.arr : DOLLAR, DOT, ID:arr, EQUAL, DOLLAR, DOT, ID:arr, EOF -$.arr != $.arr : DOLLAR, DOT, ID:arr, NOT_EQUAL, DOLLAR, DOT, ID:arr, EOF -$.obj == 17 : DOLLAR, DOT, ID:obj, EQUAL, INT:17, EOF -$.obj != 17 : DOLLAR, DOT, ID:obj, NOT_EQUAL, INT:17, EOF -$.obj <= $.arr : DOLLAR, DOT, ID:obj, LTE, DOLLAR, DOT, ID:arr, EOF -$.obj < $.arr : DOLLAR, DOT, ID:obj, LT, DOLLAR, DOT, ID:arr, EOF -$.obj <= $.obj : DOLLAR, DOT, ID:obj, LTE, DOLLAR, DOT, ID:obj, EOF -$.arr <= $.arr : DOLLAR, DOT, ID:arr, LTE, DOLLAR, DOT, ID:arr, EOF -1 <= $.arr : INT:1, LTE, DOLLAR, DOT, ID:arr, EOF -1 >= $.arr : INT:1, GTE, DOLLAR, DOT, ID:arr, EOF -1 > $.arr : INT:1, GT, DOLLAR, DOT, ID:arr, EOF -1 < $.arr : INT:1, LT, DOLLAR, DOT, ID:arr, EOF -true <= true : KEYWORD:true, LTE, KEYWORD:true, EOF -true > true : KEYWORD:true, GT, KEYWORD:true, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_12.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_12.jpath_tokens deleted file mode 100644 index 04ae305..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_12.jpath_tokens +++ /dev/null @@ -1,16 +0,0 @@ -# file_name: table_12.jpath_tokens -$.a[?@.b == 'kilo'] : DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, DOT, ID:b, EQUAL, STRING:'kilo', RBRACKET, EOF -$.a[?(@.b == 'kilo')] : DOLLAR, DOT, ID:a, LBRACKET, QMARK, LPAREN, AT, DOT, ID:b, EQUAL, STRING:'kilo', RPAREN, RBRACKET, EOF -$.a[?@>3.5] : DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, GT, FLOAT:3.5, RBRACKET, EOF -$.a[?@.b] : DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, DOT, ID:b, RBRACKET, EOF -$[?@.*] : DOLLAR, LBRACKET, QMARK, AT, DOT, STAR, RBRACKET, EOF -$[?@[?@.b]] : DOLLAR, LBRACKET, QMARK, AT, LBRACKET, QMARK, AT, DOT, ID:b, RBRACKET, RBRACKET, EOF -$.o[?@<3, ?@<3] : DOLLAR, DOT, ID:o, LBRACKET, QMARK, AT, LT, INT:3, COMMA, QMARK, AT, LT, INT:3, RBRACKET, EOF -$.a[?@<2 || @.b == "k"] : DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, LT, INT:2, OR, AT, DOT, ID:b, EQUAL, STRING:"k", RBRACKET, EOF -$.a[?match(@.b, "[jk]")] : DOLLAR, DOT, ID:a, LBRACKET, QMARK, ID:match, LPAREN, AT, DOT, ID:b, COMMA, STRING:"[jk]", RPAREN, RBRACKET, EOF -$.a[?search(@.b, "[jk]")] : DOLLAR, DOT, ID:a, LBRACKET, QMARK, ID:search, LPAREN, AT, DOT, ID:b, COMMA, STRING:"[jk]", RPAREN, RBRACKET, EOF -$.o[?@>1 && @<4] : DOLLAR, DOT, ID:o, LBRACKET, QMARK, AT, GT, INT:1, AND, AT, LT, INT:4, RBRACKET, EOF -$.o[?@>1 && @<4] : DOLLAR, DOT, ID:o, LBRACKET, QMARK, AT, GT, INT:1, AND, AT, LT, INT:4, RBRACKET, EOF -$.o[?@.u || @.x] : DOLLAR, DOT, ID:o, LBRACKET, QMARK, AT, DOT, ID:u, OR, AT, DOT, ID:x, RBRACKET, EOF -$.a[?@.b == $.x] : DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, DOT, ID:b, EQUAL, DOLLAR, DOT, ID:x, RBRACKET, EOF -$.a[?@ == @] : DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, EQUAL, AT, RBRACKET, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_14.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_14.jpath_tokens deleted file mode 100644 index 8dc5820..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_14.jpath_tokens +++ /dev/null @@ -1,15 +0,0 @@ -# file_name: table_14.jpath_tokens -$[?length(@) < 3] : DOLLAR, LBRACKET, QMARK, ID:length, LPAREN, AT, RPAREN, LT, INT:3, RBRACKET, EOF -$[?length(@.*) < 3] : DOLLAR, LBRACKET, QMARK, ID:length, LPAREN, AT, DOT, STAR, RPAREN, LT, INT:3, RBRACKET, EOF -$[?count(@.*) == 1] : DOLLAR, LBRACKET, QMARK, ID:count, LPAREN, AT, DOT, STAR, RPAREN, EQUAL, INT:1, RBRACKET, EOF -$[?count(1) == 1] : DOLLAR, LBRACKET, QMARK, ID:count, LPAREN, INT:1, RPAREN, EQUAL, INT:1, RBRACKET, EOF -$[?count(foo(@.*)) == 1] : DOLLAR, LBRACKET, QMARK, ID:count, LPAREN, ID:foo, LPAREN, AT, DOT, STAR, RPAREN, RPAREN, EQUAL, INT:1, RBRACKET, EOF -$[?match(@.timezone, 'Europe/.*')] : DOLLAR, LBRACKET, QMARK, ID:match, LPAREN, AT, DOT, ID:timezone, COMMA, STRING:'Europe/.*', RPAREN, RBRACKET, EOF -$[?match(@.timezone,'Europe/.*') == true] : DOLLAR, LBRACKET, QMARK, ID:match, LPAREN, AT, DOT, ID:timezone, COMMA, STRING:'Europe/.*', RPAREN, EQUAL, KEYWORD:true, RBRACKET, EOF -$[?value(@..color) == "red"] : DOLLAR, LBRACKET, QMARK, ID:value, LPAREN, AT, DOUBLE_DOT, ID:color, RPAREN, EQUAL, STRING:"red", RBRACKET, EOF -$[?value(@..color)] : DOLLAR, LBRACKET, QMARK, ID:value, LPAREN, AT, DOUBLE_DOT, ID:color, RPAREN, RBRACKET, EOF -$[?bar(@.a)] : DOLLAR, LBRACKET, QMARK, ID:bar, LPAREN, AT, DOT, ID:a, RPAREN, RBRACKET, EOF -$[?bnl(@.*)] : DOLLAR, LBRACKET, QMARK, ID:bnl, LPAREN, AT, DOT, STAR, RPAREN, RBRACKET, EOF -$[?blt(1==1)] : DOLLAR, LBRACKET, QMARK, ID:blt, LPAREN, INT:1, EQUAL, INT:1, RPAREN, RBRACKET, EOF -$[?blt(1)] : DOLLAR, LBRACKET, QMARK, ID:blt, LPAREN, INT:1, RPAREN, RBRACKET, EOF -$[?bal(1)] : DOLLAR, LBRACKET, QMARK, ID:bal, LPAREN, INT:1, RPAREN, RBRACKET, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_15.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_15.jpath_tokens deleted file mode 100644 index 94354c1..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_15.jpath_tokens +++ /dev/null @@ -1,4 +0,0 @@ -# file_name: table_15.jpath_tokens -$[0, 3] : DOLLAR, LBRACKET, INT:0, COMMA, INT:3, RBRACKET, EOF -$[0:2, 5] : DOLLAR, LBRACKET, SLICE:0:2, COMMA, INT:5, RBRACKET, EOF -$[0, 0] : DOLLAR, LBRACKET, INT:0, COMMA, INT:0, RBRACKET, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_16.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_16.jpath_tokens deleted file mode 100644 index a367c4c..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_16.jpath_tokens +++ /dev/null @@ -1,9 +0,0 @@ -# file_name: table_16.jpath_tokens -$..j : DOLLAR, DOUBLE_DOT, ID:j, EOF -$..j : DOLLAR, DOUBLE_DOT, ID:j, EOF -$..[0] : DOLLAR, DOUBLE_DOT, LBRACKET, INT:0, RBRACKET, EOF -$..[*] : DOLLAR, DOUBLE_DOT, LBRACKET, STAR, RBRACKET, EOF -$..* : DOLLAR, DOUBLE_DOT, STAR, EOF -$..o : DOLLAR, DOUBLE_DOT, ID:o, EOF -$.o..[*, *] : DOLLAR, DOT, ID:o, DOUBLE_DOT, LBRACKET, STAR, COMMA, STAR, RBRACKET, EOF -$.a..[0, 1] : DOLLAR, DOT, ID:a, DOUBLE_DOT, LBRACKET, INT:0, COMMA, INT:1, RBRACKET, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_17.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_17.jpath_tokens deleted file mode 100644 index 35f4bf2..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_17.jpath_tokens +++ /dev/null @@ -1,10 +0,0 @@ -# file_name: table_17.jpath_tokens -$.a : DOLLAR, DOT, ID:a, EOF -$.a[0] : DOLLAR, DOT, ID:a, LBRACKET, INT:0, RBRACKET, EOF -$.a.d : DOLLAR, DOT, ID:a, DOT, ID:d, EOF -$.b[0] : DOLLAR, DOT, ID:b, LBRACKET, INT:0, RBRACKET, EOF -$.b[*] : DOLLAR, DOT, ID:b, LBRACKET, STAR, RBRACKET, EOF -$.b[?@] : DOLLAR, DOT, ID:b, LBRACKET, QMARK, AT, RBRACKET, EOF -$.b[?@==null] : DOLLAR, DOT, ID:b, LBRACKET, QMARK, AT, EQUAL, KEYWORD:null, RBRACKET, EOF -$.c[?@.d==null] : DOLLAR, DOT, ID:c, LBRACKET, QMARK, AT, DOT, ID:d, EQUAL, KEYWORD:null, RBRACKET, EOF -$.null : DOLLAR, DOT, KEYWORD:null, EOF diff --git a/tests/jpath/lexing/jpath_token_files/table_18.jpath_tokens b/tests/jpath/lexing/jpath_token_files/table_18.jpath_tokens deleted file mode 100644 index b9dbd2b..0000000 --- a/tests/jpath/lexing/jpath_token_files/table_18.jpath_tokens +++ /dev/null @@ -1,7 +0,0 @@ -# file_name: table_18.jpath_tokens -$.a : DOLLAR, DOT, ID:a, EOF -$[1] : DOLLAR, LBRACKET, INT:1, RBRACKET, EOF -$[-3] : DOLLAR, LBRACKET, INT:-3, RBRACKET, EOF -$.a.b[1:2] : DOLLAR, DOT, ID:a, DOT, ID:b, LBRACKET, SLICE:1:2, RBRACKET, EOF -$["\u000B"] : DOLLAR, LBRACKET, STRING:"\u000B", RBRACKET, EOF -$["\u0061"] : DOLLAR, LBRACKET, STRING:"\u0061", RBRACKET, EOF diff --git a/tests/jpath/lexing/lexer_tests.json b/tests/jpath/lexing/lexer_tests.json new file mode 100644 index 0000000..4b68204 --- /dev/null +++ b/tests/jpath/lexing/lexer_tests.json @@ -0,0 +1,789 @@ +{ + "description": "Lexer tests of example paths in RFC 9535 tables 2-18. This file is autogenerated, do not edit.", + "tests": [ + { + "test_name": "table_02-$.store.book[*].author", + "json_path": "$.store.book[*].author", + "lexer_tokens": "DOLLAR, DOT, ID:store, DOT, ID:book, LBRACKET, STAR, RBRACKET, DOT, ID:author, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..author", + "json_path": "$..author", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:author, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$.store.*", + "json_path": "$.store.*", + "lexer_tokens": "DOLLAR, DOT, ID:store, DOT, STAR, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$.store..price", + "json_path": "$.store..price", + "lexer_tokens": "DOLLAR, DOT, ID:store, DOUBLE_DOT, ID:price, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..book[2]", + "json_path": "$..book[2]", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:2, RBRACKET, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..book[2].author", + "json_path": "$..book[2].author", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:2, RBRACKET, DOT, ID:author, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..book[2].publisher", + "json_path": "$..book[2].publisher", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:2, RBRACKET, DOT, ID:publisher, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..book[-1]", + "json_path": "$..book[-1]", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:-1, RBRACKET, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..book[0,1]", + "json_path": "$..book[0,1]", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, INT:0, COMMA, INT:1, RBRACKET, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..book[:2]", + "json_path": "$..book[:2]", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, SLICE[:2], RBRACKET, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..book[?@.isbn]", + "json_path": "$..book[?@.isbn]", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, QMARK, AT, DOT, ID:isbn, RBRACKET, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..book[?@.price<10]", + "json_path": "$..book[?@.price<10]", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:book, LBRACKET, QMARK, AT, DOT, ID:price, LT, INT:10, RBRACKET, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_02-$..*", + "json_path": "$..*", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, STAR, EOF", + "source_file_name": "table_02", + "is_invalid": false + }, + { + "test_name": "table_03-$", + "json_path": "$", + "lexer_tokens": "DOLLAR, EOF", + "source_file_name": "table_03", + "is_invalid": false + }, + { + "test_name": "table_05-$.o['j j']", + "json_path": "$.o['j j']", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, STRING:'j j', RBRACKET, EOF", + "source_file_name": "table_05", + "is_invalid": false + }, + { + "test_name": "table_05-$.o['j j']['k.k']", + "json_path": "$.o['j j']['k.k']", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, STRING:'j j', RBRACKET, LBRACKET, STRING:'k.k', RBRACKET, EOF", + "source_file_name": "table_05", + "is_invalid": false + }, + { + "test_name": "table_05-$.o[\"j j\"][\"k.k\"]", + "json_path": "$.o[\"j j\"][\"k.k\"]", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, STRING:\"j j\", RBRACKET, LBRACKET, STRING:\"k.k\", RBRACKET, EOF", + "source_file_name": "table_05", + "is_invalid": false + }, + { + "test_name": "table_05-$['o']['j j']['k.k']", + "json_path": "$['o']['j j']['k.k']", + "lexer_tokens": "DOLLAR, LBRACKET, STRING:'o', RBRACKET, LBRACKET, STRING:'j j', RBRACKET, LBRACKET, STRING:'k.k', RBRACKET, EOF", + "source_file_name": "table_05", + "is_invalid": false + }, + { + "test_name": "table_05-$[\"'\"][\"@\"]", + "json_path": "$[\"'\"][\"@\"]", + "lexer_tokens": "DOLLAR, LBRACKET, STRING:\"'\", RBRACKET, LBRACKET, STRING:\"@\", RBRACKET, EOF", + "source_file_name": "table_05", + "is_invalid": false + }, + { + "test_name": "table_06-$[*]", + "json_path": "$[*]", + "lexer_tokens": "DOLLAR, LBRACKET, STAR, RBRACKET, EOF", + "source_file_name": "table_06", + "is_invalid": false + }, + { + "test_name": "table_06-$.o[*]", + "json_path": "$.o[*]", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, STAR, RBRACKET, EOF", + "source_file_name": "table_06", + "is_invalid": false + }, + { + "test_name": "table_06-$.o[*]", + "json_path": "$.o[*]", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, STAR, RBRACKET, EOF", + "source_file_name": "table_06", + "is_invalid": false + }, + { + "test_name": "table_06-$.o[*, *]", + "json_path": "$.o[*, *]", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, STAR, COMMA, STAR, RBRACKET, EOF", + "source_file_name": "table_06", + "is_invalid": false + }, + { + "test_name": "table_06-$.a[*]", + "json_path": "$.a[*]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, STAR, RBRACKET, EOF", + "source_file_name": "table_06", + "is_invalid": false + }, + { + "test_name": "table_07-$[1]", + "json_path": "$[1]", + "lexer_tokens": "DOLLAR, LBRACKET, INT:1, RBRACKET, EOF", + "source_file_name": "table_07", + "is_invalid": false + }, + { + "test_name": "table_07-$[-2]", + "json_path": "$[-2]", + "lexer_tokens": "DOLLAR, LBRACKET, INT:-2, RBRACKET, EOF", + "source_file_name": "table_07", + "is_invalid": false + }, + { + "test_name": "table_09-$[1:3]", + "json_path": "$[1:3]", + "lexer_tokens": "DOLLAR, LBRACKET, SLICE[1:3], RBRACKET, EOF", + "source_file_name": "table_09", + "is_invalid": false + }, + { + "test_name": "table_09-$[5:]", + "json_path": "$[5:]", + "lexer_tokens": "DOLLAR, LBRACKET, SLICE[5:], RBRACKET, EOF", + "source_file_name": "table_09", + "is_invalid": false + }, + { + "test_name": "table_09-$[1:5:2]", + "json_path": "$[1:5:2]", + "lexer_tokens": "DOLLAR, LBRACKET, SLICE[1:5:2], RBRACKET, EOF", + "source_file_name": "table_09", + "is_invalid": false + }, + { + "test_name": "table_09-$[5:1:-2]", + "json_path": "$[5:1:-2]", + "lexer_tokens": "DOLLAR, LBRACKET, SLICE[5:1:-2], RBRACKET, EOF", + "source_file_name": "table_09", + "is_invalid": false + }, + { + "test_name": "table_09-$[::-1]", + "json_path": "$[::-1]", + "lexer_tokens": "DOLLAR, LBRACKET, SLICE[::-1], RBRACKET, EOF", + "source_file_name": "table_09", + "is_invalid": false + }, + { + "test_name": "table_11-$.absent1 == $.absent2", + "json_path": "$.absent1 == $.absent2", + "lexer_tokens": "DOLLAR, DOT, ID:absent1, EQUAL, DOLLAR, DOT, ID:absent2, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.absent1 <= $.absent2", + "json_path": "$.absent1 <= $.absent2", + "lexer_tokens": "DOLLAR, DOT, ID:absent1, LTE, DOLLAR, DOT, ID:absent2, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.absent == 'g'", + "json_path": "$.absent == 'g'", + "lexer_tokens": "DOLLAR, DOT, ID:absent, EQUAL, STRING:'g', EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.absent1 != $.absent2", + "json_path": "$.absent1 != $.absent2", + "lexer_tokens": "DOLLAR, DOT, ID:absent1, NOT_EQUAL, DOLLAR, DOT, ID:absent2, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.absent != 'g'", + "json_path": "$.absent != 'g'", + "lexer_tokens": "DOLLAR, DOT, ID:absent, NOT_EQUAL, STRING:'g', EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-1 <= 2", + "json_path": "1 <= 2", + "lexer_tokens": "INT:1, LTE, INT:2, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-1> 2", + "json_path": "1> 2", + "lexer_tokens": "INT:1, GT, INT:2, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-13 == '13'", + "json_path": "13 == '13'", + "lexer_tokens": "INT:13, EQUAL, STRING:'13', EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-'a' <= 'b'", + "json_path": "'a' <= 'b'", + "lexer_tokens": "STRING:'a', LTE, STRING:'b', EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-'a' > 'b'", + "json_path": "'a' > 'b'", + "lexer_tokens": "STRING:'a', GT, STRING:'b', EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.obj == $.arr", + "json_path": "$.obj == $.arr", + "lexer_tokens": "DOLLAR, DOT, ID:obj, EQUAL, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.obj != $.arr", + "json_path": "$.obj != $.arr", + "lexer_tokens": "DOLLAR, DOT, ID:obj, NOT_EQUAL, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.obj == $.obj", + "json_path": "$.obj == $.obj", + "lexer_tokens": "DOLLAR, DOT, ID:obj, EQUAL, DOLLAR, DOT, ID:obj, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.obj != $.obj", + "json_path": "$.obj != $.obj", + "lexer_tokens": "DOLLAR, DOT, ID:obj, NOT_EQUAL, DOLLAR, DOT, ID:obj, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.arr == $.arr", + "json_path": "$.arr == $.arr", + "lexer_tokens": "DOLLAR, DOT, ID:arr, EQUAL, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.arr != $.arr", + "json_path": "$.arr != $.arr", + "lexer_tokens": "DOLLAR, DOT, ID:arr, NOT_EQUAL, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.obj == 17", + "json_path": "$.obj == 17", + "lexer_tokens": "DOLLAR, DOT, ID:obj, EQUAL, INT:17, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.obj != 17", + "json_path": "$.obj != 17", + "lexer_tokens": "DOLLAR, DOT, ID:obj, NOT_EQUAL, INT:17, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.obj <= $.arr", + "json_path": "$.obj <= $.arr", + "lexer_tokens": "DOLLAR, DOT, ID:obj, LTE, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.obj < $.arr", + "json_path": "$.obj < $.arr", + "lexer_tokens": "DOLLAR, DOT, ID:obj, LT, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.obj <= $.obj", + "json_path": "$.obj <= $.obj", + "lexer_tokens": "DOLLAR, DOT, ID:obj, LTE, DOLLAR, DOT, ID:obj, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-$.arr <= $.arr", + "json_path": "$.arr <= $.arr", + "lexer_tokens": "DOLLAR, DOT, ID:arr, LTE, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-1 <= $.arr", + "json_path": "1 <= $.arr", + "lexer_tokens": "INT:1, LTE, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-1 >= $.arr", + "json_path": "1 >= $.arr", + "lexer_tokens": "INT:1, GTE, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-1 > $.arr", + "json_path": "1 > $.arr", + "lexer_tokens": "INT:1, GT, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-1 < $.arr", + "json_path": "1 < $.arr", + "lexer_tokens": "INT:1, LT, DOLLAR, DOT, ID:arr, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-true <= true", + "json_path": "true <= true", + "lexer_tokens": "KEYWORD:true, LTE, KEYWORD:true, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_11-true > true", + "json_path": "true > true", + "lexer_tokens": "KEYWORD:true, GT, KEYWORD:true, EOF", + "source_file_name": "table_11", + "is_invalid": false + }, + { + "test_name": "table_12-$.a[?@.b == 'kilo']", + "json_path": "$.a[?@.b == 'kilo']", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, DOT, ID:b, EQUAL, STRING:'kilo', RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.a[?(@.b == 'kilo')]", + "json_path": "$.a[?(@.b == 'kilo')]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, QMARK, LPAREN, AT, DOT, ID:b, EQUAL, STRING:'kilo', RPAREN, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.a[?@>3.5]", + "json_path": "$.a[?@>3.5]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, GT, FLOAT:3.5, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.a[?@.b]", + "json_path": "$.a[?@.b]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, DOT, ID:b, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$[?@.*]", + "json_path": "$[?@.*]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, AT, DOT, STAR, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$[?@[?@.b]]", + "json_path": "$[?@[?@.b]]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, AT, LBRACKET, QMARK, AT, DOT, ID:b, RBRACKET, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.o[?@<3, ?@<3]", + "json_path": "$.o[?@<3, ?@<3]", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, QMARK, AT, LT, INT:3, COMMA, QMARK, AT, LT, INT:3, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.a[?@<2 || @.b == \"k\"]", + "json_path": "$.a[?@<2 || @.b == \"k\"]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, LT, INT:2, OR, AT, DOT, ID:b, EQUAL, STRING:\"k\", RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.o[?@>1 && @<4]", + "json_path": "$.o[?@>1 && @<4]", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, QMARK, AT, GT, INT:1, AND, AT, LT, INT:4, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.o[?@>1 && @<4]", + "json_path": "$.o[?@>1 && @<4]", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, QMARK, AT, GT, INT:1, AND, AT, LT, INT:4, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.o[?@.u || @.x]", + "json_path": "$.o[?@.u || @.x]", + "lexer_tokens": "DOLLAR, DOT, ID:o, LBRACKET, QMARK, AT, DOT, ID:u, OR, AT, DOT, ID:x, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.a[?@.b == $.x]", + "json_path": "$.a[?@.b == $.x]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, DOT, ID:b, EQUAL, DOLLAR, DOT, ID:x, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.a[?@ == @]", + "json_path": "$.a[?@ == @]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, QMARK, AT, EQUAL, AT, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_14-$[?length(@) < 3]", + "json_path": "$[?length(@) < 3]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:length, LPAREN, AT, RPAREN, LT, INT:3, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?length(@.*) < 3]", + "json_path": "$[?length(@.*) < 3]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:length, LPAREN, AT, DOT, STAR, RPAREN, LT, INT:3, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?count(@.*) == 1]", + "json_path": "$[?count(@.*) == 1]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:count, LPAREN, AT, DOT, STAR, RPAREN, EQUAL, INT:1, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?count(1) == 1]", + "json_path": "$[?count(1) == 1]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:count, LPAREN, INT:1, RPAREN, EQUAL, INT:1, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?count(foo(@.*)) == 1]", + "json_path": "$[?count(foo(@.*)) == 1]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:count, LPAREN, ID:foo, LPAREN, AT, DOT, STAR, RPAREN, RPAREN, EQUAL, INT:1, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?match(@.timezone, 'Europe/.*')]", + "json_path": "$[?match(@.timezone, 'Europe/.*')]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:match, LPAREN, AT, DOT, ID:timezone, COMMA, STRING:'Europe/.*', RPAREN, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?match(@.timezone,'Europe/.*') == true]", + "json_path": "$[?match(@.timezone,'Europe/.*') == true]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:match, LPAREN, AT, DOT, ID:timezone, COMMA, STRING:'Europe/.*', RPAREN, EQUAL, KEYWORD:true, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?value(@..color) == \"red\"]", + "json_path": "$[?value(@..color) == \"red\"]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:value, LPAREN, AT, DOUBLE_DOT, ID:color, RPAREN, EQUAL, STRING:\"red\", RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?value(@..color)]", + "json_path": "$[?value(@..color)]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:value, LPAREN, AT, DOUBLE_DOT, ID:color, RPAREN, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?bar(@.a)]", + "json_path": "$[?bar(@.a)]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:bar, LPAREN, AT, DOT, ID:a, RPAREN, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?bnl(@.*)]", + "json_path": "$[?bnl(@.*)]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:bnl, LPAREN, AT, DOT, STAR, RPAREN, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?blt(1==1)]", + "json_path": "$[?blt(1==1)]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:blt, LPAREN, INT:1, EQUAL, INT:1, RPAREN, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?blt(1)]", + "json_path": "$[?blt(1)]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:blt, LPAREN, INT:1, RPAREN, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_14-$[?bal(1)]", + "json_path": "$[?bal(1)]", + "lexer_tokens": "DOLLAR, LBRACKET, QMARK, ID:bal, LPAREN, INT:1, RPAREN, RBRACKET, EOF", + "source_file_name": "table_14", + "is_invalid": false + }, + { + "test_name": "table_15-$[0, 3]", + "json_path": "$[0, 3]", + "lexer_tokens": "DOLLAR, LBRACKET, INT:0, COMMA, INT:3, RBRACKET, EOF", + "source_file_name": "table_15", + "is_invalid": false + }, + { + "test_name": "table_15-$[0:2, 5]", + "json_path": "$[0:2, 5]", + "lexer_tokens": "DOLLAR, LBRACKET, SLICE[0:2], COMMA, INT:5, RBRACKET, EOF", + "source_file_name": "table_15", + "is_invalid": false + }, + { + "test_name": "table_15-$[0, 0]", + "json_path": "$[0, 0]", + "lexer_tokens": "DOLLAR, LBRACKET, INT:0, COMMA, INT:0, RBRACKET, EOF", + "source_file_name": "table_15", + "is_invalid": false + }, + { + "test_name": "table_16-$..j", + "json_path": "$..j", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:j, EOF", + "source_file_name": "table_16", + "is_invalid": false + }, + { + "test_name": "table_16-$..j", + "json_path": "$..j", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:j, EOF", + "source_file_name": "table_16", + "is_invalid": false + }, + { + "test_name": "table_16-$..[0]", + "json_path": "$..[0]", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, LBRACKET, INT:0, RBRACKET, EOF", + "source_file_name": "table_16", + "is_invalid": false + }, + { + "test_name": "table_16-$..[*]", + "json_path": "$..[*]", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, LBRACKET, STAR, RBRACKET, EOF", + "source_file_name": "table_16", + "is_invalid": false + }, + { + "test_name": "table_16-$..*", + "json_path": "$..*", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, STAR, EOF", + "source_file_name": "table_16", + "is_invalid": false + }, + { + "test_name": "table_16-$..o", + "json_path": "$..o", + "lexer_tokens": "DOLLAR, DOUBLE_DOT, ID:o, EOF", + "source_file_name": "table_16", + "is_invalid": false + }, + { + "test_name": "table_16-$.o..[*, *]", + "json_path": "$.o..[*, *]", + "lexer_tokens": "DOLLAR, DOT, ID:o, DOUBLE_DOT, LBRACKET, STAR, COMMA, STAR, RBRACKET, EOF", + "source_file_name": "table_16", + "is_invalid": false + }, + { + "test_name": "table_16-$.a..[0, 1]", + "json_path": "$.a..[0, 1]", + "lexer_tokens": "DOLLAR, DOT, ID:a, DOUBLE_DOT, LBRACKET, INT:0, COMMA, INT:1, RBRACKET, EOF", + "source_file_name": "table_16", + "is_invalid": false + }, + { + "test_name": "table_17-$.a", + "json_path": "$.a", + "lexer_tokens": "DOLLAR, DOT, ID:a, EOF", + "source_file_name": "table_17", + "is_invalid": false + }, + { + "test_name": "table_17-$.a[0]", + "json_path": "$.a[0]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, INT:0, RBRACKET, EOF", + "source_file_name": "table_17", + "is_invalid": false + }, + { + "test_name": "table_17-$.a.d", + "json_path": "$.a.d", + "lexer_tokens": "DOLLAR, DOT, ID:a, DOT, ID:d, EOF", + "source_file_name": "table_17", + "is_invalid": false + }, + { + "test_name": "table_17-$.b[0]", + "json_path": "$.b[0]", + "lexer_tokens": "DOLLAR, DOT, ID:b, LBRACKET, INT:0, RBRACKET, EOF", + "source_file_name": "table_17", + "is_invalid": false + }, + { + "test_name": "table_17-$.b[*]", + "json_path": "$.b[*]", + "lexer_tokens": "DOLLAR, DOT, ID:b, LBRACKET, STAR, RBRACKET, EOF", + "source_file_name": "table_17", + "is_invalid": false + }, + { + "test_name": "table_17-$.b[?@]", + "json_path": "$.b[?@]", + "lexer_tokens": "DOLLAR, DOT, ID:b, LBRACKET, QMARK, AT, RBRACKET, EOF", + "source_file_name": "table_17", + "is_invalid": false + }, + { + "test_name": "table_17-$.b[?@==null]", + "json_path": "$.b[?@==null]", + "lexer_tokens": "DOLLAR, DOT, ID:b, LBRACKET, QMARK, AT, EQUAL, KEYWORD:null, RBRACKET, EOF", + "source_file_name": "table_17", + "is_invalid": false + }, + { + "test_name": "table_17-$.c[?@.d==null]", + "json_path": "$.c[?@.d==null]", + "lexer_tokens": "DOLLAR, DOT, ID:c, LBRACKET, QMARK, AT, DOT, ID:d, EQUAL, KEYWORD:null, RBRACKET, EOF", + "source_file_name": "table_17", + "is_invalid": false + }, + { + "test_name": "table_17-$.null", + "json_path": "$.null", + "lexer_tokens": "DOLLAR, DOT, KEYWORD:null, EOF", + "source_file_name": "table_17", + "is_invalid": false + }, + { + "test_name": "table_18-$.a", + "json_path": "$.a", + "lexer_tokens": "DOLLAR, DOT, ID:a, EOF", + "source_file_name": "table_18", + "is_invalid": false + }, + { + "test_name": "table_18-$[1]", + "json_path": "$[1]", + "lexer_tokens": "DOLLAR, LBRACKET, INT:1, RBRACKET, EOF", + "source_file_name": "table_18", + "is_invalid": false + }, + { + "test_name": "table_18-$[-3]", + "json_path": "$[-3]", + "lexer_tokens": "DOLLAR, LBRACKET, INT:-3, RBRACKET, EOF", + "source_file_name": "table_18", + "is_invalid": false + }, + { + "test_name": "table_18-$.a.b[1:2]", + "json_path": "$.a.b[1:2]", + "lexer_tokens": "DOLLAR, DOT, ID:a, DOT, ID:b, LBRACKET, SLICE[1:2], RBRACKET, EOF", + "source_file_name": "table_18", + "is_invalid": false + }, + { + "test_name": "table_18-$[\"\\u000B\"]", + "json_path": "$[\"\\u000B\"]", + "lexer_tokens": "DOLLAR, LBRACKET, STRING:\"\\u000B\", RBRACKET, EOF", + "source_file_name": "table_18", + "is_invalid": false + }, + { + "test_name": "table_18-$[\"\\u0061\"]", + "json_path": "$[\"\\u0061\"]", + "lexer_tokens": "DOLLAR, LBRACKET, STRING:\"\\u0061\", RBRACKET, EOF", + "source_file_name": "table_18", + "is_invalid": false + } + ] +} \ No newline at end of file diff --git a/tests/jpath/lexing/test_lex_rfc_examples.py b/tests/jpath/lexing/test_lex_rfc_examples.py deleted file mode 100644 index 83d9b23..0000000 --- a/tests/jpath/lexing/test_lex_rfc_examples.py +++ /dev/null @@ -1,57 +0,0 @@ - -# File: test_rfc_examples.py -# Copyright (c) 2025 Robert L. Ross -# All rights reserved. -# Open-source license to come. -# Created by: Robert L. Ross -# - -"""Test lexing against json path query strings from the tables in RFC 9535.""" -from pathlib import Path -from typing import NamedTuple, Generator - -import pytest - -from killerbunny.lexing.lexer import JPathLexer -from killerbunny.lexing.tokens import Token -from killerbunny.shared.constants import ONE_MEBIBYTE, UTF8, JPATH_DATA_SEPARATOR -from killerbunny.shared.errors import Error - -_MODULE_DIR = Path(__file__).parent -_TEST_FILE_DIR = _MODULE_DIR / "jpath_token_files" - -class PathTokens(NamedTuple): - file_name: str - jpath: str - tokens_str: str - -def tokens_to_str(tokens: list[Token], error: Error | None) -> str: - result_str: str = "" - if error: - result_str = error.as_test_string() - else: - if tokens: - result_str = ', '.join( t.__testrepr__() for t in tokens) - return result_str - -def jpath_tokens_data() -> Generator[ PathTokens, None, None] : - for file in _TEST_FILE_DIR.iterdir(): - if file.suffix == ".jpath_tokens": - with open(file, "r", encoding=UTF8, buffering=ONE_MEBIBYTE) as input_file: - for line in input_file: - line_stripped = line.strip() - if line_stripped == '' or line_stripped.startswith('#'): - continue - path_tokens = PathTokens(file.name, *line_stripped.split(JPATH_DATA_SEPARATOR)) - yield path_tokens - -@pytest.mark.parametrize("file_name, jpath, tokens_str", jpath_tokens_data()) -def test_rfc_example(file_name: str, jpath: str, tokens_str: str) -> None: - expected = tokens_str - lexer = JPathLexer(file_name, jpath) - tokens, error = lexer.tokenize() - assert error is None - actual = tokens_to_str(tokens, error) - assert actual == expected, f"{jpath} should produce: {expected}" - - \ No newline at end of file diff --git a/tests/jpath/lexing/test_lexer_rfc9535_tables.py b/tests/jpath/lexing/test_lexer_rfc9535_tables.py new file mode 100644 index 0000000..9edf945 --- /dev/null +++ b/tests/jpath/lexing/test_lexer_rfc9535_tables.py @@ -0,0 +1,77 @@ +# File: test_lexer_rfc9535_tables.py +# Copyright (c) 2025 Robert L. Ross +# All rights reserved. +# Open-source license to come. +# Created by: Robert L. Ross +# +import json +import operator +from dataclasses import dataclass +from pathlib import Path + +import pytest + +from killerbunny.lexing.lexer import JPathLexer +from killerbunny.lexing.tokens import Token +from killerbunny.shared.constants import UTF8, ONE_MEBIBYTE +from killerbunny.shared.errors import Error + +_MODULE_DIR = Path(__file__).parent +_LTC_FILE_PATH = _MODULE_DIR / "lexer_tests.json" +_FILE_LIST = [ _LTC_FILE_PATH,] + +@dataclass(frozen=True, slots=True) +class LexerTestCase: + test_name : str + json_path : str + lexer_tokens : str + source_file_name : str + is_invalid : bool = False + + +def data_loader() -> list[LexerTestCase]: + test_data: list[LexerTestCase] = [] + for file_name in _FILE_LIST: + file_path = _MODULE_DIR / file_name + with open( file_path , encoding=UTF8, buffering=ONE_MEBIBYTE) as input_file: + data = json.load(input_file) + test_data.extend( [ LexerTestCase(**test) for test in data["tests"] ] ) + return test_data + +def valid_paths() -> list[LexerTestCase]: + return [ test for test in data_loader() if not test.is_invalid ] + +def invalid_paths() -> list[LexerTestCase]: + return [ test for test in data_loader() if test.is_invalid ] + +def tokens_to_str(tokens: list[Token], error: Error | None) -> str: + result_str: str = "" + if error: + result_str = error.as_test_string() + else: + if tokens: + result_str = ', '.join( t.__testrepr__() for t in tokens) + return result_str + +EXCLUDED_TESTS_MAP: dict[str, tuple[str,str]] = {} +# during debugging of test cases, print debug info for the test names in this set +DEBUG_TEST_NAMES: set[str] = set() + +@pytest.mark.parametrize("case", valid_paths(), ids=operator.attrgetter("test_name")) +def test_lexer_valid_cases(case: LexerTestCase ) -> None: + """Test the cases in the lexer_tests file that are intended to be syntactially correct and should return a result. """ + if case.test_name in EXCLUDED_TESTS_MAP: + pytest.skip(reason=f"{EXCLUDED_TESTS_MAP[case.test_name][1]}: '{case.test_name}'") + + if case.test_name in DEBUG_TEST_NAMES: + print(f"\n* * * * * test: '{case.test_name}', json_path: {case.json_path}, expected: {case.lexer_tokens}") + + assert case.json_path is not None + lexer = JPathLexer(case.source_file_name, case.json_path) + tokens, error = lexer.tokenize() + assert error is None + + expected = case.lexer_tokens + actual = tokens_to_str(tokens, error) + + assert actual == expected, f"{case.json_path} should produce: {expected}" \ No newline at end of file From d18c238259a71aaede6cd2a4f85a6bca711da6ff Mon Sep 17 00:00:00 2001 From: killeroonie Date: Fri, 27 Jun 2025 22:31:34 -0700 Subject: [PATCH 5/9] Renamed from lexer_tests.json to lexer_test_cases.json for clarity. --- tests/jpath/lexing/{lexer_tests.json => lexer_test_cases.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/jpath/lexing/{lexer_tests.json => lexer_test_cases.json} (100%) diff --git a/tests/jpath/lexing/lexer_tests.json b/tests/jpath/lexing/lexer_test_cases.json similarity index 100% rename from tests/jpath/lexing/lexer_tests.json rename to tests/jpath/lexing/lexer_test_cases.json From 88eecb580e386a37a54693b9fbb68f2cf28fcb3a Mon Sep 17 00:00:00 2001 From: killeroonie Date: Fri, 27 Jun 2025 22:31:48 -0700 Subject: [PATCH 6/9] Renamed from lexer_tests.json to lexer_test_cases.json for clarity. --- killerbunny/shared/testgen.py | 4 +++- tests/jpath/lexing/test_lexer_rfc9535_tables.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/killerbunny/shared/testgen.py b/killerbunny/shared/testgen.py index 54da608..98296de 100644 --- a/killerbunny/shared/testgen.py +++ b/killerbunny/shared/testgen.py @@ -115,6 +115,8 @@ def generate_lexer_test_cases(path_tokens: list[ tuple[ str, str] ], FRAGILE_TEST_DIR = _MODULE_DIR / '../../tests/' FRAGILE_TEST_DIR_PATH = Path(FRAGILE_TEST_DIR) +LEXER_TEST_CASES_FILENAME = "lexer_test_cases.json" + def process_lexer_paths(input_dir: Path, suffix: str, generate_test_file: bool = False, quiet:bool = False)-> None: """Given a directory path and a file suffix, run generate_lexer_path_tokens() for each file in the directory that ends with `suffix`. No directory recursion is done. @@ -162,7 +164,7 @@ def process_lexer_paths(input_dir: Path, suffix: str, generate_test_file: bool = if generate_test_file and test_cases: - outfile_path = output_dir / "lexer_tests.json" + outfile_path = output_dir / LEXER_TEST_CASES_FILENAME if not quiet: print(f"Generating test file '{outfile_path}'") generated_files.append( outfile_path.name ) tests_list: list[dict[str, Any]] = [] diff --git a/tests/jpath/lexing/test_lexer_rfc9535_tables.py b/tests/jpath/lexing/test_lexer_rfc9535_tables.py index 9edf945..eb870d5 100644 --- a/tests/jpath/lexing/test_lexer_rfc9535_tables.py +++ b/tests/jpath/lexing/test_lexer_rfc9535_tables.py @@ -17,7 +17,8 @@ from killerbunny.shared.errors import Error _MODULE_DIR = Path(__file__).parent -_LTC_FILE_PATH = _MODULE_DIR / "lexer_tests.json" +LEXER_TEST_CASES_FILENAME = "lexer_test_cases.json" +_LTC_FILE_PATH = _MODULE_DIR / LEXER_TEST_CASES_FILENAME _FILE_LIST = [ _LTC_FILE_PATH,] @dataclass(frozen=True, slots=True) From b009028d6eba2f46d7169ab42657ffc025d087d9 Mon Sep 17 00:00:00 2001 From: killeroonie Date: Sat, 28 Jun 2025 03:30:07 -0700 Subject: [PATCH 7/9] Refactored parser unit tests. --- killerbunny/shared/testgen.py | 243 +++-- .../jpath_parser_files/table_02.jpath_ast | 19 - .../jpath_parser_files/table_03.jpath_ast | 2 - .../jpath_parser_files/table_05.jpath_ast | 6 - .../jpath_parser_files/table_06.jpath_ast | 6 - .../jpath_parser_files/table_07.jpath_ast | 3 - .../jpath_parser_files/table_09.jpath_ast | 6 - .../jpath_parser_files/table_11.jpath_ast | 29 - .../jpath_parser_files/table_12.jpath_ast | 14 - .../jpath_parser_files/table_15.jpath_ast | 4 - .../jpath_parser_files/table_16.jpath_ast | 9 - .../jpath_parser_files/table_17.jpath_ast | 10 - .../jpath_parser_files/table_18.jpath_ast | 7 - tests/jpath/parsing/parser_test_cases.json | 901 ++++++++++++++++++ .../parsing/subparse_test_cases_table_11.json | 257 +++++ .../jpath/parsing/test_parse_rfc_examples.py | 65 -- .../parsing/test_parser_rfc9535_tables.py | 101 ++ tests/jpath/parsing/test_subparsing.py | 124 +++ 18 files changed, 1558 insertions(+), 248 deletions(-) delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_02.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_03.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_05.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_06.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_07.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_09.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_11.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_12.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_15.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_16.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_17.jpath_ast delete mode 100644 tests/jpath/parsing/jpath_parser_files/table_18.jpath_ast create mode 100644 tests/jpath/parsing/parser_test_cases.json create mode 100644 tests/jpath/parsing/subparse_test_cases_table_11.json delete mode 100644 tests/jpath/parsing/test_parse_rfc_examples.py create mode 100644 tests/jpath/parsing/test_parser_rfc9535_tables.py create mode 100644 tests/jpath/parsing/test_subparsing.py diff --git a/killerbunny/shared/testgen.py b/killerbunny/shared/testgen.py index 98296de..0408dc2 100644 --- a/killerbunny/shared/testgen.py +++ b/killerbunny/shared/testgen.py @@ -9,7 +9,7 @@ """Functions for creating test data files for the Lexer, Parser, and Evaluator unit tests""" import json -from dataclasses import dataclass, asdict +from dataclasses import dataclass, asdict, is_dataclass from pathlib import Path from typing import cast, Any @@ -17,11 +17,13 @@ from killerbunny.evaluating.runtime_result import RuntimeResult from killerbunny.evaluating.value_nodes import VNodeList from killerbunny.lexing.lexer import JPathLexer +from killerbunny.lexing.tokens import Token from killerbunny.parsing.node_type import ASTNode from killerbunny.parsing.parse_result import ParseResult from killerbunny.parsing.parser import JPathParser from killerbunny.shared.constants import UTF8, ONE_MEBIBYTE, JPATH_DATA_SEPARATOR, ROOT_JSON_VALUE_KEY from killerbunny.shared.context import Context +from killerbunny.shared.errors import Error from killerbunny.shared.json_type_defs import JSON_ValueType """ @@ -44,6 +46,12 @@ _MODULE_DIR = Path(__file__).parent +FRAGILE_TEST_DIR = _MODULE_DIR / '../../tests/' +FRAGILE_TEST_DIR_PATH = Path(FRAGILE_TEST_DIR) +LEXER_TEST_CASES_FILENAME = "lexer_test_cases.json" +PARSER_TEST_CASES_FILENAME = "parser_test_cases.json" + + def load_obj_from_json_file(input_file: Path) -> JSON_ValueType: """Return the JSON object from the JSON file in the argument. Intended for a JSON file with a single object for testing and debugging. """ @@ -51,6 +59,17 @@ def load_obj_from_json_file(input_file: Path) -> JSON_ValueType: json_str = in_file.read() return cast(JSON_ValueType, json.loads(json_str)) + +def write_test_case_file(outfile_path: Path, description: str, test_cases: list[Any]) -> None: + tests_list: list[dict[str, Any]] = [] + test_file_dict = {"description":f"{description} This file is autogenerated, do not edit.", + "tests": tests_list } + for test_case in test_cases: + tests_list.append(asdict(test_case)) + + with open(outfile_path, "x", encoding=UTF8, buffering=ONE_MEBIBYTE) as outfile: + json.dump(test_file_dict, outfile, ensure_ascii=False, indent=4) + #################################################################### # LEXER TEST GENERATION #################################################################### @@ -86,7 +105,7 @@ def generate_lexer_path_tokens(input_path: Path) -> list[ tuple[ str, str]]: This method returns a list of two-item tuples. The first tuple item is the original JSON Path query string. The second tuple item is a string representation for the list of generated tokens, or a lexer error message. The string representation of each token is generated by calling __testrepr__() on each token. - Lines that start with a # (line comment) are ignored, as well as blank lines. + Lines in the input file that start with a # (line comment) are ignored, as well as blank lines. Note that # is a valid character that may be part of a member name or search string, so end-of-line comments are not supported. """ @@ -112,23 +131,17 @@ def generate_lexer_test_cases(path_tokens: list[ tuple[ str, str] ], return result_list -FRAGILE_TEST_DIR = _MODULE_DIR / '../../tests/' -FRAGILE_TEST_DIR_PATH = Path(FRAGILE_TEST_DIR) - -LEXER_TEST_CASES_FILENAME = "lexer_test_cases.json" - def process_lexer_paths(input_dir: Path, suffix: str, generate_test_file: bool = False, quiet:bool = False)-> None: """Given a directory path and a file suffix, run generate_lexer_path_tokens() for each file in the directory - that ends with `suffix`. No directory recursion is done. + that ends with `suffix`. No directory recursion is done. Each input file contains a list of JSON Path query strings. This is a utility function to help create test data for lexer unit testing. This should be used with caution to avoid accidentally ovewriting existing test files. However, the open() call for creating the test file uses the 'x' mode flag, so it will report an error if the file already exists. If `generate_test_file` is True, create a JSON test file containing each matching input file's test data - and save it as: tests/jpath/lexing/lexer_tests.json. - Test files contain multiple test cases containing a JSON Path query string and the string representation - for the list of generated tokens. + and save it as: tests/jpath/lexing/lexer_test_cases.json. + If `quiet` is True, omits writing most output to the console, except for warning or error messages. """ if not quiet: @@ -162,7 +175,6 @@ def process_lexer_paths(input_dir: Path, suffix: str, generate_test_file: bool = if generate_test_file: test_cases.extend(generate_lexer_test_cases(file_results, str(file.stem))) - if generate_test_file and test_cases: outfile_path = output_dir / LEXER_TEST_CASES_FILENAME if not quiet: print(f"Generating test file '{outfile_path}'") @@ -187,89 +199,185 @@ def process_lexer_paths(input_dir: Path, suffix: str, generate_test_file: bool = # PARSER TEST GENERATION #################################################################### -def parse_jpath_str(file_name:str, jpath_query_str: str) -> str: - """Lex and parse the JSON Path query string and return a string representation of the generated AST from the parser.""" +@dataclass(frozen=True, slots=True) +class ParserTestCase: + test_name : str + json_path : str + parser_ast : str + source_file_name : str + is_invalid : bool = False + err_msg : str = "" + subparse_production: str| None = None + + +def lex(file_name:str, jpath_query_str: str) -> tuple[ list[Token], Error | None]: lexer = JPathLexer(file_name, jpath_query_str) - tokens, error = lexer.tokenize() + return lexer.tokenize() + +def parse_jpath_str(file_name:str, jpath_query_str: str) -> ParserTestCase: + """Lex and parse the JSON Path query string and return a ParserTestCase representing the query string and + a string representation of the generated AST from the parser. Set is_invalid to True if parsing results in + an error. """ + tokens, error = lex(file_name, jpath_query_str) + test_name = f"{file_name}-{jpath_query_str}" if error: - result_str = error.as_test_string() - return result_str + err_msg = error.as_test_string() + return ParserTestCase(test_name, jpath_query_str, "", file_name, True, err_msg) parser = JPathParser(tokens) result: ParseResult = parser.parse() if result.error: - result_str = result.error.as_test_string() - return result_str + err_msg = result.error.as_test_string() + return ParserTestCase(test_name, jpath_query_str, "", file_name, True, err_msg) ast: ASTNode| None = result.node - return str(ast) + ast_str = str(ast) + return ParserTestCase(test_name, jpath_query_str, ast_str, file_name, False, '') -def generate_parser_path_values(input_path: Path) -> list[ tuple[ str, str] ]: - result_list: list[ tuple[ str, str] ] = [] - file_name: str = input_path.name +def subparse_jpath_str(file_name:str, jpath_query_str: str, production_name: str) -> ParserTestCase: + tokens, error = lex(file_name, jpath_query_str) + test_name = f"{file_name}-{jpath_query_str}" + if error: + err_msg = error.as_test_string() + return ParserTestCase(test_name, jpath_query_str, "", file_name, True, err_msg, production_name) + + parser = JPathParser(tokens) + result: tuple[ list[ tuple[str, ASTNode] ] , list[ tuple[str, Error] ] ] = parser.subparse(production_name) + node_list = result[0] + ast_str = "" + err_list = result[1] + err_msg = "" + for name, error in err_list: + if name == production_name: + err_msg = error.as_test_string() + + if err_msg: + return ParserTestCase(test_name, jpath_query_str, "", file_name, True, err_msg, production_name) + + for name, ast_node in node_list: + if name == production_name: + ast_str = str(ast_node) + + return ParserTestCase(test_name, jpath_query_str, ast_str, file_name, False, '', production_name) + +def generate_parser_test_cases(input_path: Path) -> list[ ParserTestCase]: + """For each line in the input file of JSON Path query strings, return a ParserTestCase + + Each line of the input file is a JSON Path query string. This method will pass each string to parse_jpath_str and + receive from it a ParserTestCase containing a representation of the generated ASTs corresponding to the input string, + or an error message if the parser failed to parse the input string. + + Lines in the input file that start with a # (line comment) are ignored, as well as blank lines. + Note that # is a valid character that may be part of a member name or search string, so end-of-line comments + are not supported. + """ + result_list: list[ ParserTestCase ] = [] + file_name: str = input_path.stem with open(input_path, "r", encoding=UTF8, buffering=ONE_MEBIBYTE) as input_file: for line in input_file: line_stripped = line.strip() if line_stripped == '' or line_stripped.startswith("#"): continue # ignore comment lines or blank lines - result_str: str = parse_jpath_str(file_name, line_stripped) - result_list.append( (line_stripped, result_str) ) + result_list.append( parse_jpath_str(file_name, line_stripped) ) return result_list +def generate_subparser_test_cases(input_path: Path, + output_path:Path, + production_name: str, + generate_test_file = False, + quiet: bool = False) -> None: + """Generate a test case for the input file by calling subparse(production_name) """ + test_cases: list[ ParserTestCase] = [] + file_name: str = input_path.stem + with open(input_path, "r", encoding=UTF8, buffering=ONE_MEBIBYTE) as input_file: + for line in input_file: + line_stripped = line.strip() + if line_stripped == '' or line_stripped.startswith("#"): + continue # ignore comment lines or blank lines + test_cases.append(subparse_jpath_str(file_name, line_stripped, production_name)) + + if not quiet: + display_test_cases(test_cases, str(input_path)) + + if generate_test_file and test_cases: + if not quiet: print(f"Generating test file '{output_path}'") + write_test_case_file(output_path, "Parser subparse tests of example paths in RFC 9535 table 11.", test_cases) + + + +def display_test_cases(test_cases: list[ Any ], file_name: str) -> None: + """Display the generated test cases during test generation""" + if test_cases: + print(f"{file_name} results:") + for test_case in test_cases: + if is_dataclass(test_case): + dict_ = asdict(test_case) + value = dict_.get("lexer_tokens", None) or dict_.get("parser_ast", None) or '' + msg = dict_["err_msg"] if dict_["is_invalid"] else value + print(f"{dict_['json_path']}{JPATH_DATA_SEPARATOR}{msg}") + else: + jpath, expected = test_case + print(f"{jpath}{JPATH_DATA_SEPARATOR}{expected}") + print("-" * 40) def process_parser_paths(input_dir: Path, suffix: str, output_dir: Path | None = None, - generate_test_files: bool = False, + generate_test_file: bool = False, quiet:bool = False)-> None: + """Given a directory path and a file suffix, run generate_parser_path_asts() for each file in the directory + that ends with `suffix`. No directory recursion is done. Each input file contains a list of JSON Path query strings. + + This is a utility function to help create test data for parser unit testing. This should be used with caution to avoid + accidentally ovewriting existing test files. However, the open() call for creating the test file uses the 'x' mode + flag, so it will report an error if the file already exists. + + If `generate_test_file` is True, create a JSON test file containing each matching input file's test data + and save it as: tests/jpath/parsing/parser_test_cases.json. - #output_dir = FRAGILE_TEST_DIR_PATH / "incubator/jpath" / "lexing" / "jpath_token_files" + If `quiet` is True, omits writing most output to the console, except for warning or error messages. + """ if not quiet: print(f"*** Processing jpathl files for the Parser") if not input_dir.is_dir(): raise FileNotFoundError(f'Input directory {input_dir} does not exist') - if generate_test_files and output_dir is None: + if generate_test_file and output_dir is None: raise ValueError(f"`generate_test_files` is True yet no output directory was specified.") - if generate_test_files and output_dir is not None: + if generate_test_file and output_dir is not None: # ensure test directory exists output_dir.mkdir(parents=True, exist_ok=True) if not quiet: - print() - print(f"Generating test files in {output_dir}") + print(f"\nGenerating test files in {output_dir}") generated_files: list[str] = [] - for file in input_dir.iterdir(): + test_cases: list[ParserTestCase] = [] + for file in sorted(input_dir.iterdir()): if file.name.endswith(suffix): if not quiet: - print() - print(f"Processing '{file}'") - file_results: list[ tuple[ str, str] ] = generate_parser_path_values(file) + print(f"\nProcessing '{file}'") + file_results: list[ ParserTestCase ] = generate_parser_test_cases(file) if not file_results: print(f"Warning: file '{file}' produced no results. Is it empty?") continue + + if not quiet: + display_test_cases(file_results, f"{input_dir.name}/{file.name}") - if file_results and not quiet: - print(f"{input_dir.name}/{file.name} results:") - for jpath, ast in file_results: - print(f"{jpath}{JPATH_DATA_SEPARATOR}{ast}") - print("-" * 40) - - if generate_test_files and output_dir: - outfile_path = output_dir / ( file.stem + ".jpath_ast" ) - if not quiet: print(f"Generating test file '{outfile_path}'") - generated_files.append( outfile_path.name ) - with open(outfile_path, "x", encoding=UTF8, buffering=ONE_MEBIBYTE) as outfile: - outfile.write(f"# file_name: {outfile_path.name}\n") - for jpath, ast in file_results: - outfile.write(f"{jpath}{JPATH_DATA_SEPARATOR}{ast}\n") + if generate_test_file: + test_cases.extend(file_results) + + if generate_test_file and test_cases: + outfile_path = output_dir / PARSER_TEST_CASES_FILENAME # type: ignore + if not quiet: print(f"Generating test file '{outfile_path}'") + generated_files.append( outfile_path.name ) + write_test_case_file(outfile_path, "Parser tests of example paths in RFC 9535 tables 2-18.", test_cases) if generated_files and not quiet: - print() - print(f"Generated a total of {len(generated_files)} parser test files:") + print(f"\nGenerated a total of {len(generated_files)} parser test files:") for line in sorted(generated_files): print(f" {line}" ) @@ -322,19 +430,6 @@ def generate_evaluator_path_nodelist(input_path: Path, json_value: JSON_ValueTyp ################################################################################################################## -def t1()-> None: - """TEMP CODE""" - file1 = Path(FRAGILE_TEST_DIR) / "jpath/rfc9535_examples/table_02.jpathl" - json_file = Path(FRAGILE_TEST_DIR) / "jpath/rfc9535_examples/figure.1.json" - json_value: JSON_ValueType = load_obj_from_json_file(json_file) - result = generate_evaluator_path_nodelist(file1,json_value) - for jpath, nodelist in result: - print(f"{jpath}") - for node in nodelist: - print(f"{node}") - print() - - def rename_file_suffix(input_dir: Path, from_suffix: str, to_suffix: str)-> None: """ TEMP CODE """ if not input_dir.is_dir(): @@ -351,18 +446,30 @@ def create_lexer_test_files() -> None: def create_parser_test_files() -> None: """PRODUCTION CODE""" input_dir = FRAGILE_TEST_DIR_PATH / "jpath/rfc9535_examples/" - output_dir = FRAGILE_TEST_DIR_PATH / "jpath/parsing/jpath_parser_files" - process_parser_paths(input_dir=input_dir, suffix="jpathl", output_dir=output_dir, generate_test_files=True) + output_dir = FRAGILE_TEST_DIR_PATH / "jpath/parsing/" + process_parser_paths(input_dir=input_dir, suffix="jpathl", output_dir=output_dir, generate_test_file=True) + +def create_subparser_test_files() -> None: + """PRODUCTION CODE""" + input_path = FRAGILE_TEST_DIR_PATH / "jpath/rfc9535_examples/table_11.jpathl" + output_path = FRAGILE_TEST_DIR_PATH / "jpath/parsing/subparse_test_cases_table_11.json" + generate_subparser_test_cases( + input_path=input_path, + output_path=output_path, + production_name="comparison_expr", + generate_test_file=True + ) def create_all_test_files() -> None: """PRODUCTION CODE""" create_lexer_test_files() create_parser_test_files() + create_subparser_test_files() def main() -> None: - # t1() - create_lexer_test_files() + #create_lexer_test_files() #create_parser_test_files() + create_subparser_test_files() if __name__ == '__main__': main() diff --git a/tests/jpath/parsing/jpath_parser_files/table_02.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_02.jpath_ast deleted file mode 100644 index cd7f827..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_02.jpath_ast +++ /dev/null @@ -1,19 +0,0 @@ -# file_name: table_02.jpath_ast -# Copyright (c) 2025 Robert L. Ross -# All rights reserved. -# Open-source license to come. -# Created by: Robert L. Ross -# -$.store.book[*].author : ${CS{bs[ns:store]/bs}/CS, CS{bs[ns:book]/bs}/CS, CS{bs[*]/bs}/CS, CS{bs[ns:author]/bs}/CS}/$ -$..author : ${DS{bs[ns:author]/bs}/DS}/$ -$.store.* : ${CS{bs[ns:store]/bs}/CS, CS{bs[*]/bs}/CS}/$ -$.store..price : ${CS{bs[ns:store]/bs}/CS, DS{bs[ns:price]/bs}/DS}/$ -$..book[2] : ${DS{bs[ns:book]/bs}/DS, CS{bs[is:2]/bs}/CS}/$ -$..book[2].author : ${DS{bs[ns:book]/bs}/DS, CS{bs[is:2]/bs}/CS, CS{bs[ns:author]/bs}/CS}/$ -$..book[2].publisher : ${DS{bs[ns:book]/bs}/DS, CS{bs[is:2]/bs}/CS, CS{bs[ns:publisher]/bs}/CS}/$ -$..book[-1] : ${DS{bs[ns:book]/bs}/DS, CS{bs[is:-1]/bs}/CS}/$ -$..book[0,1] : ${DS{bs[ns:book]/bs}/DS, CS{bs[is:0, is:1]/bs}/CS}/$ -$..book[:2] : ${DS{bs[ns:book]/bs}/DS, CS{bs[slice(:2:)]/bs}/CS}/$ -$..book[?@.isbn] : ${DS{bs[ns:book]/bs}/DS, CS{bs[fs{?@ segments}/fs]/bs}/CS}/$ -$..book[?@.price<10] : ${DS{bs[ns:book]/bs}/DS, CS{bs[fs{?comp_expr(@{sqs{ns:price}}/@, <, 10)}/fs]/bs}/CS}/$ -$..* : ${DS{bs[*]/bs}/DS}/$ diff --git a/tests/jpath/parsing/jpath_parser_files/table_03.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_03.jpath_ast deleted file mode 100644 index 5984bf2..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_03.jpath_ast +++ /dev/null @@ -1,2 +0,0 @@ -# file_name: table_03.jpath_ast -$ : $ diff --git a/tests/jpath/parsing/jpath_parser_files/table_05.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_05.jpath_ast deleted file mode 100644 index a418321..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_05.jpath_ast +++ /dev/null @@ -1,6 +0,0 @@ -# file_name: table_05.jpath_ast -$.o['j j'] : ${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS}/$ -$.o['j j']['k.k'] : ${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS, CS{bs[ns:k.k]/bs}/CS}/$ -$.o["j j"]["k.k"] : ${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS, CS{bs[ns:k.k]/bs}/CS}/$ -$['o']['j j']['k.k'] : ${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS, CS{bs[ns:k.k]/bs}/CS}/$ -$["'"]["@"] : ${CS{bs[ns:']/bs}/CS, CS{bs[ns:@]/bs}/CS}/$ diff --git a/tests/jpath/parsing/jpath_parser_files/table_06.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_06.jpath_ast deleted file mode 100644 index 57f0acb..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_06.jpath_ast +++ /dev/null @@ -1,6 +0,0 @@ -# file_name: table_06.jpath_ast -$[*] : ${CS{bs[*]/bs}/CS}/$ -$.o[*] : ${CS{bs[ns:o]/bs}/CS, CS{bs[*]/bs}/CS}/$ -$.o[*] : ${CS{bs[ns:o]/bs}/CS, CS{bs[*]/bs}/CS}/$ -$.o[*, *] : ${CS{bs[ns:o]/bs}/CS, CS{bs[*, *]/bs}/CS}/$ -$.a[*] : ${CS{bs[ns:a]/bs}/CS, CS{bs[*]/bs}/CS}/$ diff --git a/tests/jpath/parsing/jpath_parser_files/table_07.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_07.jpath_ast deleted file mode 100644 index b75c07c..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_07.jpath_ast +++ /dev/null @@ -1,3 +0,0 @@ -# file_name: table_07.jpath_ast -$[1] : ${CS{bs[is:1]/bs}/CS}/$ -$[-2] : ${CS{bs[is:-2]/bs}/CS}/$ diff --git a/tests/jpath/parsing/jpath_parser_files/table_09.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_09.jpath_ast deleted file mode 100644 index 50b3323..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_09.jpath_ast +++ /dev/null @@ -1,6 +0,0 @@ -# file_name: table_09.jpath_ast -$[1:3] : ${CS{bs[slice(1:3:)]/bs}/CS}/$ -$[5:] : ${CS{bs[slice(5::)]/bs}/CS}/$ -$[1:5:2] : ${CS{bs[slice(1:5:2)]/bs}/CS}/$ -$[5:1:-2] : ${CS{bs[slice(5:1:-2)]/bs}/CS}/$ -$[::-1] : ${CS{bs[slice(::-1)]/bs}/CS}/$ diff --git a/tests/jpath/parsing/jpath_parser_files/table_11.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_11.jpath_ast deleted file mode 100644 index edd103d..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_11.jpath_ast +++ /dev/null @@ -1,29 +0,0 @@ -# file_name: table_11.jpath_ast -$.absent1 == $.absent2 : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 11: $.absent1 ^==^ $.absent2 -$.absent1 <= $.absent2 : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 11: $.absent1 ^<=^ $.absent2 -$.absent == 'g' : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 10: $.absent ^==^ 'g' -$.absent1 != $.absent2 : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 11: $.absent1 ^!=^ $.absent2 -$.absent != 'g' : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 10: $.absent ^!=^ 'g' -1 <= 2 : Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ <= 2 -1> 2 : Invalid Syntax: start: Expected '$', got INT at position 1: ^1^> 2 -13 == '13' : Invalid Syntax: start: Expected '$', got INT at position 1: ^13^ == '13' -'a' <= 'b' : Invalid Syntax: start: Expected '$', got STRING at position 1: ^'a'^ <= 'b' -'a' > 'b' : Invalid Syntax: start: Expected '$', got STRING at position 1: ^'a'^ > 'b' -$.obj == $.arr : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ $.arr -$.obj != $.arr : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ $.arr -$.obj == $.obj : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ $.obj -$.obj != $.obj : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ $.obj -$.arr == $.arr : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.arr ^==^ $.arr -$.arr != $.arr : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.arr ^!=^ $.arr -$.obj == 17 : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ 17 -$.obj != 17 : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ 17 -$.obj <= $.arr : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.obj ^<=^ $.arr -$.obj < $.arr : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got < at position 7: $.obj ^<^ $.arr -$.obj <= $.obj : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.obj ^<=^ $.obj -$.arr <= $.arr : Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.arr ^<=^ $.arr -1 <= $.arr : Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ <= $.arr -1 >= $.arr : Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ >= $.arr -1 > $.arr : Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ > $.arr -1 < $.arr : Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ < $.arr -true <= true : Invalid Syntax: start: Expected '$', got true at position 1: ^true^ <= true -true > true : Invalid Syntax: start: Expected '$', got true at position 1: ^true^ > true diff --git a/tests/jpath/parsing/jpath_parser_files/table_12.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_12.jpath_ast deleted file mode 100644 index e900e88..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_12.jpath_ast +++ /dev/null @@ -1,14 +0,0 @@ -# file_name: table_12.jpath_ast -$.a[?@.b == 'kilo'] : ${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:b}}/@, ==, kilo)}/fs]/bs}/CS}/$ -$.a[?(@.b == 'kilo')] : ${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:b}}/@, ==, kilo)}/fs]/bs}/CS}/$ -$.a[?@>3.5] : ${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@, >, 3.5)}/fs]/bs}/CS}/$ -$.a[?@.b] : ${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?@ segments}/fs]/bs}/CS}/$ -$[?@.*] : ${CS{bs[fs{?@ segments}/fs]/bs}/CS}/$ -$[?@[?@.b]] : ${CS{bs[fs{?@ segments}/fs]/bs}/CS>}/fs]/bs}/CS}/$ -$.o[?@<3, ?@<3] : ${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?comp_expr(@, <, 3)}/fs, fs{?comp_expr(@, <, 3)}/fs]/bs}/CS}/$ -$.a[?@<2 || @.b == "k"] : ${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?logical_or_expr[comp_expr(@, <, 2), comp_expr(@{sqs{ns:b}}/@, ==, k)]/logical_or_expr}/fs]/bs}/CS}/$ -$.o[?@>1 && @<4] : ${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?logical_and_expr[comp_expr(@, >, 1), comp_expr(@, <, 4)]/logical_and_expr}/fs]/bs}/CS}/$ -$.o[?@>1 && @<4] : ${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?logical_and_expr[comp_expr(@, >, 1), comp_expr(@, <, 4)]/logical_and_expr}/fs]/bs}/CS}/$ -$.o[?@.u || @.x] : ${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?logical_or_expr[@ segments, @ segments]/logical_or_expr}/fs]/bs}/CS}/$ -$.a[?@.b == $.x] : ${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:b}}/@, ==, ${sqs{ns:x}}/$)}/fs]/bs}/CS}/$ -$.a[?@ == @] : ${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@, ==, @)}/fs]/bs}/CS}/$ diff --git a/tests/jpath/parsing/jpath_parser_files/table_15.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_15.jpath_ast deleted file mode 100644 index 729b21e..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_15.jpath_ast +++ /dev/null @@ -1,4 +0,0 @@ -# file_name: table_15.jpath_ast -$[0, 3] : ${CS{bs[is:0, is:3]/bs}/CS}/$ -$[0:2, 5] : ${CS{bs[slice(0:2:), is:5]/bs}/CS}/$ -$[0, 0] : ${CS{bs[is:0, is:0]/bs}/CS}/$ diff --git a/tests/jpath/parsing/jpath_parser_files/table_16.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_16.jpath_ast deleted file mode 100644 index 8013841..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_16.jpath_ast +++ /dev/null @@ -1,9 +0,0 @@ -# file_name: table_16.jpath_ast -$..j : ${DS{bs[ns:j]/bs}/DS}/$ -$..j : ${DS{bs[ns:j]/bs}/DS}/$ -$..[0] : ${DS{bs[is:0]/bs}/DS}/$ -$..[*] : ${DS{bs[*]/bs}/DS}/$ -$..* : ${DS{bs[*]/bs}/DS}/$ -$..o : ${DS{bs[ns:o]/bs}/DS}/$ -$.o..[*, *] : ${CS{bs[ns:o]/bs}/CS, DS{bs[*, *]/bs}/DS}/$ -$.a..[0, 1] : ${CS{bs[ns:a]/bs}/CS, DS{bs[is:0, is:1]/bs}/DS}/$ diff --git a/tests/jpath/parsing/jpath_parser_files/table_17.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_17.jpath_ast deleted file mode 100644 index e824a9e..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_17.jpath_ast +++ /dev/null @@ -1,10 +0,0 @@ -# file_name: table_17.jpath_ast -$.a : ${CS{bs[ns:a]/bs}/CS}/$ -$.a[0] : ${CS{bs[ns:a]/bs}/CS, CS{bs[is:0]/bs}/CS}/$ -$.a.d : ${CS{bs[ns:a]/bs}/CS, CS{bs[ns:d]/bs}/CS}/$ -$.b[0] : ${CS{bs[ns:b]/bs}/CS, CS{bs[is:0]/bs}/CS}/$ -$.b[*] : ${CS{bs[ns:b]/bs}/CS, CS{bs[*]/bs}/CS}/$ -$.b[?@] : ${CS{bs[ns:b]/bs}/CS, CS{bs[fs{?@}/fs]/bs}/CS}/$ -$.b[?@==null] : ${CS{bs[ns:b]/bs}/CS, CS{bs[fs{?comp_expr(@, ==, null)}/fs]/bs}/CS}/$ -$.c[?@.d==null] : ${CS{bs[ns:c]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:d}}/@, ==, null)}/fs]/bs}/CS}/$ -$.null : ${CS{bs[ns:null]/bs}/CS}/$ diff --git a/tests/jpath/parsing/jpath_parser_files/table_18.jpath_ast b/tests/jpath/parsing/jpath_parser_files/table_18.jpath_ast deleted file mode 100644 index cd87267..0000000 --- a/tests/jpath/parsing/jpath_parser_files/table_18.jpath_ast +++ /dev/null @@ -1,7 +0,0 @@ -# file_name: table_18.jpath_ast -$.a : ${CS{bs[ns:a]/bs}/CS}/$ -$[1] : ${CS{bs[is:1]/bs}/CS}/$ -$[-3] : ${CS{bs[is:-3]/bs}/CS}/$ -$.a.b[1:2] : ${CS{bs[ns:a]/bs}/CS, CS{bs[ns:b]/bs}/CS, CS{bs[slice(1:2:)]/bs}/CS}/$ -$["\u000B"] : ${CS{bs[ns: ]/bs}/CS}/$ -$["\u0061"] : ${CS{bs[ns:a]/bs}/CS}/$ diff --git a/tests/jpath/parsing/parser_test_cases.json b/tests/jpath/parsing/parser_test_cases.json new file mode 100644 index 0000000..60a3a3b --- /dev/null +++ b/tests/jpath/parsing/parser_test_cases.json @@ -0,0 +1,901 @@ +{ + "description": "Parser tests of example paths in RFC 9535 tables 2-18. This file is autogenerated, do not edit.", + "tests": [ + { + "test_name": "table_02-$.store.book[*].author", + "json_path": "$.store.book[*].author", + "parser_ast": "${CS{bs[ns:store]/bs}/CS, CS{bs[ns:book]/bs}/CS, CS{bs[*]/bs}/CS, CS{bs[ns:author]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..author", + "json_path": "$..author", + "parser_ast": "${DS{bs[ns:author]/bs}/DS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$.store.*", + "json_path": "$.store.*", + "parser_ast": "${CS{bs[ns:store]/bs}/CS, CS{bs[*]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$.store..price", + "json_path": "$.store..price", + "parser_ast": "${CS{bs[ns:store]/bs}/CS, DS{bs[ns:price]/bs}/DS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..book[2]", + "json_path": "$..book[2]", + "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:2]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..book[2].author", + "json_path": "$..book[2].author", + "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:2]/bs}/CS, CS{bs[ns:author]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..book[2].publisher", + "json_path": "$..book[2].publisher", + "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:2]/bs}/CS, CS{bs[ns:publisher]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..book[-1]", + "json_path": "$..book[-1]", + "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:-1]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..book[0,1]", + "json_path": "$..book[0,1]", + "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:0, is:1]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..book[:2]", + "json_path": "$..book[:2]", + "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[slice(:2:)]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..book[?@.isbn]", + "json_path": "$..book[?@.isbn]", + "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[fs{?@ segments}/fs]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..book[?@.price<10]", + "json_path": "$..book[?@.price<10]", + "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[fs{?comp_expr(@{sqs{ns:price}}/@, <, 10)}/fs]/bs}/CS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_02-$..*", + "json_path": "$..*", + "parser_ast": "${DS{bs[*]/bs}/DS}/$", + "source_file_name": "table_02", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_03-$", + "json_path": "$", + "parser_ast": "$", + "source_file_name": "table_03", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_05-$.o['j j']", + "json_path": "$.o['j j']", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS}/$", + "source_file_name": "table_05", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_05-$.o['j j']['k.k']", + "json_path": "$.o['j j']['k.k']", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS, CS{bs[ns:k.k]/bs}/CS}/$", + "source_file_name": "table_05", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_05-$.o[\"j j\"][\"k.k\"]", + "json_path": "$.o[\"j j\"][\"k.k\"]", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS, CS{bs[ns:k.k]/bs}/CS}/$", + "source_file_name": "table_05", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_05-$['o']['j j']['k.k']", + "json_path": "$['o']['j j']['k.k']", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS, CS{bs[ns:k.k]/bs}/CS}/$", + "source_file_name": "table_05", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_05-$[\"'\"][\"@\"]", + "json_path": "$[\"'\"][\"@\"]", + "parser_ast": "${CS{bs[ns:']/bs}/CS, CS{bs[ns:@]/bs}/CS}/$", + "source_file_name": "table_05", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_06-$[*]", + "json_path": "$[*]", + "parser_ast": "${CS{bs[*]/bs}/CS}/$", + "source_file_name": "table_06", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_06-$.o[*]", + "json_path": "$.o[*]", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[*]/bs}/CS}/$", + "source_file_name": "table_06", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_06-$.o[*]", + "json_path": "$.o[*]", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[*]/bs}/CS}/$", + "source_file_name": "table_06", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_06-$.o[*, *]", + "json_path": "$.o[*, *]", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[*, *]/bs}/CS}/$", + "source_file_name": "table_06", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_06-$.a[*]", + "json_path": "$.a[*]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[*]/bs}/CS}/$", + "source_file_name": "table_06", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_07-$[1]", + "json_path": "$[1]", + "parser_ast": "${CS{bs[is:1]/bs}/CS}/$", + "source_file_name": "table_07", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_07-$[-2]", + "json_path": "$[-2]", + "parser_ast": "${CS{bs[is:-2]/bs}/CS}/$", + "source_file_name": "table_07", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_09-$[1:3]", + "json_path": "$[1:3]", + "parser_ast": "${CS{bs[slice(1:3:)]/bs}/CS}/$", + "source_file_name": "table_09", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_09-$[5:]", + "json_path": "$[5:]", + "parser_ast": "${CS{bs[slice(5::)]/bs}/CS}/$", + "source_file_name": "table_09", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_09-$[1:5:2]", + "json_path": "$[1:5:2]", + "parser_ast": "${CS{bs[slice(1:5:2)]/bs}/CS}/$", + "source_file_name": "table_09", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_09-$[5:1:-2]", + "json_path": "$[5:1:-2]", + "parser_ast": "${CS{bs[slice(5:1:-2)]/bs}/CS}/$", + "source_file_name": "table_09", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_09-$[::-1]", + "json_path": "$[::-1]", + "parser_ast": "${CS{bs[slice(::-1)]/bs}/CS}/$", + "source_file_name": "table_09", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_11-$.absent1 == $.absent2", + "json_path": "$.absent1 == $.absent2", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 11: $.absent1 ^==^ $.absent2" + }, + { + "test_name": "table_11-$.absent1 <= $.absent2", + "json_path": "$.absent1 <= $.absent2", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 11: $.absent1 ^<=^ $.absent2" + }, + { + "test_name": "table_11-$.absent == 'g'", + "json_path": "$.absent == 'g'", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 10: $.absent ^==^ 'g'" + }, + { + "test_name": "table_11-$.absent1 != $.absent2", + "json_path": "$.absent1 != $.absent2", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 11: $.absent1 ^!=^ $.absent2" + }, + { + "test_name": "table_11-$.absent != 'g'", + "json_path": "$.absent != 'g'", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 10: $.absent ^!=^ 'g'" + }, + { + "test_name": "table_11-1 <= 2", + "json_path": "1 <= 2", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ <= 2" + }, + { + "test_name": "table_11-1> 2", + "json_path": "1> 2", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^> 2" + }, + { + "test_name": "table_11-13 == '13'", + "json_path": "13 == '13'", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^13^ == '13'" + }, + { + "test_name": "table_11-'a' <= 'b'", + "json_path": "'a' <= 'b'", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got STRING at position 1: ^'a'^ <= 'b'" + }, + { + "test_name": "table_11-'a' > 'b'", + "json_path": "'a' > 'b'", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got STRING at position 1: ^'a'^ > 'b'" + }, + { + "test_name": "table_11-$.obj == $.arr", + "json_path": "$.obj == $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ $.arr" + }, + { + "test_name": "table_11-$.obj != $.arr", + "json_path": "$.obj != $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ $.arr" + }, + { + "test_name": "table_11-$.obj == $.obj", + "json_path": "$.obj == $.obj", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ $.obj" + }, + { + "test_name": "table_11-$.obj != $.obj", + "json_path": "$.obj != $.obj", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ $.obj" + }, + { + "test_name": "table_11-$.arr == $.arr", + "json_path": "$.arr == $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.arr ^==^ $.arr" + }, + { + "test_name": "table_11-$.arr != $.arr", + "json_path": "$.arr != $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.arr ^!=^ $.arr" + }, + { + "test_name": "table_11-$.obj == 17", + "json_path": "$.obj == 17", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ 17" + }, + { + "test_name": "table_11-$.obj != 17", + "json_path": "$.obj != 17", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ 17" + }, + { + "test_name": "table_11-$.obj <= $.arr", + "json_path": "$.obj <= $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.obj ^<=^ $.arr" + }, + { + "test_name": "table_11-$.obj < $.arr", + "json_path": "$.obj < $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got < at position 7: $.obj ^<^ $.arr" + }, + { + "test_name": "table_11-$.obj <= $.obj", + "json_path": "$.obj <= $.obj", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.obj ^<=^ $.obj" + }, + { + "test_name": "table_11-$.arr <= $.arr", + "json_path": "$.arr <= $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.arr ^<=^ $.arr" + }, + { + "test_name": "table_11-1 <= $.arr", + "json_path": "1 <= $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ <= $.arr" + }, + { + "test_name": "table_11-1 >= $.arr", + "json_path": "1 >= $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ >= $.arr" + }, + { + "test_name": "table_11-1 > $.arr", + "json_path": "1 > $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ > $.arr" + }, + { + "test_name": "table_11-1 < $.arr", + "json_path": "1 < $.arr", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ < $.arr" + }, + { + "test_name": "table_11-true <= true", + "json_path": "true <= true", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got true at position 1: ^true^ <= true" + }, + { + "test_name": "table_11-true > true", + "json_path": "true > true", + "parser_ast": "", + "source_file_name": "table_11", + "is_invalid": true, + "err_msg": "Invalid Syntax: start: Expected '$', got true at position 1: ^true^ > true" + }, + { + "test_name": "table_12-$.a[?@.b == 'kilo']", + "json_path": "$.a[?@.b == 'kilo']", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:b}}/@, ==, kilo)}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.a[?(@.b == 'kilo')]", + "json_path": "$.a[?(@.b == 'kilo')]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:b}}/@, ==, kilo)}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.a[?@>3.5]", + "json_path": "$.a[?@>3.5]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@, >, 3.5)}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.a[?@.b]", + "json_path": "$.a[?@.b]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?@ segments}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$[?@.*]", + "json_path": "$[?@.*]", + "parser_ast": "${CS{bs[fs{?@ segments}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$[?@[?@.b]]", + "json_path": "$[?@[?@.b]]", + "parser_ast": "${CS{bs[fs{?@ segments}/fs]/bs}/CS>}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.o[?@<3, ?@<3]", + "json_path": "$.o[?@<3, ?@<3]", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?comp_expr(@, <, 3)}/fs, fs{?comp_expr(@, <, 3)}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.a[?@<2 || @.b == \"k\"]", + "json_path": "$.a[?@<2 || @.b == \"k\"]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?logical_or_expr[comp_expr(@, <, 2), comp_expr(@{sqs{ns:b}}/@, ==, k)]/logical_or_expr}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.o[?@>1 && @<4]", + "json_path": "$.o[?@>1 && @<4]", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?logical_and_expr[comp_expr(@, >, 1), comp_expr(@, <, 4)]/logical_and_expr}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.o[?@>1 && @<4]", + "json_path": "$.o[?@>1 && @<4]", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?logical_and_expr[comp_expr(@, >, 1), comp_expr(@, <, 4)]/logical_and_expr}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.o[?@.u || @.x]", + "json_path": "$.o[?@.u || @.x]", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?logical_or_expr[@ segments, @ segments]/logical_or_expr}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.a[?@.b == $.x]", + "json_path": "$.a[?@.b == $.x]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:b}}/@, ==, ${sqs{ns:x}}/$)}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_12-$.a[?@ == @]", + "json_path": "$.a[?@ == @]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@, ==, @)}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_14-$[?length(@) < 3]", + "json_path": "$[?length(@) < 3]", + "parser_ast": "${CS{bs[fs{?comp_expr(length(@)->ValueType, <, 3)}/fs]/bs}/CS}/$", + "source_file_name": "table_14", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_14-$[?length(@.*) < 3]", + "json_path": "$[?length(@.*) < 3]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Validation Error: function_expr: Expected singular query for ValueType parameter at position 11: $[?length(^@.*^) < 3]" + }, + { + "test_name": "table_14-$[?count(@.*) == 1]", + "json_path": "$[?count(@.*) == 1]", + "parser_ast": "${CS{bs[fs{?comp_expr(count(@ segments)->ValueType, ==, 1)}/fs]/bs}/CS}/$", + "source_file_name": "table_14", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_14-$[?count(1) == 1]", + "json_path": "$[?count(1) == 1]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Validation Error: function_expr: Expected NodesType but got ValueType at position 10: $[?count(^1^) == 1]" + }, + { + "test_name": "table_14-$[?count(foo(@.*)) == 1]", + "json_path": "$[?count(foo(@.*)) == 1]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Illegal Function Name: function_expr: Function name 'foo' is not registered at position 10: $[?count(^foo^(@.*)) == 1]" + }, + { + "test_name": "table_14-$[?match(@.timezone, 'Europe/.*')]", + "json_path": "$[?match(@.timezone, 'Europe/.*')]", + "parser_ast": "${CS{bs[fs{?match(@ segments, Europe/.*)->LogicalType}/fs]/bs}/CS}/$", + "source_file_name": "table_14", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_14-$[?match(@.timezone,'Europe/.*') == true]", + "json_path": "$[?match(@.timezone,'Europe/.*') == true]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Invalid Syntax: bracketed_selection: Expected ',' or ']', found == at position 34: $[?match(@.timezone,'Europe/.*') ^==^ true]" + }, + { + "test_name": "table_14-$[?value(@..color) == \"red\"]", + "json_path": "$[?value(@..color) == \"red\"]", + "parser_ast": "${CS{bs[fs{?comp_expr(value(@ segments)->ValueType, ==, red)}/fs]/bs}/CS}/$", + "source_file_name": "table_14", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_14-$[?value(@..color)]", + "json_path": "$[?value(@..color)]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Validation Error: test_expr: Function not well-typed for test_expr. Expected LogicalType or NodesType, got ValueType at position 4: $[?^value(@..color)^]" + }, + { + "test_name": "table_14-$[?bar(@.a)]", + "json_path": "$[?bar(@.a)]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Illegal Function Name: function_expr: Function name 'bar' is not registered at position 4: $[?^bar^(@.a)]" + }, + { + "test_name": "table_14-$[?bnl(@.*)]", + "json_path": "$[?bnl(@.*)]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Illegal Function Name: function_expr: Function name 'bnl' is not registered at position 4: $[?^bnl^(@.*)]" + }, + { + "test_name": "table_14-$[?blt(1==1)]", + "json_path": "$[?blt(1==1)]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Illegal Function Name: function_expr: Function name 'blt' is not registered at position 4: $[?^blt^(1==1)]" + }, + { + "test_name": "table_14-$[?blt(1)]", + "json_path": "$[?blt(1)]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Illegal Function Name: function_expr: Function name 'blt' is not registered at position 4: $[?^blt^(1)]" + }, + { + "test_name": "table_14-$[?bal(1)]", + "json_path": "$[?bal(1)]", + "parser_ast": "", + "source_file_name": "table_14", + "is_invalid": true, + "err_msg": "Illegal Function Name: function_expr: Function name 'bal' is not registered at position 4: $[?^bal^(1)]" + }, + { + "test_name": "table_15-$[0, 3]", + "json_path": "$[0, 3]", + "parser_ast": "${CS{bs[is:0, is:3]/bs}/CS}/$", + "source_file_name": "table_15", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_15-$[0:2, 5]", + "json_path": "$[0:2, 5]", + "parser_ast": "${CS{bs[slice(0:2:), is:5]/bs}/CS}/$", + "source_file_name": "table_15", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_15-$[0, 0]", + "json_path": "$[0, 0]", + "parser_ast": "${CS{bs[is:0, is:0]/bs}/CS}/$", + "source_file_name": "table_15", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_16-$..j", + "json_path": "$..j", + "parser_ast": "${DS{bs[ns:j]/bs}/DS}/$", + "source_file_name": "table_16", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_16-$..j", + "json_path": "$..j", + "parser_ast": "${DS{bs[ns:j]/bs}/DS}/$", + "source_file_name": "table_16", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_16-$..[0]", + "json_path": "$..[0]", + "parser_ast": "${DS{bs[is:0]/bs}/DS}/$", + "source_file_name": "table_16", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_16-$..[*]", + "json_path": "$..[*]", + "parser_ast": "${DS{bs[*]/bs}/DS}/$", + "source_file_name": "table_16", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_16-$..*", + "json_path": "$..*", + "parser_ast": "${DS{bs[*]/bs}/DS}/$", + "source_file_name": "table_16", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_16-$..o", + "json_path": "$..o", + "parser_ast": "${DS{bs[ns:o]/bs}/DS}/$", + "source_file_name": "table_16", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_16-$.o..[*, *]", + "json_path": "$.o..[*, *]", + "parser_ast": "${CS{bs[ns:o]/bs}/CS, DS{bs[*, *]/bs}/DS}/$", + "source_file_name": "table_16", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_16-$.a..[0, 1]", + "json_path": "$.a..[0, 1]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, DS{bs[is:0, is:1]/bs}/DS}/$", + "source_file_name": "table_16", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_17-$.a", + "json_path": "$.a", + "parser_ast": "${CS{bs[ns:a]/bs}/CS}/$", + "source_file_name": "table_17", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_17-$.a[0]", + "json_path": "$.a[0]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[is:0]/bs}/CS}/$", + "source_file_name": "table_17", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_17-$.a.d", + "json_path": "$.a.d", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[ns:d]/bs}/CS}/$", + "source_file_name": "table_17", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_17-$.b[0]", + "json_path": "$.b[0]", + "parser_ast": "${CS{bs[ns:b]/bs}/CS, CS{bs[is:0]/bs}/CS}/$", + "source_file_name": "table_17", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_17-$.b[*]", + "json_path": "$.b[*]", + "parser_ast": "${CS{bs[ns:b]/bs}/CS, CS{bs[*]/bs}/CS}/$", + "source_file_name": "table_17", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_17-$.b[?@]", + "json_path": "$.b[?@]", + "parser_ast": "${CS{bs[ns:b]/bs}/CS, CS{bs[fs{?@}/fs]/bs}/CS}/$", + "source_file_name": "table_17", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_17-$.b[?@==null]", + "json_path": "$.b[?@==null]", + "parser_ast": "${CS{bs[ns:b]/bs}/CS, CS{bs[fs{?comp_expr(@, ==, null)}/fs]/bs}/CS}/$", + "source_file_name": "table_17", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_17-$.c[?@.d==null]", + "json_path": "$.c[?@.d==null]", + "parser_ast": "${CS{bs[ns:c]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:d}}/@, ==, null)}/fs]/bs}/CS}/$", + "source_file_name": "table_17", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_17-$.null", + "json_path": "$.null", + "parser_ast": "${CS{bs[ns:null]/bs}/CS}/$", + "source_file_name": "table_17", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_18-$.a", + "json_path": "$.a", + "parser_ast": "${CS{bs[ns:a]/bs}/CS}/$", + "source_file_name": "table_18", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_18-$[1]", + "json_path": "$[1]", + "parser_ast": "${CS{bs[is:1]/bs}/CS}/$", + "source_file_name": "table_18", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_18-$[-3]", + "json_path": "$[-3]", + "parser_ast": "${CS{bs[is:-3]/bs}/CS}/$", + "source_file_name": "table_18", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_18-$.a.b[1:2]", + "json_path": "$.a.b[1:2]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[ns:b]/bs}/CS, CS{bs[slice(1:2:)]/bs}/CS}/$", + "source_file_name": "table_18", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_18-$[\"\\u000B\"]", + "json_path": "$[\"\\u000B\"]", + "parser_ast": "${CS{bs[ns:\u000b]/bs}/CS}/$", + "source_file_name": "table_18", + "is_invalid": false, + "err_msg": "" + }, + { + "test_name": "table_18-$[\"\\u0061\"]", + "json_path": "$[\"\\u0061\"]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS}/$", + "source_file_name": "table_18", + "is_invalid": false, + "err_msg": "" + } + ] +} \ No newline at end of file diff --git a/tests/jpath/parsing/subparse_test_cases_table_11.json b/tests/jpath/parsing/subparse_test_cases_table_11.json new file mode 100644 index 0000000..1fe6ba2 --- /dev/null +++ b/tests/jpath/parsing/subparse_test_cases_table_11.json @@ -0,0 +1,257 @@ +{ + "description": "Parser subparse tests of example paths in RFC 9535 table 11. This file is autogenerated, do not edit.", + "tests": [ + { + "test_name": "table_11-$.absent1 == $.absent2", + "json_path": "$.absent1 == $.absent2", + "parser_ast": "comp_expr(${sqs{ns:absent1}}/$, ==, ${sqs{ns:absent2}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.absent1 <= $.absent2", + "json_path": "$.absent1 <= $.absent2", + "parser_ast": "comp_expr(${sqs{ns:absent1}}/$, <=, ${sqs{ns:absent2}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.absent == 'g'", + "json_path": "$.absent == 'g'", + "parser_ast": "comp_expr(${sqs{ns:absent}}/$, ==, g)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.absent1 != $.absent2", + "json_path": "$.absent1 != $.absent2", + "parser_ast": "comp_expr(${sqs{ns:absent1}}/$, !=, ${sqs{ns:absent2}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.absent != 'g'", + "json_path": "$.absent != 'g'", + "parser_ast": "comp_expr(${sqs{ns:absent}}/$, !=, g)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-1 <= 2", + "json_path": "1 <= 2", + "parser_ast": "comp_expr(1, <=, 2)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-1> 2", + "json_path": "1> 2", + "parser_ast": "comp_expr(1, >, 2)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-13 == '13'", + "json_path": "13 == '13'", + "parser_ast": "comp_expr(13, ==, 13)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-'a' <= 'b'", + "json_path": "'a' <= 'b'", + "parser_ast": "comp_expr(a, <=, b)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-'a' > 'b'", + "json_path": "'a' > 'b'", + "parser_ast": "comp_expr(a, >, b)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.obj == $.arr", + "json_path": "$.obj == $.arr", + "parser_ast": "comp_expr(${sqs{ns:obj}}/$, ==, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.obj != $.arr", + "json_path": "$.obj != $.arr", + "parser_ast": "comp_expr(${sqs{ns:obj}}/$, !=, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.obj == $.obj", + "json_path": "$.obj == $.obj", + "parser_ast": "comp_expr(${sqs{ns:obj}}/$, ==, ${sqs{ns:obj}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.obj != $.obj", + "json_path": "$.obj != $.obj", + "parser_ast": "comp_expr(${sqs{ns:obj}}/$, !=, ${sqs{ns:obj}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.arr == $.arr", + "json_path": "$.arr == $.arr", + "parser_ast": "comp_expr(${sqs{ns:arr}}/$, ==, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.arr != $.arr", + "json_path": "$.arr != $.arr", + "parser_ast": "comp_expr(${sqs{ns:arr}}/$, !=, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.obj == 17", + "json_path": "$.obj == 17", + "parser_ast": "comp_expr(${sqs{ns:obj}}/$, ==, 17)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.obj != 17", + "json_path": "$.obj != 17", + "parser_ast": "comp_expr(${sqs{ns:obj}}/$, !=, 17)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.obj <= $.arr", + "json_path": "$.obj <= $.arr", + "parser_ast": "comp_expr(${sqs{ns:obj}}/$, <=, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.obj < $.arr", + "json_path": "$.obj < $.arr", + "parser_ast": "comp_expr(${sqs{ns:obj}}/$, <, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.obj <= $.obj", + "json_path": "$.obj <= $.obj", + "parser_ast": "comp_expr(${sqs{ns:obj}}/$, <=, ${sqs{ns:obj}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-$.arr <= $.arr", + "json_path": "$.arr <= $.arr", + "parser_ast": "comp_expr(${sqs{ns:arr}}/$, <=, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-1 <= $.arr", + "json_path": "1 <= $.arr", + "parser_ast": "comp_expr(1, <=, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-1 >= $.arr", + "json_path": "1 >= $.arr", + "parser_ast": "comp_expr(1, >=, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-1 > $.arr", + "json_path": "1 > $.arr", + "parser_ast": "comp_expr(1, >, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-1 < $.arr", + "json_path": "1 < $.arr", + "parser_ast": "comp_expr(1, <, ${sqs{ns:arr}}/$)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-true <= true", + "json_path": "true <= true", + "parser_ast": "comp_expr(true, <=, true)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + }, + { + "test_name": "table_11-true > true", + "json_path": "true > true", + "parser_ast": "comp_expr(true, >, true)", + "source_file_name": "table_11", + "is_invalid": false, + "err_msg": "", + "subparse_production": "comparison_expr" + } + ] +} \ No newline at end of file diff --git a/tests/jpath/parsing/test_parse_rfc_examples.py b/tests/jpath/parsing/test_parse_rfc_examples.py deleted file mode 100644 index bbc1d82..0000000 --- a/tests/jpath/parsing/test_parse_rfc_examples.py +++ /dev/null @@ -1,65 +0,0 @@ - -# File: test_rfc_examples.py -# Copyright (c) 2025 Robert L. Ross -# All rights reserved. -# Open-source license to come. -# Created by: Robert L. Ross -# -"""Test parsing against json path query strings from the tables in RFC 9535.""" - -from pathlib import Path -from typing import NamedTuple, Generator - -import pytest - -from killerbunny.lexing.lexer import JPathLexer -from killerbunny.lexing.tokens import Token -from killerbunny.parsing.parse_result import ParseResult -from killerbunny.parsing.parser import JPathParser -from killerbunny.shared.constants import ONE_MEBIBYTE, UTF8, JPATH_DATA_SEPARATOR -from killerbunny.shared.errors import Error - -_MODULE_DIR = Path(__file__).parent -_TEST_FILE_DIR = _MODULE_DIR / "jpath_parser_files" - -class PathAST(NamedTuple): - file_name: str - jpath: str - ast_str: str - -def ast_to_str(parse_result: ParseResult) -> str: - """Return either the parse error or the parse result ASTNode as a str respresentation.""" - result_str: str = "" - if parse_result.error: - result_str = parse_result.error.as_test_string() - else: - if parse_result.node: - result_str = str(parse_result.node) - return result_str - - -def jpath_ast_data() -> Generator[ PathAST, None, None] : - for file in _TEST_FILE_DIR.iterdir(): - if file.suffix == ".jpath_ast": - with open(file, "r", encoding=UTF8, buffering=ONE_MEBIBYTE) as input_file: - for line in input_file: - line_stripped = line.strip() - if line_stripped == '' or line_stripped.startswith('#'): - continue - path_ast = PathAST(file.name, *line_stripped.split(JPATH_DATA_SEPARATOR)) - yield path_ast - -@pytest.mark.parametrize("file_name, jpath, ast_str", jpath_ast_data()) -def test_rfc_example(file_name: str, jpath: str, ast_str: str) -> None: - expected = ast_str - lexer = JPathLexer(file_name, jpath) - tokens: list[Token] - error: Error | None - tokens, error = lexer.tokenize() - assert error is None, f"Lexer should not return errors for '{jpath}'" - assert tokens, f"Token list should not be empty for '{jpath}'" - parser = JPathParser(tokens) - result: ParseResult = parser.parse() - actual: str = ast_to_str(result) - assert actual == expected, f"Parsing '{jpath}' should produce: {expected}" - diff --git a/tests/jpath/parsing/test_parser_rfc9535_tables.py b/tests/jpath/parsing/test_parser_rfc9535_tables.py new file mode 100644 index 0000000..2548bbf --- /dev/null +++ b/tests/jpath/parsing/test_parser_rfc9535_tables.py @@ -0,0 +1,101 @@ +# File: test_parser_rfc9535_tables.py +# Copyright (c) 2025 Robert L. Ross +# All rights reserved. +# Open-source license to come. +# Created by: Robert L. Ross +# +import json +import operator +from dataclasses import dataclass +from pathlib import Path + +import pytest + +from killerbunny.lexing.lexer import JPathLexer +from killerbunny.parsing.node_type import ASTNode +from killerbunny.parsing.parse_result import ParseResult +from killerbunny.parsing.parser import JPathParser +from killerbunny.shared.constants import UTF8, ONE_MEBIBYTE + +_MODULE_DIR = Path(__file__).parent +PARSER_TEST_CASES_FILENAME = "parser_test_cases.json" +_PTC_FILE_PATH = _MODULE_DIR / PARSER_TEST_CASES_FILENAME +_FILE_LIST = [_PTC_FILE_PATH, ] + + +@dataclass(frozen=True, slots=True) +class ParserTestCase: + test_name : str + json_path : str + parser_ast : str + source_file_name : str + is_invalid : bool = False + err_msg : str = "" + + +def data_loader() -> list[ParserTestCase]: + test_data: list[ParserTestCase] = [] + for file_name in _FILE_LIST: + file_path = _MODULE_DIR / file_name + with open( file_path , encoding=UTF8, buffering=ONE_MEBIBYTE) as input_file: + data = json.load(input_file) + test_data.extend([ParserTestCase(**test) for test in data["tests"]]) + return test_data + +def valid_paths() -> list[ParserTestCase]: + return [ test for test in data_loader() if not test.is_invalid ] + +def invalid_paths() -> list[ParserTestCase]: + return [ test for test in data_loader() if test.is_invalid ] + +EXCLUDED_TESTS_MAP: dict[str, tuple[str,str]] = {} +# during debugging of test cases, print debug info for the test names in this set +DEBUG_TEST_NAMES: set[str] = set() + +@pytest.mark.parametrize("case", valid_paths(), ids=operator.attrgetter("test_name")) +def test_parser_valid_cases(case: ParserTestCase) -> None: + """Test the cases in the parser_test_cases.json file that are intended to be grammatically correct + and should return a result. """ + if case.test_name in EXCLUDED_TESTS_MAP: + pytest.skip(reason=f"{EXCLUDED_TESTS_MAP[case.test_name][1]}: '{case.test_name}'") + + if case.test_name in DEBUG_TEST_NAMES: + print(f"\n* * * * * test: '{case.test_name}', json_path: {case.json_path}, expected: {case.parser_ast}") + + assert case.json_path is not None + lexer = JPathLexer(case.source_file_name, case.json_path) + tokens, error = lexer.tokenize() + assert error is None + + parser = JPathParser(tokens) + result: ParseResult = parser.parse() + assert result.error is None, f"Unexpected error when parsing {case.json_path}" + + ast: ASTNode | None = result.node + assert ast is not None + + expected = case.parser_ast + actual = str(ast) + + assert actual == expected, f"Parsing {case.json_path} should produce: {expected}" + + +@pytest.mark.parametrize("case", invalid_paths(), ids=operator.attrgetter("test_name")) +def test_parser_invalid_cases(case: ParserTestCase) -> None: + """Test the cases in the parser_test_cases.json file that are intended to be not well-formed or valid""" + if case.test_name in EXCLUDED_TESTS_MAP: + pytest.skip(reason=f"{EXCLUDED_TESTS_MAP[case.test_name][1]}: '{case.test_name}'") + + assert case.json_path is not None + lexer = JPathLexer(case.source_file_name, case.json_path) + tokens, error = lexer.tokenize() + assert error is None + + parser = JPathParser(tokens) + result: ParseResult = parser.parse() + assert result.error is not None, f"Expected error when parsing {case.json_path}" + + expected = case.err_msg + actual = result.error.as_test_string() + + assert actual == expected, f"Parsing {case.json_path} should produce: {expected}" \ No newline at end of file diff --git a/tests/jpath/parsing/test_subparsing.py b/tests/jpath/parsing/test_subparsing.py new file mode 100644 index 0000000..ec4b360 --- /dev/null +++ b/tests/jpath/parsing/test_subparsing.py @@ -0,0 +1,124 @@ +# File: test_subparsing.py +# Copyright (c) 2025 Robert L. Ross +# All rights reserved. +# Open-source license to come. +# Created by: Robert L. Ross +# + +"""Table 11 in RFC 9535 consists of comparison expressions that aren't proper JSON Path query strings. We must use +subparse('comparison_expr') to generate an AST without errors. """ +import json +import operator +from dataclasses import dataclass +from pathlib import Path + +import pytest + +from killerbunny.lexing.lexer import JPathLexer +from killerbunny.lexing.tokens import Token +from killerbunny.parsing.node_type import ASTNode +from killerbunny.parsing.parser import JPathParser +from killerbunny.shared.constants import UTF8, ONE_MEBIBYTE +from killerbunny.shared.errors import Error + +_MODULE_DIR = Path(__file__).parent +SUBPARSER_TEST_CASES_FILENAME = "subparse_test_cases_table_11.json" +_PTC_FILE_PATH = _MODULE_DIR / SUBPARSER_TEST_CASES_FILENAME +_FILE_LIST = [_PTC_FILE_PATH, ] + + +@dataclass(frozen=True, slots=True) +class ParserTestCase: + test_name : str + json_path : str + parser_ast : str + source_file_name : str + is_invalid : bool = False + err_msg : str = "" + subparse_production: str| None = None + + +def data_loader() -> list[ParserTestCase]: + test_data: list[ParserTestCase] = [] + for file_name in _FILE_LIST: + file_path = _MODULE_DIR / file_name + with open( file_path , encoding=UTF8, buffering=ONE_MEBIBYTE) as input_file: + data = json.load(input_file) + test_data.extend([ParserTestCase(**test) for test in data["tests"]]) + return test_data + +def valid_paths() -> list[ParserTestCase]: + return [ test for test in data_loader() if not test.is_invalid ] + +def invalid_paths() -> list[ParserTestCase]: + return [ test for test in data_loader() if test.is_invalid ] + +EXCLUDED_TESTS_MAP: dict[str, tuple[str,str]] = {} +# during debugging of test cases, print debug info for the test names in this set +DEBUG_TEST_NAMES: set[str] = set() + + +def subparse(tokens: list[Token], production_name: str) -> tuple[str, str | None]: + parser = JPathParser(tokens) + result: tuple[ list[ tuple[str, ASTNode] ] , list[ tuple[str, Error] ] ] + result = parser.subparse(production_name) + node_list = result[0] + ast_str = "" + err_list = result[1] + err_msg = None + for name, error in err_list: + if name == production_name: + err_msg = error.as_test_string() + for name, ast_node in node_list: + if name == production_name: + ast_str = str(ast_node) + + return ast_str, err_msg + +@pytest.mark.parametrize("case", valid_paths(), ids=operator.attrgetter("test_name")) +def test_subparser_valid_cases(case: ParserTestCase) -> None: + """Test the cases in the .json test file that are intended to be sub parseable + and should return a result. """ + if case.test_name in EXCLUDED_TESTS_MAP: + pytest.skip(reason=f"{EXCLUDED_TESTS_MAP[case.test_name][1]}: '{case.test_name}'") + + if case.test_name in DEBUG_TEST_NAMES: + print(f"\n* * * * * test: '{case.test_name}', json_path: {case.json_path}, expected: {case.parser_ast}") + + assert case.json_path is not None + lexer = JPathLexer(case.source_file_name, case.json_path) + tokens, error = lexer.tokenize() + assert error is None + + assert case.subparse_production is not None + ast_str, err_msg = subparse(tokens, case.subparse_production) + assert err_msg is None + + assert ast_str == case.parser_ast, f"Parsing {case.json_path} should produce: {case.parser_ast}" + +# the following is commented out to supress pytest reporting of this skipped test. + +# @pytest.mark.skip("No invalid cases exist in the test file. ") +# @pytest.mark.parametrize("case", invalid_paths(), ids=operator.attrgetter("test_name")) +# def test_subparser_invalid_cases(case: ParserTestCase) -> None: +# """Test the cases in the .json test file that are intended to not be sub-parseable with the given production name.""" +# if case.test_name in EXCLUDED_TESTS_MAP: +# pytest.skip(reason=f"{EXCLUDED_TESTS_MAP[case.test_name][1]}: '{case.test_name}'") +# +# assert case.json_path is not None +# lexer = JPathLexer(case.source_file_name, case.json_path) +# tokens, error = lexer.tokenize() +# assert error is None +# +# parser = JPathParser(tokens) +# result: ParseResult = parser.parse() +# assert result.error is not None, f"Expected error when parsing {case.json_path}" +# +# assert case.subparse_production is not None +# ast_str, err_msg = subparse(tokens, case.subparse_production) +# assert err_msg is not None +# +# expected = case.err_msg +# actual = err_msg +# +# assert actual == expected, f"Subparsing {case.json_path} should produce error: {expected}" \ No newline at end of file From 7988e7efbb5b1352be210cd22ac95e758ab44590 Mon Sep 17 00:00:00 2001 From: killeroonie Date: Mon, 30 Jun 2025 21:07:38 -0700 Subject: [PATCH 8/9] Implemented all parser tests for Table 14. Refactored function argument validation. Cleaned up grammar and spelling. Changed to using forward references so that nested declarations are shorter when dynamically getting type hints. Improved grammar, added documentation for try_register(). --- killerbunny/parsing/function.py | 201 +++++++++++++----- killerbunny/parsing/node_type.py | 2 +- killerbunny/parsing/parse_result.py | 31 ++- killerbunny/parsing/parser.py | 60 +++--- killerbunny/shared/json_type_defs.py | 2 +- .../jpath/parsing/test_table_14_functions.py | 196 +++++++++++++++++ 6 files changed, 403 insertions(+), 89 deletions(-) create mode 100644 tests/jpath/parsing/test_table_14_functions.py diff --git a/killerbunny/parsing/function.py b/killerbunny/parsing/function.py index 2a8f576..80193c5 100644 --- a/killerbunny/parsing/function.py +++ b/killerbunny/parsing/function.py @@ -5,33 +5,32 @@ # Created by: Robert L. Ross # # +import inspect import re import threading from abc import abstractmethod from enum import Enum -from typing import TypeAlias, Union, TYPE_CHECKING, Any, Self, override, cast +from typing import (TypeAlias, Union, Any, Self, cast, get_type_hints, get_origin, get_args) +from typing import override # type: ignore -from killerbunny.evaluating.value_nodes import BooleanValue, NullValue, VNode, StringValue +from killerbunny.evaluating.value_nodes import NullValue, VNode, StringValue from killerbunny.evaluating.value_nodes import VNodeList from killerbunny.parsing.node_type import ASTNodeType, ASTNode from killerbunny.parsing.parser_nodes import RepetitionNode, RelativeQueryNode, JsonPathQueryNode, \ RelativeSingularQueryNode, AbsoluteSingularQueryNode from killerbunny.parsing.terminal_nodes import BooleanLiteralNode, NullLiteralNode, LiteralNode -from killerbunny.shared.json_type_defs import JSON_VALUE_TYPES, JSON_ARRAY_TYPES, JSON_OBJECT_TYPES - -if TYPE_CHECKING: - from killerbunny.shared.json_type_defs import JSON_ValueType +from killerbunny.shared.json_type_defs import JSON_ValueType, JSON_VALUE_TYPES, JSON_ARRAY_TYPES, JSON_OBJECT_TYPES ################################################################################################ -# FUNCTION TYPES see 2.4.1. Type System for Function Expressions, pg 35, RFC 9535 +# FUNCTION TYPES - see 2.4.1. Type System for Function Expressions, pg 35, RFC 9535 ################################################################################################ # Nothing: pg 35 RFC 9535. # The special result Nothing represents the absence of a JSON value and is distinct from any JSON value, including null. class NothingType: - """Singleton type for the Nothing instance. pg 35 RFC 9535. + """Singleton type for the Nothing instance. See page 35 RFC 9535. The special result Nothing represents the absence of a JSON value and is distinct from any JSON value, including null. """ @@ -54,8 +53,8 @@ def _init_instance(cls) -> 'NothingType': @classmethod def instance(cls) -> 'NothingType': - """ Return a singleton instance of this class in a thread safe manner. - :return the singleton instance of this class + """ Return a singleton instance of this class in a thread-safe manner. + :return: The singleton instance of this class """ if cls._instance is None: return cls._init_instance() @@ -71,9 +70,9 @@ def __str__(self) -> str: class LogicalType(Enum): - """ + """A boolean enum class used to type function parameters and return types. - see section 2.4.1. Type System for Function Expressions, pg 36 in RFC 9535 + See section 2.4.1. Type System for Function Expressions, pg 36 in RFC 9535 "LogicalTrue and LogicalFalse are unrelated to the JSON values expressed by the literals true and false." These types exist solely for typing the value returned from a function_expr or typing arguments to a function_expr @@ -102,11 +101,10 @@ def negate(self) -> 'LogicalType': return LogicalType.LogicalTrue -# ValueType: per pg 35 RFC 9535, ValueType is any valid JSON type plus Nothing +# ValueType: per pg 35 RFC 9535, ValueType is any valid JSON type plus Nothing ValueType: TypeAlias = Union['JSON_ValueType', NothingType] # for type hints VALUE_TYPES = (*JSON_VALUE_TYPES, NothingType) # for isinstance() - -NodesType: TypeAlias = 'VNodeList' +NodesType: TypeAlias = VNodeList class FunctionParamType(Enum): ValueType = ValueType @@ -135,6 +133,7 @@ def __init__(self, func_name: str, func_type: 'FunctionParam', param_list: list[ self._func_name: str = func_name self._func_type: 'FunctionParam' = func_type self._param_list: list['FunctionParam'] = param_list + self.set_python_params() @property def func_name(self) -> str: @@ -142,7 +141,7 @@ def func_name(self) -> str: @property def func_type(self) -> FunctionParamType: - """Return type of the function.""" + """Return the type of the function.""" return self._func_type.param_type @property @@ -168,7 +167,91 @@ def validate_args(self, args: "RepetitionNode") -> bool: correct number of arguments""" return True + def get_eval_type_hints(self): + """Get the type hints of the eval method for this function node instance.""" + # Get the actual implementation of the eval method from the subclass + eval_method = self.__class__.eval + + # Get the type annotations + type_hints = get_type_hints(eval_method) + + # Extract parameter types (excluding 'self' and 'return') + param_types = { + name: hint for name, hint in type_hints.items() + if name != 'return' and name != 'self' + } + + # Get the return type + return_type = type_hints.get('return', Any) + + return { + 'param_types': param_types, + 'return_type': return_type + } + + def _types_compatible(self, annotation_type, expected_type): + """Check if the annotation type is compatible with the expected type. + + This is a simplistic implementation - you might need to enhance it + based on your specific type system. + """ + # If expected_type is Any, any annotation_type is compatible + if expected_type is Any: + return True + + if annotation_type == expected_type: + return True + + # Check for Union types + origin = get_origin(annotation_type) + if origin is Union: + args = get_args(annotation_type) + # If any of the union types match the expected type, it's compatible + return any(self._types_compatible(arg, expected_type) for arg in args) + + # For simple types, direct comparison + return annotation_type == expected_type + def set_python_params(self) -> None: + """Use the eval() signature to set the expected python arg types.""" + # Get the eval method of the subclass + eval_method = self.__class__.eval + + # Get the signature of the method + sig = inspect.signature(eval_method) + parameters = list(sig.parameters.values()) + + # Get expected parameter count from the function's parameter list + expected_param_count = len(self._param_list) + actual_param_count = len(parameters) - 1 # exclude `self` + + if actual_param_count != expected_param_count: + raise ValueError( + f"Eval method must have exactly {expected_param_count} parameters " + f"(excluding 'self'), but found {actual_param_count}" + ) + + # Check if the parameter types match the declared FunctionParam types + type_hints = self.get_eval_type_hints() + param_types = type_hints['param_types'] + + for i, (param_name, param_type) in enumerate(param_types.items()): + # Get the expected type from the corresponding FunctionParam + if i < len(self._param_list): + function_param = self._param_list[i] + function_param._python_type = param_type # assign the python type based on the parameter hint + + # Check if the types are compatible? + + # Check return type compatibility + return_type = type_hints['return_type'] + self._func_type._python_type = return_type + expected_return_type = self._func_type.python_type + if not self._types_compatible(return_type, expected_return_type): + raise ValueError( + f"Return type of eval method is '{return_type}', " + f"but '{expected_return_type}' was declared in FunctionParam" + ) class FunctionCallNode(ASTNode): """AST node for a function invocation. Encapsulates a FunctionNode and an argument value list. @@ -215,7 +298,7 @@ def __str__(self) -> str: return f"{self._function_node.func_name}({arg_list})->{self._function_node.func_type}" def _validate_args(self, args: 'RepetitionNode') -> None: - """Ensure number of args equals number of defined parameters, and that their types match. """ + """Ensure that the number of args equals the number of defined parameters and that their types match. """ param_list = self._function_node.param_list arg_count_diff = len(param_list) - len(args) if arg_count_diff != 0: @@ -235,10 +318,10 @@ def __init__(self, param_name: str, param_type: 'FunctionParamType', python_type """Represents a function parameter. This could either be a function return parameter or a function argument parameter. - :param param_name: formal name of the parameter. Although, all function args are passed by position, so this is + :param param_name: Formal name of the parameter. Although, all function args are passed by position, so this is mainly to aid in debugging and logging - :param param_type: the allowed type of the parameter. - :param python_type: can be used to narrow the parameter type to a specific Python type or types, + :param param_type: The allowed type of the parameter. + :param python_type: Can be used to narrow the parameter type to a specific Python type or types, e.g., int | Nothing, as in the return type of the length() function extension. """ @@ -279,7 +362,9 @@ class FunctionArgument(ASTNode): def __init__(self, node: ASTNode) -> None: super().__init__(ASTNodeType.FUNCTION_ARG) self._arg_node = node - self._arg_type, self._python_type = self._param_type_for_node(node) + # these get set during validation + self._arg_type = None # type: ignore + self._python_type = None # type: ignore @property def arg_node(self) -> ASTNode: @@ -298,8 +383,13 @@ def __repr__(self) -> str: def __str__(self) -> str: return f"{self._arg_node}" - - def _param_type_for_node(self, arg_node: ASTNode) -> tuple[FunctionParamType, Any] : + + def _param_type_for_node(self, param: FunctionParam) -> tuple[FunctionParamType, Any] : + """Determine the appropriate type for this argument, using the param for implicit conversions, if needed. + E.g., a relative_query as an argument to a function with declared param NodesType will be typed as NodesType, + VLNodesList. But the same relative_query argument to a parameter typed as LogicalType can be converted, so + it will get typed as LogicalType.""" + arg_node = self._arg_node node_type = arg_node.node_type # literals if node_type == ASTNodeType.STRING: @@ -314,34 +404,44 @@ def _param_type_for_node(self, arg_node: ASTNode) -> tuple[FunctionParamType, An return FunctionParamType.ValueType, NullValue # logical_expr - elif node_type == ASTNodeType.LOGICAL_EXPR: + elif node_type == ASTNodeType.LOGICAL_EXPR or node_type == ASTNodeType.COMPARISON_EXPR: return FunctionParamType.LogicalType, LogicalType # function_expr elif isinstance(arg_node, FunctionCallNode): ret_type = arg_node.func_node.return_param + if ret_type.param_type == FunctionParamType.NodesType and param.param_type == FunctionParamType.LogicalType: + return FunctionParamType.LogicalType, LogicalType # this converts a NodeList type to LogicalType. return ret_type.param_type, ret_type.python_type # singluar query - elif isinstance(arg_node, (RelativeSingularQueryNode, AbsoluteSingularQueryNode)): - return FunctionParamType.ValueType, FunctionParamType.ValueType - elif isinstance(arg_node, (RepetitionNode, JsonPathQueryNode, RelativeQueryNode)) and arg_node.is_singular_query: - return FunctionParamType.ValueType, FunctionParamType.ValueType + elif ( isinstance(arg_node, (RelativeSingularQueryNode, AbsoluteSingularQueryNode)) or + ( arg_node.is_singular_query() and + isinstance(arg_node, (RepetitionNode, JsonPathQueryNode, RelativeQueryNode ))) + ): + if param.param_type == FunctionParamType.LogicalType: + return FunctionParamType.LogicalType, LogicalType # this converts a NodeList type to LogicalType. + else: + return FunctionParamType.ValueType, FunctionParamType.ValueType # filter_query elif isinstance(arg_node, (RelativeQueryNode, JsonPathQueryNode)): - return FunctionParamType.NodesType, VNodeList + if param.param_type == FunctionParamType.LogicalType: + return FunctionParamType.LogicalType, LogicalType # this converts a NodeList type to LogicalType. + else: + return FunctionParamType.NodesType, VNodeList else: - raise TypeError(f"Expected logical_expr, filter_query, function_expr, or literal but got {type(arg_node)}") + raise TypeError(f"Expected logical_expr, filter_query, function_expr, or literal but got {type(arg_node)}, node_type = {arg_node.node_type}") def validate_type(self, param: FunctionParam) -> bool: """Validate the type of the argument against the expected type of the param. See section 2.4.3. Well-Typedness of Function Expressions, page 36, RFC 9535""" + self._arg_type, self._python_type = self._param_type_for_node(param) if self._arg_type == param.param_type and self.python_type == param.python_type : - return True # exact type match, simplest case + return True # exact type match, the simplest case """ An argument is well-typed: @@ -401,8 +501,11 @@ class LengthFunction(FunctionNode): """Compute the length of a str, Array, or Object value""" def __init__(self) -> None: return_param = FunctionParam("return", FunctionParamType.ValueType, (int, NothingType)) - param1 = FunctionParam("value", FunctionParamType.ValueType, VALUE_TYPES ) + # param1 = FunctionParam("value", FunctionParamType.ValueType, VALUE_TYPES ) + param1 = FunctionParam("value", FunctionParamType.ValueType, ValueType ) + super().__init__("length", return_param, [param1]) + def eval(self, value: ValueType) -> int | NothingType: # type: ignore if isinstance(value, VNodeList): @@ -457,7 +560,7 @@ class RegexFuncBase(FunctionNode): """ def __init__(self, function_name: str) -> None: - return_param = FunctionParam("return", FunctionParamType.LogicalType, bool) + return_param = FunctionParam("return", FunctionParamType.LogicalType, LogicalType) param1 = FunctionParam("string", FunctionParamType.ValueType, str) param2 = FunctionParam("iregexp_str", FunctionParamType.ValueType, str) super().__init__(function_name, return_param, [param1, param2]) @@ -468,7 +571,7 @@ def eval(self, string: str, iregexp_str: str) -> bool: # type: ignore def _convert_args(self, string: str | StringValue | VNodeList, iregexp_str: str | StringValue | VNodeList) -> tuple[str, str]: - """Convert argument values to str values. The search/match functions may be passed a string literal or a + """Convert argument values to str values. The search/match functions may be passed in a string literal or a single element VNodeList, from which we can obtain the value. """ str_val: str if isinstance(string, VNodeList) and len(string) == 1 and isinstance(string[0].jvalue, (str, StringValue) ): # type: ignore @@ -489,8 +592,8 @@ def _convert_args(self, string: str | StringValue | VNodeList, iregexp_str: str @override def validate_args(self, args: "RepetitionNode") -> bool: - """Verify that the regexp str will compile, and warn if it doesn't comply with I-regexp spec. - :raise re.error if the regexp is not supported by the re library + """Verify that the regexp str will compile and warn if it doesn't comply with I-regexp spec. + :raise: re.error if the regexp is not supported by the re library """ if args is None: raise ValueError(f"MatchFunction.validate_args: args argument cannot be None.") @@ -502,7 +605,7 @@ def validate_args(self, args: "RepetitionNode") -> bool: raise ValueError(f"Expected {len(self._param_list)} arguments, but got {len(args)}") re_str = str(args[1]) - re.compile(re_str) # will raise re.error if re doesn't support the regexp + re.compile(re_str) # will raise re.error if `re` doesn't support the regexp return True @@ -512,13 +615,12 @@ def __init__(self) -> None: super().__init__("match") @override - def eval(self, string: str, iregexp_str: str) -> BooleanValue: # type: ignore + def eval(self, string: str, iregexp_str: str) -> LogicalType: # type: ignore try: str_val, regex_str = self._convert_args(string, iregexp_str) except TypeError: - return BooleanValue.value_for(False) - - return BooleanValue.value_for( re.fullmatch( regex_str, str_val) is not None ) + return LogicalType.value_for(False) + return LogicalType.value_for( re.fullmatch( regex_str, str_val) is not None ) class SearchFunction(RegexFuncBase): """Check whether a given string contains a substring that matches a given regular expression,""" @@ -526,13 +628,12 @@ def __init__(self) -> None: super().__init__("search") @override - def eval(self, string: str, iregexp_str: str) -> BooleanValue: # type: ignore + def eval(self, string: str, iregexp_str: str) -> LogicalType: # type: ignore try: str_val, regex_str = self._convert_args(string, iregexp_str) except TypeError: - return BooleanValue.value_for(False) - return BooleanValue.value_for( re.search( regex_str, str_val) is not None ) - + return LogicalType.value_for(False) + return LogicalType.value_for( re.search( regex_str, str_val) is not None ) class ValueFunction(FunctionNode): @@ -577,8 +678,8 @@ def _init_instance(cls) -> '_FunctionRegistry': @classmethod def instance(cls) -> '_FunctionRegistry': - """ Return a singleton instance of this class in a thread safe manner. - :return the singleton instance of this class + """ Return a singleton instance of this class in a thread-safe manner. + :return: The singleton instance of this class """ if cls._instance is not None: return cls._instance @@ -605,5 +706,7 @@ def lookup(self, func_name: str) -> FunctionNode | None: def get_registered_function(func_name: str) -> FunctionNode | None: return _FunctionRegistry.instance().lookup(func_name) -# todo we can provide an API for registering new functions such as: -# def register_function(func: FunctionNode) + +def register_function(func: FunctionNode): + """Register an instance of a FunctionNode. """ + _FunctionRegistry.instance().register(func) diff --git a/killerbunny/parsing/node_type.py b/killerbunny/parsing/node_type.py index 7a17f2f..4c015b0 100644 --- a/killerbunny/parsing/node_type.py +++ b/killerbunny/parsing/node_type.py @@ -55,7 +55,7 @@ class ASTNodeType(Enum): MEMBER_NAME_SHORTHAND = "member_name_shorthand", "mns" FUNCTION = "function", "func" - FUNCTION_PARAM = "funciton_param", "param" + FUNCTION_PARAM = "function_param", "param" FUNC_PARAM_LIST = "func_param_list", "param_list" FUNCTION_CALL = "function_call", "func()" FUNCTION_ARG = "function_arg", "arg" diff --git a/killerbunny/parsing/parse_result.py b/killerbunny/parsing/parse_result.py index 66ccc91..8a815e5 100644 --- a/killerbunny/parsing/parse_result.py +++ b/killerbunny/parsing/parse_result.py @@ -14,12 +14,13 @@ # PARSE RESULT #################################################################### +# noinspection GrazieInspection class ParseResult: """Manages error detection and reporting across recursively nested grammar productions. Usage: 1. Initialize a ParseResult at the start of a grammar production method. - E.g. production rule: foo ::= $ bar + E.g., production rule: foo ::= $ bar def foo(self): res = ParseResult() @@ -33,14 +34,14 @@ def foo(self): res.register_advancement() self.advance() - 3. When calling a sub-production method (like bar above), wrap the call in a res.register() call + 3. When calling a sub-production method (like `bar` above), wrap the call in a res.register() call ... node = res.register(self.bar()) 4. Check for error after calling res.register(): ... node = res.register(self.bar()) - if res.error return res # abort production rule and propogate error up the call stack + if res.error return res # abort production rule and propogate Error up the call stack 5. If a parse error occurs in the current parse method, return res.failure() @@ -49,28 +50,46 @@ def foo(self): if current_token != $: res.failure(SomeErrorClass(error_position, "Expected $")) - 6. If the happy path is reached, return res.success() with the object (usually a node) to be returned to caller + 6. If the happy path is reached, return res.success() with the object (usually a node) to be returned to the caller def foo(self): res = ParseResult() ... node = res.register(self.bar()) if res.error return res - # assuming parse succeeded here + # assuming parse succeeded here: return res.success(node) # can also use return res.success(None) to indicate success without a return object + 7. If a production may or may not be expected to successfully parse, use try_register(). This attempts to parse. + If successful (i.e., no Error) works just like res.register(). If an Error occurrs, it returns None and + sets internal state `to_reverse_count` to allow backtracking. + + def foo(self): + res = ParseResult() + ... + logical_expr = res.try_register(self.logical_expr()) + if logical_expr is None: + self.backtrack(res.to_reverse_count) # restore state as it was before trying logical_expr() + ... + literal_node = res.register(self.string_literal()) # try a different production rule + ... + + General notes: -------------- 1. Call res.register_advancement() in a production method every time that method itself successfully consumes a token using self.advance(). 2. When a production method detects an error specific to its own rule (e.g., expected a specific token but found another), call res.failure(SpecificError(...)). - 3. Rely on res.register(sub_production()) to propagate errors from deeper levels. + 3. Rely on res.register() to propagate errors from deeper levels. The logic within register and failure will generally ensure that the error reported at the top level is the one that occurred at the earliest significant point of failure. 4. At the very top level of your parser (e.g., in your main parse() or start() method), check res.error. If it's set, that's the error to report to the user. + 5. When several productions are possible, use res.try_register(). If it returns None, the parsing failed. Call + self.backtrack(res.to_reverse_count) + and then try the next production. This system is designed to prevent generic error messages from higher-level productions from masking more specific errors from lower-level ones, especially once a higher-level production diff --git a/killerbunny/parsing/parser.py b/killerbunny/parsing/parser.py index 825606d..22140ce 100644 --- a/killerbunny/parsing/parser.py +++ b/killerbunny/parsing/parser.py @@ -63,7 +63,7 @@ def re_init(self) -> None: self.advance() def _update_current_token(self) -> None: - """Set the self.current_token to the token at the current self.token_index element in the tokens list.""" + """Set the self.current_token to the token at the current self.token_index element in the `tokens` list.""" if self.token_index < len(self.tokens): self.current_token = self.tokens[self.token_index] else: @@ -122,13 +122,13 @@ def subparse(self, production_name: str = "all" ) -> tuple[ list[ tuple[str, ASTNode] ] , list[ tuple[str, Error] ] ] : """Primarily a debugging method, used to parse grammar symbols other than "start". If no `prodution_name` is - given, we try to parse all the symbol methods in the SUBPARSE_NAMES list. If a `production_name` is provided, - we try to parse just that method. We return the result(s) of methods that don't produce a parse error. + given, we try to parse all the symbol methods in the SUBPARSE_NAMES list. If a `production_name` is provided, + we try to parse just that method. We return the result(s) of methods that don't produce a parse error. - :return a tuple of two lists. The first list contains successful parsings, the second list contains - the Errors for unsuccessful parsings. Each list element is a 2 item tuple, the first item is the name of the - production symbol method attempted, (see SUBPARSE_NAMES), - the second item is the ASTNode for a successful parse, (first list) or the Error + :return: a tuple of two lists. The first list contains successful parsings, the second list contains + the Errors for unsuccessful parsings. Each list element is a 2-item tuple. The first item is the name of the + production symbol method attempted (see SUBPARSE_NAMES). + The second item is the ASTNode for a successful parse, (first list) or the Error for an unsuccessful parse (second list). """ method_names: list[str] = SUBPARSE_NAMES @@ -140,18 +140,19 @@ def subparse(self, method_errors: list[ tuple[str, Error ]] = [] for method_name in method_names: - self.re_init() # reset parsing state to start of token list + self.re_init() # reset the parsing state to start of the token list method = getattr(self, method_name, self.no_method) # noinspection PyAttributeOutsideInit self.method_name = method_name # this assignment is just for the error message in no_method() res = method() - if not res.error and res.node and self.current_token.token_type == TokenType.EOF: # parsed entire text with no errors + if not res.error and res.node and self.current_token.token_type == TokenType.EOF: + # parsed the entire text with no errors parsing_successes.append( ( method_name, res.node ) ) elif res.error: method_errors.append( (method_name, res.error) ) elif res.node: # parsing succeeded with leftover tokens. - #parsing_successes.append( ( method_name, res.node ) ) + #parsing_successes.append(( method_name, res.node )) current_token: Token = self.current_token # parsing stopped here last_token = self.tokens[-1] error = Error( @@ -291,7 +292,7 @@ def child_segment(self) -> ParseResult: if self.current_token.token_type == TokenType.STAR: # type: ignore wc:WildcardSelectorNode = cast(WildcardSelectorNode,res.register(self.wildcard_selector())) if res.error: return res - # we want to convert dot-wildcard into a BracketedSelectorNode ( .* -> [*] ) for normalization + # we want to convert a dot-wildcard into a BracketedSelectorNode ( .* -> [*] ) for normalization node = self._convert_to_bracketed_selection(wc) elif self.current_token.token_type in (TokenType.IDENTIFIER, *JSON_KEYWORD_TOKEN_TYPES ): mns:MemberNameShorthandNode = cast(MemberNameShorthandNode,res.register(self.member_name_shorthand())) @@ -363,7 +364,7 @@ def descendant_segment(self) -> ParseResult: def member_name_shorthand(self) -> ParseResult: - """Assumes current token type is TokenType.IDENTIFIER. + """Assumes that the current token type is TokenType.IDENTIFIER. member_name_shorthand:IDENTIFIER @@ -416,7 +417,7 @@ def bracketed_selection(self) -> ParseResult: if selector_: selector_list.append(selector_) - # finished with optional selectors, look for the closing ] + # finished with optional selectors, look for the closing ']' if TokenType.RBRACKET != self.current_token.token_type: # type: ignore return res.failure(InvalidSyntaxError(self.current_token.position, f"Expected ',' or ']', found {self.current_token.token_type}")) @@ -489,7 +490,7 @@ def wildcard_selector(self) -> ParseResult: return res.success(node) def slice_selector(self) -> ParseResult: - """Assumes current token is slice-selector . """ + """Assumes that the current token is a slice-selector. """ res = ParseResult() match = re.match(bnf.SLICE_SELECTOR, self.current_token.value) if not match: @@ -571,7 +572,7 @@ def logical_expr(self) -> ParseResult: def logical_or_expr(self) -> ParseResult: """ - disjunction, binds less tightly than conjuntion + Disjunction binds less tightly than conjuntion logical_or_expr ::= logical_and_expression ( "||" logical_and_expression )* @@ -669,7 +670,7 @@ def basic_expr(self) -> ParseResult: """ res = ParseResult() if self.current_token.token_type == TokenType.NOT: - # next symbol is either paren_expr or test_expr + # the next symbol is either paren_expr or test_expr if self.peek_next_token().token_type == TokenType.LPAREN: node = res.register(self.paren_expr()) if res.error: return res @@ -694,7 +695,7 @@ def basic_expr(self) -> ParseResult: if res.error is None and node is not None: return res.success(node) - # if the comparison_expr failed, it may just mean it wasn't a comparison_expr. We'll clear the error so we can + # If the comparison_expr failed, it may just mean it wasn't a comparison_expr. We'll clear the error so we can # try to parse a test_expr(). If that fails too it's ambiguous as to which production was intended. self.backtrack(res.to_reverse_count) res.error = None @@ -929,6 +930,7 @@ def rel_query(self) -> ParseResult: cur_node_id = CurrentNodeIdentifier(at_token) rel_query_node = RelativeQueryNode(cur_node_id, cast(RepetitionNode, segments_node)) + rel_query_node.set_pos(at_token.position.text, at_token.position.start, self.current_token.position.start) return res.success(rel_query_node) @@ -1164,7 +1166,7 @@ def name_segment(self) -> ParseResult: name_segment ::= ( "[" name_selector:STRING_LITERAL "]" ) | ( "." member_name_shorthand:IDENTIFIER ) - :return: a NameSelectorNode in ParseResult.node. Converts a member_name_shorthand to NameSelectorNode. + :return: A NameSelectorNode in ParseResult.node. Converts a member_name_shorthand to NameSelectorNode. """ res = ParseResult() @@ -1290,7 +1292,7 @@ def function_expr(self) -> ParseResult: if self.current_token.token_type == TokenType.RPAREN: # type: ignore # no function_argument list res.register_advancement() - self.advance() # consume ')' after empty arg list + self.advance() # consume `)` after an empty arg list try: fcn = FunctionCallNode( function, @@ -1336,7 +1338,7 @@ def function_expr(self) -> ParseResult: res.register_advancement() self.advance() saved_token = self.current_token - func_arg = cast(FunctionArgument, res.register(self.function_argument())) + func_arg = cast(FunctionArgument, res.register( self.function_argument() )) if res.error: return res if func_arg is None: return res.failure( @@ -1363,13 +1365,13 @@ def function_expr(self) -> ParseResult: return res.failure( ValidationError(self.current_token.position, f"Expected {len(func_params)} arguments for function, got {arg_index + 1}")) - + if self.current_token.token_type != TokenType.RPAREN: # type: ignore return res.failure(InvalidSyntaxError(self.current_token.position, f"Expected ')', got {self.current_token.token_type}")) saved_token = self.current_token res.register_advancement() - self.advance() # consume ')' after argument list + self.advance() # consume ')' after the argument list try: fcn = FunctionCallNode( function, @@ -1393,12 +1395,6 @@ def function_argument(self) -> ParseResult: :return: """ res = ParseResult() - - # try parsing a logical_expr, - # then a filter_query - # then a function_expr, - # then a literal. - if self.current_token.token_type in FILTER_QUERY_FIRST_SET: # parse as filter query @@ -1424,7 +1420,7 @@ def function_argument(self) -> ParseResult: fa.set_pos(saved_token.position.text, saved_token.position.start, self.current_token.position.start) return res.success(fa) - # try parsing as logical_expr. If returns none or error, backtrack then try literal + # Try parsing as logical_expr. If returns none or error, backtrack and then try literal saved_token = self.current_token logical_expr = res.try_register(self.logical_expr()) if res.error or logical_expr is None: @@ -1478,7 +1474,7 @@ def identifier(self) -> ParseResult: def json_keyword(self) -> ParseResult: - """Parse json keywords true, false, and null and create literal nodes for them.""" + """Parse JSON keywords true, false, and null and create literal nodes for them.""" res = ParseResult() if self.current_token.token_type not in JSON_KEYWORD_TOKEN_TYPES: return res.failure( InvalidSyntaxError(self.current_token.position, @@ -1510,14 +1506,14 @@ def number_literal(self) -> ParseResult: def string_literal(self) -> ParseResult: """ - Assumes current token is string literal. + Assumes the current token is string literal. Included for completeness. Not currently used, as the only production that uses a string literal is name-selector, and that processes the string-literal directly. string_literal:STRING_LITERAL - :return: + :return: A new StringLiteralNode in ParseResult.node """ res = ParseResult() if self.current_token.token_type != TokenType.STRING: diff --git a/killerbunny/shared/json_type_defs.py b/killerbunny/shared/json_type_defs.py index 9ab0cec..688a1cc 100644 --- a/killerbunny/shared/json_type_defs.py +++ b/killerbunny/shared/json_type_defs.py @@ -12,7 +12,7 @@ JSON_PrimitiveType: TypeAlias = Union[ str, int, float, bool, None ] # RFC 8259, pg 3 calls these "primitive types" JSON_ArrayType: TypeAlias = Sequence[ 'JSON_ValueType' ] JSON_ObjectType: TypeAlias = Mapping[ str, 'JSON_ValueType'] -JSON_StructuredType: TypeAlias = Union[ JSON_ArrayType, JSON_ObjectType] #RFC 8259, pg 3 calls these "structured types" +JSON_StructuredType: TypeAlias = Union[ 'JSON_ArrayType', 'JSON_ObjectType'] #RFC 8259, pg 3 calls these "structured types" JSON_ValueType: TypeAlias = Union[ JSON_PrimitiveType, JSON_ArrayType, JSON_ObjectType] # For use in isinstance(), e.g., if isinstance(foo, JSON_xxx_TYPES): diff --git a/tests/jpath/parsing/test_table_14_functions.py b/tests/jpath/parsing/test_table_14_functions.py new file mode 100644 index 0000000..99f488b --- /dev/null +++ b/tests/jpath/parsing/test_table_14_functions.py @@ -0,0 +1,196 @@ +# File: test_table_14_functions.py +# Copyright (c) 2025 Robert L. Ross +# All rights reserved. +# Open-source license to come. +# Created by: Robert L. Ross +# +"""Table 14 in RFC 9535 references arbitrary functions bar(), bnl(), blt() and bal() but are not registered functions. + This test creates these functions and registers them so we can test them for well-formness and validity as indicated + in Table 14. + """ +import operator +from dataclasses import dataclass + +import pytest + +from killerbunny.lexing.lexer import JPathLexer +from killerbunny.parsing.function import FunctionNode, FunctionParam, FunctionParamType, VALUE_TYPES, ValueType, \ + LogicalType, NodesType, register_function +from killerbunny.parsing.node_type import ASTNode +from killerbunny.parsing.parse_result import ParseResult +from killerbunny.parsing.parser import JPathParser + + +#################################################################### +# FUNCTION DEFINITIONS +#################################################################### + +class BarFunction1(FunctionNode): + def __init__(self) -> None: + return_param = FunctionParam("return", FunctionParamType.LogicalType, LogicalType) + param1 = FunctionParam("value", FunctionParamType.ValueType, VALUE_TYPES ) + super().__init__("bar", return_param, [param1]) + + def eval(self,value: ValueType) -> LogicalType: # type: ignore + return LogicalType.value_for( True ) + +class BarFunction2(FunctionNode): + def __init__(self) -> None: + return_param = FunctionParam("return", FunctionParamType.LogicalType, LogicalType) + param1 = FunctionParam("value", FunctionParamType.NodesType, NodesType ) + super().__init__("bar", return_param, [param1]) + + def eval(self,value: NodesType) -> LogicalType: # type: ignore + return LogicalType.value_for( True ) + +class BarFunction3(FunctionNode): + def __init__(self) -> None: + return_param = FunctionParam("return", FunctionParamType.LogicalType, LogicalType) + param1 = FunctionParam("value", FunctionParamType.LogicalType, LogicalType ) + super().__init__("bar", return_param, [param1]) + + def eval(self,value: LogicalType) -> LogicalType: # type: ignore + return LogicalType.value_for( True ) + + +class BnlFunction1(FunctionNode): + def __init__(self) -> None: + return_param = FunctionParam("return", FunctionParamType.LogicalType, LogicalType) + param1 = FunctionParam("nodes", FunctionParamType.NodesType, NodesType) + super().__init__("bnl", return_param, [param1]) + self.test_name = "bnl1" + + def eval(self, param1: NodesType) -> LogicalType: # type: ignore + return LogicalType.value_for(True) + + +class BnlFunction2(FunctionNode): + def __init__(self) -> None: + return_param = FunctionParam("return", FunctionParamType.LogicalType, LogicalType) + param1 = FunctionParam("param1", FunctionParamType.LogicalType, LogicalType ) + super().__init__("bnl", return_param, [param1]) + self.test_name = "bnl2" + + def eval(self, param1: LogicalType) -> LogicalType: # type: ignore + return LogicalType.value_for(True) + + +class BltFunction(FunctionNode): + def __init__(self) -> None: + return_param = FunctionParam("return", FunctionParamType.LogicalType, LogicalType) + param1 = FunctionParam("param1", FunctionParamType.LogicalType, LogicalType ) + super().__init__("blt", return_param, [param1]) + + def eval(self, param1: LogicalType) -> LogicalType: # type: ignore + return LogicalType.value_for(True) + + +class BalFunction(FunctionNode): + def __init__(self) -> None: + return_param = FunctionParam("return", FunctionParamType.LogicalType, LogicalType) + param1 = FunctionParam("value", FunctionParamType.ValueType, VALUE_TYPES) + super().__init__("bal", return_param, [param1]) + + def eval(self, value: ValueType) -> LogicalType: # type: ignore + return LogicalType.value_for(True) + + +@dataclass(frozen=True, slots=True) +class ParserTestCase: + test_name : str + json_path : str + parser_ast : str + source_file_name : str + is_invalid : bool = False + err_msg : str = "" + function : FunctionNode | None = None + + +def parse_helper(case: ParserTestCase)-> ParseResult: + assert case.json_path is not None, f"JSON Query string cannot be None" + lexer = JPathLexer(case.source_file_name, case.json_path) + tokens, error = lexer.tokenize() + assert error is None, f"Unexpected error while lexing: {error}" + + parser = JPathParser(tokens) + result: ParseResult = parser.parse() + return result + +bar_test_cases: list[ParserTestCase] = [ + ParserTestCase("bar1-$[?bar(@.a)]", "$[?bar(@.a)]", "${CS{bs[fs{?bar(@ segments)->LogicalType}/fs]/bs}/CS}/$", "table_14.jpathl", False, "", BarFunction1()), + ParserTestCase("bar2-$[?bar(@.a)]", "$[?bar(@.a)]", "${CS{bs[fs{?bar(@ segments)->LogicalType}/fs]/bs}/CS}/$", "table_14.jpathl", False, "", BarFunction2()), + ParserTestCase("bar3-$[?bar(@.a)]", "$[?bar(@.a)]", "${CS{bs[fs{?bar(@ segments)->LogicalType}/fs]/bs}/CS}/$", "table_14.jpathl", False, "", BarFunction3()), +] +@pytest.mark.parametrize("case", bar_test_cases, ids=operator.attrgetter("test_name")) +def test_bar_cases(case: ParserTestCase) -> None: + assert case.function is not None + register_function(case.function) + result = parse_helper(case) + if case.is_invalid: + assert result.error is not None, f"Error was none, expected error: {result.error}" + assert result.error.as_test_string() == case.err_msg , f"Expect error message to be '{case.err_msg}'" + else: + assert result.error is None, f"Expected no error, got: {result.error}" + assert result.node is not None, f"ASTNode was none, expected node: {case.parser_ast}" + ast_node: ASTNode = result.node + assert str(ast_node) == case.parser_ast, f"Expected ASTNode to be '{case.parser_ast}', got '{str(ast_node)}'" + + + +bnl_test_cases: list[ ParserTestCase ] = [ + ParserTestCase("bnl1-$[?bnl(@.*)]", "$[?bnl(@.*)]", "${CS{bs[fs{?bnl(@ segments)->LogicalType}/fs]/bs}/CS}/$", "table_14.jpathl", False, "", BnlFunction1() ), + ParserTestCase("bnl2-$[?bnl(@.*)]", "$[?bnl(@.*)]", "${CS{bs[fs{?bnl(@ segments)->LogicalType}/fs]/bs}/CS}/$", "table_14.jpathl", False, "", BnlFunction2() ), +] +@pytest.mark.parametrize("case", bnl_test_cases, ids=operator.attrgetter("test_name")) +def test_bnl_cases(case: ParserTestCase) -> None: + + assert case.function is not None + register_function(case.function) + + result = parse_helper(case) + if case.is_invalid: + assert result.error is not None, f"Error was none, expected error: {result.error}" + assert result.error.as_test_string() == case.err_msg , f"Expect error message to be '{case.err_msg}'" + else: + assert result.error is None, f"Expected no error, got: {result.error}" + assert result.node is not None, f"ASTNode was none, expected node: {case.parser_ast}" + ast_node: ASTNode = result.node + assert str(ast_node) == case.parser_ast, f"Expected ASTNode to be {case.parser_ast}, got '{str(ast_node)}'" + + +blt_test_cases: list[ParserTestCase] = [ + ParserTestCase("blt-$[?blt(1==1)]", "$[?blt(1==1)]", "${CS{bs[fs{?blt(comp_expr(1, ==, 1))->LogicalType}/fs]/bs}/CS}/$", "table_14.jpathl"), + ParserTestCase("blt-$[?blt(1)]", "$[?blt(1)]", "", "table_14.jpathl", True, 'Validation Error: function_expr: Expected LogicalType but got ValueType at position 8: $[?blt(^1^)]'), +] +@pytest.mark.parametrize("case", blt_test_cases, ids=operator.attrgetter("test_name")) +def test_blt_cases(case: ParserTestCase) -> None: + register_function(BltFunction()) + + result = parse_helper(case) + if case.is_invalid: + assert result.error is not None, f"Error was none, expected error: {result.error}" + assert result.error.as_test_string() == case.err_msg , f"Expect error message to be '{case.err_msg}'" + else: + assert result.error is None, f"Expected no error, got: {result.error}" + assert result.node is not None, f"ASTNode was none, expected node: {case.parser_ast}" + ast_node: ASTNode = result.node + assert str(ast_node) == case.parser_ast, f"Expected ASTNode to be {case.parser_ast}, got '{str(ast_node)}'" + + + +bal_test_cases: list[ParserTestCase] = [ + ParserTestCase("bal-$[?bal(1)]", "$[?bal(1)]", "${CS{bs[fs{?bal(1)->LogicalType}/fs]/bs}/CS}/$", "table_14.jpathl"), +] +@pytest.mark.parametrize("case", bal_test_cases, ids=operator.attrgetter("test_name")) +def test_bal_cases(case: ParserTestCase) -> None: + register_function(BalFunction()) + + result = parse_helper(case) + if case.is_invalid: + assert result.error is not None, f"Error was none, expected error: {result.error}" + assert result.error.as_test_string() == case.err_msg, f"Expect error message to be '{case.err_msg}'" + else: + assert result.error is None, f"Expected no error, got: {result.error}" + assert result.node is not None, f"ASTNode was none, expected node: {case.parser_ast}" + ast_node: ASTNode = result.node + assert str(ast_node) == case.parser_ast, f"Expected ASTNode to be {case.parser_ast}, got '{str(ast_node)}'" \ No newline at end of file From 7362fd8a83f2e9d99311a2184e113b06fd323e42 Mon Sep 17 00:00:00 2001 From: killeroonie Date: Wed, 2 Jul 2025 21:35:54 -0700 Subject: [PATCH 9/9] Implemented and fixed evaluator unit tests following RFC 9535 tables: - **Evaluator Updates:** - Resolved bugs in `evaluator.py` causing strings to be misinterpreted as arrays. - Enhanced `ValueError` in `evaluator_types.py` with clearer escaped path details. - **Test Enhancements:** - Added unit test generation for `create_subparser_test_files()`. - Generated `evaluator_test_cases.json` and updated `parser_test_cases.json` and `lexer_test_cases.json` after uncommenting tests for `match()` and `search()` in `table_12.jpathl`. - Introduced two new tests in `test_helper.py` to verify escaping path fixes. - Ensured no log warnings in Cycle query test. - **Code Cleanup:** - Removed unused `_write_to_json_file` method in `test_cts.py`. - Polished grammar, comments, and removed redundancy in test files. --- killerbunny/evaluating/evaluator.py | 25 +- killerbunny/evaluating/evaluator_types.py | 2 +- killerbunny/shared/testgen.py | 155 +- tests/jpath/evaluating/cts/__init__.py | 6 - .../evaluating/evaluator_test_cases.json | 3101 +++++++++++++++++ tests/jpath/evaluating/{cts => }/test_cts.py | 22 +- .../test_evaluator_rfc9535_tables.py | 83 + tests/jpath/evaluating/{nts => }/test_nts.py | 7 +- .../evaluating/test_root_value_cycles.py | 40 +- tests/jpath/evaluating/test_table_11.py | 8 +- tests/jpath/lexing/lexer_test_cases.json | 14 + tests/jpath/parsing/parser_test_cases.json | 354 +- tests/jpath/parsing/test_helper.py | 4 + .../parsing/test_parser_rfc9535_tables.py | 1 + tests/jpath/rfc9535_examples/table_02.jpathl | 1 + tests/jpath/rfc9535_examples/table_03.jpathl | 1 + tests/jpath/rfc9535_examples/table_05.jpathl | 1 + tests/jpath/rfc9535_examples/table_06.jpathl | 1 + tests/jpath/rfc9535_examples/table_07.jpathl | 1 + tests/jpath/rfc9535_examples/table_09.jpathl | 1 + tests/jpath/rfc9535_examples/table_11.jpathl | 2 + tests/jpath/rfc9535_examples/table_12.jpathl | 5 +- tests/jpath/rfc9535_examples/table_14.jpathl | 3 + tests/jpath/rfc9535_examples/table_15.jpathl | 1 + tests/jpath/rfc9535_examples/table_16.jpathl | 1 + tests/jpath/rfc9535_examples/table_17.jpathl | 1 + tests/jpath/rfc9535_examples/table_18.jpathl | 1 + 27 files changed, 3677 insertions(+), 165 deletions(-) delete mode 100644 tests/jpath/evaluating/cts/__init__.py create mode 100644 tests/jpath/evaluating/evaluator_test_cases.json rename tests/jpath/evaluating/{cts => }/test_cts.py (91%) create mode 100644 tests/jpath/evaluating/test_evaluator_rfc9535_tables.py rename tests/jpath/evaluating/{nts => }/test_nts.py (95%) diff --git a/killerbunny/evaluating/evaluator.py b/killerbunny/evaluating/evaluator.py index 6d8e8f3..6d58389 100644 --- a/killerbunny/evaluating/evaluator.py +++ b/killerbunny/evaluating/evaluator.py @@ -23,6 +23,7 @@ StringValue ) from killerbunny.parsing.function import Nothing, LogicalType, ValueType, FunctionCallNode, FunctionArgument +from killerbunny.parsing.helper import unescape_string_content from killerbunny.parsing.node_type import ASTNode, ASTNodeType from killerbunny.parsing.parser_nodes import ( JsonPathQueryNode, @@ -136,12 +137,7 @@ def visit_JsonPathQueryNode(self, node: JsonPathQueryNode, context: Context) -> segment_output: VNodeList = input_nodelist # default, in case there are no segments segments: RepetitionNode = node.segments - if segments.is_empty(): - # no segments in the absolute query, so allow all input nodes - context.set_symbol(JPATH_QUERY_RESULT_NODE_KEY, input_nodelist) - return rt_res.success(BooleanValue.value_for(True).set_context(context).set_pos(node.position.copy())) - - + for seg_node in segments: # creating a new Context provides a local scope for the segment visit seg_context = Context("", context, seg_node.position ) @@ -193,13 +189,14 @@ def _collect_vnodes_and_their_descendants(self, collected_vnodes.append(cur_node) # add children to the queue for processing during the next iteration - if isinstance(jvalue, JSON_ARRAY_TYPES): + if isinstance(jvalue, JSON_ARRAY_TYPES) and not isinstance(jvalue, str): for index, element in enumerate(jvalue): new_node = VNode(NormalizedJPath(f"{jpath}[{index}]"), element, cur_node.root_value, cur_node.node_depth + 1 ) node_queue.append((new_node, depth + 1)) elif isinstance(jvalue, JSON_OBJECT_TYPES): - for name, value in jvalue.items(): + # noinspection PyUnresolvedReferences + for name, value in jvalue.items(): # type: ignore new_node = VNode(NormalizedJPath(f"{jpath}['{name}']"), value, cur_node.root_value, cur_node.node_depth + 1) node_queue.append((new_node, depth + 1)) @@ -220,7 +217,7 @@ def _children_of(self, parent_node: VNode) -> VNodeList: child_nodes: list[VNode] = [] instance_ids: dict[int, VNode] = {id(parent_node.jvalue):parent_node} - if isinstance(parent_node.jvalue, JSON_STRUCTURED_TYPES): + if isinstance(parent_node.jvalue, JSON_STRUCTURED_TYPES) and not isinstance(parent_node.jvalue, str): base_path = parent_node.jpath if isinstance(parent_node.jvalue, JSON_ARRAY_TYPES): for index, element in enumerate(parent_node.jvalue): @@ -364,9 +361,11 @@ def visit_NameSelectorNode(self, node: NameSelectorNode, context: Context) -> Ru jpath = input_node.jpath jvalue_dict = input_node.jvalue if node.member_name in jvalue_dict: + # we have to unescape because NormalizedJPath will re-escape the entire path, resulting in a double escaping of the starting path + unescaped_jpath = unescape_string_content(jpath.jpath_str) output_nodelist.append( - VNode( NormalizedJPath(f"{jpath}['{node.member_name}']"), jvalue_dict[ node.member_name ], - input_node.root_value, input_node.node_depth +1 ) + VNode( NormalizedJPath(f"{unescaped_jpath}['{node.member_name}']"), jvalue_dict[ node.member_name ], + input_node.root_value, input_node.node_depth + 1 ) ) return rt_res.success(VNodeList(output_nodelist)) @@ -404,7 +403,7 @@ def visit_SliceSelectorNode(self, node: SliceSelectorNode, context: Context) -> input_node: VNode current_slice_obj = node.slice_op for input_node in input_nodelist: - if not isinstance(input_node.jvalue, JSON_ARRAY_TYPES): + if not isinstance(input_node.jvalue, JSON_ARRAY_TYPES) or isinstance(input_node.jvalue, str): continue jpath = input_node.jpath jvalue_list = input_node.jvalue @@ -440,7 +439,7 @@ def visit_IndexSelectorNode(self, node: IndexSelectorNode, context: Context) -> ouput_nodes: list[VNode] = [] input_node: VNode for input_node in input_nodelist: - if not isinstance(input_node.jvalue, JSON_ARRAY_TYPES): + if not isinstance(input_node.jvalue, JSON_ARRAY_TYPES) or isinstance(input_node.jvalue, str): continue jpath = input_node.jpath jvalue = input_node.jvalue diff --git a/killerbunny/evaluating/evaluator_types.py b/killerbunny/evaluating/evaluator_types.py index d4ae55a..a25615a 100644 --- a/killerbunny/evaluating/evaluator_types.py +++ b/killerbunny/evaluating/evaluator_types.py @@ -74,7 +74,7 @@ def normalize_path(jpath_str: str) -> str: match = NORMALIZED_PATH_RE.match(escaped_path) if match: return escaped_path - else: raise ValueError(f"jpath_str: '{jpath_str}' is not in normal format.") + else: raise ValueError(f"jpath_str: '{jpath_str}' (escaped: '{escaped_path}') is not in normal format.") # todo : to verify that a jpath is well-formed we actualy need to run it through the lexer and parser! # this produces an AST, however. So we would need a special evalute method in the Evaluator to construct the # jpath and verify it only contains legitimate jpath syntax. diff --git a/killerbunny/shared/testgen.py b/killerbunny/shared/testgen.py index 0408dc2..cc1bd51 100644 --- a/killerbunny/shared/testgen.py +++ b/killerbunny/shared/testgen.py @@ -9,13 +9,14 @@ """Functions for creating test data files for the Lexer, Parser, and Evaluator unit tests""" import json -from dataclasses import dataclass, asdict, is_dataclass +from dataclasses import dataclass, asdict, is_dataclass, field from pathlib import Path from typing import cast, Any from killerbunny.evaluating.evaluator import JPathEvaluator from killerbunny.evaluating.runtime_result import RuntimeResult from killerbunny.evaluating.value_nodes import VNodeList +from killerbunny.evaluating.well_formed_query import WellFormedValidQuery from killerbunny.lexing.lexer import JPathLexer from killerbunny.lexing.tokens import Token from killerbunny.parsing.node_type import ASTNode @@ -50,6 +51,7 @@ FRAGILE_TEST_DIR_PATH = Path(FRAGILE_TEST_DIR) LEXER_TEST_CASES_FILENAME = "lexer_test_cases.json" PARSER_TEST_CASES_FILENAME = "parser_test_cases.json" +EVALUATOR_TEST_CASES_FILENAME = "evaluator_test_cases.json" def load_obj_from_json_file(input_file: Path) -> JSON_ValueType: @@ -312,7 +314,8 @@ def display_test_cases(test_cases: list[ Any ], file_name: str) -> None: for test_case in test_cases: if is_dataclass(test_case): dict_ = asdict(test_case) - value = dict_.get("lexer_tokens", None) or dict_.get("parser_ast", None) or '' + value = (dict_.get("lexer_tokens", None) or dict_.get("parser_ast", None) or + dict_.get("results_values", None) or '') msg = dict_["err_msg"] if dict_["is_invalid"] else value print(f"{dict_['json_path']}{JPATH_DATA_SEPARATOR}{msg}") else: @@ -386,9 +389,25 @@ def process_parser_paths(input_dir: Path, # EVALUATOR TEST GENERATION #################################################################### -def evaluate_jpath_str(file_name:str, jpath_query_str: str, json_value:JSON_ValueType) -> VNodeList: + +@dataclass(frozen=True, slots=True) +class EvaluatorTestCase: + test_name : str + json_path : str + root_value : JSON_ValueType + source_file_name : str + is_invalid : bool = False + err_msg : str = "" + subparse_production: str| None = None + results_values: list[ JSON_ValueType ] = field(default_factory=list) + results_paths : list[ list[str] ] = field(default_factory=list) + +def evaluate_jpath_str(file_name:str, jpath_query_str: str, json_value:JSON_ValueType) -> EvaluatorTestCase: """Lex and parse the JSON Path query string and evalutate it with the JSON value in the argument. Return a VNodeList""" + + # print(f"evaluate_jpath_str({file_name}, {jpath_query_str}, {json_value})") + lexer = JPathLexer(file_name, jpath_query_str) tokens, error = lexer.tokenize() if error: @@ -398,6 +417,7 @@ def evaluate_jpath_str(file_name:str, jpath_query_str: str, json_value:JSON_Valu parser = JPathParser(tokens) result: ParseResult = parser.parse() if result.error: + # Evaluator tests shouldn't result in errors. Error cases are tested in lexing and parsing tests. result_str = result.error.as_test_string() raise ValueError(result_str) @@ -411,22 +431,106 @@ def evaluate_jpath_str(file_name:str, jpath_query_str: str, json_value:JSON_Valu if rt_result.error: raise ValueError(f"Evaluator returned Error: {rt_result.error} for query string: {jpath_query_str}") if rt_result.value is not None: - return rt_result.value + # print(f"ast_node: {ast_node}, value: {rt_result.value}, type(value) = {type(rt_result.value)}") + test_name = f"{file_name}-{jpath_query_str}" + values: list[JSON_ValueType] = list(cast(VNodeList, rt_result.value).values()) + #values_json = json.dumps(values) + paths: list[str] = [ npath.jpath_str for npath in cast(VNodeList, rt_result.value).paths() ] + test_case = EvaluatorTestCase(test_name, jpath_query_str, json_value, file_name, False, "", None, [values], [paths],) + return test_case else: raise RuntimeError(f"Evaluator returned null value for query string: {jpath_query_str}") -def generate_evaluator_path_nodelist(input_path: Path, json_value: JSON_ValueType) -> list[ tuple[ str, VNodeList] ]: - result_list: list[ tuple[ str, VNodeList] ] = [] +def generate_evaluator_test_cases(input_path: Path) -> list[ EvaluatorTestCase ]: + result_list: list[ EvaluatorTestCase ] = [] file_name: str = input_path.name + json_value: JSON_ValueType + root_value: dict[str, JSON_ValueType] = {} with open(input_path, "r", encoding=UTF8, buffering=ONE_MEBIBYTE) as input_file: for line in input_file: line_stripped = line.strip() - if line_stripped == '' or line_stripped.startswith("#"): - continue # ignore comment lines or blank lines - result_nodelist: VNodeList = evaluate_jpath_str(file_name, line_stripped, json_value) - result_list.append( (line_stripped, result_nodelist) ) + if line_stripped == '': + continue # ignore blank lines + if line_stripped.startswith("#"): + if line_stripped.find('json_file:') != -1: + data_file_name = line_stripped.lstrip(' #').partition(':')[2].strip() + data_file_path = input_path.parent / data_file_name + with open(data_file_path, "r", encoding=UTF8, buffering=ONE_MEBIBYTE) as data_file: + root_value = json.load(data_file) + + continue + test_case: EvaluatorTestCase = evaluate_jpath_str(file_name, line_stripped, root_value) + result_list.append( test_case ) return result_list + + +def process_evaluator_paths(input_dir: Path, + suffix: str, + output_dir: Path | None = None, + generate_test_file: bool = False, + quiet:bool = False)-> None: + + """Given a directory path and a file suffix, run generate_evaluator_path_nodelist() for each file in the directory + that ends with `suffix`. No directory recursion is done. Each input file contains a list of JSON Path query strings. + + This is a utility function to help create test data for evaluator unit testing. + The open() call for creating the test file uses the 'x' mode flag, so it will report an error + if the file already exists. + + If `generate_test_file` is True, create a JSON test file containing each matching input file's test data + and save it as: tests/jpath/parsing/evaluator_test_cases.json. + + If `quiet` is True, omits writing most output to the console, except for warning or error messages. + """ + if not quiet: + print(f"*** Processing jpathl files for the Evaluator") + + if not input_dir.is_dir(): + raise FileNotFoundError(f'Input directory {input_dir} does not exist') + if generate_test_file and output_dir is None: + raise ValueError(f"`generate_test_files` is True yet no output directory was specified.") + + if generate_test_file and output_dir is not None: + # ensure test directory exists + output_dir.mkdir(parents=True, exist_ok=True) + if not quiet: + print(f"\nGenerating test files in {output_dir}") + + generated_files: list[str] = [] + test_cases: list[EvaluatorTestCase] = [] + for file in sorted(input_dir.iterdir()): + if file.name in ("table_11.jpathl", "table_14.jpathl", "table_18.jpathl"): + continue # skip for now as we already have a test module for this + if file.name.endswith(suffix): + if not quiet: + print(f"\nProcessing '{file}'") + file_results: list[ EvaluatorTestCase ] = generate_evaluator_test_cases(file) + if not file_results: + print(f"Warning: file '{file}' produced no results. Is it empty?") + continue + + if not quiet: + #display_test_cases(file_results, f"{input_dir.name}/{file.name}") + print(f"{input_dir.name}/{file.name} results:") + for test_case in file_results: + print(f"\t{asdict(test_case)}") + + if generate_test_file: + test_cases.extend(file_results) + + if generate_test_file and test_cases: + outfile_path = output_dir / EVALUATOR_TEST_CASES_FILENAME # type: ignore + if not quiet: print(f"Generating test file '{outfile_path}'") + generated_files.append( outfile_path.name ) + write_test_case_file(outfile_path, "Evaluator tests of example paths in RFC 9535 tables 2-18.", test_cases) + + if generated_files and not quiet: + print(f"\nGenerated a total of {len(generated_files)} evaluator test files:") + for line in sorted(generated_files): + print(f" {line}" ) + + ################################################################################################################## @@ -459,17 +563,46 @@ def create_subparser_test_files() -> None: production_name="comparison_expr", generate_test_file=True ) + +def create_evaluator_test_files() -> None: + """PRODUCTION CODE""" + input_dir = FRAGILE_TEST_DIR_PATH / "jpath/rfc9535_examples/" + output_dir = FRAGILE_TEST_DIR_PATH / "jpath/evaluating/" + process_evaluator_paths(input_dir=input_dir, suffix="jpathl", output_dir=output_dir, generate_test_file=True) + def create_all_test_files() -> None: """PRODUCTION CODE""" create_lexer_test_files() create_parser_test_files() create_subparser_test_files() + create_evaluator_test_files() + + +def t1() -> None: + root_value: dict[str, JSON_ValueType] = { + "a": [3, 5, 1, 2, 4, 6, + {"b": "j"}, + {"b": "k"}, + {"b": {}}, + {"b": "kilo"} + ], + "o": {"p": 1, "q": 2, "r": 3, "s": 5, "t": {"u": 6}}, + "e": "f" + } + + jpath_query_str = '$[?@.*]' + query = WellFormedValidQuery.from_str(jpath_query_str) + results = query.eval(root_value) + print(f"results = {results}") def main() -> None: + pass #create_lexer_test_files() #create_parser_test_files() - create_subparser_test_files() + #create_subparser_test_files() + # create_evaluator_test_files() + # t1() if __name__ == '__main__': main() diff --git a/tests/jpath/evaluating/cts/__init__.py b/tests/jpath/evaluating/cts/__init__.py deleted file mode 100644 index 9393926..0000000 --- a/tests/jpath/evaluating/cts/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# File: __init__.py -# Copyright (c) 2025 Robert L. Ross -# All rights reserved. -# Open-source license to come. -# Created by: Robert L. Ross -# diff --git a/tests/jpath/evaluating/evaluator_test_cases.json b/tests/jpath/evaluating/evaluator_test_cases.json new file mode 100644 index 0000000..eb7adfb --- /dev/null +++ b/tests/jpath/evaluating/evaluator_test_cases.json @@ -0,0 +1,3101 @@ +{ + "description": "Evaluator tests of example paths in RFC 9535 tables 2-18. This file is autogenerated, do not edit.", + "tests": [ + { + "test_name": "table_02.jpathl-$.store.book[*].author", + "json_path": "$.store.book[*].author", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "Nigel Rees", + "Evelyn Waugh", + "Herman Melville", + "J. R. R. Tolkien" + ] + ], + "results_paths": [ + [ + "$['store']['book'][0]['author']", + "$['store']['book'][1]['author']", + "$['store']['book'][2]['author']", + "$['store']['book'][3]['author']" + ] + ] + }, + { + "test_name": "table_02.jpathl-$..author", + "json_path": "$..author", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "Nigel Rees", + "Evelyn Waugh", + "Herman Melville", + "J. R. R. Tolkien" + ] + ], + "results_paths": [ + [ + "$['store']['book'][0]['author']", + "$['store']['book'][1]['author']", + "$['store']['book'][2]['author']", + "$['store']['book'][3]['author']" + ] + ] + }, + { + "test_name": "table_02.jpathl-$.store.*", + "json_path": "$.store.*", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + { + "color": "red", + "price": 399 + } + ] + ], + "results_paths": [ + [ + "$['store']['book']", + "$['store']['bicycle']" + ] + ] + }, + { + "test_name": "table_02.jpathl-$.store..price", + "json_path": "$.store..price", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 399, + 8.95, + 12.99, + 8.99, + 22.99 + ] + ], + "results_paths": [ + [ + "$['store']['bicycle']['price']", + "$['store']['book'][0]['price']", + "$['store']['book'][1]['price']", + "$['store']['book'][2]['price']", + "$['store']['book'][3]['price']" + ] + ] + }, + { + "test_name": "table_02.jpathl-$..book[2]", + "json_path": "$..book[2]", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + } + ] + ], + "results_paths": [ + [ + "$['store']['book'][2]" + ] + ] + }, + { + "test_name": "table_02.jpathl-$..book[2].author", + "json_path": "$..book[2].author", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "Herman Melville" + ] + ], + "results_paths": [ + [ + "$['store']['book'][2]['author']" + ] + ] + }, + { + "test_name": "table_02.jpathl-$..book[2].publisher", + "json_path": "$..book[2].publisher", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [] + ], + "results_paths": [ + [] + ] + }, + { + "test_name": "table_02.jpathl-$..book[-1]", + "json_path": "$..book[-1]", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ] + ], + "results_paths": [ + [ + "$['store']['book'][3]" + ] + ] + }, + { + "test_name": "table_02.jpathl-$..book[0,1]", + "json_path": "$..book[0,1]", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + } + ] + ], + "results_paths": [ + [ + "$['store']['book'][0]", + "$['store']['book'][1]" + ] + ] + }, + { + "test_name": "table_02.jpathl-$..book[:2]", + "json_path": "$..book[:2]", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + } + ] + ], + "results_paths": [ + [ + "$['store']['book'][0]", + "$['store']['book'][1]" + ] + ] + }, + { + "test_name": "table_02.jpathl-$..book[?@.isbn]", + "json_path": "$..book[?@.isbn]", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ] + ], + "results_paths": [ + [ + "$['store']['book'][2]", + "$['store']['book'][3]" + ] + ] + }, + { + "test_name": "table_02.jpathl-$..book[?@.price<10]", + "json_path": "$..book[?@.price<10]", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + } + ] + ], + "results_paths": [ + [ + "$['store']['book'][0]", + "$['store']['book'][2]" + ] + ] + }, + { + "test_name": "table_02.jpathl-$..*", + "json_path": "$..*", + "root_value": { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + } + }, + "source_file_name": "table_02.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 399 + } + }, + [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + { + "color": "red", + "price": 399 + }, + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + }, + "red", + 399, + "reference", + "Nigel Rees", + "Sayings of the Century", + 8.95, + "fiction", + "Evelyn Waugh", + "Sword of Honour", + 12.99, + "fiction", + "Herman Melville", + "Moby Dick", + "0-553-21311-3", + 8.99, + "fiction", + "J. R. R. Tolkien", + "The Lord of the Rings", + "0-395-19395-8", + 22.99 + ] + ], + "results_paths": [ + [ + "$['store']", + "$['store']['book']", + "$['store']['bicycle']", + "$['store']['book'][0]", + "$['store']['book'][1]", + "$['store']['book'][2]", + "$['store']['book'][3]", + "$['store']['bicycle']['color']", + "$['store']['bicycle']['price']", + "$['store']['book'][0]['category']", + "$['store']['book'][0]['author']", + "$['store']['book'][0]['title']", + "$['store']['book'][0]['price']", + "$['store']['book'][1]['category']", + "$['store']['book'][1]['author']", + "$['store']['book'][1]['title']", + "$['store']['book'][1]['price']", + "$['store']['book'][2]['category']", + "$['store']['book'][2]['author']", + "$['store']['book'][2]['title']", + "$['store']['book'][2]['isbn']", + "$['store']['book'][2]['price']", + "$['store']['book'][3]['category']", + "$['store']['book'][3]['author']", + "$['store']['book'][3]['title']", + "$['store']['book'][3]['isbn']", + "$['store']['book'][3]['price']" + ] + ] + }, + { + "test_name": "table_03.jpathl-$", + "json_path": "$", + "root_value": { + "k": "v" + }, + "source_file_name": "table_03.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "k": "v" + } + ] + ], + "results_paths": [ + [ + "$" + ] + ] + }, + { + "test_name": "table_05.jpathl-$.o['j j']", + "json_path": "$.o['j j']", + "root_value": { + "o": { + "j j": { + "k.k": 3 + } + }, + "'": { + "@": 2 + } + }, + "source_file_name": "table_05.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "k.k": 3 + } + ] + ], + "results_paths": [ + [ + "$['o']['j j']" + ] + ] + }, + { + "test_name": "table_05.jpathl-$.o['j j']['k.k']", + "json_path": "$.o['j j']['k.k']", + "root_value": { + "o": { + "j j": { + "k.k": 3 + } + }, + "'": { + "@": 2 + } + }, + "source_file_name": "table_05.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 3 + ] + ], + "results_paths": [ + [ + "$['o']['j j']['k.k']" + ] + ] + }, + { + "test_name": "table_05.jpathl-$.o[\"j j\"][\"k.k\"]", + "json_path": "$.o[\"j j\"][\"k.k\"]", + "root_value": { + "o": { + "j j": { + "k.k": 3 + } + }, + "'": { + "@": 2 + } + }, + "source_file_name": "table_05.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 3 + ] + ], + "results_paths": [ + [ + "$['o']['j j']['k.k']" + ] + ] + }, + { + "test_name": "table_05.jpathl-$['o']['j j']['k.k']", + "json_path": "$['o']['j j']['k.k']", + "root_value": { + "o": { + "j j": { + "k.k": 3 + } + }, + "'": { + "@": 2 + } + }, + "source_file_name": "table_05.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 3 + ] + ], + "results_paths": [ + [ + "$['o']['j j']['k.k']" + ] + ] + }, + { + "test_name": "table_05.jpathl-$[\"'\"][\"@\"]", + "json_path": "$[\"'\"][\"@\"]", + "root_value": { + "o": { + "j j": { + "k.k": 3 + } + }, + "'": { + "@": 2 + } + }, + "source_file_name": "table_05.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 2 + ] + ], + "results_paths": [ + [ + "$['\\'']['@']" + ] + ] + }, + { + "test_name": "table_06.jpathl-$[*]", + "json_path": "$[*]", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3 + ] + }, + "source_file_name": "table_06.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "j": 1, + "k": 2 + }, + [ + 5, + 3 + ] + ] + ], + "results_paths": [ + [ + "$['o']", + "$['a']" + ] + ] + }, + { + "test_name": "table_06.jpathl-$.o[*]", + "json_path": "$.o[*]", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3 + ] + }, + "source_file_name": "table_06.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 1, + 2 + ] + ], + "results_paths": [ + [ + "$['o']['j']", + "$['o']['k']" + ] + ] + }, + { + "test_name": "table_06.jpathl-$.o[*]", + "json_path": "$.o[*]", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3 + ] + }, + "source_file_name": "table_06.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 1, + 2 + ] + ], + "results_paths": [ + [ + "$['o']['j']", + "$['o']['k']" + ] + ] + }, + { + "test_name": "table_06.jpathl-$.o[*, *]", + "json_path": "$.o[*, *]", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3 + ] + }, + "source_file_name": "table_06.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 1, + 2, + 1, + 2 + ] + ], + "results_paths": [ + [ + "$['o']['j']", + "$['o']['k']", + "$['o']['j']", + "$['o']['k']" + ] + ] + }, + { + "test_name": "table_06.jpathl-$.a[*]", + "json_path": "$.a[*]", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3 + ] + }, + "source_file_name": "table_06.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 5, + 3 + ] + ], + "results_paths": [ + [ + "$['a'][0]", + "$['a'][1]" + ] + ] + }, + { + "test_name": "table_07.jpathl-$[1]", + "json_path": "$[1]", + "root_value": [ + "a", + "b" + ], + "source_file_name": "table_07.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "b" + ] + ], + "results_paths": [ + [ + "$[1]" + ] + ] + }, + { + "test_name": "table_07.jpathl-$[-2]", + "json_path": "$[-2]", + "root_value": [ + "a", + "b" + ], + "source_file_name": "table_07.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "a" + ] + ], + "results_paths": [ + [ + "$[0]" + ] + ] + }, + { + "test_name": "table_09.jpathl-$[1:3]", + "json_path": "$[1:3]", + "root_value": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "source_file_name": "table_09.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "b", + "c" + ] + ], + "results_paths": [ + [ + "$[1]", + "$[2]" + ] + ] + }, + { + "test_name": "table_09.jpathl-$[5:]", + "json_path": "$[5:]", + "root_value": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "source_file_name": "table_09.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "f", + "g" + ] + ], + "results_paths": [ + [ + "$[5]", + "$[6]" + ] + ] + }, + { + "test_name": "table_09.jpathl-$[1:5:2]", + "json_path": "$[1:5:2]", + "root_value": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "source_file_name": "table_09.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "b", + "d" + ] + ], + "results_paths": [ + [ + "$[1]", + "$[3]" + ] + ] + }, + { + "test_name": "table_09.jpathl-$[5:1:-2]", + "json_path": "$[5:1:-2]", + "root_value": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "source_file_name": "table_09.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "f", + "d" + ] + ], + "results_paths": [ + [ + "$[5]", + "$[3]" + ] + ] + }, + { + "test_name": "table_09.jpathl-$[::-1]", + "json_path": "$[::-1]", + "root_value": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "source_file_name": "table_09.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "g", + "f", + "e", + "d", + "c", + "b", + "a" + ] + ], + "results_paths": [ + [ + "$[6]", + "$[5]", + "$[4]", + "$[3]", + "$[2]", + "$[1]", + "$[0]" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.a[?@.b == 'kilo']", + "json_path": "$.a[?@.b == 'kilo']", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "b": "kilo" + } + ] + ], + "results_paths": [ + [ + "$['a'][9]" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.a[?(@.b == 'kilo')]", + "json_path": "$.a[?(@.b == 'kilo')]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "b": "kilo" + } + ] + ], + "results_paths": [ + [ + "$['a'][9]" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.a[?@>3.5]", + "json_path": "$.a[?@>3.5]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 5, + 4, + 6 + ] + ], + "results_paths": [ + [ + "$['a'][1]", + "$['a'][4]", + "$['a'][5]" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.a[?@.b]", + "json_path": "$.a[?@.b]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ] + ], + "results_paths": [ + [ + "$['a'][6]", + "$['a'][7]", + "$['a'][8]", + "$['a'][9]" + ] + ] + }, + { + "test_name": "table_12.jpathl-$[?@.*]", + "json_path": "$[?@.*]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + } + ] + ], + "results_paths": [ + [ + "$['a']", + "$['o']" + ] + ] + }, + { + "test_name": "table_12.jpathl-$[?@[?@.b]]", + "json_path": "$[?@[?@.b]]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ] + ] + ], + "results_paths": [ + [ + "$['a']" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.o[?@<3, ?@<3]", + "json_path": "$.o[?@<3, ?@<3]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 1, + 2, + 1, + 2 + ] + ], + "results_paths": [ + [ + "$['o']['p']", + "$['o']['q']", + "$['o']['p']", + "$['o']['q']" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.a[?@<2 || @.b == \"k\"]", + "json_path": "$.a[?@<2 || @.b == \"k\"]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 1, + { + "b": "k" + } + ] + ], + "results_paths": [ + [ + "$['a'][2]", + "$['a'][7]" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.a[?match(@.b, \"[jk]\")]", + "json_path": "$.a[?match(@.b, \"[jk]\")]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "b": "j" + }, + { + "b": "k" + } + ] + ], + "results_paths": [ + [ + "$['a'][6]", + "$['a'][7]" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.a[?search(@.b, \"[jk]\")]", + "json_path": "$.a[?search(@.b, \"[jk]\")]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": "kilo" + } + ] + ], + "results_paths": [ + [ + "$['a'][6]", + "$['a'][7]", + "$['a'][9]" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.o[?@>1 && @<4]", + "json_path": "$.o[?@>1 && @<4]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 2, + 3 + ] + ], + "results_paths": [ + [ + "$['o']['q']", + "$['o']['r']" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.o[?@>1 && @<4]", + "json_path": "$.o[?@>1 && @<4]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 2, + 3 + ] + ], + "results_paths": [ + [ + "$['o']['q']", + "$['o']['r']" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.o[?@.u || @.x]", + "json_path": "$.o[?@.u || @.x]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "u": 6 + } + ] + ], + "results_paths": [ + [ + "$['o']['t']" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.a[?@.b == $.x]", + "json_path": "$.a[?@.b == $.x]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 3, + 5, + 1, + 2, + 4, + 6 + ] + ], + "results_paths": [ + [ + "$['a'][0]", + "$['a'][1]", + "$['a'][2]", + "$['a'][3]", + "$['a'][4]", + "$['a'][5]" + ] + ] + }, + { + "test_name": "table_12.jpathl-$.a[?@ == @]", + "json_path": "$.a[?@ == @]", + "root_value": { + "a": [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ], + "o": { + "p": 1, + "q": 2, + "r": 3, + "s": 5, + "t": { + "u": 6 + } + }, + "e": "f" + }, + "source_file_name": "table_12.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 3, + 5, + 1, + 2, + 4, + 6, + { + "b": "j" + }, + { + "b": "k" + }, + { + "b": {} + }, + { + "b": "kilo" + } + ] + ], + "results_paths": [ + [ + "$['a'][0]", + "$['a'][1]", + "$['a'][2]", + "$['a'][3]", + "$['a'][4]", + "$['a'][5]", + "$['a'][6]", + "$['a'][7]", + "$['a'][8]", + "$['a'][9]" + ] + ] + }, + { + "test_name": "table_15.jpathl-$[0, 3]", + "json_path": "$[0, 3]", + "root_value": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "source_file_name": "table_15.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "a", + "d" + ] + ], + "results_paths": [ + [ + "$[0]", + "$[3]" + ] + ] + }, + { + "test_name": "table_15.jpathl-$[0:2, 5]", + "json_path": "$[0:2, 5]", + "root_value": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "source_file_name": "table_15.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "a", + "b", + "f" + ] + ], + "results_paths": [ + [ + "$[0]", + "$[1]", + "$[5]" + ] + ] + }, + { + "test_name": "table_15.jpathl-$[0, 0]", + "json_path": "$[0, 0]", + "root_value": [ + "a", + "b", + "c", + "d", + "e", + "f", + "g" + ], + "source_file_name": "table_15.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + "a", + "a" + ] + ], + "results_paths": [ + [ + "$[0]", + "$[0]" + ] + ] + }, + { + "test_name": "table_16.jpathl-$..j", + "json_path": "$..j", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ] + }, + "source_file_name": "table_16.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 1, + 4 + ] + ], + "results_paths": [ + [ + "$['o']['j']", + "$['a'][2][0]['j']" + ] + ] + }, + { + "test_name": "table_16.jpathl-$..j", + "json_path": "$..j", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ] + }, + "source_file_name": "table_16.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 1, + 4 + ] + ], + "results_paths": [ + [ + "$['o']['j']", + "$['a'][2][0]['j']" + ] + ] + }, + { + "test_name": "table_16.jpathl-$..[0]", + "json_path": "$..[0]", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ] + }, + "source_file_name": "table_16.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 5, + { + "j": 4 + } + ] + ], + "results_paths": [ + [ + "$['a'][0]", + "$['a'][2][0]" + ] + ] + }, + { + "test_name": "table_16.jpathl-$..[*]", + "json_path": "$..[*]", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ] + }, + "source_file_name": "table_16.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "j": 1, + "k": 2 + }, + [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ], + 1, + 2, + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ], + { + "j": 4 + }, + { + "k": 6 + }, + 4, + 6 + ] + ], + "results_paths": [ + [ + "$['o']", + "$['a']", + "$['o']['j']", + "$['o']['k']", + "$['a'][0]", + "$['a'][1]", + "$['a'][2]", + "$['a'][2][0]", + "$['a'][2][1]", + "$['a'][2][0]['j']", + "$['a'][2][1]['k']" + ] + ] + }, + { + "test_name": "table_16.jpathl-$..*", + "json_path": "$..*", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ] + }, + "source_file_name": "table_16.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "j": 1, + "k": 2 + }, + [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ], + 1, + 2, + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ], + { + "j": 4 + }, + { + "k": 6 + }, + 4, + 6 + ] + ], + "results_paths": [ + [ + "$['o']", + "$['a']", + "$['o']['j']", + "$['o']['k']", + "$['a'][0]", + "$['a'][1]", + "$['a'][2]", + "$['a'][2][0]", + "$['a'][2][1]", + "$['a'][2][0]['j']", + "$['a'][2][1]['k']" + ] + ] + }, + { + "test_name": "table_16.jpathl-$..o", + "json_path": "$..o", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ] + }, + "source_file_name": "table_16.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + { + "j": 1, + "k": 2 + } + ] + ], + "results_paths": [ + [ + "$['o']" + ] + ] + }, + { + "test_name": "table_16.jpathl-$.o..[*, *]", + "json_path": "$.o..[*, *]", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ] + }, + "source_file_name": "table_16.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 1, + 2, + 1, + 2 + ] + ], + "results_paths": [ + [ + "$['o']['j']", + "$['o']['k']", + "$['o']['j']", + "$['o']['k']" + ] + ] + }, + { + "test_name": "table_16.jpathl-$.a..[0, 1]", + "json_path": "$.a..[0, 1]", + "root_value": { + "o": { + "j": 1, + "k": 2 + }, + "a": [ + 5, + 3, + [ + { + "j": 4 + }, + { + "k": 6 + } + ] + ] + }, + "source_file_name": "table_16.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 5, + 3, + { + "j": 4 + }, + { + "k": 6 + } + ] + ], + "results_paths": [ + [ + "$['a'][0]", + "$['a'][1]", + "$['a'][2][0]", + "$['a'][2][1]" + ] + ] + }, + { + "test_name": "table_17.jpathl-$.a", + "json_path": "$.a", + "root_value": { + "a": null, + "b": [ + null + ], + "c": [ + {} + ], + "null": 1 + }, + "source_file_name": "table_17.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + null + ] + ], + "results_paths": [ + [ + "$['a']" + ] + ] + }, + { + "test_name": "table_17.jpathl-$.a[0]", + "json_path": "$.a[0]", + "root_value": { + "a": null, + "b": [ + null + ], + "c": [ + {} + ], + "null": 1 + }, + "source_file_name": "table_17.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [] + ], + "results_paths": [ + [] + ] + }, + { + "test_name": "table_17.jpathl-$.a.d", + "json_path": "$.a.d", + "root_value": { + "a": null, + "b": [ + null + ], + "c": [ + {} + ], + "null": 1 + }, + "source_file_name": "table_17.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [] + ], + "results_paths": [ + [] + ] + }, + { + "test_name": "table_17.jpathl-$.b[0]", + "json_path": "$.b[0]", + "root_value": { + "a": null, + "b": [ + null + ], + "c": [ + {} + ], + "null": 1 + }, + "source_file_name": "table_17.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + null + ] + ], + "results_paths": [ + [ + "$['b'][0]" + ] + ] + }, + { + "test_name": "table_17.jpathl-$.b[*]", + "json_path": "$.b[*]", + "root_value": { + "a": null, + "b": [ + null + ], + "c": [ + {} + ], + "null": 1 + }, + "source_file_name": "table_17.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + null + ] + ], + "results_paths": [ + [ + "$['b'][0]" + ] + ] + }, + { + "test_name": "table_17.jpathl-$.b[?@]", + "json_path": "$.b[?@]", + "root_value": { + "a": null, + "b": [ + null + ], + "c": [ + {} + ], + "null": 1 + }, + "source_file_name": "table_17.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + null + ] + ], + "results_paths": [ + [ + "$['b'][0]" + ] + ] + }, + { + "test_name": "table_17.jpathl-$.b[?@==null]", + "json_path": "$.b[?@==null]", + "root_value": { + "a": null, + "b": [ + null + ], + "c": [ + {} + ], + "null": 1 + }, + "source_file_name": "table_17.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + null + ] + ], + "results_paths": [ + [ + "$['b'][0]" + ] + ] + }, + { + "test_name": "table_17.jpathl-$.c[?@.d==null]", + "json_path": "$.c[?@.d==null]", + "root_value": { + "a": null, + "b": [ + null + ], + "c": [ + {} + ], + "null": 1 + }, + "source_file_name": "table_17.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [] + ], + "results_paths": [ + [] + ] + }, + { + "test_name": "table_17.jpathl-$.null", + "json_path": "$.null", + "root_value": { + "a": null, + "b": [ + null + ], + "c": [ + {} + ], + "null": 1 + }, + "source_file_name": "table_17.jpathl", + "is_invalid": false, + "err_msg": "", + "subparse_production": null, + "results_values": [ + [ + 1 + ] + ], + "results_paths": [ + [ + "$['null']" + ] + ] + } + ] +} \ No newline at end of file diff --git a/tests/jpath/evaluating/cts/test_cts.py b/tests/jpath/evaluating/test_cts.py similarity index 91% rename from tests/jpath/evaluating/cts/test_cts.py rename to tests/jpath/evaluating/test_cts.py index ab4b16d..5c00837 100644 --- a/tests/jpath/evaluating/cts/test_cts.py +++ b/tests/jpath/evaluating/test_cts.py @@ -4,6 +4,7 @@ # Open-source license to come. # Created by: Robert L. Ross # +# """ Runs test cases from the jsponpath-compliance-test_suite @@ -41,14 +42,14 @@ @dataclass(frozen=True, slots=True) class CTSTestData: - """Holds a single test case from a cts json file, and maps domain names from the test file domain to the domain names in + """Holds a single test case from a `cts` JSON file, and maps domain names from the test file domain to the domain names in RFC 9535. We also abstract away the distinciton between a single test result and multiple test results for a single test case by just implementing a results_values and results_paths list. Test cases with a single result and path are represented as a single element results_values/paths list.""" test_name : str json_path : str root_value : JSON_ValueType - is_invalid : bool # json path query string is invalid and should trigger an error + is_invalid : bool # JSON path query string is invalid and should trigger an error # result_value : JSON_ValueType # result_paths : list[str] @@ -60,7 +61,7 @@ class CTSTestData: @classmethod def from_dict(cls, data: dict[str, Any]) -> 'CTSTestData': """Create a CTSTestData instance from the argument dict. `data` is assumed to be a single test case in a - cts json file. We pop known keys from the cts file argument dict and replace them with key names aligned with + `cts` JSON file. We pop known keys from the `cts` file argument dict and replace them with key names aligned with RFC 9535 nomenclature.""" kwargs = data test_name = kwargs.pop('name', '') @@ -98,8 +99,8 @@ def from_dict(cls, data: dict[str, Any]) -> 'CTSTestData': ) _MODULE_DIR = Path(__file__).parent -_CTS_FILE_PATH = _MODULE_DIR / "cts.json" -_FILE_LIST = [ "cts.json",] +_CTS_FILE_PATH = _MODULE_DIR / "cts/cts.json" +_FILE_LIST = [ _CTS_FILE_PATH ] def data_loader() -> list[CTSTestData]: test_data: list[CTSTestData] = [] @@ -116,11 +117,6 @@ def valid_paths() -> list[CTSTestData]: def invalid_paths() -> list[CTSTestData]: return [ test for test in data_loader() if test.is_invalid ] - -def _write_to_json_file(output_path: Path, json_value: Any) -> None: - with open(output_path, "w", encoding=UTF8, buffering=ONE_MEBIBYTE) as output_file: - json.dump(json_value, output_file) - # (test name, reason for excluding) EXCLUDED_TEST_NAMES = { ("functions, match, filter, match function, unicode char class, uppercase", "\\p{Lu} unsupported in Python re"), @@ -156,7 +152,7 @@ def _write_to_json_file(output_path: Path, json_value: Any) -> None: @pytest.mark.parametrize("case", valid_paths(), ids=operator.attrgetter("test_name")) def test_cts_valid(case: CTSTestData ) -> None: - """Test the cases in the cts file that are intended to be well-formed and valid and should return a result. """ + """Test the cases in the `cts` file that are intended to be well-formed and valid and should return a result. """ if case.test_name in EXCLUDED_TESTS_MAP: pytest.skip(reason=f"{EXCLUDED_TESTS_MAP[case.test_name][1]}: '{case.test_name}'") @@ -176,7 +172,7 @@ def test_cts_valid(case: CTSTestData ) -> None: @pytest.mark.parametrize("case", invalid_paths(), ids=operator.attrgetter("test_name")) def test_cts_invalid(case: CTSTestData ) -> None: - """Test the cases in the cts file that are not well-formed or valid and should fail lexing or parsing.""" + """Test the cases in the `cts` file that are not well-formed or valid and should fail lexing or parsing.""" #query = WellFormedValidQuery.from_str(case.json_path) if case.test_name in EXCLUDED_TESTS_MAP: pytest.skip(reason=f"{EXCLUDED_TESTS_MAP[case.test_name][1]}: '{case.test_name}'") @@ -184,4 +180,4 @@ def test_cts_invalid(case: CTSTestData ) -> None: if case.test_name in DEBUG_TEST_NAMES: print(f"\n* * * * * test: '{case.test_name}', json_root: {case.root_value}, json_path: {case.json_path}, expected: {case.results_values}") with pytest.raises(Exception): - query = WellFormedValidQuery.from_str(case.json_path) + _ = WellFormedValidQuery.from_str(case.json_path) diff --git a/tests/jpath/evaluating/test_evaluator_rfc9535_tables.py b/tests/jpath/evaluating/test_evaluator_rfc9535_tables.py new file mode 100644 index 0000000..60939b2 --- /dev/null +++ b/tests/jpath/evaluating/test_evaluator_rfc9535_tables.py @@ -0,0 +1,83 @@ +# File: test_evaluator_rfc9535_tables.py +# Copyright (c) 2025 Robert L. Ross +# All rights reserved. +# Open-source license to come. +# Created by: Robert L. Ross +# + +""" +RFC 9535 includes multiple tables with example paths and their expected results. The tests here implement all examples +that are intended to be evaluated without generating errors. These are tables 2-18, except for: + Table 11 - which are comparison_expressions and not valid query paths, and are tested separately in test_table_11.py + Table 14 - which tests for well-formed function declarations and not evaluation results, and + Table 18 - which are examples of NormalizedPaths and are not associated with any query parameter data +""" +import json +import operator +from dataclasses import dataclass, field +from pathlib import Path + +import pytest + +from killerbunny.evaluating.value_nodes import VNodeList +from killerbunny.evaluating.well_formed_query import WellFormedValidQuery +from killerbunny.shared.constants import ONE_MEBIBYTE, UTF8 +from killerbunny.shared.json_type_defs import JSON_ValueType + + +@dataclass(frozen=True, slots=True) +class EvaluatorTestCase: + test_name : str + json_path : str + root_value : JSON_ValueType + source_file_name : str + is_invalid : bool = False + err_msg : str = "" + subparse_production: str| None = None + results_values: list[ JSON_ValueType ] = field(default_factory=list) + results_paths : list[ list[str] ] = field(default_factory=list) + +_MODULE_DIR = Path(__file__).parent +_EVAL_TESTS_FILE_PATH = _MODULE_DIR / "evaluator_test_cases.json" +_FILE_LIST = [ _EVAL_TESTS_FILE_PATH ] + +def data_loader() -> list[EvaluatorTestCase]: + test_data: list[EvaluatorTestCase] = [] + for file_name in _FILE_LIST: + file_path = _MODULE_DIR / file_name + with open( file_path , encoding=UTF8, buffering=ONE_MEBIBYTE) as input_file: + data = json.load(input_file) + test_data.extend( [ EvaluatorTestCase(**test) for test in data["tests"] ] ) + return test_data + +def valid_paths() -> list[EvaluatorTestCase]: + return [ test for test in data_loader() if not test.is_invalid ] + +def invalid_paths() -> list[EvaluatorTestCase]: + return [ test for test in data_loader() if test.is_invalid ] + +EXCLUDED_TEST_NAMES: list[tuple[str, str]] = [] # [(test name, reason for excluding)] +EXCLUDED_TESTS_MAP: dict[str, tuple[str,str]] = { item[0]: item for item in EXCLUDED_TEST_NAMES } + +# during debugging of test cases, print debug info for the test names in this set +DEBUG_TEST_NAMES: set[str] = set() + +@pytest.mark.parametrize("case", valid_paths(), ids=operator.attrgetter("test_name")) +def test_cts_valid(case: EvaluatorTestCase ) -> None: + """Test the cases in the `_EVAL_TESTS_FILE_PATH` file that are intended to be well-formed and valid and + should return a result. """ + if case.test_name in EXCLUDED_TESTS_MAP: + pytest.skip(reason=f"{EXCLUDED_TESTS_MAP[case.test_name][1]}: '{case.test_name}'") + + if case.test_name in DEBUG_TEST_NAMES: + print(f"\n* * * * * test: '{case.test_name}', json_root: {case.root_value}, json_path: {case.json_path}, expected: {case.results_values}") + + assert case.root_value is not None + query = WellFormedValidQuery.from_str(case.json_path) + actual_nodelist:VNodeList = query.eval(case.root_value) + actual_values = list(actual_nodelist.values()) + assert actual_values in case.results_values, \ + f"Actual values {actual_values} not found in expected results_values {case.results_values}" + actual_paths_str = [npath.jpath_str for npath in actual_nodelist.paths()] + assert actual_paths_str in case.results_paths, \ + f"Actual paths {actual_paths_str} not found in expected results_paths {case.results_paths}" \ No newline at end of file diff --git a/tests/jpath/evaluating/nts/test_nts.py b/tests/jpath/evaluating/test_nts.py similarity index 95% rename from tests/jpath/evaluating/nts/test_nts.py rename to tests/jpath/evaluating/test_nts.py index 2f69a0d..00ceba5 100644 --- a/tests/jpath/evaluating/nts/test_nts.py +++ b/tests/jpath/evaluating/test_nts.py @@ -4,6 +4,7 @@ # Open-source license to come. # Created by: Robert L. Ross # +# """ Runs test cases fromjsonpath-compliance-normalized-paths @@ -68,7 +69,7 @@ def from_dict(cls, data: dict[str, Any]) -> 'CTSTestData': ) _MODULE_DIR = Path(__file__).parent -_TEST_FILE_PATH = _MODULE_DIR / "normalized_paths.json" +_TEST_FILE_PATH = _MODULE_DIR / "nts/normalized_paths.json" _FILE_LIST = [_TEST_FILE_PATH] def data_loader() -> list[CTSTestData]: @@ -81,7 +82,9 @@ def data_loader() -> list[CTSTestData]: return test_data -# todo we need to implement a Normalize function in order to run the tests against it +# todo we need to implement a Normalize function in the API in order to run the tests against it +def test_dummy() -> None: + assert True # @pytest.mark.parametrize("case", data_loader(), ids=operator.attrgetter("test_name")) # def test_normalized_paths(case: CTSTestData ) -> None: diff --git a/tests/jpath/evaluating/test_root_value_cycles.py b/tests/jpath/evaluating/test_root_value_cycles.py index 49292fd..ce40960 100644 --- a/tests/jpath/evaluating/test_root_value_cycles.py +++ b/tests/jpath/evaluating/test_root_value_cycles.py @@ -8,6 +8,7 @@ """Tests the evaluator with cyclic data. Cycles cannot be (easily?) represented in JSON text but could easily happen when generating nested data programmatically. Cycles can be used as an attack vector to crash the evaluator with a stack overflow error. """ +import logging from typing import Any, cast import pytest @@ -17,6 +18,8 @@ from killerbunny.evaluating.well_formed_query import WellFormedValidQuery from killerbunny.shared.json_type_defs import JSON_ValueType +_logger = logging.getLogger(__name__) + @pytest.fixture(scope="module", autouse=True) def child_dict_cycle() -> JSON_ValueType: parent_dict: dict[str, JSON_ValueType] = { "one": 1 } @@ -395,4 +398,39 @@ def test_ds_list_deeply_nested(doubly_nested_cycle: JSON_ValueType, caplog: LogC "already included as: $[4], {'one': 'one', 'two': 'two', 'three': 'three', " "'shared_list': ['1', '2', '3', {...}], 'cycle': ['a', 'b', 'c', ['1', '2', '3', {...}], {...}]}", caplog - ) \ No newline at end of file + ) + + +def test_table12_cycle(caplog: LogCaptureFixture) -> None: + """Originally this query generated an erroneous Cycle warning due to a bug where str values were being processed + as arrays. That bug was fixed, and this test ensures that fix is working correctly for this case.""" + root_value: dict[str, JSON_ValueType] = { + "a": [3, 5, 1, 2, 4, 6, + {"b": "j"}, + {"b": "k"}, + {"b": {}}, + {"b": "kilo"} + ], + "o": {"p": 1, "q": 2, "r": 3, "s": 5, "t": {"u": 6}}, + "e": "f" + } + + jpath_query_str = '$[?@.*]' + query = WellFormedValidQuery.from_str(jpath_query_str) + node_list:VNodeList = query.eval(root_value) + + print(f"\n{str(caplog.records)}") + + actual_values = list(node_list.values()) + actual_values_str = f"{actual_values}" + actual_paths = [npath.jpath_str for npath in node_list.paths() ] + + expected_values: list[Any] = [[3, 5, 1, 2, 4, 6, {'b': 'j'}, {'b': 'k'}, {'b': {}}, {'b': 'kilo'}], {'p': 1, 'q': 2, 'r': 3, 's': 5, 't': {'u': 6}}] + expected_values_str = "[[3, 5, 1, 2, 4, 6, {'b': 'j'}, {'b': 'k'}, {'b': {}}, {'b': 'kilo'}], {'p': 1, 'q': 2, 'r': 3, 's': 5, 't': {'u': 6}}]" + expected_paths: list[Any] = ["$['a']", "$['o']"] + + assert actual_values_str == expected_values_str + assert actual_paths == expected_paths + assert actual_values == expected_values + + assert 0 == len(caplog.records), "No warnings should be generated for this query" \ No newline at end of file diff --git a/tests/jpath/evaluating/test_table_11.py b/tests/jpath/evaluating/test_table_11.py index 4a3b777..ceaaac5 100644 --- a/tests/jpath/evaluating/test_table_11.py +++ b/tests/jpath/evaluating/test_table_11.py @@ -5,15 +5,15 @@ # Created by: Robert L. Ross # -"""Evaluate comparison expressions defined in Table 11: "Comparison Examples". +"""Evaluate comparison expressions defined in Table 11: "Comparison Examples". See section 2.3.5.3., pg 30, RFC 9535 -These exmples use the json object defined in rfc9535_examples/example_2.3.5.3.1.json: +These tests use the JSON object defined in rfc9535_examples/example_2.3.5.3.1.json: { "obj": {"x": "y"}, "arr": [2, 3] } -Note that these are not proper json path query strings; they are comparison-expression grammar productions. -We must parse the partial query strings using JPathParser.subparse(). +Note that these are not proper JSON path query strings; they are comparison-expression grammar productions. +We must parse the partial query strings using JPathParser.subparse("comparison_expr"). """ from typing import cast diff --git a/tests/jpath/lexing/lexer_test_cases.json b/tests/jpath/lexing/lexer_test_cases.json index 4b68204..3826082 100644 --- a/tests/jpath/lexing/lexer_test_cases.json +++ b/tests/jpath/lexing/lexer_test_cases.json @@ -470,6 +470,20 @@ "source_file_name": "table_12", "is_invalid": false }, + { + "test_name": "table_12-$.a[?match(@.b, \"[jk]\")]", + "json_path": "$.a[?match(@.b, \"[jk]\")]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, QMARK, ID:match, LPAREN, AT, DOT, ID:b, COMMA, STRING:\"[jk]\", RPAREN, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, + { + "test_name": "table_12-$.a[?search(@.b, \"[jk]\")]", + "json_path": "$.a[?search(@.b, \"[jk]\")]", + "lexer_tokens": "DOLLAR, DOT, ID:a, LBRACKET, QMARK, ID:search, LPAREN, AT, DOT, ID:b, COMMA, STRING:\"[jk]\", RPAREN, RBRACKET, EOF", + "source_file_name": "table_12", + "is_invalid": false + }, { "test_name": "table_12-$.o[?@>1 && @<4]", "json_path": "$.o[?@>1 && @<4]", diff --git a/tests/jpath/parsing/parser_test_cases.json b/tests/jpath/parsing/parser_test_cases.json index 60a3a3b..14a91d5 100644 --- a/tests/jpath/parsing/parser_test_cases.json +++ b/tests/jpath/parsing/parser_test_cases.json @@ -7,7 +7,8 @@ "parser_ast": "${CS{bs[ns:store]/bs}/CS, CS{bs[ns:book]/bs}/CS, CS{bs[*]/bs}/CS, CS{bs[ns:author]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..author", @@ -15,7 +16,8 @@ "parser_ast": "${DS{bs[ns:author]/bs}/DS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$.store.*", @@ -23,7 +25,8 @@ "parser_ast": "${CS{bs[ns:store]/bs}/CS, CS{bs[*]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$.store..price", @@ -31,7 +34,8 @@ "parser_ast": "${CS{bs[ns:store]/bs}/CS, DS{bs[ns:price]/bs}/DS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..book[2]", @@ -39,7 +43,8 @@ "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:2]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..book[2].author", @@ -47,7 +52,8 @@ "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:2]/bs}/CS, CS{bs[ns:author]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..book[2].publisher", @@ -55,7 +61,8 @@ "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:2]/bs}/CS, CS{bs[ns:publisher]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..book[-1]", @@ -63,7 +70,8 @@ "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:-1]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..book[0,1]", @@ -71,7 +79,8 @@ "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[is:0, is:1]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..book[:2]", @@ -79,7 +88,8 @@ "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[slice(:2:)]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..book[?@.isbn]", @@ -87,7 +97,8 @@ "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[fs{?@ segments}/fs]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..book[?@.price<10]", @@ -95,7 +106,8 @@ "parser_ast": "${DS{bs[ns:book]/bs}/DS, CS{bs[fs{?comp_expr(@{sqs{ns:price}}/@, <, 10)}/fs]/bs}/CS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_02-$..*", @@ -103,7 +115,8 @@ "parser_ast": "${DS{bs[*]/bs}/DS}/$", "source_file_name": "table_02", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_03-$", @@ -111,7 +124,8 @@ "parser_ast": "$", "source_file_name": "table_03", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_05-$.o['j j']", @@ -119,7 +133,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS}/$", "source_file_name": "table_05", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_05-$.o['j j']['k.k']", @@ -127,7 +142,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS, CS{bs[ns:k.k]/bs}/CS}/$", "source_file_name": "table_05", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_05-$.o[\"j j\"][\"k.k\"]", @@ -135,7 +151,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS, CS{bs[ns:k.k]/bs}/CS}/$", "source_file_name": "table_05", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_05-$['o']['j j']['k.k']", @@ -143,7 +160,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[ns:j j]/bs}/CS, CS{bs[ns:k.k]/bs}/CS}/$", "source_file_name": "table_05", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_05-$[\"'\"][\"@\"]", @@ -151,7 +169,8 @@ "parser_ast": "${CS{bs[ns:']/bs}/CS, CS{bs[ns:@]/bs}/CS}/$", "source_file_name": "table_05", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_06-$[*]", @@ -159,7 +178,8 @@ "parser_ast": "${CS{bs[*]/bs}/CS}/$", "source_file_name": "table_06", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_06-$.o[*]", @@ -167,7 +187,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[*]/bs}/CS}/$", "source_file_name": "table_06", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_06-$.o[*]", @@ -175,7 +196,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[*]/bs}/CS}/$", "source_file_name": "table_06", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_06-$.o[*, *]", @@ -183,7 +205,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[*, *]/bs}/CS}/$", "source_file_name": "table_06", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_06-$.a[*]", @@ -191,7 +214,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[*]/bs}/CS}/$", "source_file_name": "table_06", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_07-$[1]", @@ -199,7 +223,8 @@ "parser_ast": "${CS{bs[is:1]/bs}/CS}/$", "source_file_name": "table_07", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_07-$[-2]", @@ -207,7 +232,8 @@ "parser_ast": "${CS{bs[is:-2]/bs}/CS}/$", "source_file_name": "table_07", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_09-$[1:3]", @@ -215,7 +241,8 @@ "parser_ast": "${CS{bs[slice(1:3:)]/bs}/CS}/$", "source_file_name": "table_09", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_09-$[5:]", @@ -223,7 +250,8 @@ "parser_ast": "${CS{bs[slice(5::)]/bs}/CS}/$", "source_file_name": "table_09", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_09-$[1:5:2]", @@ -231,7 +259,8 @@ "parser_ast": "${CS{bs[slice(1:5:2)]/bs}/CS}/$", "source_file_name": "table_09", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_09-$[5:1:-2]", @@ -239,7 +268,8 @@ "parser_ast": "${CS{bs[slice(5:1:-2)]/bs}/CS}/$", "source_file_name": "table_09", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_09-$[::-1]", @@ -247,7 +277,8 @@ "parser_ast": "${CS{bs[slice(::-1)]/bs}/CS}/$", "source_file_name": "table_09", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_11-$.absent1 == $.absent2", @@ -255,7 +286,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 11: $.absent1 ^==^ $.absent2" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 11: $.absent1 ^==^ $.absent2", + "subparse_production": null }, { "test_name": "table_11-$.absent1 <= $.absent2", @@ -263,7 +295,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 11: $.absent1 ^<=^ $.absent2" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 11: $.absent1 ^<=^ $.absent2", + "subparse_production": null }, { "test_name": "table_11-$.absent == 'g'", @@ -271,7 +304,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 10: $.absent ^==^ 'g'" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 10: $.absent ^==^ 'g'", + "subparse_production": null }, { "test_name": "table_11-$.absent1 != $.absent2", @@ -279,7 +313,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 11: $.absent1 ^!=^ $.absent2" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 11: $.absent1 ^!=^ $.absent2", + "subparse_production": null }, { "test_name": "table_11-$.absent != 'g'", @@ -287,7 +322,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 10: $.absent ^!=^ 'g'" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 10: $.absent ^!=^ 'g'", + "subparse_production": null }, { "test_name": "table_11-1 <= 2", @@ -295,7 +331,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ <= 2" + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ <= 2", + "subparse_production": null }, { "test_name": "table_11-1> 2", @@ -303,7 +340,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^> 2" + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^> 2", + "subparse_production": null }, { "test_name": "table_11-13 == '13'", @@ -311,7 +349,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^13^ == '13'" + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^13^ == '13'", + "subparse_production": null }, { "test_name": "table_11-'a' <= 'b'", @@ -319,7 +358,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got STRING at position 1: ^'a'^ <= 'b'" + "err_msg": "Invalid Syntax: start: Expected '$', got STRING at position 1: ^'a'^ <= 'b'", + "subparse_production": null }, { "test_name": "table_11-'a' > 'b'", @@ -327,7 +367,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got STRING at position 1: ^'a'^ > 'b'" + "err_msg": "Invalid Syntax: start: Expected '$', got STRING at position 1: ^'a'^ > 'b'", + "subparse_production": null }, { "test_name": "table_11-$.obj == $.arr", @@ -335,7 +376,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ $.arr" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ $.arr", + "subparse_production": null }, { "test_name": "table_11-$.obj != $.arr", @@ -343,7 +385,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ $.arr" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ $.arr", + "subparse_production": null }, { "test_name": "table_11-$.obj == $.obj", @@ -351,7 +394,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ $.obj" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ $.obj", + "subparse_production": null }, { "test_name": "table_11-$.obj != $.obj", @@ -359,7 +403,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ $.obj" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ $.obj", + "subparse_production": null }, { "test_name": "table_11-$.arr == $.arr", @@ -367,7 +412,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.arr ^==^ $.arr" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.arr ^==^ $.arr", + "subparse_production": null }, { "test_name": "table_11-$.arr != $.arr", @@ -375,7 +421,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.arr ^!=^ $.arr" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.arr ^!=^ $.arr", + "subparse_production": null }, { "test_name": "table_11-$.obj == 17", @@ -383,7 +430,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ 17" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got == at position 7: $.obj ^==^ 17", + "subparse_production": null }, { "test_name": "table_11-$.obj != 17", @@ -391,7 +439,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ 17" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got != at position 7: $.obj ^!=^ 17", + "subparse_production": null }, { "test_name": "table_11-$.obj <= $.arr", @@ -399,7 +448,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.obj ^<=^ $.arr" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.obj ^<=^ $.arr", + "subparse_production": null }, { "test_name": "table_11-$.obj < $.arr", @@ -407,7 +457,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got < at position 7: $.obj ^<^ $.arr" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got < at position 7: $.obj ^<^ $.arr", + "subparse_production": null }, { "test_name": "table_11-$.obj <= $.obj", @@ -415,7 +466,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.obj ^<=^ $.obj" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.obj ^<=^ $.obj", + "subparse_production": null }, { "test_name": "table_11-$.arr <= $.arr", @@ -423,7 +475,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.arr ^<=^ $.arr" + "err_msg": "Invalid Syntax: start: Parser completed before EOF. Expected '.', '[' or '..', got <= at position 7: $.arr ^<=^ $.arr", + "subparse_production": null }, { "test_name": "table_11-1 <= $.arr", @@ -431,7 +484,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ <= $.arr" + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ <= $.arr", + "subparse_production": null }, { "test_name": "table_11-1 >= $.arr", @@ -439,7 +493,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ >= $.arr" + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ >= $.arr", + "subparse_production": null }, { "test_name": "table_11-1 > $.arr", @@ -447,7 +502,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ > $.arr" + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ > $.arr", + "subparse_production": null }, { "test_name": "table_11-1 < $.arr", @@ -455,7 +511,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ < $.arr" + "err_msg": "Invalid Syntax: start: Expected '$', got INT at position 1: ^1^ < $.arr", + "subparse_production": null }, { "test_name": "table_11-true <= true", @@ -463,7 +520,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got true at position 1: ^true^ <= true" + "err_msg": "Invalid Syntax: start: Expected '$', got true at position 1: ^true^ <= true", + "subparse_production": null }, { "test_name": "table_11-true > true", @@ -471,7 +529,8 @@ "parser_ast": "", "source_file_name": "table_11", "is_invalid": true, - "err_msg": "Invalid Syntax: start: Expected '$', got true at position 1: ^true^ > true" + "err_msg": "Invalid Syntax: start: Expected '$', got true at position 1: ^true^ > true", + "subparse_production": null }, { "test_name": "table_12-$.a[?@.b == 'kilo']", @@ -479,7 +538,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:b}}/@, ==, kilo)}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.a[?(@.b == 'kilo')]", @@ -487,7 +547,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:b}}/@, ==, kilo)}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.a[?@>3.5]", @@ -495,7 +556,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@, >, 3.5)}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.a[?@.b]", @@ -503,7 +565,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?@ segments}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$[?@.*]", @@ -511,7 +574,8 @@ "parser_ast": "${CS{bs[fs{?@ segments}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$[?@[?@.b]]", @@ -519,7 +583,8 @@ "parser_ast": "${CS{bs[fs{?@ segments}/fs]/bs}/CS>}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.o[?@<3, ?@<3]", @@ -527,7 +592,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?comp_expr(@, <, 3)}/fs, fs{?comp_expr(@, <, 3)}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.a[?@<2 || @.b == \"k\"]", @@ -535,7 +601,26 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?logical_or_expr[comp_expr(@, <, 2), comp_expr(@{sqs{ns:b}}/@, ==, k)]/logical_or_expr}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null + }, + { + "test_name": "table_12-$.a[?match(@.b, \"[jk]\")]", + "json_path": "$.a[?match(@.b, \"[jk]\")]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?match(@ segments, [jk])->LogicalType}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "", + "subparse_production": null + }, + { + "test_name": "table_12-$.a[?search(@.b, \"[jk]\")]", + "json_path": "$.a[?search(@.b, \"[jk]\")]", + "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?search(@ segments, [jk])->LogicalType}/fs]/bs}/CS}/$", + "source_file_name": "table_12", + "is_invalid": false, + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.o[?@>1 && @<4]", @@ -543,7 +628,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?logical_and_expr[comp_expr(@, >, 1), comp_expr(@, <, 4)]/logical_and_expr}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.o[?@>1 && @<4]", @@ -551,7 +637,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?logical_and_expr[comp_expr(@, >, 1), comp_expr(@, <, 4)]/logical_and_expr}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.o[?@.u || @.x]", @@ -559,7 +646,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, CS{bs[fs{?logical_or_expr[@ segments, @ segments]/logical_or_expr}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.a[?@.b == $.x]", @@ -567,7 +655,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:b}}/@, ==, ${sqs{ns:x}}/$)}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_12-$.a[?@ == @]", @@ -575,7 +664,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[fs{?comp_expr(@, ==, @)}/fs]/bs}/CS}/$", "source_file_name": "table_12", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_14-$[?length(@) < 3]", @@ -583,7 +673,8 @@ "parser_ast": "${CS{bs[fs{?comp_expr(length(@)->ValueType, <, 3)}/fs]/bs}/CS}/$", "source_file_name": "table_14", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_14-$[?length(@.*) < 3]", @@ -591,7 +682,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Validation Error: function_expr: Expected singular query for ValueType parameter at position 11: $[?length(^@.*^) < 3]" + "err_msg": "Validation Error: function_expr: Expected singular query for ValueType parameter at position 11: $[?length(^@.*^) < 3]", + "subparse_production": null }, { "test_name": "table_14-$[?count(@.*) == 1]", @@ -599,7 +691,8 @@ "parser_ast": "${CS{bs[fs{?comp_expr(count(@ segments)->ValueType, ==, 1)}/fs]/bs}/CS}/$", "source_file_name": "table_14", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_14-$[?count(1) == 1]", @@ -607,7 +700,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Validation Error: function_expr: Expected NodesType but got ValueType at position 10: $[?count(^1^) == 1]" + "err_msg": "Validation Error: function_expr: Expected NodesType but got ValueType at position 10: $[?count(^1^) == 1]", + "subparse_production": null }, { "test_name": "table_14-$[?count(foo(@.*)) == 1]", @@ -615,7 +709,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Illegal Function Name: function_expr: Function name 'foo' is not registered at position 10: $[?count(^foo^(@.*)) == 1]" + "err_msg": "Illegal Function Name: function_expr: Function name 'foo' is not registered at position 10: $[?count(^foo^(@.*)) == 1]", + "subparse_production": null }, { "test_name": "table_14-$[?match(@.timezone, 'Europe/.*')]", @@ -623,7 +718,8 @@ "parser_ast": "${CS{bs[fs{?match(@ segments, Europe/.*)->LogicalType}/fs]/bs}/CS}/$", "source_file_name": "table_14", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_14-$[?match(@.timezone,'Europe/.*') == true]", @@ -631,7 +727,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Invalid Syntax: bracketed_selection: Expected ',' or ']', found == at position 34: $[?match(@.timezone,'Europe/.*') ^==^ true]" + "err_msg": "Invalid Syntax: bracketed_selection: Expected ',' or ']', found == at position 34: $[?match(@.timezone,'Europe/.*') ^==^ true]", + "subparse_production": null }, { "test_name": "table_14-$[?value(@..color) == \"red\"]", @@ -639,7 +736,8 @@ "parser_ast": "${CS{bs[fs{?comp_expr(value(@ segments)->ValueType, ==, red)}/fs]/bs}/CS}/$", "source_file_name": "table_14", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_14-$[?value(@..color)]", @@ -647,7 +745,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Validation Error: test_expr: Function not well-typed for test_expr. Expected LogicalType or NodesType, got ValueType at position 4: $[?^value(@..color)^]" + "err_msg": "Validation Error: test_expr: Function not well-typed for test_expr. Expected LogicalType or NodesType, got ValueType at position 4: $[?^value(@..color)^]", + "subparse_production": null }, { "test_name": "table_14-$[?bar(@.a)]", @@ -655,7 +754,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Illegal Function Name: function_expr: Function name 'bar' is not registered at position 4: $[?^bar^(@.a)]" + "err_msg": "Illegal Function Name: function_expr: Function name 'bar' is not registered at position 4: $[?^bar^(@.a)]", + "subparse_production": null }, { "test_name": "table_14-$[?bnl(@.*)]", @@ -663,7 +763,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Illegal Function Name: function_expr: Function name 'bnl' is not registered at position 4: $[?^bnl^(@.*)]" + "err_msg": "Illegal Function Name: function_expr: Function name 'bnl' is not registered at position 4: $[?^bnl^(@.*)]", + "subparse_production": null }, { "test_name": "table_14-$[?blt(1==1)]", @@ -671,7 +772,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Illegal Function Name: function_expr: Function name 'blt' is not registered at position 4: $[?^blt^(1==1)]" + "err_msg": "Illegal Function Name: function_expr: Function name 'blt' is not registered at position 4: $[?^blt^(1==1)]", + "subparse_production": null }, { "test_name": "table_14-$[?blt(1)]", @@ -679,7 +781,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Illegal Function Name: function_expr: Function name 'blt' is not registered at position 4: $[?^blt^(1)]" + "err_msg": "Illegal Function Name: function_expr: Function name 'blt' is not registered at position 4: $[?^blt^(1)]", + "subparse_production": null }, { "test_name": "table_14-$[?bal(1)]", @@ -687,7 +790,8 @@ "parser_ast": "", "source_file_name": "table_14", "is_invalid": true, - "err_msg": "Illegal Function Name: function_expr: Function name 'bal' is not registered at position 4: $[?^bal^(1)]" + "err_msg": "Illegal Function Name: function_expr: Function name 'bal' is not registered at position 4: $[?^bal^(1)]", + "subparse_production": null }, { "test_name": "table_15-$[0, 3]", @@ -695,7 +799,8 @@ "parser_ast": "${CS{bs[is:0, is:3]/bs}/CS}/$", "source_file_name": "table_15", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_15-$[0:2, 5]", @@ -703,7 +808,8 @@ "parser_ast": "${CS{bs[slice(0:2:), is:5]/bs}/CS}/$", "source_file_name": "table_15", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_15-$[0, 0]", @@ -711,7 +817,8 @@ "parser_ast": "${CS{bs[is:0, is:0]/bs}/CS}/$", "source_file_name": "table_15", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_16-$..j", @@ -719,7 +826,8 @@ "parser_ast": "${DS{bs[ns:j]/bs}/DS}/$", "source_file_name": "table_16", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_16-$..j", @@ -727,7 +835,8 @@ "parser_ast": "${DS{bs[ns:j]/bs}/DS}/$", "source_file_name": "table_16", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_16-$..[0]", @@ -735,7 +844,8 @@ "parser_ast": "${DS{bs[is:0]/bs}/DS}/$", "source_file_name": "table_16", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_16-$..[*]", @@ -743,7 +853,8 @@ "parser_ast": "${DS{bs[*]/bs}/DS}/$", "source_file_name": "table_16", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_16-$..*", @@ -751,7 +862,8 @@ "parser_ast": "${DS{bs[*]/bs}/DS}/$", "source_file_name": "table_16", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_16-$..o", @@ -759,7 +871,8 @@ "parser_ast": "${DS{bs[ns:o]/bs}/DS}/$", "source_file_name": "table_16", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_16-$.o..[*, *]", @@ -767,7 +880,8 @@ "parser_ast": "${CS{bs[ns:o]/bs}/CS, DS{bs[*, *]/bs}/DS}/$", "source_file_name": "table_16", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_16-$.a..[0, 1]", @@ -775,7 +889,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, DS{bs[is:0, is:1]/bs}/DS}/$", "source_file_name": "table_16", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_17-$.a", @@ -783,7 +898,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS}/$", "source_file_name": "table_17", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_17-$.a[0]", @@ -791,7 +907,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[is:0]/bs}/CS}/$", "source_file_name": "table_17", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_17-$.a.d", @@ -799,7 +916,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[ns:d]/bs}/CS}/$", "source_file_name": "table_17", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_17-$.b[0]", @@ -807,7 +925,8 @@ "parser_ast": "${CS{bs[ns:b]/bs}/CS, CS{bs[is:0]/bs}/CS}/$", "source_file_name": "table_17", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_17-$.b[*]", @@ -815,7 +934,8 @@ "parser_ast": "${CS{bs[ns:b]/bs}/CS, CS{bs[*]/bs}/CS}/$", "source_file_name": "table_17", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_17-$.b[?@]", @@ -823,7 +943,8 @@ "parser_ast": "${CS{bs[ns:b]/bs}/CS, CS{bs[fs{?@}/fs]/bs}/CS}/$", "source_file_name": "table_17", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_17-$.b[?@==null]", @@ -831,7 +952,8 @@ "parser_ast": "${CS{bs[ns:b]/bs}/CS, CS{bs[fs{?comp_expr(@, ==, null)}/fs]/bs}/CS}/$", "source_file_name": "table_17", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_17-$.c[?@.d==null]", @@ -839,7 +961,8 @@ "parser_ast": "${CS{bs[ns:c]/bs}/CS, CS{bs[fs{?comp_expr(@{sqs{ns:d}}/@, ==, null)}/fs]/bs}/CS}/$", "source_file_name": "table_17", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_17-$.null", @@ -847,7 +970,8 @@ "parser_ast": "${CS{bs[ns:null]/bs}/CS}/$", "source_file_name": "table_17", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_18-$.a", @@ -855,7 +979,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS}/$", "source_file_name": "table_18", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_18-$[1]", @@ -863,7 +988,8 @@ "parser_ast": "${CS{bs[is:1]/bs}/CS}/$", "source_file_name": "table_18", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_18-$[-3]", @@ -871,7 +997,8 @@ "parser_ast": "${CS{bs[is:-3]/bs}/CS}/$", "source_file_name": "table_18", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_18-$.a.b[1:2]", @@ -879,7 +1006,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS, CS{bs[ns:b]/bs}/CS, CS{bs[slice(1:2:)]/bs}/CS}/$", "source_file_name": "table_18", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_18-$[\"\\u000B\"]", @@ -887,7 +1015,8 @@ "parser_ast": "${CS{bs[ns:\u000b]/bs}/CS}/$", "source_file_name": "table_18", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null }, { "test_name": "table_18-$[\"\\u0061\"]", @@ -895,7 +1024,8 @@ "parser_ast": "${CS{bs[ns:a]/bs}/CS}/$", "source_file_name": "table_18", "is_invalid": false, - "err_msg": "" + "err_msg": "", + "subparse_production": null } ] } \ No newline at end of file diff --git a/tests/jpath/parsing/test_helper.py b/tests/jpath/parsing/test_helper.py index 83fe032..f8396ef 100644 --- a/tests/jpath/parsing/test_helper.py +++ b/tests/jpath/parsing/test_helper.py @@ -13,8 +13,12 @@ escape_char_for_jsonpath_tests = [ ("$['a'']", r"$['a\'']", "Embedded Single quote is properly escaped"), ("$['\\']", "$['\\\\']", "Backslash is properly escaped"), + ("$[\"'\"][\"@\"]", r'$["\'"]["@"]', "Embedded Single quote is properly escaped"), + ("$['\'']['@']", r"$['\'']['@']", "Embedded Single quote is properly escaped"), + ] @pytest.mark.parametrize("text, expected, msg", escape_char_for_jsonpath_tests) def test_escape_char_for_jsonpath(text: str, expected: str, msg: str) -> None: actual = escape_string_content(text) + print(f"\ntext = {text}, expected = {expected}, msg = {msg}, actual = {actual}") assert actual == expected, msg \ No newline at end of file diff --git a/tests/jpath/parsing/test_parser_rfc9535_tables.py b/tests/jpath/parsing/test_parser_rfc9535_tables.py index 2548bbf..8f1600e 100644 --- a/tests/jpath/parsing/test_parser_rfc9535_tables.py +++ b/tests/jpath/parsing/test_parser_rfc9535_tables.py @@ -31,6 +31,7 @@ class ParserTestCase: source_file_name : str is_invalid : bool = False err_msg : str = "" + subparse_production: str| None = None def data_loader() -> list[ParserTestCase]: diff --git a/tests/jpath/rfc9535_examples/table_02.jpathl b/tests/jpath/rfc9535_examples/table_02.jpathl index 7da72ce..c5a6c3b 100644 --- a/tests/jpath/rfc9535_examples/table_02.jpathl +++ b/tests/jpath/rfc9535_examples/table_02.jpathl @@ -1,3 +1,4 @@ +# json_file: figure.1.json $.store.book[*].author $..author $.store.* diff --git a/tests/jpath/rfc9535_examples/table_03.jpathl b/tests/jpath/rfc9535_examples/table_03.jpathl index 6f4f765..54d026d 100644 --- a/tests/jpath/rfc9535_examples/table_03.jpathl +++ b/tests/jpath/rfc9535_examples/table_03.jpathl @@ -1 +1,2 @@ +# json_file: example_2.2.3.json $ \ No newline at end of file diff --git a/tests/jpath/rfc9535_examples/table_05.jpathl b/tests/jpath/rfc9535_examples/table_05.jpathl index 4de904a..4142f87 100644 --- a/tests/jpath/rfc9535_examples/table_05.jpathl +++ b/tests/jpath/rfc9535_examples/table_05.jpathl @@ -1,3 +1,4 @@ +# json_file: example_2.3.1.3.json $.o['j j'] $.o['j j']['k.k'] $.o["j j"]["k.k"] diff --git a/tests/jpath/rfc9535_examples/table_06.jpathl b/tests/jpath/rfc9535_examples/table_06.jpathl index e555795..9897126 100644 --- a/tests/jpath/rfc9535_examples/table_06.jpathl +++ b/tests/jpath/rfc9535_examples/table_06.jpathl @@ -1,3 +1,4 @@ +# json_file: example_2.3.2.3.json $[*] $.o[*] $.o[*] diff --git a/tests/jpath/rfc9535_examples/table_07.jpathl b/tests/jpath/rfc9535_examples/table_07.jpathl index 52a513c..9138da6 100644 --- a/tests/jpath/rfc9535_examples/table_07.jpathl +++ b/tests/jpath/rfc9535_examples/table_07.jpathl @@ -1,2 +1,3 @@ +# json_file: example_2.3.3.3.json $[1] $[-2] \ No newline at end of file diff --git a/tests/jpath/rfc9535_examples/table_09.jpathl b/tests/jpath/rfc9535_examples/table_09.jpathl index cbbe7b6..5730808 100644 --- a/tests/jpath/rfc9535_examples/table_09.jpathl +++ b/tests/jpath/rfc9535_examples/table_09.jpathl @@ -1,3 +1,4 @@ +# json_file: example_2.3.4.3.json $[1:3] $[5:] $[1:5:2] diff --git a/tests/jpath/rfc9535_examples/table_11.jpathl b/tests/jpath/rfc9535_examples/table_11.jpathl index 0891813..125abaf 100644 --- a/tests/jpath/rfc9535_examples/table_11.jpathl +++ b/tests/jpath/rfc9535_examples/table_11.jpathl @@ -1,3 +1,5 @@ +# json_file: example_2.3.5.3.1.json +# subparse: comparison_expr $.absent1 == $.absent2 $.absent1 <= $.absent2 $.absent == 'g' diff --git a/tests/jpath/rfc9535_examples/table_12.jpathl b/tests/jpath/rfc9535_examples/table_12.jpathl index 6a73aca..29eba58 100644 --- a/tests/jpath/rfc9535_examples/table_12.jpathl +++ b/tests/jpath/rfc9535_examples/table_12.jpathl @@ -1,3 +1,4 @@ +# json_file: example_2.3.5.3.2.json $.a[?@.b == 'kilo'] $.a[?(@.b == 'kilo')] $.a[?@>3.5] @@ -6,8 +7,8 @@ $[?@.*] $[?@[?@.b]] $.o[?@<3, ?@<3] $.a[?@<2 || @.b == "k"] -#$.a[?match(@.b, "[jk]")] -#$.a[?search(@.b, "[jk]")] +$.a[?match(@.b, "[jk]")] +$.a[?search(@.b, "[jk]")] $.o[?@>1 && @<4] $.o[?@>1 && @<4] $.o[?@.u || @.x] diff --git a/tests/jpath/rfc9535_examples/table_14.jpathl b/tests/jpath/rfc9535_examples/table_14.jpathl index 6f0a274..e47d999 100644 --- a/tests/jpath/rfc9535_examples/table_14.jpathl +++ b/tests/jpath/rfc9535_examples/table_14.jpathl @@ -1,3 +1,6 @@ +# todo there is no json value for these strings in the RFC, as they are intended to test well-formedness and validity. +# we have such tests in parsing.test_table_14_functions.py. Create a sample JSON text file for testing these paths +# in the evaluator $[?length(@) < 3] $[?length(@.*) < 3] $[?count(@.*) == 1] diff --git a/tests/jpath/rfc9535_examples/table_15.jpathl b/tests/jpath/rfc9535_examples/table_15.jpathl index 5f10450..024cab2 100644 --- a/tests/jpath/rfc9535_examples/table_15.jpathl +++ b/tests/jpath/rfc9535_examples/table_15.jpathl @@ -1,3 +1,4 @@ +# json_file: example_2.5.1.3.json $[0, 3] $[0:2, 5] $[0, 0] \ No newline at end of file diff --git a/tests/jpath/rfc9535_examples/table_16.jpathl b/tests/jpath/rfc9535_examples/table_16.jpathl index 2679cda..1e0fa39 100644 --- a/tests/jpath/rfc9535_examples/table_16.jpathl +++ b/tests/jpath/rfc9535_examples/table_16.jpathl @@ -1,3 +1,4 @@ +# json_file: example_2.5.2.3.json $..j $..j $..[0] diff --git a/tests/jpath/rfc9535_examples/table_17.jpathl b/tests/jpath/rfc9535_examples/table_17.jpathl index 8185d43..537cf7c 100644 --- a/tests/jpath/rfc9535_examples/table_17.jpathl +++ b/tests/jpath/rfc9535_examples/table_17.jpathl @@ -1,3 +1,4 @@ +# json_file: example_2.6.1.json $.a $.a[0] $.a.d diff --git a/tests/jpath/rfc9535_examples/table_18.jpathl b/tests/jpath/rfc9535_examples/table_18.jpathl index 6ce60ea..9e609e3 100644 --- a/tests/jpath/rfc9535_examples/table_18.jpathl +++ b/tests/jpath/rfc9535_examples/table_18.jpathl @@ -1,3 +1,4 @@ +# todo create a JSON data file for these paths, which are all Normalized Paths $.a $[1] $[-3]