File Coverage

blib/lib/SQL/Abstract/FromQuery.pm
Criterion Covered Total %
statement 151 159 94.9
branch 33 44 75.0
condition 11 18 61.1
subroutine 32 33 96.9
pod 2 11 18.1
total 229 265 86.4


line stmt bran cond sub pod time code
1             package SQL::Abstract::FromQuery;
2            
3 9     9   42577 use strict;
  9         94  
  9         233  
4 6     6   27 use warnings;
  6         9  
  6         196  
5 6     6   27 use Scalar::Util qw/refaddr reftype blessed/;
  6         12  
  6         611  
6 6     6   3369 use List::MoreUtils qw/mesh/;
  6         224736  
  6         53  
7 6     6   79881 use Module::Load qw/load/;
  6         1962  
  6         69  
8 6         529 use Params::Validate qw/validate SCALAR SCALARREF CODEREF ARRAYREF HASHREF
9 6     6   4130 UNDEF BOOLEAN/;
  6         227835  
10 6     6   2744 use UNIVERSAL::DOES qw/does/;
  6         2545  
  6         345  
11 6     6   35 use Digest::MD5 qw/md5_base64/;
  6         7  
  6         286  
12 6     6   77744 use mro 'c3';
  6         3193  
  6         30  
13            
14 6     6   3080 use namespace::clean;
  6         50961  
  6         41  
