File Coverage

blib/lib/YASF.pm
Criterion Covered Total %
statement 123 138 89.1
branch 48 64 75.0
condition 6 6 100.0
subroutine 22 27 81.4
pod 4 4 100.0
total 203 239 84.9


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