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.1';
5 11     11   215764 use 5.14.0;
  11         31  
6              
7 11     11   46 use strict;
  11         15  
  11         208  
8 11     11   34 use warnings;
  11         10  
  11         292  
9              
10 11     11   4265 use Type::Tiny;
  11         100002  
  11         373  
11 11     11   5406 use Type::Tiny::Class;
  11         27695  
  11         367  
12 11     11   60 use Scalar::Util qw/ looks_like_number /;
  11         15  
  11         540  
13 11     11   44 use List::Util qw/ reduce pairmap pairs /;
  11         13  
  11         873  
14 11     11   2654 use List::MoreUtils qw/ any all none uniq zip /;
  11         40248  
  11         86  
15 11     11   10489 use Types::Standard qw/InstanceOf HashRef StrictNum Any Str ArrayRef Int Object slurpy Dict Optional slurpy /;
  11         302863  
  11         137  
16 11     11   17957 use Type::Utils;
  11         28213  
  11         81  
17 11     11   14575 use LWP::Simple;
  11         537890  
  11         82  
18 11     11   7242 use Clone 'clone';
  11         21166  
  11         595  
19 11     11   64 use URI;
  11         15  
  11         237  
20 11     11   4541 use Class::Load qw/ load_class /;
  11         113571  
  11         657  
21              
22 11     11   4918 use Moose::Util qw/ apply_all_roles /;
  11         902002  
  11         108  
23              
24 11     11   6825 use JSON;
  11         45443  
  11         69  
25              
26 11     11   8040 use Moose;
  11         2497895  
  11         74  
27              
28 11     11   61271 use MooseX::MungeHas 'is_ro';
  11         18931  
  11         76  
29 11     11   12864 use MooseX::ClassAttribute;
  11         713697  
  11         39  
30              
31 11     11   2359120 no warnings 'uninitialized';
  11         20  
  11         18056  
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 225     225 1 544 my( $self, $url ) = @_;
110              
111 225 50       970 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 225         1555 $url = URI->new($url);
122 225         57738 $url->path( $url->path =~ y#/#/#sr );
123 225         7807 $url = $url->canonical;
124              
125 225 100       26023 if ( my $schema = $self->registered_schema($url) ) {
126 223         2150 return $schema;
127             }
128              
129 2         14 my $schema = eval { from_json LWP::Simple::get($url) };
  2         14  
130              
131 2         298710 $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         27 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 71 my $self = shift;
169 6         134 $self->spec->validate($self->schema);
170             }
171              
172             sub validate_explain_schema {
173 2     2 1 44 my $self = shift;
174 2         41 $self->spec->validate_explain($self->schema);
175             }
176              
177             sub root_schema {
178 3290     3290 1 8590 my $self = shift;
179 3290 100       2511 eval { $self->parent_schema->root_schema } || $self;
  3290         68591  
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 2314     2314 0 15093 my( $self, $subschema ) = @_;
189 2314         6572 $self->new( schema => $subschema, parent_schema => $self );
190             }
191              
192             sub absolute_id {
193 219     219 0 357 my( $self, $new_id ) = @_;
194              
195 219 100       1008 return $new_id if $new_id =~ m#://#; # looks absolute to me
196              
197 12         27 my $base = $self->ancestor_uri;
198              
199 12         106 $base =~ s#[^/]+$##;
200              
201 12         31 return $base . $new_id;
202             }
203              
204             sub _build_type {
205 2554     2554   71405 my $self = shift;
206              
207 2554         56406 $self->_set_type('');
208              
209             my @types =
210 3709 50       54072 grep { $_ and $_->name ne 'Any' }
211 2554         12032 map { $self->_process_keyword($_) }
  54500         799695  
212             $self->all_keywords;
213              
214 2554 100   1238   80152 return @types ? reduce { $a & $b } @types : Any
  1238         60648  
215             }
216              
217             sub all_keywords {
218 1958     1958 0 6036 my $self = shift;
219              
220 1958         6218 return sort map { /^_keyword_(.*)/ } $self->meta->get_method_list;
  57851         190369  
221             }
222              
223             sub _process_keyword {
224 54500     54500   48686 my( $self, $keyword ) = @_;
225              
226 54500 100       975232 return unless exists $self->schema->{$keyword};
227              
228 3987         85402 my $value = $self->schema->{$keyword};
229              
230 3987         17156 my $method = "_keyword_$keyword";
231              
232 3987         14101 $self->$method($value);
233             }
234              
235             # returns the first defined parent uri
236             sub ancestor_uri {
237 535     535 0 1644 my $self = shift;
238            
239 535   100     10930 return $self->uri || eval{ $self->parent_schema->ancestor_uri };
240             }
241              
242              
243             sub resolve_reference {
244 1524     1524 1 2019 my( $self, $ref ) = @_;
245              
246 1524 100       2852 $ref = join '/', '#', map { $self->_escape_ref($_) } @$ref
  6         14  
247             if ref $ref;
248              
249 1524 100       4915 if ( $ref =~ s/^([^#]+)// ) {
250 208         433 my $base = $1;
251 208 100       746 unless( $base =~ m#://# ) {
252 186         657 my $base_uri = $self->ancestor_uri;
253 186         1903 $base_uri =~ s#[^/]+$##;
254 186         378 $base = $base_uri . $base;
255             }
256 208         994 return $self->fetch($base)->resolve_reference($ref);
257             }
258              
259 1316         2776 $self = $self->root_schema;
260 1316 100       19248 return $self if $ref eq '#';
261            
262 655         1770 $ref =~ s/^#//;
263              
264             # return $self->references->{$ref} if $self->references->{$ref};
265              
266 655         13114 my $s = $self->schema;
267              
268 655         5066 my @refs = map { $self->_unescape_ref($_) } grep { length $_ } split '/', $ref;
  913         1457  
  1383         1510  
269              
270 655         1806 while( @refs ) {
271 907         996 my $ref = shift @refs;
272 907         1281 my $is_array = ref $s eq 'ARRAY';
273              
274 907 100       2473 $s = $is_array ? $s->[$ref] : $s->{$ref} or last;
    100          
275              
276 904 100       1487 if( ref $s eq 'HASH' ) {
277 891 100 66     3792 if( my $local_id = $s->{id} || $s->{'$id'} ) {
278 6         13 my $id = $self->absolute_id($local_id);
279 6         14 $self = $self->fetch( $self->absolute_id($id) );
280            
281 6         14 return $self->resolve_reference(\@refs);
282             }
283             }
284              
285             }
286              
287             return (
288 649 50 66     2713 ( 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 913     913   958 my( $self, $ref ) = @_;
296              
297 913         1039 $ref =~ s/~0/~/g;
298 913         791 $ref =~ s!~1!/!g;
299 913         855 $ref =~ s!%25!%!g;
300              
301 913         1623 $ref;
302             }
303              
304             sub _escape_ref {
305 6     6   6 my( $self, $ref ) = @_;
306              
307 6         5 $ref =~ s/~/~0/g;
308 6         6 $ref =~ s!/!~1!g;
309 6         5 $ref =~ s!%!%25!g;
310              
311 6         11 $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 2563     2563 0 3084418 my $self = shift;
335             # TODO rename specification to draft_version
336             # and have specifications renamed to spec
337 2563         66522 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 2563 100       7372358 $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.1
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