File Coverage

blib/lib/SQL/Translator/Parser/OpenAPI.pm
Criterion Covered Total %
statement 433 444 97.5
branch 104 114 91.2
condition 81 102 79.4
subroutine 40 41 97.5
pod 3 3 100.0
total 661 704 93.8


line stmt bran cond sub pod time code
1             package SQL::Translator::Parser::OpenAPI;
2 6     6   1967131 use strict;
  6         32  
  6         197  
3 6     6   33 use warnings;
  6         11  
  6         154  
4 6     6   3035 use JSON::Validator::OpenAPI::Mojolicious;
  6         1577545  
  6         481  
5              
6             our $VERSION = "0.07";
7 6     6   60 use constant DEBUG => $ENV{SQLTP_OPENAPI_DEBUG};
  6         14  
  6         366  
8 6     6   2904 use String::CamelCase qw(camelize decamelize);
  6         3362  
  6         409  
9 6     6   2750 use Lingua::EN::Inflect::Number qw(to_PL to_S);
  6         182620  
  6         48  
10 6     6   2159 use SQL::Translator::Schema::Constants;
  6         749  
  6         452  
11 6     6   5988 use Math::BigInt;
  6         125390  
  6         43  
12 6     6   114821 use Hash::MoreUtils qw(slice_grep);
  6         12941  
  6         489  
13 6     6   2834 use Hash::Merge qw(merge);
  6         52677  
  6         35458  
