File Coverage

blib/lib/YASF.pm
Criterion Covered Total %
statement 151 152 99.3
branch 76 78 97.4
condition 6 6 100.0
subroutine 28 28 100.0
pod 4 4 100.0
total 265 268 98.8


line stmt bran cond sub pod time code
1             ###############################################################################
2             #
3             # This file copyright © 2016 by Randy J. Ray, all rights reserved
4             #
5             # See "LICENSE AND COPYRIGHT" in the POD for terms.
6             #
7             ###############################################################################
8             #
9             # Description: A string-formatter inspired by Python's format()
10             #
11             # Functions: YASF
12             # new
13             # bind
14             # format
15             #
16             # Libraries: None (only core)
17             #
18             # Global Consts: @EXPORT_OK
19             # %NOT_ACCEPTABLE_REF
20             #
21             # Environment: None
22             #
23             ###############################################################################
24              
25             package YASF;
26              
27 7     7   379807 use 5.008;
  7         22  
28 7     7   35 use strict;
  7         13  
  7         154  
29 7     7   39 use warnings;
  7         12  
  7         642  
30 7         82 use overload fallback => 0,
31             'eq' => \&_eq,
32             'ne' => \&_ne,
33             'lt' => \&_lt,
34             'le' => \&_le,
35             'gt' => \&_gt,
36             'ge' => \&_ge,
37             'cmp' => \&_cmp,
38             q{.} => \&_dot,
39             q{.=} => \&_dotequal,
40             q{""} => \&_stringify,
41 7     7   4137 q{%} => \&_interpolate;
  7         4030  
42              
43 7     7   1106 use Carp qw(carp croak);
  7         8  
  7         321  
44 7     7   34 use Exporter qw(import);
  7         11  
  7         249  
