newmo 技術ブログ

技術で地域をカラフルに

newmoインターンがgqlparserにプルリクエストを投げた話

こんにちは。8月からnewmoでインターンをしている堀之内(@horinouchi09)と申します。

nemwoではバックエンドエンジニアとして、ビジネスドメインのAPIの開発やプラットフォームエンジニアリングのタスクなど多岐にわたってプロダクト開発に携わっています。

Go言語での開発は未経験からのスタートでしたが、バックエンドエンジニアのitoさんをはじめ多くの方々にサポートしていただき、楽しく開発ができています!

さて、今回は私がnewmoでのインターンを通して人生初のOSS Contributeをした話をします。

newmoの開発スタイル

newmoではクライアントからサーバーのAPIを呼び出すスタイルとしてGraphQLを採用しています(詳しくはこちらをご覧ください)。

GraphQLのディレクティブ

GraphQLに関する詳しい説明は省略しますが、GraphQLでは以下のようなスキーマを定義することでAPIを明示することができます。

# スキーマ定義
type Query {
  passenger: Passenger!
}

type Passenger {
  passengerId: String!
  name: String!
  nickname: String! @deprecated(reason: "old")
}

先ほどのスキーマ定義の例にあった@deprecated はディレクティブと呼ばれるものです。

GraphQLでは、ディレクティブをスキーマ定義のフィールドなどに付与することで情報を追加することができます。

例えば、先ほどの例の@deprecated は、Passengerのnicknameフィールドが非推奨であることを意味するGraphQLの標準仕様で定義されているディレクティブです。

さらに、@deprecatedのような標準で用意されているディレクティブに加えて、newmoでは独自のディレクティブを定義しています。

@validateStringというディレクティブがその一例です。

# 例
input DriverInformationInput {
  driverId: String! @validateString(format: UUID)
}

ここでの@validateString はクラアントから送られてくるdriverIdのバリデーションルールを明示できるディレクティブです。

ディレクティブとして定義することでdriverIdがUUIDの形式であることがクライアントからも明白になり仕様が明確になります。さらにnewmoではgqlgenというGo用のGraphQLコード生成ツールにプラグインを追加することでバリデーションのコードも自動で生成されるようにしています。

コード生成の実装としては、gqlgenが提供しているGenerateCodeというhookを利用してプラグインを書き、バリデーションの実装も含まれたresolverのコードを自動で生成されるようにしています。 

import "github.com/99designs/gqlgen/api"

// gqlgenによるコード生成部分
if err := api.Generate(
    cfg,
    api.AddPlugin(plugin.New()), // pluginの追加
); err != nil {
    return fmt.Errorf("failed to generate files: %w", err)
}
// pluginに実装しているGenerateCodeの例
func (m *Plugin) GenerateCode(data *codegen.Data) error {
    if !data.Config.Resolver.IsDefined() {
        return nil
    }
 
    // 具体的なロジック
    // 内部では"github.com/99designs/gqlgen/codegen/templates"のRenderでGoのファイルを生成しています。
    return m.generateSingleFile(data)
}

認可用のディレクティブの導入

APIにはそれぞれの認可用のトークンが必要です。しかし、現状のスキーマからはどのAPIがどのような認可情報が必要かわかりません。また、認可チェックを手書きで実装する必要があり漏れが発生しうる、などの問題がありました。

そこで認可用のディレクティブ@authorizationを定義することで表現力を高めつつ、そのようなディレクティブを元に認可チェックのコードも自動生成をしようと考えました。

@authorizationの仕様

type Query {
    Driver: Driver! @authorization(userType: hoge)
}

@authorizationは上記の例のようにQueryやMutationのフィールドに付与することで認可情報を表現するようにしたいと考えました。

しかし、このままでは全てのQueryやMutationのフィールドに対して@authorizationを付与していかなければなりません。また、newmoでは各スキーマの大半のAPIが同一の認可情報を必要としていたので、スキーマのデフォルトの認可情報を定義できた方が合理的です。

