File Coverage

blib/lib/DNS/Hetzner/Schema.pm
Criterion Covered Total %
statement 112 114 98.2
branch 33 36 91.6
condition 4 5 80.0
subroutine 12 13 92.3
pod 1 1 100.0
total 162 169 95.8


line stmt bran cond sub pod time code
1             package DNS::Hetzner::Schema;
2             $DNS::Hetzner::Schema::VERSION = '0.05';
3             # ABSTRACT: OpenAPI schema for the DNS API
4              
5 25     25   1578184 use v5.24;
  25         308  
6              
7 25     25   12403 use Mojo::Base -strict, -signatures;
  25         4584712  
  25         259  
8              
9 25     25   109373 use JSON::Validator;
  25         8755127  
  25         1355  
10 25     25   298 use JSON::Validator::Formats;
  25         72  
  25         764  
11 25     25   156 use List::Util qw(uniq);
  25         83  
  25         1433  
12 25     25   174 use Mojo::JSON qw(decode_json);
  25         107  
  25         1067  
13 25     25   171 use Mojo::Loader qw(data_section);
  25         80  
  25         1063  
14 25     25   169 use Mojo::Util qw(camelize);
  25         59  
  25         1563  
15              
16 25     25   463 use constant IV_SIZE => eval 'require Config;$Config::Config{ivsize}';
  25         61  
  25         2256  
