File Coverage

blib/lib/Yancy/Util.pm
Criterion Covered Total %
statement 165 179 92.1
branch 82 94 87.2
condition 30 38 78.9
subroutine 31 33 93.9
pod 11 11 100.0
total 319 355 89.8


line stmt bran cond sub pod time code
1             package Yancy::Util;
2             our $VERSION = '1.086';
3             # ABSTRACT: Utilities for Yancy
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use Yancy::Util qw( load_backend );
8             #pod my $be = load_backend( 'memory://localhost', $schema );
9             #pod
10             #pod use Yancy::Util qw( curry );
11             #pod my $helper = curry( \&_helper_sub, @args );
12             #pod
13             #pod use Yancy::Util qw( currym );
14             #pod my $sub = currym( $object, 'method_name', @args );
15             #pod
16             #pod use Yancy::Util qw( match );
17             #pod if ( match( $where, $item ) ) {
18             #pod say 'Matched!';
19             #pod }
20             #pod
21             #pod use Yancy::Util qw( fill_brackets );
22             #pod my $value = fill_brackets( $template, $item );
23             #pod
24             #pod =head1 DESCRIPTION
25             #pod
26             #pod This module contains utility functions for Yancy.
27             #pod
28             #pod =head1 SEE ALSO
29             #pod
30             #pod L
31             #pod
32             #pod =cut
33              
34 23     23   9854754 use Mojo::Base '-strict';
  23         212  
  23         140  
35 23     23   2782 use Exporter 'import';
  23         51  
  23         803  
36 23     23   135 use List::Util qw( all any none first );
  23         52  
  23         1949  
37 23     23   1084 use Mojo::Loader qw( load_class );
  23         72811  
  23         1324  
38 23     23   184 use Scalar::Util qw( blessed );
  23         51  
  23         1147  
39 23     23   1564 use Mojo::JSON::Pointer;
  23         2128  
  23         208  
40 23     23   2106 use Mojo::JSON qw( to_json );
  23         68315  
  23         1204  
41 23     23   148 use Mojo::Util qw( xml_escape );
  23         41  
  23         1096  
42 23     23   363 use Carp qw( carp );
  23         46  
  23         1258  
43 23     23   11527 use JSON::Validator;
  23         1392445  
  23         175  
