File Coverage

blib/lib/JSON/Schema/Draft201909/Vocabulary/Format.pm
Criterion Covered Total %
statement 44 45 97.7
branch 13 14 92.8
condition 6 6 100.0
subroutine 15 16 93.7
pod 0 2 0.0
total 78 83 93.9


line stmt bran cond sub pod time code
1 20     20   13800 use strict;
  20         56  
  20         739  
2 20     20   117 use warnings;
  20         47  
  20         1333  
3             package JSON::Schema::Draft201909::Vocabulary::Format;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Implementation of the JSON Schema Draft 2019-09 Format vocabulary
6              
7             our $VERSION = '0.028';
8              
9 20     20   439 use 5.016;
  20         77  
10 20     20   121 no if "$]" >= 5.031009, feature => 'indirect';
  20         45  
  20         211  
11 20     20   1116 no if "$]" >= 5.033001, feature => 'multidimensional';
  20         59  
  20         137  
12 20     20   940 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  20         79  
  20         163  
13 20     20   914 use strictures 2;
  20         155  
  20         829  
14 20     20   4123 use JSON::Schema::Draft201909::Utilities qw(is_type E A assert_keyword_type);
  20         69  
  20         1608  
15 20     20   140 use Moo;
  20         55  
  20         185  
16 20     20   8564 use Feature::Compat::Try;
  20         64  
  20         234  
17 20     20   2600 use namespace::clean;
  20         73  
  20         187  
