File Coverage

blib/lib/Data/JSONSchema/Ajv.pm
Criterion Covered Total %
statement 89 89 100.0
branch 25 28 89.2
condition 10 13 76.9
subroutine 16 16 100.0
pod 3 3 100.0
total 143 149 95.9


line stmt bran cond sub pod time code
1 2     2   95276 use strict;
  2         4  
  2         53  
2 2     2   10 use warnings;
  2         4  
  2         53  
3              
4 2     2   824 use JavaScript::Duktape::XS;
  2         17261  
  2         93  
5              
6 2     2   1582 use Data::JSONSchema::Ajv::src;
  2         9  
  2         67  
7 2     2   771 use Data::JSONSchema::Ajv::src::04;
  2         9  
  2         62  
8 2     2   743 use Data::JSONSchema::Ajv::src::06;
  2         8  
  2         115  
9              
10             =head1 NAME
11              
12             Data::JSONSchema::Ajv - JSON Schema Validator wrapping Ajv
13              
14             =head1 VERSION
15              
16             version 0.07
17              
18             =head1 DESCRIPTION
19              
20             JSON Schema Validator wrapping Ajv
21              
22             =head1 SYNOPSIS
23              
24             use Test::More;
25             use Data::JSONSchema::Ajv;
26              
27             my $ajv_options = {};
28             my $my_options = {
29             draft => '04', # Defaults to '07'
30             };
31              
32             my $ajv = Data::JSONSchema::Ajv->new($ajv_options, $my_options);
33              
34             my $validator = $ajv->make_validator(
35             { # http://json-schema.org/examples.html
36             title => "Example Schema",
37             type => "object",
38             properties => {
39             firstName => { type => "string" },
40             lastName => { type => "string" },
41             age => {
42             description => "Age in years",
43             type => "integer",
44             minimum => 0
45             }
46             },
47             required => [ "firstName", "lastName" ],
48             }
49             );
50              
51             my $payload = { firstName => 'Valentina', familyName => 'Tereshkova' };
52              
53             my $result = $validator->validate($payload);
54              
55             if ($result) {
56             is_deeply(
57             $result,
58             [ { dataPath => "",
59             keyword => "required",
60             message => "should have required property 'lastName'",
61             params => { missingProperty => "lastName" },
62             schemaPath => "#/required"
63             }
64             ],
65             "Expected errors thrown"
66             );
67             } else {
68             fail(
69             "validate() returned a false value, which means the example " .
70             "unexpectedly validated"
71             );
72             }
73              
74             =head1 WHAT WHY
75              
76             This module is an offensively light-weight wrapper
77             L.
78              
79             Light-weight in this context just means it's not very many lines of actual Perl
80              
81             =head1 METHODS
82              
83             =head2 new
84              
85             my $ajv = Data::JSONSchema::Ajv->new(
86             { v5 => $JSON::PP::true }, # Ajv options. Try: {},
87             {}, # Module options. See below
88             );
89              
90             Instantiates a new L environment and loads C into it.
91             Accepts two hashrefs (or undefs). The first is passed straight through to
92             C, whose options are documented L.
93              
94             The second one allows you to specify options of this module:
95              
96             =over
97              
98             =item ajv_src
99              
100             String that contains source code of standalone version of Ajv library, which can be
101             found L. This module already has some version
102             (possibly not last) of Ajv hardcoded inside it and will use it if this option doesn't
103             specified.
104              
105             =item draft
106              
107             A JSON Schema draft version as a string. Allowable values are C<04>, C<06>, and C<07>.
108             No support for multiple schemas at this time. Default is C<07>.
109              
110             =back
111              
112             =head2 make_validator
113              
114             my $validator = $ajv->make_validator( $hashref_schema OR $json_string );
115              
116             Compiles your schema using C and return a
117             C object, documented immediately below.
118              
119             =head2 duktape
120              
121             Need to do something else, and something magic? This is a read-only accessor
122             to the Duktape env.
123              
124             =head1 Data::JSONSchema::Ajv::Validator
125              
126             Single method object:
127              
128             =head2 validate
129              
130             my $errors = $validator->validate( $data_structure );
131             my $errors = $validator->validate( \$data_structure );
132              
133             Validate a data-structure against the schema. Whith some options specified
134             (like C, C) Ajv may modify input data. If you
135             want to receive this modifications you need to pass your data structure by
136             reference: for example if you normally pass hashref or arrayref in this case
137             you need to pass reference to hashref or reference to arrayref. If you are
138             validating just a simple scalar like string or number you can also pass it
139             by reference, but you'll not get any data modifications because on Ajv's side
140             they are passed by value and can't be modified. You can try to wrap such
141             scalars in array with one element and modify schema a little to pass this
142             limitation. Returns C on success, and a data-structure complaining
143             on failure. The data-structure is whatever C produces - you can either
144             read its documentation, or be lazy and simply L it.
145              
146             =head1 BOOLEANS AND UNDEFINED/NULL
147              
148             Perl has no special Boolean types. JSON (and indeed JavaScript) does. If you're
149             really brave, you can pass in a C option to replace the underlying
150             L at instantiation.
151              
152             =head1 SEE ALSO
153              
154             This module was written because I couldn't get any of the other JSON Schema
155             validators to work.
156              
157             Toby Inkster wrote L, which I had high hopes for, because he's a
158             smart cookie, but it seems like it's very out of date compared to modern
159             schemas.
160              
161             I was unable to get L to fail validation for any schema I gave
162             it. That's probably due to having miswritten my schemas, but it didn't give me
163             any hints, and I did get some errors along the lines of
164             L and so I gave up. I also find it
165             mildly offensive that (the most excellent) L is a dependency for
166             a JSON tool. Additionally it doesn't validate the schemas themselves, and I'm
167             too stupid to use a tool like that.
168              
169             L provides some schema tests. This passes all
170             of thems except the ones that require going and downloading external schemas.
171              
172             =head1 AUTHOR
173              
174             All the hard work was done by the guy who wrote Ajv,
175             L.
176              
177             This Perl wrapper written by Peter Sergeant.
178              
179             =cut
180              
181             package Data::JSONSchema::Ajv {
182 2     2   12 use Carp qw/croak/;
  2         4  
  2         100  
183 2     2   1083 use Storable qw/dclone/;
  2         5434  
  2         121  
184 2     2   953 use Cpanel::JSON::XS qw/decode_json/;
  2         3420  
  2         1216  
185              
186             sub new {
187 14     14 1 52733 my ( $class, $ajv_options, $my_options ) = @_;
188              
189             $ajv_options = {
190             logger => $Cpanel::JSON::XS::false,
191 14 100       50 %{ $ajv_options || {} }
  14         100  
192             };
193              
194 14   100     59 $my_options ||= {};
195 14   100     68 my $draft_version = delete $my_options->{'draft'} // '07';
196 14         32 my $ajv_src = delete $my_options->{ajv_src};
197 14   33     185 my $json_obj = delete $my_options->{'json'}
198             // Cpanel::JSON::XS->new->ascii->allow_nonref;
199 14 100       55 if ( keys %$my_options ) {
200 1         213 croak( "Unknown options: " . ( join ', ', keys %$my_options ) );
201             }
202              
203 13         16368 my $js = JavaScript::Duktape::XS->new();
204 13   66     2100330 $js->eval($ajv_src || $Data::JSONSchema::Ajv::src::src);
205              
206             # Setup appropriately for different version of the schema
207 13 100       285 if ( $draft_version eq '04' ) {
    100          
    100          
208             warn "Over-riding 'schemaId' as you specified draft-04"
209 2 50       21 if exists $ajv_options->{'schemaId'};
210 2         10 $ajv_options->{'schemaId'} = 'id';
211             warn "Over-riding 'meta' as you specified draft-04"
212 2 50       13 if exists $ajv_options->{'meta'};
213 2         192 $ajv_options->{'meta'}
214             = decode_json($Data::JSONSchema::Ajv::src::04::src);
215             }
216             elsif ( $draft_version eq '06' ) {
217             warn "Over-riding 'meta' as you specified draft-06"
218 2 50       24 if exists $ajv_options->{'meta'};
219 2         280 $ajv_options->{'meta'}
220             = $json_obj->decode($Data::JSONSchema::Ajv::src::06::src);
221             }
222             elsif ( $draft_version ne '07' ) {
223 1         1343 die "Can only accept draft versions: '04', '06', '07'";
224             }
225              
226 12         136 my $self = bless {
227             '_context' => $js,
228             '_counter' => 0,
229             '_json' => $json_obj,
230             }, $class;
231              
232 12         85 $self->_inject_escaped( ajvOptions => $ajv_options );
233              
234 12         295386 $js->eval('var ajv = new Ajv(ajvOptions);');
235 12 100       1832 $js->typeof('ajv') ne 'undefined'
236             or die "Can't create ajv object. Bad `ajv_src' specified?";
237              
238 11         3906 return $self;
239             }
240              
241             sub make_validator {
242 297     297 1 1321698 my ( $self, $schema ) = @_;
243              
244 297         836 my $counter = $self->{'_counter'}++;
245 297         841 my $schema_name = "schemaDef_$counter";
246 297         592 my $validator_name = "validator_$counter";
247              
248 297 100       884 if ( ref $schema ) {
249 295         824 $self->_inject_escaped( $schema_name, $schema );
250 295         776283 $self->{'_context'}
251             ->eval("var $validator_name = ajv.compile($schema_name);");
252             }
253             else {
254 2         171 $self->{'_context'}
255             ->eval("var $validator_name = ajv.compile($schema);");
256             }
257            
258 297 100       3446 $self->{'_context'}->typeof($validator_name) ne 'undefined'
259             or die "Can't compile schema passed";
260              
261 296         4920 return bless [ $self => $validator_name ],
262             'Data::JSONSchema::Ajv::Validator';
263             }
264              
265 1397     1397 1 2583 sub duktape { my $self = shift; return $self->{'_context'}; }
  1397         2487  
266              
267             sub _inject_escaped {
268 1397     1397   2802 my ( $self, $name, $data ) = @_;
269              
270 1397         2671 my $js = $self->duktape;
271              
272             # Change various markers to be magic strings
273 1397         8129 my $data_dump = $self->{'_json'}->encode($data);
274              
275             # Change them back in JS land if needed
276 1397         72307 $js->eval("$name = $data_dump;");
277             }
278              
279             }
280             $Data::JSONSchema::Ajv::VERSION = '0.07';;
281              
282             package Data::JSONSchema::Ajv::Validator {
283             $Data::JSONSchema::Ajv::Validator::VERSION = '0.07';
284 2     2   16 use strict;
  2         3  
  2         63  
285 2     2   12 use warnings;
  2         9  
  2         411  
286              
287             sub validate {
288 1090     1090   1581030 my ( $self, $input ) = @_;
289 1090         2516 my ( $parent, $name ) = @$self;
290 1090         2282 my $js = $parent->{'_context'};
291            
292 1090         1962 my $input_reftype = ref $input;
293              
294 1090         2085 my $data_name = "data_$name";
295 1090 100 100     6772 $parent->_inject_escaped( $data_name, $input_reftype eq 'REF' || $input_reftype eq 'SCALAR' ? $$input : $input );
296              
297 1090         42396 $js->eval("var result = $name($data_name)");
298              
299 1090         6660 my $result = $js->get('result');
300            
301 1090 100       3026 if ( $input_reftype eq 'REF' ) {
302 1         17 $$input = $js->get($data_name);
303             }
304              
305 1090         4134 $js->set( $data_name, undef );
306              
307 1090 100       4648 if ($result) {
308 591         6232 return;
309             }
310             else {
311 499         17025 $js->eval("var errors = $name.errors");
312 499         12372 my $errors = $js->get('errors');
313 499         2708 return $errors;
314             }
315              
316             }
317              
318             };
319              
320             1;