45              
46             our $VERSION = '0.005'; # VERSION
47              
48             BEGIN {
49 7     7   34 no strict 'refs'; ## no critic (ProhibitNoStrict)
  7         9  
  7         389  
50              
51 7     7   24 for my $method (qw(template bindings on_undef)) {
52 21     94   10096 *{$method} = sub { shift->{$method} }
  94         1245  
53 21         62 }
54             }
55              
56             my %NOT_ACCEPTABLE_REF = (
57             SCALAR => 1,
58             CODE => 1,
59             REF => 1,
60             GLOB => 1,
61             LVALUE => 1,
62             FORMAT => 1,
63             IO => 1,
64             VSTRING => 1,
65             Regexp => 1,
66             );
67              
68             my %VALID_ON_UNDEF = (
69             die => 1,
70             warn => 1,
71             token => 1,
72             ignore => 1,
73             );
74              
75             our @EXPORT_OK = qw(YASF);
76              
77             ###############################################################################
78             #
79             # Sub Name: YASF
80             #
81             # Description: Shortcut to calling YASF->new($str) with no other args.
82             #
83             # Arguments: NAME IN/OUT TYPE DESCRIPTION
84             # $template in scalar String template for formatter
85             #
86             # Returns: Success: new object
87             # Failure: dies
88             #
89             ###############################################################################
90             ## no critic(ProhibitSubroutinePrototypes)
91 2     2 1 1189 sub YASF ($) { return YASF->new(shift); }
92              
93             ###############################################################################
94             #
95             # Sub Name: new
96             #
97             # Description: Class constructor. Creates the basic object and
98             # pre-compiles the template into the form that the formatter
99             # uses.
100             #
101             # Arguments: NAME IN/OUT TYPE DESCRIPTION
102             # $class in scalar Name of class
103             # $template in scalar String template for formatter
104             # @args in scalar Everything else (see code)
105             #
106             # Returns: Success: new object
107             # Failure: dies
108             #
109             ###############################################################################
110             sub new {
111 31     31 1 9631 my ($class, $template, @args) = @_;
112              
113 31 100       216 croak 'new requires string template argument' if (! defined $template);
114              
115 30 100       95 my $args = @args == 1 ? $args[0] : { @args };
116 30         121 my $self = bless {
117             template => $template,
118             bindings => undef,
119             on_undef => 'warn',
120             }, $class;
121              
122 30         90 $self->_compile;
123 27 100       63 if ($args->{bindings}) {
124 14         38 $self->bind($args->{bindings});
125             }
126 27 100       64 if ($args->{on_undef}) {
127             croak "new: Invalid value for 'on_undef' ($args->{on_undef})"
128 5 50       12 if (! $VALID_ON_UNDEF{$args->{on_undef}});
129 5         6 $self->{on_undef} = $args->{on_undef};
130             }
131              
132 27         112 return $self;
133             }
134              
135             ###############################################################################
136             #
137             # Sub Name: bind
138             #
139             # Description: Add or change object-level bindings
140             #
141             # Arguments: NAME IN/OUT TYPE DESCRIPTION
142             # $self in ref Object of this class
143             # $bindings in ref New bindings
144             #
145             # Globals: %NOT_ACCEPTABLE_REF
146             #
147             # Returns: Success: $self
148             # Failure: dies
149             #
150             ###############################################################################
151             sub bind { ## no critic(ProhibitBuiltinHomonyms)
152 24     24 1 1665 my ($self, $bindings) = @_;
153              
154 24 100 100     107 if ((@_ == 2) && (! defined $bindings)) {
155             # The means of unbinding is to call $obj->bind(undef):
156 1         3 undef $self->{bindings};
157             } else {
158 23 100       128 croak 'bind: New bindings must be provided as a parameter'
159             if (! $bindings);
160              
161 22         46 my $type = ref $bindings;
162 22 100       64 if (! $type) {
    100          
163 1         101 croak 'New bindings must be a reference (HASH, ARRAY or object)';
164             } elsif ($NOT_ACCEPTABLE_REF{$type}) {
165 1         61 croak "New bindings reference type ($type) not usable";
166             }
167              
168 20         41 $self->{bindings} = $bindings;
169             }
170              
171 21         34 return $self;
172             }
173              
174             ###############################################################################
175             #
176             # Sub Name: format
177             #
178             # Description: Front-end to the recursive _format routine, which does the
179             # bulk of the parsing/interpolation of the object's template
180             # against the given bindings.
181             #
182             # Arguments: NAME IN/OUT TYPE DESCRIPTION
183             # $self in ref Object of this class
184             # $bindings in ref Optional, bindings to use in
185             # interpolation. Defaults to
186             # object-level bindings.
187             #
188             # Returns: Success: string
189             # Failure: dies
190             #
191             ###############################################################################
192             sub format { ## no critic(ProhibitBuiltinHomonyms)
193 66     66 1 1125 my ($self, $bindings) = @_;
194              
195 66   100     170 $bindings ||= $self->bindings;
196 66 100       198 croak 'format: Bindings are required if object has no internal bindings'
197             if (! $bindings);
198              
199             my $value = join q{} =>
200 365 100       725 map { ref() ? $self->_format($bindings, @{$_}) : $_ }
  248         582  
201 65         116 @{$self->{_compiled}};
  65         166  
202              
203 61         446 return $value;
204             }
205              
206             # Private functions that support the public API:
207              
208             # Compile the template into a tree structure that can be easily traversed for
209             # formatting.
210             sub _compile {
211 30     30   45 my $self = shift;
212              
213 30         69 my @tokens = $self->_tokenize;
214              
215 30         68 my @stack = ([]);
216 30         43 my $level = 0;
217 30         40 my @opens = ();
218              
219 30         105 while (my ($type, $value) = splice @tokens, 0, 2) {
220 441 100       779 if ($type eq 'STRING') {
    100          
221 113         139 push @{$stack[$level]}, $value;
  113         358  
222             } elsif ($type eq 'OPEN') {
223 165         219 push @stack, [];
224 165         207 $level++;
225 165         375 push @opens, $value;
226             } else {
227             # Must be 'CLOSE'.
228 163 100       235 if ($level) {
229 162         262 my $subtree = pop @stack;
230 162         184 $level--;
231 162         164 push @{$stack[$level]}, $subtree;
  162         245  
232 162         359 pop @opens;
233             } else {
234 1         111 croak "Unmatched closing brace at position $value";
235             }
236             }
237             }
238              
239 29 100       71 if ($level) {
240 2 100       168 croak sprintf '%d unmatched opening brace%s, last at position %d',
241             $level, $level == 1 ? q{} : 's', pop @opens;
242             }
243              
244 27         54 $self->{_compiled} = $stack[0];
245 27         52 return $self;
246             }
247              
248             # Tokenize the object's template into a sequence of (type, value) pairs that
249             # identify the opening and closing braces, and ordinary strings.
250             sub _tokenize {
251 30     30   47 my $self = shift;
252              
253 30         49 my (@list, $base, $pos, $len);
254 30         58 my $str = $self->template;
255              
256 30         54 $base = 0;
257 30         152 while ($str =~ /(?
258 328         405 $pos = pos $str;
259 328 100       483 if ($len = $pos - $base) {
260 111         218 (my $piece = substr $str, $base, $len) =~ s/\\([{}])/$1/g;
261 111         206 push @list, 'STRING', $piece;
262             }
263 328 100       607 push @list, ('{' eq substr $str, $pos, 1) ? 'OPEN' : 'CLOSE';
264 328         422 push @list, $pos;
265 328         870 $base = $pos + 1;
266             }
267              
268 30 100       79 if (length($str) > $base) {
269 2         6 (my $piece = substr $str, $base) =~ s/\\([{}])/$1/g;
270 2         5 push @list, 'STRING', $piece;
271             }
272              
273 30         179 return @list;
274             }
275              
276             # Does the hard and recursive part of the actual formatting. Not actually that
277             # hard, but a little recursive.
278             sub _format {
279 255     255   571 my ($self, $bindings, @elements) = @_;
280              
281             # Slight duplication of code from format() here, but it saves having to
282             # keep track of depth and do a conditional on every return.
283             my $expr = join q{} =>
284 255 100       425 map { ref() ? $self->_format($bindings, @{$_}) : $_ } @elements;
  262         757  
  7         14  
285              
286 255         587 return $self->_expr_to_value($bindings, $expr);
287             }
288              
289             # Converts an expression like "a.b.c" into a value from the bindings
290             sub _expr_to_value {
291 255     255   468 my ($self, $bindings, $string) = @_;
292              
293 255         645 my ($expr, $format) = split /:/ => $string, 2;
294             # For now, $format is ignored
295 255         543 my @hier = split /[.]/ => $expr;
296 255         363 my $node = $bindings;
297              
298 255         426 for my $key (@hier) {
299 258 100       623 if ($key =~ /^\d+$/) {
300 19 100       34 if (ref $node eq 'ARRAY') {
301 18         32 $node = $node->[$key];
302             } else {
303 1         70 croak "Key-type mismatch (key $key) in $expr, node is not " .
304             'an ARRAY ref';
305             }
306             } else {
307 239 100       533 if (ref $node eq 'HASH') {
    100          
    100          
308 159         402 $node = $node->{$key};
309             } elsif (ref $node eq 'ARRAY') {
310 1         69 croak "Key-type mismatch (key $key) in $expr, node is an " .
311             'ARRAY ref when expecting HASH or object';
312             } elsif (ref $node) {
313 78         1071 $node = $node->$key();
314             } else {
315 1         67 croak "Key-type mismatch (key $key) in $expr, node is not " .
316             'a HASH ref or object';
317             }
318             }
319             }
320              
321             # Because all the key-substitution has been done before this sub is called,
322             # it's probably a bad thing if $node is a ref. It's gonna get stringified
323             # as a ref, which is probably not what the caller intended.
324 252 100       835 if (ref $node) {
325 1         73 carp "Format expression $expr yielded a reference value rather than " .
326             'a scalar';
327             }
328             # If $node is undef, react accordingly based on the "on_undef" property.
329 252 100       487 if (! defined $node) {
330             # We validated this value in new(), so no need to do so here.
331 6         8 my $what_to_do = $self->on_undef;
332 6 100       20 if ($what_to_do eq 'ignore') {
    100          
    100          
333 1         2 $node = q{};
334             } elsif ($what_to_do eq 'warn') {
335 2         260 carp "No binding for reference to '$expr'";
336 2         81 $node = q{};
337             } elsif ($what_to_do eq 'die') {
338 1         77 croak "No binding for reference to '$expr'";
339             } else {
340             # Leave the token unchanged
341 2         5 $node = "{$expr}";
342             }
343             }
344              
345 251         799 return $node;
346             }
347              
348             # Actual operator-overload functions:
349              
350             # Handle the object stringification (the "" operator)
351             sub _stringify {
352 44     44   111 my $self = shift;
353 44         116 my $bindings = $self->bindings;
354              
355 44 100       177 return $bindings ? $self->format($bindings) : $self->template;
356             }
357              
358             # Handle the % interpolation operator
359             sub _interpolate {
360 10     10   622 my ($self, $bindings, $swap) = @_;
361              
362 10 100       25 if ($swap) {
363 1         2 my $class = ref $self;
364 1         60 croak "$class object must come first in % interpolation";
365             }
366              
367 9         24 return $self->format($bindings);
368             }
369              
370             # Handle the 'cmp' operator
371             sub _cmp {
372 2     2   526 my ($self, $other, $swap) = @_;
373              
374 2 100       9 return $swap ?
375             ($other cmp $self->_stringify) : ($self->_stringify cmp $other);
376             }
377              
378             # Handle the 'eq' operator
379             sub _eq {
380 3     3   523 my ($self, $other, $swap) = @_;
381              
382 3         10 return $self->_stringify eq $other;
383             }
384              
385             # Handle the 'ne' operator
386             sub _ne {
387 4     4   23 my ($self, $other, $swap) = @_;
388              
389 4         15 return $self->_stringify ne $other;
390             }
391              
392             # Handle the 'lt' operator
393             sub _lt {
394 4     4   17 my ($self, $other, $swap) = @_;
395              
396 4 100       31 return $swap ?
397             ($other lt $self->_stringify) : ($self->_stringify lt $other);
398             }
399              
400             # Handle the 'le' operator
401             sub _le {
402 8     8   33 my ($self, $other, $swap) = @_;
403              
404 8 100       37 return $swap ?
405             ($other le $self->_stringify) : ($self->_stringify le $other);
406             }
407              
408             # Handle the 'gt' operator
409             sub _gt {
410 4     4   20 my ($self, $other, $swap) = @_;
411              
412 4 100       20 return $swap ?
413             ($other gt $self->_stringify) : ($self->_stringify gt $other);
414             }
415              
416             # Handle the 'ge' operator
417             sub _ge {
418 8     8   30 my ($self, $other, $swap) = @_;
419              
420 8 100       38 return $swap ?
421             ($other ge $self->_stringify) : ($self->_stringify ge $other);
422             }
423              
424             # Handle the '.' operator
425             sub _dot {
426 5     5   21 my ($self, $other, $swap) = @_;
427              
428 5 100       36 return $swap ?
429             $other . $self->_stringify : $self->_stringify . $other;
430             }
431              
432             # Handle the '.=' operator
433             sub _dotequal {
434 2     2   1544 my ($self, $other, $swap) = @_;
435              
436 2 50       10 if (! $swap) {
437 2         9 my $class = ref $self;
438 2         348 croak "$class object cannot be on the left of .=";
439             }
440              
441 0           return $other . $self->_stringify;
442             }
443              
444             1;
445              
446             __END__