File Coverage

blib/lib/Beam/Wire.pm
Criterion Covered Total %
statement 361 381 94.7
branch 131 158 82.9
condition 47 58 81.0
subroutine 49 49 100.0
pod 12 14 85.7
total 600 660 90.9


line stmt bran cond sub pod time code
1             package Beam::Wire;
2             our $VERSION = '1.025';
3             # ABSTRACT: Lightweight Dependency Injection Container
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod # wire.yml
8             #pod captain:
9             #pod class: Person
10             #pod args:
11             #pod name: Malcolm Reynolds
12             #pod rank: Captain
13             #pod first_officer:
14             #pod $class: Person
15             #pod name: ZoĆ« Alleyne Washburne
16             #pod rank: Commander
17             #pod
18             #pod # script.pl
19             #pod use Beam::Wire;
20             #pod my $wire = Beam::Wire->new( file => 'wire.yml' );
21             #pod my $captain = $wire->get( 'captain' );
22             #pod print $captain->name; # "Malcolm Reynolds"
23             #pod
24             #pod =head1 DESCRIPTION
25             #pod
26             #pod Beam::Wire is a configuration module and a dependency injection
27             #pod container. In addition to complex data structures, Beam::Wire configures
28             #pod and creates plain old Perl objects.
29             #pod
30             #pod A dependency injection (DI) container creates an inversion of control:
31             #pod Instead of manually creating all the dependent objects (also called
32             #pod "services") before creating the main object that we actually want, a DI
33             #pod container handles that for us: We describe the relationships between
34             #pod objects, and the objects get built as needed.
35             #pod
36             #pod Dependency injection is sometimes called the opposite of garbage
37             #pod collection. Rather than ensure objects are destroyed in the right order,
38             #pod dependency injection makes sure objects are created in the right order.
39             #pod
40             #pod Using Beam::Wire in your application brings great flexibility,
41             #pod allowing users to easily add their own code to customize how your
42             #pod project behaves.
43             #pod
44             #pod For an L
45             #pod see Beam::Wire::Help::Config|Beam::Wire::Help::Config>.
46             #pod
47             #pod =cut
48              
49 24     24   2126488 use strict;
  24         277  
  24         836  
50 24     24   128 use warnings;
  24         45  
  24         751  
51              
52 24     24   127 use Scalar::Util qw( blessed );
  24         44  
  24         1544  
53 24     24   13731 use Moo;
  24         288152  
  24         131  
54 24     24   49779 use Config::Any;
  24         229512  
  24         1010  
55 24     24   209 use Module::Runtime qw( use_module );
  24         56  
  24         157  
56 24     24   16862 use Data::DPath qw ( dpath );
  24         2938114  
  24         198  
57 24     24   24658 use Path::Tiny qw( path );
  24         232656  
  24         1792  
58 24     24   239 use File::Basename qw( dirname );
  24         54  
  24         1941  
59 24     24   16648 use Types::Standard qw( :all );
  24         1898995  
  24         319  
60 24     24   1178271 use Data::Dumper;
  24         68  
  24         2133  
61 24     24   16570 use Beam::Wire::Event::ConfigService;
  24         102  
  24         1225  
62 24     24   13925 use Beam::Wire::Event::BuildService;
  24         83  
  24         1255  
63 24     24   571 use constant DEBUG => $ENV{BEAM_WIRE_DEBUG};
  24         57  
  24         110756  
