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 100.0
pod 3 3 100.0
total 661 704 94.0


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