pythonのgraphql-core上のオブジェクトからSDL (schema definition language) を出力する方法についてのメモ

graphqlのSDL (schema definition language) を出力する方法をメモしてみた。

github.com

個人的なメモなので分かりづらいかもしれない。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
  • GraphQLSchema -> SDL
  • (query) -> SDL (gql?)

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を出力できる。