File Coverage

blib/lib/JSON/Schema/AsType.pm
Criterion Covered Total %
statement 142 160 88.7
branch 32 42 76.1
condition 7 9 77.7
subroutine 35 40 87.5
pod 7 12 58.3
total 223 263 84.7


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