File Coverage

blib/lib/JSON/Schema/Modern/Vocabulary/FormatAssertion.pm
Criterion Covered Total %
statement 70 70 100.0
branch 13 14 92.8
condition 6 6 100.0
subroutine 20 20 100.0
pod 0 3 0.0
total 109 113 96.4


line stmt bran cond sub pod time code
1 11     11   5053 use strict;
  11         31  
  11         418  
2 11     11   89 use warnings;
  11         33  
  11         694  
3             package JSON::Schema::Modern::Vocabulary::FormatAssertion;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Implementation of the JSON Schema Format-Assertion vocabulary
6              
7             our $VERSION = '0.570';
8              
9 11     11   321 use 5.020;
  11         43  
10 11     11   83 use Moo;
  11         30  
  11         109  
11 11     11   5007 use strictures 2;
  11         149  
  11         535  
12 11     11   2475 use stable 0.031 'postderef';
  11         233  
  11         89  
13 11     11   2138 use experimental 'signatures';
  11         50  
  11         58  
14 11     11   1001 use if "$]" >= 5.022, experimental => 're_strict';
  11         28  
  11         140  
15 11     11   1122 no if "$]" >= 5.031009, feature => 'indirect';
  11         29  
  11         123  
16 11     11   658 no if "$]" >= 5.033001, feature => 'multidimensional';
  11         27  
  11         67  
17 11     11   614 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  11         48  
  11         97  
18 11     11   530 use JSON::Schema::Modern::Utilities qw(is_type E A assert_keyword_type abort);
  11         27  
  11         1040  
19 11     11   87 use Feature::Compat::Try;
  11         34  
  11         126  
20 11     11   1630 use namespace::clean;
  11         52  
  11         163  
