File Coverage

blib/lib/Data/Validate/CSV/Column.pm
Criterion Covered Total %
statement 92 142 64.7
branch 38 86 44.1
condition 14 44 31.8
subroutine 16 17 94.1
pod 0 3 0.0
total 160 292 54.7


line stmt bran cond sub pod time code
1 2     2   27 use v5.12;
  2         6  
2 2     2   12 use strict;
  2         4  
  2         41  
3 2     2   10 use warnings;
  2         3  
  2         127  
4              
5             package Data::Validate::CSV::Column;
6              
7             our $AUTHORITY = 'cpan:TOBYINK';
8             our $VERSION = '0.003';
9              
10 2     2   1100 use Moo;
  2         6711  
  2         10  
11 2     2   727 use Data::Validate::CSV::Types -types;
  2         5  
  2         20  
12 2     2   12347 use Types::Common::String qw( NonEmptyStr );
  2         86335  
  2         26  
13 2     2   1156 use Types::TypeTiny qw( TypeTiny );
  2         7  
  2         14  
14 2     2   457 use namespace::autoclean;
  2         4  
  2         23  
15              
16             has name => (
17             is => 'rwp',
18             isa => Str,
19             predicate => 1,
20             );
21              
22             sub maybe_set_name {
23 3     3 0 6 my $self = shift;
24 3 100       31 $self->_set_name(@_) unless $self->has_name;
25 3         45 $self;
26             }
27              
28             has titles => (
29             is => 'ro',
30             isa => Str | ArrayRef[Str] | HashRef[Str|ArrayRef[Str]],
31             coerce => 1,
32             );
33              
34             has datatype => (
35             is => 'lazy',
36             isa => HashRef->plus_coercions( Str, '{base=>$_}' ),
37             coerce => 1,
38 1     1   37 builder => sub { { base => 'string' } },
39             );
40              
41             has default => (
42             is => 'ro',
43             isa => Any,
44             predicate => 1,
45             );
46              
47             has null => (
48             is => 'ro',
49             isa => ArrayRef->of(Str)->plus_coercions(Str, '[$_]'),
50             coerce => 1,
51             predicate => 1,
52             );
53              
54             has separator => (
55             is => 'ro',
56             isa => NonEmptyStr,
57             predicate => 1,
58             );
59              
60             has ordered => (
61             is => 'ro',
62             isa => Bool,
63             default => 1,
64             );
65              
66             has required => (
67             is => 'ro',
68             isa => Bool,
69             default => 0,
70             coerce => 1,
71             );
72              
73             has type_constraint => (
74             is => 'lazy',
75             isa => TypeTiny,
76             handles => ['assert_valid', 'assert_coerce', 'coerce', 'check', 'get_message', 'has_coercion'],
77             );
78              
79             has base_type_constraint => (
80             is => 'lazy',
81             isa => TypeTiny,
82             );
83              
84             my %mapping = map { $_ => $_ } qw(
85             length maxLength minLength
86             minExclusive maxExclusive
87             minInclusive maxInclusive
88             fractionDigits totalDigits
89             explicitTimezone
90             );
91             # these silly aliases are why we need a mapping
92             $mapping{maximum} = 'maxInclusive';
93             $mapping{minimum} = 'minInclusive';
94              
95             my %is_numeric = map { $_ => 1 } qw(
96             float double decimal integer nonpositiveinteger
97             negativeinteger long int short byte nonnegativeinteger
98             positiveinteger unsignedlong unsignedint unsignedbyte
99             );
100              
101             my %is_dt = map { $_ => 1 } qw(
102             datetime datetimestamp time date gyearmonth gyear
103             gmonthday gday gmonth
104             );
105              
106             sub canonicalize_value {
107 13     13 0 36 shift->_canon(0, @_);
108             }
109              
110             sub inflate_value {
111 12     12 0 37 shift->_canon(1, @_);
112             }
113              
114             sub _canon {
115 25     25   45 my $self = shift;
116 25         66 my ($obj, $errs, @values) = @_;
117 25         407 my $base = lc $self->datatype->{base};
118            
119 25         1093 require JSON::PP;
120 25         14968 require Types::XSD;
121            
122 25 100       824018 if ($self->has_separator) {
123             @values = map {
124 8 50 33     21 ($_ eq '' || !defined) ? () : split quotemeta($self->separator)
  8         195  
125             } @values;
126             }
127              
128 25 50       113 unless ($base =~ /^(string|json|xml|html|anyatomictype)^/) {
129 25         89 s/[\t\r\n]/ /g for @values;
130             }
131            
132 25 50       80 unless ($base =~ /^(string|json|xml|html|anyatomictype|normalizedstring)^/) {
133 25         121 s/\s+/ /g for @values;
134 25         70 s/^\s+//g for @values;
135 25         83 s/\s+$//g for @values;
136             }
137              
138 25 50       85 my %is_null = map { $_ => 1 } $self->has_null ? @{$self->null} : ();
  0         0  
  0         0  
139            
140             my @coerced = map {
141 25         53 my $v = $_;
  27         116  
142 27 0 0     86 if ($self->has_default and $v eq '' || !defined $v) {
      33        
143 0         0 $v = $self->default;
144             }
145 27 100       516 my $c = $self->has_coercion ? $self->coerce($v) : $v;
146 27 50       1823 if ($is_null{$c}) {
    100          
147 0         0 undef;
148             }
149             elsif ($self->check($c)) {
150 23 50 66     7110 if ($obj and $base eq 'boolean') {
    50 66        
    50 66        
    100 66        
    50          
151             ($c eq 'true' || $c eq '1') ? JSON::PP::true() :
152             ($c eq 'false' || $c eq '0') ? JSON::PP::false() :
153 0 0 0     0 do { push @$errs, sprintf('Value %s is not a valid boolean', B::perlstring($c)); $c };
  0 0 0     0  
  0         0  
154             }
155             elsif ($obj and $base =~ /duration/) {
156 0         0 Types::XSD::dur_parse($c);
157             }
158             elsif ($obj and $base =~ /datetime/) {
159 0         0 Types::XSD::dt_parse($c)->to_datetime;
160             }
161             elsif ($obj and $is_dt{$base}) {
162 5         119 Types::XSD::dt_parse($self->base_type_constraint, $c);
163             }
164             elsif ($is_numeric{$base}) {
165 0         0 0+$c;
166             }
167             else {
168 18         84 $c;
169             }
170             }
171             else {
172 4 50       251 if ($self->base_type_constraint->check($c)) {
173 4         130 push @$errs, sprintf('Value %s is a valid %s, but fails additional constraints', B::perlstring($c), $base);
174             }
175             else {
176 0         0 push @$errs, sprintf('Value %s is a not valid %s', B::perlstring($c), $base);
177             }
178 4         21 $c;
179             }
180             } @values;
181            
182 25 100 66     861 $self->has_separator || @_ > 3 ? \@coerced : $coerced[0];
183             }
184              
185             sub _build_base_type_constraint {
186 4     4   45 my $self = shift;
187 4   50     62 my $base = lc( $self->datatype->{base} || 'string' );
188 4         69 my ($xsd_type) =
189             map Types::XSD->get_type($_),
190             grep $base eq lc($_),
191             Types::XSD->type_names;
192 4         251 $xsd_type;
193             }
194              
195             sub _build_type_constraint {
196 4     4   107 my $self = shift;
197 4         17 require Types::XSD;
198 4         9 my %dt = %{ $self->datatype };
  4         97  
199 4         125 my $base = lc delete $dt{base};
200 4         88 my $xsd_type = $self->base_type_constraint;
201 4 50       231 die "huh? $base" unless $xsd_type;
202            
203 4         27 my %facets;
204 4         39 for my $key (sort keys %mapping) {
205 48 50       96 next unless exists $dt{$key};
206 0         0 $facets{$mapping{$key}} = delete $dt{$key};
207             }
208            
209 4         16 my ($coerce_boolean, $coerce_numeric, $coerce_dt);
210 4 100       11 if (exists $dt{format}) {
211 2 50       15 if ($base eq 'boolean') {
    50          
    100          
212 0         0 $coerce_boolean = delete $dt{format};
213             }
214             elsif ($is_numeric{$base}) {
215 0         0 $coerce_numeric = delete $dt{format};
216             }
217             elsif ($is_dt{$base}) {
218 1         3 $coerce_dt = delete $dt{format};
219             }
220             else {
221 1         3 my $fmt = delete $dt{format};
222 1         15 $facets{pattern} = qr/^$fmt$/;
223             }
224             }
225            
226 4         20 my $parameterized = $xsd_type->of(%facets);
227 4 50       3599 if ($dt{'dc:title'}) {
228             $parameterized = $parameterized->create_child_type(
229 0         0 name => delete $dt{'dc:title'},
230             );
231             }
232            
233 4         15 delete $dt{$_} for grep /:/, keys %dt;
234 4 50       14 die "unrecognized keys: ".join(', ', sort keys %dt)
235             if keys %dt;
236            
237 4 50       12 if (defined $coerce_boolean) {
238 0         0 my ($t,$f) = split /\|/, $coerce_boolean;
239 0         0 $parameterized = $parameterized->plus_coercions(
240             Enum[$t,$f], sprintf('0+!!($_ eq %s)', B::perlstring($t)),
241             );
242             }
243              
244 4 50       15 if (defined $coerce_numeric) {
245 0 0       0 my %fmt = ref($coerce_numeric) ? %$coerce_numeric : (pattern => $coerce_numeric);
246             $parameterized = $parameterized->plus_coercions(
247             ~Ref, sprintf(
248             '%s->_coerce_numeric($_, %s, %s, %s)',
249             map defined($_) ? B::perlstring($_) : 'undef',
250             ref($self),
251 0 0       0 @fmt{qw(pattern decimalChar groupChar)},
252             ),
253             );
254             }
255              
256 4 100       11 if (defined $coerce_dt) {
257 1 50       6 $parameterized = $parameterized->plus_coercions(
258             ~Ref, sprintf(
259             '%s->_coerce_dt($_, %s, %s)',
260             map defined($_) ? B::perlstring($_) : 'undef',
261             ref($self),
262             $coerce_dt,
263             lc($base),
264             ),
265             );
266             }
267            
268 4         651 return $parameterized;
269             }
270              
271             sub _coerce_numeric {
272 0     0   0 shift;
273 0         0 my ($value, $pattern, $decimal_char, $group_char) = @_;
274 0   0     0 $decimal_char //= '.';
275 0   0     0 $group_char //= ',';
276 0         0 $pattern =~ s/;+$//;
277            
278 0 0       0 return 'NaN' if lc($value) eq 'nan';
279 0 0       0 return 'INF' if lc($value) eq 'inf';
280 0 0       0 return '-INF' if lc($value) eq '-inf';
281            
282 0         0 my $regexp;
283 0 0       0 if (defined $pattern) {
284 0         0 my %numeric_pattern_char = (
285             '0' => '[0-9]+',
286             '#' => '[0-9]+',
287             '-' => quotemeta('-'),
288             'E' => '[Ee]',
289             'e' => '[Ee]',
290             '%' => quotemeta('%'),
291             '‰' => quotemeta('‰'),
292             $decimal_char => quotemeta($decimal_char),
293             $group_char => quotemeta($group_char),
294             );
295 0         0 my @regexp;
296 0         0 for my $part (split /;/, $pattern) {
297 0         0 push @regexp, '';
298 0         0 while (length $part) {
299 0         0 my $next = substr($part, 0, 1, '');
300 0   0     0 $regexp[-1] .= ($numeric_pattern_char{$next}
301             or die "unrecognized numeric pattern char: $next");
302             }
303             }
304 0 0       0 if (@regexp == 1) {
305 0         0 $regexp[0] = '-?' . $regexp[0];
306             }
307 0         0 $regexp = join '|', map "(?:$_)", @regexp;
308 0         0 $regexp = qr/^($regexp)$/;
309             }
310            
311 0 0 0     0 if (!defined $pattern or $value =~ $regexp) {
312 0         0 my $dummy = quotemeta($group_char);
313 0         0 $value =~ s/$dummy//g;
314 0 0       0 unless ($decimal_char eq '.') {
315 0         0 my $dec = quotemeta($decimal_char);
316 0         0 $value =~ s/$dec/\./g;
317             }
318 0 0       0 if ($value =~ /^(.+)\%$/) {
    0          
319 0         0 $value = $1 / 100;
320             }
321             elsif ($value =~ /^(.+)‰$/) {
322 0         0 $value = $1 / 1000;
323             }
324             }
325            
326 0         0 return $value;
327             }
328              
329             my %target_patterns = (
330             datetime => '%FT%T',
331             datetimestamp => '%FT%T%z',
332             time => '%T',
333             date => '%F',
334             gyearmonth => '%Y-%m',
335             gyear => '%Y',
336             gmonthday => '--%m-%d',
337             gday => '---%d',
338             gmonth => '--%m',
339             );
340             sub _coerce_dt {
341 10     10   2408 shift;
342 10         774 require DateTime::Format::CLDR;
343 10         6562 my ($value, $pattern, $target_type) = @_;
344 10         45 my $parser = DateTime::Format::CLDR->new(
345             locale => 'en-GB', # allow override???
346             pattern => $pattern,
347             );
348 10         3528 my $dt = $parser->parse_datetime($value);
349 10 50       8603 return $value unless ref $dt;
350 10   33     53 $dt->strftime($target_patterns{$target_type} || $target_patterns{datetimestamp});
351             }
352              
353             1;