きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

Rubyist じゃないけど Vagrantifile を読みたい

(=˘ ꒳ ˘=) kubernetes を試そうと Vagrant のリハビリしようとしたら Ruby の表記を思い出すのに手間取ったの巻...

Multi-Machine - Vagrant by HashiCorpVagrantfile を例に、初見で読み方に困ったところを思い出しながら書き留めておきます。

Vagrant.configure("2") do |config|
  config.vm.provision "shell", inline: "echo Hello"

  config.vm.define "web" do |web|
    web.vm.box = "apache"
  end

  config.vm.define "db" do |db|
    db.vm.box = "mysql"
  end
end

ブロック(do..end)

Vagrant.configure("2") do |config|
  # ...
end

は以下の表現と等価ですではありませんでした。

Vagrant.configure("2") { |config|
  # ...
}

どちらの表記も見かけるので迷いますね。複数行の場合は do..end を使う...とかでしょうか。

引数のカッコを省略

  config.vm.provision "shell", inline: "echo Hello"

は以下の表現と等価です。

  config.vm.provision("shell", { inline: "echo Hello" })

他の言語から来ると悪魔のようにも見える表記ですが、慣れるとだんだん括弧が見えてくるようになりますね。 この表記のおかげで、私の中では「Rubyは設定ファイル言語」という先入観があります。

DSL-like 表記の面目躍如といった所ですね。

参考

Bundler の bundler/setup と bundle exec

(=˘ ꒳ ˘=) Ruby のコードちんぷんかんぷんで読みづらい... import やら require やら load やら色々読み込みの構文があるのに加えて、どのライブラリから来た定義なのかが追いづらかったり...

...という愚痴は置いておいて、Ruby のモジュールシステムを別の方面からややこしくしている Bundler の振る舞いについて整理したいと思います。ややこしいと書きましたが、プロジェクト下に依存ライブラリを置けるという、Node.js から来た身からすると勝手知ったるベンダリング手法が取れるありがたいツールです。

Bundler がインストールしたパッケージを使うためには bundle exec したり require bundler/setup したりと、いくつかお作法が必要そうなのですが、それぞれどう作用するかを実際に動かしながら整理していきます。

公式の記述は How to use Bundler with Ruby にあります。しかし情報がパラパラしていて追いづらかったので実際に試してみることにしたのでした。

試してみる

├── Gemfile
├── bin
│   ├── with-bundler-setup.rb
│   └── without-bundler-setup.rb
├── lib
│   └── using-bundled-packages.rb
└── vendor

というプロジェクトを用意します。 https://github.com/asa-taka/try-module-on-bundler に実際のファイルを置いておきました。

フロントのスクリプトをふた通り用意します。

# bin/with-bundler-setup.rb
require 'rubygems'
require 'bundler/setup'
require './lib/using-bundled-packages'
# bin/without-bundler-setup.rb
require './lib/using-bundled-packages'

require の振る舞いを見たかったので一段 lib/using-bundled-packages.rb というスクリプトを挟んでいます。

# lib/using-bundled-packages.rb
require 'awesome_print'

として awesome_print を呼んでいます。vendor 下にインストールされた awesome_print には print 'awesome_print by bundler loaded' と仕込んで、Bundler によりインストールされたパッケージが実行された場合にテキストがプリントされるようにしました。

試した結果

bundle exec ruby ./bin/with-bundler-setup のように各組み合わせを試したところ以下のような結果になりました。

実行コマンド bundler/setup なし bundler/setup あり
ruby .... global のパッケージ Bundler によるパッケージ
bundle exec ruby ... Bundler によるパッケージ Bundler によるパッケージ

つまり bundle exec を使う、もしくはエントリポイントで require bundler/setup をすると孫 require でも Bundler によるパッケージが使われることになりますね。

毎回 bundle exec するのも面倒なので require bundler/setup の方が楽そうですね。

どういう処理でこうなっているのかはまた掘り下げてみたいところです。

GraphQL Schema Language 中の description がコメントから block string に変わった

graphql-tools の makeExecutableSchema のコードを読んでいて commentDescription なる項目が気になり、さらに utilities/buildASTSchema.js を見ていて

