taiyoh's memorandum

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

GraphQLのresolverの登録方法についての一例(Perlの場合)

<2018-01-14追記>
本エントリはライブラリの使用方法について僕が大きな誤解をしています。修正版については以下を参照ください。 taiyoh.hatenablog.com <2018-01-14追記終わり>

所属プロジェクトでは一部のAPIリクエストにGraphQLを採用していて、最近のAPI実装をGraphQLで行うだけでなく、既存のREST APIの実装の順次置き換えを進めている。GraphQL便利すぎる。
GraphQLの各言語の実装では、定義したObject型のfieldはその実像となる集約だけで解決する必要はなく、 resolver と呼ばれるコールバック関数を定義することでfieldの所属する集約から切り離すことができる。
そのresolverの登録方法について、プロジェクトでは以下のような方法を採っている。

#!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

for my $type (grep { ref($_) eq 'GraphQL::Type::Object' } @{ $schema->types }) {
    my $pkg = sprintf 'MyApp::GraphQL::Resolver::%s', $type->name;
    for my $field (keys %{ $type->fields }) {
        if (my $resolver = $pkg->can($field)) {
            $type->fields->{$field}{resolve} = $resolver;
        }
    }
}

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

my $res = execute(
    $schema,
    $query,
    {},
);

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

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

Perl版GraphQLライブラリはバージョン0.16から from_doc という関数が追加され、GraphQLの定義の形式をそのまま読み込めるようになっている。フロントエンドを担当する人間と認識を擦り合わせたいという都合もあるので、schemaはPerlでゴリゴリ書くことはせず、定義ファイルを作ってそれをフロントエンドの人間にも読んでもらっている。
ただ当然ながら、定義ファイルにはresolverの記述はできないのでどうすればいいのかと試行錯誤している最中。
当初は「DSLっぽく登録できたらいいのかも」と思っていたが、しばらくしてテストしづらさを感じてきたので、「resolverが必要な1typeにつき1パッケージ用意する」「resolverを登録したいfieldと同じ名前の関数を定義する」というルールにしている(それが上記のサンプルの方法)。resolverの置き場所はこれで一旦落ち着けることにしたが、resolverの登録方法がhashrefに直接突っ込むスタイルなのであまり気持ちいいものに見えず、かといってそれを解消する手段を考えるために手を止めるわけにもいかないので、「あとでもっといい方法が見つかったらそれに変更する」というのが現状のステータスとなっている。