17              
18 106     106 1 100798 sub validate ( $class, $operation, $params = {} ) {
  106         325  
  106         221  
  106         214  
  106         164  
19 106         310 my ($spec, $validator) = _get_params($operation);
20              
21             my %check_params = map {
22 106         499 my $camelized = camelize $_;
  129         428  
23 129         2920 $camelized =~ s{Id$}{ID};
24              
25             $spec->{param_names}->{$camelized} ?
26             ($camelized => $params->{$_}) :
27 129 100       741 ($_ => $params->{$_});
28             } keys $params->%*;
29              
30 106         519 my @errors = $validator->validate(
31             \%check_params,
32             );
33              
34 106 100       655755 if ( @errors ) {
35 48         285 return ( undef, @errors );
36             }
37              
38 58         131 my %request_params;
39              
40 58         332 for my $param_name ( sort keys $spec->{param_names}->%* ) {
41 147         321 my $key = $spec->{param_names}->{$param_name};
42              
43 147 100       400 next if !$check_params{$param_name};
44              
45 69         239 $request_params{$key}->{$param_name} = $check_params{$param_name};
46             }
47              
48 58         305 return \%request_params;
49             }
50              
51 106     106   416 sub _get_params ($operation) {
  106         186  
  106         163  
52 106         162 state %operation_params;
53              
54 106         232 my $op_data = $operation_params{$operation};
55 106 100       454 return $op_data->@* if $op_data;
56              
57 22         167 my $api_spec = data_section(__PACKAGE__, 'openapi.json');
58 22         169398 $api_spec =~ s{/components/schemas}{}g;
59              
60 22         263 my $data = decode_json( $api_spec );
61 22         1261556 my $schemas = $data->{components}->{schemas};
62              
63 22         390 my %paths = $data->{paths}->%*;
64              
65 22         85 my $op;
66              
67 22         166 for my $path ( keys %paths ) {
68              
69             METHOD:
70 220         724 for my $method_name ( keys $paths{$path}->%* ) {
71 660 100       1820 next METHOD if 'HASH' ne ref $paths{$path}->{$method_name};
72              
73 440         766 my $method = $paths{$path}->{$method_name};
74 440 100 66     1689 if ( $method->{operationId} && $method->{operationId} eq $operation ) {
75 22         91 $op = $method;
76             }
77             }
78             }
79              
80 22 50       145 return {} if !$op;
81              
82 22         82 my $params = $op->{parameters};
83              
84 22         116 my %properties;
85             my @required;
86 22         0 my %param_names;
87              
88             PARAM:
89 22         87 for my $param ( $params->@* ) {
90 23 50       94 next PARAM if $param->{name} eq 'Auth-API-Token';
91              
92 23         60 my $name = $param->{name};
93              
94 23 100       706 if ( $param->{required} ) {
95 11         178 push @required, $name;
96             }
97              
98 23         197 $param_names{$name} = $param->{in};
99 23         71 $properties{$name} = $param;
100             }
101              
102 22         67 my ($content_type, $body_required);
103 22 100       97 if ( $op->{requestBody} ) {
104 11         43 my $body = $op->{requestBody}->{content};
105 11         35 $body_required = $op->{requestBody}->{required};
106              
107 11         67 ($content_type) = sort keys $body->%*;
108              
109 11 100       63 if ( $content_type eq 'application/json' ) {
    50          
110 9         33 my $schema = $body->{$content_type}->{schema};
111 9   100     67 my $prop = $schema->{properties} || {};
112 9         41 for my $property ( keys $prop->%* ) {
113 2         10 $properties{$property} = delete $prop->{$property};
114 2         8 $param_names{$property} = 'body';
115             }
116              
117 9 100       42 if ( $schema->{'$ref'} ) { # '$ref' is not a typo. The key is named '$ref'!
118 7         41 $properties{'$ref'} = $schema->{'$ref'};
119             }
120             }
121             elsif ( $content_type eq 'text/plain' ) {
122 2         13 $properties{text} = { type => "string" };
123 2         7 $param_names{text} = 'body';
124             }
125             }
126              
127 22         616 my $spec = {
128             type => 'object',
129             required => \@required,
130             body_required => $body_required,
131             properties => \%properties,
132             param_names => \%param_names,
133             content_type => $content_type,
134             $schemas->%*,
135             };
136              
137 22         412 my $validator = JSON::Validator->new->schema($spec);
138              
139             $validator->formats->{uint32} = sub {
140 0     0   0 my ($sub) = JSON::Validator::Formats->can('_match_number');
141              
142 0         0 $sub->( unint32 => $_[0], 'L' );
143 22         634261 };
144              
145             $validator->formats->{uint64} = sub {
146 2     2   13151 my ($sub) = JSON::Validator::Formats->can('_match_number');
147              
148 2         358 $sub->( unint32 => $_[0], IV_SIZE >= 8 ? 'Q' : '' );
149 22         2317 };
150              
151 22 100       252 if ( $spec->{properties}->{'$ref'} ) {
152 7         161 my @ref = $spec->{properties}->{'$ref'};
153 7         135 while ( my $ref = shift @ref ) {
154 18         102 $ref =~ s{^#}{};
155              
156 18         83 my $data = $validator->get( $ref );
157 18 100       1041 if ( $data->{properties} ) {
158 11         74 for my $property ( keys $data->{properties}->%* ) {
159 82 100       677 next if $data->{properties}->{$property}->{readOnly};
160              
161 22         92 $spec->{param_names}->{$property} = 'body';
162             }
163             }
164              
165 18 100       112 if ( $data->{allOf} ) {
166 11         40 push @ref, map{ $_->{'$ref'} } $data->{allOf}->@*;
  11         50  
167 11 100       125 if ( $data->{required} ) {
168 7         42 push $spec->{required}->@*, $data->{required}->@*;
169             }
170             }
171             }
172             }
173              
174 22         274 $spec->{required}->@* = uniq $spec->{required}->@*;
175              
176 22         103 $operation_params{$operation} = [ $spec, $validator ];
177 22         4035 return $operation_params{$operation}->@*;
178             }
179              
180             1;
181              
182             =pod
183              
184             =encoding UTF-8
185              
186             =head1 NAME
187              
188             DNS::Hetzner::Schema - OpenAPI schema for the DNS API
189              
190             =head1 VERSION
191              
192             version 0.05
193              
194             =head1 SYNOPSIS
195              
196             =head1 METHODS
197              
198             =head2 validate
199              
200             =head1 AUTHOR
201              
202             Renee Baecker
203              
204             =head1 COPYRIGHT AND LICENSE
205              
206             This software is Copyright (c) 2020 by Renee Baecker.
207              
208             This is free software, licensed under:
209              
210             The Artistic License 2.0 (GPL Compatible)
211              
212             =cut
213              
214             __DATA__