21              
22             with 'JSON::Schema::Modern::Vocabulary';
23              
24             sub vocabulary {
25 15     15 0 64 'https://json-schema.org/draft/2020-12/vocab/format-assertion' => 'draft2020-12';
26             }
27              
28 10     10 0 51 sub evaluation_order { 2 }
29              
30             sub keywords {
31 12     12 0 162 qw(format);
32             }
33              
34             {
35             # for now, all built-in formats are constrained to the 'string' type
36              
37             my $is_email = sub {
38             Email::Address::XS->parse($_[0])->is_valid;
39             };
40             my $is_hostname = sub {
41             Data::Validate::Domain::is_domain($_[0]);
42             };
43             my $idn_decode = sub {
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://datatracker.ietf.org/doc/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' => sub {
68             # https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6
69             $_[0] =~ m/^\d{4}-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:Z|[+-](\d\d):(\d\d))$/ia
70             && $1 >= 1 && $1 <= 12 # date-month
71             && $2 >= 1 && $2 <= 31 # date-mday
72             && $3 <= 23 # time-hour
73             && $4 <= 59 # time-minute
74             && $5 <= 60 # time-second
75             && (!defined $6 || $6 <= 23) # time-hour in time-numoffset
76             && (!defined $7 || $7 <= 59) # time-minute in time-numoffset
77              
78             # Time::Moment does month+day sanity check (with leap years), but not leap seconds
79             && ($5 <= 59 && eval { Time::Moment->from_string(uc($_[0])) }
80             || do {
81             require DateTime::Format::RFC3339;
82             eval { DateTime::Format::RFC3339->parse_datetime($_[0]) };
83             });
84             },
85             date => sub {
86             # https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6 full-date
87             $_[0] =~ m/^(\d{4})-(\d\d)-(\d\d)$/a
88             && $2 >= 1 && $2 <= 12 # date-month
89             && $3 >= 1 && $3 <= 31 # date-mday
90             && eval { Time::Moment->new(year => $1, month => $2, day => $3) };
91             },
92             time => sub {
93             return if $_[0] !~ /^(\d\d):(\d\d):(\d\d)(?:\.\d+)?([Zz]|([+-])(\d\d):(\d\d))$/a
94             or $1 > 23
95             or $2 > 59
96             or $3 > 60
97             or (defined($6) and $6 > 23)
98             or (defined($7) and $7 > 59);
99              
100             return 1 if $3 <= 59;
101             return $1 == 23 && $2 == 59 if uc($4) eq 'Z';
102              
103             my $sign = $5 eq '+' ? 1 : -1;
104             my $hour_zulu = $1 - $6*$sign;
105             my $min_zulu = $2 - $7*$sign;
106             $hour_zulu -= 1 if $min_zulu < 0;
107              
108             return $hour_zulu%24 == 23 && $min_zulu%60 == 59;
109             },
110             duration => sub { $_[0] =~ $duration_re && $_[0] !~ m{[.,][0-9]+[A-Z].} },
111             email => sub { $is_email->($_[0]) && $_[0] !~ /[^[:ascii:]]/ },
112             'idn-email' => $is_email,
113             hostname => $is_hostname,
114             'idn-hostname' => sub { $is_hostname->($idn_decode->($_[0])) },
115             ipv4 => $is_ipv4,
116             ipv6 => sub {
117             ($_[0] =~ /^(?:[[:xdigit:]]{0,4}:){0,8}[[:xdigit:]]{0,4}$/
118             || $_[0] =~ /^(?:[[:xdigit:]]{0,4}:){1,6}((?:[0-9]{1,3}\.){3}[0-9]{1,3})$/
119             && $is_ipv4->($1))
120             && $_[0] !~ /:::/
121             && $_[0] !~ /^:[^:]/
122             && $_[0] !~ /[^:]:$/
123             && do {
124             my $double_colons = ()= ($_[0] =~ /::/g);
125             my $colon_components = grep length, split(/:+/, $_[0], -1);
126             ($double_colons == 1
127             && ((!defined $1 && $colon_components < 8) || (defined $1 && $colon_components < 7)))
128             ||
129             ($double_colons == 0
130             && ((!defined $1 && $colon_components == 8) || (defined $1 && $colon_components == 7)));
131             };
132             },
133             uri => sub {
134             my $uri = Mojo::URL->new($_[0]);
135             fc($uri->to_unsafe_string) eq fc($_[0]) && $uri->is_abs && $_[0] !~ /[^[:ascii:]]/;
136             },
137             'uri-reference' => sub {
138             fc(Mojo::URL->new($_[0])->to_unsafe_string) eq fc($_[0]) && $_[0] !~ /[^[:ascii:]]/;
139             },
140             iri => sub { Mojo::URL->new($_[0])->is_abs },
141             uuid => sub { $_[0] =~ /^[[:xdigit:]]{8}-(?:[[:xdigit:]]{4}-){3}[[:xdigit:]]{12}$/ },
142             'json-pointer' => sub { (!length($_[0]) || $_[0] =~ m{^/}) && $_[0] !~ m{~(?![01])} },
143             'relative-json-pointer' => sub { $_[0] =~ m{^(?:0|[1-9][0-9]*)(?:#$|$|/)} && $_[0] !~ m{~(?![01])} },
144             regex => sub {
145             local $SIG{__WARN__} = sub { die @_ };
146             eval { qr/$_[0]/; 1 ? 1 : 0 };
147             },
148              
149             'iri-reference' => sub { 1 },
150             'uri-template' => sub { 1 },
151             };
152              
153 332     332   636 sub _get_default_format_validation ($self, $format) {
  332         636  
  332         569  
  332         596  
154 332         813 return $formats->{$format};
155             }
156             }
157              
158 18     18   32 sub _traverse_keyword_format ($self, $schema, $state) {
  18         33  
  18         31  
  18         33  
  18         27  
159 18 50       72 return if not assert_keyword_type($state, $schema, 'string');
160 18         57 return 1;
161             }
162              
163 365     365   653 sub _eval_keyword_format ($self, $data, $schema, $state) {
  365         705  
  365         715  
  365         624  
  365         592  
  365         566  
164             abort($state, 'unimplemented format "%s"', $schema->{format})
165 365 100       1105 if $schema->{format} eq 'uri-template';
166              
167             try {
168             if ($schema->{format} eq 'date-time' or $schema->{format} eq 'date') {
169             require Time::Moment;
170             }
171             elsif ($schema->{format} eq 'email' or $schema->{format} eq 'idn-email') {
172             require Email::Address::XS; Email::Address::XS->VERSION(1.04);
173             }
174             elsif ($schema->{format} eq 'hostname' or $schema->{format} eq 'idn-hostname') {
175             require Data::Validate::Domain;
176             }
177             elsif ($schema->{format} eq 'idn-hostname') {
178             require Net::IDN::Encode;
179             }
180             }
181 363         998 catch ($e) {
182             abort($state, 'EXCEPTION: cannot validate format "%s": %s', $schema->{format}, $e);
183             }
184              
185             # first check the subrefs from JSON::Schema::Modern->new(format_evaluations => { ... })
186             # and add in the type if needed
187 332         8167 my $evaluator_spec = $state->{evaluator}->_get_format_validation($schema->{format});
188 332         32643 my $default_spec = $self->_get_default_format_validation($schema->{format});
189              
190 332 100       1514 my $spec =
    100          
    100          
191             $evaluator_spec ? ($default_spec ? +{ type => 'string', sub => $evaluator_spec } : $evaluator_spec)
192             : $default_spec ? +{ type => 'string', sub => $default_spec }
193             : undef;
194              
195 332         1426 A($state, $schema->{format});
196             return E($state, 'not a%s %s', $schema->{format} =~ /^[aeio]/ ? 'n' : '', $schema->{format})
197 332 100 100     1321 if $spec and is_type($spec->{type}, $data) and not $spec->{sub}->($data);
    100 100        
198              
199 181         975 return 1;
200             }
201              
202             1;
203              
204             __END__
205              
206             =pod
207              
208             =encoding UTF-8
209              
210             =head1 NAME
211              
212             JSON::Schema::Modern::Vocabulary::FormatAssertion - Implementation of the JSON Schema Format-Assertion vocabulary
213              
214             =head1 VERSION
215              
216             version 0.570
217              
218             =head1 DESCRIPTION
219              
220             =for Pod::Coverage vocabulary evaluation_order keywords
221              
222             =for stopwords metaschema
223              
224             Implementation of the JSON Schema Draft 2020-12 "Format-Assertion" vocabulary, indicated in metaschemas
225             with the URI C<https://json-schema.org/draft/2020-12/vocab/format-assertion> and formally specified in
226             L<https://json-schema.org/draft/2020-12/json-schema-validation.html#section-7>.
227              
228             Support is also provided for
229              
230             =over 4
231              
232             =item *
233              
234             the equivalent Draft 2019-09 keyword, indicated in metaschemas with the URI C<https://json-schema.org/draft/2019-09/vocab/format> and formally specified in L<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-7>.
235              
236             =item *
237              
238             the equivalent Draft 7 keyword, as formally specified in L<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-7>.
239              
240             =back
241              
242             Assertion behaviour can be enabled by
243             L<https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.8.1.2/referencing this vocabulary explicitly>
244             in a metaschema's C<$vocabulary> keyword, or by toggling the
245             L<JSON::Schema::Modern/validate_formats> option.
246              
247             Overrides to particular format implementations, or additions of new ones, can be done through
248             L<JSON::Schema::Modern/format_validations>.
249              
250             Format C<uri-template> is not yet implemented.
251             Use of this format will always result in an error.
252              
253             =head1 SEE ALSO
254              
255             =over 4
256              
257             =item *
258              
259             L<JSON::Schema::Modern/Format Validation>
260              
261             =back
262              
263             =for stopwords OpenAPI
264              
265             =head1 SUPPORT
266              
267             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Modern/issues>.
268              
269             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
270              
271             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack
272             server|https://open-api.slack.com>, which are also great resources for finding help.
273              
274             =head1 AUTHOR
275              
276             Karen Etheridge <ether@cpan.org>
277              
278             =head1 COPYRIGHT AND LICENCE
279              
280             This software is copyright (c) 2020 by Karen Etheridge.
281              
282             This is free software; you can redistribute it and/or modify it under
283             the same terms as the Perl 5 programming language system itself.
284              
285             =cut