File Coverage

blib/lib/Bolts.pm
Criterion Covered Total %
statement 154 161 95.6
branch 40 50 80.0
condition 12 22 54.5
subroutine 29 29 100.0
pod 8 10 80.0
total 243 272 89.3


line stmt bran cond sub pod time code
1             package Bolts;
2             $Bolts::VERSION = '0.142930';
3             # ABSTRACT: An Inversion of Control framework for Perl
4              
5 8     8   108170 use Moose ();
  8         2035305  
  8         247  
6 8     8   93 use Moose::Exporter;
  8         12  
  8         49  
7              
8             # Register attribute traits
9 8     8   4680 use Bolts::Meta::Attribute::Trait::Initializer;
  8         65  
  8         241  
10              
11 8     8   41 use Class::Load ();
  8         12  
  8         98  
12 8     8   32 use Moose::Util::MetaRole ();
  8         9  
  8         103  
13 8     8   34 use Moose::Util::TypeConstraints ();
  8         6  
  8         112  
14 8     8   30 use Safe::Isa;
  8         14  
  8         754  
15 8     8   33 use Scalar::Util ();
  8         9  
  8         98  
16 8     8   29 use Carp ();
  8         10  
  8         135  
17              
18 8     8   2217 use Bolts::Util qw( locator_for );
  8         20  
  8         42  
19 8     8   6375 use Bolts::Blueprint::Given;
  8         2183  
  8         301  
20 8     8   3797 use Bolts::Blueprint::Literal;
  8         1989  
  8         249  
21 8     8   3032 use Bolts::Blueprint::Built;
  8         2364  
  8         287  
22              
23 8     8   54 use Safe::Isa;
  8         12  
  8         12347  
24              
25             our @CARP_NOT = qw( Moose::Exporter );
26              
27             # Ugly, but so far... necessary...
28             our $GLOBAL_FALLBACK_META_LOCATOR = 'Bolts::Meta::Locator';
29              
30             my @BAG_META;
31              
32             Moose::Exporter->setup_import_methods(
33             class_metaroles => {
34             class => [
35             'Bolts::Meta::Class::Trait::Locator',
36             'Bolts::Meta::Class::Trait::Bag',
37             ],
38             },
39             base_class_roles => [ 'Bolts::Role::SelfLocator' ],
40             with_meta => [ qw(
41             artifact bag builder contains dep option self such_that_each value
42             ) ],
43             also => 'Moose',
44             );
45              
46             sub init_meta {
47 9     9 0 6071 my $class = shift;
48 9         37 my $meta = Moose->init_meta(@_);
49              
50             $meta->add_attribute(__top => (
51             reader => '__top',
52             required => 1,
53 8     8   68 default => sub { shift },
54 9         31482 lazy => 1,
55             weak_ref => 1,
56             ));
57              
58 9         18322 return $meta;
59             }
60              
61             sub _bag_meta {
62 52     52   73 my ($meta) = @_;
63              
64 52 100       135 $meta = $BAG_META[-1] if @BAG_META;
65              
66 52         87 return $meta;
67             }
68              
69              
70             sub _injector {
71 38     38   69 my ($meta, $where, $type, $key, $params) = @_;
72              
73 38         36 my %params;
74              
75 38 50 33     107 if ($params->$_can('does') and $params->$_does('Bolts::Blueprint')) {
76 0         0 %params = { blueprint => $params };
77             }
78             else {
79 38         358 %params = %$params;
80             }
81              
82 38 50 33     241 Carp::croak("invalid blueprint in $where $key")
83             unless $params{blueprint}->$_can('does')
84             and $params{blueprint}->$_does('Bolts::Blueprint::Role::Injector');
85              
86 38 100       3446 $params{isa} = Moose::Util::TypeConstraints::find_or_create_isa_type_constraint($params{isa})
87             if defined $params{isa};
88 38 50       5213 $params{does} = Moose::Util::TypeConstraints::find_or_create_does_type_constraint($params{does})
89             if defined $params{does};
90              
91 38         62 $params{key} = $key;
92              
93 38         133 return $meta->acquire('injector', $type, \%params);
94             }
95              
96             # TODO This sugar requires special knowledge of the built-in blueprint
97             # types. It would be slick if this was not required. On the other hand, that
98             # sounds like very deep magic and that might just be taking the magic too far.
99             sub artifact {
100 36     36 1 12340 my $meta = _bag_meta(shift);
101 36         55 my $name = shift;
102              
103             # No arguments means it's acquired with given parameters
104 36         40 my $blueprint_name;
105             my %params;
106 36 100       147 if (@_ == 0) {
    100          
107 2         7 $blueprint_name = 'acquired';
108 2         11 $params{path} = [ "__auto_$name" ];
109 2         27 $meta->add_attribute("__auto_$name" =>
110             is => 'ro',
111             init_arg => $name,
112             );
113             }
114              
115             # One argument means it's a literal
116             elsif (@_ == 1) {
117 3         5 $blueprint_name = 'literal';
118 3         8 $params{value} = $_[0];
119             }
120              
121             # Otherwise, we gotta figure out what it is...
122             else {
123 31         83 %params = @_;
124              
125             # Is the service class named?
126 31 50 66     249 if (defined $params{blueprint}) {
    100          
    50          
    100          
    50          
127 0         0 $blueprint_name = delete $params{blueprint};
128             }
129              
130             # Is it an acquired?
131             elsif (defined $params{path} && $params{path}) {
132 2         4 $blueprint_name = 'acquired';
133              
134 2 50       8 $params{path} = [ $params{path} ] unless ref $params{path} eq 'ARRAY';
135              
136 2         4 my @path = ('__top', @{ $params{path} });
  2         4  
137              
138 2         4 $params{path} = \@path;
139             }
140              
141             # Is it a literal?
142             elsif (exists $params{value}) {
143 0         0 $blueprint_name = 'literal';
144             }
145              
146             # Is it a factory blueprint?
147             elsif (defined $params{class}) {
148 11         15 $blueprint_name = 'factory';
149             }
150              
151             # Is it a builder blueprint?
152             elsif (defined $params{builder}) {
153 18         31 $blueprint_name = 'built';
154             }
155              
156             else {
157 0         0 Carp::croak("unable to determine what kind of service $name is in ", $meta->name);
158             }
159             }
160              
161 36         5135 my @injectors;
162 36 100       102 if (defined $params{parameters}) {
163 6         15 my $parameters = delete $params{parameters};
164              
165 6 50       23 if ($parameters->$_does('Bolts::Blueprint')) {
    100          
    50          
166 0         0 push @injectors, _injector(
167             $meta, 'parameters', 'parameter_position',
168             '0', { blueprint => $parameters },
169             );
170             }
171             elsif (ref $parameters eq 'HASH') {
172 5         66 for my $key (keys %$parameters) {
173 12         35 push @injectors, _injector(
174             $meta, 'parameters', 'parameter_name',
175             $key, $parameters->{$key},
176             );
177             }
178             }
179             elsif (ref $parameters eq 'ARRAY') {
180 1         11 my $key = 0;
181 1         2 for my $params (@$parameters) {
182 3         8 push @injectors, _injector(
183             $meta, 'parameters', 'parameter_position',
184             $key++, $params,
185             );
186             }
187             }
188             else {
189 0         0 Carp::croak("parameters must be a blueprint, an array of blueprints, or a hash with blueprint values");
190             }
191             }
192              
193 36 100       91 if (defined $params{setters}) {
194 1         4 my $setters = delete $params{setters};
195              
196 1         3 for my $key (keys %$setters) {
197 1         4 push @injectors, _injector(
198             $meta, 'setters', 'setter',
199             $key, $setters->{$key},
200             );
201             }
202             }
203              
204 36 100       91 if (defined $params{indexes}) {
205 1         4 my $indexes = delete $params{indexes};
206              
207 1         5 while (my ($index, $def) = splice @$indexes, 0, 2) {
208 4 50 33     45 if (!Scalar::Util::blessed($def) && Scalar::Util::reftype($def) eq 'HASH') {
209 4   33     18 $def->{position} //= $index;
210             }
211              
212 4         8 push @injectors, _injector(
213             $meta, 'indexes', 'store_array',
214             $index, $def,
215             );
216             }
217             }
218              
219 36 100       96 if (defined $params{push}) {
220 9         21 my $push = delete $params{push};
221              
222 9         13 my $i = 0;
223 9         20 for my $def (@$push) {
224 14   66     54 my $key = $def->{key} // $i;
225              
226 14         35 push @injectors, _injector(
227             $meta, 'push', 'store_array',
228             $key, $def,
229             );
230              
231 14         49 $i++;
232             }
233             }
234              
235 36 100       97 if (defined $params{keys}) {
236 1         3 my $keys = delete $params{keys};
237              
238 1         4 for my $key (keys %$keys) {
239 4         10 push @injectors, _injector(
240             $meta, 'keys', 'store_hash',
241             $key, $keys->{$key},
242             );
243             }
244             }
245              
246             # TODO Remember the service for introspection
247              
248 36   100     166 my $scope_name = delete $params{scope} // '_';
249 36   100     127 my $infer = delete $params{infer} // 'none';
250              
251 36         133 my $scope = $meta->acquire('scope', $scope_name);
252              
253 36         137 my $blueprint = $meta->acquire('blueprint', $blueprint_name, \%params);
254              
255 36         1048 my $artifact = Bolts::Artifact->new(
256             meta_locator => $meta,
257             name => $name,
258             blueprint => $blueprint,
259             scope => $scope,
260             infer => $infer,
261             injectors => \@injectors,
262             );
263              
264 36         512 $meta->add_artifact($name, $artifact);
265 36         1904 return;
266             }
267              
268              
269             our @BAG_OF_BUILDING;
270             sub bag {
271 2     2 1 19 my ($meta, $name, $partial_def) = @_;
272              
273 2         5 $meta = _bag_meta($meta);
274              
275 2         18 my $def = $partial_def->($name);
276             $meta->add_artifact(
277             $name => Bolts::Artifact::Thunk->new(
278             thunk => sub {
279 10     10   20 my ($self, $bag, $name, %params) = @_;
280 10         276 return $def->name->new(
281             __parent => $bag,
282             %params,
283             );
284             },
285             )
286 2         85 );
287             }
288              
289             sub contains(&;$) {
290 2     2 0 32 my ($parent_meta, $code, $such_that_each) = @_;
291              
292 2         6 my $meta = _bag_meta($parent_meta);
293              
294             return sub {
295 2     2   4 my ($name) = shift;
296              
297 2         6 my $parent = $meta->name;
298              
299 2 100       26 my $bag_meta = Bolts::Bag->start_bag(
300             package => "${parent}::$name",
301             ($such_that_each ? (such_that_each => $such_that_each) : ()),
302             );
303 2         5 push @BAG_META, $bag_meta;
304              
305             $bag_meta->add_attribute(__parent => (
306             reader => '__parent',
307             required => 1,
308 0         0 default => sub { Carp::confess('why are we here?') },
309 2         17 weak_ref => 1,
310             ));
311              
312 2         4512 $bag_meta->add_artifact(
313             __top => Bolts::Artifact->new(
314             meta_locator => $bag_meta,
315             name => '__top',
316             blueprint => $bag_meta->acquire('blueprint', 'acquired', {
317             path => [ '__parent', '__top' ],
318             }),
319             scope => $bag_meta->acquire('scope', 'prototype'),
320             )
321             );
322              
323 2         89 $code->($bag_meta);
324              
325 2         6 pop @BAG_META;
326              
327 2         9 $bag_meta->finish_bag;
328              
329 2         39 return $bag_meta;
330 2         17 };
331             }
332              
333              
334             sub such_that_each($) {
335 1     1 1 18 my ($meta, $params) = @_;
336 1         5 return $params;
337             }
338              
339              
340             sub builder(&) {
341 2     2 1 34 my ($meta, $code) = @_;
342 2         8 $meta = _bag_meta($meta);
343              
344             return {
345 2         12 blueprint => $meta->acquire('blueprint', 'built_injector', {
346             builder => $code,
347             }),
348             };
349             }
350              
351              
352             sub dep($) {
353 9     9 1 121 my ($meta, $path) = @_;
354 9         24 $meta = _bag_meta($meta);
355              
356 9 100       60 $path = [ $path ] unless ref $path eq 'ARRAY';
357              
358 9         19 my @path = ('__top', @$path);
359              
360             return {
361 9         45 blueprint => $meta->acquire('blueprint', 'acquired', {
362             path => \@path,
363             }),
364             };
365             }
366              
367              
368             sub option($) {
369 8     8 1 1509 my ($meta, $p) = @_;
370              
371 8         32 my %bp = %$p;
372 8         10 my %ip;
373 8         15 for my $k (qw( isa does )) {
374 16 100       56 $ip{$k} = delete $bp{$k} if exists $bp{$k};
375             }
376              
377             return {
378 8         41 %ip,
379             blueprint => $meta->acquire('blueprint', 'given', \%bp),
380             },
381             }
382              
383              
384             sub value($) {
385 18     18 1 209 my ($meta, $value) = @_;
386              
387             return {
388 18         63 blueprint => $meta->acquire('blueprint', 'literal', {
389             value => $value,
390             }),
391             };
392             }
393              
394              
395             sub self() {
396 1     1 1 14 my ($meta) = @_;
397 1         3 $meta = _bag_meta($meta);
398              
399             return {
400 1         5 blueprint => $meta->acquire('blueprint', 'parent_bag'),
401             };
402             }
403              
404              
405             1;
406              
407             __END__
408              
409             =pod
410              
411             =encoding UTF-8
412              
413             =head1 NAME
414              
415             Bolts - An Inversion of Control framework for Perl
416              
417             =head1 VERSION
418              
419             version 0.142930
420              
421             =head1 SYNOPSIS
422              
423             package MyApp;
424             use Bolts;
425              
426             artifcat log_file => 'var/messages.log';
427             artifact logger => (
428             class => 'MyApp::Logger',
429             scope => 'singleton',
430             infer => 'acquisition',
431             );
432              
433             # Later...
434             my $log = $app->acquire('logger');
435             $log->error("Bad stuff.");
436              
437             =head1 DESCRIPTION
438              
439             B<Caution:> I<< This is an B<experimental> API. Some aspects of the API may change, possibly drastically, from version to version. That probably won't happen, but please contact me via email if you plan to use this in something and want to know what might change. Pay close attention to any B<Caution> remarks in the documentation. >>
440              
441             This is yet another Inversion of Control framework for Perl. This one is based upon a combination of L<Bread::Board>, concepts from the Spring framework, and a good mix of my own ideas and modifications after spending a few years using L<Moose> and Bread::Board.
442              
443             =head2 Inversion of Control
444              
445             For those who might now know what Inversion of Control (IOC) is, it is a design pattern aimed at helping you decouple your code, automate parts of the configuration assembly, and manage the life cycle of your objects.
446              
447             By using an IOC framework, the objects in your program need to know less about the other objects in your application. Your objects can focus on knowing what it needs from other objects without knowing where to find objects that do that or how they are configured.
448              
449             For example, early in a programs lifetime, the logger might be a local object that writes directly to a file. Later, it might be an object with the same interface, but it writes to syslog. Further on, it might be some sort of logging service that is accessed over the network through a stub provided by a service locator. If your program uses an IOC framework, the configuration files for your IOC will change to pass a different object to the application during each phase, but the program itself might not change at all.
450              
451             An IOC framework also helps you assemble complex configuration related to your application. It can join various configurations together in interesting and complex ways automatically.
452              
453             An IOC framework can make sure that your objects live only as long as they should or longer than they would normally. It can manage the list of objects that should be created each time (prototypes), objects that should last as long as a user session, objects that should last as long as some request, and objects that last for the duration of the process (singletons).
454              
455             The next sections will introduce the concepts and terminology used by this framework.
456              
457             =head2 Artifacts
458              
459             The basic building block of the Bolts IOC framework is the B<artifact>. At the simplest level, an artifact is any kind of thing your program might use. It might be a value, it might be a reference to something, it might be an object, or it might be something more complex.
460              
461             For simple values and direct references to things, you can treat any thing as an artifact. However, the real power starts when you use an implementation of L<Bolts::Role::Artifact>, usually L<Bolts::Artifact> to manage that thing. These provide utilities for constructing an object or other value according to some set of instructions and directions for managing the lifecycle of the artifact in question.
462              
463             =head2 Bags
464              
465             Artifacts are grouped into bags. A B<bag> can be any object, hash reference, or array reference. Artifacts are associated with the bag as indexes in the array, keys on the hash, or methods on the object. Literally, any object can be used as a bag, which differs from frameworks like L<Bread::Board>, which requires that its services be put inside a special container object. Bolts just uses hashes, arrays, and objects in the usual Perl way to locate artifacts.
466              
467             =head2 Locators
468              
469             A B<locator> is an object that finds things in a bag (these are things related to L<Bolts::Role::Locator>. The finding process is called, B<acquisition>. (If you are familiar with Harry Potter, this process is similar to Harry Potter using a wand to extract dittany from Hermione's handbag by saying "Accio Dittany.") After finding the object, the locator performs B<resolution>, which checks to see if returned artifact needs to be resolved further. (To continue the analogy, this is like unbottling the dittany and pouring it out, there may be another step before the artifact is completely ready for use.)
470              
471             =head2 Blueprints
472              
473             Attached to L<Bolts::Artifact> definitions are a set of blueprints (some object that implements L<Bolts::Blueprint>). A B<blueprint> describes how an artifact is located or constructed, which is part of resolution. The system provides standard blueprints that can cover all possible needs, but you can create your own to extend the framework as necessary. The built-in blueprints can locate an object by acquring it from a bag, the result of a subroutine, by use of a factory method or constructor on an object, by directly handing the value in to the bag when the bag is constructed, or set as a constant.
474              
475             =head2 Injectors
476              
477             Another step in resolution is injection. An B<injector> associates additional artifacts with the artifact being resolved. This might be values that need to be passed to the artifact during construction, methods that need to be called to configure the object following construction, or even keys on a hash that need to be set on the artifact.
478              
479             Injectors come in two flavors, injection by automatic acquisition and by given options. With acquisition, the framework will acquire or other provide addition artifacts to the artifact being resolved automatically, this is where much of the power of IOC comes from. Sometimes, however, an object just requires some configuration state to let it know how it will be used. In those cases, options can be directly passed to C<acquire> to be used for injection.
480              
481             =head2 Scope
482              
483             The B<scope> of an artifact determines during what period an artifact is valid. Bolts provides two built-in scopes, prototype and singleton. A prototype represents an artifact that must be resolved every time it is acquired. A singleton represents an artifact that is resolved only the first time it is acquired and is then reused for all following acquisitions for the lifetime of the bag.
484              
485             =head2 Infererer
486              
487             It is usually considered a bad thing in computer science if you have to configure something twice in the same program. Such duplication is tedious and leads to technical debt. This, unfortunately, is often the case when using some IOC frameworks. You configure the object once using Moose and then a second time to let the IOC framework know how to inject configuration into the artifact. This is where the inferers come in.
488              
489             An B<inferer> is a tool that can inspect an object and automatically decide how that object should be configured. Bolts provides an inferer for L<Moose> that can use the metadata about a L<Moose> object to determine how to inject into that object automatically.
490              
491             =head2 Meta Locator and Extension
492              
493             One of the goals of this system is to have the system rely on the IOC internally as much as possible and to decouple the components as much as possible. This goal has not been totally achieved, but it is something strived for. The framework itself depends on L<Bolts::Meta::Locator>, which provides all the standard definitions internally. This can be extended to provide additional or even alternate features.
494              
495             All the various components: artifact, bag, locator, blueprint, injector, scope, and inferer are completely extensible. You can create new versions of L<Bolts::Role::Artifact>. You can create bags from almost anything. You can create new locators via L<Bolts::Role::Locator>. You can create new blueprints via L<Bolts::Blueprint>. You can create new scopes via L<Bolts::Scope>. You can create new inferers via L<Bolts::Inferer>. You can then associate these components with the internals using L<Bolts::Meta::Locator>.
496              
497             =head1 THIS CLASS
498              
499             The purpose of the Bolts module itself is to provide some nice syntactic sugar for turning the class that uses it into a bag and locator.
500              
501             =head1 FUNCTIONS
502              
503             =head2 artifact
504              
505             artifact 'name';
506             artifact name => $value;
507             artifact name => %options;
508              
509             This defines an artifact in the current class. This will create a method on the current object with the given "name". If only the name is given, then the artifact to use must be passed when the bag is constructed.
510              
511             # for example, if you bag is named "MyApp"
512             my $bag = MyApp->new( name => 42 );
513             my $value = $bag->acquire('name');
514             say $value; # 42
515              
516             If a scalar or reference is passed in as a single argument in addition to the name, the artifact will be set to that literal value.
517              
518             Otherwise, you may pass in a list of pairs, which will be interpreted depending on the keys present. Here is a list of keys and their meanings:
519              
520             =over
521              
522             =item path
523              
524             This is like an alias to an artifact elsewhere within this bag or in another bag (if "locator" is passed as well). It is set to a reference to an array of names, naming the path within the bag to acquire. See L<Bolts::Blueprint::Acquired> for details.
525              
526             =item value
527              
528             This sets the artifact to a literal value, similar to passing C<$value> in the example above. See L<Bolts::Blueprint::Literal> for details.
529              
530             =item class
531              
532             This should be set to a package name. This causes the artifact to construct and return the value from a factory method or constructor. See L<Bolts::Blueprint::Factory> for details.
533              
534             =item builder
535              
536             This should be set to a subroutine. The subroutine will be called to attain this artifact and the return value used as the artifact. See L<Bolts::Blueprint::Builder> for details.
537              
538             =item blueprint
539              
540             This is set to the name of a L<Bolts::Blueprint> definition and allows you to specify the blueprint you wish to use directly.
541              
542             =item scope
543              
544             In addition to the options above, you may also specify the scope. This is usually either "prototype" or "singleton" and the default is generally "prototype".
545              
546             =back
547              
548             =head2 bag
549              
550             bag 'name' => contains {
551             artifact 'child_name' => 42;
552             };
553              
554             Attaches a bag at the named location. This provides tools for assembling complex IOC configurations.
555              
556             =head2 such_that_each
557              
558             bag 'name' => contains {
559             artifact 'child_name' => 'value';
560              
561             } such_that_each {
562             isa => 'Str',
563             };
564              
565             Causes every artifact within the bag to have the same type constraints, which is handy in some cases. The first argument is a hash that may contain an C<isa> key and a C<does> key, which will be applid to each of the artifacts within. The second argument is the bag definition, which should be built using C<contains> as shown in the description of L</bag>.
566              
567             =head2 builder
568              
569             artifact name => (
570             ...
571             parameters => {
572             thing => builder {
573             return MyApp::Thing->new,
574             },
575             },
576             );
577              
578             This is a helper for setting a L<Bolts::Blueprint::BuiltInjector> for use in passing in a dependency that is wired directly to the builder function given.
579              
580             =head2 dep
581              
582             artifact other => ( ... );
583              
584             artifact name => (
585             ...
586             parameters => {
587             thing => dep('other'),
588             },
589             );
590              
591             This is a helper for laoding dependencies from a path in the current bag (or a bag within it).
592              
593             =head2 option
594              
595             artifact name => (
596             ...
597             parameters => {
598             thing => option {
599             isa => 'MyApp::Thing',
600             required => 1,
601             },
602             other_thing => option {
603             does => 'MyApp::OtherThing',
604             },
605             },
606             );
607              
608             Helper to allow a dependency to be passed as a given option to the call to L<Bolts::Role::Locator/acquire>. To provide validators for the values pass, you may set the C<isa> and C<does> options to a L<Moose> type constraint. To make the option required, set the C<required> option.
609              
610             =head2 value
611              
612             artifact name => (
613             ...
614             parameters => {
615             thing => value 42,
616             },
617             );
618              
619             Helper that passes a literal value through as a dependency to the artifact
620             during injection.
621              
622             =head2 self
623              
624             artifact thing => (
625             ...
626             parameters => {
627             parent => self,
628             },
629             );
630              
631             Sets up a blueprint to return the artifact's parent.
632              
633             =head1 GLOBALS
634              
635             =head2 $Bolts::GLOBAL_FALLBACK_META_LOCATOR
636              
637             B<Subject to Change:> This is the name of the locator to use for locating the meta objects needed to configure within Bolts. The default is L<Bolts::Meta::Locator>, which defines the standard set of scopes, blueprints, etc.
638              
639             This is variable likely to change or disappear in the future.
640              
641             =for Pod::Coverage contains
642             init_meta
643              
644             =head1 AUTHOR
645              
646             Andrew Sterling Hanenkamp <hanenkamp@cpan.org>
647              
648             =head1 COPYRIGHT AND LICENSE
649              
650             This software is copyright (c) 2014 by Qubling Software LLC.
651              
652             This is free software; you can redistribute it and/or modify it under
653             the same terms as the Perl 5 programming language system itself.
654              
655             =cut