File Coverage

lib/Dist/Zilla/Plugin/Beam/Connector.pm
Criterion Covered Total %
statement 66 72 91.6
branch 10 16 62.5
condition n/a
subroutine 16 16 100.0
pod n/a
total 92 104 88.4


line stmt bran cond sub pod time code
1 2     2   1861 use 5.006; # our
  2         5  
2 2     2   6 use strict;
  2         1  
  2         30  
3 2     2   5 use warnings;
  2         6  
  2         116  
4              
5             package Dist::Zilla::Plugin::Beam::Connector;
6              
7             our $VERSION = '0.001001';
8              
9             # ABSTRACT: Connect events to listeners in Dist::Zilla plugins.
10              
11             our $AUTHORITY = 'cpan:KENTNL'; # AUTHORITY
12              
13 2     2   445 use Moose qw( has around with );
  2         297002  
  2         9  
14 2     2   7621 use MooseX::LazyRequire;
  2         9933  
  2         11  
15 2     2   9171 use Carp qw( croak );
  2         4  
  2         105  
16 2     2   838 use Path::Tiny qw( path );
  2         6937  
  2         661  
17             with 'Dist::Zilla::Role::Plugin';
18              
19             has 'on' => (
20             isa => 'ArrayRef[Str]',
21             is => 'ro',
22             default => sub { [] },
23             );
24              
25             has 'container' => (
26             isa => 'Str',
27             is => 'ro',
28             lazy_required => 1,
29             predicate => '_has_container',
30             );
31              
32             has '_on_parsed' => (
33             isa => 'ArrayRef',
34             is => 'ro',
35             lazy => 1,
36             builder => '_build_on_parsed',
37             );
38              
39             has '_container' => (
40             isa => 'Ref',
41             is => 'ro',
42             lazy => 1,
43             builder => '_build_container',
44             );
45              
46             around mvp_multivalue_args => sub {
47             my ( $orig, $self, @args ) = @_;
48             return ( qw( on ), $self->$orig(@args) );
49             };
50              
51             around plugin_from_config => sub {
52             my ( $orig, $plugin_class, $name, $arg, $own_section ) = @_;
53             my $instance = $plugin_class->$orig( $name, $arg, $own_section );
54             for my $connection ( @{ $instance->_on_parsed } ) {
55             $instance->_connect( $connection->{emitter}, $connection->{listener} );
56             }
57             return $instance;
58             };
59              
60             around dump_config => sub {
61             my ( $orig, $self, @args ) = @_;
62             my $config = $self->$orig(@args);
63             my $payload = $config->{ +__PACKAGE__ } = {};
64             $payload->{'on'} = $self->on;
65             if ( $self->_has_container ) {
66             $payload->{'container'} = $self->container;
67             $payload->{'container.config.keys'} = [ sort keys %{ $self->_container->config } ];
68             }
69              
70             ## no critic (RequireInterpolationOfMetachars)
71             # Skip reporting this unless somebody inherits us.
72             $payload->{ q[$] . __PACKAGE__ . '::VERSION' } = $VERSION unless __PACKAGE__ eq ref $self;
73             $payload->{'$Beam::Wire::VERSION'} = $Beam::Wire::VERSION if $INC{'Beam/Wire.pm'};
74             $payload->{'$Beam::Event::VERSION'} = $Beam::Event::VERSION if $INC{'Beam/Event.pm'};
75             $payload->{'$Beam::Emitter::VERSION'} = $Beam::Event::VERSION if $INC{'Beam/Emitter.pm'};
76             return $config;
77             };
78              
79             __PACKAGE__->meta->make_immutable;
80 2     2   9 no Moose;
  2         2  
  2         9  
