File Coverage

blib/lib/Data/Validate/CSV/Column.pm
Criterion Covered Total %
statement 23 142 16.2
branch 0 86 0.0
condition 0 44 0.0
subroutine 8 17 47.0
pod 0 3 0.0
total 31 292 10.6


line stmt bran cond sub pod time code
1 1     1   14 use v5.12;
  1         5  
2 1     1   7 use strict;
  1         2  
  1         22  
3 1     1   5 use warnings;
  1         3  
  1         53  
4              
5             package Data::Validate::CSV::Column;
6              
7             our $AUTHORITY = 'cpan:TOBYINK';
8             our $VERSION = '0.002';
9              
10 1     1   561 use Moo;
  1         2740  
  1         5  
11 1     1   331 use Data::Validate::CSV::Types -types;
  1         2  
  1         10  
12 1     1   6132 use Types::Common::String qw( NonEmptyStr );
  1         43222  
  1         15  
13 1     1   620 use Types::TypeTiny qw( TypeTiny );
  1         3  
  1         9  
14 1     1   227 use namespace::autoclean;
  1         2  
  1         10  
15              
16             has name => (
17             is => 'rwp',
18             isa => Str,
19             predicate => 1,
20             );
21              
22             sub maybe_set_name {
23 0     0 0   my $self = shift;
24 0 0         $self->_set_name(@_) unless $self->has_name;
25 0           $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 0     0     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 0     0 0   shift->_canon(0, @_);
108             }
109              
110             sub inflate_value {
111 0     0 0   shift->_canon(1, @_);
112             }
113              
114             sub _canon {
115 0     0     my $self = shift;
116 0           my ($obj, $errs, @values) = @_;
117 0           my $base = lc $self->datatype->{base};
118            
119 0           require JSON::PP;
120 0           require Types::XSD;
121            
122 0 0         if ($self->has_separator) {
123             @values = map {
124 0 0 0       ($_ eq '' || !defined) ? () : split quotemeta($self->separator)
  0            
125             } @values;
126             }
127              
128 0 0         unless ($base =~ /^(string|json|xml|html|anyatomictype)^/) {
129 0           s/[\t\r\n]/ /g for @values;
130             }
131            
132 0 0         unless ($base =~ /^(string|json|xml|html|anyatomictype|normalizedstring)^/) {
133 0           s/\s+/ /g for @values;
134 0           s/^\s+//g for @values;
135 0           s/\s+$//g for @values;
136             }
137              
138 0 0         my %is_null = map { $_ => 1 } $self->has_null ? @{$self->null} : ();
  0            
  0            
139            
140             my @coerced = map {
141 0           my $v = $_;
  0            
142 0 0 0       if ($self->has_default and $v eq '' || !defined $v) {
      0        
143 0           $v = $self->default;
144             }
145 0 0         my $c = $self->has_coercion ? $self->coerce($v) : $v;
146 0 0         if ($is_null{$c}) {
    0          
147 0           undef;
148             }
149             elsif ($self->check($c)) {
150 0 0 0       if ($obj and $base eq 'boolean') {
    0 0        
    0 0        
    0 0        
    0          
151             ($c eq 'true' || $c eq '1') ? JSON::PP::true() :
152             ($c eq 'false' || $c eq '0') ? JSON::PP::false() :
153 0 0 0       do { push @$errs, sprintf('Value %s is not a valid boolean', B::perlstring($c)); $c };
  0 0 0        
  0            
154             }
155             elsif ($obj and $base =~ /duration/) {
156 0           Types::XSD::dur_parse($c);
157             }
158             elsif ($obj and $base =~ /datetime/) {
159 0           Types::XSD::dt_parse($c)->to_datetime;
160             }
161             elsif ($obj and $is_dt{$base}) {
162 0           Types::XSD::dt_parse($self->base_type_constraint, $c);
163             }
164             elsif ($is_numeric{$base}) {
165 0           0+$c;
166             }
167             else {
168 0           $c;
169             }
170             }
171             else {
172 0 0         if ($self->base_type_constraint->check($c)) {
173 0           push @$errs, sprintf('Value %s is a valid %s, but fails additional constraints', B::perlstring($c), $base);
174             }
175             else {
176 0           push @$errs, sprintf('Value %s is a not valid %s', B::perlstring($c), $base);
177             }
178 0           $c;
179             }
180             } @values;
181            
182 0 0 0       $self->has_separator || @_ > 3 ? \@coerced : $coerced[0];
183             }
184              
185             sub _build_base_type_constraint {
186 0     0     my $self = shift;
187 0   0       my $base = $self->datatype->{base} || 'string';
188 0           my ($xsd_type) =
189             map Types::XSD->get_type($_),
190             grep $base eq lc($_),
191             Types::XSD->type_names;
192 0           $xsd_type;
193             }
194              
195             sub _build_type_constraint {
196 0     0     my $self = shift;
197 0           require Types::XSD;
198 0           my %dt = %{ $self->datatype };
  0            
199 0           my $base = lc delete $dt{base};
200 0           my $xsd_type = $self->base_type_constraint;
201 0 0         die "huh? $base" unless $xsd_type;
202            
203 0           my %facets;
204 0           for my $key (sort keys %mapping) {
205 0 0         next unless exists $dt{$key};
206 0           $facets{$mapping{$key}} = delete $dt{$key};
207             }
208            
209 0           my ($coerce_boolean, $coerce_numeric, $coerce_dt);
210 0 0         if (exists $dt{format}) {
211 0 0         if ($base eq 'boolean') {
    0          
    0          
212 0           $coerce_boolean = delete $dt{format};
213             }
214             elsif ($is_numeric{$base}) {
215 0           $coerce_numeric = delete $dt{format};
216             }
217             elsif ($is_dt{$base}) {
218 0           $coerce_dt = delete $dt{format};
219             }
220             else {
221 0           my $fmt = delete $dt{format};
222 0           $facets{pattern} = qr/^$fmt$/;
223             }
224             }
225            
226 0           my $parameterized = $xsd_type->of(%facets);
227 0 0         if ($dt{'dc:title'}) {
228             $parameterized = $parameterized->create_child_type(
229 0           name => delete $dt{'dc:title'},
230             );
231             }
232            
233 0           delete $dt{$_} for grep /:/, keys %dt;
234 0 0         die "unrecognized keys: ".join(', ', sort keys %dt)
235             if keys %dt;
236            
237 0 0         if (defined $coerce_boolean) {
238 0           my ($t,$f) = split /\|/, $coerce_boolean;
239 0           $parameterized = $parameterized->plus_coercions(
240             Enum[$t,$f], sprintf('0+!!($_ eq %s)', B::perlstring($t)),
241             );
242             }
243              
244 0 0         if (defined $coerce_numeric) {
245 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         @fmt{qw(pattern decimalChar groupChar)},
252             ),
253             );
254             }
255              
256 0 0         if (defined $coerce_dt) {
257 0 0         $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 0           return $parameterized;
269             }
270              
271             sub _coerce_numeric {
272 0     0     shift;
273 0           my ($value, $pattern, $decimal_char, $group_char) = @_;
274 0   0       $decimal_char //= '.';
275 0   0       $group_char //= ',';
276 0           $pattern =~ s/;+$//;
277            
278 0 0         return 'NaN' if lc($value) eq 'nan';
279 0 0         return 'INF' if lc($value) eq 'inf';
280 0 0         return '-INF' if lc($value) eq '-inf';
281            
282 0           my $regexp;
283 0 0         if (defined $pattern) {
284 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           my @regexp;
296 0           for my $part (split /;/, $pattern) {
297 0           push @regexp, '';
298 0           while (length $part) {
299 0           my $next = substr($part, 0, 1, '');
300 0   0       $regexp[-1] .= ($numeric_pattern_char{$next}
301             or die "unrecognized numeric pattern char: $next");
302             }
303             }
304 0 0         if (@regexp == 1) {
305 0           $regexp[0] = '-?' . $regexp[0];
306             }
307 0           $regexp = join '|', map "(?:$_)", @regexp;
308 0           $regexp = qr/^($regexp)$/;
309             }
310            
311 0 0 0       if (!defined $pattern or $value =~ $regexp) {
312 0           my $dummy = quotemeta($group_char);
313 0           $value =~ s/$dummy//g;
314 0 0         unless ($decimal_char eq '.') {
315 0           my $dec = quotemeta($decimal_char);
316 0           $value =~ s/$dec/\./g;
317             }
318 0 0         if ($value =~ /^(.+)\%$/) {
    0          
319 0           $value = $1 / 100;
320             }
321             elsif ($value =~ /^(.+)‰$/) {
322 0           $value = $1 / 1000;
323             }
324             }
325            
326 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 0     0     shift;
342 0           require DateTime::Format::CLDR;
343 0           my ($value, $pattern, $target_type) = @_;
344 0           my $parser = DateTime::Format::CLDR->new(
345             locale => 'en-GB', # allow override???
346             pattern => $pattern,
347             );
348 0           my $dt = $parser->parse_datetime($value);
349 0 0         return $value unless ref $dt;
350 0   0       $dt->strftime($target_patterns{$target_type} || $target_patterns{datetimestamp});
351             }
352              
353             1;