File Coverage

blib/lib/Perl/ToPerl6/TransformerFactory.pm
Criterion Covered Total %
statement 213 240 88.7
branch 37 66 56.0
condition 17 37 45.9
subroutine 35 35 100.0
pod 5 5 100.0
total 307 383 80.1


line stmt bran cond sub pod time code
1             package Perl::ToPerl6::TransformerFactory;
2              
3 1     1   14 use 5.006001;
  1         4  
4 1     1   5 use strict;
  1         2  
  1         18  
5 1     1   5 use warnings;
  1         1  
  1         23  
6              
7 1     1   4 use English qw(-no_match_vars);
  1         2  
  1         8  
8              
9 1     1   477 use File::Spec::Unix qw();
  1         3  
  1         23  
10 1     1   5 use List::Util qw(max);
  1         1  
  1         94  
11 1     1   4 use List::MoreUtils qw(any);
  1         2  
  1         8  
12              
13 1         52 use Perl::ToPerl6::Utils qw{
14             :characters
15             $TRANSFORMER_NAMESPACE
16             :data_conversion
17             transformer_long_name
18             transformer_short_name
19             :internal_lookup
20 1     1   366 };
  1         2  
21 1     1   787 use Perl::ToPerl6::TransformerConfig;
  1         3  
  1         31  
22 1     1   5 use Perl::ToPerl6::Exception::AggregateConfiguration;
  1         2  
  1         35  
23 1     1   5 use Perl::ToPerl6::Exception::Configuration;
  1         2  
  1         32  
24 1     1   5 use Perl::ToPerl6::Exception::Fatal::Generic qw{ throw_generic };
  1         1  
  1         43  
25 1     1   4 use Perl::ToPerl6::Exception::Fatal::Internal qw{ throw_internal };
  1         2  
  1         39  
26             use Perl::ToPerl6::Exception::Fatal::TransformerDefinition
27 1     1   529 qw{ throw_transformer_definition };
  1         3  
  1         19  
28 1     1   564 use Perl::ToPerl6::Exception::Configuration::NonExistentTransformer qw< >;
  1         2  
  1         22  
29 1     1   5 use Perl::ToPerl6::Utils::Constants qw{ :profile_strictness };
  1         1  
  1         119  
30              
31 1     1   4 use Exception::Class; # this must come after "use P::C::Exception::*"
  1         4  
  1         5  
