File Coverage

blib/lib/Kelp/Util.pm
Criterion Covered Total %
statement 115 124 92.7
branch 54 68 79.4
condition 18 22 81.8
subroutine 22 22 100.0
pod 7 9 77.7
total 216 245 88.1


line stmt bran cond sub pod time code
1             package Kelp::Util;
2              
3             # No Kelp::Base here, because Kelp::Base uses Kelp::Util;
4 56     56   136389 use v5.10;
  56         217  
5 56     56   314 use strict;
  56         163  
  56         1543  
6 56     56   256 use warnings;
  56         118  
  56         2905  
7              
8 56     56   359 use Carp;
  56         101  
  56         5826  
9 56     56   37965 use Data::Dumper qw();
  56         588202  
  56         2303  
10 56     56   32684 use Encode qw();
  56         1031035  
  56         2599  
11 56     56   30232 use Class::Inspector;
  56         251725  
  56         2577  
12 56     56   28777 use Plack::Util;
  56         771145  
  56         2846  
13 56     56   27494 use Test::Deep::NoTest qw(eq_deeply);
  56         642522  
  56         523  
14 56     56   14859 use Scalar::Util qw(reftype);
  56         117  
  56         97490  
15              
16             # improve error locations of croak
17             our @CARP_NOT = (
18             qw(
19             Kelp
20             Kelp::Base
21             Kelp::Routes
22             Kelp::Context
23             )
24             );
25              
26             sub _DEBUG
27             {
28 909     909   2885 my ($stage, @messages) = @_;
29 909         2239 my $env = $ENV{KELP_DEBUG};
30 909 50       3674 return if !$env;
31 0 0       0 return if !grep { lc $env eq $_ } '1', 'all', lc $stage;
  0         0  
32              
33 0         0 local $Data::Dumper::Sortkeys = 1;
34             my $message = join ' ', map {
35 0 0       0 ref $_ ? Data::Dumper::Dumper($_) : $_
  0         0  
36             } @messages;
37              
38 0         0 print "DEBUG: $message\n";
39             }
40              
41             sub camelize
42             {
43 202     202 1 316409 my ($string, $base, $class_only) = @_;
44 202 100       467 return $string unless $string;
45              
46 198 100 66     947 my $sigil = defined $string && $string =~ s/^(\+)// ? $1 : undef;
47 198 100       406 $base = undef if $sigil;
48              
49 198         262 my @parts;
50 198 100       548 if ($string !~ /#/) {
51              
52             # do not camelize if it doesn't look like a camelize string
53 71         193 @parts = ($string);
54             }
55             else {
56 127         369 @parts = split /\#/, $string;
57 127         273 my $sub = pop @parts;
58              
59 127 100       285 push @parts, $sub
60             if $class_only;
61              
62             @parts = map {
63 127         228 join '', map { ucfirst lc } split /\_/
  152         355  
  206         892  
64             } @parts;
65              
66 127 100       379 push @parts, $sub
67             if !$class_only;
68             }
69              
70 198 100       476 unshift @parts, $base if $base;
71 198         909 return join('::', @parts);
72             }
73              
74             sub extract_class
75             {
76 128     128 1 8395 my ($string) = @_;
77 128 100       285 return undef unless $string;
78              
79 127 100 100     1099 if ($string =~ /^(.+)::(\w+)$/ && $1 ne 'main') {
80 109         403 return $1;
81             }
82              
83 18         108 return undef;
84             }
85              
86             sub extract_function
87             {
88 108     108 1 5610 my ($string) = @_;
89 108 100       279 return undef unless $string;
90              
91 107 100       408 if ($string =~ /^(.+)::(\w+)$/) {
92 106         293 return $2;
93             }
94              
95 1         5 return $string;
96             }
97              
98             sub effective_charset
99             {
100 304     304 1 899 my $this_charset = shift;
101 304 100       5044 return Encode::find_encoding($this_charset) ? $this_charset : undef;
102             }
103              
104             sub charset_encode
105             {
106 242     242 0 13845 my ($charset, $string) = @_;
107              
108 242 100       692 return $string unless $charset;
109 241         1357 return Encode::encode $charset, $string;
110             }
111              
112             sub charset_decode
113             {
114 617     617 0 6413 my ($charset, $string) = @_;
115              
116 617 100       1631 return $string unless $charset;
117 616         4498 return Encode::decode $charset, $string;
118             }
119              
120             sub adapt_psgi
121             {
122 5     5 1 11 my ($app) = @_;
123              
124 5 50       14 croak 'Cannot adapt_psgi, unknown destination type - must be a coderef'
125             unless ref $app eq 'CODE';
126              
127             return sub {
128 11     11   26 my $context = shift->context;
129 11   100     26 my $path = charset_encode($context->app->request_charset, pop() // '');
130 11         380 my $env = $context->req->env;
131              
132             # remember script and path
133 11         63 my $orig_script = $env->{SCRIPT_NAME};
134 11         30 my $orig_path = $env->{PATH_INFO};
135              
136             # adjust slashes in paths
137 11 100       59 my $trailing_slash = $orig_path =~ m{/$} ? '/' : '';
138 11         48 $path =~ s{^/?}{/};
139 11         44 $path =~ s{/?$}{$trailing_slash};
140              
141 11         17 my $result = do {
142              
143             # adjust script and path
144 11         24 local $env->{SCRIPT_NAME} = $orig_path;
145 11         232 $env->{SCRIPT_NAME} =~ s{\Q$path\E$}{};
146 11         30 local $env->{PATH_INFO} = $path;
147              
148             # run the callback
149 11         48 $app->($env);
150             };
151              
152             # produce a response
153 11 50       170 if (ref $result eq 'ARRAY') {
    0          
154 11         18 my ($status, $headers, $body) = @{$result};
  11         29  
155              
156 11         29 my $res = $context->res;
157 11 50       91 $res->status($status) if $status;
158 11 50       128 $res->headers($headers) if $headers;
159 11 50       877 $res->body($body) if $body;
160 11         66 $res->rendered(1);
161             }
162             elsif (ref $result eq 'CODE') {
163 0         0 return $result;
164             }
165              
166             # this should be an error unless already rendered
167 11         48 return;
168 5         51 };
169             }
170              
171             sub load_package
172             {
173 1752     1752 1 6945 my $package = shift;
174 1752         4754 state $loaded = {};
175              
176             # only load package once for a given class name
177 1752   100     12393 return $loaded->{$package} //= do {
178 367 100       3617 Plack::Util::load_class($package)
179             unless Class::Inspector->loaded($package);
180 364         1843535 $package;
181             };
182             }
183              
184             sub merge
185             {
186 160     160 1 555 my ($a, $b, $allow_blessed, $sigil) = @_;
187 160 50 0 620   866 my $ref = $allow_blessed ? sub { reftype $_[0] // '' } : sub { ref $_[0] };
  0         0  
  947         3748  
188              
189 160 100 66     441 return $b
      100        
190             if !$ref->($a)
191             || !$ref->($b)
192             || $ref->($a) ne $ref->($b);
193              
194 143 100       415 if ($ref->($a) eq 'ARRAY') {
    50          
195 44 100       214 return $b unless $sigil;
196 33 100       183 if ($sigil eq '+') {
197 5         13 for my $e (@$b) {
198 11 100       1777 push @$a, $e unless grep { eq_deeply($_, $e) } @$a;
  25         16946  
199             }
200             }
201             else {
202             $a = [
203             grep {
204 28         827 my $e = $_;
  57         51695  
205 57         139 !grep { eq_deeply($_, $e) } @$b
  70         3756  
206             } @$a
207             ];
208             }
209 33         5999 return $a;
210             }
211             elsif ($ref->($a) eq 'HASH') {
212 99         350 for my $k (keys %$b) {
213              
214             # If the key is an array then look for a merge sigil
215 110 100 100     369 my $s = $ref->($b->{$k}) eq 'ARRAY' && $k =~ s/^(\+|\-)// ? $1 : '';
216              
217             $a->{$k} =
218             exists $a->{$k}
219             ? merge($a->{$k}, $b->{"$s$k"}, $allow_blessed, $s)
220 110 100       765 : $b->{$k};
221             }
222              
223 99         596 return $a;
224             }
225 0           return $b;
226             }
227              
228             1;
229              
230             __END__
231              
232             =pod
233              
234             =head1 NAME
235              
236             Kelp::Util - Kelp general utility functions
237              
238             =head1 SYNOPSIS
239              
240             use Kelp::Util;
241              
242             # MyApp::A::b
243             say Kelp::Util::camelize('a#b', 'MyApp');
244              
245             # Controller
246             say Kelp::Util::extract_class('Controller::Action');
247              
248             # Action
249             say Kelp::Util::extract_function('Controller::Action');
250              
251              
252             =head1 DESCRIPTION
253              
254             These are some helpful functions not seen in L<Plack::Util>.
255              
256             =head1 FUNCTIONS
257              
258             No functions are exported and have to be used with full package name prefix.
259              
260             =head2 camelize
261              
262             This function accepts a string and a base class. Does three things:
263              
264             =over
265              
266             =item * transforms snake_case into CamelCase for class names (with lowercasing)
267              
268             =item * replaces hashes C<#> with Perl package separators C<::>
269              
270             =item * constructs the class name in similar fasion as L<Plack::Util/load_class>
271              
272             =back
273              
274             The returned string will have leading C<+> removed and will be prepended with
275             the second argument if there was no C<+>. An optional third argument can also
276             be passed to treat the entire string as a class name.
277              
278             Will not do the camelizing if there is no C<#> sign in the string, even if
279             the third argument is present.
280              
281             =head2 extract_class
282              
283             Extracts the class name from a C<Controller::action> string. Returns undef if
284             no class in the string or the class is C<main>.
285              
286             =head2 extract_function
287              
288             Extracts the function name from a string. If there is no class name, returns
289             the entire string. Returns undef for empty strings.
290              
291             =head2 effective_charset
292              
293             Takes a charset name and returns it back if it is supported by Encode.
294             If there is no charset or it isn't supported, undef will be returned.
295              
296             =head2 adapt_psgi
297              
298             Transforms a given Plack/PSGI application (in form of a runner subroutine) to a
299             Kelp route handler. The route handler will take the last argument matched from
300             a pattern and adjust the proper environmental paths of the PSGI standard. This
301             will make the application mostly behave as if it was mounted directly where the
302             route points minus the last placeholder. For example, route C</app> will adjust
303             the script name to C<'/app'> and path info will always be empty, while route
304             C<< /app/>rest >> will have the same script name and path info set to whatever
305             was after C</app> in the URL (trailing slashes included).
306              
307             NOTE: having more than one placeholder in the pattern is mostly wasteful, as
308             their matched values will not be handled in any way (other than allowing a
309             varying request path).
310              
311             =head2 load_package
312              
313             Takes a name of a package and loads it efficiently.
314              
315             =head2 merge
316              
317             my $merged = Kelp::Util::merge($val1, $val2, $allow_blessed);
318              
319             Merges two structures. Used by config module to merge configuration files.
320             Optionally, a third argument can be passed to allow merging values of blessed
321             references as well.
322              
323             =cut
324