taiyoh's memorandum

@ttaiyoh が、技術ネタで気づいたことを書き溜めておきます。

Perl版GraphQLライブラリにおけるresolverの定義方法(修正版)

承前↓ taiyoh.hatenablog.com

これについて、自分がライブラリの実装方法について大きな誤解をしていたのでここに訂正させていただきます。
開発環境にGraphiQLを導入しようとしてinspectがうまくいかず、
原因の調査していたところ、どうやらresolverの定義方法が想定と違っていたのがわかりました。
Perl版GraphQLライブラリの場合、GraphQL::Execution::executeの引数に一次請けとしてのresolverを入れておく必要があり、この中でどのfieldなのかをまず解決する必要がありました。
ということで、以前提示したサンプルは以下のように修正されます。

#!perl

use 5.014;
use warnings;

use GraphQL::Schema;
use GraphQL::Execution qw/execute/;

use Data::Dumper;

package Person {
    use Moo; # GraphQLがMoo依存なので使わせてもらう

    has name => (
        is       => 'ro',
        required => 1, 
    );

    has favorite_fruit_ids => (
        is       => 'ro',
        required => 1,
    );
};

package Fruit {
    use Moo;

    has id => (
        is       => 'ro',
        required => 1,
    );

    has name => (
        is       => 'ro',
        required => 1, 
    );
};

package MyApp::GraphQL::Resolver::Query {
    sub person {
        my ($root_value, $args, $context, $info) = @_;
        my $person = main::get_person();
        return { name => $person->name, favorite_fruit_ids => $person->favorite_fruit_ids };
    }
};

package MyApp::GraphQL::Resolver::Person {
    sub favoriteFruits {
        my ($root_value, $args, $context, $info) = @_;
        my $fruits = main::get_fruits($root_value->{favorite_fruit_ids});
        return [ map +{ name => $_->name }, @$fruits ];
    }
};

my $idx = 1;
my @fruits = map { Fruit->new(id => $idx++, name => $_) } qw/apple banana orange grape kiwifruit/;
my $person = Person->new(name => 'taiyoh', favorite_fruit_ids => [1, 5]);

sub get_person { $person }
sub get_fruits {
    my $ids = shift;
    my %id_map = map { $_ => 1 } @$ids;
    return [ grep { $id_map{$_->id} } @fruits ];
}

my $schema = GraphQL::Schema->from_doc(<<'EOF');
type Person {
    name: String!
    favoriteFruits: [Fruit!]!
}

type Fruit {
    name: String!
}

type Query {
    person: Person
}

schema {
    query: Query
}
EOF

my $query = <<'EOQ';
{
    person {
        name
        favoriteFruits {
            name
        }
    }
}
EOQ

my $res = execute(
    $schema,
    $query,
    {}, # root_value
    {}, # context_value
    {}, # variables
    undef, # operation_name,
    sub { # resolver
        my ($root_value, $args, $context, $info) = @_;
        my $field_name  = $info->{field_name};
        my $parent_type = $info->{parent_type}->name;

        my $pkg = sprintf('MyApp::GraphQL::Resolver::%s', $parent_type);
        if (my $resolver = $pkg->can($field_name)) {
            return $resolver->($root_value, $args, $context, $info);
        }
        # https://metacpan.org/source/ETJ/GraphQL-0.26/lib/GraphQL/Execution.pm#L668-688 より
        my $root_ref = ref $root_value;
        if ($root_ref eq 'CODE') {
            return $root_value->($args, $context, $info);
        } elsif ($root_ref ne 'HASH' && $root_value->can($field_name)) {
            return $root_value->$field_name($args, $context, $info);
        }
        return $root_value->{$field_name};
    }
);

local $Data::Dumper::Indent = 1;
say Dumper($res);

__END__
$VAR1 = {
  'data' => {
    'person' => {
      'favoriteFruits' => [
        {
          'name' => 'apple'
        },
        {
          'name' => 'kiwifruit'
        }
      ],
      'name' => 'taiyoh'
    }
  }
};

この組み方でGraphiQLが正常に動作することは確認しています。resolverを特定するために $info の値を解析する必要があるというところでまだ改善の余地はありそうですが、変な注入の方法よりはずっとよさそう。