File Coverage

blib/lib/Data/Xslate.pm
Criterion Covered Total %
statement 114 123 92.6
branch 25 42 59.5
condition 2 4 50.0
subroutine 20 20 100.0
pod 4 5 80.0
total 165 194 85.0


line stmt bran cond sub pod time code
1             package Data::Xslate;
2 4     4   793854 use 5.008001;
  4         26  
3 4     4   18 use strict;
  4         5  
  4         73  
4 4     4   16 use warnings;
  4         6  
  4         136  
5             our $VERSION = '0.09';
6              
7 4     4   2025 use Text::Xslate;
  4         43825  
  4         173  
8 4     4   26 use Carp qw( croak );
  4         7  
  4         165  
9 4     4   2239 use Storable qw( freeze thaw );
  4         10929  
  4         260  
10              
11             # A tied-hash class used to expose the data as the Xslate
12             # vars when processing the data.
13             {
14             package # NO INDEX
15             Data::Xslate::Vars;
16              
17 4     4   27 use base 'Tie::Hash';
  4         8  
  4         5449  
18              
19             sub TIEHASH {
20 458     458   732 my ($class, $sub) = @_;
21 458         1304 return bless {sub=>$sub}, $class;
22             }
23              
24             sub FETCH {
25 7     7   6932 my ($self, $key) = @_;
26              
27 7         30 return $self->{sub}->( $key );
28             }
29             }
30              
31             sub new {
32 153     153 0 309036 my $class = shift;
33              
34 153 50       461 die if @_ % 2 != 0;
35 153         426 my $args = { @_ };
36              
37 153         452 my $defaults = {
38             substitution_tag => '=',
39             nested_key_tag => '=',
40             key_separator => '.',
41             };
42              
43 153         473 my $self = bless { %$defaults }, $class;
44 153         399 foreach my $key (keys %$defaults) {
45 459 100       683 next if ! exists $args->{$key};
46 450         688 $self->{$key} = delete $args->{$key};
47             }
48              
49 153   50     560 my $function = delete( $args->{function} ) || {};
50 153   50     576 $function->{node} ||= \&_find_node_for_xslate;
51 153         626 $self->{_xslate} = Text::Xslate->new(
52             type => 'text',
53             function => $function,
54             %$args,
55             );
56              
57 153         36265 return $self;
58             }
59              
60             # Arguments.
61 458     458 1 714 sub substitution_tag { $_[0]->{substitution_tag} }
62 458     458 1 666 sub nested_key_tag { $_[0]->{nested_key_tag} }
63 458     458 1 635 sub key_separator { $_[0]->{key_separator} }
64              
65             # Attributes.
66 458     458   808 sub _xslate { $_[0]->{_xslate} }
67              
68             # State variables, only used during local() calls to maintain
69             # state in recursive function calls.
70             our $XSLATE;
71             our $VARS;
72             our $ROOT;
73             our $NODES;
74             our $SUBSTITUTION_TAG;
75             our $NESTED_KEY_TAG;
76             our $KEY_SEPARATOR;
77             our $PATH_FOR_XSLATE;
78              
79             sub render {
80 458     458 1 235719 my ($self, $data) = @_;
81              
82 458         1106 $data = thaw( freeze( $data ) );
83              
84 458         22604 local $Carp::Internal{ (__PACKAGE__) } = 1;
85              
86 458         842 local $XSLATE = $self->_xslate();
87              
88 458         589 my %vars;
89 458         1494 tie %vars, 'Data::Xslate::Vars', \&_find_node_for_xslate;
90 458         654 local $VARS = \%vars;
91              
92 458         576 local $ROOT = $data;
93 458         572 local $NODES = {};
94 458         705 local $SUBSTITUTION_TAG = $self->substitution_tag();
95 458         745 local $NESTED_KEY_TAG = $self->nested_key_tag();
96 458         644 local $KEY_SEPARATOR = $self->key_separator();
97              
98 458         752 return _evaluate_node( 'root' => $data );
99             }
100              
101             sub _evaluate_node {
102 2632     2632   3885 my ($path, $node) = @_;
103              
104 2632 100       5647 return $NODES->{$path} if exists $NODES->{$path};
105              
106 1847 100       3374 if (!ref $node) {
    100          
    50          
107 931 50       1616 if (defined $node) {
108 931 100       4618 if ($node =~ m{^\Q$SUBSTITUTION_TAG\E\s*(.+?)\s*$}) {
109 157         306 $node = _find_node( $1, $path );
110             }
111             else {
112 774         1133 local $PATH_FOR_XSLATE = $path;
113 774         3053 $node = $XSLATE->render_string( $node, $VARS );
114             }
115             }
116 931         1178929 $NODES->{$path} = $node;
117             }
118             elsif (ref($node) eq 'HASH') {
119 915         1484 $NODES->{$path} = $node;
120 915         2530 foreach my $key (sort keys %$node) {
121 1386 100       5243 if ($key =~ m{^(.*)\Q$NESTED_KEY_TAG\E$}) {
122 152         433 my $sub_path = "$path$KEY_SEPARATOR$1";
123 152         306 my $value = delete $node->{$key};
124 152         310 _set_node( $sub_path, $value );
125             }
126             else {
127 1234         2229 my $sub_path = "$path$KEY_SEPARATOR$key";
128 1234         2001 $node->{$key} = _evaluate_node( $sub_path, $node->{$key} );
129             }
130             }
131             }
132             elsif (ref($node) eq 'ARRAY') {
133 1         3 $NODES->{$path} = $node;
134             @$node = (
135 1         4 map { _evaluate_node( "$path$KEY_SEPARATOR$_" => $node->[$_] ) }
  3         16  
136             (0..$#$node)
137             );
138             }
139             else {
140 0         0 croak "The config node at $path is neither a hash, array, or scalar";
141             }
142              
143 1847         5798 return $node;
144             }
145              
146             sub _load_node {
147 324     324   503 my ($path) = @_;
148              
149 324         762 my @parts = split(/\Q$KEY_SEPARATOR\E/, $path);
150 324         526 my $built_path = shift( @parts ); # root
151              
152 324         450 my $node = $ROOT;
153 324         496 while (@parts) {
154 640         814 my $key = shift( @parts );
155 640         895 $built_path .= "$KEY_SEPARATOR$key";
156              
157 640 50       1046 if (ref($node) eq 'HASH') {
    0          
158 640 100       1090 return undef if !exists $node->{$key};
159 633         959 $node = _evaluate_node( $built_path => $node->{$key} );
160             }
161             elsif (ref($node) eq 'ARRAY') {
162 0 0       0 return undef if $key > $#$node;
163 0         0 $node = _evaluate_node( $built_path => $node->[$key] );
164             }
165             else {
166 0         0 croak "The config node at $path is neither a hash or array";
167             }
168             }
169              
170 317         581 return $node;
171             }
172              
173             sub _find_node {
174 317     317   601 my ($path, $from_path) = @_;
175              
176 317 100       1126 if ($path =~ m{^\Q$KEY_SEPARATOR\E(.+)}) {
177 1         3 $path = $1;
178 1         3 $from_path = "root${KEY_SEPARATOR}root_sub_key_that_is_not_used_for_absolute_keys";
179             }
180              
181 317         1047 my @parts = split(/\Q$KEY_SEPARATOR\E/, $from_path);
182 317         475 pop( @parts );
183              
184 317         677 while (@parts) {
185 324         671 my $sub_path = join($KEY_SEPARATOR, @parts);
186              
187 324         751 my $node = _load_node( "$sub_path$KEY_SEPARATOR$path" );
188 324 100       1203 return $node if $node;
189              
190 7         12 pop( @parts );
191             }
192              
193 0         0 return _load_node( $path );
194             }
195              
196             sub _find_node_for_xslate {
197 160     160   210823 my ($path) = @_;
198 160         341 return _find_node( $path, $PATH_FOR_XSLATE );
199             }
200              
201             sub _set_node {
202 152     152   279 my ($path, $value) = @_;
203              
204 152         537 my @parts = split(/\Q$KEY_SEPARATOR\E/, $path);
205 152         280 my $built_path = shift( @parts ); # root
206 152         200 my $last_part = pop( @parts );
207              
208 152         199 my $node = $ROOT;
209 152         303 while (@parts) {
210 152         220 my $key = shift( @parts );
211 152         243 $built_path .= "$KEY_SEPARATOR$key";
212              
213 152 50       304 if (ref($node) eq 'HASH') {
    0          
214 152 50       285 return 0 if !exists $node->{$key};
215 152         298 $node = _evaluate_node( $built_path => $node->{$key} );
216             }
217             elsif (ref($node) eq 'ARRAY') {
218 0 0       0 return 0 if $key > $#$node;
219 0         0 $node = _evaluate_node( $built_path => $node->[$key] );
220             }
221             else {
222 0         0 croak "The config node at $path is neither a hash or array";
223             }
224             }
225              
226 152         277 delete $NODES->{$path};
227 152         229 $value = _evaluate_node( $path => $value );
228              
229 152 50       336 if (ref($node) eq 'HASH') {
    0          
230 152         240 $node->{$last_part} = $value;
231             }
232             elsif (ref($node) eq 'ARRAY') {
233 0         0 $node->[$last_part] = $value;
234             }
235              
236 152         366 return 1;
237             }
238              
239             1;
240             __END__