File Coverage

blib/lib/Resource/Silo.pm
Criterion Covered Total %
statement 53 53 100.0
branch 2 2 100.0
condition 4 6 66.6
subroutine 12 12 100.0
pod n/a
total 71 73 97.2


line stmt bran cond sub pod time code
1             package Resource::Silo;
2              
3 28     28   1731592 use 5.010;
  28         435  
4 28     28   154 use strict;
  28         55  
  28         618  
5 28     28   123 use warnings;
  28         76  
  28         3263  
6              
7             our $VERSION = '0.09';
8              
9             =head1 NAME
10              
11             Resource::Silo - lazy declarative resource container for Perl.
12              
13             =head1 DESCRIPTION
14              
15             We assume the following setup:
16              
17             =over
18              
19             =item * The application needs to access multiple resources, such as
20             configuration files, databases, queues, service endpoints, credentials, etc.
21              
22             =item * The application has helper scripts that don't need to initialize
23             all the resources at once, as well as a test suite where accessing resources
24             is undesirable unless a fixture or mock is provided.
25              
26             =item * The resource management has to be decoupled from the application
27             logic where possible.
28              
29             =back
30              
31             And we propose the following solution:
32              
33             =over
34              
35             =item * All available resources are declared in one place
36             and encapsulated within a single container.
37              
38             =item * Such container is equipped with methods to access resources,
39             as well as an exportable prototyped function for obtaining the one and true
40             instance of it (a.k.a. optional singleton).
41              
42             =item * Every class or script in the project accesses resources
43             through this container and only through it.
44              
45             =back
46              
47             =head1 SYNOPSIS
48              
49             The default mode is to create a one-off container for all resources
50             and export if into the calling class via C function.
51              
52             package My::App;
53             use Resource::Silo;
54              
55             use DBI;
56             use YAML::LoadFile;
57             ...
58              
59             resource config => sub { LoadFile( ... ) };
60             resource dbh => sub {
61             my $self = shift;
62             my $conf = $self->config->{dbh};
63             DBI->connect( $conf->{dsn}, $conf->{user}, $conf->{pass}, { RaiseError => 1 } );
64             };
65             resource queue => sub { My::Queue->new( ... ) };
66             ...
67              
68             my $statement = silo->dbh->prepare( $sql );
69             my $queue = silo->queue;
70              
71             For more complicated projects, it may make more sense
72             to create a dedicated class for resource management:
73              
74             # in the container class
75             package My::Project::Res;
76             use Resource::Silo -class; # resource definitions will now create
77             # eponymous methods in My::Project::Res
78              
79             resource foo => sub { ... }; # declare resources as in the above example
80             resource bar => sub { ... };
81              
82             1;
83              
84             # in all other modules/packages/scripts:
85              
86             package My::Project;
87             use My::Project::Res qw(silo);
88              
89             silo->foo; # obtain resources
90             silo->bar;
91              
92             My::Project::Res->new; # separate empty resource container
93              
94             =head1 EXPORT
95              
96             The following functions will be exported into the calling module,
97             unconditionally:
98              
99             =over
100              
101             =item * silo - a singleton function returning the resource container.
102             Note that this function will be created separately for every calling module,
103             and needs to be re-exported to be shared.
104              
105             =item * resource - a DSL for defining resources, their initialization
106             and properties. See below.
107              
108             =back
109              
110             Additionally, if the C<-class> argument was added to the use line,
111             the following things happen:
112              
113             =over
114              
115             =item * L and L are added to C<@ISA>;
116              
117             =item * C function is added to C<@EXPORT> and thus becomes re-exported
118             by default;
119              
120             =item * calling C creates a corresponding method in this package.
121              
122             =back
123              
124             =head2 resource
125              
126             resource 'name' => sub { ... };
127             resource 'name' => %options;
128              
129             %options may include:
130              
131             =head3 init => sub { $self, $name, [$argument] }
132              
133             A coderef to obtain the resource.
134             Required, unless C or C are specified.
135              
136             If the number of arguments is odd,
137             the last one is popped and considered to be the init function.
138              
139             =head3 literal => $value
140              
141             Replace initializer with C.
142              
143             In addition, C flag is set,
144             and an empty C list is implied.
145              
146             =head3 argument => C || C
147              
148             If specified, assume that the resource in question may have several instances,
149             distinguished by a string argument. Such argument will be passed as the 3rd
150             parameter to the C function.
151              
152             Only one resource instance will be cached per argument value.
153              
154             This may be useful e.g. for L result sets,
155             or for L.
156              
157             A regular expression will always be anchored to match I.
158             A function must return true for the parameter to be valid.
159              
160             If the argument is omitted, it is assumed to be an empty string.
161              
162             See L below.
163              
164             =head3 derived => 1 | 0
165              
166             Assume that resource can be derived from its dependencies,
167             or that it introduces no extra side effects compared to them.
168              
169             This also naturally applies to resources with pure initializers,
170             i.e. those having no dependencies and adding no side effects on top.
171              
172             Examples may be L built on top of a L handle
173             or L built on top of L connection.
174              
175             Derivative resources may be instantiated even in locked mode,
176             as they would only initialize if their dependencies have already been
177             initialized or overridden.
178              
179             See L.
180              
181             =head3 ignore_cache => 1 | 0
182              
183             If set, don't cache resource, always create a fresh one instead.
184             See also L.
185              
186             =head3 preload => 1 | 0
187              
188             If set, try loading the resource when Cctl-Epreload> is called.
189             Useful if you want to throw errors when a service is starting,
190             not during request processing.
191              
192             =head3 cleanup => sub { $resource_instance }
193              
194             Undo the init procedure.
195             Usually it is assumed that the resource will do it by itself in the destructor,
196             e.g. that's what a L connection would do.
197             However, if it's not the case, or resources refer circularly to one another,
198             a manual "destructor" may be specified.
199              
200             It only accepts the resource itself as an argument and will be called before
201             erasing the object from the cache.
202              
203             See also C.
204              
205             =head3 cleanup_order => $number
206              
207             The higher the number, the later the resource will get destroyed.
208              
209             The default is 0, negative numbers are also valid, if that makes sense for
210             you application
211             (e.g. destroy C<$my_service_main_object> before the resources it consumes).
212              
213             =head3 fork_cleanup => sub { $resource_instance }
214              
215             Like C, but only in case a change in process ID was detected.
216             See L
217              
218             This may be useful if cleanup is destructive and shouldn't be performed twice.
219              
220             =head3 dependencies => \@list
221              
222             List other resources that may be requested in the initializer.
223             Unless C is specified (see below),
224             the dependencies I be declared I the dependant.
225              
226             A resource with parameter may also depend on itself.
227              
228             The default is all eligible resources known so far.
229              
230             B This behavior was different prior to v.0.09
231             and may be change again in the near future.
232              
233             This parameter has a different structure
234             if C parameter is in action (see below).
235              
236             =head3 loose_deps => 1|0
237              
238             Allow dependencies that have not been declared yet.
239              
240             Not specifying the C parameter would now mean
241             there are no restrictions whatsoever.
242              
243             B Having to resort to this flag may be
244             a sign of a deeper architectural problem.
245              
246             =head3 class => 'Class::Name'
247              
248             Turn on Spring-style dependency injection.
249             This forbids C and C parameters
250             and requires C to be a hash.
251              
252             The dependencies' keys become the arguments to Cnew>,
253             and the values format is as follows:
254              
255             =over
256              
257             =item * argument_name => resource_name
258              
259             Use a resource without parameter;
260              
261             =item * argument_name => [ resource_name => argument ]
262              
263             Use a parametric resource;
264              
265             =item * resource_name => 1
266              
267             Shorthand for C resource_name>;
268              
269             =item * name => \$literal_value
270              
271             Pass $literal_value to the constructor as is.
272              
273             =back
274              
275             So this:
276              
277             resource foo =>
278             class => 'My::Foo',
279             dependencies => {
280             dbh => 1,
281             redis => [ redis => 'session' ],
282             version => \3.14,
283             };
284              
285             Is roughly equivalent to:
286              
287             resource foo =>
288             dependencies => [ 'dbh', 'redis' ],
289             init => sub {
290             my $c = shift;
291             require My::Foo;
292             My::Foo->new(
293             dbh => $c->dbh,
294             redis => $c->redis('session'),
295             version => 3.14,
296             );
297             };
298              
299             =head3 require => 'Module::Name' || \@module_list
300              
301             Load module(s) specified before calling the initializer.
302              
303             This is exactly the same as calling require 'Module::Name' in the initializer
304             itself except that it's more explicit.
305              
306             =head2 silo
307              
308             A re-exportable singleton function returning
309             one and true L instance
310             associated with the class where the resources were declared.
311              
312             B Calling C from a different module will
313             create a I container instance. You'll have to re-export
314             (or otherwise provide access to) this function.
315              
316             I
317             within the same interpreter without interference.>
318              
319             Cnew> will create a new instance of the I container class.
320              
321             =cut
322              
323 28     28   231 use Carp;
  28         74  
  28         2102  
