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   5474495 use 5.008001;
  8         31  
3 8     8   43 use strict;
  8         15  
  8         166  
4 8     8   39 use warnings;
  8         16  
  8         201  
5 8     8   2900 use GraphQL::Schema;
  8         9539345  
  8         438  
6 8     8   4128 use GraphQL::Plugin::Type::DateTime;
  8         4849763  
  8         388  
7 8     8   82 use GraphQL::Debug qw(_debug);
  8         24  
  8         461  
8 8     8   3518 use JSON::Validator;
  8         2667987  
  8         92  
9 8     8   4135 use OpenAPI::Client;
  8         24943  
  8         85  
10              
11             our $VERSION = "0.22";
12 8     8   626 use constant DEBUG => $ENV{GRAPHQL_DEBUG};
  8         23  
  8         34078  
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   2961 my ($modifier, $typespec) = @_;
29 1006 100       1994 return $typespec if !$modifier;
30 373 50       702 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     1091 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         2026 [ $modifier, { type => $typespec } ];
39             }
40              
41             sub _remove_modifiers {
42 2015     2015   3003 my ($typespec) = @_;
43 2015 100       3594 return _remove_modifiers($typespec->{type}) if ref $typespec eq 'HASH';
44 1651 100       4192 return $typespec if ref $typespec ne 'ARRAY';
45 364         613 _remove_modifiers($typespec->[1]);
46             }
47              
48             sub _map_args {
49 24     24   61 my ($type, $args, $type2info) = @_;
50 24         35 DEBUG and _debug('OpenAPI._map_args', $type, $args, $type2info);
51 24 50       59 die "Undefined type" if !defined $type;
52 24 100 100     184 return $args if $TYPE2SCALAR{$type} or ($type2info->{$type}||{})->{is_enum};
      100        
53 11 100       33 if (ref $type eq 'ARRAY') {
54             # type modifiers
55 3         14 my ($mod, $typespec) = @$type;
56 3 100       36 return _map_args($typespec->{type}, @_[1..3]) if $mod eq 'non_null';
57 2 50       12 die "Invalid typespec @$type" if $mod ne 'list';
58 2         14 return [ map _map_args($typespec->{type}, $_, @_[2..3]), @$args ];
59             }
60 8         30 my $field2prop = $type2info->{$type}{field2prop};
61 8         21 my $field2type = $type2info->{$type}{field2type};
62 8         18 my $field2is_hashpair = $type2info->{$type}{field2is_hashpair};
63             +{ map {
64 8         27 my $value;
  10         17  
65 10 100       30 if ($field2is_hashpair->{$_}) {
66 1         10 my $pairtype = _remove_modifiers($field2type->{$_});
67 1         6 my $value_type = $type2info->{$pairtype}{field2type}{value};
68 1         3 my $pairs = $args->{$_};
69 1         8 my %hashval;
70 1         4 for my $pair (@$pairs) {
71             $hashval{$pair->{key}} = _map_args(
72 1         12 $value_type, $pair->{value}, $type2info,
73             );
74             }
75 1         6 DEBUG and _debug('OpenAPI._map_args(hashpair)', $type, $pairtype, $pairs, $value_type, \%hashval);
76 1         6 $value = \%hashval;
77             } else {
78             $value = _map_args(
79 9         50 $field2type->{$_}, $args->{$_}, $type2info,
80             );
81             }
82 10         69 ($field2prop->{$_} => $value)
83             } keys %$args };
84             }
85              
86             sub make_field_resolver {
87 7     7 1 11305247 my ($type2info) = @_;
88 7         19 DEBUG and _debug('OpenAPI.make_field_resolver', $type2info);
89             sub {
90 9     9   249616 my ($root_value, $args, $context, $info) = @_;
91 9         28 my $field_name = $info->{field_name};
92 9         218 my $parent_type = $info->{parent_type}->to_string;
93 9         331 my $pseudo_type = join '.', $parent_type, $field_name;
94 9         16 DEBUG and _debug('OpenAPI.resolver', $root_value, $field_name, $pseudo_type, $args);
95 9 50 66     73 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       24 ? $root_value->{$field_name}
104             : $root_value;
105 6         21 my $result = eval {
106 6 50       25 return $property->($args, $context, $info) if ref $property eq 'CODE';
107 6 50       29 return $property if ref $root_value eq 'HASH';
108 6 50       33 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         37 my $operationId = $type2info->{$parent_type}{field2operationId}{$field_name};
115 6         26 my $mapped_args = _map_args(
116             $pseudo_type,
117             $args,
118             $type2info,
119             );
120 6         18 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         99006 my $res = shift->res;
124 6         31 DEBUG and _debug('OpenAPI.resolver(res)', $res);
125 6 50       24 die $res->body."\n" if $res->is_error;
126 6         141 my $json = $res->json;
127 6         878 DEBUG and _debug('OpenAPI.resolver(got)', $json);
128 6         21 my $return_type = $info->{return_type};
129 6         67 $return_type = $return_type->of while $return_type->can('of');
130 6 100       166 if ($type2info->{$return_type->to_string}{is_hashpair}) {
131             $json = [ map {
132 1         7 +{ key => $_, value => $json->{$_} }
133 1 50       56 } sort keys %{$json || {}} ];
  1         7  
134             }
135 6         288 DEBUG and _debug('OpenAPI.resolver(rettype)', $return_type->to_string, $json);
136 6         27 $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         51 );
142             };
143 6 50       25663 die $@ if $@;
144 6         45 $result;
145 7         515 };
146             }
147              
148             sub _trim_name {
149 1153     1153   1836 my ($name) = @_;
150 1153 50       2024 return if !defined $name;
151 1153         2156 $name =~ s#[^a-zA-Z0-9_]+#_#g;
152 1153         2063 $name;
153             }
154              
155             sub _get_type {
156 1067     1067   1791 my ($info, $maybe_name, $name2type, $type2info) = @_;
157 1067         1338 DEBUG and _debug("_get_type($maybe_name)", $info);
158 1067 100 100     3278 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     3263 if ($info->{'$ref'} and ($info->{type}//'') ne 'array') {
      100        
161 214         2835 DEBUG and _debug("_get_type($maybe_name) ref");
162 214         528 my $rawtype = $info->{'$ref'};
163 214         1622 $rawtype =~ s:^#/definitions/::;
164 214         643 return $rawtype;
165             }
166 836 100 100     2905 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     47 value => $info->{additionalProperties} // { type => 'string' },
178             },
179             },
180             },
181             $maybe_name,
182             $name2type,
183             $type2info,
184             );
185 6         23 DEBUG and _debug("_get_type($maybe_name) aP", $type);
186 6         15 $type2info->{$maybe_name}{is_hashpair} = 1;
187 6         16 return $type;
188             }
189 830 100 66     2951 if ($info->{properties} or $info->{allOf} or $info->{enum}) {
      66        
190 22         33 DEBUG and _debug("_get_type($maybe_name) p");
191 22         67 return _get_spec_from_info(
192             $maybe_name, $info,
193             $name2type,
194             $type2info,
195             );
196             }
197 808 100       1610 if ($info->{type} eq 'array') {
198 73         170 DEBUG and _debug("_get_type($maybe_name) a");
199             return _apply_modifier(
200             'list',
201             _get_type(
202 73         192 $info->{items}, $maybe_name,
203             $name2type,
204             $type2info,
205             )
206             );
207             }
208             return 'DateTime'
209             if ($info->{type}//'') eq 'string'
210 735 100 50     2527 and ($info->{format}//'') eq 'date-time';
      100        
      100        
211 724         888 DEBUG and _debug("_get_type($maybe_name) simple");
212             $TYPEMAP{$info->{type}}
213 724   50     1760 // die "'$maybe_name' unknown data type: @{[$info->{type}]}\n";
  0         0  
214             }
215              
216             sub _refinfo2fields {
217 152     152   324 my ($name, $refinfo, $name2type, $type2info) = @_;
218 152         219 my %fields;
219 152         207 my $properties = $refinfo->{properties};
220 152 100       207 my %required = map { ($_ => 1) } @{$refinfo->{required} || []};
  88         256  
  152         485  
221 152         478 for my $prop (keys %$properties) {
222 669         1360 my $info = $properties->{$prop};
223 669         972 my $field = _trim_name($prop);
224 669         1500 $type2info->{$name}{field2prop}{$field} = $prop;
225 669         809 DEBUG and _debug("_refinfo2fields($name) $prop/$field", $info, $type2info->{$name});
226 669         1519 my $rawtype = _get_type(
227             $info, $name.ucfirst($field),
228             $name2type,
229             $type2info,
230             );
231             my $fulltype = _apply_modifier(
232 669   100     1560 $required{$prop} && 'non_null',
233             $rawtype,
234             );
235 669         1324 $type2info->{$name}{field2type}{$field} = $fulltype;
236 669         1443 $fields{$field} = +{ type => $fulltype };
237             $fields{$field}->{description} = $info->{description}
238 669 100       1471 if $info->{description};
239             }
240 152         1021 \%fields;
241             }
242              
243             sub _merge_fields {
244 8     8   15 my ($f1, $f2) = @_;
245 8         30 my %merged = %$f1;
246 8         27 for my $k (keys %$f2) {
247 24 100       50 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         58 $merged{$k} = $f2->{$k};
251             }
252             }
253 8         74 \%merged;
254             }
255              
256             sub _get_spec_from_info {
257             my (
258 159     159   310 $name, $refinfo,
259             $name2type,
260             $type2info,
261             ) = @_;
262 159         200 DEBUG and _debug("_get_spec_from_info($name)", $refinfo);
263 159         221 my %implements;
264 159         244 my $fields = {};
265 159 100       435 if ($refinfo->{allOf}) {
    100          
266 4         13 for my $schema (@{$refinfo->{allOf}}) {
  4         14  
267 8         16 DEBUG and _debug("_get_spec_from_info($name)(allOf)", $schema);
268 8 100       28 if ($schema->{'$ref'}) {
269 4         40 my $othertype = _get_type($schema, '$ref', $name2type, $type2info);
270 4         20 my $othertypedef = $name2type->{$othertype};
271 2         6 push @{$implements{interfaces}}, $othertype
272 4 100       13 if $othertypedef->{kind} eq 'interface';
273 4         23 $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         14 my (%enum2value, %trimmed2suffix);
284 7         14 for my $uniqvalue (sort keys %{{ @$values, reverse @$values }}) {
  7         59  
285 21         42 my $trimmed = _trim_name($uniqvalue);
286 21 100       61 $trimmed = 'EMPTY' if !length $trimmed;
287 21   100     86 $trimmed .= $trimmed2suffix{$trimmed}++ || '';
288 21         76 $enum2value{$trimmed} = { value => $uniqvalue };
289             }
290 7         60 DEBUG and _debug("_get_spec_from_info($name)(enum)", $values, \%enum2value);
291 7         31 my $spec = +{
292             kind => 'enum',
293             name => $name,
294             values => \%enum2value,
295             };
296 7 50       24 $spec->{description} = $refinfo->{title} if $refinfo->{title};
297             $spec->{description} = $refinfo->{description}
298 7 100       21 if $refinfo->{description};
299 7         19 $name2type->{$name} = $spec;
300 7         18 $type2info->{$name}{is_enum} = 1;
301 7         29 return $name;
302             } else {
303 148         257 %$fields = (%$fields, %{_refinfo2fields(
  148         283  
304             $name, $refinfo,
305             $name2type,
306             $type2info,
307             )});
308             }
309             my $spec = +{
310 152 100       696 kind => $refinfo->{discriminator} ? 'interface' : 'type',
311             name => $name,
312             fields => $fields,
313             %implements,
314             };
315 152 100       334 $spec->{description} = $refinfo->{title} if $refinfo->{title};
316             $spec->{description} = $refinfo->{description}
317 152 100       285 if $refinfo->{description};
318 152         261 $name2type->{$name} = $spec;
319 152         321 $name;
320             }
321              
322             sub _make_union {
323 116     116   209 my ($types, $name2type) = @_;
324 116         164 my %seen;
325 116         249 my $types2 = [ sort grep !$seen{$_}++, map _remove_modifiers($_), @$types ];
326 116 50       420 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   637 my ($type, $name2type, $type2info) = @_;
339 372         489 DEBUG and _debug("_make_input", $type);
340 372 100       700 $type = $type->{type} if ref $type eq 'HASH';
341 372 100       650 if (ref $type eq 'ARRAY') {
342             # modifiers, recurse
343 55         143 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     1132 or $name2type->{$type}{kind} eq 'input';
      66        
356             # not deal with unions for now
357             # is an output "type"
358 39         101 my $input_name = $type.'Input';
359 39         77 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         222 my $fielddef = $typedef->{fields}{$_};
367             ($_ => +{
368             %$fielddef, type => _make_input(
369             $fielddef->{type},
370 108         333 $name2type,
371             $type2info,
372             ),
373             })
374 39 100 100     194 } keys %{$typedef->{fields}}
  35         124  
375             },
376             };
377 39         95 my $inputdef_fields = $inputdef->{fields};
378 39         139 $type2info->{$input_name}{field2prop} = $type2info->{$type}{field2prop};
379             $type2info->{$input_name}{field2type} = +{
380             map {
381 39         134 ($_ => $inputdef_fields->{$_}{type})
382 138         450 } keys %$inputdef_fields
383             };
384 39         95 DEBUG and _debug("_make_input(object)($input_name)", $typedef, $type2info->{$input_name}, $type2info->{$type}, $name2type, $type2info);
385 39         120 $input_name;
386             }
387              
388             sub _resolve_schema_ref {
389 315     315   2414 my ($obj, $schema) = @_;
390 315         704 my $ref = $obj->{'$ref'};
391 315 100       1274 return $obj if !$ref;
392 112         315 $ref =~ s{^#}{};
393 112         264 $schema->get($ref);
394             }
395              
396             sub _kind2name2endpoint {
397 7     7   302 my ($paths, $schema, $name2type, $type2info) = @_;
398 7         29 my %kind2name2endpoint;
399 7         39 for my $path (keys %$paths) {
400 95         491 for my $method (grep $paths->{$path}{$_}, @METHODS) {
401 116         215 my $info = $paths->{$path}{$method};
402 116   66     299 my $op_id = $info->{operationId} || $method.'_'._trim_name($path);
403 116         218 my $fieldname = _trim_name($op_id);
404 116 100       282 my $kind = $METHOD2MUTATION{$method} ? 'mutation' : 'query';
405 116         414 $type2info->{ucfirst $kind}{field2operationId}{$fieldname} = $op_id;
406             my @successresponses = map _resolve_schema_ref($_, $schema),
407             map $info->{responses}{$_},
408 116         225 grep /^2/, keys %{$info->{responses}};
  116         774  
409 116         456 DEBUG and _debug("_kind2name2endpoint($path)($method)($fieldname)($op_id)", $info->{responses}, \@successresponses);
410             my @responsetypes = map _get_type(
411 116         336 $_->{schema}, $fieldname.'Return',
412             $name2type,
413             $type2info,
414             ), @successresponses;
415 116 100       278 @responsetypes = ('String') if !@responsetypes; # void return
416 116         263 my $union = _make_union(
417             \@responsetypes,
418             $name2type,
419             );
420             my @parameters = map _resolve_schema_ref($_, $schema),
421 116         198 @{ $info->{parameters} };
  116         303  
422 116         1445 my $pseudo_type = join '.', ucfirst($kind), $fieldname;
423             my %args = map {
424 116         252 my $argprop = $_->{name};
  209         564  
425 209         335 my $argfield = _trim_name($argprop);
426 209         662 $type2info->{$pseudo_type}{field2prop}{$argfield} = $argprop;
427             my $type = _get_type(
428 209 100       642 $_->{schema} ? $_->{schema} : $_, "${fieldname}_$argfield",
429             $name2type,
430             $type2info,
431             );
432 209         406 my $typename = _remove_modifiers($type);
433 209   100     624 my $is_hashpair = ($type2info->{$typename}||{})->{is_hashpair};
434 209         404 $type = _make_input(
435             $type,
436             $name2type,
437             $type2info,
438             );
439 209 100       390 $type2info->{$pseudo_type}{field2is_hashpair}{$argfield} = $is_hashpair
440             if $is_hashpair;
441 209         468 $type2info->{$pseudo_type}{field2type}{$argfield} = $type;
442             ($argfield => {
443             type => _apply_modifier($_->{required} && 'non_null', $type),
444 209 100 100     853 $_->{description} ? (description => $_->{description}) : (),
445             })
446             } @parameters;
447 116         440 DEBUG and _debug("_kind2name2endpoint($fieldname) params", \%args);
448 116   100     389 my $description = $info->{summary} || $info->{description};
449 116 100       710 $kind2name2endpoint{$kind}->{$fieldname} = +{
    100          
450             type => $union,
451             $description ? (description => $description) : (),
452             %args ? (args => \%args) : (),
453             };
454             }
455             }
456 7         31 (\%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   1450 my ($name, $name2typeused, $name2type) = @_;
463 975         1185 DEBUG and _debug("OpenAPI._walk_type", $name, $name2typeused);#, $name2type
464 975 100       1649 return if $name2typeused->{$name}; # seen - stop
465 890 100       1696 return if $TYPE2SCALAR{$name}; # builtin scalar - stop
466 155         286 $name2typeused->{$name} = 1;
467 155         257 my $type = $name2type->{$name};
468 155 100       350 return if $KIND2SIMPLE{ $type->{kind} }; # no sub-fields, types, etc - stop
469 148 50       251 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       278 if ($type->{kind} eq 'interface') {
475 1         2 DEBUG and _debug("OpenAPI._walk_type(interface)");
476 1         14 for my $maybe_type (values %$name2type) {
477 40 100 100     112 next if $maybe_type->{kind} ne 'type' or !$maybe_type->{interfaces};
478 2 50       5 next if !grep $_ eq $name, @{$maybe_type->{interfaces}};
  2         9  
479 2         6 _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         185 for my $fieldname (keys %{ $type->{fields} }) {
  148         859  
485 751         1237 my $field_def = $type->{fields}{$fieldname};
486 751         875 DEBUG and _debug("OpenAPI._walk_type($name)(*object)", $field_def);
487 751         1218 _walk_type(_remove_modifiers($field_def->{type}), $name2typeused, $name2type);
488 751 100       1040 next if !%{ $field_def->{args} || {} };
  751 100       2255  
489 106         147 for my $argname (keys %{ $field_def->{args} }) {
  106         292  
490 209         256 DEBUG and _debug("OpenAPI._walk_type(arg)($argname)");
491 209         326 my $arg_def = $field_def->{args}{$argname};
492 209         325 _walk_type(_remove_modifiers($arg_def->{type}), $name2typeused, $name2type);
493             }
494             }
495             }
496              
497             sub to_graphql {
498 7     7 0 4427 my ($class, $spec, $app) = @_;
499 7 100       125 my %appargs = (app => $app) if $app;
500 7         47 my $openapi_schema = JSON::Validator->new->schema($spec)->schema;
501 7         1975733 DEBUG and _debug('OpenAPI.schema', $openapi_schema);
502 7         76 my $defs = $openapi_schema->get("/definitions");
503 7         396 my @ast;
504             my (
505 7         20 %name2type,
506             %type2info,
507             );
508             # all non-interface-consumers first
509             # also drop defs that are an array as not GQL-idiomatic - treat as that array
510 7   100     361 for my $name (
511             grep !$defs->{$_}{allOf} && ($defs->{$_}{type}//'') ne 'array', keys %$defs
512             ) {
513             _get_spec_from_info(
514 133         274 _trim_name($name), $defs->{$name},
515             \%name2type,
516             \%type2info,
517             );
518             }
519             # now interface-consumers and can now put in interface fields too
520 7         134 for my $name (grep $defs->{$_}{allOf}, keys %$defs) {
521             _get_spec_from_info(
522 4         11 _trim_name($name), $defs->{$name},
523             \%name2type,
524             \%type2info,
525             );
526             }
527 7         58 my ($kind2name2endpoint) = _kind2name2endpoint(
528             $openapi_schema->get("/paths"), $openapi_schema,
529             \%name2type,
530             \%type2info,
531             );
532 7         32 for my $kind (keys %$kind2name2endpoint) {
533             $name2type{ucfirst $kind} = +{
534             kind => 'type',
535             name => ucfirst $kind,
536 13         64 fields => { %{ $kind2name2endpoint->{$kind} } },
  13         135  
537             };
538             }
539 7         20 my %name2typeused;
540             _walk_type(ucfirst $_, \%name2typeused, \%name2type)
541 7         51 for keys %$kind2name2endpoint;
542 7         125 push @ast, map $name2type{$_}, keys %name2typeused;
543             +{
544 7         107 schema => GraphQL::Schema->from_ast(\@ast),
545             root_value => OpenAPI::Client->new($openapi_schema->data, %appargs),
546             resolver => make_field_resolver(\%type2info),
547             };
548             }
549              
550             =encoding utf-8
551              
552             =head1 NAME
553              
554             GraphQL::Plugin::Convert::OpenAPI - convert OpenAPI schema to GraphQL schema
555              
556             =begin markdown
557              
558             # PROJECT STATUS
559              
560             | OS | Build status |
561             |:-------:|--------------:|
562             | 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) |
563              
564             [![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)
565              
566             =end markdown
567              
568             =head1 SYNOPSIS
569              
570             use GraphQL::Plugin::Convert::OpenAPI;
571             my $converted = GraphQL::Plugin::Convert::OpenAPI->to_graphql(
572             'file-containing-spec.json',
573             );
574             print $converted->{schema}->to_doc;
575              
576             =head1 DESCRIPTION
577              
578             This module implements the L<GraphQL::Plugin::Convert> API to convert
579             a L<JSON::Validator::OpenAPI::Mojolicious> specification to L<GraphQL::Schema> etc.
580              
581             It uses, from the given API spec:
582              
583             =over
584              
585             =item * the given "definitions" as output types
586              
587             =item * the given "definitions" as input types when required for an
588             input parameter
589              
590             =item * the given operations as fields of either C<Query> if a C<GET>,
591             or C<Mutation> otherwise
592              
593             =back
594              
595             If an output type has C<additionalProperties> (effectively a hash whose
596             values are of a specified type), this poses a problem for GraphQL which
597             does not have such a concept. It will be treated as being made up of a
598             list of pairs of objects (i.e. hashes) with two keys: C<key> and C<value>.
599              
600             The queries will be run against the spec's server. If the spec starts
601             with a C</>, and a L<Mojolicious> app is supplied (see below), that
602             server will instead be the given app.
603              
604             =head1 ARGUMENTS
605              
606             To the C<to_graphql> method: a URL to a specification, or a filename
607             containing a JSON specification, or a data structure, of an OpenAPI v2.
608              
609             Optionally, a L<Mojolicious> app can be given as the second argument. In
610             this case, with a L<Mojolicious::Lite> app, do:
611              
612             my $api = plugin OpenAPI => {spec => 'data://main/api.yaml'};
613             plugin(GraphQL => {convert => [ 'OpenAPI', $api->validator->bundle, app ]});
614              
615             with the usual mapping in the case of a full app. For this to work you
616             need L<Mojolicious::Plugin::OpenAPI> version 1.25+, which returns itself
617             on C<register>.
618              
619             =head1 PACKAGE FUNCTIONS
620              
621             =head2 make_field_resolver
622              
623             This is available as C<\&GraphQL::Plugin::Convert::OpenAPI::make_field_resolver>
624             in case it is wanted for use outside of the "bundle" of the C<to_graphql>
625             method. It takes arguments:
626              
627             =over
628              
629             =item
630              
631             a hash-ref mapping from a GraphQL type-name to another hash-ref with
632             information about that type. There are addition pseudo-types with stored
633             information, named eg C<TypeName.fieldName>, for the obvious
634             purpose. The use of C<.> avoids clashing with real types. This will only
635             have information about input types.
636              
637             Valid keys:
638              
639             =over
640              
641             =item is_hashpair
642              
643             True value if that type needs transforming from a hash into pairs.
644              
645             =item field2operationId
646              
647             Hash-ref mapping from a GraphQL operation field-name (which will
648             only be done on the C<Query> or C<Mutation> types, for obvious reasons)
649             to an C<operationId>.
650              
651             =item field2type
652              
653             Hash-ref mapping from a GraphQL type's field-name to hash-ref mapping
654             its arguments, if any, to the corresponding GraphQL type-name.
655              
656             =item field2prop
657              
658             Hash-ref mapping from a GraphQL type's field-name to the corresponding
659             OpenAPI property-name.
660              
661             =item is_enum
662              
663             Boolean value indicating whether the type is a L<GraphQL::Type::Enum>.
664              
665             =back
666              
667             =back
668              
669             and returns a closure that can be used as a field resolver.
670              
671             =head1 DEBUGGING
672              
673             To debug, set environment variable C<GRAPHQL_DEBUG> to a true value.
674              
675             =head1 AUTHOR
676              
677             Ed J, C<< <etj at cpan.org> >>
678              
679             Parts based on L<https://github.com/yarax/swagger-to-graphql>
680              
681             =head1 LICENSE
682              
683             Copyright (C) Ed J
684              
685             This library is free software; you can redistribute it and/or modify
686             it under the same terms as Perl itself.
687              
688             =cut
689              
690             1;