64             with 'Beam::Emitter';
65              
66             #pod =attr file
67             #pod
68             #pod The path of the file where services are configured (typically a YAML
69             #pod file). The file's contents should be a single hashref. The keys are
70             #pod service names, and the values are L
71             #pod configurations|Beam::Wire::Help::Config>.
72             #pod
73             #pod =cut
74              
75             has file => (
76             is => 'ro',
77             isa => InstanceOf['Path::Tiny'],
78             coerce => sub {
79             if ( !blessed $_[0] || !$_[0]->isa('Path::Tiny') ) {
80             return path( $_[0] );
81             }
82             return $_[0];
83             },
84             );
85              
86             #pod =attr dir
87             #pod
88             #pod The directory path to use when searching for inner container files.
89             #pod Defaults to the directory which contains the file specified by the
90             #pod L.
91             #pod
92             #pod =cut
93              
94             has dir => (
95             is => 'ro',
96             isa => InstanceOf['Path::Tiny'],
97             lazy => 1,
98             default => sub { $_[0]->file->parent },
99             coerce => sub {
100             if ( !blessed $_[0] || !$_[0]->isa('Path::Tiny') ) {
101             return path( $_[0] );
102             }
103             return $_[0];
104             },
105             );
106              
107             #pod =attr config
108             #pod
109             #pod The raw configuration data. By default, this data is loaded by
110             #pod L using the file specified by the L.
111             #pod
112             #pod See L
113             #pod data structure looks like|Beam::Wire::Help::Config>.
114             #pod
115             #pod If you don't want to load a file, you can specify this attribute in the
116             #pod Beam::Wire constructor.
117             #pod
118             #pod =cut
119              
120             has config => (
121             is => 'ro',
122             isa => HashRef,
123             lazy => 1,
124             builder => 1
125             );
126              
127             sub _build_config {
128 25     25   361 my ( $self ) = @_;
129 25 100       223 return {} if ( !$self->file );
130 20         119 return $self->_load_config( $self->file );
131             }
132              
133             #pod =attr services
134             #pod
135             #pod A hashref of cached services built from the L. If
136             #pod you want to inject a pre-built object for other services to depend on,
137             #pod add it here.
138             #pod
139             #pod =cut
140              
141             has services => (
142             is => 'ro',
143             isa => HashRef,
144             lazy => 1,
145             builder => 1,
146             );
147              
148             sub _build_services {
149 86     86   1045 my ( $self ) = @_;
150 86         220 my $services = {};
151 86         1374 return $services;
152             }
153              
154             #pod =attr meta_prefix
155             #pod
156             #pod The character that begins a meta-property inside of a service's C. This
157             #pod includes C<$ref>, C<$path>, C<$method>, and etc...
158             #pod
159             #pod The default value is C<$>. The empty string is allowed.
160             #pod
161             #pod =cut
162              
163             has meta_prefix => (
164             is => 'ro',
165             isa => Str,
166             default => sub { q{$} },
167             );
168              
169             #pod =method get
170             #pod
171             #pod my $service = $wire->get( $name );
172             #pod my $service = $wire->get( $name, %overrides )
173             #pod
174             #pod The get method resolves and returns the service named C<$name>, creating
175             #pod it, if necessary, with L.
176             #pod
177             #pod C<%overrides> is an optional list of name-value pairs. If specified,
178             #pod get() will create an new, anonymous service that extends the named
179             #pod service with the given config overrides. For example:
180             #pod
181             #pod # test.pl
182             #pod use Beam::Wire;
183             #pod my $wire = Beam::Wire->new(
184             #pod config => {
185             #pod foo => {
186             #pod args => {
187             #pod text => 'Hello, World!',
188             #pod },
189             #pod },
190             #pod },
191             #pod );
192             #pod
193             #pod my $foo = $wire->get( 'foo', args => { text => 'Hello, Chicago!' } );
194             #pod print $foo; # prints "Hello, Chicago!"
195             #pod
196             #pod This allows you to create factories out of any service, overriding service
197             #pod configuration at run-time.
198             #pod
199             #pod If C<$name> contains a slash (C) character (e.g. C), the left
200             #pod side (C) will be used as the name of an inner container, and the
201             #pod right side (C) is a service inside that container. For example,
202             #pod these two lines are equivalent:
203             #pod
204             #pod $bar = $wire->get( 'foo/bar' );
205             #pod $bar = $wire->get( 'foo' )->get( 'bar' );
206             #pod
207             #pod Inner containers can be nested as deeply as desired (C).
208             #pod
209             #pod =cut
210              
211             sub get {
212 279     279 1 153916 my ( $self, $name, %override ) = @_;
213              
214 279         462 ; print STDERR "Get service: $name\n" if DEBUG;
215              
216 279 100       1101 if ( $name =~ q{/} ) {
217 29         150 my ( $container_name, $service_name ) = split m{/}, $name, 2;
218 29         122 my $container = $self->get( $container_name );
219             my $unsub_config = $container->on( configure_service => sub {
220 12     12   8357 my ( $event ) = @_;
221 12         116 $self->emit( configure_service =>
222             class => 'Beam::Wire::Event::ConfigService',
223             service_name => join( '/', $container_name, $event->service_name ),
224             config => $event->config,
225             );
226 29         267 } );
227             my $unsub_build = $container->on( build_service => sub {
228 12     12   7719 my ( $event ) = @_;
229 12         109 $self->emit( build_service =>
230             class => 'Beam::Wire::Event::BuildService',
231             service_name => join( '/', $container_name, $event->service_name ),
232             service => $event->service,
233             );
234 29         58612 } );
235 29         2970 my $service = $container->get( $service_name, %override );
236 29         90 $unsub_config->();
237 29         950 $unsub_build->();
238 29         863 return $service;
239             }
240              
241 250 100       800 if ( keys %override ) {
242 3         20 return $self->create_service(
243             "\$anonymous extends $name",
244             %override,
245             extends => $name,
246             );
247             }
248              
249 247         5790 my $service = $self->services->{$name};
250 247 100       3841 if ( !$service ) {
251 145         231 ; printf STDERR 'Service "%s" does not exist. Creating.' . "\n", $name if DEBUG;
252              
253 145         505 my $config_ref = $self->get_config($name);
254 145 100       1360 unless ( $config_ref ) {
255 2         25 Beam::Wire::Exception::NotFound->throw(
256             name => $name,
257             file => $self->file,
258             );
259             }
260              
261 143         219 ; print STDERR "Got service config: " . Dumper $config_ref if DEBUG;
262              
263 143 100 100     816 if ( ref $config_ref eq 'HASH' && $self->is_meta( $config_ref, 1 ) ) {
264 136         270 my %config = %{ $self->normalize_config( $config_ref ) };
  136         796  
265 136         690 $service = $self->create_service( $name, %config );
266 129 100 100     611 if ( !$config{lifecycle} || lc $config{lifecycle} ne 'factory' ) {
267 124         2539 $self->services->{$name} = $service;
268             }
269             }
270             else {
271 7         21 $self->services->{$name} = $service = $self->find_refs( $name, $config_ref );
272             }
273             }
274              
275 238         1450 ; print STDERR "Returning service: " . Dumper $service if DEBUG;
276              
277 238         1178 return $service;
278             }
279              
280             #pod =method set
281             #pod
282             #pod $wire->set( $name => $service );
283             #pod
284             #pod The set method configures and stores the specified C<$service> with the
285             #pod specified C<$name>. Use this to add or replace built services.
286             #pod
287             #pod Like L, C<$name> can contain a slash (C)
288             #pod character to traverse through nested containers.
289             #pod
290             #pod =cut
291              
292             ## no critic ( ProhibitAmbiguousNames )
293             # This was named set() before I started using Perl::Critic, and will
294             # continue to be named set() now that I no longer use Perl::Critic
295             sub set {
296 2     2 1 30 my ( $self, $name, $service ) = @_;
297 2 100       10 if ( $name =~ q{/} ) {
298 1         7 my ( $container_name, $service_name ) = split m{/}, $name, 2;
299 1         6 return $self->get( $container_name )->set( $service_name, $service );
300             }
301 1         17 $self->services->{$name} = $service;
302 1         9 return;
303             }
304              
305             #pod =method get_config
306             #pod
307             #pod my $conf = $wire->get_config( $name );
308             #pod
309             #pod Get the config with the given C<$name>. Like L
310             #pod above|/get>, C<$name> can contain slash (C) characters to traverse
311             #pod through nested containers.
312             #pod
313             #pod =cut
314              
315             sub get_config {
316 206     206 1 28377 my ( $self, $name ) = @_;
317 206 100       688 if ( $name =~ q{/} ) {
318 4         24 my ( $container_name, $service ) = split m{/}, $name, 2;
319 4         10 my %inner_config = %{ $self->get( $container_name )->get_config( $service ) };
  4         17  
320             # Fix relative references to prefix the container name
321 4         71 my ( $fixed_config ) = $self->fix_refs( $container_name, \%inner_config );
322 4         19 return $fixed_config;
323             }
324 202         3581 return $self->config->{$name};
325             }
326              
327             #pod =method normalize_config
328             #pod
329             #pod my $out_conf = $self->normalize_config( $in_conf );
330             #pod
331             #pod Normalize the given C<$in_conf> into to hash that L
332             #pod method|/create_service> expects. This method allows a service to be
333             #pod defined with prefixed meta-names (C<$class> instead of C) and
334             #pod the arguments specified without prefixes.
335             #pod
336             #pod For example, these two services are identical.
337             #pod
338             #pod foo:
339             #pod class: Foo
340             #pod args:
341             #pod fizz: buzz
342             #pod
343             #pod foo:
344             #pod $class: Foo
345             #pod fizz: buzz
346             #pod
347             #pod The C<$in_conf> must be a hash, and must already pass L
348             #pod check|/is_meta>.
349             #pod
350             #pod =cut
351              
352             sub normalize_config {
353 178     178 1 419 my ( $self, $conf ) = @_;
354              
355 178         252 ; print STDERR "In conf: " . Dumper $conf if DEBUG;
356              
357 178         403 my %meta = reverse $self->get_meta_names;
358              
359             # Confs without prefixed keys can be used as-is
360 178 100       650 return $conf if !grep { $meta{ $_ } } keys %$conf;
  333         1585  
361              
362 15         31 my %out_conf;
363 15         50 for my $key ( keys %$conf ) {
364 26 100       66 if ( $meta{ $key } ) {
365 19         90 $out_conf{ $meta{ $key } } = $conf->{ $key };
366             }
367             else {
368 7         19 $out_conf{ args }{ $key } = $conf->{ $key };
369             }
370             }
371              
372 15         30 ; print STDERR "Out conf: " . Dumper \%out_conf if DEBUG;
373              
374 15         88 return \%out_conf;
375             }
376              
377             #pod =method create_service
378             #pod
379             #pod my $service = $wire->create_service( $name, %config );
380             #pod
381             #pod Create the service with the given C<$name> and C<%config>. Config can
382             #pod contain the following keys:
383             #pod
384             #pod =over 4
385             #pod
386             #pod =item class
387             #pod
388             #pod The class name of an object to create. Can be combined with C,
389             #pod and C. An object of any class can be created with Beam::Wire.
390             #pod
391             #pod =item args
392             #pod
393             #pod The arguments to the constructor method. Used with C and
394             #pod C. Can be a simple value, or a reference to an array or
395             #pod hash which will be dereferenced and passed in to the constructor
396             #pod as a list.
397             #pod
398             #pod If the C consumes the L,
399             #pod the service's C and C will be added to the C.
400             #pod
401             #pod =item method
402             #pod
403             #pod The method to call to create the object. Only used with C.
404             #pod Defaults to C<"new">.
405             #pod
406             #pod This can also be an array of hashes which describe a list of methods
407             #pod that will be called on the object. The first method should create the
408             #pod object, and each subsequent method can be used to modify the object. The
409             #pod hashes should contain a C key, which is a string containing the
410             #pod method to call, and optionally C and C keys. The C
411             #pod key works like the top-level C key, above. The optional C
412             #pod key can have the special value C<"chain">, which will use the return
413             #pod value from the method as the value for the service (L
414             #pod examples of this|Beam::Wire::Help::Config/Multiple Constructor
415             #pod Methods>).
416             #pod
417             #pod If an array is used, the top-level C key is not used.
418             #pod
419             #pod =item value
420             #pod
421             #pod The value of this service. Can be a simple value, or a reference to an
422             #pod array or hash. This value will be simply returned by this method, and is
423             #pod mostly useful when using container files.
424             #pod
425             #pod C can not be used with C or C.
426             #pod
427             #pod =item config
428             #pod
429             #pod The path to a configuration file, relative to L.
430             #pod The file will be read with L, and the resulting data
431             #pod structure returned.
432             #pod
433             #pod =item extends
434             #pod
435             #pod The name of a service to extend. The named service's configuration will
436             #pod be merged with this configuration (via L
437             #pod method|/merge_config>).
438             #pod
439             #pod This can be used in place of the C key if the extended configuration
440             #pod contains a class.
441             #pod
442             #pod =item with
443             #pod
444             #pod Compose a role into the object's class before creating the object. This
445             #pod can be a single string, or an array reference of strings which are roles
446             #pod to combine.
447             #pod
448             #pod This uses L and L
449             #pod method|Role::Tiny/create_class_with_roles>, which should work with any
450             #pod class (as it uses L under the hood).
451             #pod
452             #pod This can be used with the C key.
453             #pod
454             #pod =item on
455             #pod
456             #pod Attach an event handler to a L. This
457             #pod is an array of hashes of event names and handlers. A handler is made from
458             #pod a service reference (C<$ref> or an anonymous service), and a subroutine to
459             #pod call on that service (C<$sub>).
460             #pod
461             #pod For example:
462             #pod
463             #pod emitter:
464             #pod class: My::Emitter
465             #pod on:
466             #pod - my_event:
467             #pod $ref: my_handler
468             #pod $sub: on_my_event
469             #pod
470             #pod This can be used with the C key.
471             #pod
472             #pod =back
473             #pod
474             #pod This method uses L to parse the C key,
475             #pod L as needed.
476             #pod
477             #pod =cut
478              
479             sub create_service {
480 152     152 1 19086 my ( $self, $name, %service_info ) = @_;
481              
482 152         224 ; print STDERR "Creating service: " . Dumper \%service_info if DEBUG;
483              
484             # Compose the parent ref into the copy, in case the parent changes
485 152         574 %service_info = $self->merge_config( %service_info );
486              
487             # value and class/extends are mutually exclusive
488             # must check after merge_config in case parent config has class/value
489 150 100 100     490 if ( exists $service_info{value} && (
      100        
490             exists $service_info{class} || exists $service_info{extends}
491             )
492             ) {
493 6         36 Beam::Wire::Exception::InvalidConfig->throw(
494             name => $name,
495             file => $self->file,
496             error => '"value" cannot be used with "class" or "extends"',
497             );
498             }
499 144 100       435 if ( exists $service_info{value} ) {
500 2         6 return $service_info{value};
501             }
502              
503 142 100       427 if ( $service_info{config} ) {
504 7         28 my $conf_path = path( $service_info{config} );
505 7 100       258 if ( $self->file ) {
506 2         11 $conf_path = path( $self->file )->parent->child( $conf_path );
507             }
508 7         257 return $self->_load_config( "$conf_path" );
509             }
510              
511 135 100       351 if ( !$service_info{class} ) {
512 2         26 Beam::Wire::Exception::InvalidConfig->throw(
513             name => $name,
514             file => $self->file,
515             error => 'Service configuration incomplete. Missing one of "class", "value", "config"',
516             );
517             }
518              
519 133         802 $self->emit( configure_service =>
520             class => 'Beam::Wire::Event::ConfigService',
521             service_name => $name,
522             config => \%service_info,
523             );
524              
525 133         131687 use_module( $service_info{class} );
526              
527 133 100       69512 if ( my $with = $service_info{with} ) {
528 4 100       21 my @roles = ref $with ? @{ $with } : ( $with );
  2         8  
529 4         43 my $class = Moo::Role->create_class_with_roles( $service_info{class}, @roles );
530 4         10327 $service_info{class} = $class;
531             }
532              
533 133   100     737 my $method = $service_info{method} || "new";
534 133         244 my $service;
535 133 100       458 if ( ref $method eq 'ARRAY' ) {
536 2         3 for my $m ( @{$method} ) {
  2         4  
537 4         6 my $method_name = $m->{method};
538 4   100     13 my $return = $m->{return} || q{};
539 4         7 delete $service_info{args};
540 4         11 my @args = $self->parse_args( $name, $service_info{class}, $m->{args} );
541 4 100       9 my $invocant = defined $service ? $service : $service_info{class};
542 4         44 my $output = $invocant->$method_name( @args );
543 4 100 100     138 $service = !defined $service || $return eq 'chain' ? $output
544             : $service;
545             }
546             }
547             else {
548 131         696 my @args = $self->parse_args( $name, @service_info{"class","args"} );
549 131 100 66     2038 if ( $service_info{class}->can( 'DOES' ) && $service_info{class}->DOES( 'Beam::Service' ) ) {
550 1         27 push @args, name => $name, container => $self;
551             }
552 131         3322 $service = $service_info{class}->$method( @args );
553             }
554              
555 132 100       52727 if ( $service_info{on} ) {
556 7         21 my %meta = $self->get_meta_names;
557 7         17 my @listeners;
558              
559 7 100       36 if ( ref $service_info{on} eq 'ARRAY' ) {
    50          
560 1         2 @listeners = map { [ %$_ ] } @{ $service_info{on} };
  2         7  
  1         3  
561             }
562             elsif ( ref $service_info{on} eq 'HASH' ) {
563 6         10 for my $event ( keys %{ $service_info{on} } ) {
  6         22  
564 6 100       21 if ( ref $service_info{on}{$event} eq 'ARRAY' ) {
565             push @listeners,
566 2         5 map {; [ $event => $_ ] }
567 1         2 @{ $service_info{on}{$event} };
  1         4  
568             }
569             else {
570 5         17 push @listeners, [ $event => $service_info{on}{$event} ];
571             }
572             }
573             }
574              
575 7         13 for my $listener ( @listeners ) {
576 9         228 my ( $event, $conf ) = @$listener;
577 9 100 66     30 if ( $conf->{ $meta{method} } && !$conf->{ $meta{sub} } ) {
578 1         6 _deprecated( 'warning: (deprecated) "$method" in event handlers is now "$sub" in service "' . $name . '"' );
579             }
580 9   100     42 my $sub_name = delete $conf->{ $meta{sub} } || delete $conf->{ $meta{method} };
581 9         30 my ( $listen_svc ) = $self->find_refs( $name, $conf );
582 9     8   66 $service->on( $event => sub { $listen_svc->$sub_name( @_ ) } );
  8         12120  
583             }
584             }
585              
586 132         99144 $self->emit( build_service =>
587             class => 'Beam::Wire::Event::BuildService',
588             service_name => $name,
589             service => $service,
590             );
591              
592 132         121196 return $service;
593             }
594              
595             #pod =method merge_config
596             #pod
597             #pod my %merged = $wire->merge_config( %config );
598             #pod
599             #pod If C<%config> contains an C key, merge the extended config together
600             #pod with this one, returning the merged service configuration. This works recursively,
601             #pod so a service can extend a service that extends another service just fine.
602             #pod
603             #pod When merging, hashes are combined, with the child configuration taking
604             #pod precedence. The C key is handled specially to allow a hash of
605             #pod args to be merged.
606             #pod
607             #pod The configuration returned is a safe copy and can be modified without
608             #pod effecting the original config.
609             #pod
610             #pod =cut
611              
612             sub merge_config {
613 190     190 1 536 my ( $self, %service_info ) = @_;
614 190 100       511 if ( $service_info{ extends } ) {
615 37         105 my $base_config_ref = $self->get_config( $service_info{extends} );
616 37 100       384 unless ( $base_config_ref ) {
617             Beam::Wire::Exception::NotFound->throw(
618             name => $service_info{extends},
619 3         31 file => $self->file,
620             );
621             }
622 34         56 my %base_config = %{ $self->normalize_config( $base_config_ref ) };
  34         103  
623             # Merge the args separately, to be a bit nicer about hashes of arguments
624 34         65 my $args;
625 34 100 100     185 if ( ref $service_info{args} eq 'HASH' && ref $base_config{args} eq 'HASH' ) {
626 8         17 $args = { %{ delete $base_config{args} }, %{ delete $service_info{args} } };
  8         24  
  8         31  
627             }
628 34         180 %service_info = ( $self->merge_config( %base_config ), %service_info );
629 34 100       117 if ( $args ) {
630 8         21 $service_info{args} = $args;
631             }
632             }
633 187         685 return %service_info;
634             }
635              
636             #pod =method parse_args
637             #pod
638             #pod my @args = $wire->parse_args( $for_name, $class, $args );
639             #pod
640             #pod Parse the arguments (C<$args>) for the given service (C<$for_name>) with
641             #pod the given class (C<$class>).
642             #pod
643             #pod C<$args> can be an array reference, a hash reference, or a simple
644             #pod scalar. The arguments will be searched for references using L
645             #pod find_refs method|/find_refs>, and then a list of arguments will be
646             #pod returned, ready to pass to the object's constructor.
647             #pod
648             #pod Nested containers are handled specially by this method: Their inner
649             #pod references are not resolved by the parent container. This ensures that
650             #pod references are always relative to the container they're in.
651             #pod
652             #pod =cut
653              
654             sub parse_args {
655 135     135 1 391 my ( $self, $for, $class, $args ) = @_;
656 135 100       366 return if not $args;
657 111         190 my @args;
658 111 100       429 if ( ref $args eq 'ARRAY' ) {
    100          
659 20         52 @args = $self->find_refs( $for, @{$args} );
  20         86  
660             }
661             elsif ( ref $args eq 'HASH' ) {
662             # Hash args could be a ref
663             # Subcontainers cannot scan for refs in their configs
664 86 100       861 if ( $class->isa( 'Beam::Wire' ) ) {
665 8         19 my %args = %{$args};
  8         42  
666 8         26 my $config = delete $args{config};
667             # Relative subcontainer files should be from the current
668             # container's directory
669 8 100 100     59 if ( exists $args{file} && !path( $args{file} )->is_absolute ) {
670 2         252 $args{file} = $self->dir->child( $args{file} );
671             }
672 8         366 @args = $self->find_refs( $for, %args );
673 8 100       41 if ( $config ) {
674 2         7 push @args, config => $config;
675             }
676             }
677             else {
678 78         345 my ( $maybe_ref ) = $self->find_refs( $for, $args );
679 78 50       341 if ( blessed $maybe_ref ) {
680 0         0 @args = ( $maybe_ref );
681             }
682             else {
683 78 0       447 @args = ref $maybe_ref eq 'HASH' ? %$maybe_ref
    50          
684             : ref $maybe_ref eq 'ARRAY' ? @$maybe_ref
685             : ( $maybe_ref );
686             }
687             }
688             }
689             else {
690             # Try anyway?
691 5         8 @args = $args;
692             }
693              
694 111         359 return @args;
695             }
696              
697             #pod =method find_refs
698             #pod
699             #pod my @resolved = $wire->find_refs( $for_name, @args );
700             #pod
701             #pod Go through the C<@args> and recursively resolve any references and
702             #pod services found inside, returning the resolved result. References are
703             #pod identified with L.
704             #pod
705             #pod If a reference contains a C<$ref> key, it will be resolved by L
706             #pod resolve_ref method|/resolve_ref>. Otherwise, the reference will be
707             #pod treated as an anonymous service, and passed directly to L
708             #pod create_service method|/create_service>.
709             #pod
710             #pod This is used when L to ensure all
711             #pod dependencies are created first.
712             #pod
713             #pod =cut
714              
715             sub find_refs {
716 245     245 1 639 my ( $self, $for, @args ) = @_;
717              
718 245         332 ; printf STDERR qq{Searching for refs for "%s": %s}, $for, Dumper \@args if DEBUG;
719              
720 245         357 my @out;
721 245         488 my %meta = $self->get_meta_names;
722 245         682 for my $arg ( @args ) {
723 369 100       912 if ( ref $arg eq 'HASH' ) {
    100          
724 137 100       394 if ( $self->is_meta( $arg ) ) {
725 44 100       174 if ( $arg->{ $meta{ ref } } ) {
726 36         138 push @out, $self->resolve_ref( $for, $arg );
727             }
728             else { # Try to treat it as a service to create
729 8         14 ; print STDERR "Creating anonymous service: " . Dumper $arg if DEBUG;
730              
731 8         30 my %service_info = %{ $self->normalize_config( $arg ) };
  8         29  
732 8         101 push @out, $self->create_service( '$anonymous', %service_info );
733             }
734             }
735             else {
736 93         226 push @out, { $self->find_refs( $for, %{$arg} ) };
  93         389  
737             }
738             }
739             elsif ( ref $arg eq 'ARRAY' ) {
740 13         61 push @out, [ map { $self->find_refs( $for, $_ ) } @{$arg} ];
  30         86  
  13         28  
741             }
742             else {
743 219         433 push @out, $arg; # simple scalars
744             }
745             }
746              
747             # In case we only pass in one argument and want one return value
748 245 100       1461 return wantarray ? @out : $out[-1];
749             }
750              
751             #pod =method is_meta
752             #pod
753             #pod my $is_meta = $wire->is_meta( $ref_hash, $root );
754             #pod
755             #pod Returns true if the given hash reference describes some kind of
756             #pod Beam::Wire service. This is used to identify service configuration
757             #pod hashes inside of larger data structures.
758             #pod
759             #pod A service hash reference must contain at least one key, and must either
760             #pod contain a L key that could create or reference an
761             #pod object (one of C, C, C, C, or C) or,
762             #pod if the C<$root> flag exists, be made completely of unprefixed meta keys
763             #pod (as returned by L).
764             #pod
765             #pod The C<$root> flag is used by L to allow unprefixed
766             #pod meta keys in the top-level hash values.
767             #pod
768             #pod =cut
769              
770             sub is_meta {
771 301     301 1 1740 my ( $self, $arg, $root ) = @_;
772              
773             # Only a hashref can be meta
774 301 50       780 return unless ref $arg eq 'HASH';
775              
776 301         961 my @keys = keys %$arg;
777 301 100       728 return unless @keys;
778              
779 298         679 my %meta = $self->get_meta_names;
780 298         1054 my %meta_names = map { $_ => 1 } values %meta;
  3874         7745  
781              
782             # A regular service does not need the prefix, but must consist
783             # only of meta keys
784 298 100 100     1299 return 1 if $root && scalar @keys eq grep { $meta{ $_ } } @keys;
  286         1776  
785              
786             # A meta service contains at least one of these keys, as these are
787             # the keys that can create a service. All other keys are
788             # modifiers
789             return 1
790 795         1888 if grep { exists $arg->{ $_ } }
791 159 100       355 map { $meta{ $_ } }
  795         1406  
792             qw( ref class extends config value );
793              
794             # Must not be meta
795 101         689 return;
796             }
797              
798             #pod =method get_meta_names
799             #pod
800             #pod my %meta_keys = $wire->get_meta_names;
801             #pod
802             #pod Get all the possible service keys with the L already
803             #pod attached.
804             #pod
805             #pod =cut
806              
807             sub get_meta_names {
808 878     878 1 1422 my ( $self ) = @_;
809 878         1900 my $prefix = $self->meta_prefix;
810 878         8105 my %meta = (
811             ref => "${prefix}ref",
812             path => "${prefix}path",
813             method => "${prefix}method",
814             args => "${prefix}args",
815             class => "${prefix}class",
816             extends => "${prefix}extends",
817             sub => "${prefix}sub",
818             call => "${prefix}call",
819             lifecycle => "${prefix}lifecycle",
820             on => "${prefix}on",
821             with => "${prefix}with",
822             value => "${prefix}value",
823             config => "${prefix}config",
824             );
825 878 50       8744 return wantarray ? %meta : \%meta;
826             }
827              
828             #pod =method resolve_ref
829             #pod
830             #pod my @value = $wire->resolve_ref( $for_name, $ref_hash );
831             #pod
832             #pod Resolves the given dependency from the configuration hash (C<$ref_hash>)
833             #pod for the named service (C<$for_name>). Reference hashes contain the
834             #pod following keys:
835             #pod
836             #pod =over 4
837             #pod
838             #pod =item $ref
839             #pod
840             #pod The name of a service in the container. Required.
841             #pod
842             #pod =item $path
843             #pod
844             #pod A data path to pick some data out of the reference. Useful with C
845             #pod and C services.
846             #pod
847             #pod # container.yml
848             #pod bounties:
849             #pod value:
850             #pod malcolm: 50000
851             #pod zoe: 35000
852             #pod simon: 100000
853             #pod
854             #pod captain:
855             #pod class: Person
856             #pod args:
857             #pod name: Malcolm Reynolds
858             #pod bounty:
859             #pod $ref: bounties
860             #pod $path: /malcolm
861             #pod
862             #pod =item $call
863             #pod
864             #pod Call a method on the referenced object and use the resulting value. This
865             #pod may be a string, which will be the method name to call, or a hash with
866             #pod C<$method> and C<$args>, which are the method name to call and the
867             #pod arguments to that method, respectively.
868             #pod
869             #pod captain:
870             #pod class: Person
871             #pod args:
872             #pod name: Malcolm Reynolds
873             #pod location:
874             #pod $ref: beacon
875             #pod $call: get_location
876             #pod bounty:
877             #pod $ref: news
878             #pod $call:
879             #pod $method: get_bounty
880             #pod $args:
881             #pod name: mreynolds
882             #pod
883             #pod =back
884             #pod
885             #pod =cut
886              
887             sub resolve_ref {
888 36     36 1 96 my ( $self, $for, $arg ) = @_;
889              
890 36         143 my %meta = $self->get_meta_names;
891              
892 36         96 my @ref;
893 36         114 my $name = $arg->{ $meta{ref} };
894 36         180 my $service = $self->get( $name );
895             # resolve service ref w/path
896 36 100       280 if ( my $path = $arg->{ $meta{path} } ) {
    100          
    100          
897             # locate foreign service data
898 2         8 my $conf = $self->get_config($name);
899 2         28 @ref = dpath( $path )->match($service);
900             }
901             elsif ( my $call = $arg->{ $meta{call} } ) {
902 3         7 my ( $method, @args );
903              
904 3 100       11 if ( ref $call eq 'HASH' ) {
905 2         6 $method = $call->{ $meta{method} };
906 2         4 my $args = $call->{ $meta{args} };
907             @args = !$args ? ()
908 2 100       9 : ref $args eq 'ARRAY' ? @{ $args }
  1 50       5  
909             : $args;
910             }
911             else {
912 1         2 $method = $call;
913             }
914              
915 3         14 @ref = $service->$method( @args );
916             }
917             elsif ( my $method = $arg->{ $meta{method} } ) {
918 3         18 _deprecated( 'warning: (deprecated) Using "$method" to get a value in a dependency is now "$call" in service "' . $for . '"' );
919 3         26 my $args = $arg->{ $meta{args} };
920             my @args = !$args ? ()
921 3 100       17 : ref $args eq 'ARRAY' ? @{ $args }
  1 100       3  
922             : $args;
923 3         16 @ref = $service->$method( @args );
924             }
925             else {
926 28         93 @ref = $service;
927             }
928              
929 36         1665 return @ref;
930             }
931              
932             #pod =method fix_refs
933             #pod
934             #pod my @fixed = $wire->fix_refs( $for_container_name, @args );
935             #pod
936             #pod Similar to L. This method searches
937             #pod through the C<@args> and recursively fixes any reference paths to be
938             #pod absolute. References are identified with L
939             #pod method|/is_meta>.
940             #pod
941             #pod This is used by L to ensure that the
942             #pod configuration can be passed directly in to L
943             #pod method|create_service>.
944             #pod
945             #pod =cut
946              
947             sub fix_refs {
948 19     19 1 47 my ( $self, $container_name, @args ) = @_;
949 19         26 my @out;
950 19         40 my %meta = $self->get_meta_names;
951 19         49 for my $arg ( @args ) {
952 22 100       62 if ( ref $arg eq 'HASH' ) {
    100          
953 9 100       25 if ( $self->is_meta( $arg, 1 ) ) {
954             #; print STDERR 'Fixing refs for arg: ' . Dumper $arg;
955 6         25 my %new = %$arg;
956 6         25 for my $key ( keys %new ) {
957 10 100       64 if ( $key =~ /(?:ref|extends)$/ ) {
958 3         21 $new{ $key } = join( q{/}, $container_name, $new{$key} );
959             }
960             else {
961 7         31 ( $new{ $key } ) = $self->fix_refs( $container_name, $new{ $key } );
962             }
963             }
964             #; print STDERR 'Fixed refs for arg: ' . Dumper \%new;
965 6         19 push @out, \%new;
966             }
967             else {
968 3         11 push @out, { $self->fix_refs( $container_name, %{$arg} ) };
  3         22  
969             }
970             }
971             elsif ( ref $arg eq 'ARRAY' ) {
972 3         5 push @out, [ map { $self->fix_refs( $container_name, $_ ) } @{$arg} ];
  5         12  
  3         7  
973             }
974             else {
975 10         23 push @out, $arg; # simple scalars
976             }
977             }
978 19         82 return @out;
979             }
980              
981              
982             #pod =method new
983             #pod
984             #pod my $wire = Beam::Wire->new( %attributes );
985             #pod
986             #pod Create a new container.
987             #pod
988             #pod =cut
989              
990             sub BUILD {
991 96     96 0 2991 my ( $self ) = @_;
992              
993 96 100 100     951 if ( $self->file && !path( $self->file )->exists ) {
994 1         67 my $file = $self->file;
995 1         7 Beam::Wire::Exception::Constructor->throw(
996             attr => 'file',
997             error => qq{Container file '$file' does not exist},
998             );
999             }
1000              
1001             # Create all the eager services
1002 95         2046 my %meta = $self->get_meta_names;
1003 95         279 for my $key ( keys %{ $self->config } ) {
  95         2072  
1004 179         4476 my $config = $self->config->{$key};
1005 179 100       1582 if ( ref $config eq 'HASH' ) {
1006 174   66     926 my $lifecycle = $config->{lifecycle} || $config->{ $meta{lifecycle} };
1007 174 100 100     662 if ( $lifecycle && $lifecycle eq 'eager' ) {
1008 1         4 $self->get($key);
1009             }
1010             }
1011             }
1012 94         1187 return;
1013             }
1014              
1015             my %deprecated_warnings;
1016             sub _deprecated {
1017 4     4   14 my ( $warning ) = @_;
1018 4 50       16 return if $deprecated_warnings{ $warning };
1019 4         68 warn $deprecated_warnings{ $warning } = $warning . "\n";
1020             }
1021              
1022             # Load a config file
1023             sub _load_config {
1024 27     27   107 my ( $self, $path ) = @_;
1025 27         57 local $Config::Any::YAML::NO_YAML_XS_WARNING = 1;
1026              
1027 27         57 my $loader;
1028 27         56 eval {
1029 27         427 $loader = Config::Any->load_files( {
1030             files => [$path], use_ext => 1, flatten_to_hash => 1
1031             } );
1032             };
1033 27 100       470798 if ( $@ ) {
1034 1         22 Beam::Wire::Exception::Config->throw(
1035             file => $self->file,
1036             config_error => $@,
1037             );
1038             }
1039              
1040 26 50       172 return "HASH" eq ref $loader ? (values(%{$loader}))[0] : {};
  26         1043  
1041             }
1042              
1043             # Check config file for known issues and report
1044             # Optionally attempt to get all configured items for complete test
1045             # Intended for use with beam-wire script
1046             sub validate {
1047 4     4 0 1813 my $error_count = 0;
1048 4         12 my @valid_dependency_nodes = qw( class method args extends lifecycle on config );
1049 4         8 my ( $self, $instantiate, $show_all_errors ) = @_;
1050              
1051 4         7 while ( my ( $name, $v ) = each %{ $self->{config} } ) {
  4         19  
1052              
1053 4 50       23 if ($instantiate) {
1054 0 0       0 if ($show_all_errors) {
1055 0         0 eval {
1056 0         0 $self->get($name);
1057             };
1058 0 0       0 print $@ if $@;
1059             }
1060             else {
1061 0         0 $self->get($name);
1062             }
1063 0         0 next;
1064             };
1065              
1066 4         6 my %config = %{ $self->get_config($name) };
  4         11  
1067 4         53 %config = $self->merge_config(%config);
1068              
1069 3 50 66     21 if ( exists $config{value} && ( exists $config{class} || exists $config{extends})) {
      66        
1070 3         5 $error_count++;
1071 3 50       8 if ($show_all_errors) {
1072 0         0 print qq(Invalid config for service '$name': "value" cannot be used with "class" or "extends"\n);
1073 0         0 next;
1074             }
1075              
1076             Beam::Wire::Exception::InvalidConfig->throw(
1077 3         17 name => $name,
1078             file => $self->file,
1079             error => '"value" cannot be used with "class" or "extends"',
1080             );
1081             }
1082              
1083 0 0       0 if ( $config{config} ) {
1084 0         0 my $conf_path = path( $config{config} );
1085 0 0       0 if ( $self->file ) {
1086 0         0 $conf_path = path( $self->file )->parent->child($conf_path);
1087             }
1088 0         0 %config = %{ $self->_load_config("$conf_path") };
  0         0  
1089             }
1090              
1091 0 0 0     0 unless ( $config{value} || $config{class} || $config{extends} ) {
      0        
1092 0         0 next;
1093             }
1094              
1095 0 0       0 if ($config{class}) {
1096 0 0       0 eval "require " . $config{class} if $config{class};
1097             }
1098             #TODO: check method chain & serial
1099             }
1100 0           return $error_count;
1101             }
1102              
1103             #pod =head1 EXCEPTIONS
1104             #pod
1105             #pod If there is an error internal to Beam::Wire, an exception will be thrown. If there is an
1106             #pod error with creating a service or calling a method, the exception thrown will be passed-
1107             #pod through unaltered.
1108             #pod
1109             #pod =head2 Beam::Wire::Exception
1110             #pod
1111             #pod The base exception class
1112             #pod
1113             #pod =cut
1114              
1115             package Beam::Wire::Exception;
1116 24     24   354 use Moo;
  24         519  
  24         197  
