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   | [![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) |

[![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?bra...

=end markdown

=head1 SYNOPSIS

  use GraphQL::Plugin::Convert::OpenAPI;
  my $converted = GraphQL::Plugin::Convert::OpenAPI->to_graphql(
    'file-containing-spec.json',
  );
  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 )