File Coverage

blib/lib/Sub/Genius.pm
Criterion Covered Total %
statement 133 148 89.8
branch 44 58 75.8
condition 9 19 47.3
subroutine 25 27 92.5
pod 1 17 5.8
total 212 269 78.8


line stmt bran cond sub pod time code
1             package Sub::Genius;
2              
3 9     9   2325431 use strict;
  9         13  
  9         297  
4 9     9   35 use warnings;
  9         11  
  9         503  
5 9     9   4134 use FLAT::PFA;
  9         1391175  
  9         374  
6 9     9   89 use FLAT::Regex::WithExtraOps;
  9         16  
  9         204  
7 9     9   48 use Digest::MD5 ();
  9         19  
  9         123  
8 9     9   35 use Storable ();
  9         16  
  9         100  
9 9     9   36 use Cwd ();
  9         28  
  9         18006  
10              
11             our $VERSION = q{0.314006};
12              
13             sub new {
14 22     22 0 8423 my $pkg = shift;
15 22         133 my %self = @_;
16 22         52 my $self = \%self;
17 22         62 bless $self, $pkg;
18 22 50       134 die qq{'preplan' parameter required!\n} if not defined $self->{preplan};
19              
20             # set to undef to disable preprocessing
21 22 100       137 if ( not exists $self->{preprocess} ) {
22 16         49 $self->{preprocess} = 1;
23             }
24              
25             # set to undef to disable caching
26 22 100       97 if ( not exists $self->{cachedir} ) {
27 19         112136 $self->cachedir( sprintf( qq{%s/%s}, Cwd::cwd(), q{_Sub::Genius} ) );
28             }
29              
30             # keep a historical record
31 22         186 $self->original_preplan($self->preplan);
32              
33             # 'pre-process' plan - this step maximizes the chance of capturing
34             # the same checksum for identical PREs that may just be formatted differently
35 22 100       113 if ( $self->{preprocess} ) {
36 16         109 $self->_trim;
37 16         89 $self->_balkanize;
38 16         76 $self->_normalize;
39             }
40              
41             # generates checksum based on post-preprocessed form
42 22         90 $self->checksum( Digest::MD5::md5_hex( $self->preplan ) );
43              
44 22         75 $self->pregex( FLAT::Regex::WithExtraOps->new( $self->preplan ) );
45 22         860 return $self;
46             }
47              
48             sub cachefile {
49 68     68 0 133 my $self = shift;
50 68 100       165 return ( $self->cachedir ) ? sprintf( qq{%s/%s}, $self->cachedir, $self->checksum ) : undef;
51             }
52              
53             sub cachedir {
54 180     180 0 506 my ( $self, $dir ) = @_;
55 180 100       514 if ($dir) {
56 19         242 $self->{cachedir} = $dir;
57 19 100       723 if ( not -d $self->{cachedir} ) {
58 1         112 mkdir $self->{cachedir}, 0700 || die $!;
59             }
60             }
61 180         619 return $self->{cachedir};
62             }
63              
64             sub checksum {
65 115     115 0 254 my ( $self, $sum ) = @_;
66 115 100       251 if ($sum) {
67 22         81 $self->{checksum} = $sum;
68             }
69 115         2354 return $self->{checksum};
70             }
71              
72             sub do_cache {
73 25     25 0 71 my $self = shift;
74 25   66     103 return ( $self->cachedir and $self->checksum and $self->cachefile );
75             }
76              
77             # strips comments and empty lines
78             sub _trim {
79 16     16   40 my $self = shift;
80 16         82 my $_pre = q{};
81 16         33 my @pre = ();
82             STRIP:
83 16         147 foreach my $line ( split /\n/, $self->{preplan} ) {
84 52 100       421 next STRIP if ( $line =~ m/^\s*#|^\s*$/ );
85 45         183 my @line = split /\s*#/, $line;
86 45         141 push @pre, $line[0];
87             }
88 16         101 $self->preplan( join qq{\n}, @pre );
89 16         47 return $self->preplan;
90             }
91              
92             sub _balkanize {
93 16     16   32 my $self = shift;
94 16 50       70 if ( $self->{preplan} =~ m/[#\[\]]+/ ) {
95 0         0 die qq{plan to be bracketized must not contain '#', '[', or ']'};
96             }
97 16         58 my $_pre = q{};
98 16         32 my @pre = ();
99             STRIP:
100 16         104 foreach my $line ( split /\n/, $self->{preplan} ) {
101              
102             # supports strings with namespace delim, '::'
103 45         447 $line =~ s/([a-zA-Z:_\d]+)/\[$1\]/g;
104 45         104 push @pre, $line;
105             }
106 16         103 $self->preplan( join qq{\n}, @pre );
107 16         54 return $self->preplan;
108             }
109              
110             # currently, removes all spaces and newlines
111             sub _normalize {
112 16     16   59 my $self = shift;
113 16         67 my @pre = split /\n/, $self->{preplan};
114 16         53 my $minified = join qq{}, @pre;
115 16         85 $minified =~ s/[\s]+//g;
116 16         48 $self->preplan($minified);
117 16         42 return $self->preplan;
118             }
119              
120             # accessor for original plan
121             sub original_preplan {
122 22     22 0 91 my ( $self, $pp ) = @_;
123 22 50       137 if ($pp) {
124 22         107 $self->{original_preplan} = $pp;
125             }
126 22         61 return $self->{original_preplan};
127             }
128              
129             # accessor for original plan
130             sub preplan {
131 174     174 0 4263 my ( $self, $pp ) = @_;
132 174 100       363 if ($pp) {
133 48         125 $self->{preplan} = $pp;
134             }
135 174         1418 return $self->{preplan};
136             }
137              
138             # accessor for original
139             sub pregex {
140 47     47 0 2271091 my ( $self, $pregex ) = @_;
141 47 100       901 if ($pregex) {
142 22         3844 $self->{pregex} = $pregex;
143             }
144 47         293 return $self->{pregex};
145             }
146              
147             # set/updated whenever ->next() and friends are called, simple way to
148             # query what plan was last created; RO, not destructive on current 'plan'
149             sub plan {
150 104     104 1 167 my $self = shift;
151 104         303 return $self->{plan};
152             }
153              
154             # setter/getter for DFA
155             sub dfa {
156 163     163 0 7757692 my ( $self, $dfa ) = @_;
157 163 100       474 if ($dfa) {
158 16         183 $self->{DFA} = $dfa;
159             }
160 163         1232 return $self->{DFA};
161             }
162              
163             # Converts plan -> PFA -> NFA -> DFA:
164             # NOTE: plan is not generated here, much call ->next()
165             # can pass param to underlying ->dfa also, like 'reset => 1'
166             sub init_plan {
167 16     16 0 1411 my ( $self, %opts ) = @_;
168              
169             # requires plan (duh)
170 16 50       103 die qq{Need to call 'new' with 'preplan => q{PRE...}' to initialize\n} if not $self->pregex;
171              
172             # convert PRE to DFA
173 16         2204 $self->convert_pregex_to_dfa(%opts);
174              
175             # warn if DFA is not acyclic (infinite strings accepted)
176 16 50       57 if ( $self->dfa->is_infinite ) {
177 0 0       0 if ( not $self->{'allow-infinite'} ) {
178 0         0 warn qq{(fatal) Infinite language detected. To avoid, do not use Kleene Star (*).\n};
179 0         0 die qq{ pass in 'allow-infinite => 1' to constructor to disable this warning.\n};
180             }
181             }
182              
183             # else - currently no meaningful way to control 'infinite' languages, this needs to
184             # be investigated
185              
186             # returns $self, for chaining in __PACKAGE__->run_any
187 16         19422 return $self;
188             }
189              
190             # to force a reset, pass in, C 1>.; this makes a lot of cool things
191             sub convert_pregex_to_dfa {
192 16     16 0 83 my ( $self, %opts ) = @_;
193              
194             # look for cached DFA
195 16 100 66     161 if ( not $self->{reset} and $self->do_cache ) {
196 15 100       39 if ( -e $self->cachefile ) {
197 7         40 $self->dfa( Storable::retrieve( $self->cachefile ) );
198 7         53 return $self->dfa;
199             }
200             }
201              
202 9 50 33     53 if ( not $self->dfa or defined $opts{reset} ) {
203 9         39 $self->dfa( $self->pregex->as_pfa->as_nfa->as_dfa->as_min_dfa->trim_sinks );
204              
205             # save to cache
206 9 100       1504 if ( $self->do_cache ) {
207 8         28 Storable::store( $self->dfa, $self->cachefile );
208             }
209             }
210 9         5163 return $self->dfa;
211             }
212              
213             # Acyclic String Iterator
214             # force a reset, pass in, C 1>.
215             sub next {
216 77     77 0 75263 my ( $self, %opts ) = @_;
217              
218 77 50       266 die qq{(fatal) Use 'inext' instead of 'next' for infinite languages.\n} if $self->dfa->is_infinite;
219              
220 77 100 100     105829 if ( not defined $self->{_acyclical_iterator} or $opts{reset} ) {
221 9         55 $self->{_acyclical_iterator} = $self->dfa->init_acyclic_iterator(q{ });
222             }
223              
224 77         51210 $self->{plan} = $self->{_acyclical_iterator}->();
225              
226 77         27633 return $self->{plan};
227             }
228              
229             # accepts the same parameters as a constructor, used to re-initialize
230             # the current reference
231             sub plan_nein {
232 0     0 0 0 my $pkg = shift;
233 0         0 my $self = __PACKAGE__->new(@_);
234              
235             # also returns $self for convenience
236 0         0 return $self;
237             }
238              
239             # wrapper that combines C and C to present an idiom,
240             # my $final_scope = Sub::Genius->new($plan)->run_any( scope => { ... });
241             sub run_any {
242 3     3 0 1374 my ( $self, %opts ) = @_;
243 3         82 $self->init_plan;
244 3         50 my $final_scope = $self->run_once(%opts);
245 3         23 return $final_scope;
246             }
247              
248             # Runs any single serialization ONCE
249             # defaults to main::, specify namespace of $sub
250             #
251             # * ns => q{Some::NS} # specify name space
252             # * scope => { } # specify initial state of pipeline accumulator
253             # * verbose => 0|1 # output runtime diagnostics
254             sub run_once {
255 52     52 0 546 my ( $self, %opts ) = @_;
256              
257             # initialize scope
258 52   50     179 $opts{scope} //= {};
259              
260             # appends '::' (no check if '::' is at the end to encourage a standard idiom)
261 52 100       142 if ( not defined $opts{ns} ) {
262 51         110 $opts{ns} = q{main::};
263             }
264             else {
265 1         6 $opts{ns} .= q{::};
266             }
267              
268             # only call interator if $self->{plan} has not yet been set
269 52 100       166 $self->next if not $self->plan;
270              
271             # check plan is set, just to be sure
272 52 50       120 if ( my $plan = $self->plan ) {
273 52 50       136 if ( $opts{verbose} ) {
274 0         0 print qq{plan: "$plan" <<<\n\nExecute:\n\n};
275             }
276 52         220 my @seq = split( / /, $plan );
277              
278             # main run loop - run once
279 52         90 local $@;
280 52         138 foreach my $sub (@seq) {
281 311         15677 eval sprintf( qq{%s%s(\$opts{scope});}, $opts{ns}, $sub );
282 311 50       6593 die $@ if $@; # be nice and die for easier debuggering
283             }
284             }
285 52         170 return $opts{scope};
286             }
287              
288             #
289             # D R A G O N S
290             # ~~~> *E X P E R I M E N T A L* (not even in POD yet)
291             #
292              
293             # Deep (Infinite) String Iterator
294             # force a reset, pass in, C 1>.
295             #
296             # To us:
297             # my $sg = Sub::Genius=->new(preplan => q{a&b*c}, => 'allow-infinite' => 1);
298             # $sg->init_plan;
299             #
300             #
301             sub inext {
302 0     0 0   my ( $self, %opts ) = @_;
303 0           local $| = 1;
304 0   0       $opts{max} //= 5;
305 0 0 0       if ( not defined $self->{_deepdft_iterator} or $opts{reset} ) {
306 0           $self->{_deepdft_iterator} = $self->dfa->init_deepdft_iterator( $opts{max}, q{ } );
307             }
308              
309 0           $self->{plan} = $self->{_deepdft_iterator}->();
310              
311 0           return $self->{plan};
312             }
313              
314             1;
315              
316             __END__