File Coverage

blib/lib/GraphQL/Plugin/Convert/OpenAPI.pm
Criterion Covered Total %
statement 282 295 95.5
branch 109 128 85.1
condition 68 82 82.9
subroutine 24 24 100.0
pod 1 2 50.0
total 484 531 91.1


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