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