diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..658aad0 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/alpine +{ + "name": "Alpine", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/base:alpine-3.22", + "features": { + "ghcr.io/haukex/devcontainer-features/perl:1": {} + }, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "cpanm --installdeps ." + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89ecc92..f359eee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,33 +4,23 @@ on: pull_request: branches: - master - push: branches: - master - jobs: build: - name: ${{ matrix.perl }} - runs-on: ubuntu-latest - + runs-on: ${{ matrix.os }} strategy: - fail-fast: false matrix: - perl: [ - '5.10', '5.12', '5.14', '5.16', '5.18', '5.20', '5.22', '5.24', - '5.26', '5.28', '5.30', '5.32', '5.34' - ] - + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + perl: ["5.38", "5.36", "5.34"] + name: Perl ${{ matrix.perl }} on ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - - uses: shogo82148/actions-setup-perl@v1 + - uses: actions/checkout@v6 + - name: Set up perl + uses: shogo82148/actions-setup-perl@v1 with: perl-version: ${{ matrix.perl }} - - - run: perl -v - - - run: cpanm --cpanfile cpanfile --installdeps --notest . - - - run: prove -l + install-modules-with: cpanm + install-modules-args: --with-develop --with-configure + - run: prove -lv t \ No newline at end of file diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml new file mode 100644 index 0000000..4860398 --- /dev/null +++ b/.github/workflows/workflow-test.yml @@ -0,0 +1,41 @@ +name: Workflow test +on: + pull_request: + branches: + - master + push: + branches: + - master +jobs: + perl-versions: + runs-on: ubuntu-latest + name: List Perl versions + outputs: + perl-versions: ${{ steps.action.outputs.perl-versions }} + steps: + - id: action + uses: perl-actions/perl-versions@v1 + with: + since-perl: v5.10 + with-devel: true + + perl_tester: + runs-on: ubuntu-latest + name: "Perl ${{ matrix.perl-version }}" + needs: [perl-versions] + + strategy: + fail-fast: false + matrix: + perl-version: ${{ fromJson (needs.perl-versions.outputs.perl-versions) }} + + container: perldocker/perl-tester:${{ matrix.perl-version }} + + steps: + - uses: actions/checkout@v6 + - name: uses install-with-cpanm + uses: perl-actions/install-with-cpanm@v1 + with: + cpanfile: "cpanfile" + sudo: false + - run: prove -lvr t \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..50ae7d1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "perlnavigator.includePaths": [ + "/home/vscode/perl5/lib", + "/home/vscode/perl5/lib/perl5" + ] +} \ No newline at end of file diff --git a/cpanfile b/cpanfile index 85eb5be..a1078a9 100644 --- a/cpanfile +++ b/cpanfile @@ -1,12 +1,14 @@ requires 'Carp::Assert'; requires 'Exporter::Tiny'; +requires 'Exporter::Shiny'; requires 'JSON::MaybeXS'; requires 'LV'; requires 'List::Util' => '1.45'; # For uniq. requires 'Readonly'; requires 'Try::Tiny'; +requires 'Class::Accessor::Fast'; requires 'perl' => '5.010'; on test => sub { - requires 'Test2::V0'; + requires 'Test2::V1'; }; diff --git a/dist.ini b/dist.ini index b1e1aef..7831085 100644 --- a/dist.ini +++ b/dist.ini @@ -5,7 +5,7 @@ copyright_holder = Aurelia Peters copyright_year = 2024 ; authordep Pod::Elemental::Transformer::List -version = 1.0.5 +version = 1.0.6 [Prereqs::FromCPANfile] [@Basic] [MetaJSON] diff --git a/lib/JSON/Path.pm b/lib/JSON/Path.pm index 141163a..a0a0892 100644 --- a/lib/JSON/Path.pm +++ b/lib/JSON/Path.pm @@ -18,13 +18,13 @@ use LV (); use overload '""' => \&to_string; sub jpath { - my ( $object, $expression ) = @_; - my @return = __PACKAGE__->new($expression)->values($object); + my ( $object, $expression, %args ) = @_; + return __PACKAGE__->new($expression)->values($object, %args); } sub jpath1 : lvalue { my ( $object, $expression ) = @_; - __PACKAGE__->new($expression)->value($object); + return __PACKAGE__->new($expression)->value($object); } sub jpath_map (&$$) { @@ -97,10 +97,10 @@ sub value : lvalue { } sub values { - my ( $self, $object ) = @_; + my ( $self, $object, %args ) = @_; croak q{non-safe evaluation, died} if "$self" =~ /\?\(/ && $JSON::Path::Safe; - return JSON::Path::Evaluator::evaluate_jsonpath( $object, "$self", script_engine => 'perl' ); + return JSON::Path::Evaluator::evaluate_jsonpath( $object, "$self", %args, script_engine => 'perl' ); } sub map { @@ -391,7 +391,7 @@ Copyright 2007 Stefan Goessner. Copyright 2010-2013 Toby Inkster. -Copyright 2021-2024 Aurelia Peters +Copyright 2021-2026 Aurelia Peters This module is tri-licensed. It is available under the X11 (a.k.a. MIT) licence; you can also redistribute it and/or modify it under the same diff --git a/lib/JSON/Path/Evaluator.pm b/lib/JSON/Path/Evaluator.pm index 6d062b1..af1adc5 100644 --- a/lib/JSON/Path/Evaluator.pm +++ b/lib/JSON/Path/Evaluator.pm @@ -146,7 +146,7 @@ sub evaluate_jsonpath { _calling_context => wantarray ? 'ARRAY' : 'SCALAR', %args ); - return $self->evaluate( $expression, want_ref => $want_ref, want_path => $want_path ); + return $self->evaluate( $expression, want_ref => $want_ref, want_path => $want_path, debug => $args{'debug'} ); } =method evaluate @@ -189,9 +189,8 @@ sub evaluate { my $json_object = $self->{root}; - my $token_stream = [ tokenize($expression) ]; -#use Data::Dumper qw/Dumper/; -#print Dumper $token_stream; + my $token_stream = [ tokenize($expression, debug => $args{'debug'}) ]; + shift @{$token_stream} if $token_stream->[0] eq $TOKEN_ROOT; shift @{$token_stream} if $token_stream->[0] eq $TOKEN_CHILD; @@ -208,7 +207,7 @@ sub evaluate { } my @ret = $self->_evaluate( $json_object, $token_stream, $args{want_ref} ); - return @ret; + return wantarray ? @ret : $ret[0]; } sub _reftable_walker { @@ -362,7 +361,12 @@ sub _get { } else { my @indices; - if ( $index =~ /$TOKEN_ARRAY_SLICE/ ) { + # Only handle slices when all elements of the index save the slice + # operator are numeric. THIS IS A KLUDGE. By rights I should know + # that $index is a string ('foo:bar') instead of a slice operation + # (1:2 or 1:2:3) I'll fix it properly when I rework paths as a + # series of anonymous subs. -- 2026-02-14 popefelix@gmail.com + if ( $index =~ /$TOKEN_ARRAY_SLICE/ && ( ! grep { $_ && ! looks_like_number($_) } split /:/, $index)) { my $length = _hashlike($object) ? scalar values %{$object} : scalar @{$object}; @indices = _slice( $index, $length ); } diff --git a/lib/JSON/Path/Tokenizer.pm b/lib/JSON/Path/Tokenizer.pm index 1e880ef..4cbf89b 100644 --- a/lib/JSON/Path/Tokenizer.pm +++ b/lib/JSON/Path/Tokenizer.pm @@ -28,6 +28,8 @@ Readonly my %OPERATORS => ( # Take an expression and break it up into tokens sub tokenize { my $expression = shift; + my %args = @_; + #print "Tokenize \"$expression\"\n"; my $chars = [ split //, $expression ]; @@ -42,6 +44,9 @@ sub tokenize { push @tokens, _read_to_filter_script_close($chars); } } + if ( $args{'debug'} ) { + print STDERR qq([DEBUG] Tokens: ) . join( ', ', map { qq("$_") } @tokens ) . qq(\n); + } return @tokens; } @@ -61,20 +66,20 @@ sub _read_to_filter_script_close { "\\" => "\x{005C}", ); - #print "$invocation: read to filter script close: " . join( '', @{$chars} ) . "\n"; +#print "$invocation: read to filter script close: " . join( '', @{$chars} ) . "\n"; my $filter; my $in_regex; my $in_quote; my $escape = 0; - my @quote_chars = ($APOSTROPHE, $QUOTATION_MARK); + my @quote_chars = ( $APOSTROPHE, $QUOTATION_MARK ); my @regex_chars = "/"; while ( defined( my $char = shift @{$chars} ) ) { - if ( $in_quote ) { - if ( $escape ) { + if ($in_quote) { + if ($escape) { ## Replace \t by tab, \\ by \, etc - $char = $escaped_chars{$char} || $char; + $char = $escaped_chars{$char} || $char; $escape = 0; } elsif ( $char eq "\\" ) { @@ -87,8 +92,8 @@ sub _read_to_filter_script_close { $in_quote = ''; } } - elsif ( $in_regex ) { - if ( $escape ) { + elsif ($in_regex) { + if ($escape) { $escape = 0; } elsif ( $char eq "\\" ) { @@ -98,10 +103,10 @@ sub _read_to_filter_script_close { $in_regex = ''; } } - elsif (grep { $_ eq $char } @quote_chars) { + elsif ( grep { $_ eq $char } @quote_chars ) { $in_quote = $char; } - elsif (grep { $_ eq $char } @regex_chars) { + elsif ( grep { $_ eq $char } @regex_chars ) { $in_regex = $char; } @@ -114,6 +119,7 @@ sub _read_to_filter_script_close { } sub _read_to_next_token { + #$invocation++; my $chars = shift; @@ -123,7 +129,8 @@ sub _read_to_next_token { while ( defined( my $char = shift @{$chars} ) ) { if ( $char eq $APOSTROPHE || $char eq $QUOTATION_MARK ) { - #print "$invocation: Char is $APOSTROPHE or $QUOTATION_MARK. Char: $char, in_quote: $in_quote\n"; + +#print "$invocation: Char is $APOSTROPHE or $QUOTATION_MARK. Char: $char, in_quote: $in_quote\n"; if ( $in_quote && $in_quote eq $char ) { $in_quote = ''; last; diff --git a/t/colon-in-key.t b/t/colon-in-key.t new file mode 100644 index 0000000..4114f96 --- /dev/null +++ b/t/colon-in-key.t @@ -0,0 +1,45 @@ +use Test2::V1 -ipP; +use JSON::Path qw/jpath/; +use JSON::MaybeXS qw/decode_json/; +use Test2::Tools::Exception qw/lives try_ok/; + +subtest simple => sub { + my $json_string = q( + { + "key:one": "value1", + "key:2": "value2", + ":keythree": "value3" + } + ); + my $data_ref = decode_json($json_string); + for my $key ( keys %{$data_ref} ) { + my $expr = sprintf q($['%s']), $key; + try_ok { + my $expected = $data_ref->{$key}; + my ($got) = jpath( $data_ref, $expr, debug => $ENV{'DEBUG_TOKENIZER'} ); + is($got, $expected, qq("$expr") ); + }; + } +}; + +subtest 'not so simple' => sub { + my $json_string = q( + { + "key1": [ + { + ":key2:test": { + "parameters": "desired_value" + } + } + ] + } + ); + + my $data_ref = decode_json($json_string); + my $expr = q($['key1'][0][':key2:test']); + my $got = jpath($data_ref, $expr); + my $expected = $data_ref->{'key1'}[0]{':key2:test'}; + is($got, $expected, qq("$expr")); + done_testing; +}; +done_testing; \ No newline at end of file diff --git a/t/evaluator/tokenize.t b/t/evaluator/tokenize.t index 5c124b1..dcbe431 100644 --- a/t/evaluator/tokenize.t +++ b/t/evaluator/tokenize.t @@ -1,5 +1,6 @@ use Test2::V0; use JSON::Path::Tokenizer qw(tokenize); +use Data::Dumper; my @EXPRESSIONS = ( q{$[1:3:2].foobar} => [qw/$ [ 1:3:2 ] . foobar/], @@ -20,6 +21,8 @@ my @EXPRESSIONS = ( q{$.[*].user[?(@.login == 'laurilehmijoki')]} => [ qw/$ . [ * ] . user [?(/, q{@.login == 'laurilehmijoki'}, ')]' ], q{$.path\.one.two} => [qw/$ . path.one . two/], q{$.'path.one'.two} => [qw/$ . path.one . two/], + q{$['foo:bar']} => ['$', '[', 'foo:bar', ']'], + q($['key1'][0][':key2:test']) => ['$', '[', 'key1', ']', '[', '0', ']', '[', ':key2:test', ']',] ); for ( 0 .. ( $#EXPRESSIONS / 2 ) ) {