1117             with 'Throwable';
1118 24     24   9888 use Types::Standard qw( :all );
  24         72  
  24         186  
1119 24     24   1152301 use overload q{""} => sub { $_[0]->error };
  24     6   68  
  24         358  
  6         16391  
1120              
1121             has error => (
1122             is => 'ro',
1123             isa => Str,
1124             );
1125              
1126             #pod =head2 Beam::Wire::Exception::Constructor
1127             #pod
1128             #pod An exception creating a Beam::Wire object
1129             #pod
1130             #pod =cut
1131              
1132             package Beam::Wire::Exception::Constructor;
1133 24     24   2930 use Moo;
  24         63  
  24         196  
1134 24     24   9921 use Types::Standard qw( :all );
  24         55  
  24         143  
1135             extends 'Beam::Wire::Exception';
1136              
1137             has attr => (
1138             is => 'ro',
1139             isa => Str,
1140             required => 1,
1141             );
1142              
1143             #pod =head2 Beam::Wire::Exception::Config
1144             #pod
1145             #pod An exception loading the configuration file.
1146             #pod
1147             #pod =cut
1148              
1149             package Beam::Wire::Exception::Config;
1150 24     24   1150118 use Moo;
  24         68  
  24         201  
1151 24     24   10287 use Types::Standard qw( :all );
  24         67  
  24         164  
