File Coverage

blib/lib/JSON/Schema/AsType.pm
Criterion Covered Total %
statement 138 156 88.4
branch 33 44 75.0
condition 8 9 88.8
subroutine 34 39 87.1
pod 7 12 58.3
total 220 260 84.6


line stmt bran cond sub pod time code
1             package JSON::Schema::AsType;
2             our $AUTHORITY = 'cpan:YANICK';
3             # ABSTRACT: generates Type::Tiny types out of JSON schemas
4             $JSON::Schema::AsType::VERSION = '0.4.3';
5 11     11   192139 use 5.14.0;
  11         60  
6              
7 11     11   56 use strict;
  11         24  
  11         1750  
8 11     11   58 use warnings;
  11         26  
  11         255  
9              
10 11     11   3957 use Type::Tiny;
  11         107770  
  11         361  
11 11     11   5339 use Type::Tiny::Class;
  11         28965  
  11         420  
12 11     11   85 use Scalar::Util qw/ looks_like_number /;
  11         49  
  11         604  
13 11     11   71 use List::Util qw/ reduce pairmap pairs /;
  11         28  
  11         928  
14 11     11   3230 use List::MoreUtils qw/ any all none uniq zip /;
  11         52418  
  11         103  
15 11     11   14774 use Types::Standard qw/InstanceOf HashRef StrictNum Any Str ArrayRef Int Object slurpy Dict Optional slurpy /;
  11         377452  
  11         199  
16 11     11   26753 use Type::Utils;
  11         33138  
  11         94  
17 11     11   21182 use LWP::Simple;
  11         578363  
  11         105  
18 11     11   7856 use Clone 'clone';
  11         23609  
  11         675  
19 11     11   96 use URI;
  11         26  
  11         260  
20              
21 11     11   4904 use Moose::Util qw/ apply_all_roles /;
  11         1241408  
  11         130  
22              
23 11     11   6892 use JSON;
  11         40708  
  11         77  
24              
25 11     11   7217 use Moose;
  11         3684679  
  11         83  
26              
27 11     11   96009 use MooseX::MungeHas 'is_ro';
  11         21271  
  11         112  
28 11     11   14045 use MooseX::ClassAttribute;
  11         959980  
  11         60  
29              
30 11     11   3295610 no warnings 'uninitialized';
  11         31  
  11         20221  
