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.088';
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   10087992 use Mojo::Base '-strict';
  23         212  
  23         151  
35 23     23   2893 use Exporter 'import';
  23         64  
  23         769  
36 23     23   136 use List::Util qw( all any none first );
  23         70  
  23         2084  
37 23     23   1099 use Mojo::Loader qw( load_class );
  23         80873  
  23         1270  
38 23     23   163 use Scalar::Util qw( blessed );
  23         62  
  23         1164  
39 23     23   1939 use Mojo::JSON::Pointer;
  23         2416  
  23         192  
40 23     23   2236 use Mojo::JSON qw( to_json );
  23         72786  
  23         1255  
41 23     23   158 use Mojo::Util qw( xml_escape );
  23         57  
  23         1183  
42 23     23   367 use Carp qw( carp );
  23         52  
  23         1260  
43 23     23   11831 use JSON::Validator;
  23         1464051  
  23         168  
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 86     86 1 83951 my ( $config, $schema ) = @_;
92 86         276 my ( $type, $arg );
93 86 100       732 if ( !ref $config ) {
    50          
94 85         618 ( $type ) = $config =~ m{^([^:]+)};
95 85   33     637 $type = $TYPE_ALIAS{ $type } // $type;
96 85         226 $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         4  
108             }
109 86         487 my $class = 'Yancy::Backend::' . ucfirst $type;
110 86 100       420 if ( my $e = load_class( $class ) ) {
111 2 100       2846 die ref $e ? "Could not load class $class: $e" : "Could not find class $class";
112             }
113 84         2350 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 611     611 1 5867 my ( $sub, @args ) = @_;
142 611     461   4266 return sub { $sub->( @args, @_ ) };
  461         2136956  
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 507     507 1 23863 my ( $obj, $meth, @args ) = @_;
158 507   100     3184 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 506         1465 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 21724     21724 1 51206 my ( $schema, $pointer, $usschema, $uspointer, $refmap ) = @_;
186 21724   100     39802 $usschema //= Mojo::JSON::Pointer->new( $schema )->get( $pointer );
187 21724   100     59692 $uspointer //= '';
188 21724   100     34083 $refmap ||= {};
189 21724 100       38730 return { '$ref' => $refmap->{ $uspointer } } if $refmap->{ $uspointer };
190             $refmap->{ $pointer } = "#$uspointer"
191 21722 100 100     76643 unless ref $usschema eq 'HASH' and $usschema->{'$ref'};
192 21722 100 100     105032 return $usschema
193             unless ref $usschema eq 'ARRAY' or ref $usschema eq 'HASH';
194 6643         8638 my $counter = 0;
195 6643 100       17120 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 4553         6375 my $ref = $usschema->{'$ref'};
204 4553 100       17613 return { map { $_ => copy_inline_refs(
205             $schema,
206             $pointer.'/'.$_,
207 16678         47588 $usschema->{ $_ },
208             $uspointer.'/'.$_,
209             $refmap,
210             ) } sort keys %$usschema } if !$ref;
211 14         48 $ref =~ s:^#::;
212 14 100       63 return { '$ref' => $refmap->{ $ref } } if $refmap->{ $ref };
213 4         15 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 611     611 1 6129 my ( $match, $item ) = @_;
235 611 100       1383 return undef if !defined $item;
236              
237 608 100       1493 if ( ref $match eq 'ARRAY' ) {
238 3     4   32 return any { match( $_, $item ) } @$match;
  4         16  
239             }
240              
241 605         917 my %test;
242 605         1290 for my $key ( keys %$match ) {
243 363 100       1431 if ( $key =~ /^-(not_)?bool/ ) {
    100          
    100          
    50          
244 17         45 my $want_false = $1;
245 17         35 $key = $match->{ $key }; # the actual field
246             $test{ $key } = sub {
247 17     17   44 my ( $value, $key ) = @_;
248 17 100       59 return $want_false ? !$value : !!$value;
249 17         94 };
250             }
251             elsif ( !ref $match->{ $key } ) {
252 237         656 $test{ $key } = $match->{ $key };
253             }
254             elsif ( ref $match->{ $key } eq 'HASH' ) {
255 105 100 100     445 if ( my $value = $match->{ $key }{ -like } || $match->{ $key }{ like } ) {
    100          
    100          
    50          
256 78         186 $value = quotemeta $value;
257 78         442 $value =~ s/(?
258 78         1075 $test{ $key } = qr{^$value$};
259             }
260             elsif ( $value = $match->{ $key }{ -has } ) {
261 9         15 my $expect = $value;
262             $test{ $key } = sub {
263 9     9   16 my ( $value, $key ) = @_;
264 9 100       28 return 0 if !defined $value;
265 8 100       27 if ( ref $value eq 'ARRAY' ) {
    50          
266 4 100       14 if ( ref $expect eq 'ARRAY' ) {
    50          
267 2         10 return all { my $e = $_; any { $_ eq $e } @$value } @$expect;
  4         18  
  4         16  
  8         22  
268             }
269             elsif ( !ref $expect ) {
270 2         10 return any { $_ eq $expect } @$value;
  4         15  
271             }
272             }
273             elsif ( ref $value eq 'HASH' ) {
274 4 50       7 if ( ref $expect eq 'HASH' ) {
275 4         8 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         53 };
285             }
286             elsif ( $value = $match->{ $key }{ -not_has } ) {
287             $test{ $key } = sub {
288 9     9   14 my $expect = $value;
289 9         16 my ( $value, $key ) = @_;
290 9 100       19 return 1 if !defined $value;
291 8 100       22 if ( ref $value eq 'ARRAY' ) {
    50          
292 4 100       11 if ( ref $expect eq 'ARRAY' ) {
    50          
293 2         13 return all { my $e = $_; none { $_ eq $e } @$value } @$expect;
  3         5  
  3         12  
  7         20  
294             }
295             elsif ( !ref $expect ) {
296 2         10 return none { $_ eq $expect } @$value;
  4         15  
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       18 if ( ref $expect eq 'HASH' ) {
304 4         8 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         85 };
314             }
315             elsif ( exists $match->{ $key }{ '!=' } ) {
316 9         18 my $expect = $match->{ $key }{ '!=' };
317             $test{ $key } = sub {
318 9     9   23 my ( $got, $key ) = @_;
319 9 50 66     30 if ( !defined $expect || !defined $got) {
320 9         31 return defined $got != defined $expect;
321             }
322 0         0 return $got ne $expect;
323 9         58 };
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         9 my @tests = @{ $match->{ $key } };
  4         13  
331             # Array is an 'OR' combiner
332             $test{ $key } = sub {
333 4     4   11 my ( $value, $key ) = @_;
334 4         10 my $sub_item = { $key => $value };
335 4         26 return any { match( { $key => $_ }, $sub_item ) } @tests;
  6         46  
336 4         24 };
337             }
338             else {
339 0         0 die "Unimplemented match ref type: " . to_json( $match->{ $key } );
340             }
341             }
342              
343             my $passes
344             = grep {
345 605         1296 !defined $test{ $_ } ? !defined $item->{ $_ }
346             : ref $test{ $_ } eq 'Regexp' ? $item->{ $_ } =~ $test{ $_ }
347             : ref $test{ $_ } eq 'CODE' ? $test{ $_ }->( $item->{ $_ }, $_ )
348 363 100 100     2571 : ($item->{ $_ }//'') eq ($test{ $_ }//'')
    100 50        
    100          
349             }
350             keys %test;
351              
352 605         2913 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 340     340 1 3162 my ( $order_by, $unordered ) = @_;
365             # Array of [ (-asc/-desc), (field) ]
366 340         541 my @sort_items;
367              
368 340 100       937 if ( ref $order_by eq 'ARRAY' ) {
    100          
369 310 100       631 @sort_items = map { [ ref $_ ? %$_ : ( -asc => $_ ) ] } @$order_by;
  321         1350  
370             }
371             elsif ( ref $order_by eq 'HASH' ) {
372 21         76 @sort_items = [ %$order_by ];
373             }
374             else {
375 9         38 @sort_items = [ -asc => $order_by ];
376             }
377              
378             my @ordered = sort {
379 340         1209 for my $item ( @sort_items ) {
  217         408  
380             my $cmp = $item->[0] eq '-asc'
381             ? ($a->{ $item->[1] }//'') cmp ($b->{ $item->[1] }//'')
382 220 100 50     1260 : ($b->{ $item->[1] }//'') cmp ($a->{ $item->[1] }//'')
      50        
      50        
      50        
383             ;
384 220   100     898 return $cmp || next;
385             }
386             }
387             @$unordered;
388              
389 340         1221 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 3570 my ( $template, $item ) = @_;
416 4         31 return scalar $template =~ s/(?{$1}/reg;
  9         76  
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 2536     2536 1 4259 my ( $type, $is_type ) = @_;
438 2536 100       4396 return unless $type;
439             return ref $type eq 'ARRAY'
440 2532 100       8814 ? !!grep { $_ eq $is_type } @$type
  1186         3593  
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 660     660 1 1168 my ( $format, $is_format ) = @_;
459 660 100       2156 return unless $format;
460 255         1157 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 236     236 1 583 my @args = @_;
482 236         1364 my $key = to_json [ caller, @args ];
483 236 100       20781 return if $DERPED{ $key };
484 18 100       171 if ( $args[0] !~ /\.$/ ) {
485 17         173 $args[0] .= '.';
486             }
487 18         8820 carp sprintf( $args[0], @args[1..$#args] );
488 18         251 $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 127     127 1 20829 my ( $schema ) = @_;
502 127         1334 my $v = JSON::Validator->new( coerce => 'bool,def,num,str' );
503 127         7850 my $formats = $v->formats;
504 127     18   11627 $formats->{ password } = sub { undef };
  18         9653  
505 127     11   634 $formats->{ filepath } = sub { undef };
  11         5986  
506 127     0   573 $formats->{ markdown } = sub { undef };
  0         0  
507 127     0   594 $formats->{ tel } = sub { undef };
  0         0  
508 127     2   586 $formats->{ textarea } = sub { undef };
  2         1497  
509 127         853 return $v;
510             }
511              
512             1;
513              
514             __END__