1152             extends 'Beam::Wire::Exception';
1153              
1154             has file => (
1155             is => 'ro',
1156             isa => Maybe[InstanceOf['Path::Tiny']],
1157             );
1158              
1159             has config_error => (
1160             is => 'ro',
1161             isa => Str,
1162             required => 1,
1163             );
1164              
1165             has '+error' => (
1166             lazy => 1,
1167             default => sub {
1168             my ( $self ) = @_;
1169             return sprintf 'Could not load container file "%s": Error from config parser: %s',
1170             $self->file,
1171             $self->config_error;
1172             },
1173             );
1174              
1175             #pod =head2 Beam::Wire::Exception::Service
1176             #pod
1177             #pod An exception with service information inside
1178             #pod
1179             #pod =cut
1180              
1181             package Beam::Wire::Exception::Service;
1182 24     24   1152632 use Moo;
  24         68  
  24         191  
1183 24     24   10090 use Types::Standard qw( :all );
  24         76  
  24         205  
1184             extends 'Beam::Wire::Exception';
1185              
1186             has name => (
1187             is => 'ro',
1188             isa => Str,
1189             required => 1,
1190             );
1191              
1192             has file => (
1193             is => 'ro',
1194             isa => Maybe[InstanceOf['Path::Tiny']],
1195             );
1196              
1197             #pod =head2 Beam::Wire::Exception::NotFound
1198             #pod
1199             #pod The requested service or configuration was not found.
1200             #pod
1201             #pod =cut
1202              
1203             package Beam::Wire::Exception::NotFound;
1204 24     24   1147909 use Moo;
  24         70  
  24         204  