そこで以下のようにschemaに対して@authorizationを付与することにしました。

extend schema @authorization(userType: hoge)

type Query {
    Driver: Driver!
}

このように定義して、@authorizationのないDriver もuserTypeがhogeであることが必要になるものとして実装を進めます。

gqlgenによるコード生成

ディレクティブの仕様が決まれば、次は@authorizationに沿ったコードを生成するgqlgenのプラグインの実装です。

新たなファイルを生成するには、以下のCodeGeneratorというインターフェイスを満たすプラグインを作り、GenrateCode内でロジックを書くことになります。

type CodeGenerator interface {
    GenerateCode(cfg *codegen.Data) error
}

ここでのcodegen.Dataは以下のような構造体です。

この中でschemaに付与したディレクティブが入ってそうなSchema *ast.Schema は以下のような構造体でした。

type Schema struct {
    Query            *Definition
    Mutation         *Definition
    Subscription     *Definition

    Types      map[string]*Definition
    Directives map[string]*DirectiveDefinition

    PossibleTypes map[string][]*Definition
    Implements    map[string][]*Definition

    Description string

    Comment *CommentGroup
}

しかし、このSchemaという構造体の中にはschemaに付与されたディレクティブが入っていませんでした(この中のDirectives はスキーマ全体で定義されているディレクティブを指していて、schemaに付与されたディレクティブを格納している訳ではありません)。

gqlgenの利用するDataにschema上のディレクティブが入っていないのでは、プラグインとして先ほどの仕様を満たすコード生成を実装することができません。

gqlparserの修正

gqlgenはgqlparserというGraphQLスキーマをパースする別リポジトリのOSSに依存しています。

特に前述したSchema *ast.Schemaには、gqlparserのLoadSchemaという関数を用いて定義されたスキーマが落としこまれています。gqlparserのLoadSchemaの中で使われているvalidatorのLoadSchemaは以下のような関数です。

func LoadSchema(inputs ...*Source) (*Schema, error) {
    sd, err := parser.ParseSchemas(inputs...)
    if err != nil {
        return nil, gqlerror.WrapIfUnwrapped(err)
    }
    return ValidateSchemaDocument(sd)
}

よくよくgqlparserを読み込んでいくと、ParserSchemaではschemaに付与したディレクティブをきちんとParseしていましたが、ValidateSchemaDocument 内で捨てられていることがわかりました。

そこで、以下のようにSchemaDirectivesというフィールドを追加しValidateSchemaDocument 内で付け足すように変更するプルリクエストを投げることにしました。

type Schema struct {
    Query            *Definition
    Mutation         *Definition
    Subscription     *Definition
    SchemaDirectives DirectiveList // このフィールドを追加!

    Types      map[string]*Definition
    Directives map[string]*DirectiveDefinition

    PossibleTypes map[string][]*Definition
    Implements    map[string][]*Definition

    Description string

    Comment *CommentGroup
}

実際のプルリクエストがこちらです。

https://github.com/vektah/gqlparser/pull/318/files

こちらのプルリクエストがマージされ、実際にnewmoのgqlgenはSchemaに付与したディレクティブを元にコード生成ができるようになりました。

終わりに

このような背景で人生初のOSS Contributeに成功することができました!

newmoでは周りのエンジニアの方々が当たり前のようにOSSにプルリクエストを投げているので、自分自身の意識も変わってきている感覚があります。

newmoはビジネスドメインの開発にスピード感がある一方で、生産性を高めるためのツールの整備や将来の負債にならないような設計などにも力を入れており、インターンとして勉強できることが大変多いです。

今後もnewmoのプロダクト開発に少しでも力になり、「移動で地域をカラフルに」を実現できるように邁進します!


newmoではエンジニアを積極的に採用中です!キャリアサイトはこちら↓ careers.newmo.me