File Coverage

blib/lib/Perl/ToPerl6/TransformerFactory.pm
Criterion Covered Total %
statement 225 240 93.7
branch 51 66 77.2
condition 22 37 59.4
subroutine 35 35 100.0
pod 5 5 100.0
total 338 383 88.2


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