15            
16             our $VERSION = '0.10';
17            
18             # root grammar (will be inherited by subclasses)
19             my $root_grammar = do {
20 6     6   189045 use Regexp::Grammars 1.038;
  6         410740  
  6         43  
21             qr{
22             #
23            
24            
25            
26            
27             \A (?:
28            
29             |
30             |
31             |
32             )
33             (?: \Z | )
34            
35            
36             (*COMMIT) (?: | )
37            
38            
39             (*COMMIT) (?: | )
40            
41            
42             <[value]>+ % ,
43            
44            
45             BETWEEN (*COMMIT) (?: AND | )
46            
47            
48             \<= | \< | \>= | \>
49            
50            
51             \<\> | -(?!\d) | != | !
52            
53            
54            
55             |
56             |
57             |
58             # | # removed from "standard" value because it might
59             # interfere with other codes like gender M/F
60            
61            
62             NULL
63            
64            
65             \.\.
66             | --
67            
68            
69             :(?::)?
70            
71            
72             Y(?:ES)? (?{ $MATCH = 1 })
73             | T(?:RUE)? (?{ $MATCH = 1 })
74             | N(?:O)? (?{ $MATCH = 0 })
75             | F(?:ALSE)? (?{ $MATCH = 0 })
76            
77            
78            
79             |
80            
81            
82             '(.*?)' (*COMMIT) (?{ $MATCH = $CAPTURE })
83             | "(.*?)" (*COMMIT) (?{ $MATCH = $CAPTURE })
84            
85            
86             [^\s,]+(?:\s+[^\s,]+)*?
87            
88             }xms;
89             };
90            
91            
92            
93             #======================================================================
94             # CLASS METHODS
95             #======================================================================
96             sub sub_grammar {
97 2     2 0 3 my $class = shift;
98 2         4 return; # should redefine method in subclasses that refine the root grammar
99             }
100            
101             my %params_for_new = (
102             -components => {type => ARRAYREF, optional => 1 },
103             -fields => {type => HASHREF, default => {} },
104             );
105            
106             sub new {
107 7     7 1 1226 my $class = shift;
108 7         13 my $self = {};
109 7         312 my %args = validate(@_, \%params_for_new);
110            
111             # load optional components
112 7 100       44 if ($args{-components}) {
113             # deactivate strict refs because we'll be playing with symbol tables
114 6     6   45 no strict 'refs';
  6         7  
  6         6902  
115            
116 5         25 my @components;
117 5         6 foreach my $component (@{$args{-components}}) {
  5         15  
118 6 50       91 $component =~ s/^\+//
119             or $component = __PACKAGE__ . "::$component";
120 6         24 load $component;
121 6         84 push @components, $component;
122 6         42 my @sub_grammar = $component->sub_grammar;
123 6 100       29 push @{$self->{grammar_ISA}}, @sub_grammar if @sub_grammar;
  4         19  
124             }
125            
126             # a new anonymous class will inherit from all components
127 5         42 $class .= "::_ANON_::" . md5_base64(join ",", @components);
128 5 50       6 unless (@{$class . "::ISA"}) {
  5         48  
129             # dynamically create that class and use 'c3' inheritance in it
130 5         5 push @{$class . "::ISA"}, @components;
  5         41  
131 5         24 mro::set_mro($class, 'c3');
132             }
133             }
134            
135             # use root grammar if no derived grammar was installed by components
136 7   100     36 $self->{grammar_ISA} ||= [ 'SQL::Abstract::FromQuery' ];
137            
138             # setup fields info
139 7         10 foreach my $type (keys %{$args{-fields}}) {
  7         33  
140 4 100       15 if ($type eq 'IGNORE') {
141 1 50       8 ref $args{-fields}{IGNORE} eq 'Regexp'
142             or die "IGNORE should be associated with a qr/.../ regular expression";
143 1         6 $self->{IGNORE} = $args{-fields}{IGNORE};
144             }
145             else {
146 3         8 my $fields_aref = $args{-fields}{$type};
147 3 50       14 does($fields_aref, 'ARRAY')
148             or die "list of fields for type $type should be an arrayref";
149 3         80 $self->{field}{$_} = $type foreach @$fields_aref;
150             }
151             }
152            
153             # return the blessed object
154 7         56 bless $self, $class;
155             }
156            
157             sub _error_handler {
158 31     31   34 my $class = shift;
159             return 'INCORRECT INPUT', sub {
160 44     44   274 my ($error, $rule, $context) = @_;
161 44         147 my $msg = {
162             negated_values => 'Expected a value after negation',
163             op_and_value => 'Expected a value after comparison operator',
164             between => 'Expected min and max after "BETWEEN"',
165             standard => 'Unexpected input after initial value',
166             }->{$rule};
167 44   33     109 $msg //= "Could not parse rule '$rule'";
168 44 100       107 $msg .= " ('$context')" if $context;
169 44         105 return $msg;
170 31         147 };
171             }
172            
173            
174             #======================================================================
175             # INSTANCE METHODS
176             #======================================================================
177            
178            
179             sub _grammar {
180 9     9   15 my ($self, $rule) = @_;
181            
182 9         13 my $extends = join "", map {"\n"} @{$self->{grammar_ISA}};
  9         38  
  9         22  
183 9         27 my $grammar = "<$rule>\n$extends";
184            
185             # compile this grammar. NOTE : since Regexp::Grammars uses a very
186             # special form of operator overloading, we must go through an eval
187             # so that qr/../ receives a string without variable interpolation;
188             # do {use Regexp::Grammars; qr{$grammar}x;} would seem logical but won't work.
189 9         12 local $@;
190 6 50   6   34 my $compiled_grammar = eval "use Regexp::Grammars; qr{$grammar}x"
  6         8  
  6         35  
  9         708  
191             or die "INVALID GRAMMAR: $@";
192            
193 9         75 return $compiled_grammar;
194             }
195            
196            
197            
198            
199             sub parse {
200 34     34 1 19936 my ($self, $data) = @_;
201 34         53 my $class = ref $self;
202            
203             # if $data is an object with ->param() method, transform into plain hashref
204 34 50 33     143 $data = $self->_flatten_into_hashref($data) if blessed $data
205             && $data->can('param');
206            
207             # set error translator for grammars
208 34         110 my ($err_msg, $err_translator) = $self->_error_handler;
209 34         106 my $tmp = Regexp::Grammars::set_error_translator($err_translator);
210            
211             # parse each field within $data
212 34         441 my %result;
213             my %errors;
214             FIELD:
215 34         120 foreach my $field (keys %$data) {
216             # ignore fields in exclusion list or fields without any data
217 46 100 66     293 !$self->{IGNORE} or $field !~ $self->{IGNORE} or next FIELD;
218 45 50       116 my $val = $data->{$field} or next FIELD;
219            
220             # decide which grammar to apply
221 45   100     160 my $rule = $self->{field}{$field} || 'standard';
222 45   66     155 my $grammar = $self->{grammar}{$rule} ||= $self->_grammar($rule);
223            
224             # invoke grammar on field content
225 45 100       295 if ($val =~ $grammar->with_actions($self)) {
226 36         155 $result{$field} = $/{$rule};
227             }
228             else {
229 9         264 $errors{$field} = [@!];
230             }
231             }
232            
233             # report errors, if any
234 34 100       123 SQL::Abstract::FromQuery::_Exception->throw($err_msg, %errors) if %errors;
235            
236 29         127 return \%result;
237             }
238            
239            
240             sub _flatten_into_hashref {
241 0     0   0 my ($self, $data) = @_;
242 0         0 my %h;
243 0         0 foreach my $field ($data->param()) {
244 0         0 my @vals = $data->param($field);
245 0         0 my $val = join ",", @vals; # TOO simple-minded - should make it more abstract
246 0         0 $h{$field} = $val;
247             }
248 0         0 return \%h;
249             }
250            
251            
252            
253             #======================================================================
254             # ACTIONS HOOKED TO THE GRAMMAR
255             #======================================================================
256            
257             sub negated_values {
258 5     5 0 108 my ($self, $h) = @_;
259 5         9 my $vals = $h->{values};
260 5 100       11 if (ref $vals) {
261 3 50       11 ref $vals eq 'HASH' or die 'unexpected reference in negation';
262 3         7 my ($op, $val, @others) = %$vals;
263 3 50       8 not @others or die 'unexpected hash size in negation';
264 3 100       11 if ($op eq '-in') {return {-not_in => $val} }
  1 50       4  
  2         7  
265 0         0 elsif ($op eq '=' ) {return {'<>' => $val} }
266             else {die "unexpected operator '$op' in negation"}
267             }
268             else {
269 2         5 return {'<>' => $vals};
270             }
271             }
272            
273            
274             sub null {
275 5     5 0 95 my ($self, $h) = @_;
276 5         16 return {'=' => undef};
277             # Note: unfortunately, we can't return just undef at this stage,
278             # because Regex::Grammars would interpret it as a parse failure.
279             }
280            
281            
282             sub op_and_value {
283 2     2 0 49 my ($self, $h) = @_;
284 2         6 return {$h->{compare} => $h->{value}};
285             }
286            
287            
288             sub between {
289 7     7 0 155 my ($self, $h) = @_;
290 7         41 return {-between => [$h->{min}, $h->{max}]};
291             }
292            
293            
294            
295             sub values {
296 29     29 0 564 my ($self, $h) = @_;
297 29         42 my $n_values = @{$h->{value}};
  29         47  
298 29 100       183 return $n_values > 1 ? {-in => $h->{value}}
299             : $h->{value}[0];
300             }
301            
302            
303             sub date {
304 4     4 0 88 my ($self, $h) = @_;
305 4 50       20 $h->{year} += 2000 if length($h->{year}) < 3;
306 4         7 return sprintf "%04d-%02d-%02d", @{$h}{qw/year month day/};
  4         27  
307             }
308            
309            
310             sub time {
311 2     2 0 46 my ($self, $h) = @_;
312 2   50     13 $h->{seconds} ||= 0;
313 2         4 return sprintf "%02d:%02d:%02d", @{$h}{qw/hour minutes seconds/};
  2         13  
314             }
315            
316            
317             sub string {
318 51     51 0 928 my ($self, $s) = @_;
319            
320             # if any '*', substitute by '%' and make it a "-like" operator
321 51         76 my $is_pattern = $s =~ tr/*/%/;
322             # NOTE : a reentrant =~ s/../../ would core dump, but tr/../../ is OK
323            
324 51 100       147 return $is_pattern ? {-like => $s} : $s;
325             }
326            
327            
328             #======================================================================
329             # PRIVATE CLASS FOR REPORTING PARSE EXCEPTIONS
330             #======================================================================
331            
332             package
333             SQL::Abstract::FromQuery::_Exception;
334 6     6   34 use strict;
  6         9  
  6         186  
335 6     6   28 use warnings;
  6         8  
  6         737  
336            
337             use overload
338             '""' => sub {
339 8     8   685 my $self = shift;
340 8         12 my $msg = $self->{err_msg};
341 8         10 for my $field (sort keys %{$self->{errors}}) {
  8         32  
342 12         16 my $field_errors = $self->{errors}{$field};
343 12         44 $msg .= "\n$field : " . join ", ", @$field_errors;
344             }
345            
346 8         52 return $msg;
347             },
348 6         689 fallback => 1,
349 6     6   29 ;
  6         7  
350            
351            
352             sub throw {
353 5     5   11 my ($class, $err_msg, %errors) = @_;
354 5         20 my $self = bless {err_msg => $err_msg, errors => \%errors}, $class;
355 5         45 die $self;
356             }
357            
358            
359             #======================================================================
360             1; # End of SQL::Abstract::FromQuery
361             #======================================================================
362            
363             __END__