File Coverage

blib/lib/Async/Redis/URI.pm
Criterion Covered Total %
statement 74 78 94.8
branch 37 46 80.4
condition 7 9 77.7
subroutine 16 16 100.0
pod 2 12 16.6
total 136 161 84.4


line stmt bran cond sub pod time code
1             package Async::Redis::URI;
2              
3 1     1   167573 use strict;
  1         2  
  1         39  
4 1     1   11 use warnings;
  1         2  
  1         49  
5 1     1   18 use 5.018;
  1         4  
6              
7             our $VERSION = '0.001';
8              
9             # URL decode
10             sub _decode {
11 17     17   33 my ($str) = @_;
12 17 50       33 return unless defined $str;
13 17         35 $str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  3         17  
14 17         45 return $str;
15             }
16              
17             sub new {
18 18     18 0 78 my ($class, %args) = @_;
19 18         101 return bless \%args, $class;
20             }
21              
22             sub parse {
23 24     24 1 289128 my ($class, $uri_string) = @_;
24              
25 24 100 100     134 return undef unless defined $uri_string && $uri_string ne '';
26              
27 22         104 my %parsed = (
28             host => 'localhost',
29             port => 6379,
30             database => 0,
31             tls => 0,
32             is_unix => 0,
33             );
34              
35             # Handle unix socket: redis+unix://[:password@]/path[?query]
36 22 100       132 if ($uri_string =~ m{^(redis\+unix)://(?:(?::([^@]*))?@)?(/[^?]+)(?:\?(.*))?$}) {
37 5         19 $parsed{scheme} = $1;
38 5 100 66     89 $parsed{password} = _decode($2) if defined $2 && $2 ne '';
39 5         19 $parsed{path} = $3;
40 5         8 $parsed{is_unix} = 1;
41              
42             # Remove host/port for unix sockets
43 5         11 delete $parsed{host};
44 5         10 delete $parsed{port};
45              
46             # Parse query string
47 5 100       17 if (defined $4) {
48 3         5 my %query;
49 3         22 for my $pair (split /&/, $4) {
50 4         15 my ($k, $v) = split /=/, $pair, 2;
51 4         8 $query{$k} = _decode($v);
52             }
53 3 50       11 $parsed{database} = $query{db} if exists $query{db};
54             }
55              
56 5         19 return $class->new(%parsed);
57             }
58              
59             # Standard URI: redis[s]://[user:pass@]host[:port][/database]
60 17 100       100 unless ($uri_string =~ m{^(rediss?)://(.+)$}) {
61 4         90 die "Invalid Redis URI: must start with redis://, rediss://, or redis+unix://";
62             }
63              
64 13         34 my $scheme = $1;
65 13         29 my $rest = $2;
66              
67 13         34 $parsed{scheme} = $scheme;
68 13 100       29 $parsed{tls} = 1 if $scheme eq 'rediss';
69              
70             # Split userinfo from host
71 13         18 my ($userinfo, $hostinfo);
72 13 100       46 if ($rest =~ /^([^@]*)@(.+)$/) {
73 7         14 $userinfo = $1;
74 7         16 $hostinfo = $2;
75             } else {
76 6         13 $hostinfo = $rest;
77             }
78              
79             # Parse userinfo: empty, :password, user:password, or just user
80 13 100 66     42 if (defined $userinfo && $userinfo ne '') {
81 7 100       28 if ($userinfo =~ /^:(.*)$/) {
    50          
82             # :password (no username)
83 2         10 $parsed{password} = _decode($1);
84             } elsif ($userinfo =~ /^([^:]*):(.*)$/) {
85             # user:password
86 5         9 $parsed{username} = _decode($1);
87 5         9 $parsed{password} = _decode($2);
88             } else {
89             # just username
90 0         0 $parsed{username} = _decode($userinfo);
91             }
92             }
93              
94             # Parse hostinfo: host[:port][/database]
95 13 50       66 if ($hostinfo =~ m{^([^:/]+)(?::(\d+))?(?:/(\d+))?$}) {
    0          
96 13         31 $parsed{host} = $1;
97 13 100       36 $parsed{port} = int($2) if defined $2;
98 13 100       35 $parsed{database} = int($3) if defined $3;
99             } elsif ($hostinfo =~ m{^([^:/]+)(?::(\d+))?/?$}) {
100             # host:port with trailing slash but no database
101 0         0 $parsed{host} = $1;
102 0 0       0 $parsed{port} = int($2) if defined $2;
103             } else {
104 0         0 die "Invalid Redis URI format: cannot parse host from '$hostinfo'";
105             }
106              
107             # Validate we got a host
108 13 50       28 die "Invalid Redis URI: empty host" unless $parsed{host};
109              
110 13         51 return $class->new(%parsed);
111             }
112              
113             # Accessors
114 4     4 0 39 sub scheme { shift->{scheme} }
115 6     6 0 31 sub host { shift->{host} }
116 7     7 0 27 sub port { shift->{port} }
117 4     4 0 24 sub path { shift->{path} }
118 12     12 0 56 sub database { shift->{database} }
119 10     10 0 51 sub username { shift->{username} }
120 11     11 0 97 sub password { shift->{password} }
121 7     7 0 31 sub tls { shift->{tls} }
122 6     6 0 43 sub is_unix { shift->{is_unix} }
123              
124             # Convert to hash suitable for Async::Redis->new()
125             sub to_hash {
126 3     3 1 15 my ($self) = @_;
127 3         5 my %hash;
128              
129 3 100       7 if ($self->is_unix) {
130 1         5 $hash{path} = $self->path;
131             } else {
132 2         6 $hash{host} = $self->host;
133 2         6 $hash{port} = $self->port;
134             }
135              
136 3 100       8 $hash{database} = $self->database if $self->database;
137 3 100       6 $hash{username} = $self->username if defined $self->username;
138 3 100       6 $hash{password} = $self->password if defined $self->password;
139 3 100       6 $hash{tls} = 1 if $self->tls;
140              
141 3         15 return %hash;
142             }
143              
144             1;
145              
146             __END__