File Coverage

blib/lib/Resource/Silo/Metadata.pm
Criterion Covered Total %
statement 142 142 100.0
branch 78 80 97.5
condition 46 54 85.1
subroutine 17 17 100.0
pod 4 4 100.0
total 287 297 96.6


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