32              
33             #-----------------------------------------------------------------------------
34              
35             # Globals. Ick!
36             my @site_transformer_names = ();
37              
38             #-----------------------------------------------------------------------------
39              
40             # Blech!!! This is ug-lee. Belongs in the constructor. And it shouldn't be
41             # called "test" mode.
42             sub import {
43              
44 4     4   526 my ( $class, %args ) = @_;
45 4         14 my $test_mode = $args{-test};
46 4         10 my $extra_test_transformers = $args{'-extra-test-transformers'};
47              
48 4 100       19 if ( not @site_transformer_names ) {
49 1         1 my $eval_worked = eval {
50 1         785 require Module::Pluggable;
51 1         11732 Module::Pluggable->import(search_path => $TRANSFORMER_NAMESPACE,
52             require => 1, inner => 0);
53 1         57 @site_transformer_names = plugins(); #Exported by Module::Pluggable
54 1         12276 1;
55             };
56              
57 1 50       6 if (not $eval_worked) {
58 0 0       0 if ( $EVAL_ERROR ) {
59 0         0 throw_generic
60             qq<Can't load Transformers from namespace "$TRANSFORMER_NAMESPACE": $EVAL_ERROR>;
61             }
62              
63             throw_generic
64 0         0 qq<Can't load Transformers from namespace "$TRANSFORMER_NAMESPACE" for an unknown reason.>;
65             }
66              
67 1 50       6 if ( not @site_transformer_names ) {
68 0         0 throw_generic
69             qq<No Transformers found in namespace "$TRANSFORMER_NAMESPACE".>;
70             }
71             }
72              
73             # In test mode, only load native transformers, not third-party ones. So this
74             # filters out any transformer that was loaded from within a directory called
75             # "blib". During the usual "./Build test" process this works fine,
76             # but it doesn't work if you are using prove to test against the code
77             # directly in the lib/ directory.
78              
79 4 100 66 1   31 if ( $test_mode && any {m/\b blib \b/xms} @INC ) {
  1         12  
80 1         7 @site_transformer_names = _modules_from_blib( @site_transformer_names );
81              
82 1 50       30 if ($extra_test_transformers) {
83             my @extra_transformer_full_names =
84 0         0 map { "${TRANSFORMER_NAMESPACE}::$_" } @{$extra_test_transformers};
  0         0  
  0         0  
85              
86 0         0 push @site_transformer_names, @extra_transformer_full_names;
87             }
88             }
89              
90 4         155 return 1;
91             }
92              
93             #-----------------------------------------------------------------------------
94             # Shuffle transformer order based on preferences, if any.
95              
96             # Transformers can request to run before or after a given list of other
97             # transformers. This code rewrites the list as follows:
98             #
99             # If transformer A requests to be run *before* transformer B, then we instead
100             # state that transformer B must be run *after* transformer A. It's the logical
101             # dual, but having only one kind of dependency to deal with makes it easier.
102             #
103             sub _invert_dependencies {
104 1     1   3 my ($dependencies) = @_;
105              
106 1         2 for my $name ( keys %{ $dependencies } ) {
  1         5  
107 37 100       77 next unless $dependencies->{$name}{before};
108 4         5 for my $_name ( keys %{ $dependencies->{$name}{before} } ) {
  4         9  
109 6         15 $dependencies->{$_name}{after}{$name} = 1;
110             }
111             }
112             }
113              
114             # Collect the preferences for all the transformers we want to run.
115             #
116             sub _collect_preferences {
117 1     1   6 my (@transformers) = @_;
118 1         2 my $preferences;
119              
120 1         2 for my $transformer ( @transformers ) {
121 37         60 my $ref_name = ref($transformer);
122 37         95 $ref_name =~ s< ^ Perl\::ToPerl6\::Transformer\:: ><>x;
123 37         78 $preferences->{$ref_name} = { };
124              
125             # Get the list of transformers this module wants to run *after*.
126             #
127 37 100       208 if ( $transformer->can('run_before') ) {
128 4         16 my @before = $transformer->run_before();
129             $preferences->{$ref_name}{before} = { map {
130 4         7 s< ^ Perl\::ToPerl6\::Transformer\:: ><>x;
  6         7  
131 6         21 $_ => 1
132             } @before };
133             }
134              
135             # Get the list of transformers this module wants to run *before*.
136             #
137 37 100       220 if ( $transformer->can('run_after') ) {
138 7         27 my @after = $transformer->run_after();
139             $preferences->{$ref_name}{after} = { map {
140 7         9 s< ^ Perl\::ToPerl6\::Transformer\:: ><>x;
  7         11  
141 7         35 $_ => 1
142             } @after };
143             }
144             }
145              
146 1         4 return $preferences;
147             }
148              
149             sub _validate_preferences {
150 1     1   3 my ($preferences) = @_;
151              
152 1         1 for my $k ( keys %{ $preferences } ) {
  1         9  
153 37 100 33     108 if ( $preferences->{$k} and
154             $preferences->{$k}{after} ) {
155 7         8 for my $_k ( keys %{ $preferences->{$k}{after} } ) {
  7         15  
156 7 50       20 next if exists $preferences->{$_k};
157 0         0 die "Module $k wanted to run after module $_k, which was not found!\n";
158             }
159             }
160 37 100 33     120 if ( $preferences->{$k} and
161             $preferences->{$k}{before} ) {
162 4         4 for my $_k ( keys %{ $preferences->{$k}{before} } ) {
  4         11  
163 6 50       17 next if exists $preferences->{$_k};
164 0         0 die "Module $k wanted to run before module $_k, which was not found!\n";
165             }
166             }
167             }
168             }
169              
170             # Transformers can now request to be run before or after a given transformer.
171             # Or transformers.
172             #
173             # We honor those requests here, by collecting the transformers, calling
174             # ->run_before() and/or ->run_after(), to get what transformers they must
175             # run after or before.
176             #
177             # Then we restate the 'before' requests in terms of 'after', dying for the
178             # moment if we can't find a module that a given module wants to run 'after'.
179             #
180             # After restating 'before' as 'after', we sort the modules in order of
181             # preference, then return the list in preference order.
182             #
183             sub topological_sort {
184 1     1 1 5 my @transformers = @_;
185 1         3 my @ordered;
186              
187             my %object;
188 1         4 for my $transformer ( @transformers ) {
189 37         66 my $ref_name = ref $transformer;
190 37         79 $ref_name =~ s< ^ Perl\::ToPerl6\::Transformer\:: ><>x;
191              
192 37         92 $object{$ref_name} = $transformer;
193             }
194              
195 1         6 my $preferences = _collect_preferences(@transformers);
196 1         4 _validate_preferences($preferences);
197              
198 1         6 _invert_dependencies($preferences);
199              
200             # This algorithm can potentially loop if it encounters a cycle in the
201             # dependencies.
202             #
203             # Specifically, the hash could look like this at the end:
204             # %foo = ( A => { after => { B => 1 } }, B => { after => { A => 1 } } );
205             # In which case this algorithm wouldn't terminate.
206             # So we count down from the number of keys in the original hash.
207             # This should give us plenty of time to detect cycles.
208             #
209             # The cycle could be arbitrarily long, and while there are fancy
210             # algorithms to detect those, I'm not going to bother.
211             #
212             # Keeping the module names in a stable order reduces the likelihood
213             # that a cycle will "trap" (I.E. come before) a module that's not
214             # involved in the cycle. It still could happen, but I'll worry about that
215             # later on.
216             #
217 1         3 my %final;
218 1         2 my $iterations = keys %{ $preferences };
  1         3  
219              
220 1         3 while( keys %{ $preferences } ) {
  3         9  
221 2 50       6 last if $iterations-- <= 0; # DO NOT REMOVE THIS.
222 2         3 for my $name ( sort keys %{ $preferences } ) {
  2         25  
223              
224             # If a module needs to run after one or more modules, try to
225             # satisfy its request.
226             #
227 43 100       80 if ( $preferences->{$name}{after} ) {
228              
229             # Walk the list of modules it needs to run after.
230             #
231 17         18 my $max = 0;
232 17         18 for my $_name ( keys %{ $preferences->{$name}{after} } ) {
  17         36  
233              
234             # If it needs to run after a module we haven't placed in
235             # order, then abandon the loop.
236             #
237 19 100       38 if ( !exists $final{$_name} ) {
238 6         7 $max = -1;
239 6         9 last;
240             }
241 13         28 $max = max($final{$_name},$max);
242             }
243              
244             # If we haven't abandoned the loop, then
245             # add the module *after* the last module in order
246             # and delete the module from the preferences list.
247             #
248 17 100       38 if ( $max >= 0 ) {
249 11         18 $final{$name} = $max + 1;
250 11         27 delete $preferences->{$name};
251             }
252             }
253              
254             # The module doesn't need to be run after any given module.
255             # So put it directly on the list, in group 0.
256             #
257             else {
258 26         31 $final{$name} = 0;
259 26         44 delete $preferences->{$name};
260             }
261             }
262             }
263              
264             # If there are any keys remaining in the preferences array, it's possible
265             # that the algorithm didn't sort dependencies correctly, but it is
266             # vastly more likely to be the case that we've encountered a cycle.
267             # Die, telling the user what happened.
268             #
269 1 50       3 if ( keys %{ $preferences } ) {
  1         4  
270 0         0 die "Found a preference loop among: " . join("\n", keys %{ $preferences });
  0         0  
271             }
272              
273 1         2 my %inverse;
274 1         8 push @{$inverse{$final{$_}}}, $_ for keys %final;
  37         85  
275 1         7 for ( sort keys %inverse ) {
276 3         4 push @ordered, map { $object{$_} } @{$inverse{$_}};
  37         57  
  3         5  
277             }
278              
279 1         21 return @ordered;
280             }
281              
282             #-----------------------------------------------------------------------------
283             # Some static helper subs
284              
285             sub _modules_from_blib {
286 1     1   15 my (@modules) = @_;
287 1         3 return grep { _was_loaded_from_blib( _module2path($_) ) } @modules;
  37         66  
288             }
289              
290             sub _module2path {
291 37   50 37   62 my $module = shift || return;
292 37         208 return File::Spec::Unix->catdir(split m/::/xms, $module) . '.pm';
293             }
294              
295             sub _was_loaded_from_blib {
296 37   50 37   70 my $path = shift || return;
297 37         98 my $full_path = $INC{$path};
298 37   33     212 return $full_path && $full_path =~ m/ (?: \A | \b b ) lib \b /xms;
299             }
300              
301             #-----------------------------------------------------------------------------
302              
303             sub new {
304              
305 4     4 1 21 my ( $class, %args ) = @_;
306 4         10 my $self = bless {}, $class;
307 4         15 $self->_init( %args );
308 4         14 return $self;
309             }
310              
311             #-----------------------------------------------------------------------------
312              
313             sub _init {
314              
315 4     4   12 my ($self, %args) = @_;
316              
317 4         7 my $profile = $args{-profile};
318 4 50       18 $self->{_profile} = $profile
319             or throw_internal q{The -profile argument is required};
320              
321 4         7 my $incoming_errors = $args{-errors};
322 4         8 my $profile_strictness = $args{'-profile-strictness'};
323 4   66     12 $profile_strictness ||= $PROFILE_STRICTNESS_DEFAULT;
324 4         9 $self->{_profile_strictness} = $profile_strictness;
325              
326 4 50       13 if ( $profile_strictness ne $PROFILE_STRICTNESS_QUIET ) {
327 4         5 my $errors;
328              
329             # If we're supposed to be strict or problems have already been found...
330 4 50 66     28 if (
      33        
331             $profile_strictness eq $PROFILE_STRICTNESS_FATAL
332 3         101 or ( $incoming_errors and @{ $incoming_errors->exceptions() } )
333             ) {
334 0 0       0 $errors =
335             $incoming_errors
336             ? $incoming_errors
337             : Perl::ToPerl6::Exception::AggregateConfiguration->new();
338             }
339              
340 4         33 $self->_validate_transformers_in_profile( $errors );
341              
342 4 50 66     18 if (
      33        
343             not $incoming_errors
344             and $errors
345             and $errors->has_exceptions()
346             ) {
347 0         0 $errors->rethrow();
348             }
349             }
350              
351 4         19 return $self;
352             }
353              
354             #-----------------------------------------------------------------------------
355              
356             sub create_transformer {
357              
358 37     37 1 75 my ($self, %args ) = @_;
359              
360             my $transformer_name = $args{-name}
361 37 50       96 or throw_internal q{The -name argument is required};
362              
363             # Normalize transformer name to a fully-qualified package name
364 37         89 $transformer_name = transformer_long_name( $transformer_name );
365 37         76 my $transformer_short_name = transformer_short_name( $transformer_name );
366              
367              
368             # Get the transformer parameters from the user profile if they were
369             # not given to us directly. If none exist, use an empty hash.
370 37         84 my $profile = $self->_profile();
371 37         44 my $transformer_config;
372 37 50       74 if ( $args{-params} ) {
373             $transformer_config =
374             Perl::ToPerl6::TransformerConfig->new(
375             $transformer_short_name, $args{-params}
376 0         0 );
377             }
378             else {
379 37         103 $transformer_config = $profile->transformer_params($transformer_name);
380 37   33     107 $transformer_config ||=
381             Perl::ToPerl6::TransformerConfig->new( $transformer_short_name );
382             }
383              
384             # Pull out base parameters.
385 37         74 return $self->_instantiate_transformer( $transformer_name, $transformer_config );
386             }
387              
388             #-----------------------------------------------------------------------------
389              
390             sub create_all_transformers {
391              
392 1     1 1 2 my ( $self, $incoming_errors ) = @_;
393              
394 1 50       4 my $errors =
395             $incoming_errors
396             ? $incoming_errors
397             : Perl::ToPerl6::Exception::AggregateConfiguration->new();
398 1         6 my @transformers;
399              
400 1         4 foreach my $name ( site_transformer_names() ) {
401 37         53 my $transformer = eval { $self->create_transformer( -name => $name ) };
  37         108  
402              
403 37         109 $errors->add_exception_or_rethrow( $EVAL_ERROR );
404              
405 37 50       93 if ( $transformer ) {
406 37         846 push @transformers, $transformer;
407             }
408             }
409              
410 1 50 33     8 if ( not $incoming_errors and $errors->has_exceptions() ) {
411 0         0 $errors->rethrow();
412             }
413              
414 1         12 my @sorted = topological_sort(@transformers);
415              
416 1         13 return @sorted;
417             }
418              
419             #-----------------------------------------------------------------------------
420              
421             sub site_transformer_names {
422 5     5 1 75 my @sorted_transformer_names = sort @site_transformer_names;
423 5         37 return @sorted_transformer_names;
424             }
425              
426             #-----------------------------------------------------------------------------
427              
428             sub _profile {
429 41     41   50 my ($self) = @_;
430              
431 41         71 return $self->{_profile};
432             }
433              
434             #-----------------------------------------------------------------------------
435              
436             # This two-phase initialization is caused by the historical lack of a
437             # requirement for Transformers to invoke their super-constructor.
438             sub _instantiate_transformer {
439 37     37   49 my ($self, $transformer_name, $transformer_config) = @_;
440              
441 37         106 $transformer_config->set_profile_strictness( $self->{_profile_strictness} );
442              
443 37         43 my $transformer = eval { $transformer_name->new( %{$transformer_config} ) };
  37         43  
  37         501  
444 37         84 _handle_transformer_instantiation_exception(
445             $transformer_name,
446             $transformer, # Note: being used as a boolean here.
447             $EVAL_ERROR,
448             );
449              
450 37         99 $transformer->__set_config( $transformer_config );
451              
452 37         46 my $eval_worked = eval { $transformer->__set_base_parameters(); 1; };
  37         193  
  37         57  
453 37         71 _handle_transformer_instantiation_exception(
454             $transformer_name, $eval_worked, $EVAL_ERROR,
455             );
456              
457 37         103 return $transformer;
458             }
459              
460             sub _handle_transformer_instantiation_exception {
461 74     74   120 my ($transformer_name, $eval_worked, $eval_error) = @_;
462              
463 74 50       172 if (not $eval_worked) {
464 0 0       0 if ($eval_error) {
465 0         0 my $exception = Exception::Class->caught();
466              
467 0 0       0 if (ref $exception) {
468 0         0 $exception->rethrow();
469             }
470              
471             throw_transformer_definition(
472 0         0 qq<Unable to create transformer "$transformer_name": $eval_error>);
473             }
474              
475             throw_transformer_definition(
476 0         0 qq<Unable to create transformer "$transformer_name" for an unknown reason.>);
477             }
478              
479 74         853 return;
480             }
481              
482             #-----------------------------------------------------------------------------
483              
484             sub _validate_transformers_in_profile {
485 4     4   6 my ($self, $errors) = @_;
486              
487 4         14 my $profile = $self->_profile();
488 4         14 my %known_transformers = hashify( $self->site_transformer_names() );
489              
490 4         30 for my $transformer_name ( $profile->listed_transformers() ) {
491 0 0       0 if ( not exists $known_transformers{$transformer_name} ) {
492 0         0 my $message = qq{Transformer "$transformer_name" is not installed.};
493              
494 0 0       0 if ( $errors ) {
495 0         0 $errors->add_exception(
496             Perl::ToPerl6::Exception::Configuration::NonExistentTransformer->new(
497             transformer => $transformer_name,
498             )
499             );
500             }
501             else {
502 0         0 warn qq{$message\n};
503             }
504             }
505             }
506              
507 4         16 return;
508             }
509              
510             #-----------------------------------------------------------------------------
511              
512             1;
513              
514             __END__
515              
516              
517             =pod
518              
519             =for stopwords TransformerFactory -params
520              
521             =head1 NAME
522              
523             Perl::ToPerl6::TransformerFactory - Instantiates Transformer objects.
524              
525              
526             =head1 DESCRIPTION
527              
528             This is a helper class that instantiates
529             L<Perl::ToPerl6::Transformer|Perl::ToPerl6::Transformer> objects with the user's
530             preferred parameters. There are no user-serviceable parts here.
531              
532              
533             =head1 INTERFACE SUPPORT
534              
535             This is considered to be a non-public class. Its interface is subject
536             to change without notice.
537              
538              
539             =head1 CONSTRUCTOR
540              
541             =over
542              
543             =item C<< new( -profile => $profile, -errors => $config_errors ) >>
544              
545             Returns a reference to a new Perl::ToPerl6::TransformerFactory object.
546              
547             B<-profile> is a reference to a
548             L<Perl::ToPerl6::UserProfile|Perl::ToPerl6::UserProfile> object. This
549             argument is required.
550              
551             B<-errors> is a reference to an instance of
552             L<Perl::ToPerl6::ConfigErrors|Perl::ToPerl6::ConfigErrors>. This
553             argument is optional. If specified, than any problems found will be
554             added to the object.
555              
556              
557             =back
558              
559              
560             =head1 METHODS
561              
562             =over
563              
564             =item C<< create_transformer( -name => $transformer_name, -params => \%param_hash ) >>
565              
566             Creates one Transformer object. If the object cannot be instantiated, it
567             will throw a fatal exception. Otherwise, it returns a reference to
568             the new Transformer object.
569              
570             B<-name> is the name of a L<Perl::ToPerl6::Transformer|Perl::ToPerl6::Transformer>
571             subclass module. The C<'Perl::ToPerl6::Transformer'> portion of the name
572             can be omitted for brevity. This argument is required.
573              
574             B<-params> is an optional reference to hash of parameters that will be
575             passed into the constructor of the Transformer. If C<-params> is not
576             defined, we will use the appropriate Transformer parameters from the
577             L<Perl::ToPerl6::UserProfile|Perl::ToPerl6::UserProfile>.
578              
579             Note that the Transformer will not have had
580             L<Perl::ToPerl6::Transformer/"initialize_if_enabled"> invoked on it, so it
581             may not yet be usable.
582              
583              
584             =item C< create_all_transformers() >
585              
586             Constructs and returns one instance of each
587             L<Perl::ToPerl6::Transformer|Perl::ToPerl6::Transformer> subclass that is
588             installed on the local system. Each Transformer will be created with the
589             appropriate parameters from the user's configuration profile.
590              
591             Note that the Transformers will not have had
592             L<Perl::ToPerl6::Transformer/"initialize_if_enabled"> invoked on them, so
593             they may not yet be usable.
594              
595              
596             =back
597              
598              
599             =head1 SUBROUTINES
600              
601             Perl::ToPerl6::TransformerFactory has a few static subroutines that are used
602             internally, but may be useful to you in some way.
603              
604             =over
605              
606             =item C<topological_sort( @transformers )>
607              
608             Given a list of Transformer objects, reorder them into the order they need to
609             be run. Variables::FormatSpecialVariables needs to reformat $0 before
610             Variables::FormatMatchVariables transforms $1 into $0, for example. If you need
611             to specify that a Transformer must be run before or after a given transformer
612             or list of transformers, then in your Transformer create a C<sub run_before()>
613             and/or C<sub run_after()> which returns a list of transformers that it must
614             run before and/or after.
615              
616             If a transformer you specified doesn't exist, your transformer code should
617             still run, but with a warning.
618              
619             =item C<site_transformer_names()>
620              
621             Returns a list of all the Transformer modules that are currently installed
622             in the Perl::ToPerl6:Transformer namespace. These will include modules that
623             are distributed with Perl::ToPerl6 plus any third-party modules that
624             have been installed.
625              
626              
627             =back
628              
629              
630             =head1 AUTHOR
631              
632             Jeffrey Goff <drforr@pobox.com>
633              
634              
635             =head1 AUTHOR EMERITUS
636              
637             Jeffrey Ryan Thalhammer <jeff@imaginative-software.com>
638              
639              
640             =head1 COPYRIGHT
641              
642             Copyright (c) 2015 Jeffrey Goff. All rights reserved.
643              
644             This program is free software; you can redistribute it and/or modify
645             it under the same terms as Perl itself. The full text of this license
646             can be found in the LICENSE file included with this module.
647              
648             =cut
649              
650             # Local Variables:
651             # mode: cperl
652             # cperl-indent-level: 4
653             # fill-column: 78
654             # indent-tabs-mode: nil
655             # c-indentation-style: bsd
656             # End:
657             # ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :