File Coverage

blib/lib/Beam/Wire.pm
Criterion Covered Total %
statement 364 381 95.5
branch 134 158 84.8
condition 49 58 84.4
subroutine 49 49 100.0
pod 12 14 85.7
total 608 660 92.1


line stmt bran cond sub pod time code
1             package Beam::Wire;
2             our $VERSION = '1.024';
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   1887796 use strict;
  24         246  
  24         658  
50 24     24   120 use warnings;
  24         39  
  24         662  
51              
52 24     24   112 use Scalar::Util qw( blessed );
  24         53  
  24         1196  
53 24     24   12266 use Moo;
  24         256379  
  24         128  
54 24     24   43700 use Config::Any;
  24         203631  
  24         915  
55 24     24   240 use Module::Runtime qw( use_module );
  24         64  
  24         194  
56 24     24   14604 use Data::DPath qw ( dpath );
  24         2615158  
  24         181  
57 24     24   22201 use Path::Tiny qw( path );
  24         209721  
  24         1626  
58 24     24   210 use File::Basename qw( dirname );
  24         51  
  24         1709  
59 24     24   14992 use Types::Standard qw( :all );
  24         1694025  
  24         294  
60 24     24   1046634 use Data::Dumper;
  24         68  
  24         1924  
61 24     24   14993 use Beam::Wire::Event::ConfigService;
  24         88  
  24         1063  
62 24     24   13106 use Beam::Wire::Event::BuildService;
  24         79  
  24         1208  
63 24     24   595 use constant DEBUG => $ENV{BEAM_WIRE_DEBUG};
  24         63  
  24         100090  
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   247 my ( $self ) = @_;
129 25 100       167 return {} if ( !$self->file );
130 20         139 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   892 my ( $self ) = @_;
150 86         191 my $services = {};
151 86         1270 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 129515 my ( $self, $name, %override ) = @_;
213              
214 279         381 ; print STDERR "Get service: $name\n" if DEBUG;
215              
216 279 100       884 if ( $name =~ q{/} ) {
217 29         101 my ( $container_name, $service_name ) = split m{/}, $name, 2;
218 29         76 my $container = $self->get( $container_name );
219             my $unsub_config = $container->on( configure_service => sub {
220 12     12   6090 my ( $event ) = @_;
221 12         98 $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         187 } );
227             my $unsub_build = $container->on( build_service => sub {
228 12     12   5907 my ( $event ) = @_;
229 12         86 $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         45531 } );
235 29         2303 my $service = $container->get( $service_name, %override );
236 29         74 $unsub_config->();
237 29         696 $unsub_build->();
238 29         667 return $service;
239             }
240              
241 250 100       658 if ( keys %override ) {
242 3         18 return $self->create_service(
243             "\$anonymous extends $name",
244             %override,
245             extends => $name,
246             );
247             }
248              
249 247         4857 my $service = $self->services->{$name};
250 247 100       3371 if ( !$service ) {
251 145         199 ; printf STDERR 'Service "%s" does not exist. Creating.' . "\n", $name if DEBUG;
252              
253 145         404 my $config_ref = $self->get_config($name);
254 145 100       1180 unless ( $config_ref ) {
255 2         24 Beam::Wire::Exception::NotFound->throw(
256             name => $name,
257             file => $self->file,
258             );
259             }
260              
261 143         190 ; print STDERR "Got service config: " . Dumper $config_ref if DEBUG;
262              
263 143 100 100     674 if ( ref $config_ref eq 'HASH' && $self->is_meta( $config_ref, 1 ) ) {
264 136         218 my %config = %{ $self->normalize_config( $config_ref ) };
  136         701  
265 136         564 $service = $self->create_service( $name, %config );
266 129 100 100     554 if ( !$config{lifecycle} || lc $config{lifecycle} ne 'factory' ) {
267 124         2089 $self->services->{$name} = $service;
268             }
269             }
270             else {
271 7         30 $self->services->{$name} = $service = $self->find_refs( $name, $config_ref );
272             }
273             }
274              
275 238         1241 ; print STDERR "Returning service: " . Dumper $service if DEBUG;
276              
277 238         987 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 25 my ( $self, $name, $service ) = @_;
297 2 100       8 if ( $name =~ q{/} ) {
298 1         3 my ( $container_name, $service_name ) = split m{/}, $name, 2;
299 1         3 return $self->get( $container_name )->set( $service_name, $service );
300             }
301 1         14 $self->services->{$name} = $service;
302 1         8 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 207     207 1 25732 my ( $self, $name ) = @_;
317 207 100       587 if ( $name =~ q{/} ) {
318 4         14 my ( $container_name, $service ) = split m{/}, $name, 2;
319 4         7 my %inner_config = %{ $self->get( $container_name )->get_config( $service ) };
  4         10  
320             # Fix relative references to prefix the container name
321 4         51 my ( $fixed_config ) = $self->fix_refs( $container_name, \%inner_config );
322 4         12 return $fixed_config;
323             }
324 203         3272 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 382 my ( $self, $conf ) = @_;
354              
355 178         219 ; print STDERR "In conf: " . Dumper $conf if DEBUG;
356              
357 178         342 my %meta = reverse $self->get_meta_names;
358              
359             # Confs without prefixed keys can be used as-is
360 178 100       654 return $conf if !grep { $meta{ $_ } } keys %$conf;
  333         1256  
361              
362 15         32 my %out_conf;
363 15         43 for my $key ( keys %$conf ) {
364 26 100       58 if ( $meta{ $key } ) {
365 19         65 $out_conf{ $meta{ $key } } = $conf->{ $key };
366             }
367             else {
368 7         19 $out_conf{ args }{ $key } = $conf->{ $key };
369             }
370             }
371              
372 15         24 ; print STDERR "Out conf: " . Dumper \%out_conf if DEBUG;
373              
374 15         81 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 18488 my ( $self, $name, %service_info ) = @_;
481              
482 152         198 ; 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         471 %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     439 if ( exists $service_info{value} && (
      100        
490             exists $service_info{class} || exists $service_info{extends}
491             )
492             ) {
493 6         38 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       310 if ( $service_info{value} ) {
500 2         9 return $service_info{value};
501             }
502              
503 142 100       304 if ( $service_info{config} ) {
504 7         27 my $conf_path = path( $service_info{config} );
505 7 100       224 if ( $self->file ) {
506 2         7 $conf_path = path( $self->file )->parent->child( $conf_path );
507             }
508 7         221 return $self->_load_config( "$conf_path" );
509             }
510              
511 135 100       340 if ( !$service_info{class} ) {
512 2         32 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         629 $self->emit( configure_service =>
520             class => 'Beam::Wire::Event::ConfigService',
521             service_name => $name,
522             config => \%service_info,
523             );
524              
525 133         118095 use_module( $service_info{class} );
526              
527 133 100       62295 if ( my $with = $service_info{with} ) {
528 4 100       18 my @roles = ref $with ? @{ $with } : ( $with );
  2         7  
529 4         41 my $class = Moo::Role->create_class_with_roles( $service_info{class}, @roles );
530 4         8737 $service_info{class} = $class;
531             }
532              
533 133   100     617 my $method = $service_info{method} || "new";
534 133         225 my $service;
535 133 100       325 if ( ref $method eq 'ARRAY' ) {
536 2         4 for my $m ( @{$method} ) {
  2         3  
537 4         8 my $method_name = $m->{method};
538 4   100     10 my $return = $m->{return} || q{};
539 4         5 delete $service_info{args};
540 4         13 my @args = $self->parse_args( $name, $service_info{class}, $m->{args} );
541 4 100       8 my $invocant = defined $service ? $service : $service_info{class};
542 4         42 my $output = $invocant->$method_name( @args );
543 4 100 100     127 $service = !defined $service || $return eq 'chain' ? $output
544             : $service;
545             }
546             }
547             else {
548 131         527 my @args = $self->parse_args( $name, @service_info{"class","args"} );
549 131 100 66     1692 if ( $service_info{class}->can( 'DOES' ) && $service_info{class}->DOES( 'Beam::Service' ) ) {
550 1         19 push @args, name => $name, container => $self;
551             }
552 131         2876 $service = $service_info{class}->$method( @args );
553             }
554              
555 132 100       43850 if ( $service_info{on} ) {
556 7         22 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         6  
  1         4  
561             }
562             elsif ( ref $service_info{on} eq 'HASH' ) {
563 6         10 for my $event ( keys %{ $service_info{on} } ) {
  6         20  
564 6 100       19 if ( ref $service_info{on}{$event} eq 'ARRAY' ) {
565             push @listeners,
566 2         6 map {; [ $event => $_ ] }
567 1         2 @{ $service_info{on}{$event} };
  1         3  
568             }
569             else {
570 5         18 push @listeners, [ $event => $service_info{on}{$event} ];
571             }
572             }
573             }
574              
575 7         15 for my $listener ( @listeners ) {
576 9         200 my ( $event, $conf ) = @$listener;
577 9 100 66     32 if ( $conf->{ $meta{method} } && !$conf->{ $meta{sub} } ) {
578 1         5 _deprecated( 'warning: (deprecated) "$method" in event handlers is now "$sub" in service "' . $name . '"' );
579             }
580 9   100     39 my $sub_name = delete $conf->{ $meta{sub} } || delete $conf->{ $meta{method} };
581 9         26 my ( $listen_svc ) = $self->find_refs( $name, $conf );
582 9     8   50 $service->on( $event => sub { $listen_svc->$sub_name( @_ ) } );
  8         11717  
583             }
584             }
585              
586 132         99619 $self->emit( build_service =>
587             class => 'Beam::Wire::Event::BuildService',
588             service_name => $name,
589             service => $service,
590             );
591              
592 132         109236 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 191     191 1 465 my ( $self, %service_info ) = @_;
614 191 100       460 if ( $service_info{ extends } ) {
615 37         94 my $base_config_ref = $self->get_config( $service_info{extends} );
616 37 100       307 unless ( $base_config_ref ) {
617             Beam::Wire::Exception::NotFound->throw(
618             name => $service_info{extends},
619 3         27 file => $self->file,
620             );
621             }
622 34         61 my %base_config = %{ $self->normalize_config( $base_config_ref ) };
  34         75  
623             # Merge the args separately, to be a bit nicer about hashes of arguments
624 34         59 my $args;
625 34 100 100     145 if ( ref $service_info{args} eq 'HASH' && ref $base_config{args} eq 'HASH' ) {
626 8         20 $args = { %{ delete $base_config{args} }, %{ delete $service_info{args} } };
  8         23  
  8         27  
627             }
628 34         152 %service_info = ( $self->merge_config( %base_config ), %service_info );
629 34 100       101 if ( $args ) {
630 8         20 $service_info{args} = $args;
631             }
632             }
633 188         637 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 336 my ( $self, $for, $class, $args ) = @_;
656 135 100       320 return if not $args;
657 111         162 my @args;
658 111 100       365 if ( ref $args eq 'ARRAY' ) {
    100          
659 20         29 @args = $self->find_refs( $for, @{$args} );
  20         55  
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       713 if ( $class->isa( 'Beam::Wire' ) ) {
665 8         15 my %args = %{$args};
  8         33  
666 8         17 my $config = delete $args{config};
667             # Relative subcontainer files should be from the current
668             # container's directory
669 8 100 100     52 if ( exists $args{file} && !path( $args{file} )->is_absolute ) {
670 2         164 $args{file} = $self->dir->child( $args{file} );
671             }
672 8         274 @args = $self->find_refs( $for, %args );
673 8 100       22 if ( $config ) {
674 2         9 push @args, config => $config;
675             }
676             }
677             else {
678 78         262 my ( $maybe_ref ) = $self->find_refs( $for, $args );
679 78 50       290 if ( blessed $maybe_ref ) {
680 0         0 @args = ( $maybe_ref );
681             }
682             else {
683 78 0       427 @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         10 @args = $args;
692             }
693              
694 111         293 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 568 my ( $self, $for, @args ) = @_;
717              
718 245         278 ; printf STDERR qq{Searching for refs for "%s": %s}, $for, Dumper \@args if DEBUG;
719              
720 245         289 my @out;
721 245         453 my %meta = $self->get_meta_names;
722 245         588 for my $arg ( @args ) {
723 369 100       735 if ( ref $arg eq 'HASH' ) {
    100          
724 137 100       309 if ( $self->is_meta( $arg ) ) {
725 44 100       146 if ( $arg->{ $meta{ ref } } ) {
726 36         135 push @out, $self->resolve_ref( $for, $arg );
727             }
728             else { # Try to treat it as a service to create
729 8         13 ; print STDERR "Creating anonymous service: " . Dumper $arg if DEBUG;
730              
731 8         13 my %service_info = %{ $self->normalize_config( $arg ) };
  8         33  
732 8         58 push @out, $self->create_service( '$anonymous', %service_info );
733             }
734             }
735             else {
736 93         169 push @out, { $self->find_refs( $for, %{$arg} ) };
  93         365  
737             }
738             }
739             elsif ( ref $arg eq 'ARRAY' ) {
740 13         18 push @out, [ map { $self->find_refs( $for, $_ ) } @{$arg} ];
  30         65  
  13         23  
741             }
742             else {
743 219         419 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       1196 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 1593 my ( $self, $arg, $root ) = @_;
772              
773             # Only a hashref can be meta
774 301 50       665 return unless ref $arg eq 'HASH';
775              
776 301         843 my @keys = keys %$arg;
777 301 100       595 return unless @keys;
778              
779 298         684 my %meta = $self->get_meta_names;
780 298         929 my %meta_names = map { $_ => 1 } values %meta;
  3874         6871  
781              
782             # A regular service does not need the prefix, but must consist
783             # only of meta keys
784 298 100 100     1072 return 1 if $root && scalar @keys eq grep { $meta{ $_ } } @keys;
  286         1476  
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         1546 if grep { exists $arg->{ $_ } }
791 159 100       301 map { $meta{ $_ } }
  795         1260  
792             qw( ref class extends config value );
793              
794             # Must not be meta
795 101         517 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 1286 my ( $self ) = @_;
809 878         1591 my $prefix = $self->meta_prefix;
810 878         6643 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       7465 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 89 my ( $self, $for, $arg ) = @_;
889              
890 36         80 my %meta = $self->get_meta_names;
891              
892 36         80 my @ref;
893 36         85 my $name = $arg->{ $meta{ref} };
894 36         148 my $service = $self->get( $name );
895             # resolve service ref w/path
896 36 100       254 if ( my $path = $arg->{ $meta{path} } ) {
    100          
    100          
897             # locate foreign service data
898 2         9 my $conf = $self->get_config($name);
899 2         30 @ref = dpath( $path )->match($service);
900             }
901             elsif ( my $call = $arg->{ $meta{call} } ) {
902 3         7 my ( $method, @args );
903              
904 3 100       17 if ( ref $call eq 'HASH' ) {
905 2         6 $method = $call->{ $meta{method} };
906 2         6 my $args = $call->{ $meta{args} };
907             @args = !$args ? ()
908 2 100       13 : ref $args eq 'ARRAY' ? @{ $args }
  1 50       4  
909             : $args;
910             }
911             else {
912 1         3 $method = $call;
913             }
914              
915 3         17 @ref = $service->$method( @args );
916             }
917             elsif ( my $method = $arg->{ $meta{method} } ) {
918 3         15 _deprecated( 'warning: (deprecated) Using "$method" to get a value in a dependency is now "$call" in service "' . $for . '"' );
919 3         19 my $args = $arg->{ $meta{args} };
920             my @args = !$args ? ()
921 3 100       12 : ref $args eq 'ARRAY' ? @{ $args }
  1 100       3  
922             : $args;
923 3         13 @ref = $service->$method( @args );
924             }
925             else {
926 28         77 @ref = $service;
927             }
928              
929 36         1573 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 36 my ( $self, $container_name, @args ) = @_;
949 19         20 my @out;
950 19         29 my %meta = $self->get_meta_names;
951 19         38 for my $arg ( @args ) {
952 22 100       45 if ( ref $arg eq 'HASH' ) {
    100          
953 9 100       18 if ( $self->is_meta( $arg, 1 ) ) {
954             #; print STDERR 'Fixing refs for arg: ' . Dumper $arg;
955 6         19 my %new = %$arg;
956 6         14 for my $key ( keys %new ) {
957 10 100       47 if ( $key =~ /(?:ref|extends)$/ ) {
958 3         14 $new{ $key } = join( q{/}, $container_name, $new{$key} );
959             }
960             else {
961 7         23 ( $new{ $key } ) = $self->fix_refs( $container_name, $new{ $key } );
962             }
963             }
964             #; print STDERR 'Fixed refs for arg: ' . Dumper \%new;
965 6         14 push @out, \%new;
966             }
967             else {
968 3         37 push @out, { $self->fix_refs( $container_name, %{$arg} ) };
  3         15  
969             }
970             }
971             elsif ( ref $arg eq 'ARRAY' ) {
972 3         5 push @out, [ map { $self->fix_refs( $container_name, $_ ) } @{$arg} ];
  5         8  
  3         6  
973             }
974             else {
975 10         15 push @out, $arg; # simple scalars
976             }
977             }
978 19         67 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 2606 my ( $self ) = @_;
992              
993 96 100 100     732 if ( $self->file && !path( $self->file )->exists ) {
994 1         63 my $file = $self->file;
995 1         6 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         1313 my %meta = $self->get_meta_names;
1003 95         235 for my $key ( keys %{ $self->config } ) {
  95         1824  
1004 179         3760 my $config = $self->config->{$key};
1005 179 100       1342 if ( ref $config eq 'HASH' ) {
1006 174   66     718 my $lifecycle = $config->{lifecycle} || $config->{ $meta{lifecycle} };
1007 174 100 100     578 if ( $lifecycle && $lifecycle eq 'eager' ) {
1008 1         5 $self->get($key);
1009             }
1010             }
1011             }
1012 94         877 return;
1013             }
1014              
1015             my %deprecated_warnings;
1016             sub _deprecated {
1017 4     4   10 my ( $warning ) = @_;
1018 4 50       12 return if $deprecated_warnings{ $warning };
1019 4         47 warn $deprecated_warnings{ $warning } = $warning . "\n";
1020             }
1021              
1022             # Load a config file
1023             sub _load_config {
1024 27     27   110 my ( $self, $path ) = @_;
1025 27         50 local $Config::Any::YAML::NO_YAML_XS_WARNING = 1;
1026              
1027 27         35 my $loader;
1028 27         45 eval {
1029 27         317 $loader = Config::Any->load_files( {
1030             files => [$path], use_ext => 1, flatten_to_hash => 1
1031             } );
1032             };
1033 27 100       382263 if ( $@ ) {
1034 1         19 Beam::Wire::Exception::Config->throw(
1035             file => $self->file,
1036             config_error => $@,
1037             );
1038             }
1039              
1040 26 50       115 return "HASH" eq ref $loader ? (values(%{$loader}))[0] : {};
  26         671  
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 1832 my $error_count = 0;
1048 4         14 my @valid_dependency_nodes = qw( class method args extends lifecycle on config );
1049 4         9 my ( $self, $instantiate, $show_all_errors ) = @_;
1050              
1051 4         7 while ( my ( $name, $v ) = each %{ $self->{config} } ) {
  5         21  
1052              
1053 5 50       12 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 5         7 my %config = %{ $self->get_config($name) };
  5         12  
1067 5         59 %config = $self->merge_config(%config);
1068              
1069 4 100 100     29 if ( exists $config{value} && ( exists $config{class} || exists $config{extends})) {
      66        
1070 3         4 $error_count++;
1071 3 50       7 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         16 name => $name,
1078             file => $self->file,
1079             error => '"value" cannot be used with "class" or "extends"',
1080             );
1081             }
1082              
1083 1 50       5 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 1 0 33     5 unless ( $config{value} || $config{class} || $config{extends} ) {
      0        
1092 0         0 next;
1093             }
1094              
1095 1 50       4 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   405 use Moo;
  24         420  
  24         192  
1117             with 'Throwable';
1118 24     24   9582 use Types::Standard qw( :all );
  24         57  
  24         197  
1119 24     24   1041710 use overload q{""} => sub { $_[0]->error };
  24     6   58  
  24         326  
  6         16555  
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   2853 use Moo;
  24         52  
  24         192  
1134 24     24   9380 use Types::Standard qw( :all );
  24         62  
  24         159  
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   1034719 use Moo;
  24         63  
  24         193  
1151 24     24   9659 use Types::Standard qw( :all );
  24         53  
  24         140  
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   1046471 use Moo;
  24         62  
  24         192  
1183 24     24   9866 use Types::Standard qw( :all );
  24         76  
  24         158  
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   1041612 use Moo;
  24         61  
  24         241  
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   12231 use Moo;
  24         56  
  24         104  
1233             extends 'Beam::Wire::Exception::Service';
1234             use overload q{""} => sub {
1235 7     7   16736 my ( $self ) = @_;
1236 7         22 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   9387 };
  24         56  
  24         368  
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__