File Coverage

blib/lib/Data/Validate/OpenAPI.pm
Criterion Covered Total %
statement 59 84 70.2
branch 22 60 36.6
condition 4 15 26.6
subroutine 12 12 100.0
pod 0 2 0.0
total 97 173 56.0


line stmt bran cond sub pod time code
1             package Data::Validate::OpenAPI;
2              
3 2     2   954 use strict;
  2         13  
  2         54  
4 2     2   10 use warnings;
  2         5  
  2         74  
5              
6             # ABSTRACT: Validate and untaint input parameters via OpenAPI schema
7             our $VERSION = '0.2.0'; # VERSION
8              
9 2     2   838 use OpenAPI::Render;
  2         2009  
  2         63  
10 2     2   839 use parent OpenAPI::Render::;
  2         568  
  2         12  
11              
12 2     2   1076 use Data::Validate qw( is_integer );
  2         138713  
  2         168  
13 2     2   879 use Data::Validate::Email qw( is_email );
  2         79293  
  2         129  
14 2     2   988 use Data::Validate::IP qw( is_ipv4 is_ipv6 );
  2         67149  
  2         168  
15 2     2   967 use Data::Validate::URI qw( is_uri );
  2         1681  
  2         114  
16 2     2   868 use DateTime::Format::RFC3339;
  2         1029215  
  2         94  
17 2     2   25 use Scalar::Util qw( blessed );
  2         6  
  2         1698  