export type BuildSchemaOptions = {
  ...GraphQLSchemaValidationOptions,

  /**
   * Descriptions are defined as preceding string literals, however an older
   * experimental version of the SDL supported preceding comments as
   * descriptions. Set to true to enable this deprecated behavior.
   *
   * Default: false
   */
  commentDescriptions?: boolean,
};

と、description の書き方が変わったらしいことに気がつきました。

https://github.com/graphql/graphql-js/issues/1245 によると graphql@0.12.0 から GraphQL Schema Language 中の description の書き方がコメントから block string というものになっていたらしいです。

簡単に GraphQLSchemaprintSchema して確認してみました。

import { GraphQLSchema, GraphQLObjectType, GraphQLString, printSchema } from 'graphql'

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'rootQuery',
    description: 'This is Root Query',
    fields: {
      hello: {
        type: GraphQLString,
        description: 'Greeting',
      },
    },
  }),
})

というスキーマを用意して

console.log(printSchema(schema))

とすると

schema {
  query: rootQuery
}

"""This is Root Query"""
type rootQuery {
  """Greeting"""
  hello: String
}

という """ で囲まれた block string による description が得られます。

今まで通りのコメントによる description を得るには commentDescription という後方互換性のためのオプションが用意されているので

console.log(printSchema(schema, { commentDescriptions: true }))

とすると

schema {
  query: rootQuery
}

# This is Root Query
type rootQuery {
  # Greeting
  hello: String
}

と今までどおりの形式で得られます。

graphql-tools の makeExecutableSchema で Directive を定義して簡易認可を実装してみる

Schema directives | GraphQL Tools によると graphql-tools の makeExecutableSchema で Directive を実装できるようなので 簡易的な認可ロジックを実装してみます。

あくまで Directive のハンズオンなので認可ロジックはとても簡単なものです。

認証情報は context 経由で処理の部分に渡すことにします。

「タイトルは自由に取得できるけど本文は許された人にしか閲覧できないドキュメント」を取得するためのクエリを定義していきます。

import { graphql } from 'graphql'
import { makeExecutableSchema } from 'graphql-tools'

const typeDefs = `
  directive @auth(permitted: [String!]) on FIELD

  type Document {
    title: String!
    content: String! @auth(permitted: ["asa-taka"])
  }

  type Query {
    document: Document!
  }
`

// Directive の処理を定義する
const directiveResolvers = {

  // @auth Directive の処理を定義
  auth(next, src, args, ctx, info) {
    return next().then(res => {

      // コンテクスの認証ユーザが `permitted` に含まれていたら値を素通し
      if (args.permitted.includes(ctx.auth.name)) return res
      
      // 認証エラーを返してあげる (*˘꒳˘*) やさしい
      // info の中身は複雑なので適当に調べながら...
      const path = info.path
      throw new Error(`User not permitted: ${path.prev.key}.${path.key}`)
    })
  },
}

const schema = makeExecutableSchema({ typeDefs, directiveResolvers })

const query = `{
  document { title content }
}`

// データソースの準備
const rootValue = {
  document: { title: 'My Document', content: 'Awesome contents...' },
}

// コンテクスト経由で認証情報を渡してやる
const context = { auth: { name: 'asa-taka' } }

graphql(schema, query, rootValue, context).then(console.log, console.error)

これを実行すると

{ data: { document: { title: 'My Document', content: 'Awesome contents...' } } }

が得られます。ここから認証情報を

const context = { auth: { name: 'some-other-user' } }

と変更すると

{ errors: [ { Error: User not permitted: document.content

とエラーが返されます。ここから更に認証フィールドである content をクエリから除外して

const query = `{
  document { title }
}`

とすると

{ data: { document: { title: 'My Document' } } }

エラーなくデータが得られます。

ディレクティブの定義

基本的な使い方は Schema directives | GraphQL Tools に書いてある通りです。

まず GraphQLSchema 内で利用する Directive を定義します。 GraphQL Schema Language では以下のように Custom Directive を定義できます

directive @auth(permitted: [String!]) on FIELD

次に定義した Directive についての処理の実装を定義します。 graphql-tools の makeExecutableSchema では directiveResolver プロパティでディレクティブの処理を定義できます。

const directiveResolvers = {
  auth(next, src, args, ctx, info) {
    return next().then(res => {
      if (args.permitted.includes(ctx.auth.name)) return res
      const path = info.path
      throw new Error(`User not permitted: ${path.prev.key}.${path.key}`)
    })
  },
}

const schema = makeExecutableSchema({ typeDefs, directiveResolvers })

GraphQL の Directive も Apollo のおかげで意外と簡単に実装できますね。

これまで

...と、Directive 実装に苦労してきた経緯があるのですが、makeExecutableSchema はどう実装されているのか気になりますね。

GraphQL Schema Language で Directive を定義する

(=˘ ꒳ ˘=) GraphQL Schema Language 内で Directive を定義する方法を探していたのですが、公式にドキュメントが見つからなかったのでメモしておきます...

directive @myDirective(age: Int) on FIELD

のように Directive を定義できるようです。

import { graphql, buildSchema } from 'graphql'

const schema = buildSchema(`
  directive @myDirective(age: Int) on FIELD
  
  type Query {
    hello: String!
  }
`)

const query = `{ hello @myDirective(age: 12) }`

const rootValue = { hello: 'world' }

graphql(schema, query, rootValue).then(console.log, console.error)

のように実行できます。

見つけた背景

GraphQLDirective を使って定義した後にそれを printSchema するとどうなるか興味本位で試していて見つけました。

import {
  graphql,
  GraphQLSchema,
  GraphQLDirective,
  GraphQLObjectType,
  GraphQLString,
  printSchema,
} from 'graphql'

const myDirective = new GraphQLDirective({
  name: 'myDirective',
  locations: ['FIELD'],
  args: {
    age: { type: GraphQLString },
  },
})

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'rootQuery',
    fields: {
      hello: { type: GraphQLString },
    },
  }),
  directives: [myDirective],
})

console.log(printSchema(schema))

これを実行すると

schema {
  query: rootQuery
}

directive @myDirective(age: String) on FIELD

type rootQuery {
  hello: String
}

と表示されます。Directives はてっきり GraphQL Schema Language の対象外かと思っていたのですが定義できたのですね。

私が見つけられていないだけで、公式の記述はどこかにあるのでしょうか。

そして定義ができることと処理を実装できることはまた別なのですよね... Directive の処理をいい感じに定義する方法はあるのでしょうか...

GraphQL の Custom Directive について

(=˘ ꒳ ˘=) GraphQL のラストフロンティア Custom Directive にもそろそろ手を出してみたい...

ここまで趣味で GraphQL のいろいろな要素を見てきました。

実行時のパラメータとして GraphQLSchema、queryString、rootValue、context、variables、 スキーマを構成する要素として Query、Mutation、Subscription、Custom Scalar と見てきて最後に残った Custom Directive について見ていきたいと思います。 色々できそうで楽しそうですよね。

しかしこの Custom Directive、意外と魔窟なのでした...

GraphQL の Directive について

とりあえず定義から眺めていきます

と、さてここから型情報を頼りに実装をしてみようかと思ったのですが、意外と難航して...

Directive の処理はどこにあるのか

てっきり GraphQLObjectType のフィールド定義の resolve のようなものがあるのかと思っていたのですが見つかりません。

GraphQLDirective のコンストラクタ引数を見てみると以下のようになっています。

export interface GraphQLDirectiveConfig {
  name: string;
  description?: string;
  locations: DirectiveLocationEnum[];
  args?: GraphQLFieldConfigArgumentMap;
  astNode?: DirectiveDefinitionNode;
}

他の箇所も探してみたのですが、フィルタ処理や変換処理の実装を記述する箇所が見つかりません。

GraphQL.js の @skip ディレクティブの定義を見てみると

export const GraphQLSkipDirective = new GraphQLDirective({
  name: 'skip',
  description:
    'Directs the executor to skip this field or fragment when the `if` ' +
    'argument is true.',
  locations: [
    DirectiveLocation.FIELD,
    DirectiveLocation.FRAGMENT_SPREAD,
    DirectiveLocation.INLINE_FRAGMENT,
  ],
  args: {
    if: {
      type: GraphQLNonNull(GraphQLBoolean),
      description: 'Skipped when true.',
    },
  },
});

https://github.com/graphql/graphql-js/blob/master/src/type/directives.js#L111-L127

と、どこにも処理の内容が書かれていません。ならばと思い execution/execution.js のコードを覗いてみると shouldIncludeNode として フィールドをフィルタリングする処理が定義されていました。

つまり Directive の処理はスキーマではなく、それを解釈する実行関数側に寄せられて実装されているのですね。 これを素直に受け取れば Custom Directives を実装したければ、実行関数である graphql のカスタム実装を自前で書け、という事になります。 それはちょっと常人にはつらい道ですね。

GraphQLDirective で定義してみる

とはいえ一度自分で行けるところまで実装してみようと GraphQLDirective を使ってコードをこねくり回してみました。

import {
  graphql,
  GraphQLSchema,
  GraphQLDirective,
  GraphQLObjectType,
  GraphQLString,
} from 'graphql'

// @myDirective というディレクティブを定義する
const myDirective = new GraphQLDirective({
  name: 'myDirective',

  // どこで使うディレクティブかを指定
  // FIELD、FRAGMENT_DEFINITION、FRAGMENT_SPREAD...など色々指定できる
  locations: ['FIELD'],

  // ディレクティブの引数を定義
  args: {
    name: {
      type: GraphQLString,
    },
  },
})

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'rootQuery',
    fields: {
      hello: {
        type: GraphQLString,
      },
    },
  }),
  directives: [myDirective], // スキーマに @myDirective を組み込む
})

// クエリ内で @myDirective を使ってもエラーにならない
const query = `{ hello @myDirective(name: "asa-taka") }`

const rootValue = {
  hello: 'world',
}

graphql(schema, query, rootValue).then(console.log, console.error)

これで一応 @myDirective という Custom Directive がクエリ内で利用できるようになりました。 正確には「クエリ内で利用してもエラーが出ない」ようにはなりました。 しかし肝心の処理がまだ定義できていません。 値の変換やフィルタ処理など、いろいろ定義したいですよね。

フィールドに付加したディレクティブの情報はどこで取得できるのか

resolve を定義して console.log で探し回った結論だけ書いておきますと、第4引数 infofieldNodesdirectives でフィールドに付加したディレクティブの情報にアクセスできるようです。

      hello: {
        type: GraphQLString,
        resolve(src, args, ctx, info) {
          console.log(info.fieldNodes[0].directives[0])
        }
      }

しかしこれ、ここまでやってディレクティブの nameargs しか取得できず、ここから更にそのディレクティブについての処理を書かなければなりません。 このディレクティブが存在すれば resolve の処理をこう変える、という処理ですね。 更にこれだと特定のフィールドに対してのみの実装ででしかない、ということも忘れてはなりません。 ディレクティブは複数フィールドで使いたい場合がほとんどだと思いますが、その分の実装を複数フィールドに行うのはとても現実的ではありませんね。

graphql-custom-directive

graphql の再実装なんてできるわけなく、フィールド毎の resolve のディレクティブ実装も現実的ではない、ということで graphql-custom-directive という便利ライブラリがあります。

使い方はREADMEに書いてある通りで、パブリックなAPIは以下のふたつです。

  • GraphQLCustomDirective
    • resolve が追加された GraphQLDirective を定義できる
    • オリジナルには存在しない GraphQLDirective.resolve を Directive の処理実装置き場としてしれっと追加している
  • applySchemaCustomDirectives
    • GraphQLSchemaDirective の処理を追加する
    • GraphQLSchema には予め directivesGraphQLCustomDirectives を列挙しておく必要がある

内部処理の大枠としては以下の通りです。

  • wrapFieldsWithMiddleware
    • GraphQLSchema の各フィールドに resolveMiddlewareWrapper を適用する
  • resolveMiddlewareWrapper
    • フィールドの resolve に Directive の resolve を絡めるラッパー
    • info からディレクティブの情報を読み取っている
  • resolveWithDirective(resolve, source, directive, context, info)
    • 通常のフィールドの resolveに対して、第1引数に Directive の resolve が拡張されたもの

コードを見ればわかるように _queryType へのアクセスなど GraphQL.js のプライベートインタフェースにかなり依存した実装となっており、バージョンアップに対してかなり無防備な実装になっています。とは言え現状 GraphQLSchema クラスの仕様上、一旦作成したスキーマを変更する手段が他にないため仕方がないともいえますね。

つまりここまで拡張しなければ Custom Directives は定義できないということで、ひとまず納得しました。

追記

そしてこの graphql-custom-directive を使ってみたところうまく動きませんでした。実は graphql-custom-directives の READMEに書いてあったところから、この s がつかない graphql-custom-directive を見つけたのですが、graphql-custom-directives の方でも同様の applySchemaCustomDirectives の実装があり、更に内部でアクセスしている GraphQLSchema のプライベートインタフェースが異なるという、どうも GraphQL.js の内部実装の変更に追従できていない様子でした。

GraphQL の Directive はあまり一利用者が実装するような段階のものではないのかもしれません...

GraphQL の色々なスキーマ表現について - GraphQL Schema Language や schema.json

(=˘ ꒳ ˘=) GraphQL のスキーマ表現いろいろ多すぎ...

で Introspection について眺めたら GraphQL のスキーマ表現について整理できてきたのでまとめてみます。

GraphQL のスキーマ表現

GraphQL Schema Language

type Query {
  hello: String!
}

のようなDSLのことを指します。GraphQL SDL(Schema Definition Language) とも呼ばれています。 人が一番読みやすい形式といったらこれですね。 GraphQL.js の buildSchema に渡されたり graphql-tools の makeExecutableSchematypeDefs として渡されます。

GraphQL.js の GraphQLSchema

import { GraphQLSchema, GraphQLObjectType } from 'graphql'

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'rootQuery',
    fields: {
      hello: {
        type: GraphQLString,
      },
    },
  }),

のような GraphQLSchema インスタンスを直接コンストラクトする表現。 resolve 定義など何でもできますが、手作業では書きたくないですね。 ある程度 GraphQL に慣れていてもそう思いますし、そうでないならなおさらだと思います。

Introspection Result

apollo-codegen のようなツールが schema.json のように出力するファイルの中身がこれにあたります。 実態はリソースとなるAPIに対する Introspection の結果が書き出されたものです。 この Introspection に用いるクエリはintrospectionQuery として定義されています。 定義内容はとても長いので手作業では書きたくないですね。

簡単なコードでこのクエリを実行してみます。

import { graphql, buildSchema, introspectionQuery } from 'graphql'

const schema = buildSchema(`
  type Query {
    hello: String!
  }
`)

graphql(schema, introspectionQuery).then(console.log)

これを実行すると schema.json の中身としてよく見かける

{ data:
   { __schema:
      { queryType: [Object],
        mutationType: null,
        subscriptionType: null,
        types: [Array],
        directives: [Array] } } }

という値が得られます。

AST Schema

GraphQL Schema Language をパースしたAST(抽象構文木)です。 ライブラリ利用者としてはあまり利用する機会はありませんが、何気なく console.log した結果によく出てくる気がします。

import { parse, buildASTSchema } from 'graphql'

const typeDefs = `
type Query {
  hello: String!
}
`

const ast = parse(typeDefs)

を実行すると

{ kind: 'Document',
  definitions:
   [ { kind: 'ObjectTypeDefinition',
       description: undefined,
       name: [Object],
       interfaces: [],
       directives: [],
       fields: [Array],
       loc: [Object] } ],
  loc: { start: 0, end: 33 } }

という AST Schema が得られます。

スキーマ表現間の変換

これらのスキーマ表現は graphql と graphql-tools の各種ユーティリティである程度の変換が可能となっています。

  • graphql
    • buildClientSchema: Introspection Result → GraphQLSchema
    • buildSchema: GraphQL Schema Language → GraphQLSchema
    • printSchema: GraphQLSchema → GraphQL Schema Language
    • parse: GraphQL Schema Language → AST Schema
    • buildASTSchema: AST Schema → GraphQLSchema
  • graphql-tools
    • makeExecutableSchema: GraphQL Schema Language + resolve/subscribe 実装 → GraphQLSchema
      • この GraphQLSchema には resolve/subscribe が含まれる

なので resolve など実行時用の定義を除けば、これらの表現は相互変換可能、ということになります。

早見表

output \ source Schema Language Introspection Result GraphQLSchema Instance AST
Schema Language printSchema
Introspection Result (*)
GraphQLSchema Instance buildSchema, makeExecutableSchema buildClientSchema buildASTSchema
AST parse

(*) graphql(schema, introspectionQuery) で取得可能

こうしてみると GraphQLSchema インスタンスへの変換が一番充実していることがわかりますね。直接変換できなくても、例えば

printSchema(buildClientSchema(introspectionResult))

のようにすると Introspection Result から GraphQL Schema Language が得られるように、複数組み合わせれば大体の場合は相互変換ができそうですね。