324 28     28   195 use Exporter;
  28         71  
  28         1124  
325 28     28   197 use Scalar::Util qw( set_prototype );
  28         62  
  28         1540  
326              
327 28     28   12773 use Resource::Silo::Metadata;
  28         81  
  28         1020  
328 28     28   13127 use Resource::Silo::Container;
  28         75  
  28         4573  
329              
330             # Store definitions here
331             our %metadata;
332              
333             sub import {
334 34     34   358 my ($self, @param) = @_;
335 34         139 my $caller = caller;
336 34         428 my $target;
337              
338 34         150 while (@param) {
339 15         43 my $flag = shift @param;
340 15 100       57 if ($flag eq '-class') {
341 14         44 $target = $caller;
342             } else {
343             # TODO if there's more than 3 elsifs, use jump table instead
344 1         211 croak "Unexpected parameter to 'use $self': '$flag'";
345             };
346             };
347              
348 33   66     257 $target ||= __PACKAGE__."::container::".$caller;
349              
350 33         160 my $spec = Resource::Silo::Metadata->new($target);
351 33         87 $metadata{$target} = $spec;
352              
353 33         53 my $instance;
354             my $silo = set_prototype {
355             # cannot instantiate target until the package is fully defined,
356             # thus go lazy
357 63   66 63   44314 $instance //= $target->new;
358 33         513 } '';
359              
360 28     28   216 no strict 'refs'; ## no critic
  28         66  
  28         971  
361 28     28   195 no warnings 'redefine', 'once'; ## no critic
  28         55  
  28         5417  
362              
363 33         86 push @{"${target}::ISA"}, 'Resource::Silo::Container';
  33         452  
364              
365 33         72 push @{"${caller}::ISA"}, 'Exporter';
  33         308  
366 33         90 push @{"${caller}::EXPORT"}, qw(silo);
  33         145  
367 33         123 *{"${caller}::resource"} = $spec->_make_dsl;
  33         152  
368 33         71 *{"${caller}::silo"} = $silo;
  33         41839  
369             };
370              
371             =head1 TESTING: LOCK AND OVERRIDES
372              
373             It's usually a bad idea to access real-world resources in one's test suite,
374             especially if it's e.g. a partner's endpoint.
375              
376             Now the #1 rule when it comes to mocks is to avoid mocks and instead design
377             the modules in such a way that they can be tested in isolation.
378             This however may not always be easily achievable.
379              
380             Thus, L provides a mechanism to substitute a subset of resources
381             with mocks and forbid the instantiation of the rest, thereby guarding against
382             unwanted side-effects.
383              
384             The C/C methods in L,
385             available via Cctl> frontend,
386             temporarily forbid instantiating new resources.
387             The resources already in cache will still be OK though.
388              
389             The C method allows to supply substitutes for resources or
390             their initializers.
391              
392             The C flag in the resource definition may be used to indicate
393             that a resource is safe to instantiate as long as its dependencies are
394             either instantiated or mocked, e.g. a L schema is probably fine
395             as long as the underlying database connection is taken care of.
396              
397             Here is an example:
398              
399             use Test::More;
400             use My::Project qw(silo);
401             silo->ctl->lock->override(
402             dbh => DBI->connect( 'dbi:SQLite:database=:memory:', '', '', { RaiseError => 1 ),
403             );
404              
405             silo->dbh; # a mocked database
406             silo->schema; # a DBIx::Class schema reliant on the dbh
407             silo->endpoint( 'partner' ); # an exception as endpoint wasn't mocked
408              
409             =head1 CAVEATS AND CONSIDERATIONS
410              
411             See L for resource container implementation.
412              
413             =head2 CACHING
414              
415             All resources are cached, the ones with arguments are cached together
416             with the argument.
417              
418             =head2 FORKING
419              
420             If the process forks, resources such as database handles may become invalid
421             or interfere with other processes' copies.
422             As of current, if a change in the process ID is detected,
423             the resource cache is reset altogether.
424              
425             This may change in the future as some resources
426             (e.g. configurations or endpoint URLs) are stateless and may be preserved.
427              
428             =head2 CIRCULAR DEPENDENCIES
429              
430             If a resource depends on other resources,
431             those will be simply created upon request.
432              
433             It is possible to make several resources depend on each other.
434             Trying to initialize such resource will cause an expection, however.
435              
436             =head2 COMPATIBILITY
437              
438             As of current, using C and C in the same package
439             doesn't work.
440              
441             Usage together with C works, but only if L comes first:
442              
443             package My::App;
444             use Resource::Silo -class;
445             use Moo;
446              
447             has config_name => is => ro, default => sub { '/etc/myapp/myfile.yaml' };
448              
449             resource config => sub { LoadFile( $_[0]->config_name ) };
450              
451             Compatibility issues are being slowly worked on.
452              
453             =head1 MORE EXAMPLES
454              
455             =head2 Resources with just the init
456              
457             package My::App;
458             use Resource::Silo;
459              
460             resource config => sub {
461             require YAML::XS;
462             YAML::XS::LoadFile( "/etc/myapp.yaml" );
463             };
464              
465             resource dbh => sub {
466             require DBI;
467             my $self = shift;
468             my $conf = $self->config->{database};
469             DBI->connect(
470             $conf->{dbi}, $conf->{username}, $conf->{password}, { RaiseError => 1 }
471             );
472             };
473              
474             resource user_agent => sub {
475             require LWP::UserAgent;
476             LWP::UserAgent->new();
477             # set your custom UserAgent header or SSL certificate(s) here
478             };
479              
480             Note that though lazy-loading the modules is not necessary,
481             it may speed up loading support scripts.
482              
483             =head2 Resources with extra options
484              
485             resource logger =>
486             cleanup_order => 9e9, # destroy as late as possible
487             require => [ 'Log::Any', 'Log::Any::Adapter' ],
488             init => sub {
489             Log::Any::Adapter->set( 'Stderr' );
490             # your rsyslog config could be here
491             Log::Any->get_logger;
492             };
493              
494             resource schema =>
495             derived => 1, # merely a frontend to dbi
496             require => 'My::App::Schema',
497             init => sub {
498             my $self = shift;
499             return My::App::Schema->connect( sub { $self->dbh } );
500             };
501              
502             =head2 Resource with parameter
503              
504             An useless but short example:
505              
506             #!/usr/bin/env perl
507              
508             use strict;
509             use warnings;
510             use Resource::Silo;
511              
512             resource fibonacci =>
513             argument => qr(\d+),
514             init => sub {
515             my ($self, $name, $arg) = @_;
516             $arg <= 1 ? $arg
517             : $self->fibonacci($arg-1) + $self->fibonacci($arg-2);
518             };
519              
520             print silo->fibonacci(shift);
521              
522             A more pragmatic one:
523              
524             package My::App;
525             use Resource::Silo;
526              
527             resource redis_conn => sub {
528             my $self = shift;
529             require Redis;
530             Redis->new( server => $self->config->{redis} );
531             };
532              
533             my %known_namespaces = (
534             lock => 1,
535             session => 1,
536             user => 1,
537             );
538              
539             resource redis =>
540             argument => sub { $known_namespaces{ $_ } },
541             require => 'Redis::Namespace',
542             init => sub {
543             my ($self, $name, $ns) = @_;
544             Redis::Namespace->new(
545             redis => $self->redis,
546             namespace => $ns,
547             );
548             };
549              
550             # later in the code
551             silo->redis; # nope!
552             silo->redis('session'); # get a prefixed namespace
553              
554             =head3 Overriding in test files
555              
556             use Test::More;
557             use My::App qw(silo);
558              
559             silo->ctl->override( dbh => $temp_sqlite_connection );
560             silo->ctl->lock;
561              
562             my $stuff = My::App::Stuff->new();
563             $stuff->frobnicate( ... ); # will only affect the sqlite instance
564              
565             $stuff->ping_partner_api(); # oops! the user_agent resource wasn't
566             # overridden, so there'll be an exception
567              
568             =head3 Fetching a dedicated resource instance
569              
570             use My::App qw(silo);
571             my $dbh = silo->ctl->fresh('dbh');
572              
573             $dbh->begin_work;
574             # Perform a Big Scary Update here
575             # Any operations on $dbh won't interfere with normal usage
576             # of silo->dbh by other application classes.
577              
578             =head1 SEE ALSO
579              
580             L - a more mature IoC / DI framework.
581              
582             =head1 ACKNOWLEDGEMENTS
583              
584             =over
585              
586             =item * This module was names after a building in the game
587             B>
588              
589             =item * This module was inspired in part by my work for
590             L.
591             That was a great time and I had great coworkers!
592              
593             =back
594              
595             =head1 BUGS
596              
597             This software is still in beta stage. Its interface is still evolving.
598              
599             Version 0.09 brings a breaking change that forbids forward dependencies.
600              
601             Please report bug reports and feature requests to
602             L
603             or via RT:
604             L.
605              
606             =head1 SUPPORT
607              
608             You can find documentation for this module with the perldoc command.
609              
610             perldoc Resource::Silo
611              
612             You can also look for information at:
613              
614             =over 4
615              
616             =item * RT: CPAN's request tracker (report bugs here)
617              
618             L
619              
620             =item * CPAN Ratings
621              
622             L
623              
624             =item * Search CPAN
625              
626             L
627              
628             =back
629              
630             =head1 COPYRIGHT AND LICENSE
631              
632             Copyright (c) 2023, Konstantin Uvarin, C<< >>
633              
634             This program is free software.
635             You can redistribute it and/or modify it under the terms of either:
636             the GNU General Public License as published by the Free Software Foundation,
637             or the Artistic License.
638              
639             See L for more information.
640              
641             =cut
642              
643             1; # End of Resource::Silo