44              
45             our @EXPORT_OK = qw( load_backend curry currym copy_inline_refs match derp fill_brackets
46             is_type order_by is_format json_validator );
47              
48             #pod =sub load_backend
49             #pod
50             #pod my $backend = load_backend( $backend_url, $schema );
51             #pod my $backend = load_backend( { $backend_name => $arg }, $schema );
52             #pod my $backend = load_backend( $db_object, $schema );
53             #pod
54             #pod Get a Yancy backend from the given backend URL, or from a hash reference
55             #pod with a backend name and optional argument. The C<$schema> hash is
56             #pod the configured JSON schema for this backend.
57             #pod
58             #pod A backend URL should begin with a name followed by a colon. The first
59             #pod letter of the name will be capitalized, and used to build a class name
60             #pod in the C namespace.
61             #pod
62             #pod The C<$backend_name> should be the name of a module in the
63             #pod C namespace. The C<$arg> is handled by the backend
64             #pod module. Read your backend module's documentation for details.
65             #pod
66             #pod The C<$db_object> can be one of: L, L,
67             #pod L, or a subclass of L. The
68             #pod appropriate backend object will be created.
69             #pod
70             #pod See L for information about
71             #pod backend URLs and L for more information about backend
72             #pod objects.
73             #pod
74             #pod =cut
75              
76             # This allows users to pass in the database object directly
77             our %BACKEND_CLASSES = (
78             'Mojo::Pg' => 'pg',
79             'Mojo::mysql' => 'mysql',
80             'Mojo::SQLite' => 'sqlite',
81             'DBIx::Class::Schema' => 'dbic',
82             );
83              
84             # Aliases allow the user to specify the same string as they pass to
85             # their database object
86             our %TYPE_ALIAS = (
87             postgresql => 'pg',
88             );
89              
90             sub load_backend {
91 85     85 1 70462 my ( $config, $schema ) = @_;
92 85         225 my ( $type, $arg );
93 85 100       402 if ( !ref $config ) {
    50          
94 84         543 ( $type ) = $config =~ m{^([^:]+)};
95 84   33     635 $type = $TYPE_ALIAS{ $type } // $type;
96 84         207 $arg = $config;
97             }
98             elsif ( blessed $config ) {
99 0         0 for my $class ( keys %BACKEND_CLASSES ) {
100 0 0       0 if ( $config->isa( $class ) ) {
101 0         0 ( $type, $arg ) = ( $BACKEND_CLASSES{ $class }, $config );
102 0         0 last;
103             }
104             }
105             }
106             else {
107 1         2 ( $type, $arg ) = %{ $config };
  1         3  
108             }
109 85         471 my $class = 'Yancy::Backend::' . ucfirst $type;
110 85 100       404 if ( my $e = load_class( $class ) ) {
111 2 100       1300 die ref $e ? "Could not load class $class: $e" : "Could not find class $class";
112             }
113 83         2288 return $class->new( $arg, $schema );
114             }
115              
116             #pod =sub curry
117             #pod
118             #pod my $curried_sub = curry( $sub, @args );
119             #pod
120             #pod Return a new subref that, when called, will call the passed-in subref with
121             #pod the passed-in C<@args> first.
122             #pod
123             #pod For example:
124             #pod
125             #pod my $add = sub {
126             #pod my ( $lop, $rop ) = @_;
127             #pod return $lop + $rop;
128             #pod };
129             #pod my $add_four = curry( $add, 4 );
130             #pod say $add_four->( 1 ); # 5
131             #pod say $add_four->( 2 ); # 6
132             #pod say $add_four->( 3 ); # 7
133             #pod
134             #pod This is more-accurately called L
135             #pod application|https://en.wikipedia.org/wiki/Partial_application>, but
136             #pod C is shorter.
137             #pod
138             #pod =cut
139              
140             sub curry {
141 602     602 1 5094 my ( $sub, @args ) = @_;
142 602     459   3999 return sub { $sub->( @args, @_ ) };
  459         1987488  
143             }
144              
145             #pod =sub currym
146             #pod
147             #pod my $curried_sub = currym( $obj, $method, @args );
148             #pod
149             #pod Return a subref that, when called, will call given C<$method> on the
150             #pod given C<$obj> with any passed-in C<@args> first.
151             #pod
152             #pod See L for an example.
153             #pod
154             #pod =cut
155              
156             sub currym {
157 500     500 1 23140 my ( $obj, $meth, @args ) = @_;
158 500   100     3103 my $sub = $obj->can( $meth )
159             || die sprintf q{Can't curry method "%s" on object of type "%s": Method is not implemented},
160             $meth, blessed( $obj );
161 499         1439 return curry( $sub, $obj, @args );
162             }
163              
164             #pod =sub copy_inline_refs
165             #pod
166             #pod my $subschema = copy_inline_refs( $schema, '/user' );
167             #pod
168             #pod Given:
169             #pod
170             #pod =over
171             #pod
172             #pod =item a "source" JSON schema (will not be mutated)
173             #pod
174             #pod =item a JSON Pointer into the source schema, from which to be copied
175             #pod
176             #pod =back
177             #pod
178             #pod will return another, copied standalone JSON schema, with any C<$ref>
179             #pod either copied in, or if previously encountered, with a C<$ref> to the
180             #pod new location.
181             #pod
182             #pod =cut
183              
184             sub copy_inline_refs {
185 21682     21682 1 46603 my ( $schema, $pointer, $usschema, $uspointer, $refmap ) = @_;
186 21682   100     37985 $usschema //= Mojo::JSON::Pointer->new( $schema )->get( $pointer );
187 21682   100     56397 $uspointer //= '';
188 21682   100     32331 $refmap ||= {};
189 21682 100       35980 return { '$ref' => $refmap->{ $uspointer } } if $refmap->{ $uspointer };
190             $refmap->{ $pointer } = "#$uspointer"
191 21680 100 100     71667 unless ref $usschema eq 'HASH' and $usschema->{'$ref'};
192 21680 100 100     100217 return $usschema
193             unless ref $usschema eq 'ARRAY' or ref $usschema eq 'HASH';
194 6627         8362 my $counter = 0;
195 6627 100       16274 return [ map copy_inline_refs(
196             $schema,
197             $pointer.'/'.$counter++,
198             $_,
199             $uspointer.'/'.$counter++,
200             $refmap,
201             ), @$usschema ] if ref $usschema eq 'ARRAY';
202             # HASH
203 4541         5916 my $ref = $usschema->{'$ref'};
204 4541 100       16629 return { map { $_ => copy_inline_refs(
205             $schema,
206             $pointer.'/'.$_,
207 16642         44714 $usschema->{ $_ },
208             $uspointer.'/'.$_,
209             $refmap,
210             ) } sort keys %$usschema } if !$ref;
211 14         37 $ref =~ s:^#::;
212 14 100       53 return { '$ref' => $refmap->{ $ref } } if $refmap->{ $ref };
213 4         10 copy_inline_refs(
214             $schema,
215             $ref,
216             Mojo::JSON::Pointer->new( $schema )->get( $ref ),
217             $uspointer,
218             $refmap,
219             );
220             }
221              
222             #pod =sub match
223             #pod
224             #pod my $bool = match( $where, $item );
225             #pod
226             #pod Test if the given C<$item> matches the given L C<$where>
227             #pod data structure. See L for the full syntax.
228             #pod
229             #pod Not all of SQL::Abstract's syntax is supported yet, so patches are welcome.
230             #pod
231             #pod =cut
232              
233             sub match {
234 607     607 1 5625 my ( $match, $item ) = @_;
235 607 100       1246 return undef if !defined $item;
236              
237 604 100       1350 if ( ref $match eq 'ARRAY' ) {
238 3     4   21 return any { match( $_, $item ) } @$match;
  4         16  
239             }
240              
241 601         869 my %test;
242 601         1302 for my $key ( keys %$match ) {
243 360 100       1464 if ( $key =~ /^-(not_)?bool/ ) {
    100          
    100          
    50          
244 17         42 my $want_false = $1;
245 17         33 $key = $match->{ $key }; # the actual field
246             $test{ $key } = sub {
247 17     17   37 my ( $value, $key ) = @_;
248 17 100       59 return $want_false ? !$value : !!$value;
249 17         84 };
250             }
251             elsif ( !ref $match->{ $key } ) {
252 234         619 $test{ $key } = $match->{ $key };
253             }
254             elsif ( ref $match->{ $key } eq 'HASH' ) {
255 105 100 100     431 if ( my $value = $match->{ $key }{ -like } || $match->{ $key }{ like } ) {
    100          
    100          
    50          
256 78         160 $value = quotemeta $value;
257 78         411 $value =~ s/(?
258 78         1037 $test{ $key } = qr{^$value$};
259             }
260             elsif ( $value = $match->{ $key }{ -has } ) {
261 9         10 my $expect = $value;
262             $test{ $key } = sub {
263 9     9   16 my ( $value, $key ) = @_;
264 9 100       25 return 0 if !defined $value;
265 8 100       20 if ( ref $value eq 'ARRAY' ) {
    50          
266 4 100       11 if ( ref $expect eq 'ARRAY' ) {
    50          
267 2         9 return all { my $e = $_; any { $_ eq $e } @$value } @$expect;
  4         8  
  4         10  
  8         17  
268             }
269             elsif ( !ref $expect ) {
270 2         10 return any { $_ eq $expect } @$value;
  4         12  
271             }
272             }
273             elsif ( ref $value eq 'HASH' ) {
274 4 50       7 if ( ref $expect eq 'HASH' ) {
275 4         6 return match( $expect, $value );
276             }
277             else {
278 0         0 die 'Bad query in -has on hash value: ' . ref $expect;
279             }
280             }
281             else {
282 0         0 die '-has query does not work on non-ref fields';
283             }
284 9         45 };
285             }
286             elsif ( $value = $match->{ $key }{ -not_has } ) {
287             $test{ $key } = sub {
288 9     9   11 my $expect = $value;
289 9         16 my ( $value, $key ) = @_;
290 9 100       17 return 1 if !defined $value;
291 8 100       27 if ( ref $value eq 'ARRAY' ) {
    50          
292 4 100       8 if ( ref $expect eq 'ARRAY' ) {
    50          
293 2         8 return all { my $e = $_; none { $_ eq $e } @$value } @$expect;
  3         6  
  3         20  
  7         17  
294             }
295             elsif ( !ref $expect ) {
296 2         10 return none { $_ eq $expect } @$value;
  4         19  
297             }
298             else {
299 0         0 die 'Bad query in -has on array value: ' . ref $expect;
300             }
301             }
302             elsif ( ref $value eq 'HASH' ) {
303 4 50       7 if ( ref $expect eq 'HASH' ) {
304 4         17 return !match( $expect, $value );
305             }
306             else {
307 0         0 die 'Bad query in -has on hash value: ' . ref $expect;
308             }
309             }
310             else {
311 0         0 die '-has query does not work on non-ref fields';
312             }
313 9         73 };
314             }
315             elsif ( exists $match->{ $key }{ '!=' } ) {
316 9         17 my $expect = $match->{ $key }{ '!=' };
317             $test{ $key } = sub {
318 9     9   20 my ( $got, $key ) = @_;
319 9 50 66     29 if ( !defined $expect || !defined $got) {
320 9         24 return defined $got != defined $expect;
321             }
322 0         0 return $got ne $expect;
323 9         47 };
324             }
325             else {
326 0         0 die "Unimplemented query type: " . to_json( $match->{ $key } );
327             }
328             }
329             elsif ( ref $match->{ $key } eq 'ARRAY' ) {
330 4         7 my @tests = @{ $match->{ $key } };
  4         15  
331             # Array is an 'OR' combiner
332             $test{ $key } = sub {
333 4     4   13 my ( $value, $key ) = @_;
334 4         9 my $sub_item = { $key => $value };
335 4         22 return any { match( { $key => $_ }, $sub_item ) } @tests;
  6         23  
336 4         27 };
337             }
338             else {
339 0         0 die "Unimplemented match ref type: " . to_json( $match->{ $key } );
340             }
341             }
342              
343             my $passes
344             = grep {
345 601         1245 !defined $test{ $_ } ? !defined $item->{ $_ }
346             : ref $test{ $_ } eq 'Regexp' ? $item->{ $_ } =~ $test{ $_ }
347             : ref $test{ $_ } eq 'CODE' ? $test{ $_ }->( $item->{ $_ }, $_ )
348 360 100 100     2363 : ($item->{ $_ }//'') eq ($test{ $_ }//'')
    100 50        
    100          
349             }
350             keys %test;
351              
352 601         2688 return $passes == keys %test;
353             }
354              
355             #pod =sub order_by
356             #pod
357             #pod my $ordered_array = order_by( $order_by, $unordered_array );
358             #pod
359             #pod Order the given arrayref by the given L order-by clause.
360             #pod
361             #pod =cut
362              
363             sub order_by {
364 331     331 1 2942 my ( $order_by, $unordered ) = @_;
365             # Array of [ (-asc/-desc), (field) ]
366 331         515 my @sort_items;
367              
368 331 100       920 if ( ref $order_by eq 'ARRAY' ) {
    100          
369 301 100       634 @sort_items = map { [ ref $_ ? %$_ : ( -asc => $_ ) ] } @$order_by;
  311         1270  
370             }
371             elsif ( ref $order_by eq 'HASH' ) {
372 21         74 @sort_items = [ %$order_by ];
373             }
374             else {
375 9         28 @sort_items = [ -asc => $order_by ];
376             }
377              
378             my @ordered = sort {
379 331         1209 for my $item ( @sort_items ) {
  207         415  
380             my $cmp = $item->[0] eq '-asc'
381             ? ($a->{ $item->[1] }//'') cmp ($b->{ $item->[1] }//'')
382 210 100 50     1231 : ($b->{ $item->[1] }//'') cmp ($a->{ $item->[1] }//'')
      50        
      50        
      50        
383             ;
384 210   100     833 return $cmp || next;
385             }
386             }
387             @$unordered;
388              
389 331         1094 return \@ordered;
390             }
391              
392             #pod =sub fill_brackets
393             #pod
394             #pod my $string = fill_brackets( $template, $item );
395             #pod
396             #pod This routine will fill in the given template string with the values from
397             #pod the given C<$item> hashref. The template contains field names within curly braces.
398             #pod Values in the C<$item> hashref will be escaped with L.
399             #pod
400             #pod my $item = {
401             #pod name => 'Doug Bell',
402             #pod email => 'doug@example.com',
403             #pod quote => 'I <3 Perl',
404             #pod };
405             #pod
406             #pod # Doug Bell
407             #pod fill_brackets( '{name} <{email}>', $item );
408             #pod
409             #pod # I <3 Perl
410             #pod fill_brackets( '{quote}', $item );
411             #pod
412             #pod =cut
413              
414             sub fill_brackets {
415 4     4 1 3104 my ( $template, $item ) = @_;
416 4         28 return scalar $template =~ s/(?{$1}/reg;
  9         75  
417             }
418              
419             #pod =sub is_type
420             #pod
421             #pod my $bool = is_type( $schema->{properties}{myprop}{type}, 'boolean' );
422             #pod
423             #pod Returns true if the given JSON schema type value (which can be a string or
424             #pod an array of strings) contains the given value, allowing the given type for
425             #pod the property.
426             #pod
427             #pod # true
428             #pod is_type( 'boolean', 'boolean' );
429             #pod is_type( [qw( boolean null )], 'boolean' );
430             #pod # false
431             #pod is_type( 'string', 'boolean' );
432             #pod is_type( [qw( string null )], 'boolean' );
433             #pod
434             #pod =cut
435              
436             sub is_type {
437 2506     2506 1 3961 my ( $type, $is_type ) = @_;
438 2506 100       4172 return unless $type;
439             return ref $type eq 'ARRAY'
440 2502 100       8490 ? !!grep { $_ eq $is_type } @$type
  1186         3466  
441             : $type eq $is_type;
442             }
443              
444             #pod =sub is_format
445             #pod
446             #pod my $bool = is_format( $schema->{properties}{myprop}{format}, 'date-time' );
447             #pod
448             #pod Returns true if the given JSON schema format value (a string) is the given value.
449             #pod
450             #pod # true
451             #pod is_format( 'date-time', 'date-time' );
452             #pod # false
453             #pod is_format( 'email', 'date-time' );
454             #pod
455             #pod =cut
456              
457             sub is_format {
458 656     656 1 1157 my ( $format, $is_format ) = @_;
459 656 100       2104 return unless $format;
460 255         931 return $format eq $is_format;
461             }
462              
463             #pod =sub derp
464             #pod
465             #pod derp "This feature is deprecated in file '%s'", $file;
466             #pod
467             #pod Print out a deprecation message as a warning. A message will only be
468             #pod printed once for each set of arguments from each caller.
469             #pod
470             #pod =cut
471              
472             our @CARP_NOT = qw(
473             Yancy::Controller::Yancy Yancy::Controller::Yancy::MultiTenant
474             Mojolicious::Plugin::Yancy Mojolicious::Plugins Mojolicious
475             Mojo::Server Yancy::Plugin::Editor Yancy::Plugin::Auth
476             Mojolicious::Renderer Yancy::Plugin::Auth::Token
477             Yancy::Plugin::Auth::Password
478             );
479             our %DERPED;
480             sub derp(@) {
481 230     230 1 524 my @args = @_;
482 230         1382 my $key = to_json [ caller, @args ];
483 230 100       20271 return if $DERPED{ $key };
484 18 100       179 if ( $args[0] !~ /\.$/ ) {
485 17         108 $args[0] .= '.';
486             }
487 18         8943 carp sprintf( $args[0], @args[1..$#args] );
488 18         286 $DERPED{ $key } = 1;
489             }
490              
491             #pod =sub json_validator
492             #pod
493             #pod my $json_validator = json_validator( $schema );
494             #pod
495             #pod Build a L object for the given schema, adding all the
496             #pod necessary attributes.
497             #pod
498             #pod =cut
499              
500             sub json_validator {
501 123     123 1 19547 my ( $schema ) = @_;
502 123         1285 my $v = JSON::Validator->new( coerce => 'bool,def,num,str' );
503 123         7395 my $formats = $v->formats;
504 123     18   10234 $formats->{ password } = sub { undef };
  18         8944  
505 123     11   557 $formats->{ filepath } = sub { undef };
  11         6240  
506 123     0   584 $formats->{ markdown } = sub { undef };
  0         0  
507 123     0   510 $formats->{ tel } = sub { undef };
  0         0  
508 123     2   546 $formats->{ textarea } = sub { undef };
  2         1356  
509 123         776 return $v;
510             }
511              
512             1;
513              
514             __END__