[
  {
    "path": ".gitattributes",
    "content": "*.php diff=php\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: softcreatr\r\ncustom: ['https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4']\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.md",
    "content": "---\r\nname: 🐛 Bug Report\r\nabout: Submit a bug report, to help us improve.\r\nlabels: \"bug\"\r\n---\r\n\r\n## 🐛 Bug Report\r\n\r\n(A clear and concise description of what the bug is)\r\n\r\n## Have you spent some time to check if this issue has been raised before?\r\n\r\n[ ] I have read googled for a similar issue or checked our older issues for a similar bug\r\n\r\n### Have you read the [Code of Conduct](https://github.com/SoftCreatR/JSONPath/blob/main/CODE_OF_CONDUCT.md)?\r\n\r\n[ ] I have read the Code of Conduct\r\n\r\n## To Reproduce\r\n\r\n(Write your steps here:)\r\n\r\n## Expected behavior\r\n\r\n<!--\r\n  How did you expect your project to behave?\r\n  It’s fine if you’re not sure your understanding is correct.\r\n  Write down what you thought would happen.\r\n-->\r\n\r\n(Write what you thought would happen.)\r\n\r\n## Actual Behavior\r\n\r\n<!--\r\n  Did something go wrong?\r\n  Is something broken, or not behaving as you expected?\r\n  Describe this section in detail, and attach screenshots if possible.\r\n  Don't only say \"it doesn't work\"!\r\n-->\r\n\r\n(Write what happened. Add screenshots, if applicable.)\r\n\r\n## Your Environment\r\n\r\n<!-- Include as many relevant details about the environment you experienced the bug in -->\r\n\r\n(Write Environment, Operating system and version etc.)\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.md",
    "content": "---\r\nname: 📚 Documentation\r\nabout: Report an issue related to documentation.\r\nlabels: \"documentation\"\r\n---\r\n\r\n## 📚 Documentation\r\n\r\n(A clear and concise description of what the issue is.)\r\n\r\n### Have you read the [Code of Conduct](https://github.com/SoftCreatR/JSONPath/blob/main/CODE_OF_CONDUCT.md)?\r\n\r\n[ ] I have read the Code of Conduct\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.md",
    "content": "---\r\nname: 💡 Feature / Idea\r\nabout: Submit a proposal for a new feature.\r\nlabels: \"feature\"\r\n---\r\n\r\n## 💡 Feature / Idea\r\n\r\n(A clear and concise description of what the feature is.)\r\n\r\n## Have you spent some time to check if this issue has been raised before?\r\n\r\n[ ] I have read googled for a similar issue or checked our older issues for a similar idea\r\n\r\n### Have you read the [Code of Conduct](https://github.com/SoftCreatR/JSONPath/blob/main/CODE_OF_CONDUCT.md)?\r\n\r\n[ ] I have read the Code of Conduct\r\n\r\n## Pitch\r\n\r\n(Please explain why this feature should be implemented and how it would be used. Add examples, if applicable.)\r\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\r\nThank you for sending the PR! We appreciate you spending the time to work on these changes.\r\n\r\nHelp us understand your motivation by explaining why you decided to make this change.\r\n\r\nHappy contributing!\r\n\r\n-->\r\n\r\n# 🔀 Pull Request\r\n\r\n## What does this PR do?\r\n\r\n(Provide a description of what this PR does.)\r\n\r\n## Test Plan\r\n\r\n(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)\r\n\r\n## Related PRs and Issues\r\n\r\n(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.)\r\n"
  },
  {
    "path": ".github/diff.json",
    "content": "{\n  \"problemMatcher\": [\n    {\n      \"owner\": \"diff\",\n      \"pattern\": [\n        {\n          \"regexp\": \"--- a/(.*)\",\n          \"file\": 1,\n          \"message\": 1\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/php-syntax.json",
    "content": "{\n  \"problemMatcher\": [\n    {\n      \"owner\": \"php -l\",\n      \"pattern\": [\n        {\n          \"regexp\": \"^\\\\s*(PHP\\\\s+)?([a-zA-Z\\\\s]+):\\\\s+(.*)\\\\s+in\\\\s+(\\\\S+)\\\\s+on\\\\s+line\\\\s+(\\\\d+)$\",\n          \"file\": 4,\n          \"line\": 5,\n          \"message\": 3\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/Test.yml",
    "content": "name: Test\n\non:\n  push:\n    paths:\n      - '**.php'\n      - 'composer.json'\n    branches:\n      - 'main'\n  pull_request:\n    paths:\n      - '**.php'\n      - 'composer.json'\n  workflow_dispatch:\n\njobs:\n  run:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        php:\n        - '8.5'\n    name: PHP ${{ matrix.php }} Test\n\n    steps:\n      - name: Git checkout\n        uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          extensions: dom, json, mbstring, xml, xmlwriter\n          tools: phpcs\n          coverage: pcov\n        env:\n          COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Setup problem matchers for PHP syntax check\n        run: echo \"::add-matcher::.github/php-syntax.json\"\n\n      - run: |\n          ! find . -type f -name '*.php' -exec php -l '{}' \\; 2>&1 |grep -v '^No syntax errors detected'\n\n      - name: Setup problem matchers for PHPUnit\n        run: echo \"::add-matcher::${{ runner.tool_cache }}/phpunit.json\"\n\n      - name: Install dependencies\n        run: composer install --prefer-dist --no-interaction --no-progress\n\n      - name: Run phpcs\n        run: composer cs\n\n      - name: Run PHPStan\n        run: composer phpstan\n\n      - name: Execute tests\n        run: |\n          set +e\n          output=$(composer test -- --coverage-clover=coverage.xml 2>&1)\n          exit_code=$?\n          echo \"$output\"\n          # If the only issue is the cache directory warning, ignore the exit code.\n          if echo \"$output\" | grep -q \"No cache directory configured, result of static analysis for code coverage will not be cached\"; then\n            echo \"Ignoring known PHPUnit warning about missing cache directory.\"\n            exit 0\n          else\n            exit $exit_code\n          fi\n\n      - name: Run codecov\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/UpdateContributors.yml",
    "content": "name: Update Contributors\n\non: [ push, workflow_dispatch]\n\njobs:\n  Update:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Git checkout\n        uses: actions/checkout@v4\n\n      - name: Update Contributors\n        uses: BobAnkh/add-contributors@master\n        with:\n          REPO_NAME: 'SoftCreatR/JSONPath'\n          CONTRIBUTOR: '## Contributors ✨'\n          ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/codestyle.yml",
    "content": "name: Code Style\n\non:\n  push:\n    paths:\n    - '**.php'\n  pull_request:\n    paths:\n    - '**.php'\n\njobs:\n  php:\n    name: PHP\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Setup PHP with tools\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: '8.5'\n        extensions: dom, json, mbstring, xml, xmlwriter\n        tools: cs2pr, phpcs, php-cs-fixer\n\n    - name: phpcs\n      run: phpcs -n -q --report=checkstyle | cs2pr\n\n    - name: php-cs-fixer\n      run: php-cs-fixer fix --dry-run --diff\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\r\n.phpunit.result.cache\r\nvendor\r\n.php_cs.cache\r\ncomposer.lock\r\ncomposer.phar\r\n.phpcs-cache\r\n"
  },
  {
    "path": ".php-cs-fixer.dist.php",
    "content": "<?php\n\nuse PhpCsFixer\\Config;\nuse PhpCsFixer\\Finder;\n\n$finder = Finder::create()\n    ->in(__DIR__)\n    ->notPath('vendor');\n\nreturn new Config()\n    ->setRiskyAllowed(true)\n    ->setRules([\n        '@PSR1' => true,\n        '@PSR2' => true,\n\n        'array_push' => true,\n        'backtick_to_shell_exec' => true,\n        'no_alias_language_construct_call' => true,\n        'no_mixed_echo_print' => true,\n        'pow_to_exponentiation' => true,\n        'random_api_migration' => true,\n\n        'array_syntax' => ['syntax' => 'short'],\n        'no_multiline_whitespace_around_double_arrow' => true,\n        'no_trailing_comma_in_singleline_array' => true,\n        'no_whitespace_before_comma_in_array' => true,\n        'normalize_index_brace' => true,\n        'whitespace_after_comma_in_array' => true,\n\n        'non_printable_character' => ['use_escape_sequences_in_strings' => true],\n\n        'lowercase_static_reference' => true,\n        'magic_constant_casing' => true,\n        'magic_method_casing' => true,\n        'native_function_casing' => true,\n        'native_function_type_declaration_casing' => true,\n\n        'cast_spaces' => ['space' => 'none'],\n        'lowercase_cast' => true,\n        'no_unset_cast' => true,\n        'short_scalar_cast' => true,\n\n        'class_attributes_separation' => true,\n        'no_blank_lines_after_class_opening' => true,\n        'no_null_property_initialization' => true,\n        'self_accessor' => true,\n        'single_class_element_per_statement' => true,\n        'single_trait_insert_per_statement' => true,\n\n        'no_empty_comment' => true,\n        'single_line_comment_style' => ['comment_types' => ['hash']],\n\n        'native_constant_invocation' => ['strict' => false],\n\n        'no_alternative_syntax' => true,\n        'no_trailing_comma_in_list_call' => true,\n        'no_unneeded_control_parentheses' => [\n            'statements' => [\n                'break',\n                'clone',\n                'continue',\n                'echo_print',\n                'return',\n                'switch_case',\n                'yield',\n                'yield_from',\n            ],\n        ],\n        'no_unneeded_curly_braces' => ['namespaces' => true],\n        'switch_continue_to_break' => true,\n        'trailing_comma_in_multiline' => ['elements' => ['arrays']],\n\n        'function_typehint_space' => true,\n        'lambda_not_used_import' => true,\n        'native_function_invocation' => ['include' => ['@internal']],\n        'no_unreachable_default_argument_value' => true,\n        'nullable_type_declaration_for_default_null_value' => true,\n        'return_type_declaration' => true,\n        'static_lambda' => true,\n\n        'fully_qualified_strict_types' => ['leading_backslash_in_global_namespace' => true],\n        'no_leading_import_slash' => true,\n        'no_unused_imports' => true,\n        'ordered_imports' => [\n            'imports_order' => ['class', 'function', 'const'],\n            'sort_algorithm' => 'alpha',\n        ],\n        'blank_line_between_import_groups' => true,\n\n        'declare_equal_normalize' => true,\n        'dir_constant' => true,\n        'explicit_indirect_variable' => true,\n        'function_to_constant' => true,\n        'is_null' => true,\n        'no_unset_on_property' => true,\n\n        'list_syntax' => ['syntax' => 'short'],\n\n        'clean_namespace' => true,\n        'no_leading_namespace_whitespace' => true,\n        'single_blank_line_before_namespace' => true,\n\n        'no_homoglyph_names' => true,\n\n        'binary_operator_spaces' => true,\n        'concat_space' => ['spacing' => 'one'],\n        'increment_style' => ['style' => 'post'],\n        'logical_operators' => true,\n        'object_operator_without_whitespace' => true,\n        'operator_linebreak' => true,\n        'standardize_increment' => true,\n        'standardize_not_equals' => true,\n        'ternary_operator_spaces' => true,\n        'ternary_to_elvis_operator' => true,\n        'ternary_to_null_coalescing' => true,\n        'unary_operator_spaces' => true,\n\n        'no_useless_return' => true,\n        'return_assignment' => true,\n\n        'multiline_whitespace_before_semicolons' => true,\n        'no_empty_statement' => true,\n        'no_singleline_whitespace_before_semicolons' => true,\n        'space_after_semicolon' => ['remove_in_empty_for_expressions' => true],\n\n        'escape_implicit_backslashes' => true,\n        'explicit_string_variable' => true,\n        'heredoc_to_nowdoc' => true,\n        'no_binary_string' => true,\n        'simple_to_complex_string_variable' => true,\n\n        'array_indentation' => true,\n        'blank_line_before_statement' => ['statements' => ['return', 'exit']],\n        'compact_nullable_typehint' => true,\n        'method_chaining_indentation' => true,\n        'no_extra_blank_lines' => [\n            'tokens' => [\n                'case',\n                'continue',\n                'curly_brace_block',\n                'default',\n                'extra',\n                'parenthesis_brace_block',\n                'square_brace_block',\n                'switch',\n                'throw',\n                'use',\n            ],\n        ],\n        'no_spaces_around_offset' => true,\n    ])\n    ->setFinder($finder);\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\r\n\r\n### 1.0.2\n- Fixed tokenizer handling for quoted bracket keys containing `$` so literals like `['[$the.size$]']` remain atomic and do not split into root tokens.\n\n### 1.0.1\n- Aligned the query runner and lexer with the JSONPath comparison suite: JSON documents are now decoded as objects to preserve `{}` vs `[]`, unsupported selectors no longer abort the runner, and dot-notation now accepts quoted keys with dots/spaces/leading `@`.\n- Hardened filter parsing: boolean-only filters (`?(true|false|null)`), literal short-circuiting (`&& false`, `|| true`), and empty filters now return the expected collections instead of throwing.\n- Slice filters gracefully skip non-countable objects.\n\r\n### 1.0.0\r\n- Rebuilt the test suite from scratch: removed bulky baseline fixtures and added compact unit/integration coverage for every filter (index, union, query, recursive, slice), lexer edge cases, and JSONPath core helpers. Runs reflection-free and deprecation-free.\r\n- Achieved and enforced 100% code coverage across AccessHelper, all filters, lexer, tokens, and JSONPath core while keeping phpstan and coding standards clean.\r\n- Added a lightweight manual query runner with curated examples to exercise selectors quickly without external datasets.\r\n- Major compatibility push toward the unofficial JSONPath standard: unions support slices/queries/wildcards, trailing commas parse correctly, negative indexes and bracket-escaped keys (quotes, brackets, wildcards, special chars) are honored, filters compare path-to-path and root references, equality/deep-equality/regex/in/nin semantics align with expectations, and null existence/value handling follows RFC behavior.\r\n- New feature highlights from this cycle:\r\n  - Multi-key unions with and without quotes: `$[name,year]` and `$[\"name\",\"year\"]`.\r\n  - Robust bracket notation for special/escaped keys, including `']'`, `'*'`, `$`, backslashes, and mixed punctuation.\r\n  - Trailing comma support in unions/slices (e.g. `$..books[0,1,2,]`).\r\n  - Negative index handling aligned with spec (short arrays return empty; -1 works where valid).\r\n  - Filter improvements: path-to-path/root comparisons, deep equality across scalars/objects/arrays/null/empties, regex matching, `in`/`nin`/`!in`, tautological expressions, and `?@` existence behavior per RFC.\r\n  - Unions combining slices/queries/wildcards now return complete results (e.g. `$[1:3,4]`, `$[*,1]`).\r\n\r\n### 0.11.0\r\n🔻 Breaking changes ahead:\r\n\r\n- Dropped support for PHP < 8.5\r\n- `JSONPathToken` now uses a `TokenType` enum and the constructor signature changed accordingly.\r\n- `JSONPath` options flag is now an `int` bitmask (was `bool`), requiring callers to pass integer flags.\r\n- `SliceFilter` returns an empty result for non-positive step values (previously iterated indefinitely).\r\n- `QueryResultFilter` now throws a `JSONPathException` for unsupported operators instead of silently proceeding.\r\n- Access helper behavior is stricter: `arrayValues` throws on invalid types; ArrayAccess lookups check `offsetExists` before reading; traversables and objects are handled distinctly.\r\n- Adopted PHP 8.5 features: `TokenType` enum, readonly value object for tokens, typed flags/options, and `#[\\Override]` usage.\r\n- CI now runs on PHP 8.5 with required extensions; code style workflow updated accordingly.\r\n- Added coverage for AccessHelper edge cases (magic getters, ArrayAccess, traversables, negative indexes), QueryResultFilter arithmetic branches, and SliceFilter negative/null bounds.\r\n- Fixed empty-expression handling in lexer and improved safety in AccessHelper traversable lookups.\r\n- Added PHPStan static analysis to the toolchain and addressed its findings.\r\n\r\n### 0.10.1\r\n- Fixed ignore whitespace after comparison value in filter expression\r\n\r\n### 0.10.0\r\n- Fixed query/selector Filter Expression With Current Object\r\n- Fixed query/selector Filter Expression With Different Grouped Operators\r\n- Fixed query/selector Filter Expression With equals_on_array_of_numbers\r\n- Fixed query/selector Filter Expression With Negation and Equals\r\n- Fixed query/selector Filter Expression With Negation and Less Than\r\n- Fixed query/selector Filter Expression Without Value\r\n- Fixed query/selector Filter Expression With Boolean AND Operator (#42)\r\n- Fixed query/selector Filter Expression With Boolean OR Operator (#43)\r\n- Fixed query/selector Filter Expression With Equals (#45)\r\n- Fixed query/selector Filter Expression With Equals false (#46)\r\n- Fixed query/selector Filter Expression With Equals null (#47)\r\n- Fixed query/selector Filter Expression With Equals Number With Fraction (#48)\r\n- Fixed query/selector Filter Expression With Equals true (#50)\r\n- Fixed query/selector Filter Expression With Greater Than (#52)\r\n- Fixed query/selector Filter Expression With Greater Than or Equal (#53)\r\n- Fixed query/selector Filter Expression With Less Than (#54)\r\n- Fixed query/selector Filter Expression With Less Than or Equal (#55)\r\n- Fixed query/selector Filter Expression With Not Equals (#56)\r\n- Fixed query/selector Filter Expression With Value (#57)\r\n- Fixed query/selector script_expression (Expected test result corrected)\r\n- Added additional NULL related query tests from JSONPath RFC\r\n\r\n### 0.9.0\r\n🔻 Breaking changes ahead:\r\n\r\n- Dropped support for PHP < 8.1\r\n\r\n### 0.8.3\r\n- Change `getData()` so that it can be mixed instead of array\r\n\r\n### 0.8.2\r\n- AccessHelper & RecursiveFilter now return a plain `object`, rather than an `ArrayAccess` object\r\n\r\n### 0.8.1\r\n- Removed strict_types\r\n- Applied some PSR-12 related changes\r\n- Small code optimizations\r\n\r\n### 0.8.0\r\n🔻 Breaking changes ahead:\r\n\r\n - Dropped support for PHP < 8.0\r\n - Removed deprecated method `JSONPath->data()`\r\n\r\n### 0.7.5\r\n - Added support for $.length\r\n - Added trim to explode to support both 1,2,3 and 1, 2, 3 inputs\r\n - Dropped in_array strict equality check to be in line with the other standard equality checks such as (== and !=)\r\n\r\n### 0.7.4\r\n - Removed PHPUnit from conflicting packages\r\n\r\n### 0.7.3\r\n - Fixed PHP 7.4+ compatibility issues\r\n\r\n### 0.7.2\r\n - Fixed query/selector \"Array Slice With Start Large Negative Number And Open End On Short Array\" (#7)\r\n - Fixed query/selector \"Union With Keys\" (#22)\r\n - Fixed query/selector \"Dot Notation After Union With Keys\" (#15)\r\n - Fixed query/selector \"Union With Keys After Array Slice\" (#23)\r\n - Fixed query/selector \"Union With Keys After Bracket Notation\" (#24)\r\n - Fixed query/selector \"Union With Keys On Object Without Key\" (#25)\r\n\r\n### 0.7.1\r\n - Fixed issues with empty tokens (`['']` and `[\"\"]`)\r\n - Fixed TypeError in AccessHelper::keyExists \r\n - Improved QueryTest\r\n\r\n### 0.7.0\r\n🔻 Breaking changes ahead:\r\n\r\n - Made JSONPath::__construct final\r\n - Added missing type hints\r\n - Partially reduced complexity\r\n - Performed some code optimizations\r\n - Updated composer.json for proper PHPUnit/PHP usage\r\n - Added support for regular expression operator (`=~`)\r\n - Added QueryTest to perform tests against all queries from https://cburgmer.github.io/json-path-comparison/\r\n - Switched Code Style from PSR-2 to PSR-12\r\n\r\n### 0.6.4\r\n - Removed unnecessary type casting, that caused problems under certain circumstances\r\n - Added support for `nin` operator\r\n - Added support for greater than or equal operator (`>=`)\r\n - Added support for less or equal operator (`<=`)\r\n\r\n### 0.6.3\r\n - Added support for `in` operator\r\n - Fixed evaluation on indexed object\r\n\r\n### 0.6.x\r\n - Dropped support for PHP < 7.1\r\n - Switched from (broken) PSR-0 to PSR-4\r\n - Updated PHPUnit to 8.5 / 9.4\r\n - Updated tests\r\n - Added missing PHPDoc blocks\r\n - Added return type hints\r\n - Moved from Travis to GitHub actions\r\n - Set `strict_types=1`\r\n\r\n### 0.5.0\r\n - Fixed the slice notation (e.g. [0:2:5] etc.). **Breaks code relying on the broken implementation**\r\n\r\n### 0.3.0\r\n - Added JSONPathToken class as value object\r\n - Lexer clean up and refactor\r\n - Updated the lexing and filtering of the recursive token (\"..\") to allow for a combination of recursion\r\n   and filters, e.g. $..[?(@.type == 'suburb')].name\r\n\r\n### 0.2.1 - 0.2.5\r\n - Various bug fixes and clean up\r\n\r\n### 0.2.0\r\n - Added a heap of array access features for more creative iterating and chaining possibilities\r\n\r\n### 0.1.x\r\n - Init\r\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\r\n\r\n## Our Pledge\r\n\r\nIn the interest of fostering an open and welcoming environment, we as\r\ncontributors and maintainers pledge to make participation in our project and\r\nour community a harassment-free experience for everyone, regardless of age, body\r\nsize, disability, ethnicity, sex characteristics, gender identity, expression,\r\nlevel of experience, education, socio-economic status, nationality, personal\r\nappearance, race, religion, or sexual identity and orientation.\r\n\r\n## Our Standards\r\n\r\nExamples of behavior that contributes to creating a positive environment\r\ninclude:\r\n\r\n* Using welcoming and inclusive language\r\n* Being respectful of differing viewpoints and experiences\r\n* Gracefully accepting constructive criticism\r\n* Focusing on what is best for the community\r\n* Showing empathy towards other community members\r\n\r\nExamples of unacceptable behavior by participants include:\r\n\r\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\r\n* Trolling, insulting/derogatory comments, and personal or political attacks\r\n* Public or private harassment\r\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\r\n* Other conduct which could reasonably be considered inappropriate in a professional setting\r\n\r\n## Our Responsibilities\r\n\r\nProject maintainers are responsible for clarifying the standards of acceptable\r\nbehavior and are expected to take appropriate and fair corrective action in\r\nresponse to any instances of unacceptable behavior.\r\n\r\nProject maintainers have the right and responsibility to remove, edit, or\r\nreject comments, commits, code, wiki edits, issues, and other contributions\r\nthat are not aligned to this Code of Conduct, or to ban temporarily or\r\npermanently any contributor for other behaviors that they deem inappropriate,\r\nthreatening, offensive, or harmful.\r\n\r\n## Scope\r\n\r\nThis Code of Conduct applies both within project spaces and in public spaces\r\nwhen an individual is representing the project or its community. Examples of\r\nrepresenting a project or community include using an official project e-mail\r\naddress, posting via an official social media account, or acting as an appointed\r\nrepresentative at an online or offline event. Representation of a project may be\r\nfurther defined and clarified by project maintainers.\r\n\r\n## Enforcement\r\n\r\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\r\nreported by contacting the project team at hello@1-2.dev. All\r\ncomplaints will be reviewed and investigated and will result in a response that\r\nis deemed necessary and appropriate to the circumstances. The project team is\r\nobligated to maintain confidentiality with regard to the reporter of an incident.\r\nFurther details of specific enforcement policies may be posted separately.\r\n\r\nProject maintainers who do not follow or enforce the Code of Conduct in good\r\nfaith may face temporary or permanent repercussions as determined by other\r\nmembers of the project's leadership.\r\n\r\n## Attribution\r\n\r\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\r\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\r\n\r\n[homepage]: https://www.contributor-covenant.org\r\n\r\nFor answers to common questions about this code of conduct, see\r\nhttps://www.contributor-covenant.org/faq\r\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\r\n\r\nOriginal work - Copyright (c) 2018 Flow Communications\r\nModified work - Copyright (c) 2020 Sascha Greuel <hello@1-2.dev>\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining a copy\r\nof this software and associated documentation files (the \"Software\"), to deal\r\nin the Software without restriction, including without limitation the rights\r\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\ncopies of the Software, and to permit persons to whom the Software is\r\nfurnished to do so, subject to the following conditions:\r\n\r\nThe above copyright notice and this permission notice shall be included in all\r\ncopies or substantial portions of the Software.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r\nSOFTWARE.\r\n"
  },
  {
    "path": "README.md",
    "content": "# JSONPath for PHP 8.5+\n\n[![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)\n[![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)\n[![Codecov branch](https://img.shields.io/codecov/c/github/SoftCreatR/JSONPath)](https://codecov.io/gh/SoftCreatR/JSONPath)\n\nThis is a [JSONPath](http://goessner.net/articles/JsonPath/) implementation for PHP that targets the de facto comparison suite/RFC semantics while keeping the API small, cached, and `eval`-free.\n\n## Highlights\n\n- PHP 8.5+ only, with enums/readonly tokens and no `eval`.\n- Works with arrays, objects, and `ArrayAccess`/traversables in any combination.\n- Unions cover slices/queries/wildcards/multi-key strings (quoted or unquoted); negative indexes and escaped bracket notation are supported.\n- Filters support path-to-path/root comparisons, regex, `in`/`nin`/`!in`, deep equality, RFC-style null existence/value handling, and literal-only short-circuiting (e.g., `?(true)`, `?(false)`, `&& false`, `|| true`).\n- Tokenized parsing with internal caching; lightweight manual runner to try bundled examples quickly.\n\n## Installation\n\nRequires PHP 8.5 or newer.\n\n```bash\ncomposer require softcreatr/jsonpath:\"^1.0\"\n```\n\n## Development\n\nUseful commands:\n\n```bash\ncomposer exec phpunit\ncomposer phpstan\ncomposer cs\n```\n\n## JSONPath Examples\n\nJSONPath                  | Result\n--------------------------|-------------------------------------\n`$.store.books[*].author` | the authors of all books in the store\n`$..author`               | all authors\n`$.store..price`          | the price of everything in the store.\n`$..books[2]`             | the third book\n`$..books[(@.length-1)]`  | the last book in order.\n`$..books[-1:]`           | the last book in order.\n`$..books[0,1]`           | the first two books\n`$..books[title,year]`    | multiple keys in a union\n`$..books[:2]`            | the first two books\n`$..books[::2]`           | every second book starting from first one\n`$..books[1:6:3]`         | every third book starting from 1 till 6\n`$..books[?(@.isbn)]`     | filter all books with isbn number\n`$..books[?(@.price<10)]` | filter all books cheaper than 10\n`$..books.length`         | the amount of books\n`$..*`                    | all elements in the data (recursively extracted)\n\n\nExpression syntax\n---\n\nSymbol                | Description\n----------------------|-------------------------\n`$`                   | The root object/element (not strictly necessary)\n`@`                   | The current object/element\n`.` or `[]`           | Child operator\n`..`                  | Recursive descent\n`*`                   | Wildcard. All child elements regardless their index.\n`[,]`                 | Array indices as a set\n`[start:end:step]`    | Array slice operator borrowed from ES4/Python.\n`?()`                 | Filters a result set by a comparison expression (constant expressions like `?(true)`/`?(false)` are allowed; unsupported/empty filters evaluate to an empty result)\n`()`                  | Uses the result of a comparison expression as the index\n\n## PHP Usage\n\n#### Using arrays\n\n```php\n<?php\nrequire_once __DIR__ . '/vendor/autoload.php';\n\n$data = ['people' => [\n    ['name' => 'Sascha'],\n    ['name' => 'Bianca'],\n    ['name' => 'Alexander'],\n    ['name' => 'Maximilian'],\n]];\n\nprint_r((new \\Flow\\JSONPath\\JSONPath($data))->find('$.people.*.name')->getData());\n\n/*\nArray\n(\n    [0] => Sascha\n    [1] => Bianca\n    [2] => Alexander\n    [3] => Maximilian\n)\n*/\n```\n\n#### Using objects\n\n```php\n<?php\nrequire_once __DIR__ . '/vendor/autoload.php';\n\n$data = json_decode('{\"name\":\"Sascha Greuel\",\"birthdate\":\"1987-12-16\",\"city\":\"Gladbeck\",\"country\":\"Germany\"}', false);\n\nprint_r((new \\Flow\\JSONPath\\JSONPath($data))->find('$')->getData()[0]);\n\n/*\nstdClass Object\n(\n    [name] => Sascha Greuel\n    [birthdate] => 1987-12-16\n    [city] => Gladbeck\n    [country] => Germany\n)\n*/\n```\n\n### Magic method access\n\nThe options flag `JSONPath::ALLOW_MAGIC` will instruct JSONPath when retrieving a value to first check if an object\nhas a magic `__get()` method and will call this method if available. This feature is *iffy* and\nnot very predictable as:\n\n-  wildcard and recursive features will only look at public properties and can't smell which properties are magically accessible\n-  there is no `property_exists` check for magic methods so an object with a magic `__get()` will always return `true` when checking\n   if the property exists\n-   any errors thrown or unpredictable behavior caused by fetching via `__get()` is your own problem to deal with\n\n```php\n<?php\n\nuse Flow\\JSONPath\\JSONPath;\n\n$myObject = (new Foo())->get('bar');\n$jsonPath = new JSONPath($myObject, JSONPath::ALLOW_MAGIC);\n```\n\n## Script expressions\n\nScript execution is intentionally **not** supported:\n\n- It would require `eval`, which we avoid.\n- Behavior would diverge across languages and defeat having a portable expression syntax.\n\nSupported filter/query patterns (200+ cases covered in the comparison suite):\n\n```\n[?(@._KEY_ _OPERATOR_ _VALUE_)]\n  Operators: ==, =, !=, <>, !==, <, >, <=, >=, =~, in, nin, !in\n\nExamples:\n[?(@.title == \"A string\")]      // equality\n[?(@.title = \"A string\")]       // SQL-style equals\n[?(@.price < 10)]               // numeric comparisons\n[?(@.title =~ /^a(nother)?/i)]  // regex\n[?(@.title in [\"A\",\"B\"])]       // membership\n[?(@.title nin [\"A\"])]          // not in\n[?(@.title !in [\"A\"])]          // alternate not in\n[?(@.key == @.other)]           // path-to-path comparison\n[?(@.key == $.rootValue)]       // root reference\n[?(@)] or [?(@==@)]             // truthy/tautology\n[?(@.length)]                   // existence checks\n[?(@['weird-key']==\"ok\")]       // bracket-escaped keys and negative indexes\n```\n\nA full list of (un)supported filter/query patterns can be found in the [JSONPath Comparison Cheatsheet](https://cburgmer.github.io/json-path-comparison/).\n\t\n## Similar projects\n\n[FlowCommunications/JSONPath](https://github.com/FlowCommunications/JSONPath) is the predecessor of this library by Stephen Frank\n\nOther / Similar implementations can be found in the [Wiki](https://github.com/SoftCreatR/JSONPath/wiki/Other-Implementations).\n\n## Changelog\n\nA list of changes can be found in the [CHANGELOG.md](CHANGELOG.md) file. \n\n## License 🌳\n\n[MIT](LICENSE.md) © [1-2.dev](https://1-2.dev)\n\nThis 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.\n\n## Contributors ✨\n\n<table>\n<tr>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/SoftCreatR>\n            <img src=https://avatars.githubusercontent.com/u/81188?v=4 width=\"100;\"  alt=Sascha Greuel/>\n            <br />\n            <sub style=\"font-size:14px\"><b>Sascha Greuel</b></sub>\n        </a>\n    </td>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/lucasnetau>\n            <img src=https://avatars.githubusercontent.com/u/9331242?v=4 width=\"100;\"  alt=James Lucas/>\n            <br />\n            <sub style=\"font-size:14px\"><b>James Lucas</b></sub>\n        </a>\n    </td>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/Schrank>\n            <img src=https://avatars.githubusercontent.com/u/379680?v=4 width=\"100;\"  alt=Fabian Blechschmidt/>\n            <br />\n            <sub style=\"font-size:14px\"><b>Fabian Blechschmidt</b></sub>\n        </a>\n    </td>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/mpesari>\n            <img src=https://avatars.githubusercontent.com/u/11061725?v=4 width=\"100;\"  alt=Mikko Pesari/>\n            <br />\n            <sub style=\"font-size:14px\"><b>Mikko Pesari</b></sub>\n        </a>\n    </td>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/warlof>\n            <img src=https://avatars.githubusercontent.com/u/648753?v=4 width=\"100;\"  alt=warlof/>\n            <br />\n            <sub style=\"font-size:14px\"><b>warlof</b></sub>\n        </a>\n    </td>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/SG5>\n            <img src=https://avatars.githubusercontent.com/u/3931761?v=4 width=\"100;\"  alt=Sergey G/>\n            <br />\n            <sub style=\"font-size:14px\"><b>Sergey G</b></sub>\n        </a>\n    </td>\n</tr>\n<tr>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/drealecs>\n            <img src=https://avatars.githubusercontent.com/u/209984?v=4 width=\"100;\"  alt=Alexandru Pătrănescu/>\n            <br />\n            <sub style=\"font-size:14px\"><b>Alexandru Pătrănescu</b></sub>\n        </a>\n    </td>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/oleg-andreyev>\n            <img src=https://avatars.githubusercontent.com/u/1244112?v=4 width=\"100;\"  alt=Oleg Andreyev/>\n            <br />\n            <sub style=\"font-size:14px\"><b>Oleg Andreyev</b></sub>\n        </a>\n    </td>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/rcjsuen>\n            <img src=https://avatars.githubusercontent.com/u/15629116?v=4 width=\"100;\"  alt=Remy Suen/>\n            <br />\n            <sub style=\"font-size:14px\"><b>Remy Suen</b></sub>\n        </a>\n    </td>\n    <td align=\"center\" style=\"word-wrap: break-word; width: 150.0; height: 150.0\">\n        <a href=https://github.com/esomething>\n            <img src=https://avatars.githubusercontent.com/u/64032?v=4 width=\"100;\"  alt=esomething/>\n            <br />\n            <sub style=\"font-size:14px\"><b>esomething</b></sub>\n        </a>\n    </td>\n</tr>\n</table>\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"softcreatr/jsonpath\",\n  \"description\": \"JSONPath implementation for parsing, searching and flattening arrays\",\n  \"license\": \"MIT\",\n  \"version\": \"1.0.2\",\n  \"authors\": [\n    {\n      \"name\": \"Stephen Frank\",\n      \"email\": \"stephen@flowsa.com\",\n      \"homepage\": \"https://prismaticbytes.com\",\n      \"role\": \"Developer\"\n    },\n    {\n      \"name\": \"Sascha Greuel\",\n      \"email\": \"hello@1-2.dev\",\n      \"homepage\": \"https://1-2.dev\",\n      \"role\": \"Developer\"\n    }\n  ],\n  \"support\": {\n    \"email\": \"hello@1-2.dev\",\n    \"issues\": \"https://github.com/SoftCreatR/JSONPath/issues\",\n    \"forum\": \"https://github.com/SoftCreatR/JSONPath/discussions\",\n    \"source\": \"https://github.com/SoftCreatR/JSONPath\"\n  },\n  \"require\": {\n    \"php\": \"^8.5\",\n    \"ext-json\": \"*\"\n  },\n  \"require-dev\": {\n    \"friendsofphp/php-cs-fixer\": \"^3.92\",\n    \"phpstan/phpstan\": \"^2.1\",\n    \"phpunit/phpunit\": \"^12\",\n    \"squizlabs/php_codesniffer\": \"^4.0\"\n  },\n  \"replace\": {\n    \"flow/jsonpath\": \"*\"\n  },\n  \"minimum-stability\": \"stable\",\n  \"autoload\": {\n    \"psr-4\": {\n      \"Flow\\\\JSONPath\\\\\": \"src/\"\n    }\n  },\n  \"autoload-dev\": {\n    \"psr-4\": {\n      \"Flow\\\\JSONPath\\\\Test\\\\\": \"tests/\"\n    }\n  },\n  \"config\": {\n    \"optimize-autoloader\": true,\n    \"preferred-install\": \"dist\"\n  },\n  \"scripts\": {\n    \"cs\": \"phpcs\",\n    \"cs-fix\": \"php-cs-fixer fix --config=.php-cs-fixer.dist.php\",\n    \"phpstan\": \"phpstan analyse --no-progress -c phpstan.neon.dist\",\n    \"test\": \"phpunit\"\n  }\n}\n"
  },
  {
    "path": "phpcs.xml",
    "content": "<?xml version=\"1.0\"?>\n<ruleset\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:noNamespaceSchemaLocation=\"https://raw.githubusercontent.com/squizlabs/PHP_CodeSniffer/master/phpcs.xsd\"\n>\n    <file>src/</file>\n    <file>tests/</file>\n    <arg name=\"extensions\" value=\"php\" />\n    <arg value=\"p\"/>\n    <arg name=\"basepath\" value=\".\"/>\n\n    <rule ref=\"PSR12\">\n        <!-- https://github.com/squizlabs/PHP_CodeSniffer/issues/3200 -->\n        <exclude name=\"PSR12.Classes.AnonClassDeclaration.SpaceAfterKeyword\"/>\n\n        <!-- We have a large number of comments between the closing brace of an `if` and the `else`. -->\n        <exclude name=\"Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace\"/>\n    </rule>\n</ruleset>\n"
  },
  {
    "path": "phpstan.neon.dist",
    "content": "parameters:\n    treatPhpDocTypesAsCertain: false\n    level: 6\n    paths:\n        - src\n        - tests\n    tmpDir: .phpstan-cache\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"./vendor/phpunit/phpunit/phpunit.xsd\"\n         bootstrap=\"vendor/autoload.php\"\n         colors=\"true\"\n>\n  <testsuites>\n    <testsuite name=\"Unit\">\n      <directory>./tests</directory>\n    </testsuite>\n  </testsuites>\n\n  <source>\n    <include>\n      <directory>./src/</directory>\n    </include>\n  </source>\n</phpunit>\n"
  },
  {
    "path": "src/AccessHelper.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath;\n\nuse ArrayAccess;\nuse Traversable;\n\nclass AccessHelper\n{\n    /**\n     * @return array<int, int|string>\n     */\n    public static function collectionKeys(mixed $collection): array\n    {\n        if (\\is_object($collection)) {\n            return \\array_keys(\\get_object_vars($collection));\n        }\n\n        return \\array_keys($collection);\n    }\n\n    public static function isCollectionType(mixed $collection): bool\n    {\n        return \\is_array($collection) || \\is_object($collection);\n    }\n\n    public static function keyExists(mixed $collection, int|string|null $key, bool $magicIsAllowed = false): bool\n    {\n        if ($magicIsAllowed && \\is_object($collection) && \\method_exists($collection, '__get')) {\n            return true;\n        }\n\n        if (\\is_array($collection)) {\n            if (\\is_int($key) && $key < 0) {\n                $keys = \\array_keys($collection);\n                $index = \\count($keys) + $key;\n\n                return $index >= 0 && \\array_key_exists($index, $keys);\n            }\n\n            return \\array_key_exists($key ?? '', $collection);\n        }\n\n        if ($collection instanceof ArrayAccess) {\n            return $collection->offsetExists($key);\n        }\n\n        if (\\is_object($collection)) {\n            return \\property_exists($collection, (string)$key);\n        }\n\n        return false;\n    }\n\n    /**\n     * @todo Optimize conditions\n     */\n    public static function getValue(mixed $collection, int|string|null $key, bool $magicIsAllowed = false): mixed\n    {\n        if (\n            $magicIsAllowed\n            && \\is_object($collection)\n            && !$collection instanceof ArrayAccess && \\method_exists($collection, '__get')\n        ) {\n            $return = $collection->__get($key);\n        } elseif (\\is_int($key) && $collection instanceof Traversable && !$collection instanceof ArrayAccess) {\n            $return = self::getValueByIndex($collection, $key);\n        } elseif (\\is_object($collection) && !$collection instanceof ArrayAccess) {\n            $return = $collection->{$key};\n        } elseif ($collection instanceof ArrayAccess) {\n            $return = $collection->offsetExists($key) ? $collection->offsetGet($key) : null;\n        } elseif (\\is_array($collection)) {\n            if (\\is_int($key) && $key < 0) {\n                $index = \\count($collection) + $key;\n                $return = $index >= 0 && \\array_key_exists($index, $collection) ? $collection[$index] : null;\n            } else {\n                $return = $collection[$key] ?? null;\n            }\n        } else {\n            $return = null;\n        }\n\n        return $return;\n    }\n\n    /**\n     * Find item in php collection by index\n     * Written this way to handle instances ArrayAccess or Traversable objects\n     */\n    private static function getValueByIndex(mixed $collection, int $key): mixed\n    {\n        $i = 0;\n\n        foreach ($collection as $val) {\n            if ($i === $key) {\n                return $val;\n            }\n\n            $i++;\n        }\n\n        if ($key < 0) {\n            $total = $i;\n            $i = 0;\n\n            foreach ($collection as $val) {\n                if ($i - $total === $key) {\n                    return $val;\n                }\n\n                $i++;\n            }\n        }\n\n        return null;\n    }\n\n    public static function setValue(mixed &$collection, int|string|null $key, mixed $value): mixed\n    {\n        if (\\is_object($collection) && !$collection instanceof ArrayAccess) {\n            $collection->{$key} = $value;\n\n            return $value;\n        }\n\n        if ($collection instanceof ArrayAccess) {\n            $collection->offsetSet($key, $value);\n\n            return $value;\n        }\n\n        $collection[$key] = $value;\n\n        return $value;\n    }\n\n    public static function unsetValue(mixed &$collection, int|string|null $key): void\n    {\n        if (\\is_object($collection) && !$collection instanceof ArrayAccess) {\n            unset($collection->{$key});\n        }\n\n        if ($collection instanceof ArrayAccess) {\n            $collection->offsetUnset($key);\n        }\n\n        if (\\is_array($collection)) {\n            unset($collection[$key]);\n        }\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    /**\n     * @return array<int, mixed>\n     * @throws JSONPathException\n     */\n    public static function arrayValues(mixed $collection): array\n    {\n        if (\\is_array($collection)) {\n            return \\array_values($collection);\n        }\n\n        if (\\is_object($collection)) {\n            return \\array_values((array)$collection);\n        }\n\n        throw new JSONPathException('Invalid variable type for arrayValues');\n    }\n}\n"
  },
  {
    "path": "src/Filters/AbstractFilter.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Filters;\n\nuse Flow\\JSONPath\\JSONPath;\nuse Flow\\JSONPath\\JSONPathToken;\n\nabstract class AbstractFilter\n{\n    protected bool $magicIsAllowed;\n\n    protected mixed $rootData = null;\n\n    public function __construct(protected JSONPathToken $token, int $options = 0)\n    {\n        $this->magicIsAllowed = ($options & JSONPath::ALLOW_MAGIC) === JSONPath::ALLOW_MAGIC;\n    }\n\n    public function setRootData(mixed $root): void\n    {\n        $this->rootData = $root;\n    }\n\n    /**\n     * @param array<array-key, mixed>|object $collection\n     * @return array<array-key, mixed>\n     */\n    abstract public function filter(array|object $collection): array;\n}\n"
  },
  {
    "path": "src/Filters/IndexFilter.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Filters;\n\nuse Flow\\JSONPath\\AccessHelper;\nuse Flow\\JSONPath\\JSONPathException;\n\nclass IndexFilter extends AbstractFilter\n{\n    /**\n     * @throws JSONPathException\n     * @inheritDoc\n     */\n    public function filter(array|object $collection): array\n    {\n        if (\\is_array($this->token->value)) {\n            $result = [];\n\n            foreach ($this->token->value as $value) {\n                if (AccessHelper::keyExists($collection, $value, $this->magicIsAllowed)) {\n                    $result[] = AccessHelper::getValue($collection, $value, $this->magicIsAllowed);\n                }\n            }\n\n            return $result;\n        }\n\n        if (AccessHelper::keyExists($collection, $this->token->value, $this->magicIsAllowed)) {\n            return [\n                AccessHelper::getValue($collection, $this->token->value, $this->magicIsAllowed),\n            ];\n        }\n\n        if ($this->token->value === '*' && !$this->token->quoted) {\n            return AccessHelper::arrayValues($collection);\n        }\n\n        if ($this->token->value === 'length') {\n            return [\n                \\count($collection),\n            ];\n        }\n\n        return [];\n    }\n}\n"
  },
  {
    "path": "src/Filters/IndexesFilter.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Filters;\n\nuse Flow\\JSONPath\\AccessHelper;\nuse Flow\\JSONPath\\JSONPath;\nuse Flow\\JSONPath\\JSONPathException;\nuse Flow\\JSONPath\\JSONPathToken;\nuse Flow\\JSONPath\\TokenType;\n\nclass IndexesFilter extends AbstractFilter\n{\n    /**\n     * @inheritDoc\n     *\n     * @throws JSONPathException\n     */\n    public function filter(array|object $collection): array\n    {\n        $return = [];\n\n        foreach ($this->token->value as $index) {\n            if (\\is_array($index) && ($index['type'] ?? null) === 'slice') {\n                $sliceToken = new JSONPathToken(TokenType::Slice, $index['value']);\n                $sliceFilter = new SliceFilter(\n                    $sliceToken,\n                    $this->magicIsAllowed ? JSONPath::ALLOW_MAGIC : 0\n                );\n                $sliceFilter->setRootData($this->rootData ?? $collection);\n\n                $return = \\array_merge($return, $sliceFilter->filter($collection));\n\n                continue;\n            }\n\n            if (\\is_array($index) && ($index['type'] ?? null) === 'query') {\n                $queryToken = new JSONPathToken(TokenType::QueryMatch, $index['value']);\n                $queryFilter = new QueryMatchFilter(\n                    $queryToken,\n                    $this->magicIsAllowed ? JSONPath::ALLOW_MAGIC : 0\n                );\n                $queryFilter->setRootData($this->rootData ?? $collection);\n\n                $return = \\array_merge($return, $queryFilter->filter($collection));\n\n                continue;\n            }\n\n            if ($index === '*' && !$this->token->quoted) {\n                $return = \\array_merge($return, AccessHelper::arrayValues($collection));\n\n                continue;\n            }\n\n            if (AccessHelper::keyExists($collection, $index, $this->magicIsAllowed)) {\n                $return[] = AccessHelper::getValue($collection, $index, $this->magicIsAllowed);\n            }\n        }\n\n        return $return;\n    }\n}\n"
  },
  {
    "path": "src/Filters/QueryMatchFilter.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Filters;\n\nuse Flow\\JSONPath\\AccessHelper;\nuse Flow\\JSONPath\\JSONPath;\nuse Flow\\JSONPath\\JSONPathException;\nuse JsonException;\nuse RuntimeException;\n\nuse const JSON_THROW_ON_ERROR;\nuse const PREG_OFFSET_CAPTURE;\nuse const PREG_UNMATCHED_AS_NULL;\n\nclass QueryMatchFilter extends AbstractFilter\n{\n    protected const string MATCH_QUERY_NEGATION_WRAPPED = '^(?<negate>!)\\((?<logicalexpr>.+)\\)$';\n\n    protected const string MATCH_QUERY_NEGATION_UNWRAPPED = '^(?<negate>!)(?<logicalexpr>.+)$';\n\n    protected const string MATCH_QUERY_OPERATORS = '\n      (@\\.(?<key>[^\\s<>!=]+)|@\\[[\"\\']?(?<keySquare>.*?)[\"\\']?\\]|(?<node>@)|(%group(?<group>\\d+)%))\n      (\\s*(?<operator>==|=~|=|<>|!==|!=|>=|<=|>|<|in|!in|nin)\\s*(?<comparisonValue>.+?(?=\\s*(?:&&|$|\\|\\||%))))?\n      (\\s*(?<logicalandor>&&|\\|\\|)\\s*)?\n    ';\n\n    protected const string MATCH_GROUPED_EXPRESSION = '#\\([^)(]*+(?:(?R)[^)(]*)*+\\)#';\n\n    /**\n     * @throws JSONPathException\n     * @inheritDoc\n     */\n    public function filter(array|object $collection): array\n    {\n        $filterExpression = $this->token->value;\n        $isShorthand = $this->token->shorthand ?? false;\n\n        if (\\is_array($filterExpression)) {\n            $isShorthand = $filterExpression['shorthand'] ?? $isShorthand;\n            $filterExpression = $filterExpression['expression'] ?? '';\n        }\n\n        $negateFilter = false;\n\n        if (\n            \\preg_match('/' . static::MATCH_QUERY_NEGATION_WRAPPED . '/x', $filterExpression, $negationMatches)\n            || \\preg_match('/' . static::MATCH_QUERY_NEGATION_UNWRAPPED . '/x', $filterExpression, $negationMatches)\n        ) {\n            $negateFilter = true;\n            $filterExpression = $negationMatches['logicalexpr'];\n        }\n\n        $literalResult = $this->evaluateLiteralExpression($filterExpression, $collection);\n\n        if ($literalResult !== null) {\n            return $literalResult;\n        }\n\n        $shortCircuitResult = $this->evaluateExpressionWithTrailingLiteral($filterExpression, $collection);\n\n        if ($shortCircuitResult !== null) {\n            return $shortCircuitResult;\n        }\n\n        $filterGroups = [];\n\n        if (\n            \\preg_match_all(\n                static::MATCH_GROUPED_EXPRESSION,\n                $filterExpression,\n                $matches,\n                PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL\n            )\n        ) {\n            foreach ($matches[0] as $i => $matchesGroup) {\n                $test = \\substr($matchesGroup[0], 1, -1);\n\n                //sanity check that our group is a group and not something within a string or regular expression\n                if (\\preg_match('/' . static::MATCH_QUERY_OPERATORS . '/x', $test)) {\n                    $filterGroups[$i] = $test;\n                    $filterExpression = \\str_replace($matchesGroup[0], \"%group{$i}%\", $filterExpression);\n                }\n            }\n        }\n\n        $match = \\preg_match_all(\n            '/' . static::MATCH_QUERY_OPERATORS . '/x',\n            $filterExpression,\n            $matches,\n            PREG_UNMATCHED_AS_NULL\n        );\n\n        if (\n            $match === false\n            || !isset($matches[1][0])\n            || isset($matches['logicalandor'][\\array_key_last($matches['logicalandor'])])\n        ) {\n            $constantResult = $this->evaluateConstantExpression($filterExpression);\n\n            if ($constantResult !== null) {\n                return $constantResult ? AccessHelper::arrayValues($collection) : [];\n            }\n\n            throw new RuntimeException('Malformed filter query');\n        }\n\n        $return = [];\n        $matchCount = \\count($matches[0]);\n\n        for ($expressionPart = 0; $expressionPart < $matchCount; $expressionPart++) {\n            $filteredCollection = $collection;\n            $logicalJoin = $expressionPart > 0 ? $matches['logicalandor'][$expressionPart - 1] : null;\n\n            if ($logicalJoin === '&&') {\n                //Restrict the nodes we need to look at to those already meeting criteria\n                $filteredCollection = $return;\n                $return = [];\n            }\n\n            //Processing a group\n            if ($matches['group'][$expressionPart] !== null) {\n                $filter = '$[?(' . $filterGroups[$matches['group'][$expressionPart]] . ')]';\n                $resolve = new JSONPath($filteredCollection)->find($filter)->getData();\n                $return = $resolve;\n\n                continue;\n            }\n\n            //Process a normal expression\n            $key = $this->normalizeKey($matches['key'][$expressionPart] ?: $matches['keySquare'][$expressionPart]);\n\n            $operator = $matches['operator'][$expressionPart] ?? null;\n            $comparisonValue = $matches['comparisonValue'][$expressionPart] ?? null;\n            $comparisonIsPath = $this->isPathComparison($comparisonValue);\n            $canCompareMissing = \\in_array($operator, ['=', '==', '!=', '!==', '<>'], true) && $comparisonIsPath;\n\n            if (\\is_string($comparisonValue)) {\n                $comparisonValue = \\preg_replace('/^\\'/', '\"', $comparisonValue);\n                $comparisonValue = \\preg_replace('/\\'$/', '\"', $comparisonValue);\n\n                try {\n                    $comparisonValue = \\json_decode($comparisonValue, true, 512, JSON_THROW_ON_ERROR);\n                } catch (JsonException) {\n                    //Leave $comparisonValue as raw (e.g. regular express or non quote wrapped string)\n                }\n            }\n\n            foreach ($filteredCollection as $nodeIndex => $node) {\n                if ($logicalJoin === '||' && \\array_key_exists($nodeIndex, $return)) {\n                    //Short-circuit, node already exists in output due to previous test\n                    continue;\n                }\n\n                $selectedNode = null;\n                $notNothing = AccessHelper::keyExists($node, $key, $this->magicIsAllowed);\n\n                if ($key) {\n                    if ($notNothing) {\n                        $selectedNode = AccessHelper::getValue($node, $key, $this->magicIsAllowed);\n                    } elseif (\\str_contains($key, '.')) {\n                        $foundValue = new JSONPath($node)->find($key)->getData();\n\n                        if ($foundValue) {\n                            $selectedNode = $foundValue[0];\n                            $notNothing = true;\n                        }\n                    } elseif ($canCompareMissing) {\n                        $notNothing = true;\n                    }\n                } else {\n                    //Node selection was plain @\n                    $selectedNode = $node;\n                    $notNothing = true;\n                }\n\n                $comparisonResult = null;\n\n                if ($notNothing) {\n                    $resolvedComparisonValue = $this->resolveComparisonValue($comparisonValue, $node);\n                    $comparisonResult = false;\n\n                    switch ($operator) {\n                        case null:\n                            if ($key === '' || $key === null) {\n                                $comparisonResult = !$isShorthand || $this->isTruthy($selectedNode);\n                            } else {\n                                $comparisonResult = AccessHelper::keyExists($node, $key, $this->magicIsAllowed)\n                                        || (!$key);\n                            }\n                            break;\n                        case \"=\":\n                        case \"==\":\n                            $comparisonResult = $this->compareEquals($selectedNode, $resolvedComparisonValue);\n                            break;\n                        case \"!=\":\n                        case \"!==\":\n                        case \"<>\":\n                            $comparisonResult = !$this->compareEquals($selectedNode, $resolvedComparisonValue);\n                            break;\n                        case '=~':\n                            $comparisonResult = @\\preg_match(\n                                (string)$resolvedComparisonValue,\n                                (string)$selectedNode\n                            );\n                            break;\n                        case '<':\n                            $comparisonResult = $this->compareLessThan($selectedNode, $resolvedComparisonValue);\n                            break;\n                        case '<=':\n                            $comparisonResult = $this->compareLessThan($selectedNode, $resolvedComparisonValue)\n                                || $this->compareEquals($selectedNode, $resolvedComparisonValue);\n                            break;\n                        case '>':\n                            //rfc semantics\n                            $comparisonResult = $this->compareLessThan($resolvedComparisonValue, $selectedNode);\n                            break;\n                        case '>=':\n                            //rfc semantics\n                            $comparisonResult = $this->compareLessThan($resolvedComparisonValue, $selectedNode)\n                                || $this->compareEquals($selectedNode, $resolvedComparisonValue);\n                            break;\n                        case \"in\":\n                            $comparisonResult = \\is_array($resolvedComparisonValue)\n                                && \\in_array($selectedNode, $resolvedComparisonValue, true);\n                            break;\n                        case 'nin':\n                        case \"!in\":\n                            $comparisonResult = \\is_array($resolvedComparisonValue)\n                                && !\\in_array($selectedNode, $resolvedComparisonValue, true);\n                            break;\n                    }\n                }\n\n                if ($negateFilter) {\n                    $comparisonResult = !$comparisonResult;\n                }\n\n                if ($comparisonResult) {\n                    $return[$nodeIndex] = $node;\n                }\n            }\n        }\n\n        //Keep out returned nodes in the same order they were defined in the original collection\n        \\ksort($return);\n\n        return $return;\n    }\n\n    protected function isNumber(mixed $value): bool\n    {\n        return !\\is_string($value) && \\is_numeric($value);\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    private function resolveComparisonValue(mixed $comparisonValue, mixed $node): mixed\n    {\n        if (!\\is_string($comparisonValue)) {\n            return $comparisonValue;\n        }\n\n        if (\\str_starts_with($comparisonValue, '@')) {\n            $path = \\substr($comparisonValue, 1);\n\n            if ($path === '' || $path === '.') {\n                return $node;\n            }\n\n            $resolved = new JSONPath($node)->find($path)->getData();\n\n            return \\is_array($resolved) && \\array_key_exists(0, $resolved) ? $resolved[0] : null;\n        }\n\n        if (\\str_starts_with($comparisonValue, '$')) {\n            $root = $this->rootData ?? $node;\n            $resolved = new JSONPath($root)->find($comparisonValue)->getData();\n\n            return \\is_array($resolved) && \\array_key_exists(0, $resolved) ? $resolved[0] : null;\n        }\n\n        return $comparisonValue;\n    }\n\n    private function normalizeKey(mixed $key): int|string|null\n    {\n        if (\\is_string($key) && \\preg_match('/^-?\\d+$/', $key)) {\n            return (int)$key;\n        }\n\n        return $key;\n    }\n\n    private function isPathComparison(mixed $comparisonValue): bool\n    {\n        return \\is_string($comparisonValue) && \\str_starts_with($comparisonValue, '@');\n    }\n\n    private function evaluateConstantExpression(string $expression): ?bool\n    {\n        $pattern = '/^\\s*(?<left>[^&|]+?)\\s*(?<operator>==|=|!=|!==|<>|<=|>=|<|>)\\s*(?<right>[^&|]+?)\\s*$/';\n\n        if (!\\preg_match($pattern, $expression, $matches)) {\n            return null;\n        }\n\n        $left = $this->decodeLiteral($matches['left']);\n        $right = $this->decodeLiteral($matches['right']);\n        $operator = $matches['operator'];\n\n        return match ($operator) {\n            '==', '=' => $this->compareEquals($left, $right),\n            '!=', '!==', '<>' => !$this->compareEquals($left, $right),\n            '<' => $this->compareLessThan($left, $right),\n            '<=' => $this->compareLessThan($left, $right) || $this->compareEquals($left, $right),\n            '>' => $this->compareLessThan($right, $left),\n            '>=' => $this->compareLessThan($right, $left) || $this->compareEquals($left, $right),\n        };\n    }\n\n    /**\n     * @param array<int, mixed>|object $collection\n     * @return array<int, mixed>|null\n     * @throws JSONPathException\n     */\n    private function evaluateLiteralExpression(string $expression, array|object $collection): ?array\n    {\n        $trimmed = \\trim($expression);\n\n        if ($trimmed === '') {\n            return [];\n        }\n\n        $literalValue = $this->decodeLiteral($trimmed);\n        $literalIsBool = \\is_bool($literalValue);\n\n        if (!$literalIsBool && $literalValue !== null) {\n            return null;\n        }\n\n        return $this->isTruthy($literalValue) ? AccessHelper::arrayValues($collection) : [];\n    }\n\n    /**\n     * @param array<int, mixed>|object $collection\n     * @return array<int, mixed>|null\n     * @throws JSONPathException\n     */\n    private function evaluateExpressionWithTrailingLiteral(\n        string $expression,\n        array|object $collection\n    ): ?array {\n        if (\n            !\\preg_match(\n                '/^(?<left>.+?)\\s*(?<op>&&|\\|\\|)\\s*(?<literal>true|false|null)\\s*$/i',\n                $expression,\n                $matches\n            )\n        ) {\n            return null;\n        }\n\n        $leftFilter = '$[?(' . $matches['left'] . ')]';\n        $leftResult = new JSONPath($collection)->find($leftFilter)->getData();\n        $literalValue = $this->decodeLiteral($matches['literal']);\n        $literalIsTrue = $this->isTruthy($literalValue);\n\n        return match ($matches['op']) {\n            '&&' => $literalIsTrue ? $leftResult : [],\n            '||' => $literalIsTrue ? AccessHelper::arrayValues($collection) : $leftResult,\n            default => [],\n        };\n    }\n\n    private function decodeLiteral(string $literal): mixed\n    {\n        $literal = \\trim($literal);\n\n        try {\n            return \\json_decode($literal, true, 512, \\JSON_THROW_ON_ERROR);\n        } catch (JsonException) {\n            if (\\is_numeric($literal)) {\n                return $literal + 0;\n            }\n\n            return $literal;\n        }\n    }\n\n    private function isTruthy(mixed $value): bool\n    {\n        return (bool)$value;\n    }\n\n    protected function compareEquals(mixed $a, mixed $b): bool\n    {\n        $type_a = \\gettype($a);\n        $type_b = \\gettype($b);\n\n        if ($type_a === $type_b || ($this->isNumber($a) && $this->isNumber($b))) {\n            //Primitives or Numbers\n            if ($a === null || \\is_scalar($a)) {\n                /** @noinspection TypeUnsafeComparisonInspection */\n                return $a == $b;\n            }\n\n            if (\\is_array($a) && \\is_array($b)) {\n                return $this->deepEqual($a, $b);\n            }\n\n            if (\\is_object($a) && \\is_object($b)) {\n                return $this->deepEqual((array)$a, (array)$b);\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * @param array<array-key, mixed> $a\n     * @param array<array-key, mixed> $b\n     */\n    private function deepEqual(array $a, array $b): bool\n    {\n        $aIsList = \\array_is_list($a);\n        $bIsList = \\array_is_list($b);\n\n        if ($aIsList !== $bIsList) {\n            return false;\n        }\n\n        if (\\count($a) !== \\count($b)) {\n            return false;\n        }\n\n        if ($aIsList) {\n            return \\array_all($a, fn ($value, $index) => \\array_key_exists($index, $b)\n                && $this->compareEquals($value, $b[$index]));\n        }\n\n        return \\array_all($a, fn ($value, $key) => \\array_key_exists($key, $b)\n            && $this->compareEquals($value, $b[$key]));\n    }\n\n    protected function compareLessThan(mixed $a, mixed $b): bool\n    {\n        if ((\\is_string($a) && \\is_string($b)) || ($this->isNumber($a) && $this->isNumber($b))) {\n            //numerical and string comparison supported only\n            return $a < $b;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Filters/QueryResultFilter.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Filters;\n\nuse Flow\\JSONPath\\AccessHelper;\nuse Flow\\JSONPath\\JSONPathException;\n\nclass QueryResultFilter extends AbstractFilter\n{\n    /**\n     * @throws JSONPathException\n     * @inheritDoc\n     */\n    public function filter(array|object $collection): array\n    {\n        if (!\\preg_match('/@\\.(?<key>\\w+)\\s*(?<operator>[-+*\\/])\\s*(?<numeric>\\d+)/', $this->token->value, $matches)) {\n            throw new JSONPathException('Unsupported operator in expression');\n        }\n\n        $matchKey = $matches['key'];\n\n        if (AccessHelper::keyExists($collection, $matchKey, $this->magicIsAllowed)) {\n            $value = AccessHelper::getValue($collection, $matchKey, $this->magicIsAllowed);\n        } elseif ($matches['key'] === 'length') {\n            $value = \\count($collection);\n        } else {\n            return [];\n        }\n\n        $resultKey = match ($matches['operator']) {\n            '+' => $value + $matches['numeric'],\n            '*' => $value * $matches['numeric'],\n            '-' => $value - $matches['numeric'],\n            '/' => $value / $matches['numeric'],\n        };\n\n        $result = [];\n\n        if (AccessHelper::keyExists($collection, $resultKey, $this->magicIsAllowed)) {\n            $result[] = AccessHelper::getValue($collection, $resultKey, $this->magicIsAllowed);\n        }\n\n        return $result;\n    }\n}\n"
  },
  {
    "path": "src/Filters/RecursiveFilter.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Filters;\n\nuse Flow\\JSONPath\\AccessHelper;\nuse Flow\\JSONPath\\JSONPathException;\n\nclass RecursiveFilter extends AbstractFilter\n{\n    /**\n     * @inheritDoc\n     *\n     * @throws JSONPathException\n     */\n    public function filter(array|object $collection): array\n    {\n        $result = [];\n\n        $this->recurse($result, $collection);\n\n        return $result;\n    }\n\n    /**\n     * @param array<int, array<array-key, mixed>> $result\n     * @param array<array-key, mixed>|object $data\n     *\n     * @throws JSONPathException\n     */\n    private function recurse(array &$result, array|object $data): void\n    {\n        $result[] = (array)$data;\n\n        if (AccessHelper::isCollectionType($data)) {\n            foreach (AccessHelper::arrayValues($data) as $value) {\n                if (AccessHelper::isCollectionType($value)) {\n                    $this->recurse($result, $value);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Filters/SliceFilter.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Filters;\n\nuse Flow\\JSONPath\\AccessHelper;\n\nclass SliceFilter extends AbstractFilter\n{\n    /**\n     * @inheritDoc\n     */\n    public function filter(array|object $collection): array\n    {\n        if (\n            !\\is_array($collection)\n            && !$collection instanceof \\Countable\n            && !$collection instanceof \\ArrayAccess\n        ) {\n            return [];\n        }\n\n        $length = \\count($collection);\n        $start = $this->token->value['start'];\n        $end = $this->token->value['end'];\n        $step = $this->token->value['step'] ?? 1;\n        $result = [];\n\n        if ($step === 0) {\n            return $result;\n        }\n\n        if ($step > 0) {\n            [$start, $end] = $this->normalizeForPositiveStep($length, $start, $end);\n\n            for ($i = $start; $i < $end; $i += $step) {\n                if (AccessHelper::keyExists($collection, $i, $this->magicIsAllowed)) {\n                    $result[] = $collection[$i];\n                }\n            }\n\n            return $result;\n        }\n\n        [$start, $end] = $this->normalizeForNegativeStep($length, $start, $end);\n\n        for ($i = $start; $i > $end; $i += $step) {\n            if (AccessHelper::keyExists($collection, $i, $this->magicIsAllowed)) {\n                $result[] = $collection[$i];\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * @return array{0: int, 1: int}\n     */\n    private function normalizeForPositiveStep(int $length, ?int $start, ?int $end): array\n    {\n        if ($start === null) {\n            $start = 0;\n        } elseif ($start < 0) {\n            $start += $length;\n        }\n\n        if ($start < 0) {\n            $start = 0;\n        } elseif ($start > $length) {\n            $start = $length;\n        }\n\n        if ($end === null) {\n            $end = $length;\n        } elseif ($end < 0) {\n            $end += $length;\n        }\n\n        if ($end < 0) {\n            $end = 0;\n        } elseif ($end > $length) {\n            $end = $length;\n        }\n\n        return [$start, $end];\n    }\n\n    /**\n     * @return array{0: int, 1: int}\n     */\n    private function normalizeForNegativeStep(int $length, ?int $start, ?int $end): array\n    {\n        if ($start === null) {\n            $start = $length - 1;\n        } else {\n            if ($start < 0) {\n                $start += $length;\n            }\n\n            if ($start < 0) {\n                $start = -1;\n            } elseif ($start >= $length) {\n                $start = $length - 1;\n            }\n        }\n\n        if ($end === null) {\n            $end = -1;\n        } else {\n            if ($end < 0) {\n                $end += $length;\n            }\n\n            if ($end < 0) {\n                $end = -1;\n            } elseif ($end >= $length) {\n                $end = $length - 1;\n            }\n        }\n\n        return [$start, $end];\n    }\n}\n"
  },
  {
    "path": "src/JSONPath.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath;\n\nuse ArrayAccess;\nuse Countable;\nuse Iterator;\nuse JsonSerializable;\nuse Override;\n\n/**\n * @implements ArrayAccess<int|string, mixed>\n * @implements Iterator<int|string, mixed>\n */\nclass JSONPath implements ArrayAccess, Iterator, JsonSerializable, Countable\n{\n    public const int ALLOW_MAGIC = 1;\n\n    /** @var array<int, list<JSONPathToken>> */\n    protected static array $tokenCache = [];\n\n    protected mixed $data = [];\n\n    protected int $options = 0;\n\n    final public function __construct(mixed $data = [], int $options = 0)\n    {\n        $this->data = $data;\n        $this->options = $options;\n    }\n\n    /**\n     * Evaluate an expression\n     *\n     * @throws JSONPathException\n     *\n     * @return static\n     */\n    public function find(string $expression): self\n    {\n        $tokens = $this->parseTokens($expression);\n        $collectionData = [$this->data];\n\n        foreach ($tokens as $token) {\n            $filter = $token->buildFilter($this->options);\n            $filter->setRootData($this->data);\n            $filteredDataList = [];\n\n            foreach ($collectionData as $value) {\n                if (AccessHelper::isCollectionType($value)) {\n                    $filteredDataList[] = $filter->filter($value);\n                }\n            }\n\n            if (!empty($filteredDataList)) {\n                $collectionData = \\array_merge(...$filteredDataList);\n            } else {\n                $collectionData = [];\n            }\n        }\n\n        return new static($collectionData, $this->options);\n    }\n\n    public function first(): mixed\n    {\n        $keys = AccessHelper::collectionKeys($this->data);\n\n        if (empty($keys)) {\n            return null;\n        }\n\n        $value = $this->data[$keys[0]] ?? null;\n\n        return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;\n    }\n\n    /**\n     * Evaluate an expression and return the last result\n     */\n    public function last(): mixed\n    {\n        $keys = AccessHelper::collectionKeys($this->data);\n\n        if (empty($keys)) {\n            return null;\n        }\n\n        $value = $this->data[\\end($keys)] ?? null;\n\n        return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;\n    }\n\n    /**\n     * Evaluate an expression and return the first key\n     */\n    public function firstKey(): string|int|null\n    {\n        $keys = AccessHelper::collectionKeys($this->data);\n\n        if (empty($keys)) {\n            return null;\n        }\n\n        return $keys[0];\n    }\n\n    /**\n     * Evaluate an expression and return the last key\n     */\n    public function lastKey(): string|int|null\n    {\n        $keys = AccessHelper::collectionKeys($this->data);\n\n        if (empty($keys)) {\n            return null;\n        }\n\n        return \\end($keys);\n    }\n\n    /**\n     * @return list<JSONPathToken>\n     * @throws JSONPathException\n     */\n    public function parseTokens(string $expression): array\n    {\n        $cacheKey = \\crc32($expression);\n\n        if (isset(static::$tokenCache[$cacheKey])) {\n            return static::$tokenCache[$cacheKey];\n        }\n\n        $lexer = new JSONPathLexer($expression);\n        $tokens = $lexer->parseExpression();\n\n        static::$tokenCache[$cacheKey] = $tokens;\n\n        return $tokens;\n    }\n\n    public function getData(): mixed\n    {\n        return $this->data;\n    }\n\n    /**\n     * @noinspection MagicMethodsValidityInspection\n     */\n    public function __get(string|int $key): mixed\n    {\n        return $this->offsetExists($key) ? $this->offsetGet($key) : null;\n    }\n\n    #[Override]\n    public function offsetExists(mixed $offset): bool\n    {\n        return AccessHelper::keyExists($this->data, $offset);\n    }\n\n    #[Override]\n    public function offsetGet(mixed $offset): mixed\n    {\n        $value = AccessHelper::getValue($this->data, $offset);\n\n        return AccessHelper::isCollectionType($value)\n            ? new static($value, $this->options)\n            : $value;\n    }\n\n    #[Override]\n    public function offsetSet(mixed $offset, mixed $value): void\n    {\n        if ($offset === null) {\n            $this->data[] = $value;\n        } else {\n            AccessHelper::setValue($this->data, $offset, $value);\n        }\n    }\n\n    #[Override]\n    public function offsetUnset(mixed $offset): void\n    {\n        AccessHelper::unsetValue($this->data, $offset);\n    }\n\n    #[Override]\n    public function jsonSerialize(): mixed\n    {\n        return $this->getData();\n    }\n\n    #[Override]\n    public function current(): mixed\n    {\n        $value = \\current($this->data);\n\n        return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;\n    }\n\n    #[Override]\n    public function next(): void\n    {\n        \\next($this->data);\n    }\n\n    #[Override]\n    public function key(): string|int|null\n    {\n        return \\key($this->data);\n    }\n\n    #[Override]\n    public function valid(): bool\n    {\n        return \\key($this->data) !== null;\n    }\n\n    #[Override]\n    public function rewind(): void\n    {\n        \\reset($this->data);\n    }\n\n    #[Override]\n    public function count(): int\n    {\n        return \\count($this->data);\n    }\n}\n"
  },
  {
    "path": "src/JSONPathException.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath;\n\nuse Exception;\n\nclass JSONPathException extends Exception\n{\n    // does nothing\n}\n"
  },
  {
    "path": "src/JSONPathLexer.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath;\n\nclass JSONPathLexer\n{\n    /*\n     * Match within bracket groups\n     * Matches are whitespace insensitive\n     */\n\n    // e.g.: foo or 40f35757-2563-4790-b0b1-caa904be455f or $\n    public const string MATCH_INDEX = '(?!-)[\\-\\w]+ | \\\\$ | \\\\*';\n\n    // Eg. 0,1,2 or *,1 or 0,1,2,\n    public const string MATCH_INDEXES = '\\s* (?:-?\\d+|\\*) (?: \\s* , \\s* (?:-?\\d+|\\*) )+ \\s* ,? \\s*';\n\n    // Eg. [0:2:1] or [-1]\n    public const string MATCH_SLICE = '(?:-?\\d*:-?\\d*(?::-?\\d*)?|-\\\\d+)';\n\n    // Eg. ?(@.length - 1)\n    public const string MATCH_QUERY_RESULT = '\\s* \\( .+? \\) \\s*';\n\n    // Eg. ?(@.foo = \"bar\")\n    public const string MATCH_QUERY_MATCH = '\\s* \\?\\(.+?\\) \\s*';\n\n    // Eg. 'bar'\n    public const string MATCH_INDEX_IN_SINGLE_QUOTES = '\\s* \\' (.+?)? \\' \\s*';\n\n    // Eg. \"bar\"\n    public const string MATCH_INDEX_IN_DOUBLE_QUOTES = '\\s* \" (.+?)? \" \\s*';\n\n    private readonly string $expression;\n\n    private readonly int $expressionLength;\n\n    public function __construct(string $expression)\n    {\n        $expression = \\trim($expression);\n        $len = \\strlen($expression);\n\n        if ($len === 0) {\n            $this->expression = '';\n            $this->expressionLength = 0;\n\n            return;\n        }\n\n        if ($expression[0] === '$' || $expression[0] === '@') {\n            $expression = \\substr($expression, 1);\n        }\n\n        if ($expression === '') {\n            $this->expression = '';\n            $this->expressionLength = 0;\n\n            return;\n        }\n\n        if ($expression[0] !== '.' && $expression[0] !== '[') {\n            $expression = '.' . $expression;\n        }\n\n        $this->expression = $expression;\n        $this->expressionLength = \\strlen($expression);\n    }\n\n    /**\n     * @return list<JSONPathToken>\n     * @throws JSONPathException\n     */\n    public function parseExpressionTokens(): array\n    {\n        $squareBracketDepth = 0;\n        $tokenValue = '';\n        $tokens = [];\n        $inBracketQuote = null;\n        $inQuote = null;\n\n        for ($i = 0; $i < $this->expressionLength; $i++) {\n            $char = $this->expression[$i];\n\n            if ($squareBracketDepth === 0 && ($char === \"'\" || $char === '\"')) {\n                $escaped = $this->isEscaped($tokenValue);\n                $inQuote = $inQuote === $char && !$escaped ? null : ($inQuote ?? $char);\n            }\n\n            if (($squareBracketDepth === 0) && $inQuote === null && $char === '.') {\n                if ($this->lookAhead($i) === '.') {\n                    $tokens[] = new JSONPathToken(TokenType::Recursive, null);\n                }\n\n                continue;\n            }\n\n            if ($char === '[' && $inBracketQuote === null) {\n                $squareBracketDepth++;\n\n                if ($squareBracketDepth === 1) {\n                    $inBracketQuote = null;\n\n                    continue;\n                }\n            }\n\n            if ($char === ']' && $squareBracketDepth > 0 && $inBracketQuote === null) {\n                $squareBracketDepth--;\n\n                if ($squareBracketDepth === 0) {\n                    $tokens[] = $this->createToken($tokenValue);\n                    $tokenValue = '';\n\n                    continue;\n                }\n            }\n\n            /*\n             * Within square brackets\n             */\n            if ($squareBracketDepth > 0) {\n                if (($char === \"'\" || $char === '\"')) {\n                    $escaped = $this->isEscaped($tokenValue);\n\n                    if ($inBracketQuote === null && !$escaped) {\n                        $inBracketQuote = $char;\n                    } elseif ($inBracketQuote === $char && !$escaped) {\n                        $inBracketQuote = null;\n                    }\n                }\n\n                $tokenValue .= $char;\n\n                continue;\n            }\n\n            /*\n             * Outside square brackets\n             */\n            $tokenValue .= $char;\n\n            if (\n                $inQuote === null\n                && ($this->atEnd($i) || \\in_array($this->lookAhead($i), ['.', '['], true))\n            ) {\n                $tokens[] = $this->createToken($tokenValue);\n                $tokenValue = '';\n            }\n        }\n\n        if ($tokenValue !== '') {\n            $tokens[] = $this->createToken($tokenValue);\n        }\n\n        return $tokens;\n    }\n\n    protected function lookAhead(int $pos, int $forward = 1): ?string\n    {\n        return $this->expression[$pos + $forward] ?? null;\n    }\n\n    protected function atEnd(int $pos): bool\n    {\n        return $pos === ($this->expressionLength - 1);\n    }\n\n    /**\n     * @return list<JSONPathToken>\n     * @throws JSONPathException\n     */\n    public function parseExpression(): array\n    {\n        return $this->parseExpressionTokens();\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    protected function createToken(string $value): JSONPathToken\n    {\n        // The IDE doesn't like, what we do with $value, so let's\n        // move it to a separate variable, to get rid of any IDE warnings\n        $tokenValue = \\trim($value);\n\n        /** @var JSONPathToken|null $ret */\n        $ret = null;\n\n        if (\\str_contains($tokenValue, ',')) {\n            $parts = \\array_values(\\array_filter(\n                \\array_map('trim', \\explode(',', $tokenValue)),\n                static fn (string $part): bool => $part !== ''\n            ));\n\n            if ($parts !== []) {\n                $union = [];\n\n                $hasSlice = false;\n                $hasQuery = false;\n\n                foreach ($parts as $part) {\n                    if (\n                        \\preg_match('/^' . static::MATCH_INDEX_IN_SINGLE_QUOTES . '$/xu', $part, $matches)\n                        || \\preg_match('/^' . static::MATCH_INDEX_IN_DOUBLE_QUOTES . '$/xu', $part, $matches)\n                    ) {\n                        $union[] = $this->decodeQuotedIndex($matches[1] ?? '', $matches[0][0]);\n\n                        continue;\n                    }\n\n                    if (\\preg_match('/^-\\\\d+$/', $part)) {\n                        $union[] = (int)$part;\n\n                        continue;\n                    }\n\n                    if (\\preg_match('/^' . static::MATCH_SLICE . '$/u', $part)) {\n                        $union[] = [\n                            'type' => 'slice',\n                            'value' => $this->parseSlice($part),\n                        ];\n                        $hasSlice = true;\n\n                        continue;\n                    }\n\n                    if (\\preg_match('/^(' . static::MATCH_INDEX . ')$/xu', $part)) {\n                        $union[] = \\preg_match('/^-?\\d+$/', $part) ? (int)$part : $part;\n\n                        continue;\n                    }\n\n                    if (\\preg_match('/^' . static::MATCH_QUERY_MATCH . '$/xu', $part)) {\n                        $union[] = [\n                            'type' => 'query',\n                            'value' => \\substr($part, 2, -1),\n                        ];\n                        $hasQuery = true;\n                    }\n                }\n\n                if (\\count($union) === \\count($parts)) {\n                    $quotedPattern = '/^(' . static::MATCH_INDEX_IN_SINGLE_QUOTES . '|'\n                        . static::MATCH_INDEX_IN_DOUBLE_QUOTES . ')$/xu';\n\n                    $quotedCallback = static function (string $part) use ($quotedPattern): bool {\n                        return \\preg_match($quotedPattern, $part) === 1;\n                    };\n\n                    $quotedParts = \\array_filter($parts, $quotedCallback);\n\n                    $allQuoted = \\count($quotedParts) === \\count($parts);\n\n                    $tokenType = ($hasSlice || $hasQuery || !$allQuoted) ? TokenType::Indexes : TokenType::Index;\n\n                    return new JSONPathToken($tokenType, $union, $allQuoted);\n                }\n            }\n        }\n\n        if (\\preg_match('/^-\\\\d+$/', $tokenValue)) {\n            return new JSONPathToken(TokenType::Index, (int)$tokenValue);\n        }\n\n        if ($tokenValue === '') {\n            return new JSONPathToken(TokenType::Indexes, []);\n        }\n\n        if (\n            ($tokenValue[0] === \"'\" || $tokenValue[0] === '\"')\n            && $tokenValue[\\strlen($tokenValue) - 1] === $tokenValue[0]\n        ) {\n            $tokenValue = $this->decodeQuotedIndex(\\substr($tokenValue, 1, -1), $tokenValue[0]);\n\n            return new JSONPathToken(TokenType::Index, $tokenValue, true);\n        }\n\n        if (\\preg_match('/^(' . static::MATCH_INDEX . ')$/xu', $tokenValue, $matches)) {\n            if (\\preg_match('/^-?\\d+$/', $tokenValue)) {\n                $tokenValue = (int)$tokenValue;\n            }\n\n            $ret = new JSONPathToken(TokenType::Index, $tokenValue);\n        } elseif (\\preg_match('/^' . static::MATCH_SLICE . '$/xu', $tokenValue, $matches)) {\n            $tokenValue = $this->parseSlice($tokenValue);\n\n            $ret = new JSONPathToken(TokenType::Slice, $tokenValue);\n        } elseif (\\preg_match('/^' . static::MATCH_QUERY_RESULT . '$/xu', $tokenValue)) {\n            $tokenValue = \\substr($tokenValue, 1, -1);\n\n            $ret = new JSONPathToken(TokenType::QueryResult, $tokenValue);\n        } elseif ($tokenValue === '?()') {\n            $ret = new JSONPathToken(TokenType::QueryMatch, '', shorthand: false);\n        } elseif ($tokenValue === '?') {\n            $ret = new JSONPathToken(TokenType::QueryMatch, '@', shorthand: true);\n        } elseif (\\preg_match('/^\\\\?@/', $tokenValue)) {\n            $expr = \\substr($tokenValue, 1);\n            $expr = $expr === '' ? '@' : $expr;\n\n            $ret = new JSONPathToken(TokenType::QueryMatch, $expr, shorthand: true);\n        } elseif (\\preg_match('/^' . static::MATCH_QUERY_MATCH . '$/xu', $tokenValue)) {\n            $tokenValue = \\substr($tokenValue, 2, -1);\n\n            $ret = new JSONPathToken(TokenType::QueryMatch, $tokenValue);\n        }\n\n        if ($ret !== null) {\n            return $ret;\n        }\n\n        throw new JSONPathException(\"Unable to parse token {$tokenValue} in expression: {$this->expression}\");\n    }\n\n    /**\n     * @return array{start: int|null, end: int|null, step: int|null}\n     */\n    private function parseSlice(string $tokenValue): array\n    {\n        $parts = \\explode(':', $tokenValue);\n\n        return [\n            'start' => $parts[0] !== '' ? (int)$parts[0] : null,\n            'end' => isset($parts[1]) && $parts[1] !== '' ? (int)$parts[1] : null,\n            'step' => isset($parts[2]) && $parts[2] !== '' ? (int)$parts[2] : null,\n        ];\n    }\n\n    private function isEscaped(string $tokenValue): bool\n    {\n        $len = \\strlen($tokenValue);\n        if ($len === 0) {\n            return false;\n        }\n\n        $backslashCount = 0;\n\n        for ($i = $len - 1; $i >= 0; $i--) {\n            if ($tokenValue[$i] === '\\\\') {\n                $backslashCount++;\n                continue;\n            }\n\n            break;\n        }\n\n        return ($backslashCount % 2) === 1;\n    }\n\n    private function decodeQuotedIndex(string $tokenValue, string $quote): string\n    {\n        // Unescape backslashes first, then the quote type used\n        $tokenValue = \\str_replace('\\\\\\\\', '\\\\', $tokenValue);\n\n        if ($quote === \"'\") {\n            $tokenValue = \\str_replace(\"\\\\'\", \"'\", $tokenValue);\n        } elseif ($quote === '\"') {\n            $tokenValue = \\str_replace('\\\\\"', '\"', $tokenValue);\n        }\n\n        return $tokenValue;\n    }\n}\n"
  },
  {
    "path": "src/JSONPathToken.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath;\n\nuse Flow\\JSONPath\\Filters\\AbstractFilter;\nuse Flow\\JSONPath\\Filters\\IndexesFilter;\nuse Flow\\JSONPath\\Filters\\IndexFilter;\nuse Flow\\JSONPath\\Filters\\QueryMatchFilter;\nuse Flow\\JSONPath\\Filters\\QueryResultFilter;\nuse Flow\\JSONPath\\Filters\\RecursiveFilter;\nuse Flow\\JSONPath\\Filters\\SliceFilter;\n\nreadonly class JSONPathToken\n{\n    public function __construct(\n        public TokenType $type,\n        public mixed $value,\n        public bool $quoted = false,\n        public bool $shorthand = false,\n    ) {\n        // ...\n    }\n\n    public function buildFilter(int $options): AbstractFilter\n    {\n        $filterClass = match ($this->type) {\n            TokenType::Index => IndexFilter::class,\n            TokenType::Indexes => IndexesFilter::class,\n            TokenType::QueryMatch => QueryMatchFilter::class,\n            TokenType::QueryResult => QueryResultFilter::class,\n            TokenType::Recursive => RecursiveFilter::class,\n            TokenType::Slice => SliceFilter::class,\n        };\n\n        return new $filterClass($this, $options);\n    }\n}\n"
  },
  {
    "path": "src/TokenType.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath;\n\nenum TokenType: string\n{\n    case Index = 'index';\n    case Recursive = 'recursive';\n    case QueryResult = 'queryResult';\n    case QueryMatch = 'queryMatch';\n    case Slice = 'slice';\n    case Indexes = 'indexes';\n}\n"
  },
  {
    "path": "tests/AccessHelperTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse ArrayAccess;\nuse ArrayIterator;\nuse Flow\\JSONPath\\AccessHelper;\nuse Flow\\JSONPath\\JSONPathException;\nuse IteratorAggregate;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\TestCase;\nuse Traversable;\n\n#[CoversClass(AccessHelper::class)]\nclass AccessHelperTest extends TestCase\n{\n    public function testKeyExistsRespectsMagicGet(): void\n    {\n        $magic = new class {\n            public function __get(string $name): string\n            {\n                return \"magic-{$name}\";\n            }\n\n            public function __set(string $name, mixed $value): void\n            {\n                $this->{$name} = $value;\n            }\n\n            public function __isset(string $name): bool\n            {\n                return isset($this->{$name});\n            }\n        };\n\n        self::assertTrue(AccessHelper::keyExists($magic, 'foo', true));\n        self::assertFalse(AccessHelper::keyExists($magic, 'foo'));\n    }\n\n    public function testKeyExistsSupportsArrayAccessAndNegativeIndex(): void\n    {\n        $arrayAccess = new class implements ArrayAccess {\n            /** @var array<string, string> */\n            private array $store = ['bar' => 'baz'];\n\n            public function offsetExists($offset): bool\n            {\n                return \\array_key_exists($offset, $this->store);\n            }\n\n            public function offsetGet($offset): mixed\n            {\n                return $this->store[$offset];\n            }\n\n            public function offsetSet($offset, $value): void\n            {\n                $this->store[$offset] = $value;\n            }\n\n            public function offsetUnset($offset): void\n            {\n                unset($this->store[$offset]);\n            }\n        };\n\n        self::assertTrue(AccessHelper::keyExists($arrayAccess, 'bar'));\n        self::assertTrue(AccessHelper::keyExists([1 => 'foo'], -1));\n    }\n\n    public function testGetValueCoversMagicArrayAndArrayAccess(): void\n    {\n        $magic = new class {\n            public function __get(string $name): string\n            {\n                return \"magic-{$name}\";\n            }\n\n            public function __set(string $name, mixed $value): void\n            {\n                $this->{$name} = $value;\n            }\n\n            public function __isset(string $name): bool\n            {\n                return isset($this->{$name});\n            }\n        };\n\n        $arrayAccess = new class implements ArrayAccess {\n            /** @var array<string, string> */\n            private array $store = ['bar' => 'baz'];\n\n            public function offsetExists($offset): bool\n            {\n                return \\array_key_exists($offset, $this->store);\n            }\n\n            public function offsetGet($offset): mixed\n            {\n                return $this->store[$offset];\n            }\n\n            public function offsetSet($offset, $value): void\n            {\n                $this->store[$offset] = $value;\n            }\n\n            public function offsetUnset($offset): void\n            {\n                unset($this->store[$offset]);\n            }\n        };\n\n        self::assertSame('magic-foo', AccessHelper::getValue($magic, 'foo', true));\n        self::assertSame('baz', AccessHelper::getValue($arrayAccess, 'bar'));\n        self::assertSame('b', AccessHelper::getValue(['a', 'b'], -1));\n        self::assertNull(AccessHelper::getValue(['a'], 'missing'));\n        $plainObject = (object)['prop' => 'value'];\n        self::assertSame('value', AccessHelper::getValue($plainObject, 'prop'));\n    }\n\n    public function testGetValueByIndexSupportsTraversableAndNegativeOffset(): void\n    {\n        $iterable = new class implements IteratorAggregate {\n            public function getIterator(): Traversable\n            {\n                return new ArrayIterator(['first', 'second', 'third']);\n            }\n        };\n\n        self::assertSame('third', AccessHelper::getValue($iterable, -1));\n        self::assertSame('second', AccessHelper::getValue($iterable, 1));\n    }\n\n    public function testGetValueNullCases(): void\n    {\n        self::assertNull(AccessHelper::getValue('scalar', 'foo'));\n        self::assertNull(AccessHelper::getValue('scalar', 5));\n\n        $iterable = new ArrayIterator(['only']);\n        self::assertNull(AccessHelper::getValue($iterable, 5));\n    }\n\n    public function testArrayValuesThrowsOnInvalidType(): void\n    {\n        $this->expectException(JSONPathException::class);\n        AccessHelper::arrayValues('not-an-array');\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testArrayValuesCastsObject(): void\n    {\n        $obj = (object)['a' => 1, 'b' => 2];\n        self::assertSame([1, 2], AccessHelper::arrayValues($obj));\n        self::assertSame([1, 2], AccessHelper::arrayValues(['a' => 1, 'b' => 2]));\n    }\n\n    public function testGetValueByIndexReturnsNullWhenOutOfRange(): void\n    {\n        $iterable = (static function () {\n            yield 'first';\n        })();\n\n        self::assertNull(AccessHelper::getValue($iterable, 10));\n    }\n\n    public function testKeyExistsAndCollectionHelpers(): void\n    {\n        $object = (object)['a' => 1];\n        self::assertTrue(AccessHelper::keyExists($object, 'a'));\n        self::assertFalse(AccessHelper::keyExists('scalar', 'a'));\n        self::assertSame(['a'], AccessHelper::collectionKeys($object));\n        self::assertSame(['b'], AccessHelper::collectionKeys(['b' => 2]));\n        self::assertFalse(AccessHelper::isCollectionType('scalar'));\n    }\n\n    public function testSetAndUnsetValueAcrossTypes(): void\n    {\n        $object = (object)['a' => 1];\n        AccessHelper::setValue($object, 'b', 2);\n        self::assertSame(2, $object->b);\n        $array = ['x' => 1];\n        AccessHelper::setValue($array, 'y', 3);\n        self::assertSame(3, $array['y']);\n\n        $arrayAccess = new class implements ArrayAccess {\n            /** @var array<string, string> */\n            public array $store = [];\n\n            public function offsetExists($offset): bool\n            {\n                return \\array_key_exists($offset, $this->store);\n            }\n\n            public function offsetGet($offset): mixed\n            {\n                return $this->store[$offset];\n            }\n\n            public function offsetSet($offset, $value): void\n            {\n                $this->store[$offset] = $value;\n            }\n\n            public function offsetUnset($offset): void\n            {\n                unset($this->store[$offset]);\n            }\n        };\n\n        AccessHelper::setValue($arrayAccess, 'k', 'v');\n        self::assertSame('v', $arrayAccess->store['k']);\n        AccessHelper::unsetValue($arrayAccess, 'k');\n        self::assertArrayNotHasKey('k', $arrayAccess->store);\n\n        AccessHelper::unsetValue($array, 'x');\n        self::assertArrayNotHasKey('x', $array);\n\n        $obj = (object)['x' => 1];\n        AccessHelper::unsetValue($obj, 'x');\n        self::assertFalse(\\property_exists($obj, 'x'));\n\n        $arrayAccess->offsetUnset('missing');\n    }\n}\n"
  },
  {
    "path": "tests/IndexFilterTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse ArrayObject;\nuse Flow\\JSONPath\\Filters\\IndexFilter;\nuse Flow\\JSONPath\\JSONPath;\nuse Flow\\JSONPath\\JSONPathException;\nuse Flow\\JSONPath\\JSONPathToken;\nuse Flow\\JSONPath\\TokenType;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\TestCase;\n\n#[CoversClass(IndexFilter::class)]\nclass IndexFilterTest extends TestCase\n{\n    /**\n     * @throws JSONPathException\n     */\n    public function testArrayValueTokenReturnsOnlyExistingKeys(): void\n    {\n        $token = new JSONPathToken(TokenType::Index, [0, 2, 99]);\n        $filter = new IndexFilter($token);\n\n        self::assertSame(\n            ['first', 'third'],\n            $filter->filter(['first', 'second', 'third'])\n        );\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testSingleIndexWorksForObjectsAndArrayAccess(): void\n    {\n        $token = new JSONPathToken(TokenType::Index, 'prop');\n        $filter = new IndexFilter($token);\n        $object = (object)['prop' => 5];\n\n        self::assertSame([5], $filter->filter($object));\n\n        $arrayObject = new ArrayObject(['prop' => 'value']);\n        self::assertSame(['value'], $filter->filter($arrayObject));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testWildcardReturnsValuesAndLengthReturnsCount(): void\n    {\n        $wildcard = new IndexFilter(new JSONPathToken(TokenType::Index, '*'));\n        $length = new IndexFilter(new JSONPathToken(TokenType::Index, 'length'));\n\n        $input = ['a' => 1, 'b' => 2];\n\n        self::assertSame([1, 2], $wildcard->filter($input));\n        self::assertSame([2], $length->filter($input));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testReturnsEmptyWhenKeyMissing(): void\n    {\n        $filter = new IndexFilter(new JSONPathToken(TokenType::Index, 'missing'));\n\n        self::assertSame([], $filter->filter(['present' => 1]));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testJSONPathFindOnScalarProducesEmptyCollection(): void\n    {\n        $result = new JSONPath(123)->find('$.missing');\n\n        self::assertSame([], $result->getData());\n    }\n}\n"
  },
  {
    "path": "tests/IndexesFilterTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse Flow\\JSONPath\\Filters\\IndexesFilter;\nuse Flow\\JSONPath\\JSONPathException;\nuse Flow\\JSONPath\\JSONPathToken;\nuse Flow\\JSONPath\\TokenType;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\TestCase;\n\n#[CoversClass(IndexesFilter::class)]\nclass IndexesFilterTest extends TestCase\n{\n    /**\n     * @throws JSONPathException\n     */\n    public function testReturnsSliceAndExplicitIndexes(): void\n    {\n        $token = new JSONPathToken(TokenType::Indexes, [\n            ['type' => 'slice', 'value' => ['start' => 1, 'end' => 3, 'step' => null]],\n            0,\n        ]);\n\n        $filter = new IndexesFilter($token);\n\n        self::assertSame([2, 3, 1], $filter->filter([1, 2, 3, 4]));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testSupportsQueryAndWildcard(): void\n    {\n        $token = new JSONPathToken(TokenType::Indexes, [\n            ['type' => 'query', 'value' => '@.v>1'],\n            '*',\n        ]);\n\n        $filter = new IndexesFilter($token);\n        $filter->setRootData([]);\n\n        $data = [\n            ['v' => 1],\n            ['v' => 2],\n        ];\n\n        $result = $filter->filter($data);\n\n        self::assertSame([['v' => 2], ['v' => 1], ['v' => 2]], $result);\n    }\n}\n"
  },
  {
    "path": "tests/JSONPathCoreTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse Flow\\JSONPath\\JSONPath;\nuse Flow\\JSONPath\\JSONPathException;\nuse Flow\\JSONPath\\TokenType;\nuse PHPUnit\\Framework\\TestCase;\n\nclass JSONPathCoreTest extends TestCase\n{\n    /**\n     * @throws JSONPathException\n     */\n    public function testFindAndHelpers(): void\n    {\n        $data = [\n            'list' => [\n                ['v' => 1],\n                ['v' => 2],\n                ['v' => 3],\n            ],\n            'nested' => ['inner' => ['x' => 9]],\n        ];\n\n        $path = new JSONPath($data);\n        $slice = $path->find('$.list[1:3]');\n\n        self::assertSame([['v' => 2], ['v' => 3]], $slice->getData());\n\n        $first = $path->first();\n        $last = $path->last();\n\n        self::assertSame($data['list'], $first instanceof JSONPath ? $first->getData() : $first);\n        self::assertSame(['inner' => ['x' => 9]], $last instanceof JSONPath ? $last->getData() : $last);\n        self::assertSame('list', $path->firstKey());\n        self::assertSame('nested', $path->lastKey());\n    }\n\n    public function testOffsetAccessAndIteration(): void\n    {\n        $path = new JSONPath(['child' => ['a' => 1]]);\n\n        self::assertTrue($path->offsetExists('child'));\n\n        /** @var JSONPath $child */\n        $child = $path['child'];\n\n        self::assertInstanceOf(JSONPath::class, $child);\n        self::assertSame(['a' => 1], $child->getData());\n\n        $path[] = 'appended';\n        $path['new'] = 'value';\n        unset($path['child']);\n\n        $collected = [];\n\n        foreach ($path as $key => $value) {\n            $collected[$key] = $value instanceof JSONPath ? $value->getData() : $value;\n        }\n\n        self::assertArrayHasKey(0, $collected);\n        self::assertSame('appended', $collected[0]);\n        self::assertSame('value', $collected['new']);\n        self::assertEquals(new JSONPathException('oops'), new JSONPathException('oops'));\n        self::assertSame('index', TokenType::Index->value);\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testParseTokensCachesResults(): void\n    {\n        $path = new JSONPath(['a' => ['b' => 1]]);\n        $first = $path->parseTokens('$.a.b');\n        $second = $path->parseTokens('$.a.b');\n\n        self::assertNotEmpty($first);\n        self::assertSame($first, $second);\n    }\n\n    public function testJsonSerializeAndMagicGet(): void\n    {\n        $path = new JSONPath(['a' => 1]);\n\n        self::assertSame(['a' => 1], $path->jsonSerialize());\n        self::assertSame(1, $path->__get('a'));\n        self::assertNull($path->__get('missing'));\n\n        $empty = new JSONPath([]);\n\n        self::assertNull($empty->first());\n        self::assertNull($empty->last());\n        self::assertNull($empty->firstKey());\n        self::assertNull($empty->lastKey());\n        self::assertSame(0, $empty->count());\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testFindOnScalarReturnsEmptyResult(): void\n    {\n        $result = new JSONPath(123)->find('$.missing')->getData();\n\n        self::assertSame([], $result);\n    }\n}\n"
  },
  {
    "path": "tests/JSONPathIntegrationTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse ArrayObject;\nuse Flow\\JSONPath\\JSONPath;\nuse Flow\\JSONPath\\JSONPathException;\nuse PHPUnit\\Framework\\TestCase;\n\nclass JSONPathIntegrationTest extends TestCase\n{\n    /**\n     * @throws JSONPathException\n     */\n    public function testArrayObjectTraversal(): void\n    {\n        $data = new ArrayObject([\n            'items' => new ArrayObject([\n                ['name' => 'keep', 'active' => true],\n                ['name' => 'skip', 'active' => false],\n            ]),\n        ]);\n\n        $result = new JSONPath($data)->find('$.items[?(@.active==true)]')->getData();\n\n        self::assertSame([['name' => 'keep', 'active' => true]], $result);\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testDashedIndexIsParsedWithoutQuotes(): void\n    {\n        $data = ['data' => ['dash-key' => 42, 'other' => 1]];\n\n        $result = new JSONPath($data)->find('$.data[dash-key]')->getData();\n\n        self::assertSame([42], $result);\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testSlicesResolveViaPublicApi(): void\n    {\n        $path = new JSONPath(['values' => [0, 1, 2, 3, 4]]);\n\n        self::assertSame([1, 2, 3], $path->find('$.values[1:-1]')->getData());\n        self::assertSame([4, 3], $path->find('$.values[-1:-3:-1]')->getData());\n    }\n}\n"
  },
  {
    "path": "tests/JSONPathLexerTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse Flow\\JSONPath\\JSONPathException;\nuse Flow\\JSONPath\\JSONPathLexer;\nuse Flow\\JSONPath\\TokenType;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\n\n#[CoversClass(JSONPathLexer::class)]\nclass JSONPathLexerTest extends TestCase\n{\n    /**\n     * @param list<array{type: TokenType, value: mixed, shorthand?: bool}> $expectedTokens\n     * @throws JSONPathException\n     */\n    #[DataProvider('expressionProvider')]\n    public function testParsesExpressions(string $expression, array $expectedTokens): void\n    {\n        $tokens = new JSONPathLexer($expression)->parseExpression();\n\n        self::assertCount(\\count($expectedTokens), $tokens);\n\n        foreach ($expectedTokens as $i => $expected) {\n            self::assertEquals($expected['type'], $tokens[$i]->type);\n            self::assertEquals($expected['value'], $tokens[$i]->value);\n\n            if (\\array_key_exists('shorthand', $expected)) {\n                self::assertSame($expected['shorthand'], $tokens[$i]->shorthand);\n            }\n        }\n    }\n\n    /**\n     * @return iterable<string, array{string, list<array{type: TokenType, value: mixed, shorthand?: bool}>}>\n     */\n    public static function expressionProvider(): iterable\n    {\n        yield 'wildcard index' => [\n            '.*',\n            [\n                ['type' => TokenType::Index, 'value' => '*'],\n            ],\n        ];\n\n        yield 'simple index' => [\n            '.foo',\n            [\n                ['type' => TokenType::Index, 'value' => 'foo'],\n            ],\n        ];\n\n        yield 'bare index normalizes dot prefix' => [\n            'foo',\n            [\n                ['type' => TokenType::Index, 'value' => 'foo'],\n            ],\n        ];\n\n        yield 'complex quoted index' => [\n            '[\"\\'b.^*_\"]',\n            [\n                ['type' => TokenType::Index, 'value' => \"'b.^*_\"],\n            ],\n        ];\n\n        yield 'integer index' => [\n            '[0]',\n            [\n                ['type' => TokenType::Index, 'value' => 0],\n            ],\n        ];\n\n        yield 'index after dot notation' => [\n            '.books[0]',\n            [\n                ['type' => TokenType::Index, 'value' => 'books'],\n                ['type' => TokenType::Index, 'value' => 0],\n            ],\n        ];\n\n        yield 'quoted index with whitespace' => [\n            '[   \"foo$-/\\'\"     ]',\n            [\n                ['type' => TokenType::Index, 'value' => \"foo$-/'\"],\n            ],\n        ];\n\n        yield 'slice with explicit bounds' => [\n            '[0:1:2]',\n            [\n                ['type' => TokenType::Slice, 'value' => ['start' => 0, 'end' => 1, 'step' => 2]],\n            ],\n        ];\n\n        yield 'negative index' => [\n            '[-1]',\n            [\n                ['type' => TokenType::Index, 'value' => -1],\n            ],\n        ];\n\n        yield 'slice all nulls' => [\n            '[:]',\n            [\n                ['type' => TokenType::Slice, 'value' => ['start' => null, 'end' => null, 'step' => null]],\n            ],\n        ];\n\n        yield 'shorthand query current' => [\n            '[?@]',\n            [\n                ['type' => TokenType::QueryMatch, 'value' => '@', 'shorthand' => true],\n            ],\n        ];\n\n        yield 'shorthand query comparison' => [\n            '[?@==null]',\n            [\n                ['type' => TokenType::QueryMatch, 'value' => '@==null', 'shorthand' => true],\n            ],\n        ];\n\n        yield 'shorthand query empty expression' => [\n            '[?]',\n            [\n                ['type' => TokenType::QueryMatch, 'value' => '@', 'shorthand' => true],\n            ],\n        ];\n\n        yield 'double quoted index with escape' => [\n            '$[\"a\\\\\"b\"]',\n            [\n                ['type' => TokenType::Index, 'value' => 'a\"b'],\n            ],\n        ];\n\n        yield 'union with slice and negative index' => [\n            '[-2,1:3]',\n            [\n                [\n                    'type' => TokenType::Indexes,\n                    'value' => [\n                        -2,\n                        [\n                            'type' => 'slice',\n                            'value' => ['start' => 1, 'end' => 3, 'step' => null],\n                        ],\n                    ],\n                ],\n            ],\n        ];\n\n        yield 'union with query' => [\n            '[1,?(@.foo>1)]',\n            [\n                [\n                    'type' => TokenType::Indexes,\n                    'value' => [\n                        1,\n                        [\n                            'type' => 'query',\n                            'value' => '@.foo>1',\n                        ],\n                    ],\n                ],\n            ],\n        ];\n\n        yield 'single quoted index with escapes' => [\n            \"$['back\\\\\\\\slash\\\\'quote']\",\n            [\n                ['type' => TokenType::Index, 'value' => \"back\\\\slash'quote\"],\n            ],\n        ];\n\n        yield 'multiple quoted indexes collapse to array' => [\n            '[\"first\",\"second\"]',\n            [\n                ['type' => TokenType::Index, 'value' => ['first', 'second'], 'quoted' => true],\n            ],\n        ];\n\n        yield 'empty quoted index resolves to empty string' => [\n            '[\"\"]',\n            [\n                ['type' => TokenType::Index, 'value' => '', 'quoted' => true],\n            ],\n        ];\n\n        yield 'quoted index in dot notation preserves dots' => [\n            \"$.'some.key'\",\n            [\n                ['type' => TokenType::Index, 'value' => 'some.key', 'quoted' => true],\n            ],\n        ];\n\n        yield 'empty bracket notation yields empty index list' => [\n            '$[]',\n            [\n                ['type' => TokenType::Indexes, 'value' => []],\n            ],\n        ];\n\n        yield 'empty filter expression tokenizes to empty query match' => [\n            '$[?()]',\n            [\n                ['type' => TokenType::QueryMatch, 'value' => '', 'shorthand' => false],\n            ],\n        ];\n\n        yield 'quoted index preserves brackets and dollar signs' => [\n            '$[\\'[$the.size$]\\']',\n            [\n                ['type' => TokenType::Index, 'value' => '[$the.size$]', 'quoted' => true],\n            ],\n        ];\n\n        yield 'query result expression' => [\n            '[(@.foo + 2)]',\n            [\n                ['type' => TokenType::QueryResult, 'value' => '@.foo + 2'],\n            ],\n        ];\n\n        yield 'query match' => [\n            \"[?(@['@language']='en')]\",\n            [\n                ['type' => TokenType::QueryMatch, 'value' => \"@['@language']='en'\"],\n            ],\n        ];\n\n        yield 'recursive simple' => [\n            '..foo',\n            [\n                ['type' => TokenType::Recursive, 'value' => null],\n                ['type' => TokenType::Index, 'value' => 'foo'],\n            ],\n        ];\n\n        yield 'recursive wildcard' => [\n            '..*',\n            [\n                ['type' => TokenType::Recursive, 'value' => null],\n                ['type' => TokenType::Index, 'value' => '*'],\n            ],\n        ];\n\n        yield 'indexes with whitespace' => [\n            '[ 1,2 , 3]',\n            [\n                ['type' => TokenType::Indexes, 'value' => [1, 2, 3]],\n            ],\n        ];\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testIndexBadlyFormed(): void\n    {\n        $this->expectException(JSONPathException::class);\n        $this->expectExceptionMessage('Unable to parse token hello* in expression: .hello*');\n\n        new JSONPathLexer('.hello*')->parseExpression();\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testRecursiveBadlyFormed(): void\n    {\n        $this->expectException(JSONPathException::class);\n        $this->expectExceptionMessage('Unable to parse token ba^r in expression: ..ba^r');\n\n        new JSONPathLexer('..ba^r')->parseExpression();\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testEmptyExpressionsReturnNoTokens(): void\n    {\n        self::assertSame([], new JSONPathLexer('')->parseExpression());\n        self::assertSame([], new JSONPathLexer('$')->parseExpression());\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testSingleCharacterExpressionNormalized(): void\n    {\n        self::assertSame([], new JSONPathLexer('.')->parseExpression());\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testUnclosedBracketThrowsAfterFinalFlush(): void\n    {\n        $this->expectException(JSONPathException::class);\n\n        new JSONPathLexer(\"['unterminated\")->parseExpression();\n    }\n}\n"
  },
  {
    "path": "tests/JSONPathTokenTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse Flow\\JSONPath\\Filters\\IndexesFilter;\nuse Flow\\JSONPath\\Filters\\IndexFilter;\nuse Flow\\JSONPath\\Filters\\QueryMatchFilter;\nuse Flow\\JSONPath\\Filters\\QueryResultFilter;\nuse Flow\\JSONPath\\Filters\\RecursiveFilter;\nuse Flow\\JSONPath\\Filters\\SliceFilter;\nuse Flow\\JSONPath\\JSONPathToken;\nuse Flow\\JSONPath\\TokenType;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\TestCase;\n\n#[CoversClass(JSONPathToken::class)]\nclass JSONPathTokenTest extends TestCase\n{\n    public function testBuildFilterReturnsExpectedTypes(): void\n    {\n        self::assertInstanceOf(\n            IndexFilter::class,\n            new JSONPathToken(TokenType::Index, null)->buildFilter(0)\n        );\n\n        self::assertInstanceOf(\n            IndexesFilter::class,\n            new JSONPathToken(TokenType::Indexes, [])->buildFilter(0)\n        );\n\n        self::assertInstanceOf(\n            QueryMatchFilter::class,\n            new JSONPathToken(TokenType::QueryMatch, '')->buildFilter(0)\n        );\n\n        self::assertInstanceOf(\n            QueryResultFilter::class,\n            new JSONPathToken(TokenType::QueryResult, '')->buildFilter(0)\n        );\n\n        self::assertInstanceOf(\n            RecursiveFilter::class,\n            new JSONPathToken(TokenType::Recursive, null)->buildFilter(0)\n        );\n\n        self::assertInstanceOf(\n            SliceFilter::class,\n            new JSONPathToken(TokenType::Slice, ['start' => 0, 'end' => 0, 'step' => 1])->buildFilter(0)\n        );\n    }\n\n    public function testConstructorSetsProperties(): void\n    {\n        $token = new JSONPathToken(TokenType::Index, 'value');\n\n        self::assertSame(TokenType::Index, $token->type);\n        self::assertSame('value', $token->value);\n    }\n}\n"
  },
  {
    "path": "tests/QueryMatchFilterTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse Flow\\JSONPath\\Filters\\QueryMatchFilter;\nuse Flow\\JSONPath\\JSONPath;\nuse Flow\\JSONPath\\JSONPathException;\nuse Flow\\JSONPath\\JSONPathToken;\nuse Flow\\JSONPath\\TokenType;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\nuse RuntimeException;\n\n#[CoversClass(QueryMatchFilter::class)]\nclass QueryMatchFilterTest extends TestCase\n{\n    /**\n     * @return iterable<string, array{data: mixed, expression: string, expected: array<int, mixed>}>\n     */\n    public static function filterProvider(): iterable\n    {\n        yield 'shorthand truthy filters values' => [\n            'data' => [0, 1, '', 'value', false],\n            'expression' => '$[?@]',\n            'expected' => [1 => 1, 3 => 'value'],\n        ];\n\n        yield 'negation wrapped' => [\n            'data' => [['flag' => true], ['flag' => false]],\n            'expression' => '$[?(!(@.flag==true))]',\n            'expected' => [['flag' => false]],\n        ];\n\n        yield 'negation unwrapped' => [\n            'data' => [['flag' => true], ['flag' => false]],\n            'expression' => '$[?(!@.flag==true)]',\n            'expected' => [['flag' => false]],\n        ];\n\n        yield 'grouped logical expressions' => [\n            'data' => [\n                ['active' => true, 'score' => 1],\n                ['active' => true, 'score' => 2],\n                ['active' => false, 'score' => 3],\n            ],\n            'expression' => '$[?(@.active==true && (@.score>1))]',\n            'expected' => [['active' => true, 'score' => 2]],\n        ];\n\n        yield 'path comparison current and root' => [\n            'data' => [\n                'threshold' => 5,\n                'items' => [\n                    ['v' => 5, 'w' => 5],\n                    ['v' => 4, 'w' => 5],\n                ],\n            ],\n            'expression' => '$.items[?(@.v==@.w && @.v==$.threshold)]',\n            'expected' => [['v' => 5, 'w' => 5]],\n        ];\n\n        yield 'missing key compared to path still evaluates' => [\n            'data' => [['foo' => 1], ['foo' => 1, 'bar' => 1]],\n            'expression' => '$[?(@.bar==@.foo)]',\n            'expected' => [['foo' => 1, 'bar' => 1]],\n        ];\n\n        yield 'dot separated key resolves through jsonpath' => [\n            'data' => [\n                ['nested' => ['value' => 3]],\n                ['nested' => ['value' => 4]],\n            ],\n            'expression' => '$[?(@.nested.value==3)]',\n            'expected' => [['nested' => ['value' => 3]]],\n        ];\n\n        yield 'deep equal lists and objects' => [\n            'data' => [\n                ['left' => [1, 2], 'right' => [1, 2]],\n                ['left' => [1, 2], 'right' => [2, 1]],\n                ['left' => (object)['a' => 1, 'b' => 2], 'right' => (object)['b' => 2, 'a' => 1]],\n                ['left' => (object)['a' => 1], 'right' => (object)['a' => 2]],\n            ],\n            'expression' => '$[?(@.left==@.right)]',\n            'expected' => [\n                ['left' => [1, 2], 'right' => [1, 2]],\n                ['left' => (object)['a' => 1, 'b' => 2], 'right' => (object)['b' => 2, 'a' => 1]],\n            ],\n        ];\n\n        yield 'plain node selection compares current node' => [\n            'data' => [0, 1, 2],\n            'expression' => '$[?(@==@)]',\n            'expected' => [0, 1, 2],\n        ];\n\n        yield 'deep equal failure branches' => [\n            'data' => [\n                ['left' => [1, 2], 'right' => ['a' => 1, 'b' => 2]],\n                ['left' => [1], 'right' => [1, 2]],\n            ],\n            'expression' => '$[?(@.left==@.right)]',\n            'expected' => [],\n        ];\n\n        yield 'regex comparison' => [\n            'data' => ['foo', 'bar'],\n            'expression' => '$[?(@ =~ /fo.*/)]',\n            'expected' => ['foo'],\n        ];\n\n        yield 'in operator' => [\n            'data' => [1, 2, 3],\n            'expression' => '$[?(@ in [1,3])]',\n            'expected' => [1, 3],\n        ];\n\n        yield 'nin operator with short circuit or' => [\n            'data' => [\n                ['a' => 1],\n                ['b' => 1],\n                ['a' => 3],\n            ],\n            'expression' => '$[?(@.a nin [2,3] || @.b==1)]',\n            'expected' => [\n                ['a' => 1],\n                ['b' => 1],\n            ],\n        ];\n\n        yield 'existence check without operator' => [\n            'data' => [\n                ['value' => 1],\n                ['other' => 2],\n            ],\n            'expression' => '$[?(@.value)]',\n            'expected' => [\n                ['value' => 1],\n            ],\n        ];\n\n        yield '!in operator' => [\n            'data' => [1, 2, 3],\n            'expression' => '$[?(@ !in [2])]',\n            'expected' => [1, 3],\n        ];\n\n        yield 'less than comparison' => [\n            'data' => [['n' => 1], ['n' => 3]],\n            'expression' => '$[?(@.n<2)]',\n            'expected' => [['n' => 1]],\n        ];\n\n        yield 'less or equal comparison' => [\n            'data' => [['n' => 1], ['n' => 2], ['n' => 3]],\n            'expression' => '$[?(@.n<=2)]',\n            'expected' => [['n' => 1], ['n' => 2]],\n        ];\n\n        yield 'greater or equal comparison' => [\n            'data' => [['n' => 1], ['n' => 2], ['n' => 3]],\n            'expression' => '$[?(@.n>=2)]',\n            'expected' => [['n' => 2], ['n' => 3]],\n        ];\n\n        yield 'not equals comparison' => [\n            'data' => [['value' => 1], ['value' => 2]],\n            'expression' => '$[?(@.value!=2)]',\n            'expected' => [['value' => 1]],\n        ];\n    }\n\n    /**\n     * @param array<int, mixed> $expected\n     * @throws JSONPathException\n     */\n    #[DataProvider('filterProvider')]\n    public function testFilterScenarios(mixed $data, string $expression, array $expected): void\n    {\n        $result = new JSONPath($data)->find($expression)->getData();\n\n        self::assertEquals(\\array_values($expected), \\array_values($result));\n    }\n\n    /**\n     * @return iterable<string, array{expression: string, expectMatch: bool}>\n     */\n    public static function constantExpressionProvider(): iterable\n    {\n        yield 'num comparison true' => ['expression' => '[?(1<2)]', 'expectMatch' => true];\n        yield 'num comparison false' => ['expression' => '[?(2>3)]', 'expectMatch' => false];\n        yield 'num with leading zeros decoded as number' => ['expression' => '[?(0123==123)]', 'expectMatch' => true];\n        yield 'string literal decoding' => ['expression' => '[?(foo==foo)]', 'expectMatch' => true];\n        yield 'invalid less than comparison for non-scalars' => ['expression' => '[?([]<1)]', 'expectMatch' => false];\n        yield 'not equals' => ['expression' => '[?(2!=3)]', 'expectMatch' => true];\n        yield 'less or equal' => ['expression' => '[?(2<=2)]', 'expectMatch' => true];\n        yield 'greater or equal' => ['expression' => '[?(1>=2)]', 'expectMatch' => false];\n        yield 'json literal deep equal' => ['expression' => '[?({\"a\":1}=={\"a\":1})]', 'expectMatch' => true];\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    #[DataProvider('constantExpressionProvider')]\n    public function testConstantExpressions(string $expression, bool $expectMatch): void\n    {\n        $data = ['keep'];\n        $result = new JSONPath($data)->find('$' . $expression)->getData();\n\n        self::assertSame($expectMatch ? ['keep'] : [], $result);\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testShorthandTokenValueArrayFiltersTruthyNodes(): void\n    {\n        $token = new JSONPathToken(TokenType::QueryMatch, ['expression' => '@', 'shorthand' => true]);\n        $filter = new QueryMatchFilter($token);\n\n        $collection = [0, 1, '', 'value', false];\n\n        self::assertSame([1 => 1, 3 => 'value'], $filter->filter($collection));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testMalformedFilterThrowsRuntimeException(): void\n    {\n        $this->expectException(RuntimeException::class);\n        $this->expectExceptionMessage('Malformed filter query');\n\n        new JSONPath([1])->find('$[?(foo)]');\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testLiteralOnlyFilterExpressionsReturnWholeCollectionOrEmpty(): void\n    {\n        $data = [1, 2, 3];\n\n        self::assertSame($data, new JSONPath($data)->find('$[?(true)]')->getData());\n        self::assertSame([], new JSONPath($data)->find('$[?(false)]')->getData());\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testLogicalExpressionsWithLiteralRightOperand(): void\n    {\n        $data = [\n            ['key' => 1],\n            ['key' => -1],\n        ];\n\n        self::assertSame(\n            [],\n            new JSONPath($data)->find('$[?(@.key>0 && false)]')->getData()\n        );\n        self::assertSame(\n            $data,\n            new JSONPath($data)->find('$[?(@.key>0 || true)]')->getData()\n        );\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testEmptyFilterExpressionReturnsEmpty(): void\n    {\n        self::assertSame([], new JSONPath([1, 2])->find('$[?()]')->getData());\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testNormalizeKeyCastsNumericStrings(): void\n    {\n        $token = new JSONPathToken(TokenType::QueryMatch, '@[\"2\"]==\"two\"');\n        $filter = new QueryMatchFilter($token);\n\n        $result = $filter->filter([\n            ['2' => 'two', '1' => 'one'],\n            ['2' => 'nope', '1' => 'one'],\n        ]);\n\n        self::assertSame([['2' => 'two', '1' => 'one']], \\array_values($result));\n    }\n}\n"
  },
  {
    "path": "tests/QueryResultFilterTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse Flow\\JSONPath\\Filters\\QueryResultFilter;\nuse Flow\\JSONPath\\JSONPathException;\nuse Flow\\JSONPath\\JSONPathToken;\nuse Flow\\JSONPath\\TokenType;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\n\n#[CoversClass(QueryResultFilter::class)]\nclass QueryResultFilterTest extends TestCase\n{\n    /**\n     * @throws JSONPathException\n     */\n    public function testFilterResolvesComputedIndex(): void\n    {\n        $token = new JSONPathToken(TokenType::QueryResult, '@.foo + 2');\n        $filter = new QueryResultFilter($token);\n\n        $collection = [\n            'foo' => 3,\n            5 => 'bar',\n        ];\n\n        self::assertSame(['bar'], $filter->filter($collection));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testFilterReturnsEmptyWhenLengthExceeded(): void\n    {\n        $token = new JSONPathToken(TokenType::QueryResult, '@.length + 1');\n        $filter = new QueryResultFilter($token);\n\n        $collection = ['a', 'b'];\n\n        self::assertSame([], $filter->filter($collection));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    #[DataProvider('arithmeticProvider')]\n    public function testFilterSupportsAllArithmeticOperators(string $expression, int|float $expectedIndex): void\n    {\n        $token = new JSONPathToken(TokenType::QueryResult, $expression);\n        $filter = new QueryResultFilter($token);\n\n        $collection = [\n            'value' => 4,\n            $expectedIndex => 'found',\n        ];\n\n        self::assertSame(['found'], $filter->filter($collection));\n    }\n\n    /**\n     * @return list<array{string, int|float}>\n     */\n    public static function arithmeticProvider(): array\n    {\n        return [\n            ['@.value - 1', 3],\n            ['@.value * 2', 8],\n            ['@.value / 2', 2],\n        ];\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testFilterFallsBackToLengthWhenKeyMissing(): void\n    {\n        $token = new JSONPathToken(TokenType::QueryResult, '@.length - 1');\n        $filter = new QueryResultFilter($token);\n\n        $collection = ['zero', 'one'];\n\n        self::assertSame(['one'], $filter->filter($collection));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testFilterReturnsEmptyWhenKeyNotFound(): void\n    {\n        $token = new JSONPathToken(TokenType::QueryResult, '@.missing + 1');\n        $filter = new QueryResultFilter($token);\n\n        self::assertSame([], $filter->filter(['foo' => 'bar']));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testFilterReturnsEmptyWhenComputedIndexMissing(): void\n    {\n        $token = new JSONPathToken(TokenType::QueryResult, '@.foo + 10');\n        $filter = new QueryResultFilter($token);\n\n        self::assertSame([], $filter->filter(['foo' => 1]));\n    }\n\n    /**\n     * @throws JSONPathException\n     */\n    public function testFilterReturnsEmptyWhenResultKeyMissing(): void\n    {\n        $token = new JSONPathToken(TokenType::QueryResult, '@.foo + 100');\n        $filter = new QueryResultFilter($token);\n\n        self::assertSame([], $filter->filter(['foo' => 1]));\n    }\n\n    public function testUnsupportedOperatorThrows(): void\n    {\n        $this->expectException(JSONPathException::class);\n\n        $token = new JSONPathToken(TokenType::QueryResult, '@.foo ^ 2');\n        new QueryResultFilter($token)->filter(['foo' => 1]);\n    }\n}\n"
  },
  {
    "path": "tests/RecursiveFilterTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse Flow\\JSONPath\\Filters\\RecursiveFilter;\nuse Flow\\JSONPath\\JSONPathException;\nuse Flow\\JSONPath\\JSONPathToken;\nuse Flow\\JSONPath\\TokenType;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\TestCase;\n\n#[CoversClass(RecursiveFilter::class)]\nclass RecursiveFilterTest extends TestCase\n{\n    /**\n     * @throws JSONPathException\n     */\n    public function testRecursesThroughNestedArraysAndObjects(): void\n    {\n        $token = new JSONPathToken(TokenType::Recursive, null);\n        $filter = new RecursiveFilter($token);\n\n        $nestedObject = (object)['inner' => ['value' => 3]];\n        $data = ['obj' => $nestedObject, 'scalar' => 1];\n\n        $result = $filter->filter($data);\n\n        self::assertSame($nestedObject, $result[0]['obj']);\n        self::assertSame(\n            [\n                ['inner' => ['value' => 3]],\n                ['value' => 3],\n            ],\n            \\array_slice($result, 1)\n        );\n    }\n}\n"
  },
  {
    "path": "tests/SliceFilterTest.php",
    "content": "<?php\n\n/**\n * JSONPath implementation for PHP.\n *\n * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License\n */\n\ndeclare(strict_types=1);\n\nnamespace Flow\\JSONPath\\Test;\n\nuse ArrayObject;\nuse Flow\\JSONPath\\Filters\\SliceFilter;\nuse Flow\\JSONPath\\JSONPathToken;\nuse Flow\\JSONPath\\TokenType;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse PHPUnit\\Framework\\TestCase;\n\n#[CoversClass(SliceFilter::class)]\nclass SliceFilterTest extends TestCase\n{\n    /**\n     * @param array<string, int|null> $slice\n     * @param array<array-key, mixed>|object $input\n     * @param array<int, mixed> $expected\n     */\n    #[DataProvider('sliceProvider')]\n    public function testFilterHandlesNegativeAndNullBounds(array $slice, array|object $input, array $expected): void\n    {\n        $token = new JSONPathToken(TokenType::Slice, $slice);\n        $filter = new SliceFilter($token);\n\n        self::assertSame($expected, $filter->filter($input));\n    }\n\n    /**\n     * @return iterable<string, array{array<string, int|null>, array<array-key, mixed>|object, array<int, mixed>}>\n     */\n    public static function edgeCaseProvider(): iterable\n    {\n        yield 'step zero returns empty' => [\n            ['start' => 0, 'end' => null, 'step' => 0],\n            ['a', 'b'],\n            [],\n        ];\n\n        yield 'start beyond length yields empty' => [\n            ['start' => 5, 'end' => null, 'step' => 1],\n            ['a', 'b'],\n            [],\n        ];\n\n        yield 'end beyond length clamps to length' => [\n            ['start' => 0, 'end' => 10, 'step' => 1],\n            ['a', 'b'],\n            ['a', 'b'],\n        ];\n\n        yield 'negative step with null bounds reverses' => [\n            ['start' => null, 'end' => null, 'step' => -1],\n            ['a', 'b', 'c'],\n            ['c', 'b', 'a'],\n        ];\n\n        yield 'positive step with end below zero yields empty' => [\n            ['start' => 0, 'end' => -10, 'step' => 1],\n            ['a', 'b', 'c'],\n            [],\n        ];\n\n        yield 'negative step with start far below length clamps to -1' => [\n            ['start' => -5, 'end' => null, 'step' => -1],\n            ['a', 'b', 'c'],\n            [],\n        ];\n\n        yield 'negative step with start beyond length clamps to last index' => [\n            ['start' => 10, 'end' => null, 'step' => -1],\n            ['a', 'b', 'c'],\n            ['c', 'b', 'a'],\n        ];\n\n        yield 'negative step with end beyond length clamps end' => [\n            ['start' => 1, 'end' => 10, 'step' => -1],\n            ['a', 'b', 'c'],\n            [],\n        ];\n        yield 'negative step with very negative start clamps to -1 and high end clamps to length' => [\n            ['start' => -5, 'end' => 10, 'step' => -1],\n            ['a', 'b', 'c'],\n            [],\n        ];\n        yield 'negative step with end far below zero still collects prefix' => [\n            ['start' => 1, 'end' => -10, 'step' => -1],\n            ['a', 'b', 'c'],\n            ['b', 'a'],\n        ];\n\n        yield 'non countable object yields empty' => [\n            ['start' => 0, 'end' => null, 'step' => 1],\n            (object)['a' => 1],\n            [],\n        ];\n    }\n\n    /**\n     * @param array<string, int|null> $slice\n     * @param array<array-key, mixed> $input\n     * @param array<int, mixed> $expected\n     */\n    #[DataProvider('edgeCaseProvider')]\n    public function testEdgeCases(array $slice, array|object $input, array $expected): void\n    {\n        $token = new JSONPathToken(TokenType::Slice, $slice);\n        $filter = new SliceFilter($token);\n\n        self::assertSame($expected, $filter->filter($input));\n    }\n\n    /**\n     * @return array<string, array{array<string, int|null>, array<array-key, mixed>|object, array<int, mixed>}>\n     */\n    public static function sliceProvider(): array\n    {\n        return [\n            'negative start clamps at zero' => [\n                ['start' => -10, 'end' => 2, 'step' => 1],\n                ['a', 'b', 'c'],\n                ['a', 'b'],\n            ],\n            'negative end wraps from length' => [\n                ['start' => 0, 'end' => -1, 'step' => 1],\n                ['a', 'b', 'c'],\n                ['a', 'b'],\n            ],\n            'nulls default to full length' => [\n                ['start' => null, 'end' => null, 'step' => 2],\n                ['a', 'b', 'c', 'd'],\n                ['a', 'c'],\n            ],\n            'negative step slices in reverse order' => [\n                ['start' => 2, 'end' => 0, 'step' => -1],\n                ['a', 'b', 'c'],\n                ['c', 'b'],\n            ],\n            'works with array object' => [\n                ['start' => 0, 'end' => 2, 'step' => 1],\n                new ArrayObject(['a', 'b', 'c']),\n                ['a', 'b'],\n            ],\n        ];\n    }\n}\n"
  }
]