File Coverage

blib/lib/GraphQL/Plugin/Convert/OpenAPI.pm
Criterion Covered Total %
statement 286 301 95.0
branch 110 130 84.6
condition 68 82 82.9
subroutine 25 25 100.0
pod 1 2 50.0
total 490 540 90.7


line stmt bran cond sub pod time code
1             package GraphQL::Plugin::Convert::OpenAPI;
2 8     8   5509206 use 5.008001;
  8         36  
3 8     8   47 use strict;
  8         19  
  8         186  
4 8     8   42 use warnings;
  8         17  
  8         232  
5 8     8   3905 use GraphQL::Schema;
  8         9880105  
  8         451  
6 8     8   5239 use GraphQL::Plugin::Type::DateTime;
  8         5191979  
  8         424  
7 8     8   89 use GraphQL::Debug qw(_debug);
  8         21  
  8         539  
8 8     8   7237 use JSON::Validator::OpenAPI::Mojolicious;
  8         3020628  
  8         114  
9 8     8   6066 use OpenAPI::Client;
  8         27327  
  8         86  
10              
11             our $VERSION = "0.21";
12 8     8   593 use constant DEBUG => $ENV{GRAPHQL_DEBUG};
  8         27  
  8         35795  
13              
14             my %TYPEMAP = (
15             string => 'String',
16             date => 'DateTime',
17             integer => 'Int',
18             number => 'Float',
19             boolean => 'Boolean',
20             file => 'String',
21             );
22             my %TYPE2SCALAR = map { ($_ => 1) } qw(ID String Int Float Boolean DateTime);
23             my %METHOD2MUTATION = map { ($_ => 1) } qw(post put patch delete);
24             my @METHODS = (keys %METHOD2MUTATION, qw(get options head));
25             my %KIND2SIMPLE = (scalar => 1, enum => 1);
26              
27             sub _apply_modifier {
28 1006     1006   3386 my ($modifier, $typespec) = @_;
29 1006 100       2441 return $typespec if !$modifier;
30 373 50       812 if (ref $modifier eq 'ARRAY') {
31             # several at once! order is innermost first
32 0         0 $typespec = _apply_modifier($_, $typespec) for @$modifier;
33 0         0 return $typespec;
34             }
35 373 50 100     1267 return $typespec if $modifier eq 'non_null'
      66        
36             and ref $typespec eq 'ARRAY'
37             and $typespec->[0] eq 'non_null'; # no double-non_null
38 373         2377 [ $modifier, { type => $typespec } ];
39             }
40              
41             sub _remove_modifiers {
42 2015     2015   3597 my ($typespec) = @_;
43 2015 100       4282 return _remove_modifiers($typespec->{type}) if ref $typespec eq 'HASH';
44 1651 100       4754 return $typespec if ref $typespec ne 'ARRAY';
45 364         762 _remove_modifiers($typespec->[1]);
46             }
47              
48             sub _map_args {
49 24     24   65 my ($type, $args, $type2info) = @_;
50 24         39 DEBUG and _debug('OpenAPI._map_args', $type, $args, $type2info);
51 24 50       58 die "Undefined type" if !defined $type;
52 24 100 100     173 return $args if $TYPE2SCALAR{$type} or ($type2info->{$type}||{})->{is_enum};
      100        
53 11 100       37 if (ref $type eq 'ARRAY') {
54             # type modifiers
55 3         15 my ($mod, $typespec) = @$type;
56 3 100       15 return _map_args($typespec->{type}, @_[1..3]) if $mod eq 'non_null';
57 2 50       8 die "Invalid typespec @$type" if $mod ne 'list';
58 2         18 return [ map _map_args($typespec->{type}, $_, @_[2..3]), @$args ];
59             }
60 8         29 my $field2prop = $type2info->{$type}{field2prop};
61 8         26 my $field2type = $type2info->{$type}{field2type};
62 8         18 my $field2is_hashpair = $type2info->{$type}{field2is_hashpair};
63             +{ map {
64 8         31 my $value;
  10         20  
65 10 100       36 if ($field2is_hashpair->{$_}) {
66 1         7 my $pairtype = _remove_modifiers($field2type->{$_});
67 1         6 my $value_type = $type2info->{$pairtype}{field2type}{value};
68 1         3 my $pairs = $args->{$_};
69 1         2 my %hashval;
70 1         6 for my $pair (@$pairs) {
71             $hashval{$pair->{key}} = _map_args(
72 1         5 $value_type, $pair->{value}, $type2info,
73             );
74             }
75 1         2 DEBUG and _debug('OpenAPI._map_args(hashpair)', $type, $pairtype, $pairs, $value_type, \%hashval);
76 1         4 $value = \%hashval;
77             } else {
78             $value = _map_args(
79 9         41 $field2type->{$_}, $args->{$_}, $type2info,
80             );
81             }
82 10         77 ($field2prop->{$_} => $value)
83             } keys %$args };
84             }
85              
86             sub make_field_resolver {
87 7     7 1 12411283 my ($type2info) = @_;
88 7         20 DEBUG and _debug('OpenAPI.make_field_resolver', $type2info);
89             sub {
90 9     9   253833 my ($root_value, $args, $context, $info) = @_;
91 9         32 my $field_name = $info->{field_name};
92 9         227 my $parent_type = $info->{parent_type}->to_string;
93 9         279 my $pseudo_type = join '.', $parent_type, $field_name;
94 9         20 DEBUG and _debug('OpenAPI.resolver', $root_value, $field_name, $pseudo_type, $args);
95 9 50 66     76 if (
      33        
96             ref($root_value) eq 'HASH' and
97             $type2info->{$parent_type} and
98             my $prop = $type2info->{$parent_type}{field2prop}{$field_name}
99             ) {
100 3         16 return $root_value->{$prop};
101             }
102             my $property = ref($root_value) eq 'HASH'
103 6 50       27 ? $root_value->{$field_name}
104             : $root_value;
105 6         16 my $result = eval {
106 6 50       24 return $property->($args, $context, $info) if ref $property eq 'CODE';
107 6 50       36 return $property if ref $root_value eq 'HASH';
108 6 50       38 if (!UNIVERSAL::isa($root_value, 'OpenAPI::Client')) {
109 0 0 0     0 return $property // die "OpenAPI.resolver could not resolve '$field_name'\n"
110             if !$root_value->can($field_name);
111 0         0 return $root_value->$field_name($args, $context, $info);
112             }
113             # call OAC method
114 6         35 my $operationId = $type2info->{$parent_type}{field2operationId}{$field_name};
115 6         37 my $mapped_args = _map_args(
116             $pseudo_type,
117             $args,
118             $type2info,
119             );
120 6         14 DEBUG and _debug('OpenAPI.resolver(c)', $operationId, $args, $mapped_args);
121             my $got = $root_value->call_p($operationId => $mapped_args)->then(
122             sub {
123 6         104137 my $res = shift->res;
124 6         36 DEBUG and _debug('OpenAPI.resolver(res)', $res);
125 6 50       23 die $res->body."\n" if $res->is_error;
126 6         151 my $json = $res->json;
127 6         886 DEBUG and _debug('OpenAPI.resolver(got)', $json);
128 6         25 my $return_type = $info->{return_type};
129 6         63 $return_type = $return_type->of while $return_type->can('of');
130 6 100       195 if ($type2info->{$return_type->to_string}{is_hashpair}) {
131             $json = [ map {
132 1         8 +{ key => $_, value => $json->{$_} }
133 1 50       68 } sort keys %{$json || {}} ];
  1         8  
134             }
135 6         309 DEBUG and _debug('OpenAPI.resolver(rettype)', $return_type->to_string, $json);
136 6         26 $json;
137             }, sub {
138 0         0 DEBUG and _debug('OpenAPI.resolver(error)', shift->res->body);
139 0         0 die shift->res->body . "\n";
140             }
141 6         64 );
142             };
143 6 50       26644 die $@ if $@;
144 6         37 $result;
145 7         627 };
146             }
147              
148             sub _trim_name {
149 1153     1153   2093 my ($name) = @_;
150 1153 50       2275 return if !defined $name;
151 1153         2721 $name =~ s#[^a-zA-Z0-9_]+#_#g;
152 1153         2501 $name;
153             }
154              
155             sub _get_type {
156 1067     1067   2049 my ($info, $maybe_name, $name2type, $type2info) = @_;
157 1067         1618 DEBUG and _debug("_get_type($maybe_name)", $info);
158 1067 100 100     4066 return 'String' if !$info or !%$info; # bodge but unavoidable
159             # ignore definitions that are an array as not GQL-idiomatic, deal as array
160 1050 100 100     4056 if ($info->{'$ref'} and ($info->{type}//'') ne 'array') {
      100        
161 214         3516 DEBUG and _debug("_get_type($maybe_name) ref");
162 214         629 my $rawtype = $info->{'$ref'};
163 214         1870 $rawtype =~ s:^#/definitions/::;
164 214         748 return $rawtype;
165             }
166 836 100 100     3905 if (
      100        
      100        
167             $info->{additionalProperties}
168             or (($info->{type}//'') eq 'object' and !$info->{properties})
169             ) {
170             my $type = _get_type(
171             {
172             type => 'array',
173             items => {
174             type => 'object',
175             properties => {
176             key => { type => 'string' },
177 6   100     108 value => $info->{additionalProperties} // { type => 'string' },
178             },
179             },
180             },
181             $maybe_name,
182             $name2type,
183             $type2info,
184             );
185 6         30 DEBUG and _debug("_get_type($maybe_name) aP", $type);
186 6         18 $type2info->{$maybe_name}{is_hashpair} = 1;
187 6         20 return $type;
188             }
189 830 100 66     3734 if ($info->{properties} or $info->{allOf} or $info->{enum}) {
      66        
190 22         37 DEBUG and _debug("_get_type($maybe_name) p");
191 22         81 return _get_spec_from_info(
192             $maybe_name, $info,
193             $name2type,
194             $type2info,
195             );
196             }
197 808 100       1852 if ($info->{type} eq 'array') {
198 73         178 DEBUG and _debug("_get_type($maybe_name) a");
199             return _apply_modifier(
200             'list',
201             _get_type(
202 73         329 $info->{items}, $maybe_name,
203             $name2type,
204             $type2info,
205             )
206             );
207             }
208             return 'DateTime'
209             if ($info->{type}//'') eq 'string'
210 735 100 50     3155 and ($info->{format}//'') eq 'date-time';
      100        
      100        
211 724         1052 DEBUG and _debug("_get_type($maybe_name) simple");
212             $TYPEMAP{$info->{type}}
213 724   50     2434 // die "'$maybe_name' unknown data type: @{[$info->{type}]}\n";
  0         0  
214             }
215              
216             sub _refinfo2fields {
217 152     152   295 my ($name, $refinfo, $name2type, $type2info) = @_;
218 152         241 my %fields;
219 152         277 my $properties = $refinfo->{properties};
220 152 100       234 my %required = map { ($_ => 1) } @{$refinfo->{required} || []};
  88         283  
  152         571  
221 152         643 for my $prop (keys %$properties) {
222 669         1611 my $info = $properties->{$prop};
223 669         1265 my $field = _trim_name($prop);
224 669         1787 $type2info->{$name}{field2prop}{$field} = $prop;
225 669         990 DEBUG and _debug("_refinfo2fields($name) $prop/$field", $info, $type2info->{$name});
226 669         1987 my $rawtype = _get_type(
227             $info, $name.ucfirst($field),
228             $name2type,
229             $type2info,
230             );
231             my $fulltype = _apply_modifier(
232 669   100     1998 $required{$prop} && 'non_null',
233             $rawtype,
234             );
235 669         1638 $type2info->{$name}{field2type}{$field} = $fulltype;
236 669         1751 $fields{$field} = +{ type => $fulltype };
237             $fields{$field}->{description} = $info->{description}
238 669 100       1721 if $info->{description};
239             }
240 152         1161 \%fields;
241             }
242              
243             sub _merge_fields {
244 8     8   18 my ($f1, $f2) = @_;
245 8         28 my %merged = %$f1;
246 8         29 for my $k (keys %$f2) {
247 24 100       55 if (exists $merged{$k}) {
248 1 50       4 $merged{$k} = $f2->{$k} if ref $f2->{$k}{type}; # ie modified ie non-null
249             } else {
250 23         80 $merged{$k} = $f2->{$k};
251             }
252             }
253 8         70 \%merged;
254             }
255              
256             sub _get_spec_from_info {
257             my (
258 159     159   339 $name, $refinfo,
259             $name2type,
260             $type2info,
261             ) = @_;
262 159         234 DEBUG and _debug("_get_spec_from_info($name)", $refinfo);
263 159         252 my %implements;
264 159         275 my $fields = {};
265 159 100       497 if ($refinfo->{allOf}) {
    100          
266 4         20 for my $schema (@{$refinfo->{allOf}}) {
  4         15  
267 8         15 DEBUG and _debug("_get_spec_from_info($name)(allOf)", $schema);
268 8 100       33 if ($schema->{'$ref'}) {
269 4         35 my $othertype = _get_type($schema, '$ref', $name2type, $type2info);
270 4         13 my $othertypedef = $name2type->{$othertype};
271 2         10 push @{$implements{interfaces}}, $othertype
272 4 100       15 if $othertypedef->{kind} eq 'interface';
273 4         13 $fields = _merge_fields($fields, $othertypedef->{fields});
274             } else {
275 4         17 $fields = _merge_fields($fields, _refinfo2fields(
276             $name, $schema,
277             $name2type,
278             $type2info,
279             ));
280             }
281             }
282             } elsif (my $values = $refinfo->{enum}) {
283 7         20 my (%enum2value, %trimmed2suffix);
284 7         14 for my $uniqvalue (sort keys %{{ @$values, reverse @$values }}) {
  7         74  
285 21         47 my $trimmed = _trim_name($uniqvalue);
286 21 100       61 $trimmed = 'EMPTY' if !length $trimmed;
287 21   100     97 $trimmed .= $trimmed2suffix{$trimmed}++ || '';
288 21         76 $enum2value{$trimmed} = { value => $uniqvalue };
289             }
290 7         107 DEBUG and _debug("_get_spec_from_info($name)(enum)", $values, \%enum2value);
291 7         33 my $spec = +{
292             kind => 'enum',
293             name => $name,
294             values => \%enum2value,
295             };
296 7 50       28 $spec->{description} = $refinfo->{title} if $refinfo->{title};
297             $spec->{description} = $refinfo->{description}
298 7 100       27 if $refinfo->{description};
299 7         21 $name2type->{$name} = $spec;
300 7         21 $type2info->{$name}{is_enum} = 1;
301 7         33 return $name;
302             } else {
303 148         316 %$fields = (%$fields, %{_refinfo2fields(
  148         321  
304             $name, $refinfo,
305             $name2type,
306             $type2info,
307             )});
308             }
309             my $spec = +{
310 152 100       775 kind => $refinfo->{discriminator} ? 'interface' : 'type',
311             name => $name,
312             fields => $fields,
313             %implements,
314             };
315 152 100       383 $spec->{description} = $refinfo->{title} if $refinfo->{title};
316             $spec->{description} = $refinfo->{description}
317 152 100       326 if $refinfo->{description};
318 152         309 $name2type->{$name} = $spec;
319 152         426 $name;
320             }
321              
322             sub _make_union {
323 116     116   227 my ($types, $name2type) = @_;
324 116         199 my %seen;
325 116         288 my $types2 = [ sort grep !$seen{$_}++, map _remove_modifiers($_), @$types ];
326 116 50       474 return $types->[0] if @$types2 == 1; # no need for a union
327 0         0 my $typename = join '', @$types2, 'Union';
328 0         0 DEBUG and _debug("_make_union", $types, $types2, $typename);
329 0   0     0 $name2type->{$typename} ||= {
330             name => $typename,
331             kind => 'union',
332             types => $types2,
333             };
334 0         0 $typename;
335             }
336              
337             sub _make_input {
338 372     372   743 my ($type, $name2type, $type2info) = @_;
339 372         519 DEBUG and _debug("_make_input", $type);
340 372 100       837 $type = $type->{type} if ref $type eq 'HASH';
341 372 100       746 if (ref $type eq 'ARRAY') {
342             # modifiers, recurse
343 55         158 return _apply_modifier(
344             $type->[0],
345             _make_input(
346             $type->[1],
347             $name2type,
348             $type2info,
349             ),
350             )
351             }
352             return $type
353             if $TYPE2SCALAR{$type}
354             or $name2type->{$type}{kind} eq 'enum'
355 317 100 100     1344 or $name2type->{$type}{kind} eq 'input';
      66        
356             # not deal with unions for now
357             # is an output "type"
358 39         109 my $input_name = $type.'Input';
359 39         115 my $typedef = $name2type->{$type};
360             my $inputdef = $name2type->{$input_name} ||= {
361             name => $input_name,
362             kind => 'input',
363             $typedef->{description} ? (description => $typedef->{description}) : (),
364             fields => +{
365             map {
366 108         234 my $fielddef = $typedef->{fields}{$_};
367             ($_ => +{
368             %$fielddef, type => _make_input(
369             $fielddef->{type},
370 108         416 $name2type,
371             $type2info,
372             ),
373             })
374 39 100 100     217 } keys %{$typedef->{fields}}
  35         159  
375             },
376             };
377 39         99 my $inputdef_fields = $inputdef->{fields};
378 39         126 $type2info->{$input_name}{field2prop} = $type2info->{$type}{field2prop};
379             $type2info->{$input_name}{field2type} = +{
380             map {
381 39         136 ($_ => $inputdef_fields->{$_}{type})
382 138         439 } keys %$inputdef_fields
383             };
384 39         89 DEBUG and _debug("_make_input(object)($input_name)", $typedef, $type2info->{$input_name}, $type2info->{$type}, $name2type, $type2info);
385 39         154 $input_name;
386             }
387              
388             sub _resolve_schema_ref {
389 315     315   3160 my ($obj, $schema) = @_;
390 315         896 my $ref = $obj->{'$ref'};
391 315 100       1466 return $obj if !$ref;
392 112         361 $ref =~ s{^#}{};
393 112         416 $schema->get($ref);
394             }
395              
396             sub _kind2name2endpoint {
397 7     7   413 my ($paths, $schema, $name2type, $type2info) = @_;
398 7         19 my %kind2name2endpoint;
399 7         63 for my $path (keys %$paths) {
400 95         570 for my $method (grep $paths->{$path}{$_}, @METHODS) {
401 116         243 my $info = $paths->{$path}{$method};
402 116   66     367 my $op_id = $info->{operationId} || $method.'_'._trim_name($path);
403 116         398 my $fieldname = _trim_name($op_id);
404 116 100       315 my $kind = $METHOD2MUTATION{$method} ? 'mutation' : 'query';
405 116         489 $type2info->{ucfirst $kind}{field2operationId}{$fieldname} = $op_id;
406             my @successresponses = map _resolve_schema_ref($_, $schema),
407             map $info->{responses}{$_},
408 116         258 grep /^2/, keys %{$info->{responses}};
  116         881  
409 116         528 DEBUG and _debug("_kind2name2endpoint($path)($method)($fieldname)($op_id)", $info->{responses}, \@successresponses);
410             my @responsetypes = map _get_type(
411 116         422 $_->{schema}, $fieldname.'Return',
412             $name2type,
413             $type2info,
414             ), @successresponses;
415 116 100       359 @responsetypes = ('String') if !@responsetypes; # void return
416 116         274 my $union = _make_union(
417             \@responsetypes,
418             $name2type,
419             );
420             my @parameters = map _resolve_schema_ref($_, $schema),
421 116         219 @{ $info->{parameters} };
  116         408  
422 116         1848 my $pseudo_type = join '.', ucfirst($kind), $fieldname;
423             my %args = map {
424 116         230 my $argprop = $_->{name};
  209         661  
425 209         407 my $argfield = _trim_name($argprop);
426 209         835 $type2info->{$pseudo_type}{field2prop}{$argfield} = $argprop;
427             my $type = _get_type(
428 209 100       738 $_->{schema} ? $_->{schema} : $_, "${fieldname}_$argfield",
429             $name2type,
430             $type2info,
431             );
432 209         446 my $typename = _remove_modifiers($type);
433 209   100     763 my $is_hashpair = ($type2info->{$typename}||{})->{is_hashpair};
434 209         524 $type = _make_input(
435             $type,
436             $name2type,
437             $type2info,
438             );
439 209 100       483 $type2info->{$pseudo_type}{field2is_hashpair}{$argfield} = $is_hashpair
440             if $is_hashpair;
441 209         563 $type2info->{$pseudo_type}{field2type}{$argfield} = $type;
442             ($argfield => {
443             type => _apply_modifier($_->{required} && 'non_null', $type),
444 209 100 100     1081 $_->{description} ? (description => $_->{description}) : (),
445             })
446             } @parameters;
447 116         471 DEBUG and _debug("_kind2name2endpoint($fieldname) params", \%args);
448 116   100     423 my $description = $info->{summary} || $info->{description};
449 116 100       819 $kind2name2endpoint{$kind}->{$fieldname} = +{
    100          
450             type => $union,
451             $description ? (description => $description) : (),
452             %args ? (args => \%args) : (),
453             };
454             }
455             }
456 7         43 (\%kind2name2endpoint);
457             }
458              
459             # possible "kind"s: scalar enum type input union interface
460             # mutates %$name2typeused - is boolean
461             sub _walk_type {
462 975     975   1799 my ($name, $name2typeused, $name2type) = @_;
463 975         1426 DEBUG and _debug("OpenAPI._walk_type", $name, $name2typeused);#, $name2type
464 975 100       1887 return if $name2typeused->{$name}; # seen - stop
465 890 100       1961 return if $TYPE2SCALAR{$name}; # builtin scalar - stop
466 155         354 $name2typeused->{$name} = 1;
467 155         378 my $type = $name2type->{$name};
468 155 100       426 return if $KIND2SIMPLE{ $type->{kind} }; # no sub-fields, types, etc - stop
469 148 50       294 if ($type->{kind} eq 'union') {
470 0         0 DEBUG and _debug("OpenAPI._walk_type(union)");
471 0         0 _walk_type($_, $name2typeused, $name2type) for @{$type->{types}};
  0         0  
472 0         0 return;
473             }
474 148 100       315 if ($type->{kind} eq 'interface') {
475 1         5 DEBUG and _debug("OpenAPI._walk_type(interface)");
476 1         7 for my $maybe_type (values %$name2type) {
477 40 100 100     150 next if $maybe_type->{kind} ne 'type' or !$maybe_type->{interfaces};
478 2 50       4 next if !grep $_ eq $name, @{$maybe_type->{interfaces}};
  2         10  
479 2         9 _walk_type($maybe_type->{name}, $name2typeused, $name2type);
480             }
481             # continue to pick up the fields' types too
482             }
483             # now only input and output object remain (but still interfaces too)
484 148         215 for my $fieldname (keys %{ $type->{fields} }) {
  148         637  
485 751         1505 my $field_def = $type->{fields}{$fieldname};
486 751         1007 DEBUG and _debug("OpenAPI._walk_type($name)(*object)", $field_def);
487 751         1647 _walk_type(_remove_modifiers($field_def->{type}), $name2typeused, $name2type);
488 751 100       1152 next if !%{ $field_def->{args} || {} };
  751 100       2614  
489 106         169 for my $argname (keys %{ $field_def->{args} }) {
  106         356  
490 209         324 DEBUG and _debug("OpenAPI._walk_type(arg)($argname)");
491 209         399 my $arg_def = $field_def->{args}{$argname};
492 209         447 _walk_type(_remove_modifiers($arg_def->{type}), $name2typeused, $name2type);
493             }
494             }
495             }
496              
497             sub to_graphql {
498 7     7 0 3921 my ($class, $spec, $app) = @_;
499 7 100       134 my %appargs = (app => $app) if $app;
500 7         107 my $openapi_schema = JSON::Validator::OpenAPI::Mojolicious->new(
501             %appargs
502             )->schema($spec)->schema;
503 7         2325917 DEBUG and _debug('OpenAPI.schema', $openapi_schema);
504 7         97 my $defs = $openapi_schema->get("/definitions");
505 7         537 my @ast;
506             my (
507 7         21 %name2type,
508             %type2info,
509             );
510             # all non-interface-consumers first
511             # also drop defs that are an array as not GQL-idiomatic - treat as that array
512 7   100     546 for my $name (
513             grep !$defs->{$_}{allOf} && ($defs->{$_}{type}//'') ne 'array', keys %$defs
514             ) {
515             _get_spec_from_info(
516 133         314 _trim_name($name), $defs->{$name},
517             \%name2type,
518             \%type2info,
519             );
520             }
521             # now interface-consumers and can now put in interface fields too
522 7         215 for my $name (grep $defs->{$_}{allOf}, keys %$defs) {
523             _get_spec_from_info(
524 4         12 _trim_name($name), $defs->{$name},
525             \%name2type,
526             \%type2info,
527             );
528             }
529 7         90 my ($kind2name2endpoint) = _kind2name2endpoint(
530             $openapi_schema->get("/paths"), $openapi_schema,
531             \%name2type,
532             \%type2info,
533             );
534 7         46 for my $kind (keys %$kind2name2endpoint) {
535             $name2type{ucfirst $kind} = +{
536             kind => 'type',
537             name => ucfirst $kind,
538 13         75 fields => { %{ $kind2name2endpoint->{$kind} } },
  13         203  
539             };
540             }
541 7         24 my %name2typeused;
542             _walk_type(ucfirst $_, \%name2typeused, \%name2type)
543 7         49 for keys %$kind2name2endpoint;
544 7         313 push @ast, map $name2type{$_}, keys %name2typeused;
545             +{
546 7         143 schema => GraphQL::Schema->from_ast(\@ast),
547             root_value => OpenAPI::Client->new($openapi_schema->data, %appargs),
548             resolver => make_field_resolver(\%type2info),
549             };
550             }
551              
552             =encoding utf-8
553              
554             =head1 NAME
555              
556             GraphQL::Plugin::Convert::OpenAPI - convert OpenAPI schema to GraphQL schema
557              
558             =begin markdown
559              
560             # PROJECT STATUS
561              
562             | OS | Build status |
563             |:-------:|--------------:|
564             | Linux | [![Build Status](https://travis-ci.org/graphql-perl/GraphQL-Plugin-Convert-OpenAPI.svg?branch=master)](https://travis-ci.org/graphql-perl/GraphQL-Plugin-Convert-OpenAPI) |
565              
566             [![CPAN version](https://badge.fury.io/pl/GraphQL-Plugin-Convert-OpenAPI.svg)](https://metacpan.org/pod/GraphQL::Plugin::Convert::OpenAPI) [![Coverage Status](https://coveralls.io/repos/github/graphql-perl/GraphQL-Plugin-Convert-OpenAPI/badge.svg?branch=master)](https://coveralls.io/github/graphql-perl/GraphQL-Plugin-Convert-OpenAPI?branch=master)
567              
568             =end markdown
569              
570             =head1 SYNOPSIS
571              
572             use GraphQL::Plugin::Convert::OpenAPI;
573             my $converted = GraphQL::Plugin::Convert::OpenAPI->to_graphql(
574             'file-containing-spec.json',
575             );
576             print $converted->{schema}->to_doc;
577              
578             =head1 DESCRIPTION
579              
580             This module implements the L<GraphQL::Plugin::Convert> API to convert
581             a L<JSON::Validator::OpenAPI::Mojolicious> specification to L<GraphQL::Schema> etc.
582              
583             It uses, from the given API spec:
584              
585             =over
586              
587             =item * the given "definitions" as output types
588              
589             =item * the given "definitions" as input types when required for an
590             input parameter
591              
592             =item * the given operations as fields of either C<Query> if a C<GET>,
593             or C<Mutation> otherwise
594              
595             =back
596              
597             If an output type has C<additionalProperties> (effectively a hash whose
598             values are of a specified type), this poses a problem for GraphQL which
599             does not have such a concept. It will be treated as being made up of a
600             list of pairs of objects (i.e. hashes) with two keys: C<key> and C<value>.
601              
602             The queries will be run against the spec's server. If the spec starts
603             with a C</>, and a L<Mojolicious> app is supplied (see below), that
604             server will instead be the given app.
605              
606             =head1 ARGUMENTS
607              
608             To the C<to_graphql> method: a URL to a specification, or a filename
609             containing a JSON specification, or a data structure, of an OpenAPI v2.
610              
611             Optionally, a L<Mojolicious> app can be given as the second argument. In
612             this case, with a L<Mojolicious::Lite> app, do:
613              
614             my $api = plugin OpenAPI => {spec => 'data://main/api.yaml'};
615             plugin(GraphQL => {convert => [ 'OpenAPI', $api->validator->bundle, app ]});
616              
617             with the usual mapping in the case of a full app. For this to work you
618             need L<Mojolicious::Plugin::OpenAPI> version 1.25+, which returns itself
619             on C<register>.
620              
621             =head1 PACKAGE FUNCTIONS
622              
623             =head2 make_field_resolver
624              
625             This is available as C<\&GraphQL::Plugin::Convert::OpenAPI::make_field_resolver>
626             in case it is wanted for use outside of the "bundle" of the C<to_graphql>
627             method. It takes arguments:
628              
629             =over
630              
631             =item
632              
633             a hash-ref mapping from a GraphQL type-name to another hash-ref with
634             information about that type. There are addition pseudo-types with stored
635             information, named eg C<TypeName.fieldName>, for the obvious
636             purpose. The use of C<.> avoids clashing with real types. This will only
637             have information about input types.
638              
639             Valid keys:
640              
641             =over
642              
643             =item is_hashpair
644              
645             True value if that type needs transforming from a hash into pairs.
646              
647             =item field2operationId
648              
649             Hash-ref mapping from a GraphQL operation field-name (which will
650             only be done on the C<Query> or C<Mutation> types, for obvious reasons)
651             to an C<operationId>.
652              
653             =item field2type
654              
655             Hash-ref mapping from a GraphQL type's field-name to hash-ref mapping
656             its arguments, if any, to the corresponding GraphQL type-name.
657              
658             =item field2prop
659              
660             Hash-ref mapping from a GraphQL type's field-name to the corresponding
661             OpenAPI property-name.
662              
663             =item is_enum
664              
665             Boolean value indicating whether the type is a L<GraphQL::Type::Enum>.
666              
667             =back
668              
669             =back
670              
671             and returns a closure that can be used as a field resolver.
672              
673             =head1 DEBUGGING
674              
675             To debug, set environment variable C<GRAPHQL_DEBUG> to a true value.
676              
677             =head1 AUTHOR
678              
679             Ed J, C<< <etj at cpan.org> >>
680              
681             Parts based on L<https://github.com/yarax/swagger-to-graphql>
682              
683             =head1 LICENSE
684              
685             Copyright (C) Ed J
686              
687             This library is free software; you can redistribute it and/or modify
688             it under the same terms as Perl itself.
689              
690             =cut
691              
692             1;