File Coverage

blib/lib/Articulate/Syntax.pm
Criterion Covered Total %
statement 104 120 86.6
branch 30 36 83.3
condition 23 26 88.4
subroutine 25 28 89.2
pod 10 10 100.0
total 192 220 87.2


line stmt bran cond sub pod time code
1             package Articulate::Syntax;
2 11     11   18362 use strict;
  11         15  
  11         350  
3 11     11   51 use warnings;
  11         42  
  11         291  
4              
5 11     11   43 use Scalar::Util qw(blessed);
  11         10  
  11         788  
6 11     11   16875 use Module::Load ();
  11         9712  
  11         220  
7              
8 11     11   5640 use Exporter::Declare;
  11         200289  
  11         49  
9             default_exports qw(
10             instantiate instantiate_array instantiate_selection instantiate_array_selection
11             new_response new_request
12             new_credentials new_permission
13             new_location new_location_specification
14             dpath_get dpath_set
15             hash_merge
16             throw_error
17             select_from
18             is_single_key_hash
19             );
20              
21 11     11   16310 use Articulate::Error;
  11         29  
  11         82  
22 11     11   10332 use Data::DPath qw(dpath dpathr);
  11         1115966  
  11         75  
23 11     11   13268 use Hash::Merge ();
  11         23030  
  11         269  
24              
25 11     11   68 use Articulate::Error;
  11         18  
  11         108  
26 11     11   8335 use Articulate::Credentials;
  11         30  
  11         45  
27 11     11   6916 use Articulate::File;
  11         27  
  11         309  
28 11     11   2892 use Articulate::Item;
  11         21  
  11         290  
29 11     11   3734 use Articulate::Location;
  11         28  
  11         42  
30 11     11   7141 use Articulate::LocationSpecification;
  11         29  
  11         40  
31 11     11   7028 use Articulate::Permission;
  11         27  
  11         49  
32 11     11   7033 use Articulate::Request;
  11         30  
  11         44  
33 11     11   5056 use Articulate::Response;
  11         23  
  11         50  