1205             extends 'Beam::Wire::Exception::Service';
1206              
1207             has '+error' => (
1208             lazy => 1,
1209             default => sub {
1210             my ( $self ) = @_;
1211             my $name = $self->name;
1212             my $file = $self->file;
1213             return "Service '$name' not found" . ( $file ? " in file '$file'" : '' );
1214             },
1215             );
1216              
1217             #pod =head2 Beam::Wire::Exception::InvalidConfig
1218             #pod
1219             #pod The configuration is invalid:
1220             #pod
1221             #pod =over 4
1222             #pod
1223             #pod =item *
1224             #pod
1225             #pod Both "value" and "class" or "extends" are defined. These are mutually-exclusive.
1226             #pod
1227             #pod =back
1228             #pod
1229             #pod =cut
1230              
1231             package Beam::Wire::Exception::InvalidConfig;
1232 24     24   12726 use Moo;
  24         63  
  24         129  
1233             extends 'Beam::Wire::Exception::Service';
1234             use overload q{""} => sub {
1235 7     7   16964 my ( $self ) = @_;
1236 7         25 my $file = $self->file;
1237              
1238 7 100       74 sprintf "Invalid config for service '%s': %s%s",
1239             $self->name,
1240             $self->error,
1241             ( $file ? " in file '$file'" : "" ),
1242             ;
1243 24     24   10782 };
  24         61  
  24         372  
1244              
1245             #pod =head1 EVENTS
1246             #pod
1247             #pod The container emits the following events.
1248             #pod
1249             #pod =head2 configure_service
1250             #pod
1251             #pod This event is emitted when a new service is configured, but before it is
1252             #pod instantiated or any classes loaded. This allows altering of the
1253             #pod configuration before the service is built. Already-built services will
1254             #pod not fire this event.
1255             #pod
1256             #pod Event handlers get a L object as their
1257             #pod only argument.
1258             #pod
1259             #pod This event will bubble up from child containers.
1260             #pod
1261             #pod =head2 build_service
1262             #pod
1263             #pod This event is emitted when a new service is built. Cached services will
1264             #pod not fire this event.
1265             #pod
1266             #pod Event handlers get a L object as their
1267             #pod only argument.
1268             #pod
1269             #pod This event will bubble up from child containers.
1270             #pod
1271             #pod =cut
1272              
1273             1;
1274              
1275             __END__