81              
82             sub _parse_connector {
83 8     8   5 my ($connector) = @_;
84 8 100       19 if ( $connector =~ /\Aplugin:(.+?)[#]([^#]+)\z/sx ) {
85 6         19 return { type => 'plugin', name => "$1", connection => "$2", };
86             }
87 2 50       6 if ( $connector =~ /\Acontainer:(.+?)[#]([^#]+)\z/sx ) {
88 2         35 return { type => 'container', name => "$1", connection => "$2", };
89             }
90 0         0 croak "Invalid connector specification \"$connector\"\n" #
91             . q[Didn't match "(plugin|container):<id>#<event|listener>"];
92             }
93              
94             sub _parse_on_directive {
95 4     4   2 my ($connection_string) = @_;
96              
97             # Remove leading padding
98 4         9 $connection_string =~ s/\A\s*//sx;
99 4         30 $connection_string =~ s/\s*\z//sx;
100 4 50       22 if ( $connection_string =~ /\A(.+?)\s*=>\s*(.+?)\z/sx ) {
101 4         7 my ( $emitter, $listener ) = ( $1, $2 );
102             return {
103 4         6 emitter => _parse_connector($emitter),
104             listener => _parse_connector($listener),
105             };
106             }
107 0         0 croak "Can't parse 'on' directive \"$connection_string\"\n" #
108             . q[Didn't match "emitter => listener"];
109             }
110              
111             sub _find_connector {
112 8     8   8 my ( $self, $spec ) = @_;
113 8 100       17 if ( 'plugin' eq $spec->{type} ) {
114 6         143 my $plugin = $self->zilla->plugin_named( $spec->{name} );
115 6 50       784 return $plugin if defined $plugin;
116 0         0 croak "Can't resolve plugin \"$spec->{name}\" to an instance.\n" #
117             . q[Did the plugin exist? Is the connection *after* it?];
118             }
119 2 50       6 if ( 'container' eq $spec->{type} ) {
120 2         69 return $self->_container->get( $spec->{'name'} );
121             }
122 0         0 croak "Unknown connector type \"$spec->{type}\"";
123             }
124              
125             # This is to avoid making the sub a closure that contains the emitter
126             sub _make_connector {
127 4     4   6 my ( $recipient, $method_name ) = @_;
128              
129             # Maybe weak ref? IDK
130             return sub {
131 4     4   84 my ($event) = @_;
132 4         19 $recipient->$method_name($event);
133 4         30 };
134             }
135              
136             sub _connect {
137 4     4   4 my ( $self, $emitter, $listener ) = @_;
138 4         6 my $emitter_object = $self->_find_connector($emitter);
139 4         7 my $listener_object = $self->_find_connector($listener);
140              
141 4         69 my $emit_name = $emitter->{type} . $emitter->{name};
142 4         6 my $listen_name = $listener->{type} . $listener->{name};
143              
144 4         6 my $emit_on = $emitter->{connection};
145 4         4 my $listen_on = $listener->{connection};
146              
147 4 50       17 if ( not $emitter_object->can('on') ) {
148 0         0 croak qq[Emitter Target "$emit_name" has no "on" method to register listeners];
149             }
150 4 50       15 if ( not $listener_object->can($listen_on) ) {
151 0         0 croak qq[Listener Target "$listen_name" has no "$listen_on" method to recive events];
152             }
153              
154 4         15 $self->log_debug( [ 'Connecting %s#<%s> to %s#<%s>', $emit_name, $emit_on, $listen_name, $listen_on ] );
155 4         325 $emitter_object->on( $emit_on, _make_connector( $listener_object, $listen_on ) );
156 4         45 return;
157              
158             }
159              
160             sub _build_on_parsed {
161 1     1   1 my ($self) = @_;
162 1         1 return [ map { _parse_on_directive($_) } @{ $self->on } ];
  4         5  
  1         28  
163             }
164              
165             sub _build_container {
166 1     1   1 my ($self) = @_;
167 1         30 my $file = $self->container;
168 1         582 require Beam::Wire;
169 1         193074 $self->log_debug( [ 'Loading Beam::Wire container from %s', $file ] );
170 1         100 my $wire = Beam::Wire->new( file => path( $self->zilla->root, $file ) );
171 1         27895 return $wire;
172             }
173              
174             1;
175              
176             __END__
177              
178             =pod
179              
180             =encoding UTF-8
181              
182             =head1 NAME
183              
184             Dist::Zilla::Plugin::Beam::Connector - Connect events to listeners in Dist::Zilla plugins.
185              
186             =head1 VERSION
187              
188             version 0.001001
189              
190             =head1 SYNOPSIS
191              
192             [Some::PluginA / PluginA]
193             [Some::PluginB / PluginB]
194              
195             [Beam::Connector]
196             ; PluginA emitting event 'foo' passes the event to PluginB
197             on = plugin:PluginA#foo => plugin:PluginB#handle_foo
198             on = plugin:PluginA#bar => plugin:PluginB#handle_bar
199             ; Load 'beam.yml' as a Beam::Wire container
200             container = beam.yml
201             ; Handle Dist::Zilla plugin events with arbitrary classes
202             ; loaded by Beam::Wire
203             on = plugin:PluginA#foo => container:servicename#handle_foo
204             on = plugin:PluginA#bar => container:otherservicename#handle_bar
205              
206             =head1 DESCRIPTION
207              
208             This module aims to allow L<< C<Dist::Zilla>|Dist::Zilla >> to use plugins
209             using L<< C<Beam::Event>|Beam::Event >> and L<< C<Beam::Emitter>|Beam::Emitter >>,
210             and perhaps reduce the need for massive amounts of composition and role application
211             proliferating C<CPAN>.
212              
213             This is in lieu of a decent dependency injection system, and is presently relying
214             on C<Dist::Zilla> to load and construct the plugins itself, and then you just connect
215             the plugins together informally, without necessitating each plugin be specifically
216             tailored to the recipient.
217              
218             Hopefully, this may also give scope for non-C<dzil> plugins being loadable into memory
219             some day, and allowing message passing of events to those plugins. ( Hence, the C<plugin:> prefix )
220              
221             A Real World Example of what a future could look like?
222              
223             [GatherDir]
224              
225             [Test::Compile]
226              
227             [Beam::Connector]
228             on = plugin:GatherDir#collect => plugin:Test::Compile#generate_test
229              
230             C<GatherDir> in this example would build a mutable tree of files,
231             attach them to an event C<::GatherDir::Tree>, and pass that event to C<Test::Compile#generate_test>,
232             which would then add ( or remove, or mutate ) any files in that tree.
233              
234             Tree state mutation then happens in order of prescription, in the order given
235             by the various C<on> declarations.
236              
237             Thus, a single plugin can be in 2 places in the same logical stage.
238              
239             [Beam::Connector]
240             on = plugin:GatherDir#collect => plugin:Test::Compile#generate_test
241             ; lots more collectors here
242             on = plugin:GatherDir#collect => plugin:Test::Compile#finalize_test
243              
244             Whereas presently, order of affect is either governed by:
245              
246             =over 4
247              
248             =item * phase - where you can add but not remove or mutate, mutate but not add or remove, remove, but not add or mutate
249              
250             =item * plugin order - where a single plugin cant be both early in a single phase and late
251              
252             =back
253              
254             If that example is not convincing enough for you, consider all the different ways
255             there are presently for implementing C<[MakeMaker]>. If you're following the standard logic
256             its fine, but as soon as you set out of the box, you have a few things you're going to have to do instead:
257              
258             =over 4
259              
260             =item * Subclass C<MakeMaker> in some way
261              
262             =item * Re-implement C<MakeMaker> in some way
263              
264             =item * Fuss a lot with phase ordering and then inject code in the C<File> that C<MakeMaker> generates.
265              
266             =back
267              
268             These approaches all work, but they're an open door to everyone re-implementing the same thing
269             thousands of times over.
270              
271             [MakeMaker]
272              
273             [DynamicPrereqs]
274             -phases = none
275              
276             [Beam::Connector]
277             on = plugin:MakeMaker#collect_augments => plugin:DynamicPrereqs#inject_augments
278              
279             C<MakeMaker> here can just create an C<event>, pass it to C<DynamicPrereqs>,
280             C<DynamicPrereqs> can inject its desired content into the C<event>,
281             and then C<MakeMaker> can integrate the injected events at "wherever" the right place for them is.
282              
283             This is much superior to scraping the generated text file and injecting events
284             at a given place based on a C<RegEx> match.
285              
286             =head1 PARAMETERS
287              
288             =head2 C<container>
289              
290             Allows loading an arbitrary C<Beam::Wire> container L<< specification|Beam::Wire::Help::Config >>, initializing the
291             relevant objects lazily, and connecting them to relevant events emitted by C<dzil> plugins.
292              
293             [Beam::Connector]
294             container = inc/dist_beam.yml
295              
296             The value can be a path to any file name that C<< Beam::Wire->new( file => ... ) >> understands, (which itself
297             is any file name that C<< Config::Any->load_files >> understands).
298              
299             Items in loaded container can then be referred to by their identifiers to the L<< C<on>|/on >> parameter in the form
300              
301             container:${name}#${method}
302              
303             For example:
304              
305             [Beam::Connector]
306             container = inc/dist_beam.yml
307             on = plugin:GatherDir#gather_files => container:file_gatherer#on_gather_files
308              
309             This would register the object called C<file_gatherer> inside the container to be a recipient of any events called
310             C<gather_files> emitted by the plugin I<named> C<GatherDir>
311              
312             =head2 C<on>
313              
314             Defines a connection between an event emitter and a listener.
315              
316             The general syntax is:
317              
318             on = emitterspec => listenerspec
319              
320             Where C<emitterspec> and C<listenerspec> are of the form
321              
322             objectnamespace:objectname#connector
323              
324             =head3 C<objectnamespace>
325              
326             There are presently two defined object name-spaces.
327              
328             =over 4
329              
330             =item * C<plugin>: Resolves C<objectname> to a C<Dist::Zilla> plugin by its C<name> identifier
331              
332             =item * C<container>: Resolves C<objectname> to an explicitly named object inside an associated L<< C<container>|/container >>
333              
334             =back
335              
336             =head3 C<connector>
337              
338             For an C<emitter>, the C<connector> property identifies the name of the event that is expected to be emitted by
339             that C<emitter>
340              
341             For a C<listener>, the C<connector> property identifies the name of a C<method> that is expected to receive the event.
342              
343             =head1 WRITING EVENT EMITTERS
344              
345             Adding support for hookable events in new and existing C<Dist::Zilla> plugins is relatively straight-forward,
346             and uses L<< C<Beam::Emitter>|Beam::Emitter >>
347              
348             # Somewhere after `use Moose`
349             with "Beam::Emitter";
350              
351             And your class is now ready to broadcast events, and plugins are now able to hook events. Even though they don't
352             exist yet.
353              
354             But that's not very useful in itself. You need to find good places in your code to write events, and construct
355             little bundles of state, "messages" to pass around, and perhaps, allow modifying.
356              
357             =head2 Designing an Event
358              
359             You want to start off designing an event class that communicates the I<absolute minimum> required to be useful.
360              
361             Carrying too much state, or too much indirect state is the enemy.
362              
363             For instance, it would generally be unwise to design an Event that you passed to something which carried a C<$zilla>
364             instance with it.
365              
366             You want to make it as obscure as possible who is even sending the event, as the contents of the event should be usable
367             in total isolation, because you have no idea where your events are going to get sent ( because that is outside the
368             scope of your plugin ), and receivers have no solid expectations of where events are going to come from ( because that
369             is dictated by the connector ).
370              
371             =for stopwords Namespace namespace
372              
373             =head2 Namespace and Indexing recommendations
374              
375             It is presently recommended you define these events inline somewhere, either in the plugin that emits them,
376             or in some shared container.
377              
378             The B<< recommended namespace >> scheme to follow is:
379              
380             Dist::Zilla::Event::
381              
382             Preferably, structuring it similar to your plugin
383              
384             Dist::Zilla::Plugin::Thing::Dooer
385             Dist::Zilla::Event::Thing::Dooer::BeforeDoingThing
386              
387             This I'm sure you'll agree is much nicer than
388              
389             Dist::Zilla::Plugin::Thing::Dooer::BeforeDoingThingEvent # O_O
390             Dist::Zilla::Plugin::BeforeDoingThingEvent # Not a plugin
391              
392             It is also recommended to I<NOT> index said Event packages at present, as that
393             would encourage people depending on the events at some point, which for this system, is
394             likely unwanted toxicity.
395              
396             Only people emitting events should be caring about loading the class.
397              
398             =head2 Implementing an Event
399              
400             Events themselves are quite straight forward: They're just objects, objects extending
401             L<< C<Beam::Event>|Beam::Event >>.
402              
403             This is an example event definition: It will communicate a file name it intends to prepend lines to
404             and pass a mutable, empty array for the event handler to inject lines into.
405              
406             package # hide from PAUSE
407             Dist::Zilla::Event::Prepender::BeforePrepend;
408              
409             use Moose; # or Moo, both work
410             extends "Beam::Event"
411              
412             has 'filename' => (
413             is => 'ro',
414             isa => Str,
415             required => 1,
416             );
417             has 'lines' => (
418             is => 'rw',
419             isa => ArrayRef[Str],
420             lazy => 1,
421             default => sub { [] },
422             );
423             __PACKAGE__->meta->make_immutable;
424              
425             See L<< Using Custom Events in Beam::Emitter|Beam::Emitter/Using Custom Events >> for details.
426              
427             =head2 Emitting and Handling an Event
428              
429             Once you have an Event class designed, gluing it into your code is also quite simple:
430              
431             # somewhere deep in your plugin
432              
433             my $event = $self->emit(
434             'before_append', # the "name" of the event, this corresponds to the "connector"
435             # property in Beam::Connector
436              
437             class => 'Dist::Zilla::Event::Prepender::BeforePrepend', # The class to construct an instance of
438              
439             filename => 'lib/Foo.pm', # attribute property of the Event object.
440             );
441              
442             An instance of C<class> is created with the defined name, and is passed in-order to all the objects who subscribed to the
443             C<before_append> event, and then returned once they're done.
444              
445             And then you can extract any of the state in the passed object and use it to do your work.
446              
447             =head1 WRITING EVENT LISTENERS
448              
449             Fortunately, the requirements for an Event Receiver is B<very> low.
450              
451             =head2 Receiving Events
452              
453             If you're using the C<Dist::Zilla::Plugin>/C<plugin:> approach, all that is required is
454              
455             =over 4
456              
457             =item * A Valid C<Dist::Zilla> plugin that registers in C<< $zilla->plugins >>
458              
459             =item * Some method name of any description that can be passed an argument
460              
461             =back
462              
463             For Example:
464              
465             package My::Plugin;
466              
467             use Moose;
468             with 'Dist::Zilla::Role::Plugin';
469              
470             sub on_before_append {
471             my ( $self, $event ) = @_;
472             ...
473             }
474              
475             If you're using the C<Beam::Wire>/C<container:> approach, all that is required is:
476              
477             =over 4
478              
479             =item * A named object
480              
481             =item * Some method name of any description that can be passed an argument
482              
483             =back
484              
485             For Example:
486              
487             package My::Listener;
488              
489             sub new { bless {}, $_[0] }
490              
491             sub on_before_append {
492             my ( $self, $event ) = @_;
493             ...
494             }
495              
496             These listeners will do nothing on their own, but have events routed to them by
497             relevant C<Beam> configuration.
498              
499             =head2 Identifying and Handling Events
500              
501             Your method will be called with one argument: The event.
502              
503             sub on_whatever {
504             my ( $self, $event ) = @_;
505              
506             }
507              
508             What sort of events you receive of course depends on who sent them.
509              
510             You can then filter them the same way as you would with any Perl Object,
511             via C<< ->isa >> etc,
512              
513             sub on_whatever {
514             my ( $self, $event ) = @_;
515             if ( $event->isa('Dist::Zilla::Plugin::Prepender::AppenderEvent') ) {
516              
517             }
518             }
519              
520             But you can identify events by other means, via the C<< ->name >> property.
521              
522             sub on_whatever {
523             my ( $self, $event ) = @_;
524             if ( q[before_append] eq $event->name ) ) {
525              
526             }
527             }
528              
529             You can then read the data of the event, or potentially modify it in-place, to communicate
530             data back to the sender of the event.
531              
532             sub on_whatever {
533             my ( $self, $event ) = @_;
534             if ( q[before_append] eq $event->name ) ) {
535             push @{$event->lines}, 'use Moose;' if $event->filename =~ /\bMooseX\b/; # Rediculous example I know.
536             }
537             }
538              
539             But you don't need to return anything from the C<sub>, return values are entirely ignored.
540              
541             =head1 FOOTNOTE
542              
543             =for stopwords intra API
544              
545             C<Beam::Event> and C<Beam::Emitter> have some tools for controlling intra-event flow,
546             however, their usage is not 100% clear and their API may be subject to change in future.
547              
548             So I have deleted the L<< relevant instruction on this|https://github.com/kentnl/Dist-Zilla-Plugin-Beam-Connector/compare/1c312f2...5025113 >>
549             and it will be resurrected when I'm more sure about how it should be instructed.
550              
551             =head1 AUTHOR
552              
553             Kent Fredric <kentnl@cpan.org>
554              
555             =head1 COPYRIGHT AND LICENSE
556              
557             This software is copyright (c) 2016 by Kent Fredric <kentfredric@gmail.com>.
558              
559             This is free software; you can redistribute it and/or modify it under
560             the same terms as the Perl 5 programming language system itself.
561              
562             =cut