File Coverage

blib/lib/Resource/Silo/Metadata.pm
Criterion Covered Total %
statement 157 157 100.0
branch 82 86 95.3
condition 47 56 83.9
subroutine 18 18 100.0
pod 5 5 100.0
total 309 322 95.9


line stmt bran cond sub pod time code
1             package Resource::Silo::Metadata;
2              
3 29     29   192 use strict;
  29         55  
  29         826  
4 29     29   141 use warnings;
  29         52  
  29         1100  
5             our $VERSION = '0.10';
6              
7             =head1 NAME
8              
9             Resource::Silo::Metadata - resource container metadata for L.
10              
11             =head1 DESCRIPTION
12              
13             This class stores information about available resources in a specific
14             container class. Normally only used internally.
15              
16             See also L.
17              
18             =head1 METHODS
19              
20             =cut
21              
22 29     29   144 use Carp;
  29         63  
  29         1469  
23 29     29   14961 use Module::Load qw( load );
  29         34084  
  29         181  
24 29     29   2195 use Scalar::Util qw( looks_like_number reftype );
  29         63  
  29         1599  
25 29     29   14394 use Sub::Quote qw( quote_sub );
  29         144797  
  29         30611  
26              
27             my $BARE_REX = '[a-z][a-z_0-9]*';
28             my $ID_REX = qr(^$BARE_REX$)i;
29             my $MOD_REX = qr(^$BARE_REX(?:::$BARE_REX)*$)i;
30              
31             # Define possible reftypes portably
32             my $CODE = reftype sub { };
33             my $REGEXP = ref qr/.../;
34 73     73   290 sub _is_empty { $_[0] eq '' };
35              
36             =head2 new( $target )
37              
38             $target is the name of the module where resource access methods will be created.
39              
40             =cut
41              
42             sub new {
43 35     35 1 105 my ($class, $target) = @_;
44 35         165 return bless {
45             target => $target,
46             preload => [],
47             }, $class;
48             };
49              
50             =head2 add( $resource_name, ... )
51              
52             Create resource type. See L for details.
53              
54             =cut
55              
56             my %known_args = (
57             argument => 1,
58             class => 1,
59             dependencies => 1,
60             derived => 1,
61             cleanup => 1,
62             cleanup_order => 1,
63             fork_cleanup => 1,
64             ignore_cache => 1,
65             init => 1,
66             literal => 1,
67             loose_deps => 1,
68             preload => 1,
69             require => 1,
70             );
71             sub add {
72 100     100 1 210 my $self = shift;
73 100         198 my $name = shift;
74 100 100       349 if (@_ % 2) {
75 37         72 my $init = pop @_;
76 37         127 unshift @_, init => $init;
77             }
78 100         341 my (%spec) = @_;
79 100         313 my $target = $self->{target};
80              
81 100 100 100     1307 croak "resource: name must be an identifier"
      100        
82             unless defined $name and !ref $name and $name =~ $ID_REX;
83             croak "resource: attempt to redefine resource '$name'"
84 95 100       405 if defined $self->{resource}{$name};
85 94 100       1032 croak "resource: attempt to replace existing method '$name' in $target"
86             if $target->can($name);
87              
88 92         330 my @extra = grep { !$known_args{$_} } keys %spec;
  159         536  
89 92 100       286 croak "resource '$name': unknown arguments in specification: @extra"
90             if @extra;
91              
92             {
93             # validate 'require' before 'class'
94 91 100       159 if (!ref $spec{require}) {
  91         250  
95 86 100       325 $spec{require} = defined $spec{require} ? [ $spec{require} ] : [];
96             };
97             croak "resource '$name': 'require' must be a module name or a list thereof"
98 91 100       311 unless ref $spec{require} eq 'ARRAY';
99 90         157 my @bad = grep { $_ !~ $MOD_REX } @{ $spec{require} };
  9         66  
  90         234  
100             croak "resource '$name': 'require' doesn't look like module name(s): "
101 90 100       269 .join ", ", map { "'$_'" } @bad
  2         15  
102             if @bad;
103             };
104              
105 89 100       296 if (defined (my $value = $spec{literal})) {
106             defined $spec{$_}
107             and croak "resource '$name': 'literal' is incompatible with '$_'"
108 4   66     55 for qw( init class argument );
109 2     1   13 $spec{init} = sub { $value };
  1         9  
110 2   50     13 $spec{dependencies} //= [];
111 2   50     11 $spec{derived} //= 1;
112 2   50     56 $spec{cleanup_order} //= 9 ** 9 ** 9;
113             };
114              
115             _make_init_class($self, $name, \%spec)
116 87 100       282 if (defined $spec{class});
117              
118 81 100       250 if (my $deps = delete $spec{dependencies}) {
119 14 100       58 croak "resource '$name': 'dependencies' must be an array"
120             unless ref $deps eq 'ARRAY';
121 13         41 my @bad = grep { !/$ID_REX/ } @$deps;
  12         122  
122             croak "resource '$name': illegal dependency name(s): "
123 13 100       84 .join ", ", map { "'$_'" } @bad
  2         34  
124             if @bad;
125 11         32 $spec{allowdeps} = { map { $_ => 1 } @$deps };
  10         42  
126             };
127              
128 78 100       195 unless ($spec{loose_deps}) {
129             # resources with argument should be allowed to depend on themselves
130             local $self->{resource}{$name} = {}
131 73 100       233 if defined $spec{argument};
132              
133 73 100       243 if ($spec{allowdeps}) {
134 8         21 my @fwd = grep { !$self->{resource}{$_} } keys %{ $spec{allowdeps} };
  7         36  
  8         27  
135             croak "resource '$name': forward dependencies require 'loose_deps' flag: "
136 8 100       27 .join( ", ", map { "'$_'" } @fwd)
  1         22  
137             if @fwd;
138             } else {
139 65         136 $spec{autodeps} = 1;
140             $spec{allowdeps} = {
141 65         118 map { $_ => 1 } keys %{ $self->{resource} },
  70         250  
  65         238  
142             };
143             };
144             };
145              
146             croak "resource '$name': 'init' must be a function"
147 77 100 100     629 unless ref $spec{init} and reftype $spec{init} eq $CODE;
148              
149 74 100 100     303 if (!defined $spec{argument}) {
    100          
    100          
150 65         177 $spec{orig_argument} = '';
151 65         184 $spec{argument} = \&_is_empty;
152             } elsif (ref $spec{argument} eq $REGEXP) {
153 6         135 my $rex = qr(^(?:$spec{argument})$);
154 6         34 $spec{orig_argument} = $spec{argument};
155 6     24   63 $spec{argument} = sub { $_[0] =~ $rex };
  24         160  
156             } elsif ((reftype $spec{argument} // '') eq $CODE) {
157             # do nothing, we're fine
158             } else {
159 1         13 croak "resource '$name': 'argument' must be a regexp or function";
160             }
161              
162 73   100     413 $spec{cleanup_order} //= 0;
163             croak "resource '$name': 'cleanup_order' must be a number"
164 73 100       397 unless looks_like_number($spec{cleanup_order});
165              
166             croak "resource '$name': 'cleanup*' is useless while 'ignore_cache' is in use"
167             if $spec{ignore_cache} and (
168             defined $spec{cleanup}
169             or defined $spec{fork_cleanup}
170 72 100 66     272 or $spec{cleanup_order} != 0
      66        
171             );
172              
173             croak "resource '$name': 'cleanup' must be a function"
174 70 100 100     380 if defined $spec{cleanup} and (reftype $spec{cleanup} // '') ne $CODE;
      100        
175             croak "resource '$name': 'fork_cleanup' must be a function"
176 68 100 100     297 if defined $spec{fork_cleanup} and (reftype $spec{fork_cleanup} // '') ne $CODE;
      100        
177              
178 66 100       188 if ($spec{preload}) {
179 1         2 push @{ $self->{preload} }, $name;
  1         4  
180             };
181              
182 66         4319 $spec{origin} = Carp::shortmess("declared");
183 66         15030 $spec{origin} =~ s/\s+$//s;
184 66         300 $self->{resource}{$name} = \%spec;
185              
186             # Move code generation into Resource::Silo::Container
187             # so that exceptions via croak() are attributed correctly.
188             {
189 29     29   264 no strict 'refs'; ## no critic Strictures
  29         95  
  29         33611  
  66         131  
190 66         271 *{"${target}::$name"} =
  66         355  
191             Resource::Silo::Container::_make_resource_accessor($name, \%spec);
192             }
193              
194 66         339 return $self;
195             };
196              
197             sub _make_init_class {
198 8     8   59 my ($self, $name, $spec) = @_;
199              
200 8         17 my $class = $spec->{class};
201 8   100     25 $spec->{dependencies} //= {};
202              
203 8 100       73 croak "resource '$name': 'class' doesn't look like a package name: '$class'"
204             unless $class =~ $MOD_REX;
205             defined $spec->{$_} and croak "resource '$name': 'class' is incompatible with '$_'"
206 7   66     45 for qw(init argument);
207             croak "resource '$name': 'class' requires 'dependencies' to be a hash"
208 6 100       31 unless ref $spec->{dependencies} eq 'HASH';
209              
210 5         9 my %deps = %{ $spec->{dependencies} };
  5         21  
211              
212 5         11 push @{ $spec->{require} }, $class;
  5         12  
213              
214 5         10 my %pass_args;
215             my @realdeps;
216 5         15 my @body = ("my \$c = shift;", "$class->new(" );
217              
218             # format: constructor_arg => [ resource_name, resource_arg ]
219 5         14 foreach my $key (keys %deps) {
220 7         13 my $entry = $deps{$key};
221              
222 7 100       17 if (ref $entry eq 'SCALAR') {
223             # pass a literal value to the constructor
224 1         3 $pass_args{$key} = $$entry;
225 1         6 next;
226             };
227              
228 6 100 66     37 if (defined $entry and !ref $entry) {
229             # allow bareword, and alias `foo => 1` to `foo => ['foo']
230 1 50       5 $entry = $key if $entry eq '1';
231 1         3 $entry = [ $entry ];
232             };
233 6 100 100     83 croak "resource '$name': dependency '$key' has wrong format"
      50        
      100        
234             unless (
235             ref $entry eq 'ARRAY'
236             and @$entry <= 2
237             and ($entry->[0] // '') =~ $ID_REX
238             );
239 3         9 push @realdeps, $entry->[0];
240              
241 3 100 100     25 push @body, length ($entry->[1] // '')
242             ? sprintf( "\t'%s' => \$c->%s('%s'),",
243             quotemeta $key, $entry->[0], quotemeta $entry->[1] )
244             : sprintf( "\t'%s' => \$c->%s,", quotemeta $key, $entry->[0] );
245             };
246 2 100       13 push @body, "\t\%pass_args"
247             if %pass_args;
248 2         6 push @body, ");";
249              
250             $spec->{init} = quote_sub(
251             "init_of_$name",
252             join( "\n", @body ),
253             (%pass_args ? { '%pass_args' => \%pass_args, } : {}),
254             {
255             no_install => 1,
256             package => $self->{target},
257             }
258 2 100       30 );
259 2         315 $spec->{dependencies} = \@realdeps;
260             };
261              
262             sub _make_dsl {
263 35     35   68 my $inst = shift;
264 35     100   147 return sub { $inst->add(@_) };
  100         71471  
265             };
266              
267             =head2 list
268              
269             Returns a list (or arrayref in scalar context)
270             containing the names of known resources.
271              
272             The order is not guaranteed.
273              
274             B. Return value structure is subject to change.
275              
276             =cut
277              
278             sub list {
279 6     6 1 34 my $self = shift;
280 6         24 my @list = sort keys %{ $self->{resource} };
  6         74  
281 6 100       69 return wantarray ? @list : \@list;
282             };
283              
284             =head2 show( $name )
285              
286             Returns a shallow copy of resource specification.
287              
288             B. Return value structure is subject to change.
289              
290             =cut
291              
292             sub show {
293 6     6 1 2812 my ($self, $name) = @_;
294              
295 6         12 my $all = $self->{resource};
296 6         11 my $spec = $all->{$name};
297 6 100       28 croak "Unknown resource '$name'"
298             unless $spec;
299              
300 5         33 my %show = %$spec; # shallow copy
301              
302 5 50       32 if (my $deps = delete $show{allowdeps}) {
303 5         19 $show{dependencies} = [ keys %$deps ];
304             };
305              
306 5 50       16 if (exists $show{orig_argument}) {
307 5         21 $show{argument} = delete $show{orig_argument};
308             };
309              
310 5         20 return \%show;
311             };
312              
313             =head2 self_check()
314              
315             Check setup validity. Dies on errors, return C<$self> otherwise.
316              
317             The following checks are available so far:
318              
319             =over
320              
321             =item * dependencies must be defined;
322              
323             =item * required modules must be loadable.
324              
325             =back
326              
327             B. Interface & performed checks may change in the future.
328              
329             =cut
330              
331             sub self_check {
332 6     6 1 11 my $self = shift;
333              
334 6         13 my $res = $self->{resource};
335 6         28 foreach my $name (sort keys %$res) {
336 10         19 my $entry = $res->{$name};
337              
338 10 50       15 my @missing_deps = grep { !$res->{$_} } keys %{ $entry->{allowdeps} || {} };
  8         29  
  10         50  
339             croak "resource '$name': missing dependencies: ".
340 10 100       38 join ", ", map { "'$_'" } @missing_deps
  2         28  
341             if @missing_deps;
342              
343 8         15 foreach my $mod ( @{ $entry->{require} } ) {
  8         27  
344 2 100       4 eval { load $mod; 1 }
  2         8  
  1         123  
345             or croak "resource '$name': failed to load '$mod': $@";
346             };
347             };
348              
349 3         14 return $self;
350             };
351              
352             =head1 COPYRIGHT AND LICENSE
353              
354             Copyright (c) 2023, Konstantin Uvarin, C<< >>
355              
356             This program is free software.
357             You can redistribute it and/or modify it under the terms of either:
358             the GNU General Public License as published by the Free Software Foundation,
359             or the Artistic License.
360              
361             See L for more information.
362              
363             =cut
364              
365             1;