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 9     9   14430 use strict;
  9         12  
  9         298  
3 9     9   41 use warnings;
  9         12  
  9         236  
4              
5 9     9   39 use Scalar::Util qw(blessed);
  9         12  
  9         2133  
6 9     9   10930 use Module::Load ();
  9         8747  
  9         183  
7              
8 9     9   4944 use Exporter::Declare;
  9         183475  
  9         41  
9             default_exports qw(
10             instantiate instantiate_array instantiate_selection instantiate_array_selection
11             response articulate_request
12             credentials permission
13             loc locspec
14             dpath_get dpath_set
15             hash_merge
16             throw_error
17             select_from
18             is_single_key_hash
19             );
20              
21 9     9   15578 use Articulate::Error;
  9         22  
  9         60  
22 9     9   9961 use Data::DPath qw(dpath dpathr);
  9         1037859  
  9         58  
23 9     9   7945 use Hash::Merge ();
  9         19262  
  9         248  
24              
25 9     9   60 use Articulate::Error;
  9         15  
  9         83  
26 9     9   7833 use Articulate::Credentials;
  9         28  
  9         42  
27 9     9   6529 use Articulate::File;
  9         103  
  9         269  
28 9     9   2204 use Articulate::Item;
  9         24  
  9         279  
29 9     9   3389 use Articulate::Location;
  9         22  
  9         36  
30 9     9   6253 use Articulate::LocationSpecification;
  9         28  
  9         40  
31 9     9   6375 use Articulate::Permission;
  9         30  
  9         43  
32 9     9   6816 use Articulate::Request;
  9         29  
  9         43  
33 9     9   4688 use Articulate::Response;
  9         23  
  9         55  
34              
35              
36             # sub throw_error { Articulate::Error::throw_error(@_) };
37             # sub loc { Articulate::Location::loc(@_) };
38              
39             =head3 instantiate_array
40              
41             C accepts an arrayref of values which represent objects. For each value, if it is not an object, it will attempt to instantiate one using C.
42              
43             If you pass C a value which is not an arrayref, it will assume you meant to give it an arrayref with a single item; or, if you pass it C, it will return an empty arrayref.
44              
45             The purpose of this function is to enable the following:
46              
47             package Articulate::SomeDelegatingComponent;
48             use Moo;
49             has delegates_to =>
50             is => 'rw',
51             default => sub { [] },
52             coerce => sub{ instantiate_array(@_) };
53              
54             Which means given config like the following:
55              
56             Articulate::SomeDelegatingComponent:
57             delegates_to:
58             - My::Validation::For::Articles
59             - class: My::Validation::For::Images
60             args:
61             - max_width: 1024
62             max_height: 768
63             - class: My::Validation::For::Documents
64             constructor: preset
65             args: pdf
66              
67             You can be guaranteed that looping through C<< @{ $self->delegates_to } >> will always produce objects.
68              
69             =head3 instantiate
70              
71             Attempts to create an object from the hashref or class name provided.
72              
73             If the value is a string, it will treat as a class name, and perform C<< $class->new >>, or, if the method exists, C<< $class->instance >> will be preferred (for instance, as provided by C).
74              
75             If the value is a hashref, it will look at the values for the keys C, C, and C. It will then attempt to perform C<< $class->$constructor(@$args) >>, unless the constructor is absent (in which case C or C will be supplied), or if C is not an arrayref, in which case it will be passed to the constructor as a single argument (or the empty list will be passed if C is undefined).
76              
77             If the value is an object, the object will simply be returned.
78              
79             =cut
80              
81             sub instantiate {
82 260     260 1 7079 my $original = shift;
83 260 100       861 if ( blessed $original ) {
    100          
    50          
84 156         540 return $original;
85             }
86             elsif ( !ref $original ) {
87 64         228 Module::Load::load($original);
88 64 100       3314 if ( $original->can('instance') ) {
89 1         4 return $original->instance()
90             }
91             else {
92 63         410 return $original->new();
93             }
94             }
95             elsif ( ref $original eq ref {} ) {
96 40         77 my $class = $original->{class};
97 40         63 my $args = $original->{args};
98 40 100 100     299 if ( 1 == keys %$original and join ( '', keys %$original ) !~ /^[a-z_]/ ) { # single key that looks like a class
99 28         79 $class = join '', keys %$original;
100 28         80 $args = $original->{$class};
101             }
102 40 50       98 throw_error Internal => 'Instantiation failed: expecting key class, got '.(join ', ', keys %$original) unless defined $class;
103 40         141 Module::Load::load ( $class );
104 40 100 66     5109 my $constructor = $original->{constructor} // ($class->can('instance') ? 'instance' : 'new');
105 40 100       208 my @args = (
    100          
106             (defined $args)
107             ? (ref $args eq ref [])
108             ? @$args
109             : $args
110             : ()
111             );
112 40         244 return $class->$constructor(@args);
113             }
114             }
115              
116             sub instantiate_array {
117 59     59 1 2806 my $arrayref = shift;
118 59 50       163 return [] unless defined $arrayref;
119             # delegates_to => "Class::Name" should be interpreted as delegates_to => ["Class::Name"]
120 59 100 100     357 $arrayref = [$arrayref] unless ref $arrayref and ref $arrayref eq ref [];
121 59         259 return [ map { instantiate $_ } @$arrayref ];
  85         3412  
122             }
123              
124             =head3 from_meta
125              
126             sub email_address { from_meta (shift, 'schema/user/email_address'); }
127              
128             This method uses Data::DPath to retrieve a field from the metadata structure.
129              
130             =cut
131              
132             sub from_meta {
133 0     0 1 0 my $structure = shift;
134 0         0 my $item = shift;
135 0         0 my @results = dpath ($item->meta)->match($structure);
136 0         0 return shift @results;
137             }
138              
139             =head3 dpath_get
140              
141             my $value = dpath_get($structure, '/path/in/structure');
142              
143             =cut
144              
145             sub dpath_get {
146 23     23 1 1438 my $structure = shift;
147 23         27 my $path = shift;
148 23         57 my @results = dpath($path)->match($structure);
149 23         6427 return shift @results;
150             }
151              
152             =head3 dpath_set
153              
154             dpath_set($structure, '/path/in/structure', $value);
155              
156             =cut
157              
158             sub dpath_set {
159 1     1 1 2 my $structure = shift;
160 1         2 my $path = shift;
161 1         1 my $value = shift;
162 1         5 my @results = dpathr($path)->match($structure);
163 1         223 map { $$_ = $value } @results;
  2         3  
164 1 50       6 return $value if @results;
165             }
166              
167             =head3 instantiate_selection
168              
169             # in config:
170              
171             rules:
172             default:
173             alias: main_schema
174             main_schema:
175             dsn: 'csv:main.csv'
176             utf8: 1
177              
178             has schemata =>
179             is => 'rw',
180             default => sub { {} },
181             coerce => sub { instantiate_selection @_ };
182              
183             Expects a hash. If any of the values are single-key hashes with the key 'alias' then the alias is resolved. Otherwise the value is instantiated.
184              
185             If a value other than a hash is given, returns a hash with the key 'default' and the original value instantiated.
186              
187             =cut
188              
189             sub instantiate_selection {
190 5     5 1 1958 my $orig = shift;
191 5         21 _instantiate_selection( $orig, \&instantiate );
192             }
193              
194             =head3 instantiate_array_selection
195              
196             rules:
197             default:
198             alias: two_rules
199             two_rules:
200             - Some::Rule
201             - Some::Other::Rule
202              
203             has rules =>
204             is => 'rw',
205             default => sub { {} },
206             coerce => sub { instantiate_array_selection @_ };
207              
208             Expects a hash. If any of the values are single-key hashes with the key 'alias' then the alias is resolved. Otherwise the value is instantiated as an array (see C).
209              
210             If a value other than a hash is given, returns a hash with the key 'default' and the original value instantiated as an array.
211              
212             =cut
213              
214              
215             sub instantiate_array_selection {
216 2     2 1 2163 my $orig = shift;
217 2         5 _instantiate_selection( $orig, \&instantiate_array );
218             }
219              
220             sub _instantiate_selection {
221 7     7   89 my $orig = shift;
222 7         9 my $instantiate = shift;
223 7 100       27 if ( ref $orig eq ref {} ) {
224 5         14 for my $i ( 1..5 ) {
225 25         3143 foreach my $this ( keys %$orig ) {
226 180         10581 my $got = $orig->{$this};
227 180 100       268 if ( is_single_key_hash( $got, 'alias' ) ) {
228 8 100       22 $orig->{$this} = $orig->{ $got->{alias} } if $i > 1;
229             }
230             else {
231 172         255 $orig->{$this} = $instantiate->($got);
232             }
233             }
234             }
235 5         87 return $orig;
236             }
237             else {
238 2         5 return { default => $instantiate->($orig) };
239             }
240             }
241              
242             =head3 select_from
243              
244             # given this config:
245              
246             schemata:
247             default:
248             "[Complicated] configuration: can_be->[string, hash, whatever]"
249             schema_generic:
250             alias: default
251              
252             # if your class has
253             sub schema { select_from schemata => @_ }
254              
255             # then you can do
256             $self->schema;
257             $self->schema('default'); # same thing
258             $self->schema('schema_generic'); # same thing, because of alias
259             $self->schemata->{default}; # This is what they all do in practice
260              
261             Implements a user-friendly selection mechanism like the one implemented by C.
262              
263             =cut
264              
265             sub select_from {
266 0     0 1 0 my ($attribute, $self, $which) = @_;
267 0   0     0 $which //= 'default';
268 0         0 my $this = $which;
269 0         0 my $selectables = $self->$attribute;
270 0         0 for ( 1..5 ) { # if more than this then you probably have recusion
271 0         0 my $got = $selectables->{$this};
272 0 0       0 if ( is_single_key_hash( $got, 'alias' ) ) {
273 0         0 $this = $got->{alias};
274 0         0 next;
275             }
276 0         0 return $got;
277             }
278             }
279              
280             =head3 is_single_key_hash
281              
282             is_single_key_hash ( { foo => 123 } ); # returns 1
283             is_single_key_hash ( { foo => 123 }, 'foo' ); # returns 1
284             is_single_key_hash ( { foo => 123 }, 'bar' ); # returns 0
285              
286             Returns 1 if the first argument is a hashref with exactly one key. If a second argument is provided, then the key, if it exists, must be equal to that argument, or the return value will be 0.
287              
288             =cut
289              
290             sub is_single_key_hash {
291 187     187 1 1186 my $got = shift;
292 187         174 my $key = shift;
293 187 100 100     1134 return 1 if ( defined $key and ref $got eq ref {} and 1 == scalar keys %$got and $key eq [keys %$got]->[0] );
      100        
      100        
294 178 100 100     425 return 1 if ( !defined $key and ref $got eq ref {} and 1 == scalar keys %$got );
      100        
295 177         311 return 0;
296             }
297              
298             =head3 hash_merge
299              
300             my $merged = hash_merge ($parent, $child)
301              
302             Returns a new hashref whose values represent a union of the parent's and the child's. The child's values overwrite the parent, in case of conflict. The merge is deep (i.e. it handles nested hashes), using L with right precedence.
303              
304             =cut
305              
306             my $hash_merger = Hash::Merge->new('RIGHT_PRECEDENT');
307              
308             sub hash_merge { # very naive, will change to something like Hash::Merge
309 0     0 1   my ($parent, $child) = @_;
310 0           return $hash_merger->merge($parent, $child); # todo: more
311             }
312              
313             1;