File Coverage

blib/lib/SQL/Translator/Parser/OpenAPI.pm
Criterion Covered Total %
statement 396 411 96.3
branch 86 96 89.5
condition 62 79 78.4
subroutine 34 35 97.1
pod 2 2 100.0
total 580 623 93.1


line stmt bran cond sub pod time code
1             package SQL::Translator::Parser::OpenAPI;
2 6     6   1980694 use 5.008001;
  6         47  
3 6     6   34 use strict;
  6         15  
  6         128  
4 6     6   26 use warnings;
  6         16  
  6         203  
5 6     6   2889 use JSON::Validator::OpenAPI;
  6         1537203  
  6         489  
6              
7             our $VERSION = "0.05";
8 6     6   60 use constant DEBUG => $ENV{SQLTP_OPENAPI_DEBUG};
  6         16  
  6         390  
9 6     6   2926 use String::CamelCase qw(camelize decamelize wordsplit);
  6         3447  
  6         446  
10 6     6   2817 use Lingua::EN::Inflect::Number qw(to_PL to_S);
  6         181766  
  6         53  
11 6     6   2116 use SQL::Translator::Schema::Constants;
  6         798  
  6         445  
12 6     6   5911 use Math::BigInt;
  6         124836  
  6         42  
13              
14             my %TYPE2SQL = (
15             integer => 'int',
16             int32 => 'int',
17             int64 => 'bigint',
18             float => 'float',
19             number => 'double',
20             double => 'double',
21             string => 'varchar',
22             byte => 'byte',
23             binary => 'binary',
24             boolean => 'bit',
25             date => 'date',
26             'date-time' => 'datetime',
27             password => 'varchar',
28             );
29             my %SQL2TYPE = reverse %TYPE2SQL; # unreliable order but ok as still reversible
30              
31             # from GraphQL::Debug
32             sub _debug {
33 0     0   0 my $func = shift;
34 0         0 require Data::Dumper;
35 0         0 require Test::More;
36 0         0 local ($Data::Dumper::Sortkeys, $Data::Dumper::Indent, $Data::Dumper::Terse);
37 0         0 $Data::Dumper::Sortkeys = $Data::Dumper::Indent = $Data::Dumper::Terse = 1;
38 0         0 Test::More::diag("$func: ", Data::Dumper::Dumper([ @_ ]));
39             }
40              
41             # heuristic 1: strip out single-item objects
42             sub _strip_thin {
43 5     5   20 my ($defs) = @_;
44             my @thin = grep {
45 5         31 my @props = keys %{ $defs->{$_}{properties} };
  54         88  
  54         220  
46 54 100 100     290 @props == 1 or (@props == 2 and grep /count/i, @props)
47             } keys %$defs;
48 5         14 if (DEBUG) {
49             _debug("OpenAPI($_) thin, ignoring", $defs->{$_}{properties})
50             for sort @thin;
51             }
52 5         20 @thin;
53             }
54              
55             # heuristic 2: find objects with same propnames, drop those with longer names
56             sub _strip_dup {
57 5     5   19 my ($defs, $def2mask, $reffed) = @_;
58 5         12 my %sig2names;
59 5         30 push @{ $sig2names{$def2mask->{$_}} }, $_ for keys %$def2mask;
  36         806  
60 5         100 DEBUG and _debug("OpenAPI sig2names", \%sig2names);
61 5         33 my @nondups = grep @{ $sig2names{$_} } == 1, keys %sig2names;
  35         83  
62 5         28 delete @sig2names{@nondups};
63 5         14 my @dups;
64 5         18 for my $sig (keys %sig2names) {
65 1 50       3 next if grep $reffed->{$_}, @{ $sig2names{$sig} };
  1         6  
66 0         0 my @names = sort { (length $a <=> length $b) } @{ $sig2names{$sig} };
  0         0  
  0         0  
67 0         0 DEBUG and _debug("OpenAPI dup($sig)", \@names);
68 0         0 shift @names; # keep the first i.e. shortest
69 0         0 push @dups, @names;
70             }
71 5         12 DEBUG and _debug("dup ret", \@dups);
72 5         29 @dups;
73             }
74              
75             # sorted list of all propnames
76             sub _get_all_propnames {
77 7     7   26 my ($defs) = @_;
78 7         15 my %allprops;
79 7         34 for my $defname (keys %$defs) {
80 108         179 $allprops{$_} = 1 for keys %{ $defs->{$defname}{properties} };
  108         497  
81             }
82 7         169 [ sort keys %allprops ];
83             }
84              
85             sub defs2mask {
86 7     7 1 2184 my ($defs) = @_;
87 7         33 my $allpropnames = _get_all_propnames($defs);
88 7         27 my $count = 0;
89 7         17 my %prop2count;
90 7         20 for my $propname (@$allpropnames) {
91 181         283 $prop2count{$propname} = $count;
92 181         296 $count++;
93             }
94 7         17 my %def2mask;
95 7         31 for my $defname (keys %$defs) {
96 108   33     172862 $def2mask{$defname} ||= Math::BigInt->new(0);
97             $def2mask{$defname} |= (Math::BigInt->new(1) << $prop2count{$_})
98 108         12863 for keys %{ $defs->{$defname}{properties} };
  108         650  
99             }
100 7         10836 \%def2mask;
101             }
102              
103             # heuristic 3: find objects with set of propnames that is subset of
104             # another object's propnames
105             sub _strip_subset {
106 5     5   29 my ($defs, $def2mask, $reffed) = @_;
107 5         60 my %subsets;
108 5         43 for my $defname (keys %$defs) {
109 36         3736 DEBUG and _debug("_strip_subset $defname maybe", $reffed);
110 36 100       136 next if $reffed->{$defname};
111 21         52 my $thismask = $def2mask->{$defname};
112 21         127 for my $supersetname (grep $_ ne $defname, keys %$defs) {
113 168         33045 my $supermask = $def2mask->{$supersetname};
114 168 100       403 next unless ($thismask & $supermask) == $thismask;
115 11         2066 DEBUG and _debug("mask $defname subset $supersetname");
116 11         46 $subsets{$defname} = 1;
117             }
118             }
119 5         388 my @subset = keys %subsets;
120 5         11 DEBUG and _debug("subset ret", [ sort @subset ]);
121 5         22 @subset;
122             }
123              
124             sub _prop2sqltype {
125 153     153   302 my ($prop) = @_;
126 153   100     583 my $format_type = $prop->{format} || $prop->{type};
127 153   100     564 my $lookup = $TYPE2SQL{$format_type || ''};
128 153         222 DEBUG and _debug("_prop2sqltype($format_type)($lookup)", $prop);
129 153         446 my %retval = (data_type => $lookup);
130 153 100       249 if (@{$prop->{enum} || []}) {
  153 100       668  
131 5         36 $retval{data_type} = 'enum';
132 5         14 $retval{extra} = { list => [ @{$prop->{enum}} ] };
  5         23  
133             }
134 153         291 DEBUG and _debug("_prop2sqltype(end)", \%retval);
135 153         376 \%retval;
136             }
137              
138             sub _make_not_null {
139 98     98   235 my ($table, $field_in) = @_;
140 98 100       356 my @fields = ref($field_in) eq 'ARRAY' ? @$field_in : $field_in;
141 98         213 for my $field (@fields) {
142 101         2275 $field->is_nullable(0);
143             }
144             $table->add_constraint(type => $_, fields => \@fields)
145 98         73514 for (NOT_NULL);
146             }
147              
148             sub _make_pk {
149 37     37   472 my ($table, $field_in) = @_;
150 37 100       155 my @fields = ref($field_in) eq 'ARRAY' ? @$field_in : $field_in;
151 37         691 $_->is_primary_key(1) for @fields;
152 37 100       1649 $fields[0]->is_auto_increment(1) if @fields == 1;
153             $table->add_constraint(type => $_, fields => \@fields)
154 37         835 for (PRIMARY_KEY);
155 37         85659 my $index = $table->add_index(
156             name => join('_', 'pk', map $_->name, @fields),
157             fields => \@fields,
158             );
159 37         27973 _make_not_null($table, \@fields);
160             }
161              
162             sub _def2tablename {
163 62     62   210 to_PL decamelize $_[0];
164             }
165              
166             sub _ref2def {
167 101     101   16671 my ($ref) = @_;
168 101 50       600 $ref =~ s:^#/definitions/:: or return;
169 101         381 $ref;
170             }
171              
172             sub _make_fk {
173 25     25   2561 my ($table, $field, $foreign_tablename, $foreign_id) = @_;
174 25         93 $table->add_constraint(
175             type => FOREIGN_KEY, fields => $field,
176             reference_table => $foreign_tablename,
177             reference_fields => $foreign_id,
178             );
179             }
180              
181             sub _get_entity {
182 25     25   63 my ($schema, $name, $view2real) = @_;
183 25 100       62 $schema->get_table($name) || $schema->get_table($view2real->{$name});
184             }
185              
186             sub _fk_hookup {
187 25     25   79 my ($schema, $fromtable, $fromkey, $totable, $tokey, $required, $view2real) = @_;
188 25         40 DEBUG and _debug("_fk_hookup $fromtable.$fromkey $totable.$tokey $required");
189 25         97 my $from_obj = $schema->get_table($fromtable);
190 25         335 my $to_obj = _get_entity($schema, $totable, $view2real);
191 25         6211 my $tokey_obj = $to_obj->get_field($tokey);
192 25   66     1323 my $field = $from_obj->get_field($fromkey) || $from_obj->add_field(
193             name => $fromkey, data_type => $tokey_obj->data_type,
194             );
195 25 50       38412 die $from_obj->error if !$field;
196 25         6292 _make_fk($from_obj, $field, $to_obj->name, $tokey);
197 25 100       28983 _make_not_null($from_obj, $field) if $required;
198 25         24300 $field;
199             }
200              
201             sub _def2table {
202 38     38   454 my ($name, $def, $schema, $m2m, $view2real) = @_;
203 38         109 my $props = $def->{properties};
204 38         98 my $tname = _def2tablename($name);
205 38         68981 DEBUG and _debug("_def2table($name)($tname)($m2m)", $props);
206 38 100       148 if (my $view_of = $def->{'x-view-of'}) {
207 1         5 my $target_table = _def2tablename($view_of);
208 1         990 $view2real->{$tname} = $target_table;
209 1         5 return (undef, []);
210             }
211             my $table = $schema->add_table(
212             name => $tname, comments => $def->{description},
213 37         232 );
214 37 100 100     27015 if (!$props->{id} and !$m2m) {
215             # we need a relational id
216 24         102 $props->{id} = { type => 'integer' };
217             }
218 37 100       77 my %prop2required = map { ($_ => 1) } @{ $def->{required} || [] };
  56         191  
  37         185  
219 37         83 my (@fixups);
220 37         259 for my $propname (sort keys %$props) {
221 176         52091 my $field;
222 176         460 my $thisprop = $props->{$propname};
223 176         279 DEBUG and _debug("_def2table($propname)");
224 176 100 100     903 if (my $ref = $thisprop->{'$ref'}) {
    100          
225             push @fixups, {
226             from => $tname,
227             fromkey => $propname . '_id',
228             to => _def2tablename(_ref2def($ref)),
229             tokey => 'id',
230 12         116 required => $prop2required{$propname},
231             type => 'one',
232             };
233             } elsif (($thisprop->{type} // '') eq 'array') {
234 11 50       77 if (my $ref = $thisprop->{items}{'$ref'}) {
235 11         71 push @fixups, {
236             from => _ref2def(_def2tablename($ref)),
237             fromkey => to_S($propname) . "_id",
238             to => $tname,
239             tokey => 'id',
240             required => 1,
241             type => 'many',
242             };
243             }
244 11         6612 DEBUG and _debug("_def2table(array)($propname)", \@fixups);
245             } else {
246 153         424 my $sqltype = _prop2sqltype($thisprop);
247             $field = $table->add_field(
248             name => $propname, %$sqltype, comments => $thisprop->{description},
249 153         850 );
250 153 100       206035 if ($propname eq 'id') {
251 34         122 _make_pk($table, $field);
252             }
253             }
254 176 100 100     52924 if ($field and $prop2required{$propname} and $propname ne 'id') {
      100        
255 39         9539 _make_not_null($table, $field);
256             }
257             }
258 37 100       19461 if ($m2m) {
259 3         13 _make_pk($table, scalar $table->get_fields);
260             }
261 37         3571 DEBUG and _debug("table", $table, \@fixups);
262 37         177 ($table, \@fixups);
263             }
264              
265             # mutates $def
266             sub _merge_one {
267 6     6   13 my ($def, $from, $ignore_required) = @_;
268 6         10 DEBUG and _debug('OpenAPI._merge_one', $def, $from);
269 6 50       36 push @{ $def->{required} }, @{ $from->{required} || [] } if !$ignore_required;
  4 100       11  
  4         27  
270 6 50       14 $def->{properties} = { %{$def->{properties} || {}}, %{$from->{properties}} };
  6         23  
  6         34  
271 6 100       37 $def->{type} = $from->{type} if $from->{type};
272             }
273              
274             sub _merge_allOf {
275 5     5   15 my ($defs) = @_;
276 5         12 DEBUG and _debug('OpenAPI._merge_allOf', $defs);
277             my %def2discrim = map {
278 1         5 ($_ => 1)
279 5         43 } grep $defs->{$_}{discriminator}, keys %$defs;
280 5         15 my %def2referrers;
281 5         29 for my $defname (sort keys %$defs) {
282 38         70 my $thisdef = $defs->{$defname};
283 38 100       87 next if !exists $thisdef->{allOf};
284 4         9 for my $partial (@{ $thisdef->{allOf} }) {
  4         13  
285 8 100       28 next if !(my $ref = $partial->{'$ref'});
286 4         30 push @{ $def2referrers{_ref2def($ref)} }, $defname;
  4         13  
287             }
288             }
289 5         13 DEBUG and _debug('OpenAPI._merge_allOf(def2referrers)', \%def2referrers);
290 5         15 my %newdefs;
291             my %def2ignore;
292 5         26 for my $defname (sort grep $def2discrim{$_}, keys %def2referrers) {
293             # assimilate instead of be assimilated by
294 1         4 $def2ignore{$defname} = 1;
295 1         2 my $thisdef = $defs->{$defname};
296 1         7 my %new = %$thisdef;
297 1         3 for my $assimilee (@{ $def2referrers{$defname} }) {
  1         4  
298 2         6 $def2ignore{$assimilee} = 1;
299 2         4 my $assimileedef = $defs->{$assimilee};
300 2         4 my @all = @{ $assimileedef->{allOf} };
  2         6  
301 2         5 for my $partial (@all) {
302 4 100       29 next if exists $partial->{'$ref'};
303 2         8 _merge_one(\%new, $partial, 1);
304             }
305             }
306 1         5 $newdefs{$defname} = \%new;
307             }
308 5         44 for my $defname (sort grep !$def2ignore{$_}, keys %$defs) {
309 35         73 my $thisdef = $defs->{$defname};
310 35         123 my %new = %$thisdef;
311 35 100       95 if (exists $thisdef->{allOf}) {
312 2         13 my @all = @{ delete $thisdef->{allOf} };
  2         9  
313 2         8 for my $partial (@all) {
314 4 100       17 if (exists $partial->{'$ref'}) {
315 2         29 _merge_one(\%new, $defs->{ _ref2def($partial->{'$ref'}) }, 0);
316             } else {
317 2         7 _merge_one(\%new, $partial, 0);
318             }
319             }
320             }
321 35         97 $newdefs{$defname} = \%new;
322             }
323 5         14 DEBUG and _debug('OpenAPI._merge_allOf(end)', \%newdefs);
324 5         43 \%newdefs;
325             }
326              
327             sub _find_referenced {
328 5     5   16 my ($defs) = @_;
329 5         9 DEBUG and _debug('OpenAPI._find_referenced', $defs);
330 5         14 my %reffed;
331 5         40 for my $defname (sort keys %$defs) {
332 36   50     106 my $theseprops = $defs->{$defname}{properties} || {};
333 36         104 for my $propname (keys %$theseprops) {
334 158 100 100     792 if (my $ref = $theseprops->{$propname}{'$ref'}
335             || ($theseprops->{$propname}{items} && $theseprops->{$propname}{items}{'$ref'})
336             ) {
337 22         158 $reffed{ _ref2def($ref) } = 1;
338             }
339             }
340             }
341 5         27 DEBUG and _debug('OpenAPI._find_referenced(end)', \%reffed);
342 5         21 \%reffed;
343             }
344              
345             sub _extract_objects {
346 5     5   15 my ($defs) = @_;
347 5         11 DEBUG and _debug('OpenAPI._extract_objects', $defs);
348 5         58 my %newdefs = %$defs;
349 5         48 for my $defname (sort keys %$defs) {
350 29   50     111 my $theseprops = $defs->{$defname}{properties} || {};
351 29         94 for my $propname (keys %$theseprops) {
352 137         315 my $thisprop = $theseprops->{$propname};
353             next if $thisprop->{'$ref'}
354 137 100 100     448 or $thisprop->{items} && $thisprop->{items}{'$ref'};
      100        
355 119         164 my $ref;
356 119 100 100     419 if (($thisprop->{type} // '') eq 'object') {
    50 50        
      66        
357 5         10 $ref = $thisprop;
358             } elsif (
359             $thisprop->{items} && ($thisprop->{items}{type} // '') eq 'object'
360             ) {
361 0         0 $ref = $thisprop->{items};
362             } else {
363 114         225 next;
364             }
365 5         35 my $newtype = join '', map camelize($_), $defname, $propname;
366 5         131 $newdefs{$newtype} = { %$ref };
367 5         25 %$ref = ('$ref' => "#/definitions/$newtype");
368             }
369             }
370 5         28 DEBUG and _debug('OpenAPI._extract_objects(end)', [ sort keys %newdefs ], \%newdefs);
371 5         48 \%newdefs;
372             }
373              
374             sub _extract_array_simple {
375 5     5   13 my ($defs) = @_;
376 5         11 DEBUG and _debug('OpenAPI._extract_array_simple', $defs);
377 5         39 my %newdefs = %$defs;
378 5         38 for my $defname (sort keys %$defs) {
379 34   100     126 my $theseprops = $defs->{$defname}{properties} || {};
380 34         103 for my $propname (keys %$theseprops) {
381 141         357 my $thisprop = $theseprops->{$propname};
382 141 100       286 next if $thisprop->{'$ref'};
383             next unless
384 124 100 50     344 $thisprop->{items} && ($thisprop->{items}{type} // '') ne 'object';
      100        
385 2         4 my $ref = $thisprop->{items};
386 2         14 my $newtype = join '', map camelize($_), $defname, $propname;
387 2         74 $newdefs{$newtype} = {
388             type => 'object',
389             properties => {
390             value => { %$ref }
391             },
392             required => [ 'value' ],
393             };
394 2         13 %$ref = ('$ref' => "#/definitions/$newtype");
395             }
396             }
397 5         20 DEBUG and _debug('OpenAPI._extract_array_simple(end)', [ sort keys %newdefs ], \%newdefs);
398 5         43 \%newdefs;
399             }
400              
401             sub _fixup_addProps {
402 5     5   12 my ($defs) = @_;
403 5         18 DEBUG and _debug('OpenAPI._fixup_addProps', $defs);
404 5         49 my %def2aP = map {$_,1} grep $defs->{$_}{additionalProperties}, keys %$defs;
  3         10  
405 5         22 DEBUG and _debug("OpenAPI._fixup_addProps(d2aP)", \%def2aP);
406 5         33 for my $defname (sort keys %$defs) {
407 36   100     101 my $theseprops = $defs->{$defname}{properties} || {};
408 36         53 DEBUG and _debug("OpenAPI._fixup_addProps(arrayfix)($defname)", $theseprops);
409 36         122 for my $propname (keys %$theseprops) {
410 143         305 my $thisprop = $theseprops->{$propname};
411 143         195 DEBUG and _debug("OpenAPI._fixup_addProps(p)($propname)", $thisprop);
412             next unless $thisprop->{'$ref'}
413 143 100 66     478 or $thisprop->{items} && $thisprop->{items}{'$ref'};
      100        
414 25         127 DEBUG and _debug("OpenAPI._fixup_addProps(p)($propname)(y)");
415 25         35 my $ref;
416 25 100 33     103 if ($thisprop->{'$ref'}) {
    50          
417 17         61 $ref = $thisprop;
418             } elsif ($thisprop->{items} && $thisprop->{items}{'$ref'}) {
419 8         45 $ref = $thisprop->{items};
420             } else {
421 0         0 next;
422             }
423 25         62 my $refname = $ref->{'$ref'};
424 25         89 DEBUG and _debug("OpenAPI._fixup_addProps(p)($propname)(y2)($refname)", $ref);
425 25 100       60 next if !$def2aP{_ref2def($refname)};
426 3         14 %$ref = (type => 'array', items => { '$ref' => $refname });
427 3         6 DEBUG and _debug("OpenAPI._fixup_addProps(p)($propname)(y3)", $ref);
428             }
429             }
430 5         48 my %newdefs = %$defs;
431 5         26 for my $defname (keys %def2aP) {
432             my %kv = (type => 'object', properties => {
433             key => { type => 'string' },
434             value => { type => $defs->{$defname}{additionalProperties}{type} },
435 3         17 });
436 3         9 $newdefs{$defname} = \%kv;
437             }
438 5         11 DEBUG and _debug('OpenAPI._fixup_addProps(end)', \%newdefs);
439 5         41 \%newdefs;
440             }
441              
442             sub _absorb_nonobject {
443 5     5   21 my ($defs) = @_;
444 5         11 DEBUG and _debug('OpenAPI._absorb_nonobject', $defs);
445 5         81 my %def2nonobj = map {$_,1} grep $defs->{$_}{type} ne 'object', keys %$defs;
  1         5  
446 5         16 DEBUG and _debug("OpenAPI._absorb_nonobject(d2nonobj)", \%def2nonobj);
447 5         35 for my $defname (sort keys %$defs) {
448 36   50     99 my $theseprops = $defs->{$defname}{properties} || {};
449 36         54 DEBUG and _debug("OpenAPI._absorb_nonobject(t)($defname)", $theseprops);
450 36         98 for my $propname (keys %$theseprops) {
451 149         252 my $thisprop = $theseprops->{$propname};
452 149         203 DEBUG and _debug("OpenAPI._absorb_nonobject(p)($propname)", $thisprop);
453             next unless $thisprop->{'$ref'}
454 149 100 66     519 or $thisprop->{items} && $thisprop->{items}{'$ref'};
      100        
455 25         127 DEBUG and _debug("OpenAPI._absorb_nonobject(p)($propname)(y)");
456 25         37 my $ref;
457 25 100 33     94 if ($thisprop->{'$ref'}) {
    50          
458 14         55 $ref = $thisprop;
459             } elsif ($thisprop->{items} && $thisprop->{items}{'$ref'}) {
460 11         41 $ref = $thisprop->{items};
461             } else {
462 0         0 next;
463             }
464 25         61 my $refname = $ref->{'$ref'};
465 25         88 DEBUG and _debug("OpenAPI._absorb_nonobject(p)($propname)(y2)($refname)", $ref);
466 25         52 my $refdef = _ref2def($refname);
467 25 100       73 next if !$def2nonobj{$refdef};
468 2         6 %$ref = %{ $defs->{$refdef} };
  2         27  
469 2         48 DEBUG and _debug("OpenAPI._absorb_nonobject(p)($propname)(y3)", $ref);
470             }
471             }
472 5         39 my %newdefs = %$defs;
473 5         40 delete @newdefs{ keys %def2nonobj };
474 5         12 DEBUG and _debug('OpenAPI._absorb_nonobject(end)', \%newdefs);
475 5         54 \%newdefs;
476             }
477              
478             sub _tuple2name {
479 10     10   6103 my ($fixup) = @_;
480 10         25 my $from = $fixup->{from};
481 10         18 my $fromkey = $fixup->{fromkey};
482 10         64 $fromkey =~ s#_id$##;
483 10         40 camelize join '_', map to_S($_), $from, $fromkey;
484             }
485              
486             sub _make_many2many {
487 5     5   18 my ($fixups, $schema) = @_;
488 5         26 DEBUG and _debug("tables to do", $fixups);
489 5         42 my @manyfixups = grep $_->{type} eq 'many', @$fixups;
490 5         12 my %from_tos;
491 5         18 push @{ $from_tos{$_->{from}}{$_->{to}} }, $_ for @manyfixups;
  11         55  
492 5         11 my %to_froms;
493 5         17 push @{ $to_froms{$_->{to}}{$_->{from}} }, $_ for @manyfixups;
  11         51  
494 5         17 my %m2m;
495             my %ref2nonm2mfixup;
496 5         42 $ref2nonm2mfixup{$_} = $_ for @$fixups;
497 5         26 for my $from (keys %from_tos) {
498 10         17 for my $to (keys %{ $from_tos{$from} }) {
  10         37  
499 10         17 for my $fixup (@{ $from_tos{$from}{$to} }) {
  10         27  
500 11         23 for my $other (@{ $to_froms{$from}{$to} }) {
  11         41  
501 5         26 my ($f1, $f2) = sort { $a->{from} cmp $b->{from} } $fixup, $other;
  5         23  
502 5         22 $m2m{_tuple2name($f1)}{_tuple2name($f2)} = [ $f1, $f2 ];
503 5         6983 delete $ref2nonm2mfixup{$_} for $f1, $f2;
504             }
505             }
506             }
507             }
508 5         21 my @replacefixups;
509 5         32 for my $n1 (sort keys %m2m) {
510 3         271 for my $n2 (sort keys %{ $m2m{$n1} }) {
  3         14  
511 3         8 my ($f1, $f2) = @{ $m2m{$n1}{$n2} };
  3         13  
512 3         21 my ($t1_obj, $t2_obj) = map $schema->get_table($_->{to}), $f1, $f2;
513 3         78 my $f1_fromkey = $f1->{fromkey};
514 3         6 my $f2_fromkey = $f2->{fromkey};
515 3 100 66     31 if ($f1_fromkey eq $f2_fromkey and $f1->{from} eq $f2->{from}) {
516 1         8 $f1_fromkey =~ s#_id$#_to_id#;
517 1         7 $f2_fromkey =~ s#_id$#_from_id#;
518             }
519             my ($table) = _def2table(
520             $n1.$n2,
521             {
522             type => 'object',
523             properties => {
524             $f1_fromkey => {
525             type => $SQL2TYPE{$t1_obj->get_field($f1->{tokey})->data_type}
526             },
527             $f2_fromkey => {
528 3         18 type => $SQL2TYPE{$t2_obj->get_field($f2->{tokey})->data_type}
529             },
530             },
531             },
532             $schema,
533             1,
534             );
535             push @replacefixups, {
536             to => $f1->{from},
537             tokey => 'id',
538             from => $table->name,
539             fromkey => $f1_fromkey,
540             required => 1,
541             }, {
542             to => $f2->{from},
543 3         71 tokey => 'id',
544             from => $table->name,
545             fromkey => $f2_fromkey,
546             required => 1,
547             };
548             }
549             }
550             my @newfixups = (
551             (sort {
552 5         513 $a->{from} cmp $b->{from} || $a->{fromkey} cmp $b->{fromkey}
553 40 50       104 } values %ref2nonm2mfixup),
554             @replacefixups,
555             );
556 5         14 DEBUG and _debug("fixups still to do", \@newfixups);
557 5         39 \@newfixups;
558             }
559              
560             sub _remove_fields {
561 10     10   30 my ($defs, $name) = @_;
562 10         17 DEBUG and _debug("OpenAPI._remove_fields($name)", $defs);
563 10         60 for my $defname (sort keys %$defs) {
564 76   50     221 my $theseprops = $defs->{$defname}{properties} || {};
565 76         100 DEBUG and _debug("OpenAPI._remove_fields(t)($defname)", $theseprops);
566 76         183 for my $propname (keys %$theseprops) {
567 297         594 my $thisprop = $theseprops->{$propname};
568 297         398 DEBUG and _debug("OpenAPI._remove_fields(p)($propname)", $thisprop);
569 297 100       679 delete $theseprops->{$propname} if $thisprop->{$name};
570             }
571             }
572             }
573              
574             sub parse {
575 5     5 1 226928 my ($tr, $data) = @_;
576 5         61 my $openapi_schema = JSON::Validator::OpenAPI->new->schema($data)->schema;
577 5         25134 my %defs = %{ $openapi_schema->get("/definitions") };
  5         24  
578 5         221 DEBUG and _debug('OpenAPI.definitions', \%defs);
579 5         173 my $schema = $tr->schema;
580 5         18180 DEBUG and $schema->translator(undef); # reduce debug output
581 5         33 my @thin = _strip_thin(\%defs);
582 5         12 DEBUG and _debug("thin ret", \@thin);
583 5         18 delete @defs{@thin};
584 5         26 _remove_fields(\%defs, 'x-artifact');
585 5         25 _remove_fields(\%defs, 'x-input-only');
586 5         15 %defs = %{ _merge_allOf(\%defs) };
  5         26  
587 5         36 my $def2mask = defs2mask(\%defs);
588 5         29 my $reffed = _find_referenced(\%defs);
589 5         27 my @dup = _strip_dup(\%defs, $def2mask, $reffed);
590 5         14 delete @defs{@dup};
591 5         45 my @subset = _strip_subset(\%defs, $def2mask, $reffed);
592 5         52 delete @defs{@subset};
593 5         14 %defs = %{ _extract_objects(\%defs) };
  5         23  
594 5         24 %defs = %{ _extract_array_simple(\%defs) };
  5         25  
595 5         22 my (@fixups, %view2real);
596 5         10 %defs = %{ _fixup_addProps(\%defs) };
  5         20  
597 5         28 %defs = %{ _absorb_nonobject(\%defs) };
  5         42  
598 5         48 for my $name (sort keys %defs) {
599 35         164 my ($table, $thesefixups) = _def2table($name, $defs{$name}, $schema, 0, \%view2real);
600 35         115 push @fixups, @$thesefixups;
601             }
602 5         39 my ($newfixups) = _make_many2many(\@fixups, $schema);
603 5         20 for my $fixup (@$newfixups) {
604 25         54 _fk_hookup($schema, @{$fixup}{qw(from fromkey to tokey required)}, \%view2real);
  25         113  
605             }
606 5         268 1;
607             }
608              
609             =encoding utf-8
610              
611             =head1 NAME
612              
613             SQL::Translator::Parser::OpenAPI - convert OpenAPI schema to SQL::Translator schema
614              
615             =begin markdown
616              
617             # PROJECT STATUS
618              
619             | OS | Build status |
620             |:-------:|--------------:|
621             | Linux | [![Build Status](https://travis-ci.org/mohawk2/SQL-Translator-Parser-OpenAPI.svg?branch=master)](https://travis-ci.org/mohawk2/SQL-Translator-Parser-OpenAPI) |
622              
623             [![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)
624              
625             =end markdown
626              
627             =head1 SYNOPSIS
628              
629             use SQL::Translator;
630             use SQL::Translator::Parser::OpenAPI;
631              
632             my $translator = SQL::Translator->new;
633             $translator->parser("OpenAPI");
634             $translator->producer("YAML");
635             $translator->translate($file);
636              
637             # or...
638             $ sqlt -f OpenAPI -t MySQL my-mysqlschema.sql
639              
640             =head1 DESCRIPTION
641              
642             This module implements a L to convert
643             a L specification to a L.
644              
645             It uses, from the given API spec, the given "definitions" to generate
646             tables in an RDBMS with suitable columns and types.
647              
648             To try to make the data model represent the "real" data, it applies heuristics:
649              
650             =over
651              
652             =item *
653              
654             to remove object definitions that only have one property (which the
655             author calls "thin objects"), or that have two properties, one of whose
656             names has the substring "count" (case-insensitive).
657              
658             =item *
659              
660             for definitions that have C, either merge them together if there
661             is a C, or absorb properties from referred definitions
662              
663             =item *
664              
665             to find object definitions that have all the same properties as another,
666             and remove all but the shortest-named one
667              
668             =item *
669              
670             to remove object definitions whose properties are a strict subset
671             of another
672              
673             =item *
674              
675             creates object definitions for any properties that are an object
676              
677             =item *
678              
679             creates object definitions for any properties that are an array of simple
680             OpenAPI types (e.g. C)
681              
682             =item *
683              
684             creates object definitions for any objects that are
685             C (i.e. freeform key/value pairs), that are
686             key/value rows
687              
688             =item *
689              
690             absorbs any definitions that are in fact not objects, into the referring
691             property
692              
693             =item *
694              
695             injects foreign-key relationships for array-of-object properties, and
696             creates many-to-many tables for any two-way array relationships
697              
698             =back
699              
700             =head1 ARGUMENTS
701              
702             None at present.
703              
704             =head1 PACKAGE FUNCTIONS
705              
706             =head2 parse
707              
708             Standard as per L. The input $data is a scalar
709             that can be understood as a L
710             specification|JSON::Validator/schema>.
711              
712             =head2 defs2mask
713              
714             Given a hashref that is a JSON pointer to an OpenAPI spec's
715             C, returns a hashref that maps each definition name to a
716             bitmask. The bitmask is set from each property name in that definition,
717             according to its order in the complete sorted list of all property names
718             in the definitions. Not exported. E.g.
719              
720             # properties:
721             my $defs = {
722             d1 => {
723             properties => {
724             p1 => 'string',
725             p2 => 'string',
726             },
727             },
728             d2 => {
729             properties => {
730             p2 => 'string',
731             p3 => 'string',
732             },
733             },
734             };
735             my $mask = SQL::Translator::Parser::OpenAPI::defs2mask($defs);
736             # all prop names, sorted: qw(p1 p2 p3)
737             # $mask:
738             {
739             d1 => (1 << 0) | (1 << 1),
740             d2 => (1 << 1) | (1 << 2),
741             }
742              
743             =head1 OPENAPI SPEC EXTENSIONS
744              
745             =head2 C
746              
747             Under C, a key of C will name another
748             definition (NB: not a full JSON pointer). That will make C<$defname>
749             not be created as a table. The handling of creating the "view" of the
750             relevant table is left to the CRUD implementation. This gives it scope
751             to use things like the current requesting user, or web parameters,
752             which otherwise would require a parameterised view. These are not widely
753             available.
754              
755             =head2 C
756              
757             Under C, a key of
758             C with a true value will indicate this is not to be stored,
759             and will not cause a column to be created. The value will instead be
760             derived by other means. The value of this key may become the definition
761             of that derivation.
762              
763             =head2 C
764              
765             Under C, a key of
766             C with a true value will indicate this is not to be stored,
767             and will not cause a column to be created. This may end up being merged
768             with C.
769              
770             =head1 DEBUGGING
771              
772             To debug, set environment variable C to a true value.
773              
774             =head1 AUTHOR
775              
776             Ed J, C<< >>
777              
778             =head1 LICENSE
779              
780             Copyright (C) Ed J
781              
782             This library is free software; you can redistribute it and/or modify
783             it under the same terms as Perl itself.
784              
785             =head1 SEE ALSO
786              
787             L.
788              
789             L.
790              
791             L.
792              
793             =cut
794              
795             1;