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   183 use strict;
  29         52  
  29         817  
4 29     29   124 use warnings;
  29         55  
  29         1031  
5             our $VERSION = '0.11';
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   141 use Carp;
  29         54  
  29         1478  
23 29     29   12328 use Module::Load qw( load );
  29         30307  
  29         179  
24 29     29   2114 use Scalar::Util qw( looks_like_number reftype );
  29         55  
  29         1423  
25 29     29   12310 use Sub::Quote qw( quote_sub );
  29         130721  
  29         27785  
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 74     74   253 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 116 my ($class, $target) = @_;
44 35         190 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 192 my $self = shift;
73 100         178 my $name = shift;
74 100 100       325 if (@_ % 2) {
75 37         71 my $init = pop @_;
76 37         108 unshift @_, init => $init;
77             }
78 100         315 my (%spec) = @_;
79 100         295 my $target = $self->{target};
80              
81 100 100 100     1190 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       414 if defined $self->{resource}{$name};
85 94 100       1138 croak "resource: attempt to replace existing method '$name' in $target"
86             if $target->can($name);
87              
88 92         326 my @extra = grep { !$known_args{$_} } keys %spec;
  159         468  
89 92 100       260 croak "resource '$name': unknown arguments in specification: @extra"
90             if @extra;
91              
92             {
93             # validate 'require' before 'class'
94 91 100       150 if (!ref $spec{require}) {
  91         253  
95 86 100       300 $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       307 unless ref $spec{require} eq 'ARRAY';
99 90         161 my @bad = grep { $_ !~ $MOD_REX } @{ $spec{require} };
  9         57  
  90         221  
100             croak "resource '$name': 'require' doesn't look like module name(s): "
101 90 100       241 .join ", ", map { "'$_'" } @bad
  2         18  
102             if @bad;
103             };
104              
105 89 100       244 if (defined (my $value = $spec{literal})) {
106             defined $spec{$_}
107             and croak "resource '$name': 'literal' is incompatible with '$_'"
108 4   66     45 for qw( init class argument );
109 2     1   11 $spec{init} = sub { $value };
  1         11  
110 2   50     13 $spec{dependencies} //= [];
111 2   50     11 $spec{derived} //= 1;
112 2   50     10 $spec{cleanup_order} //= 9 ** 9 ** 9;
113             };
114              
115             _make_init_class($self, $name, \%spec)
116 87 100       255 if (defined $spec{class});
117              
118 81 100       224 if (my $deps = delete $spec{dependencies}) {
119 14 100       82 croak "resource '$name': 'dependencies' must be an array"
120             unless ref $deps eq 'ARRAY';
121 13         28 my @bad = grep { !/$ID_REX/ } @$deps;
  12         62  
122             croak "resource '$name': illegal dependency name(s): "
123 13 100       45 .join ", ", map { "'$_'" } @bad
  2         27  
124             if @bad;
125 11         25 $spec{allowdeps} = { map { $_ => 1 } @$deps };
  10         37  
126             };
127              
128 78 100       184 unless ($spec{loose_deps}) {
129             # resources with argument should be allowed to depend on themselves
130             local $self->{resource}{$name} = {}
131 73 100       216 if defined $spec{argument};
132              
133 73 100       158 if ($spec{allowdeps}) {
134 8         12 my @fwd = grep { !$self->{resource}{$_} } keys %{ $spec{allowdeps} };
  7         19  
  8         20  
135             croak "resource '$name': forward dependencies require 'loose_deps' flag: "
136 8 100       28 .join( ", ", map { "'$_'" } @fwd)
  1         19  
137             if @fwd;
138             } else {
139 65         131 $spec{autodeps} = 1;
140             $spec{allowdeps} = {
141 65         108 map { $_ => 1 } keys %{ $self->{resource} },
  70         198  
  65         202  
142             };
143             };
144             };
145              
146             croak "resource '$name': 'init' must be a function"
147 77 100 100     670 unless ref $spec{init} and reftype $spec{init} eq $CODE;
148              
149 74 100 100     270 if (!defined $spec{argument}) {
    100          
    100          
150 65         140 $spec{orig_argument} = '';
151 65         154 $spec{argument} = \&_is_empty;
152             } elsif (ref $spec{argument} eq $REGEXP) {
153 6         112 my $rex = qr(^(?:$spec{argument})$);
154 6         18 $spec{orig_argument} = $spec{argument};
155 6     24   32 $spec{argument} = sub { $_[0] =~ $rex };
  24         146  
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     363 $spec{cleanup_order} //= 0;
163             croak "resource '$name': 'cleanup_order' must be a number"
164 73 100       252 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     281 or $spec{cleanup_order} != 0
      66        
171             );
172              
173             croak "resource '$name': 'cleanup' must be a function"
174 70 100 100     306 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     284 if defined $spec{fork_cleanup} and (reftype $spec{fork_cleanup} // '') ne $CODE;
      100        
177              
178 66 100       184 if ($spec{preload}) {
179 1         2 push @{ $self->{preload} }, $name;
  1         2  
180             };
181              
182 66         3735 $spec{origin} = Carp::shortmess("declared");
183 66         15080 $spec{origin} =~ s/\s+$//s;
184 66         265 $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   288 no strict 'refs'; ## no critic Strictures
  29         85  
  29         29659  
  66         172  
190 66         472 *{"${target}::$name"} =
  66         383  
191             Resource::Silo::Container::_silo_make_accessor($name, \%spec);
192             }
193              
194 66         251 return $self;
195             };
196              
197             sub _make_init_class {
198 8     8   41 my ($self, $name, $spec) = @_;
199              
200 8         16 my $class = $spec->{class};
201 8   100     25 $spec->{dependencies} //= {};
202              
203 8 100       64 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     42 for qw(init argument);
207             croak "resource '$name': 'class' requires 'dependencies' to be a hash"
208 6 100       30 unless ref $spec->{dependencies} eq 'HASH';
209              
210 5         9 my %deps = %{ $spec->{dependencies} };
  5         16  
211              
212 5         12 push @{ $spec->{require} }, $class;
  5         13  
213              
214 5         8 my %pass_args;
215             my @realdeps;
216 5         16 my @body = ("my \$c = shift;", "$class->new(" );
217              
218             # format: constructor_arg => [ resource_name, resource_arg ]
219 5         20 foreach my $key (keys %deps) {
220 7         12 my $entry = $deps{$key};
221              
222 7 100       19 if (ref $entry eq 'SCALAR') {
223             # pass a literal value to the constructor
224 1         2 $pass_args{$key} = $$entry;
225 1         2 next;
226             };
227              
228 6 100 66     31 if (defined $entry and !ref $entry) {
229             # allow bareword, and alias `foo => 1` to `foo => ['foo']
230 1 50       4 $entry = $key if $entry eq '1';
231 1         2 $entry = [ $entry ];
232             };
233 6 100 100     81 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         7 push @realdeps, $entry->[0];
240              
241 3 100 100     22 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       7 push @body, "\t\%pass_args"
247             if %pass_args;
248 2         3 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       24 );
259 2         238 $spec->{dependencies} = \@realdeps;
260             };
261              
262             sub _make_dsl {
263 35     35   78 my $inst = shift;
264 35     100   187 return sub { $inst->add(@_) };
  100         71167  
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 695 my $self = shift;
280 6         11 my @list = sort keys %{ $self->{resource} };
  6         32  
281 6 100       44 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 1887 my ($self, $name) = @_;
294              
295 6         12 my $all = $self->{resource};
296 6         13 my $spec = $all->{$name};
297 6 100       35 croak "Unknown resource '$name'"
298             unless $spec;
299              
300 5         43 my %show = %$spec; # shallow copy
301              
302 5 50       18 if (my $deps = delete $show{allowdeps}) {
303 5         17 $show{dependencies} = [ keys %$deps ];
304             };
305              
306 5 50       20 if (exists $show{orig_argument}) {
307 5         13 $show{argument} = delete $show{orig_argument};
308             };
309              
310 5         18 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         14 my $res = $self->{resource};
335 6         29 foreach my $name (sort keys %$res) {
336 10         20 my $entry = $res->{$name};
337              
338 10 50       17 my @missing_deps = grep { !$res->{$_} } keys %{ $entry->{allowdeps} || {} };
  8         27  
  10         49  
339             croak "resource '$name': missing dependencies: ".
340 10 100       30 join ", ", map { "'$_'" } @missing_deps
  2         28  
341             if @missing_deps;
342              
343 8         23 foreach my $mod ( @{ $entry->{require} } ) {
  8         24  
344 2 100       6 eval { load $mod; 1 }
  2         12  
  1         135  
345             or croak "resource '$name': failed to load '$mod': $@";
346             };
347             };
348              
349 3         24 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;