14              
15             my %TYPE2SQL = (
16             integer => 'int',
17             int32 => 'int',
18             int64 => 'bigint',
19             float => 'float',
20             number => 'double',
21             double => 'double',
22             string => 'varchar',
23             byte => 'byte',
24             binary => 'binary',
25             boolean => 'bit',
26             date => 'date',
27             'date-time' => 'datetime',
28             password => 'varchar',
29             );
30             my %SQL2TYPE = reverse %TYPE2SQL; # unreliable order but ok as still reversible
31              
32             # from GraphQL::Debug
33             sub _debug {
34 0     0   0 my $func = shift;
35 0         0 require Data::Dumper;
36 0         0 require Test::More;
37 0         0 local ($Data::Dumper::Sortkeys, $Data::Dumper::Indent, $Data::Dumper::Terse);
38 0         0 $Data::Dumper::Sortkeys = $Data::Dumper::Indent = $Data::Dumper::Terse = 1;
39 0         0 Test::More::diag("$func: ", Data::Dumper::Dumper([ @_ ]));
40             }
41              
42             # heuristic 1: strip out single-item objects - RHS = ref if array
43             sub _strip_thin {
44 6     6   17 my ($defs) = @_;
45             my %thin2real = map {
46 6         31 my $theseprops = $defs->{$_}{properties};
  78         171  
47 78         376 my @props = grep !/count/i, keys %$theseprops;
48 78 100       206 my $real = @props == 1 ? $theseprops->{$props[0]} : undef;
49 78 100 100     245 my $is_array = $real = $real->{items} if $real and $real->{type} eq 'array';
50 78 100       289 $real = $real->{'$ref'} if $real;
51 78 100       226 $real = _ref2def($real) if $real;
52 78 100       259 @props == 1 ? ($_ => $is_array ? \$real : $real) : ()
    100          
53             } keys %$defs;
54 6         25 DEBUG and _debug("OpenAPI._strip_thin", \%thin2real);
55 6         29 \%thin2real;
56             }
57              
58             # heuristic 2: find objects with same propnames, drop those with longer names
59             sub _strip_dup {
60 6     6   20 my ($defs, $def2mask, $reffed) = @_;
61 6         13 my %sig2names;
62 6         94 push @{ $sig2names{$def2mask->{$_}} }, $_ for keys %$def2mask;
  78         1524  
63 6         140 DEBUG and _debug("OpenAPI sig2names", \%sig2names);
64 6         37 my @nondups = grep @{ $sig2names{$_} } == 1, keys %sig2names;
  63         125  
65 6         57 delete @sig2names{@nondups};
66 6         18 my %dup2real;
67 6         20 for my $sig (keys %sig2names) {
68 8 100       17 next if grep $reffed->{$_}, @{ $sig2names{$sig} };
  8         40  
69 6         38 my @names = sort { (length $a <=> length $b) } @{ $sig2names{$sig} };
  15         45  
  6         22  
70 6         15 DEBUG and _debug("OpenAPI dup($sig)", \@names);
71 6         11 my $real = shift @names; # keep the first i.e. shortest
72 6         29 $dup2real{$_} = $real for @names;
73             }
74 6         72 DEBUG and _debug("dup ret", \%dup2real);
75 6         38 \%dup2real;
76             }
77              
78             # sorted list of all propnames
79             sub _get_all_propnames {
80 8     8   21 my ($defs) = @_;
81 8         29 my %allprops;
82 8         45 for my $defname (keys %$defs) {
83 150         223 $allprops{$_} = 1 for keys %{ $defs->{$defname}{properties} };
  150         652  
84             }
85 8         181 [ sort keys %allprops ];
86             }
87              
88             sub defs2mask {
89 8     8 1 2470 my ($defs) = @_;
90 8         29 my $allpropnames = _get_all_propnames($defs);
91 8         33 my $count = 0;
92 8         14 my %prop2count;
93 8         21 for my $propname (@$allpropnames) {
94 213         336 $prop2count{$propname} = $count;
95 213         310 $count++;
96             }
97 8         22 my %def2mask;
98 8         52 for my $defname (keys %$defs) {
99 150   33     196434 $def2mask{$defname} ||= Math::BigInt->new(0);
100             $def2mask{$defname} |= (Math::BigInt->new(1) << $prop2count{$_})
101 150         17275 for keys %{ $defs->{$defname}{properties} };
  150         957  
102             }
103 8         12237 \%def2mask;
104             }
105              
106             # heuristic 3: find objects with set of propnames that is subset of
107             # another object's propnames
108             sub _strip_subset {
109 6     6   23 my ($defs, $def2mask, $reffed) = @_;
110 6         14 my %subset2real;
111 6         34 for my $defname (keys %$defs) {
112 78         9041 DEBUG and _debug("_strip_subset $defname maybe", $reffed);
113 78 100       242 next if $reffed->{$defname};
114 61         123 my $thismask = $def2mask->{$defname};
115 61         504 for my $supersetname (grep $_ ne $defname, keys %$defs) {
116 1193         184443 my $supermask = $def2mask->{$supersetname};
117 1193 100       2694 next unless ($thismask & $supermask) == $thismask;
118 102         12522 DEBUG and _debug("mask $defname subset $supersetname");
119 102         315 $subset2real{$defname} = $supersetname;
120             }
121             }
122 6         699 DEBUG and _debug("subset ret", \%subset2real);
123 6         28 \%subset2real;
124             }
125              
126             sub _prop2sqltype {
127 174     174   363 my ($prop) = @_;
128 174   100     668 my $format_type = $prop->{format} || $prop->{type};
129 174   100     607 my $lookup = $TYPE2SQL{$format_type || ''};
130 174         263 DEBUG and _debug("_prop2sqltype($format_type)($lookup)", $prop);
131 174         512 my %retval = (data_type => $lookup);
132 174 100       274 if (@{$prop->{enum} || []}) {
  174 100       757  
133 5         25 $retval{data_type} = 'enum';
134 5         15 $retval{extra} = { list => [ @{$prop->{enum}} ] };
  5         22  
135             }
136 174         330 DEBUG and _debug("_prop2sqltype(end)", \%retval);
137 174         404 \%retval;
138             }
139              
140             sub _make_not_null {
141 123     123   294 my ($table, $field_in) = @_;
142 123 100       438 my @fields = ref($field_in) eq 'ARRAY' ? @$field_in : $field_in;
143 123         279 for my $field (@fields) {
144 127         2795 $field->is_nullable(0);
145             }
146             $table->add_constraint(type => $_, fields => \@fields)
147 123         96338 for (NOT_NULL);
148             }
149              
150             sub _make_pk {
151 42     42   587 my ($table, $field_in) = @_;
152 42 100       168 my @fields = ref($field_in) eq 'ARRAY' ? @$field_in : $field_in;
153 42         785 $_->is_primary_key(1) for @fields;
154 42 100 66     2059 $fields[0]->is_auto_increment(1) if @fields == 1 and $fields[0]->data_type =~ /int/;
155             $table->add_constraint(type => $_, fields => \@fields)
156 42         1042 for (PRIMARY_KEY);
157 42         98067 my $index = $table->add_index(
158             name => join('_', 'pk', map $_->name, @fields),
159             fields => \@fields,
160             );
161 42         31954 _make_not_null($table, \@fields);
162             }
163              
164             sub _def2tablename {
165 73     73   169 my ($def, $args) = @_;
166 73 100       248 return $def unless $args->{snake_case};
167 62         229 to_PL decamelize $def;
168             }
169              
170             sub _ref2def {
171 140     140   356 my ($ref) = @_;
172 140 50       751 $ref =~ s:^#/definitions/:: or return;
173 140         478 $ref;
174             }
175              
176             sub _make_fk {
177 30     30   3091 my ($table, $field, $foreign_tablename, $foreign_id) = @_;
178 30         104 $table->add_constraint(
179             type => FOREIGN_KEY, fields => $field,
180             reference_table => $foreign_tablename,
181             reference_fields => $foreign_id,
182             );
183             }
184              
185             sub _get_entity {
186 30     30   72 my ($schema, $name, $view2real) = @_;
187 30 100       76 $schema->get_table($name) || $schema->get_table($view2real->{$name});
188             }
189              
190             sub _fk_hookup {
191 30     30   106 my ($schema, $fromtable, $fromkey, $totable, $tokey, $required, $view2real) = @_;
192 30         88 DEBUG and _debug("_fk_hookup $fromtable.$fromkey $totable.$tokey $required");
193 30         123 my $from_obj = $schema->get_table($fromtable);
194 30         472 my $to_obj = _get_entity($schema, $totable, $view2real);
195 30         7554 my $tokey_obj = $to_obj->get_field($tokey);
196 30   66     1580 my $field = $from_obj->get_field($fromkey) || $from_obj->add_field(
197             name => $fromkey, data_type => $tokey_obj->data_type,
198             );
199 30 50       45076 die $from_obj->error if !$field;
200 30         7741 _make_fk($from_obj, $field, $to_obj->name, $tokey);
201 30 100       34949 _make_not_null($from_obj, $field) if $required;
202 30         29496 $field;
203             }
204              
205             sub _def2table {
206 44     44   651 my ($name, $def, $schema, $m2m, $view2real, $def2relationalid, $args) = @_;
207 44         114 my $props = $def->{properties};
208 44         136 my $tname = _def2tablename($name, $args);
209 44         69285 DEBUG and _debug("_def2table($name)($tname)($m2m)", $props);
210 44 100       168 if (my $view_of = $def->{'x-view-of'}) {
211 2         7 my $target_table = _def2tablename($view_of, $args);
212 2         966 $view2real->{$tname} = $target_table;
213 2         9 return (undef, []);
214             }
215             my $table = $schema->add_table(
216             name => $tname, comments => $def->{description},
217 42         283 );
218 42         30959 my $relational_id_field = $def2relationalid->{$name};
219 42 100 100     271 if (!$m2m and !$props->{$relational_id_field}) {
220             # we need a relational id
221 29         114 $props->{$relational_id_field} = { type => 'integer' };
222             }
223 42 100       121 my %prop2required = map { ($_ => 1) } @{ $def->{required} || [] };
  78         250  
  42         223  
224 42         95 my (@fixups);
225 42         289 for my $propname (sort keys %$props) {
226 201         63721 my $field;
227 201         529 my $thisprop = $props->{$propname};
228 201         302 DEBUG and _debug("_def2table($propname)");
229 201 100 100     1011 if (my $ref = $thisprop->{'$ref'}) {
    100          
230 14         112 my $refname = _ref2def($ref);
231             push @fixups, {
232             from => $tname,
233             fromkey => $propname . '_id',
234             to => _def2tablename($refname, $args),
235             tokey => $def2relationalid->{$refname},
236 14         57 required => $prop2required{$propname},
237             type => 'one',
238             };
239             } elsif (($thisprop->{type} // '') eq 'array') {
240 13 50       78 if (my $ref = $thisprop->{items}{'$ref'}) {
241 13         83 my $refname = _ref2def($ref);
242 13         29 my $fromkey;
243 13 100       46 if ($args->{snake_case}) {
244 11         52 $fromkey = to_S($propname) . "_id";
245             } else {
246 2         5 $fromkey = $propname . "_id";
247             }
248 13         8423 push @fixups, {
249             from => _def2tablename($refname, $args),
250             fromkey => $fromkey,
251             to => $tname,
252             tokey => $relational_id_field,
253             required => 1,
254             type => 'many',
255             };
256             }
257 13         10428 DEBUG and _debug("_def2table(array)($propname)", \@fixups);
258             } else {
259 174         459 my $sqltype = _prop2sqltype($thisprop);
260             $field = $table->add_field(
261             name => $propname, %$sqltype, comments => $thisprop->{description},
262 174         919 );
263 174 100 100     231029 if ($propname eq ($relational_id_field // '')) {
    100 100        
264 38         164 _make_pk($table, $field);
265             } elsif ($propname eq ($def->{'x-id-field'} // '')) {
266             $table->add_constraint(type => $_, fields => [ $field ])
267 4         19 for (UNIQUE);
268 4         5334 my $index = $table->add_index(
269             name => join('_', 'unique', map $_->name, $field),
270             fields => [ $field ],
271             );
272             }
273             }
274 201 100 100     61137 if ($field and $prop2required{$propname} and $propname ne $relational_id_field) {
      100        
275 54         13038 _make_not_null($table, $field);
276             }
277             }
278 42 100       24096 if ($m2m) {
279 4         21 _make_pk($table, scalar $table->get_fields);
280             }
281 42         4814 DEBUG and _debug("table", $table, \@fixups);
282 42         221 ($table, \@fixups);
283             }
284              
285             sub _merge_allOf {
286 6     6   19 my ($defs) = @_;
287 6         13 DEBUG and _debug('OpenAPI._merge_allOf', $defs);
288 6     78   56 my %r2ds = slice_grep { $_{$_}{allOf} } $defs;
  78         493  
289             my @defref_pairs = map {
290 6         55 my $referrer = $_;
  4         11  
291             map [ $_, $referrer ],
292 4         6 grep $defs->{$_}{discriminator}, map _ref2def($_), grep defined, map $_->{'$ref'}, @{ $r2ds{$referrer}{allOf} }
  4         22  
293             } keys %r2ds;
294 6         12 DEBUG and _debug('OpenAPI._merge_allOf(defref_pairs)', \@defref_pairs);
295 6         59 my %newdefs = %$defs;
296 6         19 my %def2ignore;
297 6         28 for (@defref_pairs) {
298 2         632 my ($defname, $assimilee) = @$_;
299 2         7 @def2ignore{@$_} = (1, 1);
300             $newdefs{$defname} = merge $newdefs{$defname}, $_
301 2         4 for grep !$_->{'$ref'}, @{ $defs->{$assimilee}{allOf} };
  2         12  
302             }
303 6   100     390 for my $defname (grep !$def2ignore{$_} && exists $newdefs{$_}{allOf}, keys %$defs) {
304             $newdefs{$defname} = merge $newdefs{$defname}, (exists $_->{'$ref'}
305             ? $defs->{ _ref2def($_->{'$ref'}) }
306 2 100       5 : $_) for @{ $newdefs{$defname}{allOf} };
  2         19  
307 2         1956 delete $newdefs{$defname}{allOf}; # delete as will now be copy
308             }
309 6         19 DEBUG and _debug('OpenAPI._merge_allOf(end)', \%newdefs);
310 6         75 \%newdefs;
311             }
312              
313             sub _find_referenced {
314 6     6   25 my ($defs, $thin2real) = @_;
315 6         13 DEBUG and _debug('OpenAPI._find_referenced', $defs);
316 6         17 my %reffed;
317 6         76 for my $defname (grep !$thin2real->{$_}, keys %$defs) {
318 51   50     141 my $theseprops = $defs->{$defname}{properties} || {};
319 51         139 for my $propname (keys %$theseprops) {
320 196 100 100     886 if (my $ref = $theseprops->{$propname}{'$ref'}
321             || ($theseprops->{$propname}{items} && $theseprops->{$propname}{items}{'$ref'})
322             ) {
323 25         193 $reffed{ _ref2def($ref) } = 1;
324             }
325             }
326             }
327 6         19 DEBUG and _debug('OpenAPI._find_referenced(end)', \%reffed);
328 6         17 \%reffed;
329             }
330              
331             sub _extract_objects {
332 6     6   58 my ($defs, $args) = @_;
333 6         16 DEBUG and _debug('OpenAPI._extract_objects', $defs);
334 6         47 my %newdefs = %$defs;
335 6         53 for my $defname (sort keys %$defs) {
336 33   50     139 my $theseprops = $defs->{$defname}{properties} || {};
337 33         118 for my $propname (keys %$theseprops) {
338 157         402 my $thisprop = $theseprops->{$propname};
339             next if $thisprop->{'$ref'}
340 157 100 100     513 or $thisprop->{items} && $thisprop->{items}{'$ref'};
      100        
341 136         196 my $ref;
342 136 100 100     464 if (($thisprop->{type} // '') eq 'object') {
    50 50        
      66        
343 5         7 $ref = $thisprop;
344             } elsif (
345             $thisprop->{items} && ($thisprop->{items}{type} // '') eq 'object'
346             ) {
347 0         0 $ref = $thisprop->{items};
348             } else {
349 131         299 next;
350             }
351 5         7 my $newtype;
352 5 50       12 if ($args->{snake_case}) {
353 5         20 $newtype = join '', map camelize($_), $defname, $propname;
354             } else {
355 0         0 $newtype = join '_', $defname, $propname;
356             }
357 5         128 $newdefs{$newtype} = { %$ref };
358 5         26 %$ref = ('$ref' => "#/definitions/$newtype");
359             }
360             }
361 6         24 DEBUG and _debug('OpenAPI._extract_objects(end)', [ sort keys %newdefs ], \%newdefs);
362 6         63 \%newdefs;
363             }
364              
365             sub _extract_array_simple {
366 6     6   39 my ($defs, $args) = @_;
367 6         14 DEBUG and _debug('OpenAPI._extract_array_simple', $defs);
368 6         41 my %newdefs = %$defs;
369 6         42 for my $defname (sort keys %$defs) {
370 38   100     147 my $theseprops = $defs->{$defname}{properties} || {};
371 38         104 for my $propname (keys %$theseprops) {
372 161         379 my $thisprop = $theseprops->{$propname};
373 161 100       317 next if $thisprop->{'$ref'};
374             next unless
375 142 100 50     381 $thisprop->{items} && ($thisprop->{items}{type} // '') ne 'object';
      100        
376 3         7 my $ref = $thisprop->{items};
377 3         7 my $newtype;
378 3 100       12 if ($args->{snake_case}) {
379 2         14 $newtype = join '', map camelize($_), $defname, $propname;
380             } else {
381 1         6 $newtype = join '_', $defname, $propname;
382             }
383 3         85 $newdefs{$newtype} = {
384             type => 'object',
385             properties => {
386             value => { %$ref }
387             },
388             required => [ 'value' ],
389             };
390 3         19 %$ref = ('$ref' => "#/definitions/$newtype");
391             }
392             }
393 6         23 DEBUG and _debug('OpenAPI._extract_array_simple(end)', [ sort keys %newdefs ], \%newdefs);
394 6         63 \%newdefs;
395             }
396              
397             sub _fixup_addProps {
398 6     6   29 my ($defs) = @_;
399 6         10 DEBUG and _debug('OpenAPI._fixup_addProps', $defs);
400 6         56 my %def2aP = map {$_,1} grep $defs->{$_}{additionalProperties}, keys %$defs;
  3         9  
401 6         17 DEBUG and _debug("OpenAPI._fixup_addProps(d2aP)", \%def2aP);
402 6         38 for my $defname (sort keys %$defs) {
403 41   100     116 my $theseprops = $defs->{$defname}{properties} || {};
404 41         58 DEBUG and _debug("OpenAPI._fixup_addProps(arrayfix)($defname)", $theseprops);
405 41         119 for my $propname (keys %$theseprops) {
406 164         288 my $thisprop = $theseprops->{$propname};
407 164         213 DEBUG and _debug("OpenAPI._fixup_addProps(p)($propname)", $thisprop);
408             next unless $thisprop->{'$ref'}
409 164 100 66     545 or $thisprop->{items} && $thisprop->{items}{'$ref'};
      100        
410 29         142 DEBUG and _debug("OpenAPI._fixup_addProps(p)($propname)(y)");
411 29         49 my $ref;
412 29 100 33     104 if ($thisprop->{'$ref'}) {
    50          
413 19         78 $ref = $thisprop;
414             } elsif ($thisprop->{items} && $thisprop->{items}{'$ref'}) {
415 10         44 $ref = $thisprop->{items};
416             } else {
417 0         0 next;
418             }
419 29         78 my $refname = $ref->{'$ref'};
420 29         101 DEBUG and _debug("OpenAPI._fixup_addProps(p)($propname)(y2)($refname)", $ref);
421 29 100       71 next if !$def2aP{_ref2def($refname)};
422 3         14 %$ref = (type => 'array', items => { '$ref' => $refname });
423 3         8 DEBUG and _debug("OpenAPI._fixup_addProps(p)($propname)(y3)", $ref);
424             }
425             }
426 6         55 my %newdefs = %$defs;
427 6         25 for my $defname (keys %def2aP) {
428             my %kv = (type => 'object', properties => {
429             key => { type => 'string' },
430             value => { type => $defs->{$defname}{additionalProperties}{type} },
431 3         17 });
432 3         10 $newdefs{$defname} = \%kv;
433             }
434 6         12 DEBUG and _debug('OpenAPI._fixup_addProps(end)', \%newdefs);
435 6         60 \%newdefs;
436             }
437              
438             sub _absorb_nonobject {
439 6     6   15 my ($defs) = @_;
440 6         12 DEBUG and _debug('OpenAPI._absorb_nonobject', $defs);
441 6         60 my %def2nonobj = map {$_,1} grep $defs->{$_}{type} ne 'object', keys %$defs;
  1         5  
442 6         14 DEBUG and _debug("OpenAPI._absorb_nonobject(d2nonobj)", \%def2nonobj);
443 6         38 for my $defname (sort keys %$defs) {
444 41   50     110 my $theseprops = $defs->{$defname}{properties} || {};
445 41         61 DEBUG and _debug("OpenAPI._absorb_nonobject(t)($defname)", $theseprops);
446 41         107 for my $propname (keys %$theseprops) {
447 170         278 my $thisprop = $theseprops->{$propname};
448 170         237 DEBUG and _debug("OpenAPI._absorb_nonobject(p)($propname)", $thisprop);
449             next unless $thisprop->{'$ref'}
450 170 100 66     566 or $thisprop->{items} && $thisprop->{items}{'$ref'};
      100        
451 29         143 DEBUG and _debug("OpenAPI._absorb_nonobject(p)($propname)(y)");
452 29         46 my $ref;
453 29 100 33     102 if ($thisprop->{'$ref'}) {
    50          
454 16         70 $ref = $thisprop;
455             } elsif ($thisprop->{items} && $thisprop->{items}{'$ref'}) {
456 13         58 $ref = $thisprop->{items};
457             } else {
458 0         0 next;
459             }
460 29         78 my $refname = $ref->{'$ref'};
461 29         103 DEBUG and _debug("OpenAPI._absorb_nonobject(p)($propname)(y2)($refname)", $ref);
462 29         70 my $refdef = _ref2def($refname);
463 29 100       93 next if !$def2nonobj{$refdef};
464 2         4 %$ref = %{ $defs->{$refdef} };
  2         27  
465 2         50 DEBUG and _debug("OpenAPI._absorb_nonobject(p)($propname)(y3)", $ref);
466             }
467             }
468 6         48 my %newdefs = %$defs;
469 6         24 delete @newdefs{ keys %def2nonobj };
470 6         11 DEBUG and _debug('OpenAPI._absorb_nonobject(end)', \%newdefs);
471 6         46 \%newdefs;
472             }
473              
474             sub _tuple2name {
475 12     12   5999 my ($fixup, $args) = @_;
476 12         26 my $from = $fixup->{from};
477 12         25 my $fromkey = $fixup->{fromkey};
478 12         60 $fromkey =~ s#_id$##;
479 12 100       46 if ($args->{snake_case}) {
480 10         34 camelize join '_', map to_S($_), $from, $fromkey;
481             } else {
482 2         11 join '_', $from, $fromkey;
483             }
484             }
485              
486             sub _make_many2many {
487 6     6   23 my ($fixups, $schema, $def2relationalid, $args) = @_;
488 6         15 DEBUG and _debug("_make_many2many", $fixups);
489 6         52 my @manyfixups = grep $_->{type} eq 'many', @$fixups;
490 6         17 my %from_tos;
491 6         34 push @{ $from_tos{$_->{from}}{$_->{to}} }, $_ for @manyfixups;
  13         68  
492 6         13 my %to_froms;
493 6         18 push @{ $to_froms{$_->{to}}{$_->{from}} }, $_ for @manyfixups;
  13         49  
494 6         24 my %m2m;
495             my %ref2nonm2mfixup;
496 6         72 $ref2nonm2mfixup{$_} = $_ for @$fixups;
497 6         30 for my $from (keys %from_tos) {
498 12         22 for my $to (keys %{ $from_tos{$from} }) {
  12         50  
499 12         24 for my $fixup (@{ $from_tos{$from}{$to} }) {
  12         33  
500 13         21 for my $other (@{ $to_froms{$from}{$to} }) {
  13         47  
501 6         28 my ($f1, $f2) = sort { $a->{from} cmp $b->{from} } $fixup, $other;
  6         28  
502 6         22 $m2m{_tuple2name($f1, $args)}{_tuple2name($f2, $args)} = [ $f1, $f2 ];
503 6         6895 delete $ref2nonm2mfixup{$_} for $f1, $f2;
504             }
505             }
506             }
507             }
508 6         18 my @replacefixups;
509 6         29 for my $n1 (sort keys %m2m) {
510 4         598 for my $n2 (sort keys %{ $m2m{$n1} }) {
  4         20  
511 4         10 my ($f1, $f2) = @{ $m2m{$n1}{$n2} };
  4         16  
512 4         35 my ($t1_obj, $t2_obj) = map $schema->get_table($_->{to}), $f1, $f2;
513 4         107 my $f1_fromkey = $f1->{fromkey};
514 4         14 my $f2_fromkey = $f2->{fromkey};
515 4 100 66     27 if ($f1_fromkey eq $f2_fromkey and $f1->{from} eq $f2->{from}) {
516 2         13 $f1_fromkey =~ s#_id$#_to_id#;
517 2         11 $f2_fromkey =~ s#_id$#_from_id#;
518             }
519 4         14 my $new_table = $n1.$n2;
520 4 100       13 if ($args->{snake_case}) {
521 3         10 $new_table = $n1.$n2;
522             } else {
523 1         4 $new_table = join '_', $n1, $n2;
524             }
525             my ($table) = _def2table(
526             $new_table,
527             {
528             type => 'object',
529             properties => {
530             $f1_fromkey => {
531             type => $SQL2TYPE{$t1_obj->get_field($f1->{tokey})->data_type}
532             },
533             $f2_fromkey => {
534 4         24 type => $SQL2TYPE{$t2_obj->get_field($f2->{tokey})->data_type}
535             },
536             },
537             },
538             $schema,
539             1,
540             undef,
541             $def2relationalid,
542             $args,
543             );
544             push @replacefixups, {
545             to => $f1->{from},
546             tokey => 'id',
547             from => $table->name,
548             fromkey => $f1_fromkey,
549             required => 1,
550             }, {
551             to => $f2->{from},
552 4         114 tokey => 'id',
553             from => $table->name,
554             fromkey => $f2_fromkey,
555             required => 1,
556             };
557             }
558             }
559             my @newfixups = (
560             (sort {
561 6         696 $a->{from} cmp $b->{from} || $a->{fromkey} cmp $b->{fromkey}
562 42 50       112 } values %ref2nonm2mfixup),
563             @replacefixups,
564             );
565 6         14 DEBUG and _debug("fixups still to do", \@newfixups);
566 6         51 \@newfixups;
567             }
568              
569             sub _remove_fields {
570 12     12   37 my ($defs, $name) = @_;
571 12         24 DEBUG and _debug("OpenAPI._remove_fields($name)", $defs);
572 12         114 for my $defname (sort keys %$defs) {
573 156   100     573 my $theseprops = $defs->{$defname}{properties} || {};
574 156         213 DEBUG and _debug("OpenAPI._remove_fields(t)($defname)", $theseprops);
575 156         359 for my $propname (keys %$theseprops) {
576 442         921 my $thisprop = $theseprops->{$propname};
577 442         574 DEBUG and _debug("OpenAPI._remove_fields(p)($propname)", $thisprop);
578 442 100       1012 delete $theseprops->{$propname} if $thisprop->{$name};
579             }
580             }
581             }
582              
583             sub _decide_id_fields {
584 6     6   17 my ($defs) = @_;
585 6         11 DEBUG and _debug('OpenAPI._decide_id_fields', $defs);
586 6         22 my %def2relationalid;
587 6         50 for my $defname (sort keys %$defs) {
588 40   50     106 my $thisdef = $defs->{$defname} || {};
589 40   50     87 my $theseprops = $thisdef->{properties} || {};
590 40         56 DEBUG and _debug("OpenAPI._decide_id_fields($defname)", $thisdef);
591 40 100 100     218 if (
    50 100        
      33        
592             ($theseprops->{id} and $theseprops->{id}{type} =~ /int/) or
593             !$theseprops->{id}
594             ) {
595 38         107 $def2relationalid{$defname} = 'id';
596             } elsif (
597             ($thisdef->{'x-id-field'} and $theseprops->{$thisdef->{'x-id-field'}}{type} =~ /int/)
598             ) {
599 0         0 $def2relationalid{$defname} = $thisdef->{'x-id-field'};
600             } else {
601 2         7 $def2relationalid{$defname} = _find_unique_name($theseprops);
602             }
603             }
604 6         30 DEBUG and _debug('OpenAPI._decide_id_fields(end)', \%def2relationalid);
605 6         20 \%def2relationalid;
606             }
607              
608             sub _find_unique_name {
609 2     2   6 my ($props) = @_;
610 2         3 DEBUG and _debug('OpenAPI._find_unique_name', $props);
611 2         5 my $id_field = '_relational_id00';
612 2         7 $id_field++ while $props->{$id_field};
613 2         3 DEBUG and _debug('OpenAPI._find_unique_name(end)', $id_field);
614 2         6 $id_field;
615             }
616              
617 1436 100   1436   3175 sub _maybe_deref { ref($_[0]) ? ${$_[0]} : $_[0] }
  190         450  
618              
619             sub _map_thru {
620 6     6   37 my ($x2y) = @_;
621 6         16 DEBUG and _debug("OpenAPI._map_thru 1", $x2y);
622 6         67 my %mapped = %$x2y;
623 6         33 for my $fake (keys %mapped) {
624 45         91 my $real = $mapped{$fake};
625 45 100       85 next if !_maybe_deref $real;
626 40 50       207 $mapped{$_} = (ref $mapped{$_} ? \$real : $real) for
627             grep $fake eq _maybe_deref($mapped{$_}),
628             grep _maybe_deref($mapped{$_}),
629             keys %mapped;
630             }
631 6         27 DEBUG and _debug("OpenAPI._map_thru 2", \%mapped);
632 6         137 \%mapped;
633             }
634              
635             sub definitions_non_fundamental {
636 6     6 1 18 my ($defs) = @_;
637 6         26 my $thin2real = _strip_thin($defs);
638 6         26 my $def2mask = defs2mask($defs);
639 6         37 my $reffed = _find_referenced($defs, $thin2real);
640 6         30 my $dup2real = _strip_dup($defs, $def2mask, $reffed);
641 6         53 my $subset2real = _strip_subset($defs, $def2mask, $reffed);
642 6         169 _map_thru({ %$thin2real, %$dup2real, %$subset2real });
643             }
644              
645             sub parse {
646 6     6 1 382328 my ($tr, $data) = @_;
647 6         124 my $args = $tr->parser_args;
648 6         239 my $openapi_schema = JSON::Validator::OpenAPI::Mojolicious->new->schema($data)->schema;
649 6         34512 my %defs = %{ $openapi_schema->get("/definitions") };
  6         49  
650 6         277 DEBUG and _debug('OpenAPI.definitions', \%defs);
651 6         180 my $schema = $tr->schema;
652 6         18139 DEBUG and $schema->translator(undef); # reduce debug output
653 6         37 _remove_fields(\%defs, 'x-artifact');
654 6         44 _remove_fields(\%defs, 'x-input-only');
655 6         28 %defs = %{ _merge_allOf(\%defs) };
  6         44  
656 6         46 my $bestmap = definitions_non_fundamental(\%defs);
657 6         53 delete @defs{keys %$bestmap};
658 6         19 %defs = %{ _extract_objects(\%defs, $args) };
  6         26  
659 6         59 %defs = %{ _extract_array_simple(\%defs, $args) };
  6         30  
660 6         68 my (@fixups, %view2real);
661 6         15 %defs = %{ _fixup_addProps(\%defs) };
  6         26  
662 6         33 %defs = %{ _absorb_nonobject(\%defs) };
  6         32  
663 6         32 my $def2relationalid = _decide_id_fields(\%defs);
664 6         47 for my $name (sort keys %defs) {
665 40         178 my ($table, $thesefixups) = _def2table($name, $defs{$name}, $schema, 0, \%view2real, $def2relationalid, $args);
666 40         133 push @fixups, @$thesefixups;
667             }
668 6         60 my ($newfixups) = _make_many2many(\@fixups, $schema, $def2relationalid, $args);
669 6         33 for my $fixup (@$newfixups) {
670 30         74 _fk_hookup($schema, @{$fixup}{qw(from fromkey to tokey required)}, \%view2real);
  30         121  
671             }
672 6         271 1;
673             }
674              
675             =encoding utf-8
676              
677             =head1 NAME
678              
679             SQL::Translator::Parser::OpenAPI - convert OpenAPI schema to SQL::Translator schema
680              
681             =begin markdown
682              
683             # PROJECT STATUS
684              
685             | OS | Build status |
686             |:-------:|--------------:|
687             | Linux | [![Build Status](https://travis-ci.org/mohawk2/SQL-Translator-Parser-OpenAPI.svg?branch=master)](https://travis-ci.org/mohawk2/SQL-Translator-Parser-OpenAPI) |
688              
689             [![CPAN version](https://badge.fury.io/pl/SQL-Translator-Parser-OpenAPI.svg)](https://metacpan.org/pod/SQL::Translator::Parser::OpenAPI) [![Coverage Status](https://coveralls.io/repos/github/mohawk2/SQL-Translator-Parser-OpenAPI/badge.svg?branch=master)](https://coveralls.io/github/mohawk2/SQL-Translator-Parser-OpenAPI?branch=master)
690              
691             =end markdown
692              
693             =head1 SYNOPSIS
694              
695             use SQL::Translator;
696             use SQL::Translator::Parser::OpenAPI;
697              
698             my $translator = SQL::Translator->new;
699             $translator->parser("OpenAPI");
700             $translator->producer("YAML");
701             $translator->translate($file);
702              
703             # or...
704             $ sqlt -f OpenAPI -t MySQL my-mysqlschema.sql
705              
706             # or, applying an overlay:
707             $ perl -MHash::Merge=merge -Mojo \
708             -e 'print j merge map j(f($_)->slurp), @ARGV' \
709             t/06-corpus.json t/06-corpus.json.overlay |
710             sqlt -f OpenAPI -t MySQL >my-mysqlschema.sql
711              
712             =head1 DESCRIPTION
713              
714             This module implements a L to convert
715             a L specification to a L.
716              
717             It uses, from the given API spec, the given "definitions" to generate
718             tables in an RDBMS with suitable columns and types.
719              
720             To try to make the data model represent the "real" data, it applies heuristics:
721              
722             =over
723              
724             =item *
725              
726             to remove object definitions considered non-fundamental; see
727             L.
728              
729             =item *
730              
731             for definitions that have C, either merge them together if there
732             is a C, or absorb properties from referred definitions
733              
734             =item *
735              
736             creates object definitions for any properties that are an object
737              
738             =item *
739              
740             creates object definitions for any properties that are an array of simple
741             OpenAPI types (e.g. C)
742              
743             =item *
744              
745             creates object definitions for any objects that are
746             C (i.e. freeform key/value pairs), that are
747             key/value rows
748              
749             =item *
750              
751             absorbs any definitions that are in fact not objects, into the referring
752             property
753              
754             =item *
755              
756             injects foreign-key relationships for array-of-object properties, and
757             creates many-to-many tables for any two-way array relationships
758              
759             =back
760              
761             =head1 ARGUMENTS
762              
763             =head2 snake_case
764              
765             If true, will create table names that are not the definition names, but
766             instead the pluralised snake_case version, in line with SQL convention. By
767             default, the tables will be named after simply the definitions.
768              
769             =head1 PACKAGE FUNCTIONS
770              
771             =head2 parse
772              
773             Standard as per L. The input $data is a scalar
774             that can be understood as a L
775             specification|JSON::Validator/schema>.
776              
777             =head2 defs2mask
778              
779             Given a hashref that is a JSON pointer to an OpenAPI spec's
780             C, returns a hashref that maps each definition name to a
781             bitmask. The bitmask is set from each property name in that definition,
782             according to its order in the complete sorted list of all property names
783             in the definitions. Not exported. E.g.
784              
785             # properties:
786             my $defs = {
787             d1 => {
788             properties => {
789             p1 => 'string',
790             p2 => 'string',
791             },
792             },
793             d2 => {
794             properties => {
795             p2 => 'string',
796             p3 => 'string',
797             },
798             },
799             };
800             my $mask = SQL::Translator::Parser::OpenAPI::defs2mask($defs);
801             # all prop names, sorted: qw(p1 p2 p3)
802             # $mask:
803             {
804             d1 => (1 << 0) | (1 << 1),
805             d2 => (1 << 1) | (1 << 2),
806             }
807              
808             =head2 definitions_non_fundamental
809              
810             Given the C of an OpenAPI spec, will return a hash-ref
811             mapping names of definitions considered non-fundamental to a
812             value. The value is either the name of another definition that I
813             fundamental, or or C if it just contains e.g. a string. It will
814             instead be a reference to such a value if it is to an array of such.
815              
816             This may be used e.g. to determine the "real" input or output of an
817             OpenAPI operation.
818              
819             Non-fundamental is determined according to these heuristics:
820              
821             =over
822              
823             =item *
824              
825             object definitions that only have one property (which the author calls
826             "thin objects"), or that have two properties, one of whose names has
827             the substring "count" (case-insensitive).
828              
829             =item *
830              
831             object definitions that have all the same properties as another, and
832             are not the shortest-named one between the two.
833              
834             =item *
835              
836             object definitions whose properties are a strict subset of another.
837              
838             =back
839              
840             =head1 OPENAPI SPEC EXTENSIONS
841              
842             =head2 C
843              
844             Under C, a key of C will name a
845             field within the C to be the unique ID for that entity.
846             If it is not given, the C field will be used if in the spec, or
847             created if not.
848              
849             This will form the ostensible "key" for the generated table. If the
850             key used here is an integer type, it will also be the primary key,
851             being a suitable "natural" key. If not, then a "surrogate" key (with a
852             generated name starting with C<_relational_id>) will be added as the primary
853             key. If a surrogate key is made, the natural key will be given a unique
854             constraint and index, making it still suitable for lookups. Foreign key
855             relations will however be constructed using the relational primary key,
856             be that surrogate if created, or natural.
857              
858             =head2 C
859              
860             Under C, a key of C will name another
861             definition (NB: not a full JSON pointer). That will make C<$defname>
862             not be created as a table. The handling of creating the "view" of the
863             relevant table is left to the CRUD implementation. This gives it scope
864             to use things like the current requesting user, or web parameters,
865             which otherwise would require a parameterised view. These are not widely
866             available.
867              
868             =head2 C
869              
870             Under C, a key of
871             C with a true value will indicate this is not to be stored,
872             and will not cause a column to be created. The value will instead be
873             derived by other means. The value of this key may become the definition
874             of that derivation.
875              
876             =head2 C
877              
878             Under C, a key of
879             C with a true value will indicate this is not to be stored,
880             and will not cause a column to be created. This may end up being merged
881             with C.
882              
883             =head1 DEBUGGING
884              
885             To debug, set environment variable C to a true value.
886              
887             =head1 AUTHOR
888              
889             Ed J, C<< >>
890              
891             =head1 LICENSE
892              
893             Copyright (C) Ed J
894              
895             This library is free software; you can redistribute it and/or modify
896             it under the same terms as Perl itself.
897              
898             =head1 SEE ALSO
899              
900             L.
901              
902             L.
903              
904             L.
905              
906             =cut
907              
908             1;