GraphQL-Plugin-Convert-OpenAPI
view release on metacpan or search on metacpan
lib/GraphQL/Plugin/Convert/OpenAPI.pm view on Meta::CPAN
sub _make_input {
my ($type, $name2type, $type2info) = @_;
DEBUG and _debug("_make_input", $type);
$type = $type->{type} if ref $type eq 'HASH';
if (ref $type eq 'ARRAY') {
# modifiers, recurse
return _apply_modifier(
$type->[0],
_make_input(
$type->[1],
$name2type,
$type2info,
),
)
}
return $type
if $TYPE2SCALAR{$type}
or $name2type->{$type}{kind} eq 'enum'
or $name2type->{$type}{kind} eq 'input';
# not deal with unions for now
# is an output "type"
my $input_name = $type.'Input';
my $typedef = $name2type->{$type};
my $inputdef = $name2type->{$input_name} ||= {
name => $input_name,
kind => 'input',
$typedef->{description} ? (description => $typedef->{description}) : (),
fields => +{
map {
my $fielddef = $typedef->{fields}{$_};
($_ => +{
%$fielddef, type => _make_input(
$fielddef->{type},
$name2type,
$type2info,
),
})
} keys %{$typedef->{fields}}
},
};
my $inputdef_fields = $inputdef->{fields};
$type2info->{$input_name}{field2prop} = $type2info->{$type}{field2prop};
$type2info->{$input_name}{field2type} = +{
map {
($_ => $inputdef_fields->{$_}{type})
} keys %$inputdef_fields
};
DEBUG and _debug("_make_input(object)($input_name)", $typedef, $type2info->{$input_name}, $type2info->{$type}, $name2type, $type2info);
$input_name;
}
sub _resolve_schema_ref {
my ($obj, $schema) = @_;
my $ref = $obj->{'$ref'};
return $obj if !$ref;
$ref =~ s{^#}{};
$schema->get($ref);
}
sub _kind2name2endpoint {
my ($paths, $schema, $name2type, $type2info) = @_;
my %kind2name2endpoint;
for my $path (keys %$paths) {
for my $method (grep $paths->{$path}{$_}, @METHODS) {
my $info = $paths->{$path}{$method};
my $op_id = $info->{operationId} || $method.'_'._trim_name($path);
my $fieldname = _trim_name($op_id);
my $kind = $METHOD2MUTATION{$method} ? 'mutation' : 'query';
$type2info->{ucfirst $kind}{field2operationId}{$fieldname} = $op_id;
my @successresponses = map _resolve_schema_ref($_, $schema),
map $info->{responses}{$_},
grep /^2/, keys %{$info->{responses}};
DEBUG and _debug("_kind2name2endpoint($path)($method)($fieldname)($op_id)", $info->{responses}, \@successresponses);
my @responsetypes = map _get_type(
$_->{schema}, $fieldname.'Return',
$name2type,
$type2info,
), @successresponses;
@responsetypes = ('String') if !@responsetypes; # void return
my $union = _make_union(
\@responsetypes,
$name2type,
);
my @parameters = map _resolve_schema_ref($_, $schema),
@{ $info->{parameters} };
my $pseudo_type = join '.', ucfirst($kind), $fieldname;
my %args = map {
my $argprop = $_->{name};
my $argfield = _trim_name($argprop);
$type2info->{$pseudo_type}{field2prop}{$argfield} = $argprop;
my $type = _get_type(
$_->{schema} ? $_->{schema} : $_, "${fieldname}_$argfield",
$name2type,
$type2info,
);
my $typename = _remove_modifiers($type);
my $is_hashpair = ($type2info->{$typename}||{})->{is_hashpair};
$type = _make_input(
$type,
$name2type,
$type2info,
);
$type2info->{$pseudo_type}{field2is_hashpair}{$argfield} = $is_hashpair
if $is_hashpair;
$type2info->{$pseudo_type}{field2type}{$argfield} = $type;
($argfield => {
type => _apply_modifier($_->{required} && 'non_null', $type),
$_->{description} ? (description => $_->{description}) : (),
})
} @parameters;
DEBUG and _debug("_kind2name2endpoint($fieldname) params", \%args);
my $description = $info->{summary} || $info->{description};
$kind2name2endpoint{$kind}->{$fieldname} = +{
type => $union,
$description ? (description => $description) : (),
%args ? (args => \%args) : (),
};
}
}
(\%kind2name2endpoint);
}
# possible "kind"s: scalar enum type input union interface
# mutates %$name2typeused - is boolean
sub _walk_type {
my ($name, $name2typeused, $name2type) = @_;
DEBUG and _debug("OpenAPI._walk_type", $name, $name2typeused);#, $name2type
return if $name2typeused->{$name}; # seen - stop
return if $TYPE2SCALAR{$name}; # builtin scalar - stop
$name2typeused->{$name} = 1;
my $type = $name2type->{$name};
return if $KIND2SIMPLE{ $type->{kind} }; # no sub-fields, types, etc - stop
if ($type->{kind} eq 'union') {
DEBUG and _debug("OpenAPI._walk_type(union)");
_walk_type($_, $name2typeused, $name2type) for @{$type->{types}};
return;
}
if ($type->{kind} eq 'interface') {
DEBUG and _debug("OpenAPI._walk_type(interface)");
for my $maybe_type (values %$name2type) {
next if $maybe_type->{kind} ne 'type' or !$maybe_type->{interfaces};
next if !grep $_ eq $name, @{$maybe_type->{interfaces}};
_walk_type($maybe_type->{name}, $name2typeused, $name2type);
}
# continue to pick up the fields' types too
}
# now only input and output object remain (but still interfaces too)
for my $fieldname (keys %{ $type->{fields} }) {
my $field_def = $type->{fields}{$fieldname};
DEBUG and _debug("OpenAPI._walk_type($name)(*object)", $field_def);
_walk_type(_remove_modifiers($field_def->{type}), $name2typeused, $name2type);
next if !%{ $field_def->{args} || {} };
for my $argname (keys %{ $field_def->{args} }) {
DEBUG and _debug("OpenAPI._walk_type(arg)($argname)");
my $arg_def = $field_def->{args}{$argname};
_walk_type(_remove_modifiers($arg_def->{type}), $name2typeused, $name2type);
}
}
}
sub to_graphql {
my ($class, $spec, $app) = @_;
my %appargs = (app => $app) if $app;
my $openapi_schema = JSON::Validator->new->schema($spec)->schema;
DEBUG and _debug('OpenAPI.schema', $openapi_schema);
my $defs = $openapi_schema->get("/definitions");
my @ast;
my (
%name2type,
%type2info,
);
# all non-interface-consumers first
# also drop defs that are an array as not GQL-idiomatic - treat as that array
for my $name (
grep !$defs->{$_}{allOf} && ($defs->{$_}{type}//'') ne 'array', keys %$defs
) {
_get_spec_from_info(
_trim_name($name), $defs->{$name},
\%name2type,
\%type2info,
);
}
# now interface-consumers and can now put in interface fields too
for my $name (grep $defs->{$_}{allOf}, keys %$defs) {
_get_spec_from_info(
_trim_name($name), $defs->{$name},
\%name2type,
\%type2info,
);
}
my ($kind2name2endpoint) = _kind2name2endpoint(
$openapi_schema->get("/paths"), $openapi_schema,
\%name2type,
\%type2info,
);
for my $kind (keys %$kind2name2endpoint) {
$name2type{ucfirst $kind} = +{
kind => 'type',
name => ucfirst $kind,
fields => { %{ $kind2name2endpoint->{$kind} } },
};
}
my %name2typeused;
_walk_type(ucfirst $_, \%name2typeused, \%name2type)
for keys %$kind2name2endpoint;
push @ast, map $name2type{$_}, keys %name2typeused;
+{
schema => GraphQL::Schema->from_ast(\@ast),
root_value => OpenAPI::Client->new($openapi_schema->data, %appargs),
resolver => make_field_resolver(\%type2info),
};
}
=encoding utf-8
=head1 NAME
GraphQL::Plugin::Convert::OpenAPI - convert OpenAPI schema to GraphQL schema
=begin markdown
# PROJECT STATUS
| OS | Build status |
|:-------:|--------------:|
| Linux | [](https://travis-ci.org/graphql-perl/GraphQL-Plugin-Convert-OpenAPI) |
[](https://metacpan.org/pod/GraphQL::Plugin::Convert::OpenAPI) [;
print $converted->{schema}->to_doc;
=head1 DESCRIPTION
This module implements the L<GraphQL::Plugin::Convert> API to convert
a L<JSON::Validator::OpenAPI::Mojolicious> specification to L<GraphQL::Schema> etc.
It uses, from the given API spec:
=over
=item * the given "definitions" as output types
=item * the given "definitions" as input types when required for an
input parameter
=item * the given operations as fields of either C<Query> if a C<GET>,
or C<Mutation> otherwise
=back
If an output type has C<additionalProperties> (effectively a hash whose
values are of a specified type), this poses a problem for GraphQL which
does not have such a concept. It will be treated as being made up of a
list of pairs of objects (i.e. hashes) with two keys: C<key> and C<value>.
The queries will be run against the spec's server. If the spec starts
with a C</>, and a L<Mojolicious> app is supplied (see below), that
( run in 1.122 second using v1.01-cache-2.11-cpan-0bb4e1dffa6 )