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.087';
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   10295364 use Mojo::Base '-strict';
  23         224  
  23         143  
35 23     23   3112 use Exporter 'import';
  23         64  
  23         821  
36 23     23   164 use List::Util qw( all any none first );
  23         62  
  23         2159  
37 23     23   1204 use Mojo::Loader qw( load_class );
  23         84026  
  23         1309  
38 23     23   173 use Scalar::Util qw( blessed );
  23         59  
  23         1299  
39 23     23   1946 use Mojo::JSON::Pointer;
  23         2483  
  23         230  
40 23     23   2478 use Mojo::JSON qw( to_json );
  23         76932  
  23         1409  
41 23     23   181 use Mojo::Util qw( xml_escape );
  23         47  
  23         1215  
42 23     23   408 use Carp qw( carp );
  23         58  
  23         1364  
43 23     23   12960 use JSON::Validator;
  23         1543072  
  23         229  
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 89090 my ( $config, $schema ) = @_;
92 85         246 my ( $type, $arg );
93 85 100       387 if ( !ref $config ) {
    50          
94 84         564 ( $type ) = $config =~ m{^([^:]+)};
95 84   33     661 $type = $TYPE_ALIAS{ $type } // $type;
96 84         229 $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         4 ( $type, $arg ) = %{ $config };
  1         4  
108             }
109 85         458 my $class = 'Yancy::Backend::' . ucfirst $type;
110 85 100       429 if ( my $e = load_class( $class ) ) {
111 2 100       2330 die ref $e ? "Could not load class $class: $e" : "Could not find class $class";
112             }
113 83         2322 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 6747 my ( $sub, @args ) = @_;
142 602     459   3964 return sub { $sub->( @args, @_ ) };
  459         2034039  
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 24392 my ( $obj, $meth, @args ) = @_;
158 500   100     3144 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         1434 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 51382 my ( $schema, $pointer, $usschema, $uspointer, $refmap ) = @_;
186 21682   100     40297 $usschema //= Mojo::JSON::Pointer->new( $schema )->get( $pointer );
187 21682   100     59255 $uspointer //= '';
188 21682   100     34481 $refmap ||= {};
189 21682 100       38186 return { '$ref' => $refmap->{ $uspointer } } if $refmap->{ $uspointer };
190             $refmap->{ $pointer } = "#$uspointer"
191 21680 100 100     75382 unless ref $usschema eq 'HASH' and $usschema->{'$ref'};
192 21680 100 100     104277 return $usschema
193             unless ref $usschema eq 'ARRAY' or ref $usschema eq 'HASH';
194 6627         8503 my $counter = 0;
195 6627 100       16850 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         6378 my $ref = $usschema->{'$ref'};
204 4541 100       17649 return { map { $_ => copy_inline_refs(
205             $schema,
206             $pointer.'/'.$_,
207 16642         47490 $usschema->{ $_ },
208             $uspointer.'/'.$_,
209             $refmap,
210             ) } sort keys %$usschema } if !$ref;
211 14         49 $ref =~ s:^#::;
212 14 100       62 return { '$ref' => $refmap->{ $ref } } if $refmap->{ $ref };
213 4         14 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 6642 my ( $match, $item ) = @_;
235 607 100       1338 return undef if !defined $item;
236              
237 604 100       1409 if ( ref $match eq 'ARRAY' ) {
238 3     4   24 return any { match( $_, $item ) } @$match;
  4         22  
239             }
240              
241 601         966 my %test;
242 601         1390 for my $key ( keys %$match ) {
243 360 100       1443 if ( $key =~ /^-(not_)?bool/ ) {
    100          
    100          
    50          
244 17         53 my $want_false = $1;
245 17         39 $key = $match->{ $key }; # the actual field
246             $test{ $key } = sub {
247 17     17   43 my ( $value, $key ) = @_;
248 17 100       66 return $want_false ? !$value : !!$value;
249 17         98 };
250             }
251             elsif ( !ref $match->{ $key } ) {
252 234         603 $test{ $key } = $match->{ $key };
253             }
254             elsif ( ref $match->{ $key } eq 'HASH' ) {
255 105 100 100     501 if ( my $value = $match->{ $key }{ -like } || $match->{ $key }{ like } ) {
    100          
    100          
    50          
256 78         193 $value = quotemeta $value;
257 78         498 $value =~ s/(?
258 78         1201 $test{ $key } = qr{^$value$};
259             }
260             elsif ( $value = $match->{ $key }{ -has } ) {
261 9         15 my $expect = $value;
262             $test{ $key } = sub {
263 9     9   20 my ( $value, $key ) = @_;
264 9 100       33 return 0 if !defined $value;
265 8 100       28 if ( ref $value eq 'ARRAY' ) {
    50          
266 4 100       13 if ( ref $expect eq 'ARRAY' ) {
    50          
267 2         12 return all { my $e = $_; any { $_ eq $e } @$value } @$expect;
  4         10  
  4         14  
  8         22  
268             }
269             elsif ( !ref $expect ) {
270 2         14 return any { $_ eq $expect } @$value;
  4         16  
271             }
272             }
273             elsif ( ref $value eq 'HASH' ) {
274 4 50       8 if ( ref $expect eq 'HASH' ) {
275 4         9 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         54 };
285             }
286             elsif ( $value = $match->{ $key }{ -not_has } ) {
287             $test{ $key } = sub {
288 9     9   13 my $expect = $value;
289 9         23 my ( $value, $key ) = @_;
290 9 100       22 return 1 if !defined $value;
291 8 100       21 if ( ref $value eq 'ARRAY' ) {
    50          
292 4 100       11 if ( ref $expect eq 'ARRAY' ) {
    50          
293 2         11 return all { my $e = $_; none { $_ eq $e } @$value } @$expect;
  3         7  
  3         14  
  7         22  
294             }
295             elsif ( !ref $expect ) {
296 2         21 return none { $_ eq $expect } @$value;
  4         28  
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       9 if ( ref $expect eq 'HASH' ) {
304 4         13 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         55 };
314             }
315             elsif ( exists $match->{ $key }{ '!=' } ) {
316 9         19 my $expect = $match->{ $key }{ '!=' };
317             $test{ $key } = sub {
318 9     9   26 my ( $got, $key ) = @_;
319 9 50 66     31 if ( !defined $expect || !defined $got) {
320 9         33 return defined $got != defined $expect;
321             }
322 0         0 return $got ne $expect;
323 9         50 };
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         11 my @tests = @{ $match->{ $key } };
  4         18  
331             # Array is an 'OR' combiner
332             $test{ $key } = sub {
333 4     4   14 my ( $value, $key ) = @_;
334 4         11 my $sub_item = { $key => $value };
335 4         27 return any { match( { $key => $_ }, $sub_item ) } @tests;
  6         28  
336 4         28 };
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         1386 !defined $test{ $_ } ? !defined $item->{ $_ }
346             : ref $test{ $_ } eq 'Regexp' ? $item->{ $_ } =~ $test{ $_ }
347             : ref $test{ $_ } eq 'CODE' ? $test{ $_ }->( $item->{ $_ }, $_ )
348 360 100 100     2546 : ($item->{ $_ }//'') eq ($test{ $_ }//'')
    100 50        
    100          
349             }
350             keys %test;
351              
352 601         2891 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 3520 my ( $order_by, $unordered ) = @_;
365             # Array of [ (-asc/-desc), (field) ]
366 331         535 my @sort_items;
367              
368 331 100       1002 if ( ref $order_by eq 'ARRAY' ) {
    100          
369 301 100       640 @sort_items = map { [ ref $_ ? %$_ : ( -asc => $_ ) ] } @$order_by;
  311         1380  
370             }
371             elsif ( ref $order_by eq 'HASH' ) {
372 21         85 @sort_items = [ %$order_by ];
373             }
374             else {
375 9         37 @sort_items = [ -asc => $order_by ];
376             }
377              
378             my @ordered = sort {
379 331         1383 for my $item ( @sort_items ) {
  210         464  
380             my $cmp = $item->[0] eq '-asc'
381             ? ($a->{ $item->[1] }//'') cmp ($b->{ $item->[1] }//'')
382 213 100 50     1401 : ($b->{ $item->[1] }//'') cmp ($a->{ $item->[1] }//'')
      50        
      50        
      50        
383             ;
384 213   100     1033 return $cmp || next;
385             }
386             }
387             @$unordered;
388              
389 331         1210 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 4170 my ( $template, $item ) = @_;
416 4         38 return scalar $template =~ s/(?{$1}/reg;
  9         106  
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 4088 my ( $type, $is_type ) = @_;
438 2506 100       4248 return unless $type;
439             return ref $type eq 'ARRAY'
440 2502 100       9026 ? !!grep { $_ eq $is_type } @$type
  1186         3693  
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 1209 my ( $format, $is_format ) = @_;
459 656 100       2164 return unless $format;
460 255         989 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 525 my @args = @_;
482 230         1392 my $key = to_json [ caller, @args ];
483 230 100       20540 return if $DERPED{ $key };
484 18 100       209 if ( $args[0] !~ /\.$/ ) {
485 17         97 $args[0] .= '.';
486             }
487 18         9421 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 20113 my ( $schema ) = @_;
502 123         1270 my $v = JSON::Validator->new( coerce => 'bool,def,num,str' );
503 123         7840 my $formats = $v->formats;
504 123     18   10869 $formats->{ password } = sub { undef };
  18         10307  
505 123     11   612 $formats->{ filepath } = sub { undef };
  11         5313  
506 123     0   567 $formats->{ markdown } = sub { undef };
  0         0  
507 123     0   567 $formats->{ tel } = sub { undef };
  0         0  
508 123     2   585 $formats->{ textarea } = sub { undef };
  2         1313  
509 123         793 return $v;
510             }
511              
512             1;
513              
514             __END__