diff --git a/.github/workflows/cpan.yaml b/.github/workflows/cpan.yaml index 9619182..b025cc7 100644 --- a/.github/workflows/cpan.yaml +++ b/.github/workflows/cpan.yaml @@ -18,3 +18,25 @@ jobs: perlcritic: uses: PerlToolsTeam/github_workflows/.github/workflows/cpan-perlcritic.yml@main + + check-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up perl + uses: shogo82148/actions-setup-perl@v1 + + - name: Perl version + run: perl -V + + - name: Configure with Makefile.PL + id: configure-with-eumm + run: perl Makefile.PL + + - name: Install Perl::Tidy + run: cpanm --notest Perl::Tidy + + - name: Check formatting + run: make check-format + diff --git a/.gitignore b/.gitignore index 2ccb3d9..40b5f90 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ pm_to_blib inc/ MYMETA.* /Protocol-Redis*.tar.gz +/README +*.bak diff --git a/.perltidyrc b/.perltidyrc new file mode 100644 index 0000000..6a9eff2 --- /dev/null +++ b/.perltidyrc @@ -0,0 +1,43 @@ +# General behavinor +--standard-error-output +--backup-and-modify-in-place + +# Indentation +--indent-columns=4 +--continuation-indentation=2 + +# Line length +--maximum-line-length=78 +--maximum-consecutive-blank-lines=4 + +# Vertical tightness +--vertical-tightness=0 + +# Horizontal tightness +--paren-tightness=2 +--brace-tightness=2 +--square-bracket-tightness=2 +--block-brace-tightness=1 + +# Stack tokens +--stack-opening-tokens +#--stack-closing-tokens + +# Other style +#--cuddled-else +--opening-token-right +--closing-token-indentation=0 + +--nospace-for-semicolon +--nooutdent-long-lines + +--want-break-before="% + - * / x != == >= <= =~ < > | & **= += *= &= <<= &&= -= /= |= >>= ||= .= %= ^= x=" + +# Comments + +# troublesome for commented out code +--nohanging-side-comments + +# block comments may only be indented if they have some space characters before the # +--indent-spaced-block-comments + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0c2aaa6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: perl -perl: - - "5.28" - - "5.26" - - "5.24" - - "5.22" - - "5.20" - - "5.18" - - "5.16" - - "5.14" - - "5.12" - - "5.10" - - "5.8" diff --git a/Changes b/Changes index f4559d0..06cffdc 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,8 @@ This file documents the revision history for Perl extension Protocol::Redis +2.0002 11.02.2026 00:00:00 + - Improve README, format code. + 2.0001 04.02.2026 00:00:00 - Test and fix encoding 0 as a double in RESP3 (Grinnz). diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP index 2dd7f3b..80c7838 100644 --- a/MANIFEST.SKIP +++ b/MANIFEST.SKIP @@ -4,6 +4,9 @@ ^Makefile$ ^\.git ^MANIFEST\. +^README.pod$ +^scripts/.* +^.perltidyrc$ \.bak$ \.swp$ MYMETA diff --git a/Makefile.PL b/Makefile.PL index 30b9985..72dffb5 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -1,8 +1,8 @@ use 5.008001; - + use strict; use warnings; - + use ExtUtils::MakeMaker; WriteMakefile( @@ -14,24 +14,50 @@ WriteMakefile( META_MERGE => { dynamic_config => 0, - 'meta-spec' => { version => 2 }, + 'meta-spec' => {version => 2}, resources => { - license => [ 'http://dev.perl.org/licenses/' ], + license => ['http://dev.perl.org/licenses/'], repository => { - type => 'git', - url => 'https://github.com/und3f/protocol-redis.git', - web => 'https://github.com/und3f/protocol-redis', + type => 'git', + url => 'https://github.com/und3f/protocol-redis.git', + web => 'https://github.com/und3f/protocol-redis', }, - bugtracker => { web => 'https://github.com/und3f/protocol-redis/issues' }, + bugtracker => + {web => 'https://github.com/und3f/protocol-redis/issues'}, + }, + prereqs => { + develop => { + requires => { + 'Pod::Readme::Brief' => 0, + 'Pod::Simple::JustPod' => 0, + } + } }, }, MIN_PERL_VERSION => '5.008001', - PREREQ_PM => { - 'Carp' => 0, + PREREQ_PM => { + 'Carp' => 0, 'Math::BigInt' => 0, - 'Test::More' => '0.94', + 'Test::More' => '0.94', }, - test => {TESTS => 't/*.t'}, - + test => {TESTS => 't/*.t'}, ); + +sub MY::postamble { + return <<'MAKEFILE'; +create_distdir: README + +README: scripts/create-readme.pl + $(PERLRUN) $< $@ + +README.pod: scripts/create-readme-pod.pl + $(PERLRUN) $< $@ + +format: + find . -iname "*.p[ml]" -o -name "*.t" | xargs perltidy + +check-format: + find . -iname "*.p[ml]" -o -name "*.t" | xargs perltidy --assert-tidy +MAKEFILE +} diff --git a/README.pod b/README.pod index f225175..5de1ac4 100644 --- a/README.pod +++ b/README.pod @@ -1,3 +1,5 @@ +=pod + =head1 NAME Protocol::Redis - Redis protocol parser/encoder with asynchronous capabilities. @@ -203,7 +205,7 @@ Get parsed message or undef. Calls callback on each parsed message. =head2 C - + my $string = $redis->encode({type => '+', data => 'test'}); $string = $redis->encode( {type => '*', data => [ @@ -217,13 +219,13 @@ Encode data into redis message. Get API version. - =head1 SUPPORT =head2 IRC #redis on irc.perl.org + =head1 DEVELOPMENT =head2 Repository @@ -259,3 +261,4 @@ Copyright (C) 2011-2026, Serhii Zasenko. This program is free software, you can redistribute it and/or modify it under the same terms as Perl 5.10. +=cut diff --git a/examples/benchmark.pl b/examples/benchmark.pl index 3793706..2e34a73 100755 --- a/examples/benchmark.pl +++ b/examples/benchmark.pl @@ -11,7 +11,7 @@ use Protocol::Redis::Faster; use Protocol::Redis::Test; -my $redis = Protocol::Redis->new(api => 1); +my $redis = Protocol::Redis->new(api => 1); my $redis_fast = Protocol::Redis::Faster->new(api => 1); $redis->on_message(sub { }); @@ -32,64 +32,72 @@ sub encode { # Encode multi-bulk $redis->encode({type => '*', data => [{type => '$', data => 'test'}]}); - $redis->encode( - { type => '*', + $redis->encode({ + type => '*', data => [ {type => '$', data => 'test1'}, {type => '$', data => 'test2'} ] } - ); + ); $redis->encode({type => '*', data => []}); $redis->encode({type => '*', data => undef}); - $redis->encode( - { type => '*', + $redis->encode({ + type => '*', data => [ {type => '$', data => 'foo'}, {type => '$', data => undef}, {type => '$', data => 'bar'} ] } - ); + ); } -cmpthese(2**17, { +cmpthese( + 2**17, { 'P:R:F encode' => sub { encode($redis_fast) }, - 'P:R encode' => sub { encode($redis) }, - }); + 'P:R encode' => sub { encode($redis) }, + } +); my $status_message = $redis->encode({type => '+', data => 'OK'}); cmpthese( - -1, - { 'P:R Status parse' => sub { $redis->parse($status_message) }, + -1, { + 'P:R Status parse' => sub { $redis->parse($status_message) }, 'P:R:F Status parse' => sub { $redis_fast->parse($status_message) }, -}); + } +); -my $a = 'cmpthese',( - -1, - { +my $a = 'cmpthese', ( + -1, { 'P:R Splitted status parse' => sub { $redis->parse("+OK"); $redis->parse("\r\n") }, 'P:R:F Splitted status parse' => sub { $redis_fast->parse("+OK"); $redis->parse("\r\n") }, } -); + ); -my $bulk_message = $redis->encode({type => '$', data => 'test'}) . $redis->encode({type => '$', data => undef}); -cmpthese(-1, { - 'Bulk parse P:R' => sub { $redis->parse($bulk_message) }, +my $bulk_message = $redis->encode({type => '$', data => 'test'}) + . $redis->encode({type => '$', data => undef}); +cmpthese( + -1, { + 'Bulk parse P:R' => sub { $redis->parse($bulk_message) }, 'Bulk parse P:R:F' => sub { $redis_fast->parse($bulk_message) }, -}); + } +); -my $mbulk_message = $redis->encode( - { type => '*', +my $mbulk_message = $redis->encode({ + type => '*', data => [{type => '$', data => 'test1'}, {type => '$', data => 'test2'}] } ); -cmpthese(-1, { - 'P:R Multi-Bulk parse' => sub { $redis->parse($mbulk_message) }, - 'P:R:F Multi-Bulk parse' => sub { $redis_fast->parse($mbulk_message) }, - }); +cmpthese( + -1, { + 'P:R Multi-Bulk parse' => sub { $redis->parse($mbulk_message) }, + 'P:R:F Multi-Bulk parse' => + sub { $redis_fast->parse($mbulk_message) }, + } +); diff --git a/lib/Protocol/Redis.pm b/lib/Protocol/Redis.pm index 037cddc..c3a9f0c 100644 --- a/lib/Protocol/Redis.pm +++ b/lib/Protocol/Redis.pm @@ -15,7 +15,9 @@ sub new { my $self = {@_}; Carp::croak(qq/Unknown Protocol::Redis API version $self->{api}/) - unless $self->{api} == 1 or $self->{api} == 2 or $self->{api} == 3; + unless $self->{api} == 1 + or $self->{api} == 2 + or $self->{api} == 3; bless $self, $class; @@ -66,22 +68,28 @@ sub _encode_resp2 { # Bulk string if ($message->{type} eq '$') { if (defined $message->{data}) { - $encoded_message .= '$' . length($message->{data}) . "\r\n" . $message->{data} . "\r\n"; + $encoded_message + .= '$' + . length($message->{data}) . "\r\n" + . $message->{data} . "\r\n"; } else { $encoded_message .= "\$-1\r\n"; } } + # Array (multi bulk) elsif ($message->{type} eq '*') { if (defined $message->{data}) { - $encoded_message .= '*' . scalar(@{$message->{data}}) . "\r\n"; + $encoded_message + .= '*' . scalar(@{$message->{data}}) . "\r\n"; unshift @_, @{$message->{data}}; } else { $encoded_message .= "*-1\r\n"; } } + # String, error, integer elsif (exists $simple_types{$message->{type}}) { $encoded_message .= $message->{type} . $message->{data} . "\r\n"; @@ -95,47 +103,52 @@ sub _encode_resp2 { } sub _parse_resp2 { - my $self = shift; - $self->{_buffer}.= shift; + my $self = shift; + $self->{_buffer} .= shift; my $message = $self->{_message} ||= {}; my $buffer = \$self->{_buffer}; - CHUNK: + CHUNK: while ((my $pos = index($$buffer, "\r\n")) != -1) { + # Check our state: are we parsing new message or completing existing if (!$message->{type}) { if ($pos < 1) { Carp::croak(qq/Unexpected input "$$buffer"/); } - $message->{type} = substr $$buffer, 0, 1; - $message->{_argument} = substr $$buffer, 1, $pos - 1; - substr $$buffer, 0, $pos + 2, ''; # Remove type + argument + \r\n + $message->{type} = substr $$buffer, 0, 1; + $message->{_argument} = substr $$buffer, 1, $pos - 1; + substr $$buffer, 0, $pos + 2, ''; # Remove type + argument + \r\n } # Simple Strings, Errors, Integers if (exists $simple_types{$message->{type}}) { $message->{data} = delete $message->{_argument}; } + # Bulk Strings elsif ($message->{type} eq '$') { if ($message->{_argument} eq '-1') { $message->{data} = undef; } elsif (length($$buffer) >= $message->{_argument} + 2) { - $message->{data} = substr $$buffer, 0, $message->{_argument}, ''; - substr $$buffer, 0, 2, ''; # Remove \r\n + $message->{data} = substr $$buffer, 0, $message->{_argument}, + ''; + substr $$buffer, 0, 2, ''; # Remove \r\n } else { - return # Wait more data + return # Wait more data } } + # Arrays elsif ($message->{type} eq '*') { if ($message->{_argument} eq '-1') { $message->{data} = undef; - } else { + } + else { $message->{data} = []; if ($message->{_argument} > 0) { $message = $self->{_message} = {_parent => $message}; @@ -143,6 +156,7 @@ sub _parse_resp2 { } } } + # Invalid input else { Carp::croak(qq/Unexpected input "$self->{_message}{type}"/); @@ -170,7 +184,7 @@ sub _parse_resp2 { } } -my %blob_types = ('$' => 1, '!' => 1, '=' => 1); +my %blob_types = ('$' => 1, '!' => 1, '=' => 1); my %aggregate_types = ('*' => 1, '%' => 1, '~' => 1, '|' => 1, '>' => 1); sub _encode_resp3 { @@ -183,59 +197,86 @@ sub _encode_resp3 { # Attributes if (defined $message->{attributes}) { my %append_message = %$message; - my $attributes = delete $append_message{attributes}; + my $attributes = delete $append_message{attributes}; $encoded_message .= '|' . keys(%$attributes) . "\r\n"; - unshift @_, (map { ({type => '$', data => $_}, $attributes->{$_}) } - sort keys %$attributes), \%append_message; + unshift @_, ( + map { ({type => '$', data => $_}, $attributes->{$_}) } + sort keys %$attributes + ), + \%append_message; } # Bulk string, Blob error, Verbatim string elsif (exists $blob_types{$message->{type}}) { my $text = $message->{data}; if ($message->{type} eq '=') { - my $format = defined $message->{format} ? $message->{format} : 'txt'; + my $format = + defined $message->{format} ? $message->{format} : 'txt'; $text = "$format:$text"; } - $encoded_message .= $message->{type} . length($text) . "\r\n" . $text . "\r\n"; + $encoded_message + .= $message->{type} . length($text) . "\r\n" . $text . "\r\n"; } + # Array, Set, Push - elsif ($message->{type} eq '*' or $message->{type} eq '~' or $message->{type} eq '>') { - $encoded_message .= $message->{type} . scalar(@{$message->{data}}) . "\r\n"; + elsif ($message->{type} eq '*' + or $message->{type} eq '~' + or $message->{type} eq '>') + { + $encoded_message + .= $message->{type} . scalar(@{$message->{data}}) . "\r\n"; unshift @_, @{$message->{data}}; } + # Map elsif ($message->{type} eq '%') { if (ref $message->{data} eq 'ARRAY') { - $encoded_message .= $message->{type} . int(@{$message->{data}} / 2) . "\r\n"; + $encoded_message + .= $message->{type} . int(@{$message->{data}} / 2) . "\r\n"; unshift @_, @{$message->{data}}; - } else { - $encoded_message .= $message->{type} . keys(%{$message->{data}}) . "\r\n"; - unshift @_, map { ({type => '$', data => $_}, $message->{data}{$_}) } - sort keys %{$message->{data}}; + } + else { + $encoded_message + .= $message->{type} . keys(%{$message->{data}}) . "\r\n"; + unshift @_, + map { ({type => '$', data => $_}, $message->{data}{$_}) } + sort keys %{$message->{data}}; } } + # String, error, integer, big number - elsif (exists $simple_types{$message->{type}} or $message->{type} eq '(') { + elsif (exists $simple_types{$message->{type}} + or $message->{type} eq '(') + { $encoded_message .= $message->{type} . $message->{data} . "\r\n"; } + # Double elsif ($message->{type} eq ',') { + # inf - if ($message->{data} != 0 and $message->{data} == $message->{data} * 2) { - $encoded_message .= ',' . ($message->{data} > 0 ? '' : '-') . "inf\r\n"; + if ( $message->{data} != 0 + and $message->{data} == $message->{data} * 2) + { + $encoded_message + .= ',' . ($message->{data} > 0 ? '' : '-') . "inf\r\n"; } + # nan elsif ($message->{data} != $message->{data}) { $encoded_message .= ",nan\r\n"; } else { - $encoded_message .= $message->{type} . $message->{data} . "\r\n"; + $encoded_message + .= $message->{type} . $message->{data} . "\r\n"; } } + # Null elsif ($message->{type} eq '_') { $encoded_message .= "_\r\n"; } + # Boolean elsif ($message->{type} eq '#') { $encoded_message .= '#' . ($message->{data} ? 't' : 'f') . "\r\n"; @@ -249,23 +290,24 @@ sub _encode_resp3 { } sub _parse_resp3 { - my $self = shift; - $self->{_buffer}.= shift; + my $self = shift; + $self->{_buffer} .= shift; my $message = $self->{_message} ||= {}; my $buffer = \$self->{_buffer}; - CHUNK: + CHUNK: while ((my $pos = index($$buffer, "\r\n")) != -1) { + # Check our state: are we parsing new message or completing existing if (!$message->{type}) { if ($pos < 1) { Carp::croak(qq/Unexpected input "$$buffer"/); } - $message->{type} = substr $$buffer, 0, 1; - $message->{_argument} = substr $$buffer, 1, $pos - 1; - substr $$buffer, 0, $pos + 2, ''; # Remove type + argument + \r\n + $message->{type} = substr $$buffer, 0, 1; + $message->{_argument} = substr $$buffer, 1, $pos - 1; + substr $$buffer, 0, $pos + 2, ''; # Remove type + argument + \r\n } # Streamed String Parts - must be checked for first @@ -279,38 +321,46 @@ sub _parse_resp3 { } elsif (length($$buffer) >= $message->{_argument} + 2) { my $streaming = delete $message->{_streaming}; - $streaming->{data} .= substr $$buffer, 0, $message->{_argument}, ''; - substr $$buffer, 0, 2, ''; # Remove \r\n + $streaming->{data} .= substr $$buffer, 0, + $message->{_argument}, ''; + substr $$buffer, 0, 2, ''; # Remove \r\n $message = $self->{_message} = {_streaming => $streaming}; next; } else { - return # Wait more data + return # Wait more data } } + # Simple Strings, Errors, Integers elsif (exists $simple_types{$message->{type}}) { $message->{data} = delete $message->{_argument}; } + # Null elsif ($message->{type} eq '_') { delete $message->{_argument}; $message->{data} = undef; } + # Booleans elsif ($message->{type} eq '#') { $message->{data} = !!(delete($message->{_argument}) eq 't'); } + # Doubles elsif ($message->{type} eq ',') { $message->{data} = delete $message->{_argument}; $message->{data} = 'nan' if $message->{data} =~ m/^[-+]?nan/i; } + # Big Numbers elsif ($message->{type} eq '(') { require Math::BigInt; - $message->{data} = Math::BigInt->new(delete $message->{_argument}); + $message->{data} = + Math::BigInt->new(delete $message->{_argument}); } + # Bulk/Blob Strings, Blob Errors, Verbatim Strings elsif (exists $blob_types{$message->{type}}) { if ($message->{type} eq '$' and $message->{_argument} eq '?') { @@ -319,21 +369,26 @@ sub _parse_resp3 { next; } elsif (length($$buffer) >= $message->{_argument} + 2) { - $message->{data} = substr $$buffer, 0, $message->{_argument}, ''; - if ($message->{type} eq '=' and $message->{data} =~ s/^(.{3})://s) { + $message->{data} = substr $$buffer, 0, $message->{_argument}, + ''; + if ( $message->{type} eq '=' + and $message->{data} =~ s/^(.{3})://s) + { $message->{format} = $1; } - substr $$buffer, 0, 2, ''; # Remove \r\n + substr $$buffer, 0, 2, ''; # Remove \r\n } else { - return # Wait more data + return # Wait more data } } + # Arrays, Maps, Sets, Attributes, Push elsif (exists $aggregate_types{$message->{type}}) { if ($message->{type} eq '%' or $message->{type} eq '|') { $message->{data} = {}; - } else { + } + else { $message->{data} = []; } @@ -341,6 +396,7 @@ sub _parse_resp3 { $message = $self->{_message} = {_parent => $message}; next; } + # Populate empty attributes for next message if we reach here if ($message->{type} eq '|') { $message->{attributes} = {}; @@ -350,11 +406,16 @@ sub _parse_resp3 { next; } } + # Streamed Aggregate End - elsif ($message->{type} eq '.' and $message->{_parent} and $message->{_parent}{_argument} eq '?') { + elsif ( $message->{type} eq '.' + and $message->{_parent} + and $message->{_parent}{_argument} eq '?') + { $message = delete $message->{_parent}; delete $message->{_elements}; } + # Invalid input else { Carp::croak(qq/Unexpected input "$self->{_message}{type}"/); @@ -365,15 +426,18 @@ sub _parse_resp3 { # Fill parents with data while (my $parent = delete $message->{_parent}) { + # Map key or value if ($parent->{type} eq '%' or $parent->{type} eq '|') { if (exists $parent->{_key}) { $parent->{_elements}++; $parent->{data}{delete $parent->{_key}} = $message; - } else { + } + else { $parent->{_key} = $message->{data}; } } + # Array or set element else { $parent->{_elements}++; @@ -381,7 +445,9 @@ sub _parse_resp3 { } # Do we need more elements? - if ($parent->{_argument} eq '?' or ($parent->{_elements} || 0) < $parent->{_argument}) { + if ($parent->{_argument} eq '?' + or ($parent->{_elements} || 0) < $parent->{_argument}) + { $message = $self->{_message} = {_parent => $parent}; next CHUNK; } @@ -394,7 +460,7 @@ sub _parse_resp3 { # Attributes apply to the following message if ($message->{type} eq '|') { - $self->{_message} = $message; + $self->{_message} = $message; $message->{attributes} = delete $message->{data}; delete $message->{type}; next CHUNK; diff --git a/lib/Protocol/Redis/Test.pm b/lib/Protocol/Redis/Test.pm index 3f378e5..ec57312 100644 --- a/lib/Protocol/Redis/Test.pm +++ b/lib/Protocol/Redis/Test.pm @@ -88,18 +88,19 @@ sub _test_version_3 { # Parsing method tests $redis->on_message(undef); + # simple string, error string, number _parse_string_ok($redis); _parse_null_ok($redis); _parse_boolean_ok($redis); _parse_double_ok($redis); _parse_big_number_ok($redis); - _parse_blob_ok($redis, '$'); # blob string - _parse_blob_ok($redis, '!'); # blob error - _parse_verbatim_ok($redis); # verbatim string - _parse_array_ok($redis, '*'); # array - _parse_array_ok($redis, '~'); # set - _parse_array_ok($redis, '>'); # push + _parse_blob_ok($redis, '$'); # blob string + _parse_blob_ok($redis, '!'); # blob error + _parse_verbatim_ok($redis); # verbatim string + _parse_array_ok($redis, '*'); # array + _parse_array_ok($redis, '~'); # set + _parse_array_ok($redis, '>'); # push _parse_map_ok($redis); _parse_attribute_ok($redis); _parse_streamed_string_ok($redis); @@ -156,7 +157,7 @@ sub _parse_string_ok { sub _parse_blob_ok { my $redis = shift; - my $type = shift || '$'; + my $type = shift || '$'; # Bulk message $redis->parse("${type}4\r\ntest\r\n"); @@ -224,13 +225,17 @@ sub _parse_verbatim_ok { $redis->parse(join("\r\n", '=8', 'txt:test', '=6', 'mkd:OK')); $redis->parse("\r\n"); is_deeply $redis->get_message, - {type => '=', data => 'test', format => 'txt'}, "two chunked verbatim string messages"; - is_deeply $redis->get_message, {type => '=', data => 'OK', format => 'mkd'}; + {type => '=', data => 'test', format => 'txt'}, + "two chunked verbatim string messages"; + is_deeply $redis->get_message, + {type => '=', data => 'OK', format => 'mkd'}; # Pipelined verbatim message $redis->parse(join("\r\n", ('=7', 'txt:ok1'), ('=7', 'txt:ok2'), '')); - is_deeply [$redis->get_message, $redis->get_message], - [{type => '=', data => 'ok1', format => 'txt'}, {type => '=', data => 'ok2', format => 'txt'}], + is_deeply [$redis->get_message, $redis->get_message], [ + {type => '=', data => 'ok1', format => 'txt'}, + {type => '=', data => 'ok2', format => 'txt'} + ], "pipelined verbatim string message"; # Binary test @@ -257,7 +262,7 @@ sub _parse_verbatim_ok { sub _parse_array_ok { my $redis = shift; - my $type = shift || '*'; + my $type = shift || '*'; # Array message $redis->parse("${type}1\r\n\$4\r\ntest\r\n"); @@ -286,14 +291,17 @@ sub _parse_array_ok { is_deeply $redis->get_message, { type => $type, - data => [ - {type => $type, data => [ - {type => '+', data => 'test1'}, - {type => '+', data => 'test2'}, - ]}, + data => [{ + type => $type, + data => [ + {type => '+', data => 'test1'}, + {type => '+', data => 'test2'}, + ] + }, {type => '+', data => 'test3'}, ] - }, "nested $type array message"; + }, + "nested $type array message"; $redis->parse("${type}0\r\n"); is_deeply $redis->get_message, @@ -358,7 +366,8 @@ sub _parse_array_ok { $redis->parse( join("\r\n", ("${type}2", '$3', 'ok1', '$3', 'ok2'), - ("${type}1", '$3', 'ok3'), '') + ("${type}1", '$3', 'ok3'), + '') ); is_deeply $redis->get_message, { @@ -443,14 +452,14 @@ sub _parse_double_ok { my $message = $redis->get_message; is $message->{type}, ',', 'double message type'; cmp_ok $message->{data}, '==', $message->{data} + 1, 'received infinity'; - cmp_ok $message->{data}, '>', 0, 'received positive infinity'; + cmp_ok $message->{data}, '>', 0, 'received positive infinity'; # Negative infinity message $redis->parse(",-inf\r\n"); $message = $redis->get_message; is $message->{type}, ',', 'double message type'; cmp_ok $message->{data}, '==', $message->{data} - 1, 'received infinity'; - cmp_ok $message->{data}, '<', 0, 'received negative infinity'; + cmp_ok $message->{data}, '<', 0, 'received negative infinity'; # NaN message $redis->parse(",nan\r\n"); @@ -514,12 +523,15 @@ sub _parse_map_ok { $redis->parse("%3\r\n\$5\r\ntest1\r\n"); $redis->parse(":1\r\n+test2\r\n,0.01\r\n:3\r\n_\r\n"); - is_deeply $redis->get_message, - {type => '%', data => { - test1 => {type => ':', data => '1'}, - test2 => {type => ',', data => '0.01'}, - '3' => {type => '_', data => undef}, - }}, 'complex map message'; + is_deeply $redis->get_message, { + type => '%', + data => { + test1 => {type => ':', data => '1'}, + test2 => {type => ',', data => '0.01'}, + '3' => {type => '_', data => undef}, + } + }, + 'complex map message'; } sub _parse_set_ok { @@ -528,14 +540,17 @@ sub _parse_set_ok { # Set message $redis->parse("~5\r\n+test1\r\n\$5\r\ntest2\r\n:42\r\n#t\r\n_\r\n"); - is_deeply $redis->get_message, - {type => '~', data => [ - {type => '+', data => 'test1'}, - {type => '$', data => 'test2'}, - {type => ':', data => '42'}, - {type => '#', data => !!1}, - {type => '_', data => undef}, - ]}, 'set message'; + is_deeply $redis->get_message, { + type => '~', + data => [ + {type => '+', data => 'test1'}, + {type => '$', data => 'test2'}, + {type => ':', data => '42'}, + {type => '#', data => !!1}, + {type => '_', data => undef}, + ] + }, + 'set message'; # Empty set message $redis->parse("~0\r\n"); @@ -547,29 +562,35 @@ sub _parse_set_ok { # Set message with duplicates $redis->parse("~3\r\n+test1\r\n+test2\r\n+test1\r\n"); - is_deeply $redis->get_message, - {type => '~', data => [ - {type => '+', data => 'test1'}, - {type => '+', data => 'test2'}, - {type => '+', data => 'test1'}, - ]}, 'set message with duplicates'; + is_deeply $redis->get_message, { + type => '~', + data => [ + {type => '+', data => 'test1'}, + {type => '+', data => 'test2'}, + {type => '+', data => 'test1'}, + ] + }, + 'set message with duplicates'; # Set message with non-duplicate but equal strings $redis->parse("~8\r\n+test1\r\n\$5\r\ntest1\r\n"); $redis->parse(":1\r\n,1\r\n#t\r\n"); $redis->parse("_\r\n+\r\n\$0\r\n\r\n"); - is_deeply $redis->get_message, - {type => '~', data => [ - {type => '+', data => 'test1'}, - {type => '$', data => 'test1'}, - {type => ':', data => '1'}, - {type => ',', data => '1'}, - {type => '#', data => !!1}, - {type => '_', data => undef}, - {type => '+', data => ''}, - {type => '$', data => ''}, - ]}, 'set message with non-duplicate but equal strings'; + is_deeply $redis->get_message, { + type => '~', + data => [ + {type => '+', data => 'test1'}, + {type => '$', data => 'test1'}, + {type => ':', data => '1'}, + {type => ',', data => '1'}, + {type => '#', data => !!1}, + {type => '_', data => undef}, + {type => '+', data => ''}, + {type => '$', data => ''}, + ] + }, + 'set message with non-duplicate but equal strings'; } sub _parse_attribute_ok { @@ -579,23 +600,29 @@ sub _parse_attribute_ok { $redis->parse("|1\r\n+total\r\n:5\r\n*2\r\n"); $redis->parse("\$5\r\ntest1\r\n\$5\r\ntest2\r\n"); - is_deeply $redis->get_message, - {type => '*', data => [ - {type => '$', data => 'test1'}, - {type => '$', data => 'test2'}, - ], attributes => { - total => {type => ':', data => '5'}, - }}, 'message with attributes'; + is_deeply $redis->get_message, { + type => '*', + data => + [{type => '$', data => 'test1'}, {type => '$', data => 'test2'},], + attributes => { + total => {type => ':', data => '5'}, + } + }, + 'message with attributes'; # Multiple attributes $redis->parse("|3\r\n+x\r\n,0.5\r\n+y\r\n,-3.4\r\n+z\r\n:42\r\n+OK\r\n"); - is_deeply $redis->get_message, - {type => '+', data => 'OK', attributes => { - x => {type => ',', data => '0.5'}, - y => {type => ',', data => '-3.4'}, - z => {type => ':', data => '42'}, - }}, 'message with multiple attributes'; + is_deeply $redis->get_message, { + type => '+', + data => 'OK', + attributes => { + x => {type => ',', data => '0.5'}, + y => {type => ',', data => '-3.4'}, + z => {type => ':', data => '42'}, + } + }, + 'message with multiple attributes'; # Empty attributes $redis->parse("|0\r\n-ERR no response\r\n"); @@ -608,26 +635,44 @@ sub _parse_attribute_ok { $redis->parse("*2\r\n|2\r\n+min\r\n:0\r\n+max\r\n:10\r\n:5\r\n"); $redis->parse("|2\r\n+min\r\n:4\r\n+max\r\n:8\r\n:7\r\n"); - is_deeply $redis->get_message, - {type => '*', data => [ - {type => ':', data => '5', attributes => { - min => {type => ':', data => '0'}, - max => {type => ':', data => '10'}, - }}, - {type => ':', data => '7', attributes => { - min => {type => ':', data => '4'}, - max => {type => ':', data => '8'}, - }}, - ]}, 'message with embedded attributes'; + is_deeply $redis->get_message, { + type => '*', + data => [{ + type => ':', + data => '5', + attributes => { + min => {type => ':', data => '0'}, + max => {type => ':', data => '10'}, + } + }, { + type => ':', + data => '7', + attributes => { + min => {type => ':', data => '4'}, + max => {type => ':', data => '8'}, + } + }, + ] + }, + 'message with embedded attributes'; # Aggregate attributes $redis->parse("|1\r\n+array\r\n*2\r\n+test1\r\n+test2\r\n+test\r\n"); - is_deeply $redis->get_message, - {type => '+', data => 'test', attributes => {array => {type => '*', data => [ - {type => '+', data => 'test1'}, - {type => '+', data => 'test2'}, - ]}}}, 'message with aggregate attribute values'; + is_deeply $redis->get_message, { + type => '+', + data => 'test', + attributes => { + array => { + type => '*', + data => [ + {type => '+', data => 'test1'}, + {type => '+', data => 'test2'}, + ] + } + } + }, + 'message with aggregate attribute values'; } sub _parse_push_ok { @@ -636,11 +681,11 @@ sub _parse_push_ok { # Simple push message $redis->parse(">2\r\n\$5\r\ntest1\r\n:42\r\n"); - is_deeply $redis->get_message, - {type => '>', data => [ - {type => '$', data => 'test1'}, - {type => ':', data => '42'}, - ]}, 'simple push message'; + is_deeply $redis->get_message, { + type => '>', + data => [{type => '$', data => 'test1'}, {type => ':', data => '42'},] + }, + 'simple push message'; # Empty push message $redis->parse(">0\r\n"); @@ -669,32 +714,39 @@ sub _parse_streamed_string_ok { # Charwise streamed string $redis->parse("\$?\r\n"); - $redis->parse(";1\r\n$_\r\n") for 'a'..'z'; + $redis->parse(";1\r\n$_\r\n") for 'a' .. 'z'; $redis->parse(";0\r\n"); is_deeply $redis->get_message, - {type => '$', data => join('', 'a'..'z')}, + {type => '$', data => join('', 'a' .. 'z')}, 'string streamed by character'; } sub _parse_streamed_aggregate_ok { my $redis = shift; - my $type = shift || '*'; + my $type = shift || '*'; # Simple streamed aggregate $redis->parse("${type}?\r\n+test1\r\n+test2\r\n.\r\n"); if ($type eq '%') { - is_deeply $redis->get_message, - {type => $type, data => { - test1 => {type => '+', data => 'test2'}, - }}, "simple $type streamed aggregate message"; - } else { - is_deeply $redis->get_message, - {type => $type, data => [ - {type => '+', data => 'test1'}, - {type => '+', data => 'test2'}, - ]}, "simple $type streamed aggregate message"; + is_deeply $redis->get_message, { + type => $type, + data => { + test1 => {type => '+', data => 'test2'}, + } + }, + "simple $type streamed aggregate message"; + } + else { + is_deeply $redis->get_message, { + type => $type, + data => [ + {type => '+', data => 'test1'}, + {type => '+', data => 'test2'}, + ] + }, + "simple $type streamed aggregate message"; } # Empty streamed aggregate @@ -707,32 +759,44 @@ sub _parse_streamed_aggregate_ok { # Complex streamed aggregate $redis->parse("${type}?\r\n"); $redis->parse("\$?\r\n;4\r\ntest\r\n;1\r\n1\r\n;0\r\n"); - $redis->parse("\$5\r\ntest$_\r\n") for 2..9; + $redis->parse("\$5\r\ntest$_\r\n") for 2 .. 9; $redis->parse("*?\r\n:10\r\n,11\r\n.\r\n"); $redis->parse(".\r\n"); if ($type eq '%') { - is_deeply $redis->get_message, - {type => $type, data => { - test1 => {type => '$', data => 'test2'}, - test3 => {type => '$', data => 'test4'}, - test5 => {type => '$', data => 'test6'}, - test7 => {type => '$', data => 'test8'}, - test9 => {type => '*', data => [ - {type => ':', data => '10'}, - {type => ',', data => '11'}, - ]}, - }}, "complex $type streamed aggregate message"; - } else { - is_deeply $redis->get_message, - {type => $type, data => [ - {type => '$', data => 'test1'}, - (map {+{type => '$', data => "test$_"}} 2..9), - {type => '*', data => [ - {type => ':', data => '10'}, - {type => ',', data => '11'}, - ]}, - ]}, "complex $type streamed aggregate message"; + is_deeply $redis->get_message, { + type => $type, + data => { + test1 => {type => '$', data => 'test2'}, + test3 => {type => '$', data => 'test4'}, + test5 => {type => '$', data => 'test6'}, + test7 => {type => '$', data => 'test8'}, + test9 => { + type => '*', + data => [ + {type => ':', data => '10'}, + {type => ',', data => '11'}, + ] + }, + } + }, + "complex $type streamed aggregate message"; + } + else { + is_deeply $redis->get_message, { + type => $type, + data => [ + {type => '$', data => 'test1'}, + (map { +{type => '$', data => "test$_"} } 2 .. 9), { + type => '*', + data => [ + {type => ':', data => '10'}, + {type => ',', data => '11'}, + ] + }, + ] + }, + "complex $type streamed aggregate message"; } } @@ -827,8 +891,7 @@ sub _encode_v3_ok { my $redis = shift; # Encode simple RESP3 types - is $redis->encode({type => '_', data => undef}), "_\r\n", - 'encode null'; + is $redis->encode({type => '_', data => undef}), "_\r\n", 'encode null'; is $redis->encode({type => '#', data => 1}), "#t\r\n", 'encode boolean true'; @@ -850,13 +913,20 @@ sub _encode_v3_ok { is $redis->encode({type => ',', data => -sin 9**9**9}), ",nan\r\n", 'encode nan'; - is $redis->encode({type => '(', - data => '3492890328409238509324850943850943825024385'}), + is $redis->encode({ + type => '(', + data => '3492890328409238509324850943850943825024385' + } + ), "(3492890328409238509324850943850943825024385\r\n", 'encode big number'; require Math::BigInt; - is $redis->encode({type => '(', - data => Math::BigInt->new('-3492890328409238509324850943850943825024385')}), + is $redis->encode({ + type => '(', + data => Math::BigInt->new( + '-3492890328409238509324850943850943825024385') + } + ), "(-3492890328409238509324850943850943825024385\r\n", 'encode bigint as big number'; is $redis->encode({type => '(', data => '0'}), "(0\r\n", @@ -876,34 +946,67 @@ sub _encode_v3_ok { "=7\r\ntxt:\0\r\n\r\n", 'encode binary verbatim string'; # Encode aggregate RESP3 types - is $redis->encode({type => '%', data => {foo => {type => '+', data => 'bar'}}}), + is $redis->encode( + {type => '%', data => {foo => {type => '+', data => 'bar'}}}), join("\r\n", '%1', '$3', 'foo', '+bar', ''), 'encode map'; - is $redis->encode({type => '~', data => [{type => ':', data => 5}, - {type => '+', data => 'test'}]}), + is $redis->encode({ + type => '~', + data => [{type => ':', data => 5}, {type => '+', data => 'test'}] + } + ), join("\r\n", '~2', ':5', '+test', ''), 'encode set'; - is $redis->encode({type => '>', data => [{type => '+', data => 'test'}, - {type => ',', data => '4.2'}]}), + is $redis->encode({ + type => '>', + data => + [{type => '+', data => 'test'}, {type => ',', data => '4.2'}] + } + ), join("\r\n", '>2', '+test', ',4.2', ''), 'encode push'; # Encode attributes - is $redis->encode({type => '+', data => 'test', - attributes => {foo => {type => '+', data => 'bar'}}}), + is $redis->encode({ + type => '+', + data => 'test', + attributes => {foo => {type => '+', data => 'bar'}} + } + ), join("\r\n", '|1', '$3', 'foo', '+bar', '+test', ''), 'encode simple string with attributes'; - is $redis->encode({type => '*', data => [{type => '_', data => undef}], - attributes => {test => {type => '#', data => 1}}}), + is $redis->encode({ + type => '*', + data => [{type => '_', data => undef}], + attributes => {test => {type => '#', data => 1}} + } + ), join("\r\n", '|1', '$4', 'test', '#t', '*1', '_', ''), 'encode array with attributes'; - is $redis->encode({type => '~', data => [{type => ',', data => '-5.5', - attributes => {precision => {type => ':', data => 1}}}]}), + is $redis->encode({ + type => '~', + data => [{ + type => ',', + data => '-5.5', + attributes => {precision => {type => ':', data => 1}} + } + ] + } + ), join("\r\n", '~1', '|1', '$9', 'precision', ':1', ',-5.5', ''), 'encode set with embedded attributes'; - is $redis->encode({type => '+', data => 'test', - attributes => {array => {type => '*', - data => [{type => ':', data => 1}, {type => ':', data => 2}]}}}), + is $redis->encode({ + type => '+', + data => 'test', + attributes => { + array => { + type => '*', + data => + [{type => ':', data => 1}, {type => ':', data => 2}] + } + } + } + ), join("\r\n", '|1', '$5', 'array', '*2', ':1', ':2', '+test', ''), 'encode array attributes'; } diff --git a/scripts/create-readme-pod.pl b/scripts/create-readme-pod.pl new file mode 100644 index 0000000..8b4edb0 --- /dev/null +++ b/scripts/create-readme-pod.pl @@ -0,0 +1,28 @@ +#!/usr/bin/env perl + +use Pod::Simple::JustPod; + +$PROTOCOL_REDIS_PM = "lib/Protocol/Redis.pm"; + +sub create_pod_readme($) { + my $readme_out = $_[0]; + + my $parser = Pod::Simple::JustPod->new(); + + open my $fh, ">$readme_out" or die "Can't write to $readme_out $!"; + $parser->output_fh($fh); + + $parser->parse_file($PROTOCOL_REDIS_PM); + + close $fh or die "Can't close $outfile: $!"; +} + +if (scalar(@ARGV) < 1) { + print STDERR "Usage: $0 README.pod\n"; + exit(2); +} + +print "Write $readme_out... "; +create_pod_readme $ARGV[0]; +print "Done.\n"; + diff --git a/scripts/create-readme.pl b/scripts/create-readme.pl new file mode 100644 index 0000000..38607fc --- /dev/null +++ b/scripts/create-readme.pl @@ -0,0 +1,28 @@ +#!/usr/bin/env perl + +use Pod::Readme::Brief; + +$PROTOCOL_REDIS_PM = "lib/Protocol/Redis.pm"; + +sub create_plaintext_readme($) { + my $out_filename = $_[0]; + + my $readme = Pod::Readme::Brief->new( + do { open my $fh, '<', $PROTOCOL_REDIS_PM or die $!; readline $fh; } + ); + + open(my $fh, '>', $out_filename) + or die "Could not open '$out_filename': $!"; + print $fh $readme->render(installer => 'eumm'); + close($fh); +} + +if (scalar(@ARGV) < 1) { + print STDERR "Usage: $0 README\n"; + exit(2); +} + +my $readme_out = $ARGV[0]; +print "Write plaintext readme '$readme_out'... "; +create_plaintext_readme($readme_out); +print "Done.\n";