pythonのgraphql-core上のオブジェクトからSDL (schema definition language) を出力する方法についてのメモ
graphqlのSDL (schema definition language) を出力する方法をメモしてみた。
個人的なメモなので分かりづらいかもしれない。graphqlに対応したコード生成などを考える上でどの表現だけを残しておけば良いのかなどを気にしたかった。その上でいろんな表現からSDLを出力できるならその元となる表現だけを持っていても良さそうと思ったので。
注意事項。
- :warning: graphql-core-nextはgraphql-coreにマージされた
- :warning: graphql-coreは下層のライブラリ。普通の人は触らない。普通の人はgraphene, ariadne, strawberryから使う
install
versionが3以上ならgraphql-core-nextがインストールされる。
$ pip install graphql-core>=3
概要
それぞれの表現についてまとめてみる。
- graphql SDL
- graphql.schema.GraphQLSchema
- graphql.ast.DocumentNode
例はPersonというオブジェクトを返すpeopleというfieldが提供されているというような例。
graphql SDL (schema definition language)で記述した場合は以下のようになる。
type Person { name: String! } type Query { people: [Person]! }
内部表現としてのgraphql.schema.GraphQLSchemaを直接記述すると以下の様になる(SDLからはbuild_schema()を使えば得られる)。
Person = GraphQLObjectType( "Person", lambda: { "name": GraphQLField(GraphQLNonNull(GraphQLString,)), }, ) Query = GraphQLObjectType( "Query", lambda: { "people": GraphQLField( GraphQLNonNull(GraphQLList(Person)) ) }, ) schema = GraphQLSchema(Query)
graphql.ast.DocumentNodeは内部的に使われるAST ... (記述するのは面倒だったので例は省略。まぁよくあるabstract syntax treeにマッピングされたNodeオブジェクト)
実行
実行にはschemaとqueryが必要。schema,queryはそれぞれ内部的にはASTを経由して消費される。
schema :: SDL (string) -> DocumentNode (AST) -> GraphQLSchema query :: query (string) -> DocumentNode (AST)
queryを実行するときには graphql_sync()
か非同期の graphql()
が使われる。引数がとても多い。通常は、graphql_sync(schema, source, root_value=root_value)
くらいで考えれば十分かもしれない(sourceはquery)。
pydocの結果を貼っておく。
graphql.graphql_sync = graphql_sync( schema: graphql.type.schema.GraphQLSchema, source: Union[str, graphql.language.source.Source], root_value: Any = None, context_value: Any = None, variable_values: Dict[str, Any] = None, operation_name: str = None, field_resolver: Callable[..., Any] = None, type_resolver: Callable[[Any, graphql.type.definition.GraphQLResolveInfo, ForwardRef('GraphQLAbstractType')], Union[Awaitable[Union[ForwardRef('GraphQLObjectType'), str, NoneType]], ForwardRef('GraphQLObjectType'), str, NoneType]] = None, middleware: Union[Tuple, List, graphql.execution.middleware.MiddlewareManager, NoneType] = None, execution_context_class: Type[graphql.execution.execute.ExecutionContext] = <class 'graphql.execution.execute.ExecutionContext'> ) -> graphql.execution.execute.ExecutionResult Execute a GraphQL operation synchronously. The graphql_sync function also fulfills GraphQL operations by parsing, validating, and executing a GraphQL document along side a GraphQL schema. However, it guarantees to complete synchronously (or throw an error) assuming that all field resolvers are also synchronous.
真面目なqueryの場合には context_value
からuser agentのようなものを取り出したり、 variable_values
からパラメーターを取り出したりもするかもしれない。
run code
hello world的なコードはこんな感じ。何もqueryしていないけれど。
- query部分は
{ people { name } }
- schemaは、SDLで定義した場合のもの
import graphql schema = graphql.build_schema( """ type Person { name: String! } type Query { people: [Person]! } """ ) data = {"people": [{"name": "foo"}, {"name": "bar"}]} result = graphql.graphql_sync(schema, "{ people { name }}", data) print(result)
実行結果
ExecutionResult(data={'people': [{'name': 'foo'}, {'name': 'bar'}]}, errors=None)
はい。
補足
実際の探索の際には定義されたresolverが実行されていくことになる。先程の例では何も定義していないように見えるが実際の内部の状況を少しだけ補足。
resolver
もう少し真面目に説明すると探索時には以下の様なresolverが使われる
- type resolver (この記事では出てこない)
- field resolver
シンプルな構造のものの場合field resolverしか使われない。type resolverがいつ使われるかというと、例えばInterfaceが定義されていた場合。
これはgoでinterfaceが使われていたときにはswitchでtype assertionsなどを使って分岐する必要があったり (type switches in go)、あるいはjsonschemaやopenAPIでoneOfで定義されているschemaの検証の際にはどの要素のschemaに属するかを確認するひつようがあったりするのと同じこと(inheritance and polymorphism in openAPI)。
default field resolver
default_field_resolverがdictのkeyを見てくれるので、このようなresolverを定義していた状態と同じ。
def resolve_people(root, info): return data["people"] schema.get_type("Query").fields["people"].resolve = resolve_people
SDLを出力したい
これまでのメモは復習のようなもので本題はこちら。いろいろな表現からSDLを出力したいと言うのがゴール。
DocumentNode -> SDL
graphql.ast.DocumentNodeからSDLを出力するには graphql.language.print_ast を使う。
import graphql from graphql.language import print_ast type_defs = """ type Person { name: String! } type Query { people: [Person]! } """ document_node = graphql.parse(type_defs) print(print_ast(document_node)) # or graphql.print_ast(document_node)
実行結果
type Person { name: String! } type Query { people: [Person]! }
GraphQLSchema -> SDL
graphql.type.GraphQLSchemaからSDLを出力するには graphql.utilities.print_schema を使う。
import graphql from graphql.utilities import print_schema Person = graphql.GraphQLObjectType( "Person", lambda: { "name": graphql.GraphQLField(graphql.GraphQLNonNull(graphql.GraphQLString,)), }, ) Query = graphql.GraphQLObjectType( "Query", lambda: { "people": graphql.GraphQLField( graphql.GraphQLNonNull(graphql.GraphQLList(Person)) ) }, ) schema = graphql.GraphQLSchema(Query) print(print_schema(schema)) # or graphql.print_schema(schema)
実行結果
type Person { name: String! } type Query { people: [Person]! }
(query) -> SDL (gql?)
queryの方も内部的にはDocumentNodeになる。こちらもparseとprint_astを使う。もはやSDLではないような気がするけれど。DocumentNodeを返すので。一応。
import graphql from graphql.language import print_ast document_node = graphql.parse( """ { root { name } } """ ) print(f"{document_node.__class__.__module__}.{document_node.__class__.__name__}") print(print_ast(document_node))
実行結果
graphql.language.ast.DocumentNode { root { name } }
まとめ
最悪、GraphQLSchemaが得られればSDLを出力できる。