31              
32             our $strict_string = 1;
33              
34             class_has schema_registry => (
35             is => 'ro',
36             lazy => 1,
37             default => sub { +{} },
38             traits => [ 'Hash' ],
39             handles => {
40             all_schemas => 'elements',
41             all_schema_uris => 'keys',
42             registered_schema => 'get',
43             register_schema => 'set',
44             },
45             );
46              
47             around register_schema => sub {
48             # TODO Use a type instead to coerce into canonical
49             my( $orig, $self, $uri, $schema ) = @_;
50             $uri =~ s/#$//;
51             $orig->($self,$uri,$schema);
52             };
53              
54             has type => (
55             is => 'rwp',
56             handles => [ qw/ check validate validate_explain / ],
57             builder => 1,
58             lazy => 1
59             );
60              
61             has draft_version => (
62             is => 'ro',
63             lazy => 1,
64             default => sub {
65             $_[0]->has_specification ? $_[0]->specification =~ /(\d+)/ && $1
66             : eval { $_[0]->parent_schema->draft_version } || 4;
67             },
68             isa => enum([ 3, 4, 6 ]),
69             );
70              
71             has spec => (
72             is => 'ro',
73             lazy => 1,
74             default => sub {
75             $_[0]->fetch( sprintf "http://json-schema.org/draft-%02d/schema", $_[0]->draft_version );
76             },
77             );
78              
79             has schema => (
80             predicate => 'has_schema',
81             lazy => 1,
82             default => sub {
83             my $self = shift;
84            
85             my $uri = $self->uri or die "schema or uri required";
86              
87             return $self->fetch($uri)->schema;
88             },
89             );
90              
91             has parent_schema => (
92             clearer => 1,
93             );
94              
95             has strict_string => (
96             is => 'ro',
97             lazy => 1,
98             default => sub {
99             my $self = shift;
100              
101             $self->parent_schema->strict_string if $self->parent_schema;
102              
103             return $JSON::Schema::AsType::strict_string;
104             },
105             );
106              
107             sub fetch {
108 447     447 1 1631 my( $self, $url ) = @_;
109              
110 447 50       2435 unless ( $url =~ m#^\w+://# ) { # doesn't look like an uri
111 0         0 my $id =$self->uri;
112 0         0 $id =~ s#[^/]*$##;
113 0         0 $url = $id . $url;
114             # such that the 'id's can cascade
115 0 0       0 if ( my $p = $self->parent_schema ) {
116 0         0 return $p->fetch( $url );
117             }
118             }
119              
120 447         3210 $url = URI->new($url);
121 447         91355 $url->path( $url->path =~ y#/#/#sr );
122 447         23137 $url = $url->canonical;
123              
124 447 100       74226 if ( my $schema = $self->registered_schema($url) ) {
125 445 50       15081 return $schema if $schema->has_schema;
126             }
127              
128 2         19 my $schema = eval { from_json LWP::Simple::get($url) };
  2         16  
129              
130 2 50       292936 die "couldn't get schema from '$url'\n" unless ref $schema eq 'HASH';
131              
132 2         34 return $self->register_schema( $url => $self->new( uri => $url, schema => $schema ) );
133             }
134              
135             has uri => (
136             is => 'rw',
137             trigger => sub {
138             my( $self, $uri ) = @_;
139             $self->register_schema($uri,$self);
140             $self->clear_parent_schema;
141             } );
142              
143             has references => sub {
144             +{}
145 0     0   0 };
146              
147             has specification => (
148             predicate => 1,
149             is => 'ro',
150             lazy => 1,
151             default => sub {
152             return 'draft'.$_[0]->draft_version;
153             eval { $_[0]->parent_schema->specification } || 'draft4' },
154             isa => enum 'JsonSchemaSpecification', [ qw/ draft3 draft4 draft6 / ],
155             );
156              
157             sub specification_schema {
158 0     0 1 0 my $self = shift;
159              
160 0         0 $self->spec->schema;
161             }
162              
163             sub validate_schema {
164 6     6 1 86 my $self = shift;
165 6         157 $self->spec->validate($self->schema);
166             }
167              
168             sub validate_explain_schema {
169 2     2 1 59 my $self = shift;
170 2         45 $self->spec->validate_explain($self->schema);
171             }
172              
173             sub root_schema {
174 3593     3593 1 16809 my $self = shift;
175 3593 100       5745 eval { $self->parent_schema->root_schema } || $self;
  3593         78525  
176             }
177              
178             sub is_root_schema {
179 0     0 1 0 my $self = shift;
180 0         0 return not $self->parent_schema;
181             }
182              
183             sub sub_schema {
184 3069     3069 0 26804 my( $self, $subschema ) = @_;
185 3069         14159 $self->new( schema => $subschema, parent_schema => $self );
186             }
187              
188             sub absolute_id {
189 428     428 0 1420 my( $self, $new_id ) = @_;
190              
191 428 100       2750 return $new_id if $new_id =~ m#://#; # looks absolute to me
192              
193 25         83 my $base = $self->ancestor_uri;
194              
195 25         337 $base =~ s#[^/]+$##;
196              
197 25         125 return $base . $new_id;
198             }
199              
200             sub _build_type {
201 3357     3357   111990 my $self = shift;
202              
203 3357         85349 $self->_set_type('');
204              
205             my @types =
206 4911 50       114604 grep { $_ and $_->name ne 'Any' }
207 3357         26514 map { $self->_process_keyword($_) }
  73945         2067281  
208             $self->all_keywords;
209              
210 3357 100   1653   135073 return @types ? reduce { $a & $b } @types : Any
  1653         111946  
211             }
212              
213             sub all_keywords {
214 2552     2552 0 14705 my $self = shift;
215              
216 2552         12542 return sort map { /^_keyword_(.*)/ } $self->meta->get_method_list;
  78808         425401  
217             }
218              
219             sub _process_keyword {
220 73945     73945   141519 my( $self, $keyword ) = @_;
221              
222 73945 100       1447240 return unless exists $self->schema->{$keyword};
223              
224 5404         145715 my $value = $self->schema->{$keyword};
225              
226 5404         46816 my $method = "_keyword_$keyword";
227              
228 5404         27298 $self->$method($value);
229             }
230              
231             # returns the first defined parent uri
232             sub ancestor_uri {
233 1092     1092 0 6723 my $self = shift;
234            
235 1092   100     25094 return $self->uri || eval{ $self->parent_schema->ancestor_uri };
236             }
237              
238              
239             sub resolve_reference {
240 1990     1990 1 8994 my( $self, $ref ) = @_;
241              
242 1990 100       5474 $ref = join '/', '#', map { $self->_escape_ref($_) } @$ref
  16         55  
243             if ref $ref;
244              
245 1990 100       8908 if ( $ref =~ s/^([^#]+)// ) {
246 420         1616 my $base = $1;
247 420 100       2040 unless( $base =~ m#://# ) {
248 382         2111 my $base_uri = $self->ancestor_uri;
249 382         5519 $base_uri =~ s#[^/]+$##;
250 382         1457 $base = $base_uri . $base;
251             }
252 420         2670 return $self->fetch($base)->resolve_reference($ref);
253             }
254              
255 1570         5433 $self = $self->root_schema;
256 1570 100       29958 return $self if $ref eq '#';
257            
258 889         3234 $ref =~ s/^#//;
259              
260             # return $self->references->{$ref} if $self->references->{$ref};
261              
262 889         19995 my $s = $self->schema;
263              
264 889         10745 my @refs = map { $self->_unescape_ref($_) } grep { length $_ } split '/', $ref;
  992         2431  
  1503         2826  
265              
266 889         3144 while( @refs ) {
267 976         2088 my $ref = shift @refs;
268 976         2177 my $is_array = ref $s eq 'ARRAY';
269              
270 976 100       3521 $s = $is_array ? $s->[$ref] : $s->{$ref} or last;
    100          
271              
272 973 100       2525 if( ref $s eq 'HASH' ) {
273 960 100 100     5685 if( my $local_id = $s->{id} || $s->{'$id'} ) {
274 16         60 my $id = $self->absolute_id($local_id);
275 16         48 $self = $self->fetch( $self->absolute_id($id) );
276            
277 16         145 return $self->resolve_reference(\@refs);
278             }
279             }
280              
281             }
282              
283             return (
284 873 50 66     4980 ( ref $s eq 'HASH' or ref $s eq 'JSON::PP::Boolean' )
285             ? $self->sub_schema($s)
286             : Any );
287              
288             }
289              
290             sub _unescape_ref {
291 992     992   2074 my( $self, $ref ) = @_;
292              
293 992         2394 $ref =~ s/~0/~/g;
294 992         1792 $ref =~ s!~1!/!g;
295 992         1748 $ref =~ s!%25!%!g;
296              
297 992         2635 $ref;
298             }
299              
300             sub _escape_ref {
301 16     16   38 my( $self, $ref ) = @_;
302              
303 16         31 $ref =~ s/~/~0/g;
304 16         31 $ref =~ s!/!~1!g;
305 16         26 $ref =~ s!%!%25!g;
306              
307 16         43 $ref;
308             }
309              
310             sub _add_reference {
311 0     0   0 my( $self, $path, $schema ) = @_;
312              
313 0 0       0 $path = join '/', '#', map { $self->_escape_ref($_) } @$path
  0         0  
314             if ref $path;
315              
316 0         0 $self->references->{$path} = $schema;
317             }
318              
319             sub _add_to_type {
320 0     0   0 my( $self, $t ) = @_;
321              
322 0 0       0 if( my $already = $self->type ) {
323 0         0 $t = $already & $t;
324             }
325              
326 0         0 $self->_set_type( $t );
327             }
328              
329             sub BUILD {
330 3404     3404 0 6628135 my $self = shift;
331             # TODO rename specification to draft_version
332             # and have specifications renamed to spec
333 3404         100767 apply_all_roles( $self, 'JSON::Schema::AsType::' . ucfirst $self->specification );
334              
335             # TODO move the role into a trait, which should take care of this
336 3404 100       16847336 $self->type if $self->has_schema;
337             }
338              
339             1;
340              
341             __END__
342              
343             =pod
344              
345             =encoding UTF-8
346              
347             =head1 NAME
348              
349             JSON::Schema::AsType - generates Type::Tiny types out of JSON schemas
350              
351             =head1 VERSION
352              
353             version 0.4.3
354              
355             =head1 SYNOPSIS
356              
357             use JSON::Schema::AsType;
358              
359             my $schema = JSON::Schema::AsType->new( schema => {
360             properties => {
361             foo => { type => 'integer' },
362             bar => { type => 'object' },
363             },
364             });
365              
366             print 'valid' if $schema->check({ foo => 1, bar => { two => 2 } }); # prints 'valid'
367              
368             print $schema->validate_explain({ foo => 'potato', bar => { two => 2 } });
369              
370             =head1 DESCRIPTION
371              
372             This module takes in a JSON Schema (L<http://json-schema.org/>) and turns it into a
373             L<Type::Tiny> type.
374              
375             =head2 Strings and Numbers
376              
377             By default, C<JSON::Schema::AsType> follows the
378             JSON schema specs and distinguish between strings and
379             numbers.
380              
381             value String? Number?
382             "a" yes no
383             1 no yes
384             "1" yes no
385              
386             If you want the usual Perl
387             behavior and considers the JSON schema type C<String>
388             to be a superset of C<Number>. That is:
389              
390             value String? Number?
391             "a" yes no
392             1 yes yes
393             "1" yes yes
394              
395             Then you can set the object's attribute C<strict_string> to C<0>.
396             Setting the global variable C<$JSON::Schema::AsType::strict_string> to C<0>
397             will work too, but that's deprecated and will eventually go away.
398              
399             =head1 METHODS
400              
401             =head2 new( %args )
402              
403             my $schema = JSON::Schema::AsType->new( schema => $my_schema );
404              
405             The class constructor. Accepts the following arguments.
406              
407             =over
408              
409             =item schema => \%schema
410              
411             The JSON schema to compile, as a hashref.
412              
413             If not given, will be retrieved from C<uri>.
414              
415             An error will be thrown is neither C<schema> nor C<uri> is given.
416              
417             =item uri => $uri
418              
419             Optional uri associated with the schema.
420              
421             If provided, the schema will also
422             be added to a schema cache. There is currently no way to prevent this.
423             If this is an issue for you, you can manipulate the cache by accessing
424             C<%JSON::Schema::AsType::EXTERNAL_SCHEMAS> directly.
425              
426             =item draft_version => $version
427              
428             The version of the JSON-Schema specification to use. Accepts C<3> or C<4>,
429             defaults to '4'.
430              
431             =back
432              
433             =head2 type
434              
435             Returns the compiled L<Type::Tiny> type.
436              
437             =head2 check( $struct )
438              
439             Returns C<true> if C<$struct> is valid as per the schema.
440              
441             =head2 validate( $struct )
442              
443             Returns a short explanation if C<$struct> didn't validate, nothing otherwise.
444              
445             =head2 validate_explain( $struct )
446              
447             Returns a log explanation if C<$struct> didn't validate, nothing otherwise.
448              
449             =head2 validate_schema
450              
451             Like C<validate>, but validates the schema itself against its specification.
452              
453             print $schema->validate_schema;
454              
455             # equivalent to
456              
457             print $schema->specification_schema->validate($schema);
458              
459             =head2 validate_explain_schema
460              
461             Like C<validate_explain>, but validates the schema itself against its specification.
462              
463             =head2 draft_version
464              
465             Returns the draft version used by the object.
466              
467             =head2 spec
468              
469             Returns the L<JSON::Schema::AsType> object associated with the
470             specs of this object's schema.
471              
472             I.e., if the current object is a draft4 schema, C<spec> will
473             return the schema definining draft4.
474              
475             =head2 schema
476              
477             Returns the JSON schema, as a hashref.
478              
479             =head2 parent_schema
480              
481             Returns the L<JSON::Schema::AsType> object for the parent schema, or
482             C<undef> is the current schema is the top-level one.
483              
484             =head2 fetch( $url )
485              
486             Fetches the schema at the given C<$url>. If already present, it will use the schema in
487             the cache. If not, the newly fetched schema will be added to the cache.
488              
489             =head2 uri
490              
491             Returns the uri associated with the schema, if any.
492              
493             =head2 specification
494              
495             Returns the JSON Schema specification used by the object.
496              
497             =head2 specification_schema
498              
499             Returns the L<JSON::Schema::AsType> object representing the schema of
500             the current object's specification.
501              
502             =head2 root_schema
503              
504             Returns the top-level schema including this schema.
505              
506             =head2 is_root_schema
507              
508             Returns C<true> if this schema is a top-level
509             schema.
510              
511             =head2 resolve_reference( $ref )
512              
513             my $sub_schema = $schema->resolve_reference( '#/properties/foo' );
514              
515             print $sub_schema->check( $struct );
516              
517             Returns the L<JSON::Schema::AsType> object associated with the
518             type referenced by C<$ref>.
519              
520             =head1 SEE ALSO
521              
522             =over
523              
524             =item L<JSON::Schema>
525              
526             =item L<JSV>
527              
528             =back
529              
530             =head1 AUTHOR
531              
532             Yanick Champoux <yanick@babyl.dyndns.org>
533              
534             =head1 COPYRIGHT AND LICENSE
535              
536             This software is copyright (c) 2017, 2015 by Yanick Champoux.
537              
538             This is free software; you can redistribute it and/or modify it under
539             the same terms as the Perl 5 programming language system itself.
540              
541             =cut