Rubyist じゃないけど Vagrantifile を読みたい
(=˘ ꒳ ˘=) kubernetes を試そうと Vagrant のリハビリしようとしたら Ruby の表記を思い出すのに手間取ったの巻...
Multi-Machine - Vagrant by HashiCorp の Vagrantfile
を例に、初見で読み方に困ったところを思い出しながら書き留めておきます。
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 というものになっていたらしいです。
簡単に GraphQLSchema
を printSchema
して確認してみました。
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 の処理はどこにあるのか
- GraphQLDirective で定義してみる
- フィールドに付加したディレクティブの情報はどこで取得できるのか
- graphql-custom-directive
- 追記
GraphQL の Directive について
とりあえず定義から眺めていきます
- GraphQL.org
- Working Draft – October 2016
と、さてここから型情報を頼りに実装をしてみようかと思ったのですが、意外と難航して...
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引数 info
の fieldNodes
の directives
でフィールドに付加したディレクティブの情報にアクセスできるようです。
hello: { type: GraphQLString, resolve(src, args, ctx, info) { console.log(info.fieldNodes[0].directives[0]) } }
しかしこれ、ここまでやってディレクティブの name
と args
しか取得できず、ここから更にそのディレクティブについての処理を書かなければなりません。
このディレクティブが存在すれば resolve
の処理をこう変える、という処理ですね。
更にこれだと特定のフィールドに対してのみの実装ででしかない、ということも忘れてはなりません。
ディレクティブは複数フィールドで使いたい場合がほとんどだと思いますが、その分の実装を複数フィールドに行うのはとても現実的ではありませんね。
graphql-custom-directive
graphql
の再実装なんてできるわけなく、フィールド毎の resolve
のディレクティブ実装も現実的ではない、ということで
graphql-custom-directive
という便利ライブラリがあります。
使い方はREADMEに書いてある通りで、パブリックなAPIは以下のふたつです。
GraphQLCustomDirective
resolve
が追加されたGraphQLDirective
を定義できる- オリジナルには存在しない
GraphQLDirective.resolve
を Directive の処理実装置き場としてしれっと追加している
applySchemaCustomDirectives
GraphQLSchema
にDirective
の処理を追加するGraphQLSchema
には予めdirectives
にGraphQLCustomDirectives
を列挙しておく必要がある
内部処理の大枠としては以下の通りです。
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 の makeExecutableSchema
に typeDefs
として渡されます。
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 Languageparse
: GraphQL Schema Language → AST SchemabuildASTSchema
: 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 が得られるように、複数組み合わせれば大体の場合は相互変換ができそうですね。