File Coverage

blib/lib/YASF.pm
Criterion Covered Total %
statement 140 141 99.2
branch 65 66 98.4
condition 6 6 100.0
subroutine 28 28 100.0
pod 4 4 100.0
total 243 245 99.1


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