├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── documentation.md │ └── feature.md ├── PULL_REQUEST_TEMPLATE.md ├── diff.json ├── php-syntax.json └── workflows │ ├── Test.yml │ ├── UpdateContributors.yml │ └── codestyle.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── composer.json ├── phpcs.xml ├── phpunit.xml.dist ├── src ├── AccessHelper.php ├── Filters │ ├── AbstractFilter.php │ ├── IndexFilter.php │ ├── IndexesFilter.php │ ├── QueryMatchFilter.php │ ├── QueryResultFilter.php │ ├── RecursiveFilter.php │ └── SliceFilter.php ├── JSONPath.php ├── JSONPathException.php ├── JSONPathLexer.php └── JSONPathToken.php └── tests ├── JSONPathArrayAccessTest.php ├── JSONPathArrayTest.php ├── JSONPathDashedIndexTest.php ├── JSONPathLexerTest.php ├── JSONPathSliceAccessTest.php ├── JSONPathTest.php ├── JSONPathTestClass.php ├── QueryTest.php ├── Traits └── TestDataTrait.php └── data ├── baselineFailedQueries.txt ├── conferences.json ├── example.json ├── extra.json ├── indexed-object.json ├── locations.json ├── numerical-indexes-array.json ├── numerical-indexes-object.json ├── simple-integers.json ├── with-dots.json └── with-slashes.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: softcreatr 2 | custom: ['https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4'] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Submit a bug report, to help us improve. 4 | labels: "bug" 5 | --- 6 | 7 | ## 🐛 Bug Report 8 | 9 | (A clear and concise description of what the bug is) 10 | 11 | ## Have you spent some time to check if this issue has been raised before? 12 | 13 | [ ] I have read googled for a similar issue or checked our older issues for a similar bug 14 | 15 | ### Have you read the [Code of Conduct](https://github.com/SoftCreatR/JSONPath/blob/main/CODE_OF_CONDUCT.md)? 16 | 17 | [ ] I have read the Code of Conduct 18 | 19 | ## To Reproduce 20 | 21 | (Write your steps here:) 22 | 23 | ## Expected behavior 24 | 25 | 30 | 31 | (Write what you thought would happen.) 32 | 33 | ## Actual Behavior 34 | 35 | 41 | 42 | (Write what happened. Add screenshots, if applicable.) 43 | 44 | ## Your Environment 45 | 46 | 47 | 48 | (Write Environment, Operating system and version etc.) 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📚 Documentation 3 | about: Report an issue related to documentation. 4 | labels: "documentation" 5 | --- 6 | 7 | ## 📚 Documentation 8 | 9 | (A clear and concise description of what the issue is.) 10 | 11 | ### Have you read the [Code of Conduct](https://github.com/SoftCreatR/JSONPath/blob/main/CODE_OF_CONDUCT.md)? 12 | 13 | [ ] I have read the Code of Conduct 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature / Idea 3 | about: Submit a proposal for a new feature. 4 | labels: "feature" 5 | --- 6 | 7 | ## 💡 Feature / Idea 8 | 9 | (A clear and concise description of what the feature is.) 10 | 11 | ## Have you spent some time to check if this issue has been raised before? 12 | 13 | [ ] I have read googled for a similar issue or checked our older issues for a similar idea 14 | 15 | ### Have you read the [Code of Conduct](https://github.com/SoftCreatR/JSONPath/blob/main/CODE_OF_CONDUCT.md)? 16 | 17 | [ ] I have read the Code of Conduct 18 | 19 | ## Pitch 20 | 21 | (Please explain why this feature should be implemented and how it would be used. Add examples, if applicable.) 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | # 🔀 Pull Request 11 | 12 | ## What does this PR do? 13 | 14 | (Provide a description of what this PR does.) 15 | 16 | ## Test Plan 17 | 18 | (Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.) 19 | 20 | ## Related PRs and Issues 21 | 22 | (If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.) 23 | -------------------------------------------------------------------------------- /.github/diff.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "diff", 5 | "pattern": [ 6 | { 7 | "regexp": "--- a/(.*)", 8 | "file": 1, 9 | "message": 1 10 | } 11 | ] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/php-syntax.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "php -l", 5 | "pattern": [ 6 | { 7 | "regexp": "^\\s*(PHP\\s+)?([a-zA-Z\\s]+):\\s+(.*)\\s+in\\s+(\\S+)\\s+on\\s+line\\s+(\\d+)$", 8 | "file": 4, 9 | "line": 5, 10 | "message": 3 11 | } 12 | ] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/Test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.php' 7 | - 'composer.json' 8 | branches: 9 | - 'main' 10 | pull_request: 11 | paths: 12 | - '**.php' 13 | - 'composer.json' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | run: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | php: 22 | - '8.1' 23 | - '8.2' 24 | - '8.3' 25 | - '8.4' 26 | continue-on-error: ${{ matrix.php == '8.4' }} 27 | name: PHP ${{ matrix.php }} Test 28 | 29 | steps: 30 | - name: Git checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Setup PHP 34 | uses: shivammathur/setup-php@v2 35 | with: 36 | php-version: ${{ matrix.php }} 37 | extensions: json 38 | tools: phpcs 39 | coverage: pcov 40 | env: 41 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: Setup problem matchers for PHP syntax check 44 | run: echo "::add-matcher::.github/php-syntax.json" 45 | 46 | - run: | 47 | ! find . -type f -name '*.php' -exec php -l '{}' \; 2>&1 |grep -v '^No syntax errors detected' 48 | 49 | - name: Setup problem matchers for PHPUnit 50 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 51 | 52 | - name: Install dependencies 53 | run: composer update --prefer-dist --no-interaction 54 | 55 | - name: Run phpcs 56 | run: composer cs 57 | 58 | - name: Execute tests 59 | run: | 60 | set +e 61 | output=$(composer test -- --coverage-clover=coverage.xml 2>&1) 62 | exit_code=$? 63 | echo "$output" 64 | # If the only issue is the cache directory warning, ignore the exit code. 65 | if echo "$output" | grep -q "No cache directory configured, result of static analysis for code coverage will not be cached"; then 66 | echo "Ignoring known PHPUnit warning about missing cache directory." 67 | exit 0 68 | else 69 | exit $exit_code 70 | fi 71 | 72 | - name: Run codecov 73 | uses: codecov/codecov-action@v4 74 | -------------------------------------------------------------------------------- /.github/workflows/UpdateContributors.yml: -------------------------------------------------------------------------------- 1 | name: Update Contributors 2 | 3 | on: [ push, workflow_dispatch] 4 | 5 | jobs: 6 | Update: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Git checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Update Contributors 14 | uses: BobAnkh/add-contributors@master 15 | with: 16 | REPO_NAME: 'SoftCreatR/JSONPath' 17 | CONTRIBUTOR: '## Contributors ✨' 18 | ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}} 19 | -------------------------------------------------------------------------------- /.github/workflows/codestyle.yml: -------------------------------------------------------------------------------- 1 | name: Code Style 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.php' 7 | pull_request: 8 | paths: 9 | - '**.php' 10 | 11 | jobs: 12 | php: 13 | name: PHP 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Setup PHP with tools 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: '8.1' 23 | extensions: json 24 | tools: cs2pr, phpcs, php-cs-fixer 25 | 26 | - name: phpcs 27 | run: phpcs -n -q --report=checkstyle | cs2pr 28 | 29 | - name: php-cs-fixer 30 | run: php-cs-fixer fix --dry-run --diff 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .phpunit.result.cache 3 | vendor 4 | .php_cs.cache 5 | composer.lock 6 | composer.phar 7 | .phpcs-cache 8 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | exclude('*/vendor/*') 4 | ->in(__DIR__); 5 | 6 | return (new PhpCsFixer\Config()) 7 | ->setRiskyAllowed(true) 8 | ->setRules([ 9 | '@PSR1' => true, 10 | '@PSR2' => true, 11 | '@PSR12' => true, 12 | '@PER' => true, 13 | 14 | 'array_push' => true, 15 | 'backtick_to_shell_exec' => true, 16 | 'no_alias_language_construct_call' => true, 17 | 'no_mixed_echo_print' => true, 18 | 'pow_to_exponentiation' => true, 19 | 'random_api_migration' => true, 20 | 21 | 'array_syntax' => ['syntax' => 'short'], 22 | 'no_multiline_whitespace_around_double_arrow' => true, 23 | 'no_trailing_comma_in_singleline_array' => true, 24 | 'no_whitespace_before_comma_in_array' => true, 25 | 'normalize_index_brace' => true, 26 | 'whitespace_after_comma_in_array' => true, 27 | 28 | 'non_printable_character' => ['use_escape_sequences_in_strings' => true], 29 | 30 | 'magic_constant_casing' => true, 31 | 'magic_method_casing' => true, 32 | 'native_function_casing' => true, 33 | 'native_function_type_declaration_casing' => true, 34 | 35 | 'cast_spaces' => ['space' => 'none'], 36 | 'no_unset_cast' => true, 37 | 38 | 'class_attributes_separation' => true, 39 | 'no_null_property_initialization' => true, 40 | 'self_accessor' => true, 41 | 'single_class_element_per_statement' => true, 42 | 43 | 'no_empty_comment' => true, 44 | 'single_line_comment_style' => ['comment_types' => ['hash']], 45 | 46 | 'native_constant_invocation' => ['strict' => false], 47 | 48 | 'no_alternative_syntax' => true, 49 | 'no_trailing_comma_in_list_call' => true, 50 | 'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'return', 'switch_case', 'yield', 'yield_from']], 51 | 'no_unneeded_curly_braces' => ['namespaces' => true], 52 | 'switch_continue_to_break' => true, 53 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 54 | 55 | 'function_typehint_space' => true, 56 | 'lambda_not_used_import' => true, 57 | 'native_function_invocation' => ['include' => ['@internal']], 58 | 'no_unreachable_default_argument_value' => true, 59 | 'nullable_type_declaration_for_default_null_value' => true, 60 | 'static_lambda' => true, 61 | 62 | 'fully_qualified_strict_types' => true, 63 | 'no_unused_imports' => true, 64 | 65 | 'dir_constant' => true, 66 | 'explicit_indirect_variable' => true, 67 | 'function_to_constant' => true, 68 | 'is_null' => true, 69 | 'no_unset_on_property' => true, 70 | 71 | 'list_syntax' => ['syntax' => 'short'], 72 | 73 | 'clean_namespace' => true, 74 | 'no_leading_namespace_whitespace' => true, 75 | 76 | 'no_homoglyph_names' => true, 77 | 78 | 'binary_operator_spaces' => true, 79 | 'concat_space' => ['spacing' => 'one'], 80 | 'increment_style' => ['style' => 'post'], 81 | 'logical_operators' => true, 82 | 'object_operator_without_whitespace' => true, 83 | 'operator_linebreak' => true, 84 | 'standardize_increment' => true, 85 | 'standardize_not_equals' => true, 86 | 'ternary_to_elvis_operator' => true, 87 | 'ternary_to_null_coalescing' => true, 88 | 'unary_operator_spaces' => true, 89 | 90 | 'no_useless_return' => true, 91 | 'return_assignment' => true, 92 | 93 | 'multiline_whitespace_before_semicolons' => true, 94 | 'no_empty_statement' => true, 95 | 'no_singleline_whitespace_before_semicolons' => true, 96 | 'space_after_semicolon' => ['remove_in_empty_for_expressions' => true], 97 | 98 | 'escape_implicit_backslashes' => true, 99 | 'explicit_string_variable' => true, 100 | 'heredoc_to_nowdoc' => true, 101 | 'no_binary_string' => true, 102 | 'simple_to_complex_string_variable' => true, 103 | 104 | 'array_indentation' => true, 105 | 'blank_line_before_statement' => ['statements' => ['return', 'exit']], 106 | 'method_chaining_indentation' => true, 107 | 'no_extra_blank_lines' => ['tokens' => ['case', 'continue', 'curly_brace_block', 'default', 'extra', 'parenthesis_brace_block', 'square_brace_block', 'switch', 'throw', 'use']], 108 | 'no_spaces_around_offset' => true, 109 | 110 | // SoftCreatR style 111 | 'global_namespace_import' => [ 112 | 'import_classes' => true, 113 | 'import_constants' => true, 114 | 'import_functions' => false, 115 | ], 116 | 'ordered_imports' => [ 117 | 'imports_order' => ['class', 'function', 'const'], 118 | ], 119 | ]) 120 | ->setFinder($finder); 121 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.10.1 4 | - Fixed ignore whitespace after comparison value in filter expression 5 | 6 | ### 0.10.0 7 | - Fixed query/selector Filter Expression With Current Object 8 | - Fixed query/selector Filter Expression With Different Grouped Operators 9 | - Fixed query/selector Filter Expression With equals_on_array_of_numbers 10 | - Fixed query/selector Filter Expression With Negation and Equals 11 | - Fixed query/selector Filter Expression With Negation and Less Than 12 | - Fixed query/selector Filter Expression Without Value 13 | - Fixed query/selector Filter Expression With Boolean AND Operator (#42) 14 | - Fixed query/selector Filter Expression With Boolean OR Operator (#43) 15 | - Fixed query/selector Filter Expression With Equals (#45) 16 | - Fixed query/selector Filter Expression With Equals false (#46) 17 | - Fixed query/selector Filter Expression With Equals null (#47) 18 | - Fixed query/selector Filter Expression With Equals Number With Fraction (#48) 19 | - Fixed query/selector Filter Expression With Equals true (#50) 20 | - Fixed query/selector Filter Expression With Greater Than (#52) 21 | - Fixed query/selector Filter Expression With Greater Than or Equal (#53) 22 | - Fixed query/selector Filter Expression With Less Than (#54) 23 | - Fixed query/selector Filter Expression With Less Than or Equal (#55) 24 | - Fixed query/selector Filter Expression With Not Equals (#56) 25 | - Fixed query/selector Filter Expression With Value (#57) 26 | - Fixed query/selector script_expression (Expected test result corrected) 27 | - Added additional NULL related query tests from JSONPath RFC 28 | 29 | ### 0.9.0 30 | 🔻 Breaking changes ahead: 31 | 32 | - Dropped support for PHP < 8.1 33 | 34 | ### 0.8.3 35 | - Change `getData()` so that it can be mixed instead of array 36 | 37 | ### 0.8.2 38 | - AccessHelper & RecursiveFilter now return a plain `object`, rather than an `ArrayAccess` object 39 | 40 | ### 0.8.1 41 | - Removed strict_types 42 | - Applied some PSR-12 related changes 43 | - Small code optimizations 44 | 45 | ### 0.8.0 46 | 🔻 Breaking changes ahead: 47 | 48 | - Dropped support for PHP < 8.0 49 | - Removed deprecated method `JSONPath->data()` 50 | 51 | ### 0.7.5 52 | - Added support for $.length 53 | - Added trim to explode to support both 1,2,3 and 1, 2, 3 inputs 54 | - Dropped in_array strict equality check to be in line with the other standard equality checks such as (== and !=) 55 | 56 | ### 0.7.4 57 | - Removed PHPUnit from conflicting packages 58 | 59 | ### 0.7.3 60 | - Fixed PHP 7.4+ compatibility issues 61 | 62 | ### 0.7.2 63 | - Fixed query/selector "Array Slice With Start Large Negative Number And Open End On Short Array" (#7) 64 | - Fixed query/selector "Union With Keys" (#22) 65 | - Fixed query/selector "Dot Notation After Union With Keys" (#15) 66 | - Fixed query/selector "Union With Keys After Array Slice" (#23) 67 | - Fixed query/selector "Union With Keys After Bracket Notation" (#24) 68 | - Fixed query/selector "Union With Keys On Object Without Key" (#25) 69 | 70 | ### 0.7.1 71 | - Fixed issues with empty tokens (`['']` and `[""]`) 72 | - Fixed TypeError in AccessHelper::keyExists 73 | - Improved QueryTest 74 | 75 | ### 0.7.0 76 | 🔻 Breaking changes ahead: 77 | 78 | - Made JSONPath::__construct final 79 | - Added missing type hints 80 | - Partially reduced complexity 81 | - Performed some code optimizations 82 | - Updated composer.json for proper PHPUnit/PHP usage 83 | - Added support for regular expression operator (`=~`) 84 | - Added QueryTest to perform tests against all queries from https://cburgmer.github.io/json-path-comparison/ 85 | - Switched Code Style from PSR-2 to PSR-12 86 | 87 | ### 0.6.4 88 | - Removed unnecessary type casting, that caused problems under certain circumstances 89 | - Added support for `nin` operator 90 | - Added support for greater than or equal operator (`>=`) 91 | - Added support for less or equal operator (`<=`) 92 | 93 | ### 0.6.3 94 | - Added support for `in` operator 95 | - Fixed evaluation on indexed object 96 | 97 | ### 0.6.x 98 | - Dropped support for PHP < 7.1 99 | - Switched from (broken) PSR-0 to PSR-4 100 | - Updated PHPUnit to 8.5 / 9.4 101 | - Updated tests 102 | - Added missing PHPDoc blocks 103 | - Added return type hints 104 | - Moved from Travis to GitHub actions 105 | - Set `strict_types=1` 106 | 107 | ### 0.5.0 108 | - Fixed the slice notation (e.g. [0:2:5] etc.). **Breaks code relying on the broken implementation** 109 | 110 | ### 0.3.0 111 | - Added JSONPathToken class as value object 112 | - Lexer clean up and refactor 113 | - Updated the lexing and filtering of the recursive token ("..") to allow for a combination of recursion 114 | and filters, e.g. $..[?(@.type == 'suburb')].name 115 | 116 | ### 0.2.1 - 0.2.5 117 | - Various bug fixes and clean up 118 | 119 | ### 0.2.0 120 | - Added a heap of array access features for more creative iterating and chaining possibilities 121 | 122 | ### 0.1.x 123 | - Init 124 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity, expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 26 | * Trolling, insulting/derogatory comments, and personal or political attacks 27 | * Public or private harassment 28 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 29 | * Other conduct which could reasonably be considered inappropriate in a professional setting 30 | 31 | ## Our Responsibilities 32 | 33 | Project maintainers are responsible for clarifying the standards of acceptable 34 | behavior and are expected to take appropriate and fair corrective action in 35 | response to any instances of unacceptable behavior. 36 | 37 | Project maintainers have the right and responsibility to remove, edit, or 38 | reject comments, commits, code, wiki edits, issues, and other contributions 39 | that are not aligned to this Code of Conduct, or to ban temporarily or 40 | permanently any contributor for other behaviors that they deem inappropriate, 41 | threatening, offensive, or harmful. 42 | 43 | ## Scope 44 | 45 | This Code of Conduct applies both within project spaces and in public spaces 46 | when an individual is representing the project or its community. Examples of 47 | representing a project or community include using an official project e-mail 48 | address, posting via an official social media account, or acting as an appointed 49 | representative at an online or offline event. Representation of a project may be 50 | further defined and clarified by project maintainers. 51 | 52 | ## Enforcement 53 | 54 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 55 | reported by contacting the project team at hello@1-2.dev. All 56 | complaints will be reviewed and investigated and will result in a response that 57 | is deemed necessary and appropriate to the circumstances. The project team is 58 | obligated to maintain confidentiality with regard to the reporter of an incident. 59 | Further details of specific enforcement policies may be posted separately. 60 | 61 | Project maintainers who do not follow or enforce the Code of Conduct in good 62 | faith may face temporary or permanent repercussions as determined by other 63 | members of the project's leadership. 64 | 65 | ## Attribution 66 | 67 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 68 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 69 | 70 | [homepage]: https://www.contributor-covenant.org 71 | 72 | For answers to common questions about this code of conduct, see 73 | https://www.contributor-covenant.org/faq 74 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Original work - Copyright (c) 2018 Flow Communications 4 | Modified work - Copyright (c) 2020 Sascha Greuel 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSONPath for PHP 8.1+ 2 | 3 | [![Build](https://img.shields.io/github/actions/workflow/status/SoftCreatR/JSONPath/.github/workflows/Test.yml?branch=main)](https://github.com/SoftCreatR/JSONPath/actions/workflows/Test.yml) [![Latest Release](https://img.shields.io/packagist/v/SoftCreatR/JSONPath?color=blue&label=Latest%20Release)](https://packagist.org/packages/softcreatr/jsonpath) 4 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![Plant Tree](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=Plant%20Tree&query=%24.total&url=https%3A%2F%2Fpublic.ecologi.com%2Fusers%2Fsoftcreatr%2Ftrees)](https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4) 5 | [![Codecov branch](https://img.shields.io/codecov/c/github/SoftCreatR/JSONPath)](https://codecov.io/gh/SoftCreatR/JSONPath) 6 | [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability-percentage/SoftCreatR/JSONPath)](https://codeclimate.com/github/SoftCreatR/JSONPath) 7 | 8 | This is a [JSONPath](http://goessner.net/articles/JsonPath/) implementation for PHP based on Stefan Goessner's JSONPath script. 9 | 10 | JSONPath is an XPath-like expression language for filtering, flattening and extracting data. 11 | 12 | This project aims to be a clean and simple implementation with the following goals: 13 | 14 | - Object-oriented code (should be easier to manage or extend in future) 15 | - Expressions are parsed into tokens using code inspired by the Doctrine Lexer. The tokens are cached internally to avoid re-parsing the expressions. 16 | - There is no `eval()` in use 17 | - Any combination of objects/arrays/ArrayAccess-objects can be used as the data input which is great if you're de-serializing JSON in to objects or if you want to process your own data structures. 18 | 19 | ## Installation 20 | 21 | ```bash 22 | composer require softcreatr/jsonpath:"^0.9" 23 | ``` 24 | 25 | ## JSONPath Examples 26 | 27 | JSONPath | Result 28 | --------------------------|------------------------------------- 29 | `$.store.books[*].author` | the authors of all books in the store 30 | `$..author` | all authors 31 | `$.store..price` | the price of everything in the store. 32 | `$..books[2]` | the third book 33 | `$..books[(@.length-1)]` | the last book in order. 34 | `$..books[-1:]` | the last book in order. 35 | `$..books[0,1]` | the first two books 36 | `$..books[:2]` | the first two books 37 | `$..books[::2]` | every second book starting from first one 38 | `$..books[1:6:3]` | every third book starting from 1 till 6 39 | `$..books[?(@.isbn)]` | filter all books with isbn number 40 | `$..books[?(@.price<10)]` | filter all books cheaper than 10 41 | `$..books.length` | the amount of books 42 | `$..*` | all elements in the data (recursively extracted) 43 | 44 | 45 | Expression syntax 46 | --- 47 | 48 | Symbol | Description 49 | ----------------------|------------------------- 50 | `$` | The root object/element (not strictly necessary) 51 | `@` | The current object/element 52 | `.` or `[]` | Child operator 53 | `..` | Recursive descent 54 | `*` | Wildcard. All child elements regardless their index. 55 | `[,]` | Array indices as a set 56 | `[start:end:step]` | Array slice operator borrowed from ES4/Python. 57 | `?()` | Filters a result set by a script expression 58 | `()` | Uses the result of a script expression as the index 59 | 60 | ## PHP Usage 61 | 62 | #### Using arrays 63 | 64 | ```php 65 | [ 69 | ['name' => 'Sascha'], 70 | ['name' => 'Bianca'], 71 | ['name' => 'Alexander'], 72 | ['name' => 'Maximilian'], 73 | ]]; 74 | 75 | print_r((new \Flow\JSONPath\JSONPath($data))->find('$.people.*.name')->getData()); 76 | 77 | /* 78 | Array 79 | ( 80 | [0] => Sascha 81 | [1] => Bianca 82 | [2] => Alexander 83 | [3] => Maximilian 84 | ) 85 | */ 86 | ``` 87 | 88 | #### Using objects 89 | 90 | ```php 91 | find('$')->getData()[0]); 97 | 98 | /* 99 | stdClass Object 100 | ( 101 | [name] => Sascha Greuel 102 | [birthdate] => 1987-12-16 103 | [city] => Gladbeck 104 | [country] => Germany 105 | ) 106 | */ 107 | ``` 108 | 109 | More examples can be found in the [Wiki](https://github.com/SoftCreatR/JSONPath/wiki/Queries) 110 | 111 | ### Magic method access 112 | 113 | The options flag `JSONPath::ALLOW_MAGIC` will instruct JSONPath when retrieving a value to first check if an object 114 | has a magic `__get()` method and will call this method if available. This feature is *iffy* and 115 | not very predictable as: 116 | 117 | - wildcard and recursive features will only look at public properties and can't smell which properties are magically accessible 118 | - there is no `property_exists` check for magic methods so an object with a magic `__get()` will always return `true` when checking 119 | if the property exists 120 | - any errors thrown or unpredictable behaviour caused by fetching via `__get()` is your own problem to deal with 121 | 122 | ```php 123 | use Flow\JSONPath\JSONPath; 124 | 125 | $myObject = (new Foo())->get('bar'); 126 | $jsonPath = new JSONPath($myObject, JSONPath::ALLOW_MAGIC); 127 | ``` 128 | 129 | For more examples, check the JSONPathTest.php tests file. 130 | 131 | ## Script expressions 132 | 133 | Script expressions are not supported as the original author intended because: 134 | 135 | - This would only be achievable through `eval` (boo). 136 | - Using the script engine from different languages defeats the purpose of having a single expression evaluate the same way in different 137 | languages which seems like a bit of a flaw if you're creating an abstract expression syntax. 138 | 139 | So here are the types of query expressions that are supported: 140 | 141 | [?(@._KEY_ _OPERATOR_ _VALUE_)] // <, >, <=, >=, !=, ==, =~, in and nin 142 | e.g. 143 | [?(@.title == "A string")] // 144 | [?(@.title = "A string")] 145 | // A single equals is not an assignment but the SQL-style of '==' 146 | [?(@.title =~ /^a(nother)? string$/i)] 147 | [?(@.title in ["A string", "Another string"])] 148 | [?(@.title nin ["A string", "Another string"])] 149 | 150 | ## Known issues 151 | 152 | - This project has not implemented multiple string indexes e.g. `$[name,year]` or `$["name","year"]`. I have no ETA on that feature, and it would require some re-writing of the parser that uses a very basic regex implementation. 153 | 154 | ## Similar projects 155 | 156 | [FlowCommunications/JSONPath](https://github.com/FlowCommunications/JSONPath) is the predecessor of this library by Stephen Frank 157 | 158 | Other / Similar implementations can be found in the [Wiki](https://github.com/SoftCreatR/JSONPath/wiki/Other-Implementations). 159 | 160 | ## Changelog 161 | 162 | A list of changes can be found in the [CHANGELOG.md](CHANGELOG.md) file. 163 | 164 | ## License 🌳 165 | 166 | [MIT](LICENSE.md) © [1-2.dev](https://1-2.dev) 167 | 168 | This package is Treeware. If you use it in production, then we ask that you [**buy the world a tree**](https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4) to thank us for our work. By contributing to the ecologi project, you’ll be creating employment for local families and restoring wildlife habitats. 169 | 170 | ## Contributors ✨ 171 | 172 | 173 | 174 | 181 | 188 | 195 | 202 | 209 | 216 | 217 | 218 | 225 | 232 | 239 | 240 |
175 | 176 | Sascha 177 |
178 | Sascha Greuel 179 |
180 |
182 | 183 | James 184 |
185 | James Lucas 186 |
187 |
189 | 190 | Fabian 191 |
192 | Fabian Blechschmidt 193 |
194 |
196 | 197 | warlof/ 198 |
199 | warlof 200 |
201 |
203 | 204 | Sergey 205 |
206 | Sergey G 207 |
208 |
210 | 211 | Alexandru 212 |
213 | Alexandru Pătrănescu 214 |
215 |
219 | 220 | Oleg 221 |
222 | Oleg Andreyev 223 |
224 |
226 | 227 | Remy 228 |
229 | Remy Suen 230 |
231 |
233 | 234 | esomething/ 235 |
236 | esomething 237 |
238 |
241 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "softcreatr/jsonpath", 3 | "description": "JSONPath implementation for parsing, searching and flattening arrays", 4 | "license": "MIT", 5 | "version": "0.10.0", 6 | "authors": [ 7 | { 8 | "name": "Stephen Frank", 9 | "email": "stephen@flowsa.com", 10 | "homepage": "https://prismaticbytes.com", 11 | "role": "Developer" 12 | }, 13 | { 14 | "name": "Sascha Greuel", 15 | "email": "hello@1-2.dev", 16 | "homepage": "https://1-2.dev", 17 | "role": "Developer" 18 | } 19 | ], 20 | "support": { 21 | "email": "hello@1-2.dev", 22 | "issues": "https://github.com/SoftCreatR/JSONPath/issues", 23 | "forum": "https://github.com/SoftCreatR/JSONPath/discussions", 24 | "source": "https://github.com/SoftCreatR/JSONPath" 25 | }, 26 | "require": { 27 | "php": "8.1 - 8.4", 28 | "ext-json": "*" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "10 - 12", 32 | "friendsofphp/php-cs-fixer": "^3.58", 33 | "squizlabs/php_codesniffer": "^3.10" 34 | }, 35 | "replace": { 36 | "flow/jsonpath": "*" 37 | }, 38 | "minimum-stability": "stable", 39 | "autoload": { 40 | "psr-4": { 41 | "Flow\\JSONPath\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Flow\\JSONPath\\Test\\": "tests/" 47 | } 48 | }, 49 | "config": { 50 | "optimize-autoloader": true, 51 | "preferred-install": "dist" 52 | }, 53 | "scripts": { 54 | "test": "phpunit", 55 | "cs": "phpcs", 56 | "cs-fix": "php-cs-fixer fix --config=.php-cs-fixer.dist.php" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | src 4 | tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | 15 | ./src/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/AccessHelper.php: -------------------------------------------------------------------------------- 1 | offsetExists($key); 45 | } 46 | 47 | if (\is_object($collection)) { 48 | return \property_exists($collection, (string)$key); 49 | } 50 | 51 | return false; 52 | } 53 | 54 | /** 55 | * @todo Optimize conditions 56 | */ 57 | public static function getValue(mixed $collection, $key, bool $magicIsAllowed = false) 58 | { 59 | if ( 60 | $magicIsAllowed 61 | && \is_object($collection) 62 | && !$collection instanceof ArrayAccess && \method_exists($collection, '__get') 63 | ) { 64 | $return = $collection->__get($key); 65 | } elseif (\is_object($collection) && !$collection instanceof ArrayAccess) { 66 | $return = $collection->{$key}; 67 | } elseif ($collection instanceof ArrayAccess) { 68 | $return = $collection->offsetGet($key); 69 | } elseif (\is_array($collection)) { 70 | if (\is_int($key) && $key < 0) { 71 | $return = \array_slice($collection, $key, 1)[0]; 72 | } else { 73 | $return = $collection[$key]; 74 | } 75 | } elseif (\is_int($key)) { 76 | $return = self::getValueByIndex($collection, $key); 77 | } else { 78 | $return = $collection[$key]; 79 | } 80 | 81 | return $return; 82 | } 83 | 84 | /** 85 | * Find item in php collection by index 86 | * Written this way to handle instances ArrayAccess or Traversable objects 87 | */ 88 | private static function getValueByIndex(mixed $collection, $key): mixed 89 | { 90 | $i = 0; 91 | 92 | foreach ($collection as $val) { 93 | if ($i === $key) { 94 | return $val; 95 | } 96 | 97 | $i++; 98 | } 99 | 100 | if ($key < 0) { 101 | $total = $i; 102 | $i = 0; 103 | 104 | foreach ($collection as $val) { 105 | if ($i - $total === $key) { 106 | return $val; 107 | } 108 | 109 | $i++; 110 | } 111 | } 112 | 113 | return null; 114 | } 115 | 116 | public static function setValue(mixed &$collection, $key, $value) 117 | { 118 | if (\is_object($collection) && !$collection instanceof ArrayAccess) { 119 | return $collection->{$key} = $value; 120 | } 121 | 122 | if ($collection instanceof ArrayAccess) { 123 | /** @noinspection PhpVoidFunctionResultUsedInspection */ 124 | return $collection->offsetSet($key, $value); 125 | } 126 | 127 | return $collection[$key] = $value; 128 | } 129 | 130 | public static function unsetValue(mixed &$collection, $key): void 131 | { 132 | if (\is_object($collection) && !$collection instanceof ArrayAccess) { 133 | unset($collection->{$key}); 134 | } 135 | 136 | if ($collection instanceof ArrayAccess) { 137 | $collection->offsetUnset($key); 138 | } 139 | 140 | if (\is_array($collection)) { 141 | unset($collection[$key]); 142 | } 143 | } 144 | 145 | /** 146 | * @throws JSONPathException 147 | */ 148 | public static function arrayValues(array|object $collection): array|ArrayAccess 149 | { 150 | if (\is_array($collection)) { 151 | return \array_values($collection); 152 | } 153 | 154 | if (\is_object($collection)) { 155 | return \array_values((array)$collection); 156 | } 157 | 158 | throw new JSONPathException("Invalid variable type for arrayValues"); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Filters/AbstractFilter.php: -------------------------------------------------------------------------------- 1 | token = $token; 24 | $this->magicIsAllowed = (bool)($options & JSONPath::ALLOW_MAGIC); 25 | } 26 | 27 | abstract public function filter(array|ArrayAccess $collection): array; 28 | } 29 | -------------------------------------------------------------------------------- /src/Filters/IndexFilter.php: -------------------------------------------------------------------------------- 1 | token->value)) { 22 | $result = []; 23 | foreach ($this->token->value as $value) { 24 | if (AccessHelper::keyExists($collection, $value, $this->magicIsAllowed)) { 25 | $result[] = AccessHelper::getValue($collection, $value, $this->magicIsAllowed); 26 | } 27 | } 28 | 29 | return $result; 30 | } 31 | 32 | if (AccessHelper::keyExists($collection, $this->token->value, $this->magicIsAllowed)) { 33 | return [ 34 | AccessHelper::getValue($collection, $this->token->value, $this->magicIsAllowed), 35 | ]; 36 | } 37 | 38 | if ($this->token->value === '*') { 39 | return AccessHelper::arrayValues($collection); 40 | } 41 | 42 | if ($this->token->value === 'length') { 43 | return [ 44 | \count($collection), 45 | ]; 46 | } 47 | 48 | return []; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Filters/IndexesFilter.php: -------------------------------------------------------------------------------- 1 | token->value as $index) { 20 | if (AccessHelper::keyExists($collection, $index, $this->magicIsAllowed)) { 21 | $return[] = AccessHelper::getValue($collection, $index, $this->magicIsAllowed); 22 | } 23 | } 24 | 25 | return $return; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Filters/QueryMatchFilter.php: -------------------------------------------------------------------------------- 1 | !)\((?.+)\)$'; 26 | 27 | protected const MATCH_QUERY_NEGATION_UNWRAPPED = '^(?!)(?.+)$'; 28 | 29 | protected const MATCH_QUERY_OPERATORS = ' 30 | (@\.(?[^\s<>!=]+)|@\[["\']?(?.*?)["\']?\]|(?@)|(%group(?\d+)%)) 31 | (\s*(?==|=~|=|<>|!==|!=|>=|<=|>|<|in|!in|nin)\s*(?.+?(?=\s*(?:&&|$|\|\||%))))? 32 | (\s*(?&&|\|\|)\s*)? 33 | '; 34 | 35 | protected const MATCH_GROUPED_EXPRESSION = '#\([^)(]*+(?:(?R)[^)(]*)*+\)#'; 36 | 37 | /** 38 | * @throws JSONPathException 39 | */ 40 | public function filter($collection): array 41 | { 42 | $filterExpression = $this->token->value; 43 | $negateFilter = false; 44 | if ( 45 | \preg_match('/' . static::MATCH_QUERY_NEGATION_WRAPPED . '/x', $filterExpression, $negationMatches) 46 | || \preg_match('/' . static::MATCH_QUERY_NEGATION_UNWRAPPED . '/x', $filterExpression, $negationMatches) 47 | ) { 48 | $negateFilter = true; 49 | $filterExpression = $negationMatches['logicalexpr']; 50 | } 51 | 52 | $filterGroups = []; 53 | if ( 54 | \preg_match_all( 55 | static::MATCH_GROUPED_EXPRESSION, 56 | $filterExpression, 57 | $matches, 58 | PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL 59 | ) 60 | ) { 61 | foreach ($matches[0] as $i => $matchesGroup) { 62 | $test = \substr($matchesGroup[0], 1, -1); 63 | //sanity check that our group is a group and not something within a string or regular expression 64 | if (\preg_match('/' . static::MATCH_QUERY_OPERATORS . '/x', $test)) { 65 | $filterGroups[$i] = $test; 66 | $filterExpression = \str_replace($matchesGroup[0], "%group{$i}%", $filterExpression); 67 | } 68 | } 69 | } 70 | 71 | $match = \preg_match_all( 72 | '/' . static::MATCH_QUERY_OPERATORS . '/x', 73 | $filterExpression, 74 | $matches, 75 | PREG_UNMATCHED_AS_NULL 76 | ); 77 | 78 | if ( 79 | $match === false 80 | || !isset($matches[1][0]) 81 | || isset($matches['logicalandor'][\array_key_last($matches['logicalandor'])]) 82 | ) { 83 | throw new RuntimeException('Malformed filter query'); 84 | } 85 | 86 | $return = []; 87 | $matchCount = \count($matches[0]); 88 | 89 | for ($expressionPart = 0; $expressionPart < $matchCount; $expressionPart++) { 90 | $filteredCollection = $collection; 91 | $logicalJoin = $expressionPart > 0 ? $matches['logicalandor'][$expressionPart - 1] : null; 92 | 93 | if ($logicalJoin === '&&') { 94 | //Restrict the nodes we need to look at to those already meeting criteria 95 | $filteredCollection = $return; 96 | $return = []; 97 | } 98 | 99 | //Processing a group 100 | if ($matches['group'][$expressionPart] !== null) { 101 | $filter = '$[?(' . $filterGroups[$matches['group'][$expressionPart]] . ')]'; 102 | $resolve = (new JSONPath($filteredCollection))->find($filter)->getData(); 103 | $return = $resolve; 104 | 105 | continue; 106 | } 107 | 108 | //Process a normal expression 109 | $key = $matches['key'][$expressionPart] ?: $matches['keySquare'][$expressionPart]; 110 | 111 | $operator = $matches['operator'][$expressionPart] ?? null; 112 | $comparisonValue = $matches['comparisonValue'][$expressionPart] ?? null; 113 | 114 | if (\is_string($comparisonValue)) { 115 | $comparisonValue = \preg_replace('/^\'/', '"', $comparisonValue); 116 | $comparisonValue = \preg_replace('/\'$/', '"', $comparisonValue); 117 | 118 | try { 119 | $comparisonValue = \json_decode($comparisonValue, true, 512, JSON_THROW_ON_ERROR); 120 | } catch (JsonException) { 121 | //Leave $comparisonValue as raw (e.g. regular express or non quote wrapped string) 122 | } 123 | } 124 | 125 | foreach ($filteredCollection as $nodeIndex => $node) { 126 | if ($logicalJoin === '||' && \array_key_exists($nodeIndex, $return)) { 127 | //Short-circuit, node already exists in output due to previous test 128 | continue; 129 | } 130 | 131 | $selectedNode = null; 132 | $notNothing = AccessHelper::keyExists($node, $key, $this->magicIsAllowed); 133 | 134 | if ($key) { 135 | if ($notNothing) { 136 | $selectedNode = AccessHelper::getValue($node, $key, $this->magicIsAllowed); 137 | } elseif (\str_contains($key, '.')) { 138 | $foundValue = (new JSONPath($node))->find($key)->getData(); 139 | 140 | if ($foundValue) { 141 | $selectedNode = $foundValue[0]; 142 | $notNothing = true; 143 | } 144 | } 145 | } else { 146 | //Node selection was plain @ 147 | $selectedNode = $node; 148 | $notNothing = true; 149 | } 150 | 151 | $comparisonResult = null; 152 | 153 | if ($notNothing) { 154 | $comparisonResult = match ($operator) { 155 | null => AccessHelper::keyExists($node, $key, $this->magicIsAllowed) || (!$key), 156 | "=", "==" => $this->compareEquals($selectedNode, $comparisonValue), 157 | "!=", "!==", "<>" => !$this->compareEquals($selectedNode, $comparisonValue), 158 | '=~' => @\preg_match($comparisonValue, $selectedNode), 159 | '<' => $this->compareLessThan($selectedNode, $comparisonValue), 160 | '<=' => $this->compareLessThan($selectedNode, $comparisonValue) 161 | || $this->compareEquals($selectedNode, $comparisonValue), 162 | '>' => $this->compareLessThan($comparisonValue, $selectedNode), //rfc semantics 163 | '>=' => $this->compareLessThan($comparisonValue, $selectedNode) //rfc semantics 164 | || $this->compareEquals($selectedNode, $comparisonValue), 165 | "in" => \is_array($comparisonValue) && \in_array($selectedNode, $comparisonValue, true), 166 | 'nin', "!in" => \is_array($comparisonValue) && !\in_array($selectedNode, $comparisonValue, true) 167 | }; 168 | } 169 | 170 | if ($negateFilter) { 171 | $comparisonResult = !$comparisonResult; 172 | } 173 | 174 | if ($comparisonResult) { 175 | $return[$nodeIndex] = $node; 176 | } 177 | } 178 | } 179 | 180 | //Keep out returned nodes in the same order they were defined in the original collection 181 | \ksort($return); 182 | 183 | return $return; 184 | } 185 | 186 | protected function isNumber($value): bool 187 | { 188 | return !\is_string($value) && \is_numeric($value); 189 | } 190 | 191 | protected function compareEquals($a, $b): bool 192 | { 193 | $type_a = \gettype($a); 194 | $type_b = \gettype($b); 195 | 196 | if ($type_a === $type_b || ($this->isNumber($a) && $this->isNumber($b))) { 197 | //Primitives or Numbers 198 | if ($a === null || \is_scalar($a)) { 199 | /** @noinspection TypeUnsafeComparisonInspection */ 200 | return $a == $b; 201 | } 202 | //Object/Array 203 | //@TODO array and object comparison 204 | } 205 | 206 | return false; 207 | } 208 | 209 | protected function compareLessThan($a, $b): bool 210 | { 211 | if ((\is_string($a) && \is_string($b)) || ($this->isNumber($a) && $this->isNumber($b))) { 212 | //numerical and string comparison supported only 213 | return $a < $b; 214 | } 215 | 216 | return false; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/Filters/QueryResultFilter.php: -------------------------------------------------------------------------------- 1 | \w+)\s*(?[-+*\/])\s*(?\d+)/', $this->token->value, $matches); 22 | 23 | $matchKey = $matches['key']; 24 | 25 | if (AccessHelper::keyExists($collection, $matchKey, $this->magicIsAllowed)) { 26 | $value = AccessHelper::getValue($collection, $matchKey, $this->magicIsAllowed); 27 | } elseif ($matches['key'] === 'length') { 28 | $value = \count($collection); 29 | } else { 30 | return []; 31 | } 32 | 33 | $resultKey = match ($matches['operator']) { 34 | '+' => $value + $matches['numeric'], 35 | '*' => $value * $matches['numeric'], 36 | '-' => $value - $matches['numeric'], 37 | '/' => $value / $matches['numeric'], 38 | default => throw new JSONPathException('Unsupported operator in expression'), 39 | }; 40 | 41 | $result = []; 42 | 43 | if (AccessHelper::keyExists($collection, $resultKey, $this->magicIsAllowed)) { 44 | $result[] = AccessHelper::getValue($collection, $resultKey, $this->magicIsAllowed); 45 | } 46 | 47 | return $result; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Filters/RecursiveFilter.php: -------------------------------------------------------------------------------- 1 | recurse($result, $collection); 24 | 25 | return $result; 26 | } 27 | 28 | /** 29 | * @throws JSONPathException 30 | */ 31 | private function recurse(array &$result, array|object $data): void 32 | { 33 | $result[] = (array)$data; 34 | 35 | if (AccessHelper::isCollectionType($data)) { 36 | foreach (AccessHelper::arrayValues($data) as $value) { 37 | if (AccessHelper::isCollectionType($value)) { 38 | $this->recurse($result, $value); 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Filters/SliceFilter.php: -------------------------------------------------------------------------------- 1 | token->value['start']; 19 | $end = $this->token->value['end']; 20 | $step = $this->token->value['step'] ?: 1; 21 | 22 | if ($start === null) { 23 | $start = 0; 24 | } 25 | 26 | if ($start < 0) { 27 | $start = $length + $start; 28 | if ($start < 0) { 29 | $start = 0; 30 | } 31 | } 32 | 33 | if ($end === null) { 34 | // negative index start means the end is -1, else the end is the last index 35 | $end = $length; 36 | } 37 | 38 | if ($end < 0) { 39 | $end = $length + $end; 40 | } 41 | 42 | $result = []; 43 | 44 | for ($i = $start; $i < $end; $i += $step) { 45 | $index = $i; 46 | 47 | if ($i < 0) { 48 | $index = $length + $i; 49 | } 50 | 51 | if (AccessHelper::keyExists($collection, $index, $this->magicIsAllowed)) { 52 | $result[] = $collection[$index]; 53 | } 54 | } 55 | 56 | return $result; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/JSONPath.php: -------------------------------------------------------------------------------- 1 | data = $data; 29 | $this->options = $options; 30 | } 31 | 32 | /** 33 | * Evaluate an expression 34 | * 35 | * @throws JSONPathException 36 | * 37 | * @return static 38 | */ 39 | public function find(string $expression): self 40 | { 41 | $tokens = $this->parseTokens($expression); 42 | $collectionData = [$this->data]; 43 | 44 | foreach ($tokens as $token) { 45 | /** @var JSONPathToken $token */ 46 | $filter = $token->buildFilter($this->options); 47 | $filteredDataList = []; 48 | 49 | foreach ($collectionData as $value) { 50 | if (AccessHelper::isCollectionType($value)) { 51 | $filteredDataList[] = $filter->filter($value); 52 | } 53 | } 54 | 55 | if (!empty($filteredDataList)) { 56 | $collectionData = \array_merge(...$filteredDataList); 57 | } else { 58 | $collectionData = []; 59 | } 60 | } 61 | 62 | return new static($collectionData, $this->options); 63 | } 64 | 65 | public function first(): mixed 66 | { 67 | $keys = AccessHelper::collectionKeys($this->data); 68 | 69 | if (empty($keys)) { 70 | return null; 71 | } 72 | 73 | $value = $this->data[$keys[0]] ?? null; 74 | 75 | return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value; 76 | } 77 | 78 | /** 79 | * Evaluate an expression and return the last result 80 | */ 81 | public function last(): mixed 82 | { 83 | $keys = AccessHelper::collectionKeys($this->data); 84 | 85 | if (empty($keys)) { 86 | return null; 87 | } 88 | 89 | $value = $this->data[\end($keys)] ?: null; 90 | 91 | return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value; 92 | } 93 | 94 | /** 95 | * Evaluate an expression and return the first key 96 | */ 97 | public function firstKey(): mixed 98 | { 99 | $keys = AccessHelper::collectionKeys($this->data); 100 | 101 | if (empty($keys)) { 102 | return null; 103 | } 104 | 105 | return $keys[0]; 106 | } 107 | 108 | /** 109 | * Evaluate an expression and return the last key 110 | */ 111 | public function lastKey(): mixed 112 | { 113 | $keys = AccessHelper::collectionKeys($this->data); 114 | 115 | if (empty($keys) || \end($keys) === false) { 116 | return null; 117 | } 118 | 119 | return \end($keys); 120 | } 121 | 122 | /** 123 | * @throws JSONPathException 124 | */ 125 | public function parseTokens(string $expression): array 126 | { 127 | $cacheKey = \crc32($expression); 128 | 129 | if (isset(static::$tokenCache[$cacheKey])) { 130 | return static::$tokenCache[$cacheKey]; 131 | } 132 | 133 | $lexer = new JSONPathLexer($expression); 134 | $tokens = $lexer->parseExpression(); 135 | 136 | static::$tokenCache[$cacheKey] = $tokens; 137 | 138 | return $tokens; 139 | } 140 | 141 | public function getData(): mixed 142 | { 143 | return $this->data; 144 | } 145 | 146 | /** 147 | * @return mixed|null 148 | * @noinspection MagicMethodsValidityInspection 149 | */ 150 | public function __get($key) 151 | { 152 | return $this->offsetExists($key) ? $this->offsetGet($key) : null; 153 | } 154 | 155 | /** 156 | * @inheritDoc 157 | */ 158 | public function offsetExists($offset): bool 159 | { 160 | return AccessHelper::keyExists($this->data, $offset); 161 | } 162 | 163 | /** 164 | * @inheritDoc 165 | */ 166 | public function offsetGet($offset): mixed 167 | { 168 | $value = AccessHelper::getValue($this->data, $offset); 169 | 170 | return AccessHelper::isCollectionType($value) 171 | ? new static($value, $this->options) 172 | : $value; 173 | } 174 | 175 | /** 176 | * @inheritDoc 177 | */ 178 | public function offsetSet($offset, $value): void 179 | { 180 | if ($offset === null) { 181 | $this->data[] = $value; 182 | } else { 183 | AccessHelper::setValue($this->data, $offset, $value); 184 | } 185 | } 186 | 187 | /** 188 | * @inheritDoc 189 | */ 190 | public function offsetUnset($offset): void 191 | { 192 | AccessHelper::unsetValue($this->data, $offset); 193 | } 194 | 195 | /** 196 | * @inheritDoc 197 | */ 198 | public function jsonSerialize(): array 199 | { 200 | return $this->getData(); 201 | } 202 | 203 | /** 204 | * @inheritDoc 205 | */ 206 | public function current(): mixed 207 | { 208 | $value = \current($this->data); 209 | 210 | return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value; 211 | } 212 | 213 | /** 214 | * @inheritDoc 215 | */ 216 | public function next(): void 217 | { 218 | \next($this->data); 219 | } 220 | 221 | /** 222 | * @inheritDoc 223 | */ 224 | public function key(): string|int|null 225 | { 226 | return \key($this->data); 227 | } 228 | 229 | /** 230 | * @inheritDoc 231 | */ 232 | public function valid(): bool 233 | { 234 | return \key($this->data) !== null; 235 | } 236 | 237 | /** 238 | * @inheritDoc 239 | */ 240 | public function rewind(): void 241 | { 242 | \reset($this->data); 243 | } 244 | 245 | /** 246 | * @inheritDoc 247 | */ 248 | public function count(): int 249 | { 250 | return \count($this->data); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/JSONPathException.php: -------------------------------------------------------------------------------- 1 | 1) { 47 | if ($expression[0] === '$') { 48 | $expression = \substr($expression, 1); 49 | $len--; 50 | } 51 | 52 | if ($expression[0] !== '.' && $expression[0] !== '[') { 53 | $expression = '.' . $expression; 54 | $len++; 55 | } 56 | 57 | $this->expression = $expression; 58 | $this->expressionLength = $len; 59 | } 60 | } 61 | 62 | /** 63 | * @throws JSONPathException 64 | */ 65 | public function parseExpressionTokens(): array 66 | { 67 | $dotIndexDepth = 0; 68 | $squareBracketDepth = 0; 69 | $tokenValue = ''; 70 | $tokens = []; 71 | 72 | for ($i = 0; $i < $this->expressionLength; $i++) { 73 | $char = $this->expression[$i]; 74 | 75 | if (($squareBracketDepth === 0) && $char === '.') { 76 | if ($this->lookAhead($i) === '.') { 77 | $tokens[] = new JSONPathToken(JSONPathToken::T_RECURSIVE, null); 78 | } 79 | 80 | continue; 81 | } 82 | 83 | if ($char === '[') { 84 | $squareBracketDepth++; 85 | 86 | if ($squareBracketDepth === 1) { 87 | continue; 88 | } 89 | } 90 | 91 | if ($char === ']') { 92 | $squareBracketDepth--; 93 | 94 | if ($squareBracketDepth === 0) { 95 | continue; 96 | } 97 | } 98 | 99 | /* 100 | * Within square brackets 101 | */ 102 | if ($squareBracketDepth > 0) { 103 | $tokenValue .= $char; 104 | 105 | if ($squareBracketDepth === 1 && $this->lookAhead($i) === ']') { 106 | $tokens[] = $this->createToken($tokenValue); 107 | $tokenValue = ''; 108 | } 109 | } 110 | 111 | /* 112 | * Outside square brackets 113 | */ 114 | if ($squareBracketDepth === 0) { 115 | $tokenValue .= $char; 116 | 117 | // Double dot ".." 118 | if ($char === '.' && $dotIndexDepth > 1) { 119 | $tokens[] = $this->createToken($tokenValue); 120 | $tokenValue = ''; 121 | continue; 122 | } 123 | 124 | if ($this->atEnd($i) || \in_array($this->lookAhead($i), ['.', '['])) { 125 | $tokens[] = $this->createToken($tokenValue); 126 | $tokenValue = ''; 127 | $dotIndexDepth--; 128 | } 129 | } 130 | } 131 | 132 | if ($tokenValue !== '') { 133 | $tokens[] = $this->createToken($tokenValue); 134 | } 135 | 136 | return $tokens; 137 | } 138 | 139 | protected function lookAhead(int $pos, int $forward = 1): ?string 140 | { 141 | return $this->expression[$pos + $forward] ?? null; 142 | } 143 | 144 | protected function atEnd(int $pos): bool 145 | { 146 | return $pos === $this->expressionLength; 147 | } 148 | 149 | /** 150 | * @throws JSONPathException 151 | */ 152 | public function parseExpression(): array 153 | { 154 | return $this->parseExpressionTokens(); 155 | } 156 | 157 | /** 158 | * @throws JSONPathException 159 | */ 160 | protected function createToken(string $value): JSONPathToken 161 | { 162 | // The IDE doesn't like, what we do with $value, so let's 163 | // move it to a separate variable, to get rid of any IDE warnings 164 | $tokenValue = $value; 165 | 166 | /** @var JSONPathToken|null $ret */ 167 | $ret = null; 168 | 169 | if (\preg_match('/^(' . static::MATCH_INDEX . ')$/xu', $tokenValue, $matches)) { 170 | if (\preg_match('/^-?\d+$/', $tokenValue)) { 171 | $tokenValue = (int)$tokenValue; 172 | } 173 | 174 | $ret = new JSONPathToken(JSONPathToken::T_INDEX, $tokenValue); 175 | } elseif (\preg_match('/^' . static::MATCH_INDEXES . '$/xu', $tokenValue, $matches)) { 176 | $tokenValue = \explode(',', \trim($tokenValue, ',')); 177 | 178 | foreach ($tokenValue as $i => $v) { 179 | $tokenValue[$i] = (int)\trim($v); 180 | } 181 | 182 | $ret = new JSONPathToken(JSONPathToken::T_INDEXES, $tokenValue); 183 | } elseif (\preg_match('/^' . static::MATCH_SLICE . '$/xu', $tokenValue, $matches)) { 184 | $parts = \explode(':', $tokenValue); 185 | $tokenValue = [ 186 | 'start' => isset($parts[0]) && $parts[0] !== '' ? (int)$parts[0] : null, 187 | 'end' => isset($parts[1]) && $parts[1] !== '' ? (int)$parts[1] : null, 188 | 'step' => isset($parts[2]) && $parts[2] !== '' ? (int)$parts[2] : null, 189 | ]; 190 | 191 | $ret = new JSONPathToken(JSONPathToken::T_SLICE, $tokenValue); 192 | } elseif (\preg_match('/^' . static::MATCH_QUERY_RESULT . '$/xu', $tokenValue)) { 193 | $tokenValue = \substr($tokenValue, 1, -1); 194 | 195 | $ret = new JSONPathToken(JSONPathToken::T_QUERY_RESULT, $tokenValue); 196 | } elseif (\preg_match('/^' . static::MATCH_QUERY_MATCH . '$/xu', $tokenValue)) { 197 | $tokenValue = \substr($tokenValue, 2, -1); 198 | 199 | $ret = new JSONPathToken(JSONPathToken::T_QUERY_MATCH, $tokenValue); 200 | } elseif ( 201 | \preg_match('/^' . static::MATCH_INDEX_IN_SINGLE_QUOTES . '$/xu', $tokenValue, $matches) 202 | || \preg_match('/^' . static::MATCH_INDEX_IN_DOUBLE_QUOTES . '$/xu', $tokenValue, $matches) 203 | ) { 204 | if (isset($matches[1])) { 205 | $tokenValue = $matches[1]; 206 | $tokenValue = \trim($tokenValue); 207 | 208 | $possibleArray = false; 209 | if ($matches[0][0] === '"') { 210 | $possibleArray = \explode('","', $tokenValue); 211 | } elseif ($matches[0][0] === "'") { 212 | $possibleArray = \explode("','", $tokenValue); 213 | } 214 | if ($possibleArray !== false && \count($possibleArray) > 1) { 215 | $tokenValue = $possibleArray; 216 | } 217 | } else { 218 | $tokenValue = ''; 219 | } 220 | 221 | $ret = new JSONPathToken(JSONPathToken::T_INDEX, $tokenValue); 222 | } 223 | 224 | if ($ret !== null) { 225 | return $ret; 226 | } 227 | 228 | throw new JSONPathException("Unable to parse token {$tokenValue} in expression: {$this->expression}"); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/JSONPathToken.php: -------------------------------------------------------------------------------- 1 | validateType($type); 38 | 39 | $this->type = $type; 40 | $this->value = $value; 41 | } 42 | 43 | /** 44 | * @throws JSONPathException 45 | */ 46 | public function validateType(string $type): void 47 | { 48 | if (!\in_array($type, static::getTypes(), true)) { 49 | throw new JSONPathException('Invalid token: ' . $type); 50 | } 51 | } 52 | 53 | public static function getTypes(): array 54 | { 55 | return [ 56 | static::T_INDEX, 57 | static::T_RECURSIVE, 58 | static::T_QUERY_RESULT, 59 | static::T_QUERY_MATCH, 60 | static::T_SLICE, 61 | static::T_INDEXES, 62 | ]; 63 | } 64 | 65 | /** 66 | * @throws JSONPathException 67 | */ 68 | public function buildFilter(bool $options) 69 | { 70 | $filterClass = 'Flow\\JSONPath\\Filters\\' . \ucfirst($this->type) . 'Filter'; 71 | 72 | if (!\class_exists($filterClass)) { 73 | throw new JSONPathException("No filter class exists for token [{$this->type}]"); 74 | } 75 | 76 | return new $filterClass($this, $options); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/JSONPathArrayAccessTest.php: -------------------------------------------------------------------------------- 1 | getData('conferences')); 30 | $jsonPath = new JSONPath($container); 31 | 32 | $teams = $jsonPath 33 | ->find('.conferences.*') 34 | ->find('..teams.*'); 35 | 36 | self::assertEquals('Dodger', $teams[0]['name']); 37 | self::assertEquals('Mets', $teams[1]['name']); 38 | 39 | $teams = $jsonPath 40 | ->find('.conferences.*') 41 | ->find('..teams.*'); 42 | 43 | self::assertEquals('Dodger', $teams[0]['name']); 44 | self::assertEquals('Mets', $teams[1]['name']); 45 | 46 | $teams = $jsonPath 47 | ->find('.conferences..teams.*'); 48 | 49 | self::assertEquals('Dodger', $teams[0]['name']); 50 | self::assertEquals('Mets', $teams[1]['name']); 51 | } 52 | 53 | /** 54 | * @throws Exception 55 | */ 56 | public function testIterating(): void 57 | { 58 | $container = new ArrayObject($this->getData('conferences')); 59 | 60 | $conferences = (new JSONPath($container)) 61 | ->find('.conferences.*'); 62 | 63 | $names = []; 64 | 65 | foreach ($conferences as $conference) { 66 | $players = $conference 67 | ->find('.teams.*.players[?(@.active=yes)]'); 68 | 69 | foreach ($players as $player) { 70 | $names[] = $player->name; 71 | } 72 | } 73 | 74 | self::assertEquals(['Joe Face', 'something'], $names); 75 | } 76 | 77 | /** 78 | * @throws JsonException 79 | * 80 | * @testWith [false] 81 | * [true] 82 | */ 83 | public function testDifferentStylesOfAccess(bool $asArray = true): void 84 | { 85 | $container = new ArrayObject($this->getData('conferences', $asArray)); 86 | $data = new JSONPath($container); 87 | 88 | self::assertArrayHasKey('conferences', $data); 89 | 90 | $conferences = $data->__get('conferences')->getData(); 91 | 92 | if (\is_array($conferences[0])) { 93 | self::assertEquals('Western Conference', $conferences[0]['name']); 94 | } else { 95 | self::assertEquals('Western Conference', $conferences[0]->name); 96 | } 97 | } 98 | 99 | /** 100 | * @noinspection PhpUndefinedFieldInspection 101 | */ 102 | public function testUpdate(): void 103 | { 104 | $container = new ArrayObject($this->getData('conferences')); 105 | $data = new JSONPath($container); 106 | 107 | $data->offsetSet('name', 'Major League Football'); 108 | self::assertEquals('Major League Football', $data->name); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/JSONPathArrayTest.php: -------------------------------------------------------------------------------- 1 | getData('conferences'))); 28 | 29 | $teams = $jsonPath 30 | ->find('.conferences.*') 31 | ->find('..teams.*'); 32 | 33 | self::assertEquals('Dodger', $teams[0]['name']); 34 | self::assertEquals('Mets', $teams[1]['name']); 35 | 36 | $teams = $jsonPath 37 | ->find('.conferences.*') 38 | ->find('..teams.*'); 39 | 40 | self::assertEquals('Dodger', $teams[0]['name']); 41 | self::assertEquals('Mets', $teams[1]['name']); 42 | 43 | $teams = $jsonPath 44 | ->find('.conferences..teams.*'); 45 | 46 | self::assertEquals('Dodger', $teams[0]['name']); 47 | self::assertEquals('Mets', $teams[1]['name']); 48 | } 49 | 50 | /** 51 | * @throws Exception 52 | */ 53 | public function testIterating(): void 54 | { 55 | $data = $this->getData('conferences'); 56 | 57 | $conferences = (new JSONPath($data)) 58 | ->find('.conferences.*'); 59 | 60 | $names = []; 61 | 62 | foreach ($conferences as $conference) { 63 | $players = $conference 64 | ->find('.teams.*.players[?(@.active=yes)]'); 65 | 66 | foreach ($players as $player) { 67 | $names[] = $player->name; 68 | } 69 | } 70 | 71 | self::assertEquals(['Joe Face', 'something'], $names); 72 | } 73 | 74 | /** 75 | * @throws Exception 76 | */ 77 | public function testDifferentStylesOfAccess(): void 78 | { 79 | $data = (new JSONPath($this->getData('conferences', \random_int(0, 1)))); 80 | 81 | self::assertArrayHasKey('conferences', $data); 82 | 83 | $conferences = $data->__get('conferences')->getData(); 84 | 85 | if (\is_array($conferences[0])) { 86 | self::assertEquals('Western Conference', $conferences[0]['name']); 87 | } else { 88 | self::assertEquals('Western Conference', $conferences[0]->name); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/JSONPathDashedIndexTest.php: -------------------------------------------------------------------------------- 1 | ['test-test-test' => 'foo']], 29 | ['foo'], 30 | ], 31 | [ 32 | '$.data[40f35757-2563-4790-b0b1-caa904be455f]', 33 | ['data' => ['40f35757-2563-4790-b0b1-caa904be455f' => 'bar']], 34 | ['bar'], 35 | ], 36 | ]; 37 | } 38 | 39 | /** 40 | * @throws JSONPathException 41 | */ 42 | #[DataProvider('indexDataProvider')] 43 | public function testSlice(string $path, array $data, array $expected): void 44 | { 45 | $results = (new JSONPath($data)) 46 | ->find($path); 47 | 48 | self::assertEquals($expected, $results->getData()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/JSONPathLexerTest.php: -------------------------------------------------------------------------------- 1 | parseExpression(); 27 | 28 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type); 29 | self::assertEquals("*", $tokens[0]->value); 30 | } 31 | 32 | /** 33 | * @throws JSONPathException 34 | */ 35 | public function testIndexSimple(): void 36 | { 37 | $tokens = (new JSONPathLexer('.foo')) 38 | ->parseExpression(); 39 | 40 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type); 41 | self::assertEquals("foo", $tokens[0]->value); 42 | } 43 | 44 | /** 45 | * @throws JSONPathException 46 | */ 47 | public function testIndexRecursive(): void 48 | { 49 | $tokens = (new JSONPathLexer('..teams.*')) 50 | ->parseExpression(); 51 | 52 | self::assertCount(3, $tokens); 53 | self::assertEquals(JSONPathToken::T_RECURSIVE, $tokens[0]->type); 54 | self::assertEquals(null, $tokens[0]->value); 55 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[1]->type); 56 | self::assertEquals('teams', $tokens[1]->value); 57 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[2]->type); 58 | self::assertEquals('*', $tokens[2]->value); 59 | } 60 | 61 | /** 62 | * @throws JSONPathException 63 | */ 64 | public function testIndexComplex(): void 65 | { 66 | $tokens = (new JSONPathLexer('["\'b.^*_"]')) 67 | ->parseExpression(); 68 | 69 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type); 70 | self::assertEquals("'b.^*_", $tokens[0]->value); 71 | } 72 | 73 | /** 74 | * @throws JSONPathException 75 | */ 76 | public function testIndexBadlyFormed(): void 77 | { 78 | $this->expectException(JSONPathException::class); 79 | $this->expectExceptionMessage('Unable to parse token hello* in expression: .hello*'); 80 | 81 | (new JSONPathLexer('.hello*')) 82 | ->parseExpression(); 83 | } 84 | 85 | /** 86 | * @throws JSONPathException 87 | */ 88 | public function testIndexInteger(): void 89 | { 90 | $tokens = (new JSONPathLexer('[0]')) 91 | ->parseExpression(); 92 | 93 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type); 94 | self::assertEquals("0", $tokens[0]->value); 95 | } 96 | 97 | /** 98 | * @throws JSONPathException 99 | */ 100 | public function testIndexIntegerAfterDotNotation(): void 101 | { 102 | $tokens = (new JSONPathLexer('.books[0]')) 103 | ->parseExpression(); 104 | 105 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type); 106 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[1]->type); 107 | self::assertEquals("books", $tokens[0]->value); 108 | self::assertEquals("0", $tokens[1]->value); 109 | } 110 | 111 | /** 112 | * @throws JSONPathException 113 | */ 114 | public function testIndexWord(): void 115 | { 116 | $tokens = (new JSONPathLexer('["foo$-/\'"]')) 117 | ->parseExpression(); 118 | 119 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type); 120 | self::assertEquals("foo$-/'", $tokens[0]->value); 121 | } 122 | 123 | /** 124 | * @throws JSONPathException 125 | */ 126 | public function testIndexWordWithWhitespace(): void 127 | { 128 | $tokens = (new JSONPathLexer('[ "foo$-/\'" ]')) 129 | ->parseExpression(); 130 | 131 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type); 132 | self::assertEquals("foo$-/'", $tokens[0]->value); 133 | } 134 | 135 | /** 136 | * @throws JSONPathException 137 | */ 138 | public function testSliceSimple(): void 139 | { 140 | $tokens = (new JSONPathLexer('[0:1:2]')) 141 | ->parseExpression(); 142 | 143 | self::assertEquals(JSONPathToken::T_SLICE, $tokens[0]->type); 144 | self::assertEquals(['start' => 0, 'end' => 1, 'step' => 2], $tokens[0]->value); 145 | } 146 | 147 | /** 148 | * @throws JSONPathException 149 | */ 150 | public function testIndexNegativeIndex(): void 151 | { 152 | $tokens = (new JSONPathLexer('[-1]')) 153 | ->parseExpression(); 154 | 155 | self::assertEquals(JSONPathToken::T_SLICE, $tokens[0]->type); 156 | self::assertEquals(['start' => -1, 'end' => null, 'step' => null], $tokens[0]->value); 157 | } 158 | 159 | /** 160 | * @throws JSONPathException 161 | */ 162 | public function testSliceAllNull(): void 163 | { 164 | $tokens = (new JSONPathLexer('[:]')) 165 | ->parseExpression(); 166 | 167 | self::assertEquals(JSONPathToken::T_SLICE, $tokens[0]->type); 168 | self::assertEquals(['start' => null, 'end' => null, 'step' => null], $tokens[0]->value); 169 | } 170 | 171 | /** 172 | * @throws JSONPathException 173 | */ 174 | public function testQueryResultSimple(): void 175 | { 176 | $tokens = (new JSONPathLexer('[(@.foo + 2)]')) 177 | ->parseExpression(); 178 | 179 | self::assertEquals(JSONPathToken::T_QUERY_RESULT, $tokens[0]->type); 180 | self::assertEquals('@.foo + 2', $tokens[0]->value); 181 | } 182 | 183 | /** 184 | * @throws JSONPathException 185 | */ 186 | public function testQueryMatchSimple(): void 187 | { 188 | $tokens = (new JSONPathLexer('[?(@.foo < \'bar\')]')) 189 | ->parseExpression(); 190 | 191 | self::assertEquals(JSONPathToken::T_QUERY_MATCH, $tokens[0]->type); 192 | self::assertEquals('@.foo < \'bar\'', $tokens[0]->value); 193 | } 194 | 195 | /** 196 | * @throws JSONPathException 197 | */ 198 | public function testQueryMatchNotEqualTO(): void 199 | { 200 | $tokens = (new JSONPathLexer('[?(@.foo != \'bar\')]')) 201 | ->parseExpression(); 202 | 203 | self::assertEquals(JSONPathToken::T_QUERY_MATCH, $tokens[0]->type); 204 | self::assertEquals('@.foo != \'bar\'', $tokens[0]->value); 205 | } 206 | 207 | /** 208 | * @throws JSONPathException 209 | */ 210 | public function testQueryMatchBrackets(): void 211 | { 212 | $tokens = (new JSONPathLexer("[?(@['@language']='en')]")) 213 | ->parseExpression(); 214 | 215 | self::assertEquals(JSONPathToken::T_QUERY_MATCH, $tokens[0]->type); 216 | self::assertEquals("@['@language']='en'", $tokens[0]->value); 217 | } 218 | 219 | /** 220 | * @throws JSONPathException 221 | */ 222 | public function testRecursiveSimple(): void 223 | { 224 | $tokens = (new JSONPathLexer('..foo')) 225 | ->parseExpression(); 226 | 227 | self::assertEquals(JSONPathToken::T_RECURSIVE, $tokens[0]->type); 228 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[1]->type); 229 | self::assertEquals(null, $tokens[0]->value); 230 | self::assertEquals('foo', $tokens[1]->value); 231 | } 232 | 233 | /** 234 | * @throws JSONPathException 235 | */ 236 | public function testRecursiveWildcard(): void 237 | { 238 | $tokens = (new JSONPathLexer('..*')) 239 | ->parseExpression(); 240 | 241 | self::assertEquals(JSONPathToken::T_RECURSIVE, $tokens[0]->type); 242 | self::assertEquals(JSONPathToken::T_INDEX, $tokens[1]->type); 243 | self::assertEquals(null, $tokens[0]->value); 244 | self::assertEquals('*', $tokens[1]->value); 245 | } 246 | 247 | /** 248 | * @throws JSONPathException 249 | */ 250 | public function testRecursiveBadlyFormed(): void 251 | { 252 | $this->expectException(JSONPathException::class); 253 | $this->expectExceptionMessage('Unable to parse token ba^r in expression: ..ba^r'); 254 | 255 | (new JSONPathLexer('..ba^r')) 256 | ->parseExpression(); 257 | } 258 | 259 | /** 260 | * @throws JSONPathException 261 | */ 262 | public function testIndexesSimple(): void 263 | { 264 | $tokens = (new JSONPathLexer('[1,2,3]')) 265 | ->parseExpression(); 266 | 267 | self::assertEquals(JSONPathToken::T_INDEXES, $tokens[0]->type); 268 | self::assertEquals([1, 2, 3], $tokens[0]->value); 269 | } 270 | 271 | /** 272 | * @throws JSONPathException 273 | */ 274 | public function testIndexesWhitespace(): void 275 | { 276 | $tokens = (new JSONPathLexer('[ 1,2 , 3]')) 277 | ->parseExpression(); 278 | 279 | self::assertEquals(JSONPathToken::T_INDEXES, $tokens[0]->type); 280 | self::assertEquals([1, 2, 3], $tokens[0]->value); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /tests/JSONPathSliceAccessTest.php: -------------------------------------------------------------------------------- 1 | ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 26 | ['foo1', 'foo2'], 27 | ], 28 | [ 29 | '$.data[4:]', 30 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 31 | ['foo4', 'foo5'], 32 | ], 33 | [ 34 | '$.data[:2]', 35 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 36 | ['foo0', 'foo1'], 37 | ], 38 | [ 39 | '$.data[:]', 40 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 41 | ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5'], 42 | ], 43 | [ 44 | '$.data[-1]', 45 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 46 | ['foo5'], 47 | ], 48 | [ 49 | '$.data[-2:]', 50 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 51 | ['foo4', 'foo5'], 52 | ], 53 | [ 54 | '$.data[:-2]', 55 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 56 | ['foo0', 'foo1', 'foo2', 'foo3'], 57 | ], 58 | [ 59 | '$.data[::2]', 60 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 61 | ['foo0', 'foo2', 'foo4'], 62 | ], 63 | [ 64 | '$.data[2::2]', 65 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 66 | ['foo2', 'foo4'], 67 | ], 68 | [ 69 | '$.data[:-2:2]', 70 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 71 | ['foo0', 'foo2'], 72 | ], 73 | [ 74 | '$.data[1:5:2]', 75 | ['data' => ['foo0', 'foo1', 'foo2', 'foo3', 'foo4', 'foo5']], 76 | ['foo1', 'foo3'], 77 | ], 78 | ]; 79 | } 80 | 81 | /** 82 | * @throws JSONPathException 83 | */ 84 | #[DataProvider('sliceDataProvider')] 85 | public function testSlice(string $path, array $data, array $expected): void 86 | { 87 | $result = (new JSONPath($data)) 88 | ->find($path); 89 | 90 | self::assertEquals($expected, $result->getData()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/JSONPathTest.php: -------------------------------------------------------------------------------- 1 | getData('example'))) 33 | ->find('$.store.books[0].title'); 34 | 35 | self::assertEquals('Sayings of the Century', $result[0]); 36 | } 37 | 38 | /** 39 | * @throws JSONPathException|JsonException 40 | */ 41 | public function testIndexesObject(): void 42 | { 43 | $result = (new JSONPath($this->getData('indexed-object'))) 44 | ->find('$.store.books[3].title'); 45 | 46 | self::assertEquals('Sword of Honour', $result[0]); 47 | } 48 | 49 | /** 50 | * $['store']['books'][0]['title'] 51 | * 52 | * @throws JSONPathException|JsonException 53 | */ 54 | public function testChildOperatorsAlt(): void 55 | { 56 | $result = (new JSONPath($this->getData('example'))) 57 | ->find("$['store']['books'][0]['title']"); 58 | 59 | self::assertEquals('Sayings of the Century', $result[0]); 60 | } 61 | 62 | /** 63 | * $.array[start:end:step] 64 | * 65 | * @throws JSONPathException|JsonException 66 | */ 67 | public function testFilterSliceA(): void 68 | { 69 | // Copy all items... similar to a wildcard 70 | $result = (new JSONPath($this->getData('example'))) 71 | ->find("$['store']['books'][:].title"); 72 | 73 | self::assertEquals( 74 | ['Sayings of the Century', 'Sword of Honour', 'Moby Dick', 'The Lord of the Rings'], 75 | $result->getData() 76 | ); 77 | } 78 | 79 | /** 80 | * Positive end indexes 81 | * $[0:2] 82 | * 83 | * @throws JSONPathException 84 | */ 85 | public function testFilterSlicePositiveEndIndexes(): void 86 | { 87 | $jsonPath = (new JSONPath(['first', 'second', 'third', 'fourth', 'fifth'])); 88 | 89 | $result = $jsonPath 90 | ->find('$[0:0]'); 91 | 92 | self::assertEquals([], $result->getData()); 93 | 94 | $result = $jsonPath 95 | ->find('$[0:1]'); 96 | 97 | self::assertEquals(['first'], $result->getData()); 98 | 99 | $result = $jsonPath 100 | ->find('$[0:2]'); 101 | 102 | self::assertEquals(['first', 'second'], $result->getData()); 103 | 104 | $result = $jsonPath 105 | ->find('$[:2]'); 106 | 107 | self::assertEquals(['first', 'second'], $result->getData()); 108 | 109 | $result = $jsonPath 110 | ->find('$[1:2]'); 111 | 112 | self::assertEquals(['second'], $result->getData()); 113 | 114 | $result = $jsonPath 115 | ->find('$[0:3:1]'); 116 | 117 | self::assertEquals(['first', 'second', 'third'], $result->getData()); 118 | 119 | $result = $jsonPath 120 | ->find('$[0:3:0]'); 121 | 122 | self::assertEquals(['first', 'second', 'third'], $result->getData()); 123 | } 124 | 125 | /** 126 | * @throws JSONPathException 127 | */ 128 | public function testFilterSliceNegativeStartIndexes(): void 129 | { 130 | $result = (new JSONPath(['first', 'second', 'third', 'fourth', 'fifth'])) 131 | ->find('$[-2:]'); 132 | 133 | self::assertEquals(['fourth', 'fifth'], $result->getData()); 134 | 135 | $result = (new JSONPath(['first', 'second', 'third', 'fourth', 'fifth'])) 136 | ->find('$[-1:]'); 137 | 138 | self::assertEquals(['fifth'], $result->getData()); 139 | 140 | $result = (new JSONPath(['first', 'second', 'third'])) 141 | ->find('$[-4:]'); 142 | 143 | self::assertEquals(['first', 'second', 'third'], $result->getData()); 144 | } 145 | 146 | /** 147 | * Negative end indexes 148 | * $[:-2] 149 | * 150 | * @throws JSONPathException 151 | */ 152 | public function testFilterSliceNegativeEndIndexes(): void 153 | { 154 | $jsonPath = (new JSONPath(['first', 'second', 'third', 'fourth', 'fifth'])); 155 | 156 | $result = $jsonPath 157 | ->find('$[:-2]'); 158 | 159 | self::assertEquals(['first', 'second', 'third'], $result->getData()); 160 | 161 | $result = $jsonPath 162 | ->find('$[0:-2]'); 163 | 164 | self::assertEquals(['first', 'second', 'third'], $result->getData()); 165 | } 166 | 167 | /** 168 | * Negative end indexes 169 | * $[:-2] 170 | * 171 | * @throws JSONPathException 172 | */ 173 | public function testFilterSliceNegativeStartAndEndIndexes(): void 174 | { 175 | $jsonPath = (new JSONPath(['first', 'second', 'third', 'fourth', 'fifth'])); 176 | 177 | $result = $jsonPath 178 | ->find('$[-2:-1]'); 179 | 180 | self::assertEquals(['fourth'], $result->getData()); 181 | 182 | $result = $jsonPath 183 | ->find('$[-4:-2]'); 184 | 185 | self::assertEquals(['second', 'third'], $result->getData()); 186 | } 187 | 188 | /** 189 | * Negative end indexes 190 | * $[:-2] 191 | * 192 | * @throws JSONPathException 193 | */ 194 | public function testFilterSliceNegativeStartAndPositiveEnd(): void 195 | { 196 | $result = (new JSONPath(['first', 'second', 'third', 'fourth', 'fifth'])) 197 | ->find('$[-2:2]'); 198 | 199 | self::assertEquals([], $result->getData()); 200 | } 201 | 202 | /** 203 | * @throws JSONPathException 204 | */ 205 | public function testFilterSliceStepBy2(): void 206 | { 207 | $result = (new JSONPath(['first', 'second', 'third', 'fourth', 'fifth'])) 208 | ->find('$[0:4:2]'); 209 | 210 | self::assertEquals(['first', 'third'], $result->getData()); 211 | } 212 | 213 | /** 214 | * The Last item 215 | * $[-1] 216 | * 217 | * @throws JSONPathException 218 | */ 219 | public function testFilterLastIndex(): void 220 | { 221 | $result = (new JSONPath(['first', 'second', 'third', 'fourth', 'fifth'])) 222 | ->find('$[-1]'); 223 | 224 | self::assertEquals(['fifth'], $result->getData()); 225 | } 226 | 227 | /** 228 | * Array index slice only end 229 | * $[:2] 230 | * 231 | * @throws JSONPathException 232 | */ 233 | public function testFilterSliceG(): void 234 | { 235 | // Fetch up to the second index 236 | $result = (new JSONPath(['first', 'second', 'third', 'fourth', 'fifth'])) 237 | ->find('$[:2]'); 238 | 239 | self::assertEquals(['first', 'second'], $result->getData()); 240 | } 241 | 242 | /** 243 | * $.store.books[(@.length-1)].title 244 | * 245 | * This notation is only partially implemented eg. hacked in 246 | * 247 | * @throws JSONPathException|JsonException 248 | */ 249 | public function testChildQuery(): void 250 | { 251 | $result = (new JSONPath($this->getData('example'))) 252 | ->find('$.store.books[(@.length-1)].title'); 253 | 254 | self::assertEquals(['The Lord of the Rings'], $result->getData()); 255 | } 256 | 257 | /** 258 | * $.store.books[?(@.price < 10)].title 259 | * Filter books that have a price less than 10 260 | * 261 | * @throws JSONPathException|JsonException 262 | */ 263 | public function testQueryMatchLessThan(): void 264 | { 265 | $result = (new JSONPath($this->getData('example'))) 266 | ->find('$.store.books[?(@.price < 10)].title'); 267 | 268 | self::assertEquals(['Sayings of the Century', 'Moby Dick'], $result->getData()); 269 | } 270 | 271 | /** 272 | * $.store.books[?(@.price > 10)].title 273 | * Filter books that have a price more than 10 274 | * 275 | * @throws JSONPathException|JsonException 276 | */ 277 | public function testQueryMatchMoreThan(): void 278 | { 279 | $result = (new JSONPath($this->getData('example'))) 280 | ->find('$.store.books[?(@.price > 10)].title'); 281 | 282 | self::assertEquals(['Sword of Honour', 'The Lord of the Rings'], $result->getData()); 283 | } 284 | 285 | /** 286 | * $.store.books[?(@.price <= 12.99)].title 287 | * Filter books that have a price less or equal to 12.99 288 | * 289 | * @throws JSONPathException|JsonException 290 | */ 291 | public function testQueryMatchLessOrEqual(): void 292 | { 293 | $result = (new JSONPath($this->getData('example'))) 294 | ->find('$.store.books[?(@.price <= 12.99)].title'); 295 | 296 | self::assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], $result->getData()); 297 | } 298 | 299 | /** 300 | * $.store.books[?(@.price >= 12.99)].title 301 | * Filter books that have a price less or equal to 12.99 302 | * 303 | * @throws JSONPathException|JsonException 304 | */ 305 | public function testQueryMatchEqualOrMore(): void 306 | { 307 | $result = (new JSONPath($this->getData('example'))) 308 | ->find('$.store.books[?(@.price >= 12.99)].title'); 309 | 310 | self::assertEquals(['Sword of Honour', 'The Lord of the Rings'], $result->getData()); 311 | } 312 | 313 | /** 314 | * $..books[?(@.author == "J. R. R. Tolkien")] 315 | * Filter books that have an author equal to "..." 316 | * 317 | * @throws JSONPathException|JsonException 318 | */ 319 | public function testQueryMatchEquals(): void 320 | { 321 | $result = (new JSONPath($this->getData('example'))) 322 | ->find('$..books[?(@.author == "J. R. R. Tolkien")].title'); 323 | 324 | self::assertEquals('The Lord of the Rings', $result[0]); 325 | } 326 | 327 | /** 328 | * $..books[?(@.category=="fiction" && @.author == \'Evelyn Waugh\')].title' 329 | * Filter books that are in the "..." category and have an author equal to "..." 330 | * 331 | * @throws JSONPathException|JsonException 332 | */ 333 | public function testQueryMatchWhitespaceIgnored(): void 334 | { 335 | // Additional spaces in filter string are intended to be there 336 | $result = (new JSONPath($this->getData('example'))) 337 | ->find('$..books[?( @.category=="fiction" && @.author == \'Evelyn Waugh\' )].title'); 338 | 339 | self::assertEquals('Sword of Honour', $result[0]); 340 | } 341 | 342 | /** 343 | * $..books[?(@.author = 1)] 344 | * Filter books that have a title equal to "..." 345 | * 346 | * @throws JSONPathException|JsonException 347 | */ 348 | public function testQueryMatchEqualsWithUnquotedInteger(): void 349 | { 350 | $results = (new JSONPath($this->getData('simple-integers'))) 351 | ->find('$..features[?(@.value = 1)]'); 352 | 353 | self::assertEquals('foo', $results[0]->name); 354 | self::assertEquals('baz', $results[1]->name); 355 | } 356 | 357 | /** 358 | * $..books[?(@.author != "J. R. R. Tolkien")] 359 | * Filter books that have an author not equal to "..." 360 | * 361 | * @throws JSONPathException|JsonException 362 | */ 363 | public function testQueryMatchNotEqualsTo(): void 364 | { 365 | $jsonPath = (new JSONPath($this->getData('example'))); 366 | 367 | $results = $jsonPath 368 | ->find('$..books[?(@.author != "J. R. R. Tolkien")].title'); 369 | 370 | self::assertcount(3, $results); 371 | self::assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], $results->getData()); 372 | 373 | $results = $jsonPath 374 | ->find('$..books[?(@.author !== "J. R. R. Tolkien")].title'); 375 | 376 | self::assertcount(3, $results); 377 | self::assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], $results->getData()); 378 | 379 | $results = $jsonPath 380 | ->find('$..books[?(@.author <> "J. R. R. Tolkien")].title'); 381 | 382 | self::assertcount(3, $results); 383 | self::assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], $results->getData()); 384 | } 385 | 386 | /** 387 | * $..books[?(@.author =~ /nigel ree?s/i)] 388 | * Filter books where author matches regex 389 | * 390 | * @throws JSONPathException|JsonException 391 | */ 392 | public function testQueryMatchWithRegexCaseSensitive(): void 393 | { 394 | $jsonPath = (new JSONPath($this->getData('example'))); 395 | 396 | $results = $jsonPath 397 | ->find('$..books[?(@.author =~ /nigel ree?s/i)].title'); 398 | 399 | self::assertcount(1, $results); 400 | self::assertEquals(['Sayings of the Century'], $results->getData()); 401 | 402 | $results = $jsonPath 403 | ->find('$..books[?(@.title =~ /^(Say|The).*/)].title'); 404 | 405 | self::assertcount(2, $results); 406 | self::assertEquals(['Sayings of the Century', 'The Lord of the Rings'], $results->getData()); 407 | } 408 | 409 | /** 410 | * $..books[?(@.author =~ "J. R. R. Tolkien")] 411 | * Filter books where author matches invalid regex 412 | * 413 | * @throws JSONPathException|JsonException 414 | */ 415 | public function testQueryMatchWithInvalidRegex(): void 416 | { 417 | $result = (new JSONPath($this->getData('example'))) 418 | ->find('$..books[?(@.author =~ "J. R. R. Tolkien")].title'); 419 | 420 | self::assertEmpty($result->getData()); 421 | } 422 | 423 | /** 424 | * $..books[?(@.author in ["J. R. R. Tolkien", "Nigel Rees"])] 425 | * Filter books that have a title in ["...", "..."] 426 | * 427 | * @throws JSONPathException|JsonException 428 | */ 429 | public function testQueryMatchIn(): void 430 | { 431 | $results = (new JSONPath($this->getData('example'))) 432 | ->find('$..books[?(@.author in ["J. R. R. Tolkien", "Nigel Rees"])].title'); 433 | 434 | self::assertEquals(['Sayings of the Century', 'The Lord of the Rings'], $results->getData()); 435 | } 436 | 437 | /** 438 | * $..books[?(@.author nin ["J. R. R. Tolkien", "Nigel Rees"])] 439 | * Filter books that don't have a title in ["...", "..."] 440 | * 441 | * @throws JSONPathException|JsonException 442 | */ 443 | public function testQueryMatchNin(): void 444 | { 445 | $results = (new JSONPath($this->getData('example'))) 446 | ->find('$..books[?(@.author nin ["J. R. R. Tolkien", "Nigel Rees"])].title'); 447 | 448 | self::assertEquals(['Sword of Honour', 'Moby Dick'], $results->getData()); 449 | } 450 | 451 | /** 452 | * $..books[?(@.author nin ["J. R. R. Tolkien", "Nigel Rees"])] 453 | * Filter books that don't have a title in ["...", "..."] 454 | * 455 | * @throws JSONPathException|JsonException 456 | */ 457 | public function testQueryMatchNotIn(): void 458 | { 459 | $results = (new JSONPath($this->getData('example'))) 460 | ->find('$..books[?(@.author !in ["J. R. R. Tolkien", "Nigel Rees"])].title'); 461 | 462 | self::assertEquals(['Sword of Honour', 'Moby Dick'], $results->getData()); 463 | } 464 | 465 | /** 466 | * $.store.books[*].author 467 | * 468 | * @throws JSONPathException|JsonException 469 | */ 470 | public function testWildcardAltNotation(): void 471 | { 472 | $results = (new JSONPath($this->getData('example'))) 473 | ->find('$.store.books[*].author'); 474 | 475 | self::assertEquals(['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'], $results->getData()); 476 | } 477 | 478 | /** 479 | * $..author 480 | * 481 | * @throws JSONPathException|JsonException 482 | */ 483 | public function testRecursiveChildSearch(): void 484 | { 485 | $result = (new JSONPath($this->getData('example'))) 486 | ->find('$..author'); 487 | 488 | self::assertEquals(['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'], $result->getData()); 489 | } 490 | 491 | /** 492 | * $.store.* 493 | * all things in store 494 | * the structure of the example data makes this test look weird 495 | * 496 | * @throws JSONPathException|JsonException 497 | */ 498 | public function testWildCard(): void 499 | { 500 | $result = (new JSONPath($this->getData('example'))) 501 | ->find('$.store.*'); 502 | 503 | if (\is_object($result[0][0])) { 504 | self::assertEquals('Sayings of the Century', $result[0][0]->title); 505 | } else { 506 | self::assertEquals('Sayings of the Century', $result[0][0]['title']); 507 | } 508 | 509 | if (\is_object($result[1])) { 510 | self::assertEquals('red', $result[1]->color); 511 | } else { 512 | self::assertEquals('red', $result[1]['color']); 513 | } 514 | } 515 | 516 | /** 517 | * $.store..price 518 | * the price of everything in the store. 519 | * 520 | * @throws JSONPathException|JsonException 521 | */ 522 | public function testRecursiveChildSearchAlt(): void 523 | { 524 | $result = (new JSONPath($this->getData('example'))) 525 | ->find('$.store..price'); 526 | 527 | self::assertEquals([8.95, 12.99, 8.99, 22.99, 19.95], $result->getData()); 528 | } 529 | 530 | /** 531 | * $..books[2] 532 | * the third book 533 | * 534 | * @throws JSONPathException|JsonException 535 | */ 536 | public function testRecursiveChildSearchWithChildIndex(): void 537 | { 538 | $result = (new JSONPath($this->getData('example'))) 539 | ->find('$..books[2].title'); 540 | 541 | self::assertEquals(['Moby Dick'], $result->getData()); 542 | } 543 | 544 | /** 545 | * $..books[(@.length-1)] 546 | * 547 | * @throws JSONPathException|JsonException 548 | */ 549 | public function testRecursiveChildSearchWithChildQuery(): void 550 | { 551 | $result = (new JSONPath($this->getData('example'))) 552 | ->find('$..books[(@.length-1)].title'); 553 | 554 | self::assertEquals(['The Lord of the Rings'], $result->getData()); 555 | } 556 | 557 | /** 558 | * $..books[-1:] 559 | * Return the last results 560 | * 561 | * @throws JSONPathException|JsonException 562 | */ 563 | public function testRecursiveChildSearchWithSliceFilter(): void 564 | { 565 | $result = (new JSONPath($this->getData('example'))) 566 | ->find('$..books[-1:].title'); 567 | 568 | self::assertEquals(['The Lord of the Rings'], $result->getData()); 569 | } 570 | 571 | /** 572 | * $..books[?(@.isbn)] 573 | * filter all books with isbn number 574 | * 575 | * @throws JSONPathException|JsonException 576 | */ 577 | public function testRecursiveWithQueryMatch(): void 578 | { 579 | $result = (new JSONPath($this->getData('example'))) 580 | ->find('$..books[?(@.isbn)].isbn'); 581 | 582 | self::assertEquals(['0-553-21311-3', '0-395-19395-8'], $result->getData()); 583 | } 584 | 585 | /** 586 | * .data.tokens[?(@.Employee.FirstName)] 587 | * Verify that it is possible to filter with a key containing punctuation 588 | * 589 | * @throws JSONPathException|JsonException 590 | */ 591 | public function testRecursiveWithQueryMatchWithDots(): void 592 | { 593 | $result = (new JSONPath($this->getData('with-dots'))) 594 | ->find(".data.tokens[?(@.Employee.FirstName)]"); 595 | $result = \json_decode( 596 | \json_encode($result, JSON_THROW_ON_ERROR), 597 | true, 598 | 512, 599 | JSON_THROW_ON_ERROR 600 | ); 601 | 602 | self::assertEquals([['Employee.FirstName' => 'Jack']], $result); 603 | } 604 | 605 | /** 606 | * $..* 607 | * All members of JSON structure 608 | * 609 | * @throws JSONPathException|JsonException 610 | */ 611 | public function testRecursiveWithWildcard(): void 612 | { 613 | $result = (new JSONPath($this->getData('example'))) 614 | ->find('$..*'); 615 | $result = \json_decode( 616 | \json_encode($result, JSON_THROW_ON_ERROR), 617 | true, 618 | 512, 619 | JSON_THROW_ON_ERROR 620 | ); 621 | 622 | self::assertEquals('Sayings of the Century', $result[0]['books'][0]['title']); 623 | self::assertEquals(19.95, $result[27]); 624 | } 625 | 626 | /** 627 | * Tests direct key access. 628 | * 629 | * @throws JSONPathException 630 | */ 631 | public function testSimpleArrayAccess(): void 632 | { 633 | $result = (new JSONPath(['title' => 'test title'])) 634 | ->find('title'); 635 | 636 | self::assertEquals(['test title'], $result->getData()); 637 | } 638 | 639 | /** 640 | * @throws JSONPathException 641 | */ 642 | public function testFilteringOnNoneArrays(): void 643 | { 644 | $result = (new JSONPath(['foo' => 'asdf'])) 645 | ->find('$.foo.bar'); 646 | 647 | self::assertEquals([], $result->getData()); 648 | } 649 | 650 | /** 651 | * @throws JSONPathException 652 | */ 653 | public function testMagicMethods(): void 654 | { 655 | $fooClass = new JSONPathTestClass(); 656 | $results = (new JSONPath($fooClass, JSONPath::ALLOW_MAGIC))->find('$.foo'); 657 | 658 | self::assertEquals(['bar'], $results->getData()); 659 | } 660 | 661 | /** 662 | * @throws JSONPathException|JsonException 663 | */ 664 | public function testMatchWithComplexSquareBrackets(): void 665 | { 666 | $result = (new JSONPath($this->getData('extra'))) 667 | ->find("$['http://www.w3.org/2000/01/rdf-schema#label'][?(@['@language']='en')]['@language']"); 668 | 669 | self::assertEquals(["en"], $result->getData()); 670 | } 671 | 672 | /** 673 | * @throws JSONPathException|JsonException 674 | */ 675 | public function testQueryMatchWithRecursive(): void 676 | { 677 | $result = (new JSONPath($this->getData('locations'))) 678 | ->find("..[?(@.type == 'suburb')].name"); 679 | 680 | self::assertEquals(["Rosebank"], $result->getData()); 681 | } 682 | 683 | /** 684 | * @throws JSONPathException|JsonException 685 | */ 686 | public function testFirst(): void 687 | { 688 | $result = (new JSONPath($this->getData('extra'))) 689 | ->find("$['http://www.w3.org/2000/01/rdf-schema#label'].*"); 690 | 691 | self::assertEquals(["@language" => "en"], $result->first()->getData()); 692 | } 693 | 694 | /** 695 | * @throws JSONPathException|JsonException 696 | */ 697 | public function testLast(): void 698 | { 699 | $result = (new JSONPath($this->getData('extra'))) 700 | ->find("$['http://www.w3.org/2000/01/rdf-schema#label'].*"); 701 | 702 | self::assertEquals(["@language" => "de"], $result->last()->getData()); 703 | } 704 | 705 | /** 706 | * @throws JSONPathException|JsonException 707 | */ 708 | public function testSlashesInIndex(): void 709 | { 710 | $result = (new JSONPath($this->getData('with-slashes'))) 711 | ->find("$['mediatypes']['image/png']"); 712 | 713 | self::assertEquals(["/core/img/filetypes/image.png"], $result->getData()); 714 | } 715 | 716 | /** 717 | * @throws JSONPathException 718 | */ 719 | public function testUnionWithKeys(): void 720 | { 721 | $result = (new JSONPath( 722 | [ 723 | "key" => "value", 724 | "another" => "entry", 725 | ] 726 | ))->find("$['key','another']"); 727 | 728 | self::assertEquals(["value", "entry"], $result->getData()); 729 | } 730 | 731 | /** 732 | * @throws JSONPathException 733 | */ 734 | public function testCyrillicText(): void 735 | { 736 | $jsonPath = (new JSONPath(["трололо" => 1])); 737 | 738 | $result = $jsonPath 739 | ->find("$['трололо']"); 740 | 741 | self::assertEquals([1], $result->getData()); 742 | 743 | $result = $jsonPath 744 | ->find("$.трололо"); 745 | 746 | self::assertEquals([1], $result->getData()); 747 | } 748 | 749 | public function testOffsetUnset(): void 750 | { 751 | $jsonIterator = new JSONPath( 752 | [ 753 | "route" => [ 754 | ["name" => "A", "type" => "type of A"], 755 | ["name" => "B", "type" => "type of B"], 756 | ], 757 | ] 758 | ); 759 | 760 | /** @var JSONPath $route */ 761 | $route = $jsonIterator->offsetGet('route'); 762 | $route->offsetUnset(0); 763 | $first = $route->first(); 764 | 765 | self::assertEquals("B", $first['name']); 766 | } 767 | 768 | public function testFirstKey(): void 769 | { 770 | // Array test for array 771 | $firstKey = (new JSONPath(['a' => 'A', 'b', 'B']))->firstKey(); 772 | 773 | self::assertEquals('a', $firstKey); 774 | 775 | // Array test for object 776 | $firstKey = (new JSONPath((object)['a' => 'A', 'b', 'B']))->firstKey(); 777 | 778 | self::assertEquals('a', $firstKey); 779 | } 780 | 781 | public function testLastKey(): void 782 | { 783 | // Array test for array 784 | $lastKey = (new JSONPath(['a' => 'A', 'b' => 'B', 'c' => 'C']))->lastKey(); 785 | 786 | self::assertEquals('c', $lastKey); 787 | 788 | // Array test for object 789 | $lastKey = (new JSONPath((object)['a' => 'A', 'b' => 'B', 'c' => 'C']))->lastKey(); 790 | 791 | self::assertEquals('c', $lastKey); 792 | } 793 | 794 | /** 795 | * Test: ensure trailing comma is stripped during parsing 796 | * 797 | * @throws JSONPathException|JsonException 798 | */ 799 | public function testTrailingComma(): void 800 | { 801 | $result = (new JSONPath($this->getData('example'))) 802 | ->find("$..books[0,1,2,]"); 803 | 804 | self::assertCount(3, $result); 805 | } 806 | 807 | /** 808 | * Test: ensure negative indexes return -n from last index 809 | * 810 | * @throws JSONPathException|JsonException 811 | */ 812 | public function testNegativeIndex(): void 813 | { 814 | $result = (new JSONPath($this->getData('example'))) 815 | ->find('$..books[-2]'); 816 | 817 | self::assertEquals("Herman Melville", $result[0]['author']); 818 | } 819 | 820 | /** 821 | * @throws JSONPathException|JsonException 822 | */ 823 | public function testQueryAccessWithNumericalIndexes(): void 824 | { 825 | $result = (new JSONPath($this->getData('numerical-indexes-object'))) 826 | ->find("$.result.list[?(@.o == \"11.51000\")]"); 827 | 828 | self::assertEquals("11.51000", $result[0]->o); 829 | 830 | $result = (new JSONPath($this->getData('numerical-indexes-array'))) 831 | ->find("$.result.list[?(@[1] == \"11.51000\")]"); 832 | 833 | self::assertEquals("11.51000", $result[0][1]); 834 | } 835 | } 836 | -------------------------------------------------------------------------------- /tests/JSONPathTestClass.php: -------------------------------------------------------------------------------- 1 | 'bar', 17 | ]; 18 | 19 | /** 20 | * @noinspection MagicMethodsValidityInspection 21 | */ 22 | public function __get(mixed $key): ?string 23 | { 24 | return $this->attributes[$key] ?? null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/QueryTest.php: -------------------------------------------------------------------------------- 1 | find($selector)); 74 | 75 | self::assertEquals($consensus, $results); 76 | 77 | if (\in_array($id, self::$baselineFailedQueries, true)) { 78 | throw new ExpectationFailedException( 79 | "XFAIL test {$id} unexpectedly passed, update baselineFailedQueries.txt" 80 | ); 81 | } 82 | } catch (ExpectationFailedException $e) { 83 | try { 84 | // In some cases, the consensus is just disordered, while 85 | // the actual result is correct. Let's perform a canonical 86 | // assert in these cases. There might be still some false positives 87 | // (e.g. multidimensional comparisons), but that's okay, I guess. Maybe, 88 | // we can also find a way around that in the future. 89 | $message = "==========================\n"; 90 | $message .= "Query: {$query}\n\nMore information: {$url}\n"; 91 | $message .= "==========================\n\n"; 92 | self::assertEqualsCanonicalizing( 93 | \json_decode($consensus, true), 94 | \json_decode($results, true), 95 | $message 96 | ); 97 | } catch (ExpectationFailedException) { 98 | if (!\in_array($id, self::$baselineFailedQueries, true)) { 99 | throw new ExpectationFailedException( 100 | $e->getMessage() . "\nQuery: {$query}\n\nMore information: {$url}", 101 | $e->getComparisonFailure() 102 | ); 103 | } 104 | } 105 | } catch (JSONPathException | RuntimeException $e) { 106 | if (!\in_array($id, self::$baselineFailedQueries, true)) { 107 | throw new RuntimeException( 108 | $e->getMessage() . "\nQuery: {$query}\n\nMore information: {$url}", 109 | ); 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * Returns a list of queries, test data and expected results. 116 | * 117 | * A handful of queries may run forever, thus they should 118 | * be skipped for now. 119 | * 120 | * Queries that are currently known as "problematic" are: 121 | * 122 | * - array_slice_with_negative_step_and_start_greater_than_end 123 | * - array_slice_with_open_end_and_negative_step 124 | * - array_slice_with_large_number_for_start 125 | * - array_slice_with_large_number_for_end 126 | * - array_slice_with_open_start_and_negative_step 127 | * - array_slice_with_negative_step_only 128 | * 129 | * The list is generated automatically, based on the results 130 | * at https://cburgmer.github.io/json-path-comparison. 131 | * 132 | * @return string[] 133 | */ 134 | public static function queryDataProvider(): array 135 | { 136 | return [ 137 | [ // data set #0 138 | 'array_slice', 139 | '$[1:3]', 140 | '["first","second","third","forth","fifth"]', 141 | '["second","third"]', 142 | ], 143 | [ // data set #1 144 | 'array_slice_on_exact_match', 145 | '$[0:5]', 146 | '["first","second","third","forth","fifth"]', 147 | '["first","second","third","forth","fifth"]', 148 | ], 149 | [ // data set #2 150 | 'array_slice_on_non_overlapping_array', 151 | '$[7:10]', 152 | '["first","second","third"]', 153 | '[]', 154 | ], 155 | [ // data set #3 156 | 'array_slice_on_object', 157 | '$[1:3]', 158 | '{":":42,"more":"string","a":1,"b":2,"c":3,"1:3":"nice"}', 159 | '[]', 160 | ], 161 | [ // data set #4 162 | 'array_slice_on_partially_overlapping_array', 163 | '$[1:10]', 164 | '["first","second","third"]', 165 | '["second","third"]', 166 | ], 167 | [ // data set #5 168 | 'array_slice_with_large_number_for_end', 169 | '$[2:113667776004]', 170 | '["first","second","third","forth","fifth"]', 171 | '["third","forth","fifth"]', 172 | true, // skip 173 | ], 174 | [ // data set #6 - unknown consensus, fallback to Proposal A 175 | 'array_slice_with_large_number_for_end_and_negative_step', 176 | '$[2:-113667776004:-1]', 177 | '["first","second","third","forth","fifth"]', 178 | '["third","second","first"]', 179 | ], 180 | [ // data set #7 181 | 'array_slice_with_large_number_for_start', 182 | '$[-113667776004:2]', 183 | '["first","second","third","forth","fifth"]', 184 | '["first","second"]', 185 | true, // skip 186 | ], 187 | [ // data set #8 - unknown consensus, fallback to Proposal A 188 | 'array_slice_with_large_number_for_start_end_negative_step', 189 | '$[113667776004:2:-1]', 190 | '["first","second","third","forth","fifth"]', 191 | '["fifth","forth"]', 192 | ], 193 | [ // data set #9 194 | 'array_slice_with_negative_start_and_end_and_range_of_-1', 195 | '$[-4:-5]', 196 | '[2,"a",4,5,100,"nice"]', 197 | '[]', 198 | ], 199 | [ // data set #10 200 | 'array_slice_with_negative_start_and_end_and_range_of_0', 201 | '$[-4:-4]', 202 | '[2,"a",4,5,100,"nice"]', 203 | '[]', 204 | ], 205 | [ // data set #11 206 | 'array_slice_with_negative_start_and_end_and_range_of_1', 207 | '$[-4:-3]', 208 | '[2,"a",4,5,100,"nice"]', 209 | '[4]', 210 | ], 211 | [ // data set #12 212 | 'array_slice_with_negative_start_and_positive_end_and_range_of_-1', 213 | '$[-4:1]', 214 | '[2,"a",4,5,100,"nice"]', 215 | '[]', 216 | ], 217 | [ // data set #13 218 | 'array_slice_with_negative_start_and_positive_end_and_range_of_0', 219 | '$[-4:2]', 220 | '[2,"a",4,5,100,"nice"]', 221 | '[]', 222 | ], 223 | [ // data set #14 224 | 'array_slice_with_negative_start_and_positive_end_and_range_of_1', 225 | '$[-4:3]', 226 | '[2,"a",4,5,100,"nice"]', 227 | '[4]', 228 | ], 229 | [ // data set #15 - unknown consensus, fallback to Proposal A 230 | 'array_slice_with_negative_step', 231 | '$[3:0:-2]', 232 | '["first","second","third","forth","fifth"]', 233 | '["forth","second"]', 234 | ], 235 | [ // data set #16 - unknown consensus, fallback to Proposal A 236 | 'array_slice_with_negative_step_and_start_greater_than_end', 237 | '$[0:3:-2]', 238 | '["first","second","third","forth","fifth"]', 239 | '[]', 240 | true, // skip 241 | ], 242 | [ // data set #17 - unknown consensus, fallback to Proposal A 243 | 'array_slice_with_negative_step_on_partially_overlapping_array', 244 | '$[7:3:-1]', 245 | '["first","second","third","forth","fifth"]', 246 | '["fifth"]', 247 | ], 248 | [ // data set #18 - unknown consensus, fallback to Proposal A 249 | 'array_slice_with_negative_step_only', 250 | '$[::-2]', 251 | '["first","second","third","forth","fifth"]', 252 | '["fifth","third","first"]', 253 | true, // skip 254 | ], 255 | [ // data set #19 256 | 'array_slice_with_open_end', 257 | '$[1:]', 258 | '["first","second","third","forth","fifth"]', 259 | '["second","third","forth","fifth"]', 260 | ], 261 | [ // data set #20 - unknown consensus, fallback to Proposal A 262 | 'array_slice_with_open_end_and_negative_step', 263 | '$[3::-1]', 264 | '["first","second","third","forth","fifth"]', 265 | '["forth","third","second","first"]', 266 | true, // skip 267 | ], 268 | [ // data set #21 269 | 'array_slice_with_open_start', 270 | '$[:2]', 271 | '["first","second","third","forth","fifth"]', 272 | '["first","second"]', 273 | ], 274 | [ // data set #22 275 | 'array_slice_with_open_start_and_end', 276 | '$[:]', 277 | '["first","second"]', 278 | '["first","second"]', 279 | ], 280 | [ // data set #23 281 | 'array_slice_with_open_start_and_end_and_step_empty', 282 | '$[::]', 283 | '["first","second"]', 284 | '["first","second"]', 285 | ], 286 | [ // data set #24 - unknown consensus, fallback to Proposal A 287 | 'array_slice_with_open_start_and_end_on_object', 288 | '$[:]', 289 | '{":":42,"more":"string"}', 290 | '[]', 291 | ], 292 | [ // data set #25 - unknown consensus, fallback to Proposal A 293 | 'array_slice_with_open_start_and_negative_step', 294 | '$[:2:-1]', 295 | '["first","second","third","forth","fifth"]', 296 | '["fifth","forth"]', 297 | true, // skip 298 | ], 299 | [ // data set #26 300 | 'array_slice_with_positive_start_and_negative_end_and_range_of_-1', 301 | '$[3:-4]', 302 | '[2,"a",4,5,100,"nice"]', 303 | '[]', 304 | ], 305 | [ // data set #27 306 | 'array_slice_with_positive_start_and_negative_end_and_range_of_0', 307 | '$[3:-3]', 308 | '[2,"a",4,5,100,"nice"]', 309 | '[]', 310 | ], 311 | [ // data set #28 312 | 'array_slice_with_positive_start_and_negative_end_and_range_of_1', 313 | '$[3:-2]', 314 | '[2,"a",4,5,100,"nice"]', 315 | '[5]', 316 | ], 317 | [ // data set #29 318 | 'array_slice_with_range_of_-1', 319 | '$[2:1]', 320 | '["first","second","third","forth"]', 321 | '[]', 322 | ], 323 | [ // data set #30 324 | 'array_slice_with_range_of_0', 325 | '$[0:0]', 326 | '["first","second"]', 327 | '[]', 328 | ], 329 | [ // data set #31 330 | 'array_slice_with_range_of_1', 331 | '$[0:1]', 332 | '["first","second"]', 333 | '["first"]', 334 | ], 335 | [ // data set #32 336 | 'array_slice_with_start_-1_and_open_end', 337 | '$[-1:]', 338 | '["first","second","third"]', 339 | '["third"]', 340 | ], 341 | [ // data set #33 342 | 'array_slice_with_start_-2_and_open_end', 343 | '$[-2:]', 344 | '["first","second","third"]', 345 | '["second","third"]', 346 | ], 347 | [ // data set #34 348 | 'array_slice_with_start_large_negative_number_and_open_end_on_short_array', 349 | '$[-4:]', 350 | '["first","second","third"]', 351 | '["first","second","third"]', 352 | ], 353 | [ // data set #35 354 | 'array_slice_with_step', 355 | '$[0:3:2]', 356 | '["first","second","third","forth","fifth"]', 357 | '["first","third"]', 358 | ], 359 | [ // data set #36 - unknown consensus 360 | 'array_slice_with_step_0', 361 | '$[0:3:0]', 362 | '["first","second","third","forth","fifth"]', 363 | '', 364 | ], 365 | [ // data set #37 366 | 'array_slice_with_step_1', 367 | '$[0:3:1]', 368 | '["first","second","third","forth","fifth"]', 369 | '["first","second","third"]', 370 | ], 371 | [ // data set #38 372 | 'array_slice_with_step_and_leading_zeros', 373 | '$[010:024:010]', 374 | '[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]', 375 | '[10,20]', 376 | ], 377 | [ // data set #39 378 | 'array_slice_with_step_but_end_not_aligned', 379 | '$[0:4:2]', 380 | '["first","second","third","forth","fifth"]', 381 | '["first","third"]', 382 | ], 383 | [ // data set #40 384 | 'array_slice_with_step_empty', 385 | '$[1:3:]', 386 | '["first","second","third","forth","fifth"]', 387 | '["second","third"]', 388 | ], 389 | [ // data set #41 390 | 'array_slice_with_step_only', 391 | '$[::2]', 392 | '["first","second","third","forth","fifth"]', 393 | '["first","third","fifth"]', 394 | ], 395 | [ // data set #42 396 | 'bracket_notation', 397 | '$[\'key\']', 398 | '{"key":"value"}', 399 | '["value"]', 400 | ], 401 | [ // data set #43 402 | 'bracket_notation_after_recursive_descent', 403 | '$..[0]', 404 | '["first",{"key":["first nested",{"more":[{"nested":["deepest","second"]},["more","values"]]}]}]', 405 | '["deepest","first nested","first","more",{"nested":["deepest","second"]}]', 406 | ], 407 | [ // data set #44 408 | 'bracket_notation_on_object_without_key', 409 | '$[\'missing\']', 410 | '{"key":"value"}', 411 | '[]', 412 | ], 413 | [ // data set #45 414 | 'bracket_notation_with_NFC_path_on_NFD_key', 415 | '$[\'ü\']', 416 | '{"u\\u0308":42}', 417 | '[]', 418 | ], 419 | [ // data set #46 420 | 'bracket_notation_with_dot', 421 | '$[\'two.some\']', 422 | '{"one":{"key":"value"},"two":{"some":"more","key":"other value"},"two.some":"42"}', 423 | '["42"]', 424 | ], 425 | [ // data set #47 426 | 'bracket_notation_with_double_quotes', 427 | '$["key"]', 428 | '{"key":"value"}', 429 | '["value"]', 430 | ], 431 | [ // data set #48 - unknown consensus 432 | 'bracket_notation_with_empty_path', 433 | '$[]', 434 | '{"":42,"\'\'":123,"\\"\\"":222}', 435 | '', 436 | ], 437 | [ // data set #49 438 | 'bracket_notation_with_empty_string', 439 | '$[\'\']', 440 | '{"":42,"\'\'":123,"\\"\\"":222}', 441 | '[42]', 442 | ], 443 | [ // data set #50 444 | 'bracket_notation_with_empty_string_doubled_quoted', 445 | '$[""]', 446 | '{"":42,"\'\'":123,"\\"\\"":222}', 447 | '[42]', 448 | ], 449 | [ // data set #51 450 | 'bracket_notation_with_negative_number_on_short_array', 451 | '$[-2]', 452 | '["one element"]', 453 | '[]', 454 | ], 455 | [ // data set #52 456 | 'bracket_notation_with_number', 457 | '$[2]', 458 | '["first","second","third","forth","fifth"]', 459 | '["third"]', 460 | ], 461 | [ // data set #53 462 | 'bracket_notation_with_number_-1', 463 | '$[-1]', 464 | '["first","second","third"]', 465 | '["third"]', 466 | ], 467 | [ // data set #54 468 | 'bracket_notation_with_number_-1_on_empty_array', 469 | '$[-1]', 470 | '[]', 471 | '[]', 472 | ], 473 | [ // data set #55 474 | 'bracket_notation_with_number_0', 475 | '$[0]', 476 | '["first","second","third","forth","fifth"]', 477 | '["first"]', 478 | ], 479 | [ // data set #56 480 | 'bracket_notation_with_number_after_dot_notation_with_wildcard_on_nested_arrays_with_different_length', 481 | '$.*[1]', 482 | '[[1],[2,3]]', 483 | '[3]', 484 | ], 485 | [ // data set #57 - unknown consensus, fallback to Proposal A 486 | 'bracket_notation_with_number_on_object', 487 | '$[0]', 488 | '{"0":"value"}', 489 | '[]', 490 | ], 491 | [ // data set #58 492 | 'bracket_notation_with_number_on_short_array', 493 | '$[1]', 494 | '["one element"]', 495 | '[]', 496 | ], 497 | [ // data set #59 - unknown consensus, fallback to Proposal A 498 | 'bracket_notation_with_number_on_string', 499 | '$[0]', 500 | '"Hello World"', 501 | '[]', 502 | ], 503 | [ // data set #60 504 | 'bracket_notation_with_quoted_array_slice_literal', 505 | '$[\':\']', 506 | '{":":"value","another":"entry"}', 507 | '["value"]', 508 | ], 509 | [ // data set #61 510 | 'bracket_notation_with_quoted_closing_bracket_literal', 511 | '$[\']\']', 512 | '{"]":42}', 513 | '[42]', 514 | ], 515 | [ // data set #62 516 | 'bracket_notation_with_quoted_current_object_literal', 517 | '$[\'@\']', 518 | '{"@":"value","another":"entry"}', 519 | '["value"]', 520 | ], 521 | [ // data set #63 522 | 'bracket_notation_with_quoted_dot_literal', 523 | '$[\'.\']', 524 | '{".":"value","another":"entry"}', 525 | '["value"]', 526 | ], 527 | [ // data set #64 528 | 'bracket_notation_with_quoted_dot_wildcard', 529 | '$[\'.*\']', 530 | '{"key":42,".*":1,"":10}', 531 | '[1]', 532 | ], 533 | [ // data set #65 534 | 'bracket_notation_with_quoted_double_quote_literal', 535 | '$[\'"\']', 536 | '{"\\"":"value","another":"entry"}', 537 | '["value"]', 538 | ], 539 | [ // data set #66 - unknown consensus, fallback to Proposal A 540 | 'bracket_notation_with_quoted_escaped_backslash', 541 | '$[\'\\\\\']', 542 | '{"\\\\":"value"}', 543 | '["value"]', 544 | ], 545 | [ // data set #67 - unknown consensus, fallback to Proposal A 546 | 'bracket_notation_with_quoted_escaped_single_quote', 547 | '$[\'\\\'\']', 548 | '{"\'":"value"}', 549 | '["value"]', 550 | ], 551 | [ // data set #68 552 | 'bracket_notation_with_quoted_number_on_object', 553 | '$[\'0\']', 554 | '{"0":"value"}', 555 | '["value"]', 556 | ], 557 | [ // data set #69 558 | 'bracket_notation_with_quoted_root_literal', 559 | '$[\'$\']', 560 | '{"$":"value","another":"entry"}', 561 | '["value"]', 562 | ], 563 | [ // data set #70 - unknown consensus, fallback to Proposal A 564 | 'bracket_notation_with_quoted_special_characters_combined', 565 | '$[\':@."$,*\\\'\\\\\']', 566 | '{":@.\\"$,*\'\\\\":42}', 567 | '[42]', 568 | ], 569 | [ // data set #71 - unknown consensus 570 | 'bracket_notation_with_quoted_string_and_unescaped_single_quote', 571 | '$[\'single\'quote\']', 572 | '{"single\'quote":"value"}', 573 | '', 574 | ], 575 | [ // data set #72 576 | 'bracket_notation_with_quoted_union_literal', 577 | '$[\',\']', 578 | '{",":"value","another":"entry"}', 579 | '["value"]', 580 | ], 581 | [ // data set #73 582 | 'bracket_notation_with_quoted_wildcard_literal', 583 | '$[\'*\']', 584 | '{"*":"value","another":"entry"}', 585 | '["value"]', 586 | ], 587 | [ // data set #74 - unknown consensus, fallback to Proposal A 588 | 'bracket_notation_with_quoted_wildcard_literal_on_object_without_key', 589 | '$[\'*\']', 590 | '{"another":"entry"}', 591 | '[]', 592 | ], 593 | [ // data set #75 594 | 'bracket_notation_with_string_including_dot_wildcard', 595 | '$[\'ni.*\']', 596 | '{"nice":42,"ni.*":1,"mice":100}', 597 | '[1]', 598 | ], 599 | [ // data set #76 - unknown consensus 600 | 'bracket_notation_with_two_literals_separated_by_dot', 601 | '$[\'two\'.\'some\']', 602 | '{"one":{"key":"value"},"two":{"some":"more","key":"other value"},"two.some":"42","two\'.\'some":"43' 603 | . '"}', 604 | '', 605 | ], 606 | [ // data set #77 - unknown consensus 607 | 'bracket_notation_with_two_literals_separated_by_dot_without_quotes', 608 | '$[two.some]', 609 | '{"one":{"key":"value"},"two":{"some":"more","key":"other value"},"two.some":"42"}', 610 | '', 611 | ], 612 | [ // data set #78 613 | 'bracket_notation_with_wildcard_after_array_slice', 614 | '$[0:2][*]', 615 | '[[1,2],["a","b"],[0,0]]', 616 | '[1,2,"a","b"]', 617 | ], 618 | [ // data set #79 619 | 'bracket_notation_with_wildcard_after_dot_notation_after_bracket_notation_with_wildcard', 620 | '$[*].bar[*]', 621 | '[{"bar":[42]}]', 622 | '[42]', 623 | ], 624 | [ // data set #80 625 | 'bracket_notation_with_wildcard_after_recursive_descent', 626 | '$..[*]', 627 | '{"key":"value","another key":{"complex":"string","primitives":[0,1]}}', 628 | '["string","value",0,1,[0,1],{"complex":"string","primitives":[0,1]}]', 629 | ], 630 | [ // data set #81 631 | 'bracket_notation_with_wildcard_on_array', 632 | '$[*]', 633 | '["string",42,{"key":"value"},[0,1]]', 634 | '["string",42,{"key":"value"},[0,1]]', 635 | ], 636 | [ // data set #82 637 | 'bracket_notation_with_wildcard_on_empty_array', 638 | '$[*]', 639 | '[]', 640 | '[]', 641 | ], 642 | [ // data set #83 643 | 'bracket_notation_with_wildcard_on_empty_object', 644 | '$[*]', 645 | '{}', 646 | '[]', 647 | ], 648 | [ // data set #84 649 | 'bracket_notation_with_wildcard_on_null_value_array', 650 | '$[*]', 651 | '[40,null,42]', 652 | '[40,null,42]', 653 | ], 654 | [ // data set #85 655 | 'bracket_notation_with_wildcard_on_object', 656 | '$[*]', 657 | '{"some":"string","int":42,"object":{"key":"value"},"array":[0,1]}', 658 | '["string",42,[0,1],{"key":"value"}]', 659 | ], 660 | [ // data set #86 - unknown consensus 661 | 'bracket_notation_without_quotes', 662 | '$[key]', 663 | '{"key":"value"}', 664 | '', 665 | ], 666 | [ // data set #87 - unknown consensus 667 | 'dot_bracket_notation', 668 | '$.[\'key\']', 669 | '{"key":"value","other":{"key":[{"key":42}]}}', 670 | '', 671 | ], 672 | [ // data set #88 - unknown consensus 673 | 'dot_bracket_notation_with_double_quotes', 674 | '$.["key"]', 675 | '{"key":"value","other":{"key":[{"key":42}]}}', 676 | '', 677 | ], 678 | [ // data set #89 - unknown consensus 679 | 'dot_bracket_notation_without_quotes', 680 | '$.[key]', 681 | '{"key":"value","other":{"key":[{"key":42}]}}', 682 | '', 683 | ], 684 | [ // data set #90 685 | 'dot_notation', 686 | '$.key', 687 | '{"key":"value"}', 688 | '["value"]', 689 | ], 690 | [ // data set #91 691 | 'dot_notation_after_array_slice', 692 | '$[0:2].key', 693 | '[{"key":"ey"},{"key":"bee"},{"key":"see"}]', 694 | '["ey","bee"]', 695 | ], 696 | [ // data set #92 697 | 'dot_notation_after_bracket_notation_after_recursive_descent', 698 | '$..[1].key', 699 | '{"k":[{"key":"some value"},{"key":42}],"kk":[[{"key":100},{"key":200},{"key":300}],[{"key":400},{"k' 700 | . 'ey":500},{"key":600}]],"key":[0,1]}', 701 | '[200,42,500]', 702 | ], 703 | [ // data set #93 704 | 'dot_notation_after_bracket_notation_with_wildcard', 705 | '$[*].a', 706 | '[{"a":1},{"a":1}]', 707 | '[1,1]', 708 | ], 709 | [ // data set #94 710 | 'dot_notation_after_bracket_notation_with_wildcard_on_one_matching', 711 | '$[*].a', 712 | '[{"a":1}]', 713 | '[1]', 714 | ], 715 | [ // data set #95 716 | 'dot_notation_after_bracket_notation_with_wildcard_on_some_matching', 717 | '$[*].a', 718 | '[{"a":1},{"b":1}]', 719 | '[1]', 720 | ], 721 | [ // data set #96 722 | 'dot_notation_after_filter_expression', 723 | '$[?(@.id==42)].name', 724 | '[{"id":42,"name":"forty-two"},{"id":1,"name":"one"}]', 725 | '["forty-two"]', 726 | ], 727 | [ // data set #97 728 | 'dot_notation_after_recursive_descent', 729 | '$..key', 730 | '{"object":{"key":"value","array":[{"key":"something"},{"key":{"key":"russian dolls"}}]},"key":"top"' 731 | . '}', 732 | '["russian dolls","something","top","value",{"key":"russian dolls"}]', 733 | ], 734 | [ // data set #98 735 | 'dot_notation_after_recursive_descent_after_dot_notation', 736 | '$.store..price', 737 | '{"store":{"book":[{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","p' 738 | . 'rice":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},' 739 | . '{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price"' 740 | . ':8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-' 741 | . '395-19395-8","price":22.99}],"bicycle":{"color":"red","price":19.95}}}', 742 | '[12.99,19.95,22.99,8.95,8.99]', 743 | ], 744 | [ // data set #99 745 | 'dot_notation_after_union', 746 | '$[0,2].key', 747 | '[{"key":"ey"},{"key":"bee"},{"key":"see"}]', 748 | '["ey","see"]', 749 | ], 750 | [ // data set #100 751 | 'dot_notation_after_union_with_keys', 752 | '$[\'one\',\'three\'].key', 753 | '{"one":{"key":"value"},"two":{"k":"v"},"three":{"some":"more","key":"other value"}}', 754 | '["value","other value"]', 755 | ], 756 | [ // data set #101 757 | 'dot_notation_on_array', 758 | '$.key', 759 | '[0,1]', 760 | '[]', 761 | ], 762 | [ // data set #102 763 | 'dot_notation_on_array_value', 764 | '$.key', 765 | '{"key":["first","second"]}', 766 | '[["first","second"]]', 767 | ], 768 | [ // data set #103 769 | 'dot_notation_on_array_with_containing_object_matching_key', 770 | '$.id', 771 | '[{"id":2}]', 772 | '[]', 773 | ], 774 | [ // data set #104 775 | 'dot_notation_on_empty_object_value', 776 | '$.key', 777 | '{"key":{}}', 778 | '[{}]', 779 | ], 780 | [ // data set #105 781 | 'dot_notation_on_null_value', 782 | '$.key', 783 | '{"key":null}', 784 | '[null]', 785 | ], 786 | [ // data set #106 787 | 'dot_notation_on_object_without_key', 788 | '$.missing', 789 | '{"key":"value"}', 790 | '[]', 791 | ], 792 | [ // data set #107 793 | 'dot_notation_with_dash', 794 | '$.key-dash', 795 | '{"key-dash":"value"}', 796 | '["value"]', 797 | ], 798 | [ // data set #108 - unknown consensus 799 | 'dot_notation_with_double_quotes', 800 | '$."key"', 801 | '{"key":"value","\\"key\\"":42}', 802 | '', 803 | ], 804 | [ // data set #109 - unknown consensus 805 | 'dot_notation_with_double_quotes_after_recursive_descent', 806 | '$.."key"', 807 | '{"object":{"key":"value","\\"key\\"":100,"array":[{"key":"something","\\"key\\"":0},{"key":{"key":"' 808 | . 'russian dolls"},"\\"key\\"":{"\\"key\\"":99}}]},"key":"top","\\"key\\"":42}', 809 | '', 810 | ], 811 | [ // data set #110 - unknown consensus 812 | 'dot_notation_with_empty_path', 813 | '$.', 814 | '{"key":42,"":9001,"\'\'":"nice"}', 815 | '', 816 | ], 817 | [ // data set #111 818 | 'dot_notation_with_key_named_in', 819 | '$.in', 820 | '{"in":"value"}', 821 | '["value"]', 822 | ], 823 | [ // data set #112 824 | 'dot_notation_with_key_named_length', 825 | '$.length', 826 | '{"length":"value"}', 827 | '["value"]', 828 | ], 829 | [ // data set #113 830 | 'dot_notation_with_key_named_length_on_array', 831 | '$.length', 832 | '[4,5,6]', 833 | '[3]', 834 | ], 835 | [ // data set #114 836 | 'dot_notation_with_key_named_null', 837 | '$.null', 838 | '{"null":"value"}', 839 | '["value"]', 840 | ], 841 | [ // data set #115 842 | 'dot_notation_with_key_named_true', 843 | '$.true', 844 | '{"true":"value"}', 845 | '["value"]', 846 | ], 847 | [ // data set #116 - unknown consensus, fallback to Proposal A 848 | 'dot_notation_with_key_root_literal', 849 | '$.$', 850 | '{"$":"value"}', 851 | '["value"]', 852 | ], 853 | [ // data set #117 854 | 'dot_notation_with_non_ASCII_key', 855 | '$.屬性', 856 | '{"\\u5c6c\\u6027":"value"}', 857 | '["value"]', 858 | ], 859 | [ // data set #118 - unknown consensus, fallback to Proposal A 860 | 'dot_notation_with_number', 861 | '$.2', 862 | '["first","second","third","forth","fifth"]', 863 | '[]', 864 | ], 865 | [ // data set #119 - unknown consensus, fallback to Proposal A 866 | 'dot_notation_with_number_-1', 867 | '$.-1', 868 | '["first","second","third","forth","fifth"]', 869 | '[]', 870 | ], 871 | [ // data set #120 872 | 'dot_notation_with_number_on_object', 873 | '$.2', 874 | '{"a":"first","2":"second","b":"third"}', 875 | '["second"]', 876 | ], 877 | [ // data set #121 - unknown consensus 878 | 'dot_notation_with_single_quotes', 879 | '$.\'key\'', 880 | '{"key":"value","\'key\'":42}', 881 | '', 882 | ], 883 | [ // data set #122 - unknown consensus 884 | 'dot_notation_with_single_quotes_after_recursive_descent', 885 | '$..\'key\'', 886 | '{"object":{"key":"value","\'key\'":100,"array":[{"key":"something","\'key\'":0},{"key":{"key":"russ' 887 | . 'ian dolls"},"\'key\'":{"\'key\'":99}}]},"key":"top","\'key\'":42}', 888 | '', 889 | ], 890 | [ // data set #123 - unknown consensus 891 | 'dot_notation_with_single_quotes_and_dot', 892 | '$.\'some.key\'', 893 | '{"some.key":42,"some":{"key":"value"},"\'some.key\'":43}', 894 | '', 895 | ], 896 | [ // data set #124 897 | 'dot_notation_with_wildcard_after_dot_notation_after_dot_notation_with_wildcard', 898 | '$.*.bar.*', 899 | '[{"bar":[42]}]', 900 | '[42]', 901 | ], 902 | [ // data set #125 903 | 'dot_notation_with_wildcard_after_dot_notation_with_wildcard_on_nested_arrays', 904 | '$.*.*', 905 | '[[1,2,3],[4,5,6]]', 906 | '[1,2,3,4,5,6]', 907 | ], 908 | [ // data set #126 909 | 'dot_notation_with_wildcard_after_recursive_descent', 910 | '$..*', 911 | '{"key":"value","another key":{"complex":"string","primitives":[0,1]}}', 912 | '["string","value",0,1,[0,1],{"complex":"string","primitives":[0,1]}]', 913 | ], 914 | [ // data set #127 915 | 'dot_notation_with_wildcard_after_recursive_descent_on_null_value_array', 916 | '$..*', 917 | '[40,null,42]', 918 | '[40,42,null]', 919 | ], 920 | [ // data set #128 921 | 'dot_notation_with_wildcard_after_recursive_descent_on_scalar', 922 | '$..*', 923 | '42', 924 | '[]', 925 | ], 926 | [ // data set #129 927 | 'dot_notation_with_wildcard_on_array', 928 | '$.*', 929 | '["string",42,{"key":"value"},[0,1]]', 930 | '["string",42,{"key":"value"},[0,1]]', 931 | ], 932 | [ // data set #130 933 | 'dot_notation_with_wildcard_on_empty_array', 934 | '$.*', 935 | '[]', 936 | '[]', 937 | ], 938 | [ // data set #131 939 | 'dot_notation_with_wildcard_on_empty_object', 940 | '$.*', 941 | '{}', 942 | '[]', 943 | ], 944 | [ // data set #132 945 | 'dot_notation_with_wildcard_on_object', 946 | '$.*', 947 | '{"some":"string","int":42,"object":{"key":"value"},"array":[0,1]}', 948 | '["string",42,[0,1],{"key":"value"}]', 949 | ], 950 | [ // data set #133 - unknown consensus 951 | 'dot_notation_without_root', 952 | 'key', 953 | '{"key":"value"}', 954 | '', 955 | ], 956 | [ // data set #134 - unknown consensus, fallback to Proposal A 957 | 'filter_expression_after_dot_notation_with_wildcard_after_recursive_descent', 958 | '$..*[?(@.id>2)]', 959 | '[{"complext":{"one":[{"name":"first","id":1},{"name":"next","id":2},{"name":"another","id":3},{"nam' 960 | . 'e":"more","id":4}],"more":{"name":"next to last","id":5}}},{"name":"last","id":6}]', 961 | '[{"id":3,"name":"another"},{"id":4,"name":"more"},{"id":5,"name":"next to last"}]', 962 | ], 963 | [ // data set #135 - unknown consensus, fallback to Proposal A 964 | 'filter_expression_after_recursive_descent', 965 | '$..[?(@.id==2)]', 966 | '{"id":2,"more":[{"id":2},{"more":{"id":2}},{"id":{"id":2}},[{"id":2}]]}', 967 | '[{"id":2},{"id":2},{"id":2},{"id":2}]', 968 | ], 969 | [ // data set #136 - unknown consensus, fallback to Proposal A 970 | 'filter_expression_on_object', 971 | '$[?(@.key)]', 972 | '{"key":42,"another":{"key":1}}', 973 | '[{"key":1}]', 974 | ], 975 | [ // data set #137 - unknown consensus 976 | 'filter_expression_with_addition', 977 | '$[?(@.key+50==100)]', 978 | '[{"key":60},{"key":50},{"key":10},{"key":-50},{"key+50":100}]', 979 | '', 980 | ], 981 | [ // data set #138 - unknown consensus, fallback to Proposal A 982 | 'filter_expression_with_boolean_and_operator', 983 | '$[?(@.key>42 && @.key<44)]', 984 | '[{"key":42},{"key":43},{"key":44}]', 985 | '[{"key":43}]', 986 | ], 987 | [ // data set #139 - unknown consensus 988 | 'filter_expression_with_boolean_and_operator_and_value_false', 989 | '$[?(@.key>0 && false)]', 990 | '[{"key":1},{"key":3},{"key":"nice"},{"key":true},{"key":null},{"key":false},{"key":{}},{"key":[]},{' 991 | . '"key":-1},{"key":0},{"key":""}]', 992 | '', 993 | ], 994 | [ // data set #140 - unknown consensus 995 | 'filter_expression_with_boolean_and_operator_and_value_true', 996 | '$[?(@.key>0 && true)]', 997 | '[{"key":1},{"key":3},{"key":"nice"},{"key":true},{"key":null},{"key":false},{"key":{}},{"key":[]},{' 998 | . '"key":-1},{"key":0},{"key":""}]', 999 | '', 1000 | ], 1001 | [ // data set #141 - unknown consensus, fallback to Proposal A 1002 | 'filter_expression_with_boolean_or_operator', 1003 | '$[?(@.key>43 || @.key<43)]', 1004 | '[{"key":42},{"key":43},{"key":44}]', 1005 | '[{"key":42},{"key":44}]', 1006 | ], 1007 | [ // data set #142 - unknown consensus 1008 | 'filter_expression_with_boolean_or_operator_and_value_false', 1009 | '$[?(@.key>0 || false)]', 1010 | '[{"key":1},{"key":3},{"key":"nice"},{"key":true},{"key":null},{"key":false},{"key":{}},{"key":[]},{' 1011 | . '"key":-1},{"key":0},{"key":""}]', 1012 | '', 1013 | ], 1014 | [ // data set #143 - unknown consensus 1015 | 'filter_expression_with_boolean_or_operator_and_value_true', 1016 | '$[?(@.key>0 || true)]', 1017 | '[{"key":1},{"key":3},{"key":"nice"},{"key":true},{"key":null},{"key":false},{"key":{}},{"key":[]},{' 1018 | . '"key":-1},{"key":0},{"key":""}]', 1019 | '', 1020 | ], 1021 | [ // data set #144 1022 | 'filter_expression_with_bracket_notation', 1023 | '$[?(@[\'key\']==42)]', 1024 | '[{"key":0},{"key":42},{"key":-1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"key":100},' 1025 | . '{"some":"value"}]', 1026 | '[{"key":42}]', 1027 | ], 1028 | [ // data set #145 1029 | 'filter_expression_with_bracket_notation_and_current_object_literal', 1030 | '$[?(@[\'@key\']==42)]', 1031 | '[{"@key":0},{"@key":42},{"key":42},{"@key":43},{"some":"value"}]', 1032 | '[{"@key":42}]', 1033 | ], 1034 | [ // data set #146 - unknown consensus, fallback to Proposal A 1035 | 'filter_expression_with_bracket_notation_with_-1', 1036 | '$[?(@[-1]==2)]', 1037 | '[[2,3],["a"],[0,2],[2]]', 1038 | '[[0,2],[2]]', 1039 | ], 1040 | [ // data set #147 1041 | 'filter_expression_with_bracket_notation_with_number', 1042 | '$[?(@[1]==\'b\')]', 1043 | '[["a","b"],["x","y"]]', 1044 | '[["a","b"]]', 1045 | ], 1046 | [ // data set #148 - unknown consensus, fallback to Proposal A 1047 | 'filter_expression_with_bracket_notation_with_number_on_object', 1048 | '$[?(@[1]==\'b\')]', 1049 | '{"1":["a","b"],"2":["x","y"]}', 1050 | '[["a","b"]]', 1051 | ], 1052 | [ // data set #149 - unknown consensus, fallback to Proposal A 1053 | 'filter_expression_with_current_object', 1054 | '$[?(@)]', 1055 | '["some value",null,"value",0,1,-1,"",[],{},false,true]', 1056 | '["some value",null,"value",0,1,-1,"",[],{},false,true]', 1057 | ], 1058 | [ // data set #150 - unknown consensus, fallback to Proposal A 1059 | 'filter_expression_with_different_grouped_operators', 1060 | '$[?(@.a && (@.b || @.c))]', 1061 | '[{"a":true},{"a":true,"b":true},{"a":true,"b":true,"c":true},{"b":true,"c":true},{"a":true,"c":true' 1062 | . '},{"c":true},{"b":true}]', 1063 | '[{"a":true,"b":true},{"a":true,"b":true,"c":true},{"a":true,"c":true}]', 1064 | ], 1065 | [ // data set #151 - unknown consensus 1066 | 'filter_expression_with_different_ungrouped_operators', 1067 | '$[?(@.a && @.b || @.c)]', 1068 | '[{"a":true,"b":true},{"a":true,"b":true,"c":true},{"b":true,"c":true},{"a":true,"c":true},{"a":true' 1069 | . '},{"b":true},{"c":true},{"d":true},{}]', 1070 | '', 1071 | ], 1072 | [ // data set #152 - unknown consensus 1073 | 'filter_expression_with_division', 1074 | '$[?(@.key/10==5)]', 1075 | '[{"key":60},{"key":50},{"key":10},{"key":-50},{"key\\/10":5}]', 1076 | '', 1077 | ], 1078 | [ // data set #153 - unknown consensus 1079 | 'filter_expression_with_empty_expression', 1080 | '$[?()]', 1081 | '[1,{"key":42},"value",null]', 1082 | '', 1083 | ], 1084 | [ // data set #154 - unknown consensus, fallback to Proposal A 1085 | 'filter_expression_with_equals', 1086 | '$[?(@.key==42)]', 1087 | '[{"key":0},{"key":42},{"key":-1},{"key":1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"' 1088 | . 'key":100},{"key":"some"},{"key":"42"},{"key":null},{"key":420},{"key":""},{"key":{}},{"key":[]},{"k' 1089 | . 'ey":[42]},{"key":{"key":42}},{"key":{"some":42}},{"some":"value"}]', 1090 | '[{"key":42}]', 1091 | ], 1092 | [ // data set #155 - unknown consensus 1093 | 'filter_expression_with_equals_array', 1094 | '$[?(@.d==["v1","v2"])]', 1095 | '[{"d":["v1","v2"]},{"d":["a","b"]},{"d":"v1"},{"d":"v2"},{"d":{}},{"d":[]},{"d":null},{"d":-1},{"d"' 1096 | . ':0},{"d":1},{"d":"[\'v1\',\'v2\']"},{"d":"[\'v1\', \'v2\']"},{"d":"v1,v2"},{"d":"[\\"v1\\", \\"v2\\' 1097 | . '"]"},{"d":"[\\"v1\\",\\"v2\\"]"}]', 1098 | '', 1099 | ], 1100 | [ // data set #156 - unknown consensus 1101 | 'filter_expression_with_equals_array_for_array_slice_with_range_1', 1102 | '$[?(@[0:1]==[1])]', 1103 | '[[1,2,3],[1],[2,3],1,2]', 1104 | '', 1105 | ], 1106 | [ // data set #157 - unknown consensus 1107 | 'filter_expression_with_equals_array_for_dot_notation_with_star', 1108 | '$[?(@.*==[1,2])]', 1109 | '[[1,2],[2,3],[1],[2],[1,2,3],1,2,3]', 1110 | '', 1111 | ], 1112 | [ // data set #158 - unknown consensus 1113 | 'filter_expression_with_equals_array_with_single_quotes', 1114 | '$[?(@.d==[\'v1\',\'v2\'])]', 1115 | '[{"d":["v1","v2"]},{"d":["a","b"]},{"d":"v1"},{"d":"v2"},{"d":{}},{"d":[]},{"d":null},{"d":-1},{"d"' 1116 | . ':0},{"d":1},{"d":"[\'v1\',\'v2\']"},{"d":"[\'v1\', \'v2\']"},{"d":"v1,v2"},{"d":"[\\"v1\\", \\"v2\\' 1117 | . '"]"},{"d":"[\\"v1\\",\\"v2\\"]"}]', 1118 | '', 1119 | ], 1120 | [ // data set #159 - unknown consensus 1121 | 'filter_expression_with_equals_boolean_expression_value', 1122 | '$[?((@.key<44)==false)]', 1123 | '[{"key":42},{"key":43},{"key":44}]', 1124 | '', 1125 | ], 1126 | [ // data set #160 - unknown consensus, fallback to Proposal A 1127 | 'filter_expression_with_equals_false', 1128 | '$[?(@.key==false)]', 1129 | '[{"some":"some value"},{"key":true},{"key":false},{"key":null},{"key":"value"},{"key":""},{"key":0}' 1130 | . ',{"key":1},{"key":-1},{"key":42},{"key":{}},{"key":[]}]', 1131 | '[{"key":false}]', 1132 | ], 1133 | [ // data set #161 - unknown consensus, fallback to Proposal A 1134 | 'filter_expression_with_equals_null', 1135 | '$[?(@.key==null)]', 1136 | '[{"some":"some value"},{"key":true},{"key":false},{"key":null},{"key":"value"},{"key":""},{"key":0}' 1137 | . ',{"key":1},{"key":-1},{"key":42},{"key":{}},{"key":[]}]', 1138 | '[{"key":null}]', 1139 | ], 1140 | [ // data set #162 - unknown consensus 1141 | 'filter_expression_with_equals_number_for_array_slice_with_range_1', 1142 | '$[?(@[0:1]==1)]', 1143 | '[[1,2,3],[1],[2,3],1,2]', 1144 | '', 1145 | ], 1146 | [ // data set #163 - unknown consensus 1147 | 'filter_expression_with_equals_number_for_bracket_notation_with_star', 1148 | '$[?(@[*]==2)]', 1149 | '[[1,2],[2,3],[1],[2],[1,2,3],1,2,3]', 1150 | '', 1151 | ], 1152 | [ // data set #164 - unknown consensus 1153 | 'filter_expression_with_equals_number_for_dot_notation_with_star', 1154 | '$[?(@.*==2)]', 1155 | '[[1,2],[2,3],[1],[2],[1,2,3],1,2,3]', 1156 | '', 1157 | ], 1158 | [ // data set #165 - unknown consensus, fallback to Proposal A 1159 | 'filter_expression_with_equals_number_with_fraction', 1160 | '$[?(@.key==-0.123e2)]', 1161 | '[{"key":-12.3},{"key":-0.123},{"key":-12},{"key":12.3},{"key":2},{"key":"-0.123e2"}]', 1162 | '[{"key":-12.3}]', 1163 | ], 1164 | [ // data set #166 - unknown consensus 1165 | 'filter_expression_with_equals_number_with_leading_zeros', 1166 | '$[?(@.key==010)]', 1167 | '[{"key":"010"},{"key":"10"},{"key":10},{"key":0},{"key":8}]', 1168 | '', 1169 | ], 1170 | [ // data set #167 - unknown consensus 1171 | 'filter_expression_with_equals_object', 1172 | '$[?(@.d=={"k":"v"})]', 1173 | '[{"d":{"k":"v"}},{"d":{"a":"b"}},{"d":"k"},{"d":"v"},{"d":{}},{"d":[]},{"d":null},{"d":-1},{"d":0},' 1174 | . '{"d":1},{"d":"[object Object]"},{"d":"{\\"k\\": \\"v\\"}"},{"d":"{\\"k\\":\\"v\\"}"},"v"]', 1175 | '', 1176 | ], 1177 | [ // data set #168 - unknown consensus, fallback to Proposal A 1178 | 'filter_expression_with_equals_on_array_of_numbers', 1179 | '$[?(@==42)]', 1180 | '[0,42,-1,41,43,42.0001,41.9999,null,100]', 1181 | '[42]', 1182 | ], 1183 | [ // data set #169 1184 | 'filter_expression_with_equals_on_array_without_match', 1185 | '$[?(@.key==43)]', 1186 | '[{"key":42}]', 1187 | '[]', 1188 | ], 1189 | [ // data set #170 - unknown consensus, fallback to Proposal A 1190 | 'filter_expression_with_equals_on_object', 1191 | '$[?(@.key==42)]', 1192 | '{"a":{"key":0},"b":{"key":42},"c":{"key":-1},"d":{"key":41},"e":{"key":43},"f":{"key":42.0001},"g":' 1193 | . '{"key":41.9999},"h":{"key":100},"i":{"some":"value"}}', 1194 | '[{"key":42}]', 1195 | ], 1196 | [ // data set #171 - unknown consensus, fallback to Proposal A 1197 | 'filter_expression_with_equals_on_object_with_key_matching_query', 1198 | '$[?(@.id==2)]', 1199 | '{"id":2}', 1200 | '[]', 1201 | ], 1202 | [ // data set #172 - unknown consensus, fallback to Proposal A 1203 | 'filter_expression_with_equals_string', 1204 | '$[?(@.key=="value")]', 1205 | '[{"key":"some"},{"key":"value"},{"key":null},{"key":0},{"key":1},{"key":-1},{"key":""},{"key":{}},{' 1206 | . '"key":[]},{"key":"valuemore"},{"key":"morevalue"},{"key":["value"]},{"key":{"some":"value"}},{"key"' 1207 | . ':{"key":"value"}},{"some":"value"}]', 1208 | '[{"key":"value"}]', 1209 | ], 1210 | [ // data set #173 1211 | 'filter_expression_with_equals_string_with_current_object_literal', 1212 | '$[?(@.key=="hi@example.com")]', 1213 | '[{"key":"some"},{"key":"value"},{"key":"hi@example.com"}]', 1214 | '[{"key":"hi@example.com"}]', 1215 | ], 1216 | [ // data set #174 1217 | 'filter_expression_with_equals_string_with_dot_literal', 1218 | '$[?(@.key=="some.value")]', 1219 | '[{"key":"some"},{"key":"value"},{"key":"some.value"}]', 1220 | '[{"key":"some.value"}]', 1221 | ], 1222 | [ // data set #175 1223 | 'filter_expression_with_equals_string_with_single_quotes', 1224 | '$[?(@.key==\'value\')]', 1225 | '[{"key":"some"},{"key":"value"}]', 1226 | '[{"key":"value"}]', 1227 | ], 1228 | [ // data set #176 - unknown consensus, fallback to Proposal A 1229 | 'filter_expression_with_equals_true', 1230 | '$[?(@.key==true)]', 1231 | '[{"some":"some value"},{"key":true},{"key":false},{"key":null},{"key":"value"},{"key":""},{"key":0}' 1232 | . ',{"key":1},{"key":-1},{"key":42},{"key":{}},{"key":[]}]', 1233 | '[{"key":true}]', 1234 | ], 1235 | [ // data set #177 - unknown consensus, fallback to Proposal A 1236 | 'filter_expression_with_equals_with_root_reference', 1237 | '$.items[?(@.key==$.value)]', 1238 | '{"value":42,"items":[{"key":10},{"key":42},{"key":50}]}', 1239 | '[{"key":42}]', 1240 | ], 1241 | [ // data set #178 - unknown consensus, fallback to Proposal A 1242 | 'filter_expression_with_greater_than', 1243 | '$[?(@.key>42)]', 1244 | '[{"key":0},{"key":42},{"key":-1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"key":100},' 1245 | . '{"key":"43"},{"key":"42"},{"key":"41"},{"key":"value"},{"some":"value"}]', 1246 | '[{"key":43},{"key":42.0001},{"key":100}]', 1247 | ], 1248 | [ // data set #179 - unknown consensus, fallback to Proposal A 1249 | 'filter_expression_with_greater_than_or_equal', 1250 | '$[?(@.key>=42)]', 1251 | '[{"key":0},{"key":42},{"key":-1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"key":100},' 1252 | . '{"key":"43"},{"key":"42"},{"key":"41"},{"key":"value"},{"some":"value"}]', 1253 | '[{"key":42},{"key":43},{"key":42.0001},{"key":100}]', 1254 | ], 1255 | [ // data set #180 - unknown consensus 1256 | 'filter_expression_with_in_array_of_values', 1257 | '$[?(@.d in [2, 3])]', 1258 | '[{"d":1},{"d":2},{"d":1},{"d":3},{"d":4}]', 1259 | '[{"d":2},{"d":3}]', 1260 | ], 1261 | [ // data set #181 - unknown consensus 1262 | 'filter_expression_with_in_current_object', 1263 | '$[?(2 in @.d)]', 1264 | '[{"d":[1,2,3]},{"d":[2]},{"d":[1]},{"d":[3,4]},{"d":[4,2]}]', 1265 | '', 1266 | ], 1267 | [ // data set #182 - unknown consensus, fallback to Proposal A 1268 | 'filter_expression_with_less_than', 1269 | '$[?(@.key<42)]', 1270 | '[{"key":0},{"key":42},{"key":-1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"key":100},' 1271 | . '{"key":"43"},{"key":"42"},{"key":"41"},{"key":"value"},{"some":"value"}]', 1272 | '[{"key":0},{"key":-1},{"key":41},{"key":41.9999}]', 1273 | ], 1274 | [ // data set #183 - unknown consensus, fallback to Proposal A 1275 | 'filter_expression_with_less_than_or_equal', 1276 | '$[?(@.key<=42)]', 1277 | '[{"key":0},{"key":42},{"key":-1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"key":100},' 1278 | . '{"key":"43"},{"key":"42"},{"key":"41"},{"key":"value"},{"some":"value"}]', 1279 | '[{"key":0},{"key":42},{"key":-1},{"key":41},{"key":41.9999}]', 1280 | ], 1281 | [ // data set #184 - unknown consensus 1282 | 'filter_expression_with_multiplication', 1283 | '$[?(@.key*2==100)]', 1284 | '[{"key":60},{"key":50},{"key":10},{"key":-50},{"key*2":100}]', 1285 | '', 1286 | ], 1287 | [ // data set #185 - unknown consensus, fallback to Proposal A 1288 | 'filter_expression_with_negation_and_equals', 1289 | '$[?(!(@.key==42))]', 1290 | '[{"key":0},{"key":42},{"key":-1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"key":100},' 1291 | . '{"key":"43"},{"key":"42"},{"key":"41"},{"key":"value"},{"some":"value"}]', 1292 | '[{"key":0},{"key":-1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"key":100},{"key":"43"' 1293 | . '},{"key":"42"},{"key":"41"},{"key":"value"},{"some":"value"}]', 1294 | ], 1295 | [ // data set #186 - unknown consensus, fallback to Proposal A 1296 | 'filter_expression_with_negation_and_less_than', 1297 | '$[?(!(@.key<42))]', 1298 | '[{"key":0},{"key":42},{"key":-1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"key":100},' 1299 | . '{"key":"43"},{"key":"42"},{"key":"41"},{"key":"value"},{"some":"value"}]', 1300 | '[{"key":42},{"key":43},{"key":42.0001},{"key":100},{"key":"43"},{"key":"42"},{"key":"41"},{"key":"v' 1301 | . 'alue"},{"some":"value"}]', 1302 | ], 1303 | [ // data set #187 - unknown consensus, fallback to Proposal A 1304 | 'filter_expression_with_not_equals', 1305 | '$[?(@.key!=42)]', 1306 | '[{"key":0},{"key":42},{"key":-1},{"key":1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"' 1307 | . 'key":100},{"key":"some"},{"key":"42"},{"key":null},{"key":420},{"key":""},{"key":{}},{"key":[]},{"k' 1308 | . 'ey":[42]},{"key":{"key":42}},{"key":{"some":42}},{"some":"value"}]', 1309 | '[{"key":0},{"key":-1},{"key":1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"key":100},{' 1310 | . '"key":"some"},{"key":"42"},{"key":null},{"key":420},{"key":""},{"key":{}},{"key":[]},{"key":[42]},{' 1311 | . '"key":{"key":42}},{"key":{"some":42}}]', 1312 | ], 1313 | [ // data set #188 - unknown consensus 1314 | 'filter_expression_with_regular_expression', 1315 | '$[?(@.name=~/hello.*/)]', 1316 | '[{"name":"hullo world"},{"name":"hello world"},{"name":"yes hello world"},{"name":"HELLO WORLD"},{"' 1317 | . 'name":"good bye"}]', 1318 | '', 1319 | ], 1320 | [ // data set #189 - unknown consensus 1321 | 'filter_expression_with_set_wise_comparison_to_scalar', 1322 | '$[?(@[*]>=4)]', 1323 | '[[1,2],[3,4],[5,6]]', 1324 | '', 1325 | ], 1326 | [ // data set #190 - unknown consensus 1327 | 'filter_expression_with_set_wise_comparison_to_set', 1328 | '$.x[?(@[*]>=$.y[*])]', 1329 | '{"x":[[1,2],[3,4],[5,6]],"y":[3,4,5]}', 1330 | '', 1331 | ], 1332 | [ // data set #191 - unknown consensus 1333 | 'filter_expression_with_single_equal', 1334 | '$[?(@.key=42)]', 1335 | '[{"key":0},{"key":42},{"key":-1},{"key":1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"' 1336 | . 'key":100},{"key":"some"},{"key":"42"},{"key":null},{"key":420},{"key":""},{"key":{}},{"key":[]},{"k' 1337 | . 'ey":[42]},{"key":{"key":42}},{"key":{"some":42}},{"some":"value"}]', 1338 | '', 1339 | ], 1340 | [ // data set #192 - unknown consensus 1341 | 'filter_expression_with_subfilter', 1342 | '$[?(@.a[?(@.price>10)])]', 1343 | '[{"a":[{"price":1},{"price":3}]},{"a":[{"price":11}]},{"a":[{"price":8},{"price":12},{"price":3}]},' 1344 | . '{"a":[]}]', 1345 | '', 1346 | ], 1347 | [ // data set #193 1348 | 'filter_expression_with_subpaths', 1349 | '$[?(@.address.city==\'Berlin\')]', 1350 | '[{"address":{"city":"Berlin"}},{"address":{"city":"London"}}]', 1351 | '[{"address":{"city":"Berlin"}}]', 1352 | ], 1353 | [ // data set #194 - unknown consensus, fallback to Proposal A 1354 | 'filter_expression_with_subtraction', 1355 | '$[?(@.key-50==-100)]', 1356 | '[{"key":60},{"key":50},{"key":10},{"key":-50},{"key-50":-100}]', 1357 | '[{"key-50":-100}]', 1358 | ], 1359 | [ // data set #195 - unknown consensus, fallback to Proposal A 1360 | 'filter_expression_with_tautological_comparison', 1361 | '$[?(1==1)]', 1362 | '[1,3,"nice",true,null,false,{},[],-1,0,""]', 1363 | '[1,3,"nice",true,null,false,{},[],-1,0,""]', 1364 | ], 1365 | [ // data set #196 - unknown consensus 1366 | 'filter_expression_with_triple_equal', 1367 | '$[?(@.key===42)]', 1368 | '[{"key":0},{"key":42},{"key":-1},{"key":1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"' 1369 | . 'key":100},{"key":"some"},{"key":"42"},{"key":null},{"key":420},{"key":""},{"key":{}},{"key":[]},{"k' 1370 | . 'ey":[42]},{"key":{"key":42}},{"key":{"some":42}},{"some":"value"}]', 1371 | '', 1372 | ], 1373 | [ // data set #197 - unknown consensus, fallback to Proposal A 1374 | 'filter_expression_with_value', 1375 | '$[?(@.key)]', 1376 | '[{"some":"some value"},{"key":true},{"key":false},{"key":null},{"key":"value"},{"key":""},{"key":0}' 1377 | . ',{"key":1},{"key":-1},{"key":42},{"key":{}},{"key":[]}]', 1378 | '[{"key":true},{"key":false},{"key":null},{"key":"value"},{"key":""},{"key":0},{"key":1},{"key":-1},' 1379 | . '{"key":42},{"key":{}},{"key":[]}]', 1380 | ], 1381 | [ // data set #198 - unknown consensus, fallback to Proposal A 1382 | 'filter_expression_with_value_after_dot_notation_with_wildcard_on_array_of_objects', 1383 | '$.*[?(@.key)]', 1384 | '[{"some":"some value"},{"key":"value"}]', 1385 | '[]', 1386 | ], 1387 | [ // data set #199 - unknown consensus, fallback to Proposal A 1388 | 'filter_expression_with_value_after_recursive_descent', 1389 | '$..[?(@.id)]', 1390 | '{"id":2,"more":[{"id":2},{"more":{"id":2}},{"id":{"id":2}},[{"id":2}]]}', 1391 | '[{"id":2},{"id":2},{"id":2},{"id":2},{"id":{"id":2}}]', 1392 | ], 1393 | [ // data set #200 - unknown consensus 1394 | 'filter_expression_with_value_false', 1395 | '$[?(false)]', 1396 | '[1,3,"nice",true,null,false,{},[],-1,0,""]', 1397 | '', 1398 | ], 1399 | [ // data set #201 - unknown consensus 1400 | 'filter_expression_with_value_from_recursive_descent', 1401 | '$[?(@..child)]', 1402 | '[{"key":[{"child":1},{"child":2}]},{"key":[{"child":2}]},{"key":[{}]},{"key":[{"something":42}]},{}' 1403 | . ']', 1404 | '', 1405 | ], 1406 | [ // data set #202 - unknown consensus 1407 | 'filter_expression_with_value_null', 1408 | '$[?(null)]', 1409 | '[1,3,"nice",true,null,false,{},[],-1,0,""]', 1410 | '', 1411 | ], 1412 | [ // data set #203 - unknown consensus 1413 | 'filter_expression_with_value_true', 1414 | '$[?(true)]', 1415 | '[1,3,"nice",true,null,false,{},[],-1,0,""]', 1416 | '', 1417 | ], 1418 | [ // data set #204 - unknown consensus 1419 | 'filter_expression_without_parens', 1420 | '$[?@.key==42]', 1421 | '[{"key":0},{"key":42},{"key":-1},{"key":1},{"key":41},{"key":43},{"key":42.0001},{"key":41.9999},{"' 1422 | . 'key":100},{"key":"some"},{"key":"42"},{"key":null},{"key":420},{"key":""},{"key":{}},{"key":[]},{"k' 1423 | . 'ey":[42]},{"key":{"key":42}},{"key":{"some":42}},{"some":"value"}]', 1424 | '', 1425 | ], 1426 | [ // data set #205 - unknown consensus, fallback to Proposal A 1427 | 'filter_expression_without_value', 1428 | '$[?(!@.key)]', 1429 | '[{"some":"some value"},{"key":true},{"key":false},{"key":null},{"key":"value"},{"key":""},{"key":0}' 1430 | . ',{"key":1},{"key":-1},{"key":42},{"key":{}},{"key":[]}]', 1431 | '[{"some":"some value"}]', 1432 | ], 1433 | [ // data set #206 - unknown consensus 1434 | 'parens_notation', 1435 | '$(key,more)', 1436 | '{"key":1,"some":2,"more":3}', 1437 | '', 1438 | ], 1439 | [ // data set #207 - unknown consensus 1440 | 'recursive_descent', 1441 | '$..', 1442 | '[{"a":{"b":"c"}},[0,1]]', 1443 | '', 1444 | ], 1445 | [ // data set #208 - unknown consensus 1446 | 'recursive_descent_after_dot_notation', 1447 | '$.key..', 1448 | '{"some key":"value","key":{"complex":"string","primitives":[0,1]}}', 1449 | '', 1450 | ], 1451 | [ // data set #209 1452 | 'root', 1453 | '$', 1454 | '{"key":"value","another key":{"complex":["a",1]}}', 1455 | '[{"another key":{"complex":["a",1]},"key":"value"}]', 1456 | ], 1457 | [ // data set #210 1458 | 'root_on_scalar', 1459 | '$', 1460 | '42', 1461 | '[42]', 1462 | ], 1463 | [ // data set #211 1464 | 'root_on_scalar_false', 1465 | '$', 1466 | 'false', 1467 | '[false]', 1468 | ], 1469 | [ // data set #212 1470 | 'root_on_scalar_true', 1471 | '$', 1472 | 'true', 1473 | '[true]', 1474 | ], 1475 | [ // data set #213 - unknown consensus 1476 | 'script_expression', 1477 | '$[(@.length-1)]', 1478 | '["first","second","third","forth","fifth"]', 1479 | '["fifth"]', 1480 | ], 1481 | [ // data set #214 1482 | 'union', 1483 | '$[0,1]', 1484 | '["first","second","third"]', 1485 | '["first","second"]', 1486 | ], 1487 | [ // data set #215 - unknown consensus, fallback to Proposal A 1488 | 'union_with_filter', 1489 | '$[?(@.key<3),?(@.key>6)]', 1490 | '[{"key":1},{"key":8},{"key":3},{"key":10},{"key":7},{"key":2},{"key":6},{"key":4}]', 1491 | '[{"key":1},{"key":2},{"key":8},{"key":10},{"key":7}]', 1492 | ], 1493 | [ // data set #216 1494 | 'union_with_keys', 1495 | '$[\'key\',\'another\']', 1496 | '{"key":"value","another":"entry"}', 1497 | '["value","entry"]', 1498 | ], 1499 | [ // data set #217 1500 | 'union_with_keys_after_array_slice', 1501 | '$[:][\'c\',\'d\']', 1502 | '[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","d":"dd2","e":"ee2"}]', 1503 | '["cc1","dd1","cc2","dd2"]', 1504 | ], 1505 | [ // data set #218 1506 | 'union_with_keys_after_bracket_notation', 1507 | '$[0][\'c\',\'d\']', 1508 | '[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","d":"dd2","e":"ee2"}]', 1509 | '["cc1","dd1"]', 1510 | ], 1511 | [ // data set #219 - unknown consensus, fallback to Proposal A 1512 | 'union_with_keys_after_dot_notation_with_wildcard', 1513 | '$.*[\'c\',\'d\']', 1514 | '[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","d":"dd2","e":"ee2"}]', 1515 | '["cc1","dd1","cc2","dd2"]', 1516 | ], 1517 | [ // data set #220 - unknown consensus, fallback to Proposal A 1518 | 'union_with_keys_after_recursive_descent', 1519 | '$..[\'c\',\'d\']', 1520 | '[{"c":"cc1","d":"dd1","e":"ee1"},{"c":"cc2","child":{"d":"dd2"}},{"c":"cc3"},{"d":"dd4"},{"child":{' 1521 | . '"c":"cc5"}}]', 1522 | '["cc1","cc2","cc3","cc5","dd1","dd2","dd4"]', 1523 | ], 1524 | [ // data set #221 1525 | 'union_with_keys_on_object_without_key', 1526 | '$[\'missing\',\'key\']', 1527 | '{"key":"value","another":"entry"}', 1528 | '["value"]', 1529 | ], 1530 | [ // data set #222 1531 | 'union_with_numbers_in_decreasing_order', 1532 | '$[4,1]', 1533 | '[1,2,3,4,5]', 1534 | '[5,2]', 1535 | ], 1536 | [ // data set #223 - unknown consensus, fallback to Proposal A 1537 | 'union_with_repeated_matches_after_dot_notation_with_wildcard', 1538 | '$.*[0,:5]', 1539 | '{"a":["string",null,true],"b":[false,"string",5.4]}', 1540 | '["string","string",null,true,false,false,"string",5.4]', 1541 | ], 1542 | [ // data set #224 - unknown consensus, fallback to Proposal A 1543 | 'union_with_slice_and_number', 1544 | '$[1:3,4]', 1545 | '[1,2,3,4,5]', 1546 | '[2,3,5]', 1547 | ], 1548 | [ // data set #225 1549 | 'union_with_spaces', 1550 | '$[ 0 , 1 ]', 1551 | '["first","second","third"]', 1552 | '["first","second"]', 1553 | ], 1554 | [ // data set #226 - unknown consensus, fallback to Proposal A 1555 | 'union_with_wildcard_and_number', 1556 | '$[*,1]', 1557 | '["first","second","third","forth","fifth"]', 1558 | '["first","second","third","forth","fifth","second"]', 1559 | ], 1560 | [ 1561 | 'rfc_semantics_of_null_object_value', 1562 | '$.a', 1563 | '{"a": null, "b": [null], "c": [{}], "null": 1}', 1564 | '[null]', 1565 | ], 1566 | [ 1567 | 'rfc_semantics_of_null_null_used_as_array', 1568 | '$.a[0]', 1569 | '{"a": null, "b": [null], "c": [{}], "null": 1}', 1570 | 'XFAIL', 1571 | ], 1572 | [ 1573 | 'rfc_semantics_of_null_null_used_as_object', 1574 | '$.a.d', 1575 | '{"a": null, "b": [null], "c": [{}], "null": 1}', 1576 | 'XFAIL', 1577 | ], 1578 | [ 1579 | 'rfc_semantics_of_null_array_value', 1580 | '$.b[0]', 1581 | '{"a": null, "b": [null], "c": [{}], "null": 1}', 1582 | '[null]', 1583 | ], 1584 | [ 1585 | 'rfc_semantics_of_null_array_value_2', 1586 | '$.b[*]', 1587 | '{"a": null, "b": [null], "c": [{}], "null": 1}', 1588 | '[null]', 1589 | ], 1590 | [ 1591 | 'rfc_semantics_of_null_existence', 1592 | '$.b[?@]', 1593 | '{"a": null, "b": [null], "c": [{}], "null": 1}', 1594 | '[null]', 1595 | ], 1596 | [ 1597 | 'rfc_semantics_of_null_comparison', 1598 | '$.b[?@==null]', 1599 | '{"a": null, "b": [null], "c": [{}], "null": 1}', 1600 | '[null]', 1601 | ], 1602 | [ 1603 | 'rfc_semantics_of_null_comparison_with_missing_value', 1604 | '$.c[?@.d==null]', 1605 | '{"a": null, "b": [null], "c": [{}], "null": 1}', 1606 | 'XFAIL', 1607 | ], 1608 | [ 1609 | 'rfc_semantics_of_null_null_string', 1610 | '$.null', 1611 | '{"a": null, "b": [null], "c": [{}], "null": 1}', 1612 | '[1]', 1613 | ], 1614 | ]; 1615 | } 1616 | } 1617 | -------------------------------------------------------------------------------- /tests/Traits/TestDataTrait.php: -------------------------------------------------------------------------------- 1 | getMessage()}"); 35 | } 36 | 37 | return $json; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/data/baselineFailedQueries.txt: -------------------------------------------------------------------------------- 1 | bracket_notation_with_quoted_closing_bracket_literal 2 | dot_notation_with_key_root_literal 3 | filter_expression_with_tautological_comparison 4 | union_with_wildcard_and_number 5 | array_slice_with_large_number_for_end_and_negative_step 6 | array_slice_with_large_number_for_start_end_negative_step 7 | array_slice_with_negative_step 8 | array_slice_with_negative_step_on_partially_overlapping_array 9 | bracket_notation_with_negative_number_on_short_array 10 | bracket_notation_with_number_on_object 11 | bracket_notation_with_quoted_escaped_backslash 12 | bracket_notation_with_quoted_escaped_single_quote 13 | bracket_notation_with_quoted_special_characters_combined 14 | bracket_notation_with_quoted_wildcard_literal_on_object_without_key 15 | bracket_notation_with_wildcard_after_recursive_descent 16 | dot_notation_with_number 17 | dot_notation_with_number_-1 18 | dot_notation_with_wildcard_after_recursive_descent 19 | filter_expression_with_bracket_notation_with_-1 20 | filter_expression_with_equals_with_root_reference 21 | # XFAIL 22 | rfc_semantics_of_null_null_used_as_array 23 | # XFAIL 24 | rfc_semantics_of_null_null_used_as_object 25 | rfc_semantics_of_null_existence 26 | rfc_semantics_of_null_comparison 27 | # XFAIL 28 | rfc_semantics_of_null_comparison_with_missing_value 29 | union_with_filter 30 | union_with_repeated_matches_after_dot_notation_with_wildcard 31 | union_with_slice_and_number -------------------------------------------------------------------------------- /tests/data/conferences.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Major League Baseball", 3 | "abbr": "MLB", 4 | "conferences": [ 5 | { 6 | "name": "Western Conference", 7 | "abbr": "West", 8 | "teams": [ 9 | { 10 | "name": "Dodger", 11 | "city": "Los Angeles", 12 | "whatever": "else", 13 | "players": [ 14 | { 15 | "name": "Bob Smith", 16 | "number": 22 17 | }, 18 | { 19 | "name": "Joe Face", 20 | "number": 23, 21 | "active": "yes" 22 | } 23 | ] 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "Eastern Conference", 29 | "abbr": "East", 30 | "teams": [ 31 | { 32 | "name": "Mets", 33 | "city": "New York", 34 | "whatever": "else", 35 | "players": [ 36 | { 37 | "name": "something", 38 | "number": 14, 39 | "active": "yes" 40 | }, 41 | { 42 | "name": "something", 43 | "number": 15 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /tests/data/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "books": [ 4 | { 5 | "category": "reference", 6 | "author": "Nigel Rees", 7 | "title": "Sayings of the Century", 8 | "price": 8.95 9 | }, 10 | { 11 | "category": "fiction", 12 | "author": "Evelyn Waugh", 13 | "title": "Sword of Honour", 14 | "price": 12.99 15 | }, 16 | { 17 | "category": "fiction", 18 | "author": "Herman Melville", 19 | "title": "Moby Dick", 20 | "isbn": "0-553-21311-3", 21 | "price": 8.99 22 | }, 23 | { 24 | "category": "fiction", 25 | "author": "J. R. R. Tolkien", 26 | "title": "The Lord of the Rings", 27 | "isbn": "0-395-19395-8", 28 | "price": 22.99 29 | } 30 | ], 31 | "bicycle": { 32 | "color": "red", 33 | "price": 19.95 34 | } 35 | }, 36 | "expensive": 10 37 | } 38 | -------------------------------------------------------------------------------- /tests/data/extra.json: -------------------------------------------------------------------------------- 1 | { 2 | "http://www.w3.org/2000/01/rdf-schema#label": [ 3 | { 4 | "@language": "en" 5 | }, 6 | { 7 | "@language": "de" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/data/indexed-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "store": { 3 | "books": { 4 | "4": { 5 | "category": "reference", 6 | "author": "Nigel Rees", 7 | "title": "Sayings of the Century", 8 | "price": 8.95 9 | }, 10 | "3": { 11 | "category": "fiction", 12 | "author": "Evelyn Waugh", 13 | "title": "Sword of Honour", 14 | "price": 12.99 15 | }, 16 | "2": { 17 | "category": "fiction", 18 | "author": "Herman Melville", 19 | "title": "Moby Dick", 20 | "isbn": "0-553-21311-3", 21 | "price": 8.99 22 | }, 23 | "1": { 24 | "category": "fiction", 25 | "author": "J. R. R. Tolkien", 26 | "title": "The Lord of the Rings", 27 | "isbn": "0-395-19395-8", 28 | "price": 22.99 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/data/locations.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gauteng", 3 | "type": "province", 4 | "child": { 5 | "name": "Johannesburg", 6 | "type": "city", 7 | "child": { 8 | "name": "Rosebank", 9 | "type": "suburb" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/data/numerical-indexes-array.json: -------------------------------------------------------------------------------- 1 | { 2 | "result": { 3 | "list": [ 4 | [ 5 | 1477526400, 6 | "11.51000" 7 | ], 8 | [ 9 | 1477612800, 10 | "11.49870" 11 | ] 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/data/numerical-indexes-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "result": { 3 | "list": [ 4 | { 5 | "time": 1477526400, 6 | "o": "11.51000" 7 | }, 8 | { 9 | "time": 1477612800, 10 | "o": "11.49870" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/data/simple-integers.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": [ 3 | { 4 | "name": "foo", 5 | "value": 1 6 | }, 7 | { 8 | "name": "bar", 9 | "value": 2 10 | }, 11 | { 12 | "name": "baz", 13 | "value": 1 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/data/with-dots.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "tokens": [ 4 | { 5 | "Employee.FirstName": "Jack" 6 | }, 7 | { 8 | "Employee.LastName": "Daniels" 9 | }, 10 | { 11 | "Employee.Email": "jd@example.com" 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/data/with-slashes.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": [ 3 | ], 4 | "mediatypes": { 5 | "image/png": "/core/img/filetypes/image.png", 6 | "image/jpeg": "/core/img/filetypes/image.png", 7 | "image/gif": "/core/img/filetypes/image.png", 8 | "application/postscript": "/core/img/filetypes/image-vector.png" 9 | } 10 | } 11 | --------------------------------------------------------------------------------