18              
19             with 'JSON::Schema::Draft201909::Vocabulary';
20              
21 0     0 0 0 sub vocabulary { 'https://json-schema.org/draft/2019-09/vocab/format' }
22              
23             sub keywords {
24 15200     15200 0 36322 qw(format);
25             }
26              
27             {
28             # for now, all built-in formats are constrained to the 'string' type
29              
30             my $is_datetime = sub {
31             eval { require Time::Moment; 1 } or return 1;
32             eval { Time::Moment->from_string(uc($_[0])) } ? 1 : 0,
33             };
34             my $is_email = sub {
35             eval { require Email::Address::XS; Email::Address::XS->VERSION(1.01); 1 } or return 1;
36             Email::Address::XS->parse($_[0])->is_valid;
37             };
38             my $is_hostname = sub {
39             eval { require Data::Validate::Domain; 1 } or return 1;
40             Data::Validate::Domain::is_domain($_[0]);
41             };
42             my $idn_decode = sub {
43             eval { require Net::IDN::Encode; 1 } or return $_[0];
44             try { return Net::IDN::Encode::domain_to_ascii($_[0]) } catch ($e) { return $_[0]; }
45             };
46             my $is_ipv4 = sub {
47             my @o = split(/\./, $_[0], 5);
48             @o == 4 && (grep /^(0|[1-9][0-9]{0,2})$/, @o) == 4 && (grep $_ < 256, @o) == 4;
49             };
50             # https://tools.ietf.org/html/rfc3339#appendix-A with some additions for the 2000 version
51             # as defined in https://en.wikipedia.org/wiki/ISO_8601#Durations
52             my $duration_re = do {
53             my $num = qr{[0-9]+(?:[.,][0-9]+)?};
54             my $second = qr{${num}S};
55             my $minute = qr{${num}M};
56             my $hour = qr{${num}H};
57             my $time = qr{T(?=[0-9])(?:$hour)?(?:$minute)?(?:$second)?};
58             my $day = qr{${num}D};
59             my $month = qr{${num}M};
60             my $year = qr{${num}Y};
61             my $week = qr{${num}W};
62             my $date = qr{(?=[0-9])(?:$year)?(?:$month)?(?:$day)?};
63             qr{^P(?:(?=.)(?:$date)?(?:$time)?|$week)$};
64             };
65              
66             my $formats = +{
67             'date-time' => $is_datetime,
68             date => sub { $_[0] =~ /^\d{4}-(\d\d)-(\d\d)$/ && $is_datetime->($_[0].'T00:00:00Z') },
69             time => sub {
70             return if $_[0] !~ /^(\d\d):(\d\d):(\d\d)(?:\.\d+)?([Zz]|([+-])(\d\d):(\d\d))$/
71             or $1 > 23
72             or $2 > 59
73             or $3 > 60
74             or (defined($6) and $6 > 23)
75             or (defined($7) and $7 > 59);
76              
77             return 1 if $3 <= 59;
78             return $1 == 23 && $2 == 59 if uc($4) eq 'Z';
79              
80             my $sign = $5 eq '+' ? 1 : -1;
81             my $hour_zulu = $1 - $6*$sign;
82             my $min_zulu = $2 - $7*$sign;
83             $hour_zulu -= 1 if $min_zulu < 0;
84              
85             return $hour_zulu%24 == 23 && $min_zulu%60 == 59;
86             },
87             duration => sub { $_[0] =~ $duration_re && $_[0] !~ m{[.,][0-9]+[A-Z].} },
88             email => sub { $is_email->($_[0]) && $_[0] !~ /[^[:ascii:]]/ },
89             'idn-email' => $is_email,
90             hostname => $is_hostname,
91             'idn-hostname' => sub { $is_hostname->($idn_decode->($_[0])) },
92             ipv4 => $is_ipv4,
93             ipv6 => sub {
94             ($_[0] =~ /^(?:[[:xdigit:]]{0,4}:){0,7}[[:xdigit:]]{0,4}$/
95             || $_[0] =~ /^(?:[[:xdigit:]]{0,4}:){1,6}((?:[0-9]{1,3}\.){3}[0-9]{1,3})$/
96             && $is_ipv4->($1))
97             && $_[0] !~ /:::/
98             && $_[0] !~ /^:[^:]/
99             && $_[0] !~ /[^:]:$/
100             && do {
101             my $double_colons = ()= ($_[0] =~ /::/g);
102             my $colon_components = grep length, split(/:+/, $_[0], -1);
103             $double_colons < 2 && ($double_colons > 0
104             || ($colon_components == 8 && !defined $1)
105             || ($colon_components == 7 && defined $1))
106             };
107             },
108             uri => sub {
109             my $uri = Mojo::URL->new($_[0]);
110             fc($uri->to_unsafe_string) eq fc($_[0]) && $uri->is_abs && $_[0] !~ /[^[:ascii:]]/;
111             },
112             'uri-reference' => sub {
113             fc(Mojo::URL->new($_[0])->to_unsafe_string) eq fc($_[0]) && $_[0] !~ /[^[:ascii:]]/;
114             },
115             iri => sub { Mojo::URL->new($_[0])->is_abs },
116             uuid => sub { $_[0] =~ /^[[:xdigit:]]{8}-(?:[[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$/ },
117             'json-pointer' => sub { (!length($_[0]) || $_[0] =~ m{^/}) && $_[0] !~ m{~(?![01])} },
118             'relative-json-pointer' => sub { $_[0] =~ m{^(?:0|[1-9][0-9]*)(?:#$|$|/)} && $_[0] !~ m{~(?![01])} },
119             regex => sub {
120             local $SIG{__WARN__} = sub { die @_ };
121             eval { qr/$_[0]/; 1 ? 1 : 0 };
122             },
123              
124             # TODO: if the metaschema's $vocabulary entry is true, then we must die on
125             # encountering these unimplemented formats.
126             'iri-reference' => sub { 1 },
127             'uri-template' => sub { 1 },
128             };
129              
130             sub _get_default_format_validation {
131 590     590   1357 my ($self, $format) = @_;
132 590         1606 return $formats->{$format};
133             }
134             }
135              
136             sub _traverse_keyword_format {
137 877     877   2129 my ($self, $schema, $state) = @_;
138 877 50       2520 return if not assert_keyword_type($state, $schema, 'string');
139             # TODO: if the metaschema's $vocabulary entry is true, then we must die on
140             # encountering unimplemented formats specified by the vocabulary (iri-reference, uri-template).
141             }
142              
143             sub _eval_keyword_format {
144 902     902   2407 my ($self, $data, $schema, $state) = @_;
145              
146 902 100       2319 if ($state->{validate_formats}) {
147             # first check the subrefs from JSON::Schema::Draft201909->new(format_evaluations => { ... })
148             # and add in the type if needed
149 590         14519 my $evaluator_spec = $state->{evaluator}->_get_format_validation($schema->{format});
150 590         58843 my $default_spec = $self->_get_default_format_validation($schema->{format});
151              
152 590 100       2728 my $spec =
    100          
    100          
153             $evaluator_spec ? ($default_spec ? +{ type => 'string', sub => $evaluator_spec } : $evaluator_spec)
154             : $default_spec ? +{ type => 'string', sub => $default_spec }
155             : undef;
156              
157             return E($state, 'not a%s %s', $schema->{format} =~ /^[aeio]/ ? 'n' : '', $schema->{format})
158 590 100 100     2829 if $spec and is_type($spec->{type}, $data) and not $spec->{sub}->($data);
    100 100        
159             }
160              
161 709         15308 return A($state, $schema->{format});
162             }
163              
164             1;
165              
166             __END__
167              
168             =pod
169              
170             =encoding UTF-8
171              
172             =head1 NAME
173              
174             JSON::Schema::Draft201909::Vocabulary::Format - Implementation of the JSON Schema Draft 2019-09 Format vocabulary
175              
176             =head1 VERSION
177              
178             version 0.028
179              
180             =head1 DESCRIPTION
181              
182             =for Pod::Coverage vocabulary keywords
183              
184             =for stopwords metaschema
185              
186             Implementation of the JSON Schema Draft 2019-09 "Format" vocabulary, indicated in metaschemas
187             with the URI C<https://json-schema.org/draft/2019-09/vocab/format> and formally specified in
188             L<https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.7>.
189              
190             Overrides to particular format implementations, or additions of new ones, can be done through
191             L<JSON::Schema::Draft201909/format_validations>.
192              
193             =head1 SEE ALSO
194              
195             =over 4
196              
197             =item *
198              
199             L<JSON::Schema::Draft201909/Format Validation>
200              
201             =back
202              
203             =head1 SUPPORT
204              
205             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Draft201909/issues>.
206              
207             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
208              
209             =head1 AUTHOR
210              
211             Karen Etheridge <ether@cpan.org>
212              
213             =head1 COPYRIGHT AND LICENCE
214              
215             This software is copyright (c) 2020 by Karen Etheridge.
216              
217             This is free software; you can redistribute it and/or modify it under
218             the same terms as the Perl 5 programming language system itself.
219              
220             =cut