18              
19             # Global variable for reporter subroutine
20             our $reporter;
21              
22             =head1 SYNOPSIS
23              
24             use CGI;
25             use Data::Validate::OpenAPI;
26              
27             my $validator = Data::Validate::OpenAPI->new( $parsed_openapi_json );
28             my $params = $validator->validate( '/', 'post', CGI->new );
29              
30             =head1 DESCRIPTION
31              
32             C<Data::Validate::OpenAPI> validates and untaints CGI parameters using a supplied OpenAPI schema.
33             It applies format-specific validation and untainting using appropriate L<Data::Validate> subclasses, including email, IP, URI and other.
34             Also it checks values against enumerators and patterns, if provided.
35              
36             =head1 SUBROUTINES
37              
38             =method C<new>
39              
40             Takes a parsed OpenAPI schema as returned by L<JSON> module's C<decode_json()>.
41             Returns validator ready to validate CGI parameters.
42              
43             =method C<validate>
44              
45             Takes a call path, HTTP method and a CGI object.
46             Returns a hash of validated pairs of CGI parameter keys and their values.
47             At this point values failing to validate are not reported.
48             Keys for parameters having no valid values are omitted from the returned hash.
49              
50             =cut
51              
52             sub validate
53             {
54 7     7 0 29456 my( $self, $path, $method, $input ) = @_;
55              
56             # FIXME: More specific parameters override less specific ones.
57             # FIXME: Request body parameters should be taken from CGI object
58             # using their own specific methods.
59 7         21 my $api = $self->{api};
60             my @parameters =
61 7         37 grep { $_->{in} eq 'query' }
62             exists $api->{paths}{$path}{parameters}
63 0         0 ? @{$api->{paths}{$path}{parameters}} : (),
64             exists $api->{paths}{$path}{$method}{parameters}
65 7         40 ? @{$api->{paths}{$path}{$method}{parameters}} : (),
66             exists $api->{paths}{$path}{$method}{requestBody}
67 7 50       36 ? OpenAPI::Render::RequestBody2Parameters( $api->{paths}{$path}{$method}{requestBody} ) : ();
    50          
    50          
68              
69 7         14 my $par = {};
70 7         14 my $par_hash = $input;
71              
72 7 50       29 if( blessed $par_hash ) {
73 0         0 $par_hash = { $par_hash->Vars }; # object is assumed to be CGI
74             }
75              
76 7         16 for my $description (@parameters) {
77 7         15 my $name = $description->{name};
78 7 50       19 my $schema = $description->{schema} if $description->{schema};
79 7 100       18 if( !exists $par_hash->{$name} ) {
80 1 50 33     8 if( $schema && exists $schema->{default} ) {
81 0         0 $par->{$name} = $schema->{default};
82             }
83 1         3 next;
84             }
85              
86 6 50 33     32 if( $schema && $schema->{type} eq 'array' ) {
87 0         0 my( @good_values, @bad_values );
88 0 0       0 for (ref $par_hash->{$name} eq 'ARRAY' ? @{$par_hash->{$name}} : split "\0", $par_hash->{$name}) {
  0         0  
89 0         0 my $value = validate_value( $_, $schema );
90 0 0       0 push @good_values, $value if defined $value;
91 0 0       0 push @bad_values, $value unless defined $value;
92             }
93 0 0       0 $par->{$name} = \@good_values if @good_values;
94 0 0 0     0 $reporter->( $name, @bad_values ) if $reporter && @bad_values;
95             } else {
96 6         41 my $value = validate_value( $par_hash->{$name}, $schema );
97 6 100       19 $par->{$name} = $value if defined $value;
98 6 50       22 if( $reporter ) {
99 0         0 $reporter->( $name, $par_hash->{$name} );
100             }
101             }
102             }
103              
104 7         20 return $par;
105             }
106              
107             =head1 VALIDATION ERROR REPORTING
108              
109             By default validation errors are silent by default.
110             However, this can be overridden by setting module variable C<$Data::Validate::OpenAPI::reporter> to a subroutine reference to be called upon validation failure with the following signature:
111              
112             $reporter->( $parameter_name, @bad_values );
113              
114             At this point the module does not indicate which particular check failed during the validation.
115              
116             =cut
117              
118             sub validate_value
119             {
120 6     6 0 23 my( $value, $schema ) = @_;
121              
122 6 50       18 my $format = $schema->{format} if $schema;
123              
124             # FIXME: Maybe employ a proper JSON Schema validator? Not sure
125             # if it untaints, though.
126 6 50       28 if( !defined $format ) {
    100          
    50          
    50          
    0          
    0          
    0          
    0          
127             # nothing to do here
128             } elsif( $format eq 'date-time' ) {
129 1         11 my $parser = DateTime::Format::RFC3339->new;
130 1         17 $value = $parser->format_datetime( $parser->parse_datetime( $value ) );
131             } elsif( $format eq 'email' ) {
132 0         0 $value = is_email $value;
133             } elsif( $format eq 'integer' ) {
134 5         123 $value = is_integer $value;
135             } elsif( $format eq 'ipv4' ) {
136 0         0 $value = is_ipv4 $value;
137             } elsif( $format eq 'ipv6' ) {
138 0         0 $value = is_ipv6 $value;
139             } elsif( $format eq 'uri' ) {
140 0         0 $value = is_uri $value;
141             } elsif( $format eq 'uuid' ) {
142             # Regex taken from Data::Validate::UUID. Module is not used as
143             # it does not untaint the value.
144 0 0       0 if( $value =~ /^([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i ) {
145 0         0 $value = $1;
146             } else {
147 0         0 return;
148             }
149             }
150              
151 6 100       2700 return unless defined $value;
152              
153 4 50 33     34 if( $schema && $schema->{enum} ) {
154 0         0 ( $value ) = grep { $value eq $_ } @{$schema->{enum}};
  0         0  
  0         0  
155 0 0       0 return unless defined $value;
156             }
157              
158 4 50 33     22 if( $schema && $schema->{pattern} ) {
159 0 0       0 return unless $value =~ /^($schema->{pattern})$/;
160 0         0 $value = $1;
161             }
162              
163             ## Not sure this is appropriate here
164             # if( defined $value && $value eq '' &&
165             # ( !exists $description->{allowEmptyValue} ||
166             # $description->{allowEmptyValue} eq 'false' ) ) {
167             # return; # nothing to do
168             # }
169              
170 4         13 return $value;
171             }
172              
173             1;