34              
35             # sub throw_error { Articulate::Error::throw_error(@_) };
36             # sub new_location { Articulate::Location::new_location(@_) };
37              
38             =head1 NAME
39              
40             Articulate::Syntax - Common functions and syntactic sugar for
41             Articulate
42              
43             =head1 FUNCTIONS
44              
45             =head3 new_response
46              
47             See L.
48              
49             =head3 new_request
50              
51             See L.
52              
53             =head3 new_credentials
54              
55             See L.
56              
57             =head3 new_permission
58              
59             See L.
60              
61             =head3 new_location
62              
63             See L.
64              
65             =head3 new_location_specification
66              
67             See L.
68              
69             =cut
70              
71             =head3 instantiate_array
72              
73             C accepts an arrayref of values which represent
74             objects. For each value, if it is not an object, it will attempt to
75             instantiate one using C.
76              
77             If you pass C a value which is not an arrayref, it
78             will assume you meant to give it an arrayref with a single item; or, if
79             you pass it C, it will return an empty arrayref.
80              
81             The purpose of this function is to enable the following:
82              
83             package Articulate::SomeDelegatingComponent;
84             use Moo;
85             has delegates_to =>
86             is => 'rw',
87             default => sub { [] },
88             coerce => sub{ instantiate_array(@_) };
89              
90             Which means given config like the following:
91              
92             Articulate::SomeDelegatingComponent:
93             delegates_to:
94             - My::Validation::For::Articles
95             - class: My::Validation::For::Images
96             args:
97             - max_width: 1024
98             max_height: 768
99             - class: My::Validation::For::Documents
100             constructor: preset
101             args: pdf
102              
103             You can be guaranteed that looping through C<< @{ $self->delegates_to }
104             >> will always produce objects.
105              
106             =head3 instantiate
107              
108             Attempts to create an object from the hashref or class name provided.
109              
110             If the value is a string, it will treat as a class name, and perform
111             C<< $class->new >>, or, if the method exists, C<< $class->instance >>
112             will be preferred (for instance, as provided by C).
113              
114             If the value is a hashref, it will look at the values for the keys
115             C, C, and C. It will then attempt to perform
116             C<< $class->$constructor(@$args) >>, unless the constructor is absent
117             (in which case C or C will be supplied), or if C
118             is not an arrayref, in which case it will be passed to the constructor
119             as a single argument (or the empty list will be passed if C is
120             undefined).
121              
122             If the value is an object, the object will simply be returned.
123              
124             =cut
125              
126             sub instantiate {
127 424     424 1 6947 my $original = shift;
128 424 100       1278 if ( blessed $original ) {
    100          
    50          
129 255         662 return $original;
130             }
131             elsif ( !ref $original ) {
132 104         298 Module::Load::load($original);
133 104 100       3662 if ( $original->can('instance') ) {
134 1         4 return $original->instance();
135             }
136             else {
137 103         554 return $original->new();
138             }
139             }
140             elsif ( ref $original eq ref {} ) {
141 65         109 my $class = $original->{class};
142 65         90 my $args = $original->{args};
143 65 100 100     551 if ( 1 == keys %$original and join( '', keys %$original ) !~ /^[a-z_]/ )
144             { # single key that looks like a class
145 51         143 $class = join '', keys %$original;
146 51         150 $args = $original->{$class};
147             }
148 65 50       154 throw_error Internal => 'Instantiation failed: expecting key class, got '
149             . ( join ', ', keys %$original )
150             unless defined $class;
151 65         194 Module::Load::load($class);
152 64 100 66     4714 my $constructor = $original->{constructor}
153             // ( $class->can('instance') ? 'instance' : 'new' );
154 64 100       376 my @args = (
    100          
155             ( defined $args )
156             ? ( ref $args eq ref [] )
157             ? @$args
158             : $args
159             : ()
160             );
161 64         282 return $class->$constructor(@args);
162             }
163             }
164              
165             sub instantiate_array {
166 83     83 1 3869 my $arrayref = shift;
167 83 50       258 return [] unless defined $arrayref;
168              
169             # delegates_to => "Class::Name" should be interpreted as delegates_to => ["Class::Name"]
170 83 100 100     492 $arrayref = [$arrayref] unless ref $arrayref and ref $arrayref eq ref [];
171 83         374 return [ map { instantiate $_ } @$arrayref ];
  124         4976  
172             }
173              
174             =head3 from_meta
175              
176             sub email_address { from_meta (shift, 'schema/user/email_address'); }
177              
178             This method uses Data::DPath to retrieve a field from the metadata
179             structure.
180              
181             =cut
182              
183             sub from_meta {
184 0     0 1 0 my $structure = shift;
185 0         0 my $item = shift;
186 0         0 my @results = dpath( $item->meta )->match($structure);
187 0         0 return shift @results;
188             }
189              
190             =head3 dpath_get
191              
192             my $value = dpath_get($structure, '/path/in/structure');
193              
194             =cut
195              
196             sub dpath_get {
197 23     23 1 1919 my $structure = shift;
198 23         28 my $path = shift;
199 23         57 my @results = dpath($path)->match($structure);
200 23         5832 return shift @results;
201             }
202              
203             =head3 dpath_set
204              
205             dpath_set($structure, '/path/in/structure', $value);
206              
207             =cut
208              
209             sub dpath_set {
210 1     1 1 3 my $structure = shift;
211 1         2 my $path = shift;
212 1         2 my $value = shift;
213 1         5 my @results = dpathr($path)->match($structure);
214 1         241 map { $$_ = $value } @results;
  2         3  
215 1 50       8 return $value if @results;
216             }
217              
218             =head3 instantiate_selection
219              
220             # in config:
221              
222             rules:
223             default:
224             alias: main_schema
225             main_schema:
226             dsn: 'csv:main.csv'
227             utf8: 1
228              
229             has schemata =>
230             is => 'rw',
231             default => sub { {} },
232             coerce => sub { instantiate_selection @_ };
233              
234             Expects a hash. If any of the values are single-key hashes with the key
235             'alias' then the alias is resolved. Otherwise the value is
236             instantiated.
237              
238             If a value other than a hash is given, returns a hash with the key
239             'default' and the original value instantiated.
240              
241             =cut
242              
243             sub instantiate_selection {
244 7     7 1 2616 my $orig = shift;
245 7         34 _instantiate_selection( $orig, \&instantiate );
246             }
247              
248             =head3 instantiate_array_selection
249              
250             rules:
251             default:
252             alias: two_rules
253             two_rules:
254             - Some::Rule
255             - Some::Other::Rule
256              
257             has rules =>
258             is => 'rw',
259             default => sub { {} },
260             coerce => sub { instantiate_array_selection @_ };
261              
262             Expects a hash. If any of the values are single-key hashes with the key
263             'alias' then the alias is resolved. Otherwise the value is instantiated
264             as an array (see C).
265              
266             If a value other than a hash is given, returns a hash with the key
267             'default' and the original value instantiated as an array.
268              
269             =cut
270              
271             sub instantiate_array_selection {
272 2     2 1 2985 my $orig = shift;
273 2         7 _instantiate_selection( $orig, \&instantiate_array );
274             }
275              
276             sub _instantiate_selection {
277 9     9   13 my $orig = shift;
278 9         14 my $instantiate = shift;
279 9 100       34 if ( ref $orig eq ref {} ) {
280 7         197 for my $i ( 1 .. 5 ) {
281 35         1095 foreach my $this ( keys %$orig ) {
282 305         18984 my $got = $orig->{$this};
283 305 100       438 if ( is_single_key_hash( $got, 'alias' ) ) {
284 9 100       31 $orig->{$this} = $orig->{ $got->{alias} } if $i > 1;
285             }
286             else {
287 296         460 $orig->{$this} = $instantiate->($got);
288             }
289             }
290             }
291 7         142 return $orig;
292             }
293             else {
294 2         4 return { default => $instantiate->($orig) };
295             }
296             }
297              
298             =head3 select_from
299              
300             # given this config:
301              
302             schemata:
303             default:
304             "[Complicated] configuration: can_be->[string, hash, whatever]"
305             schema_generic:
306             alias: default
307              
308             # if your class has
309             sub schema { select_from schemata => @_ }
310              
311             # then you can do
312             $self->schema;
313             $self->schema('default'); # same thing
314             $self->schema('schema_generic'); # same thing, because of alias
315             $self->schemata->{default}; # This is what they all do in practice
316              
317             Implements a user-friendly selection mechanism like the one implemented
318             by C.
319              
320             =cut
321              
322             sub select_from {
323 0     0 1 0 my ( $attribute, $self, $which ) = @_;
324 0   0     0 $which //= 'default';
325 0         0 my $this = $which;
326 0         0 my $selectables = $self->$attribute;
327 0         0 for ( 1 .. 5 ) { # if more than this then you probably have recusion
328 0         0 my $got = $selectables->{$this};
329 0 0       0 if ( is_single_key_hash( $got, 'alias' ) ) {
330 0         0 $this = $got->{alias};
331 0         0 next;
332             }
333 0         0 return $got;
334             }
335             }
336              
337             =head3 is_single_key_hash
338              
339             is_single_key_hash ( { foo => 123 } ); # returns 1
340             is_single_key_hash ( { foo => 123 }, 'foo' ); # returns 1
341             is_single_key_hash ( { foo => 123 }, 'bar' ); # returns 0
342              
343             Returns 1 if the first argument is a hashref with exactly one key. If a
344             second argument is provided, then the key, if it exists, must be equal
345             to that argument, or the return value will be 0.
346              
347             =cut
348              
349             sub is_single_key_hash {
350 312     312 1 1781 my $got = shift;
351 312         287 my $key = shift;
352 312 100 100     1868 return 1
      100        
      100        
353             if (defined $key
354             and ref $got eq ref {}
355             and 1 == scalar keys %$got
356             and $key eq [ keys %$got ]->[0] );
357 302 100 100     698 return 1
      100        
358             if ( !defined $key and ref $got eq ref {} and 1 == scalar keys %$got );
359 301         504 return 0;
360             }
361              
362             =head3 hash_merge
363              
364             my $merged = hash_merge ($parent, $child)
365              
366             Returns a new hashref whose values represent a union of the parent's
367             and the child's. The child's values overwrite the parent, in case of
368             conflict. The merge is deep (i.e. it handles nested hashes), using
369             L with right precedence.
370              
371             =cut
372              
373             my $hash_merger = Hash::Merge->new('RIGHT_PRECEDENT');
374              
375             sub hash_merge { # very naive, will change to something like Hash::Merge
376 0     0 1   my ( $parent, $child ) = @_;
377 0           return $hash_merger->merge( $parent, $child ); # todo: more
378             }
379              
380             1;