[iOS] Apollo CLIを使って、Swiftファイルを書き出したい

2019.06.16

2019/07/31更新
元の投稿では複数graphqlファイルから複数swiftファイルを生成していたのですが、
それだとfragmentの扱いが面倒なので、1枚のswiftファイルに集めて生成するように変更しました。

===

こんにちは。きんくまです。

GraphQLをiOSからつなぎたくて、このブログで諏訪氏が書いた記事を参考にいろいろと試しています。 Apollo iOSを使ってGitHub API v4(GraphQL)クライアントを作る

今回は記事の中の、Apollo CLIを使ってSwift用のコードを生成する部分をカスタマイズしてみました。

できあがったもの (GitHub)
cm-tsmaeda/ApolloCLIGenSwiftSample

input/query または input/mutation に置いたファイルを ouput フォルダ内に Swiftファイルとして書き出します。

やりたいこと

Apollo iOSのドキュメントを読むと、Xcodeの中でビルドのたびに、ファイルを生成していました。
ですが実案件では、そんなに型定義が変わることもないです。
なのでXcodeに組み込むのではなくて、nodeプロジェクトとして独立させて、任意のタイミングで生成したかったです。

Adding a code generation build step

また、複数のquery/mutationファイルから、名前を指定して個別のSwiftファイルを生成したいです。

Apollo CLI

Apollo CLIを使うと、Schemaファイルをダウンロードしたり、queryからSwiftファイルを生成できます。

apollographql/apollo-tooling

インストール手順のところに、 -g オプションでグローバルにインストールするように書いてあったのですが、今回はプロジェクトディレクトリにだけインストールするようにしました。

設定ファイル

src/settings.js は設定ファイルです。

authToken の箇所を自分のトークンに置き換えてください。

// API end point
const APIEndPoint = "https://api.github.com/graphql";

// トークン
const authToken = "Your token is here!";

// スキーマの出力パス
const schemaPath = "./output/github_schema.json";

// 入力ファイルのディレクトリ
const inputDirPath = "./input";

// 出力Swiftファイルのディレクトリ
const outputSwiftDirPath = "./output";

// 出力Swiftファイルの名前
const outputSwiftFileName = "GeneratedGraphQLTypes.swift";

// exports
exports.APIEndPoint = APIEndPoint;
exports.authToken = authToken;
exports.schemaPath = schemaPath;
exports.inputDirPath = inputDirPath;
exports.outputSwiftDirPath = outputSwiftDirPath;
exports.outputSwiftFileName = outputSwiftFileName;

Schemaファイルダウンロード

Schemaのダウンロードをします。設定ファイルを読み取って、apolloコマンドを叩いています。

download_schema.js

const exec = require('child_process').exec;
const settings = require('./settings');

const mainCommand = "apollo client:download-schema";
const parameters = {
    "endpoint": `${settings.APIEndPoint}`,
    "header": `Authorization: Bearer ${settings.authToken}`
};
const output = `${settings.schemaPath}`;

function buildCommand(mainCommand, parameters, output){
    let result = mainCommand;
    result += ` ${output}`;
    for(let key in parameters){
        result += ` --${key}="${parameters[key]}"`;
    }
    return result;
}

let command = buildCommand(mainCommand, parameters, output);
console.log(command);

exec(command, (err, stdout, stderr) => {
    if (stderr) {
        console.log(stderr);
    }
    console.log(stdout);
});

Swiftファイルの生成

設定ファイルを読み取って、inputフォルダ内の複数graphqlファイルから、ouputフォルダへ1枚のSwiftファイルを生成します。

src/gen_swift_files.js

const exec = require('child_process').exec;
const fs = require('fs');
const settings = require('./settings');

const WORKING_FILE_PATH = "./input/temp.graphql";

function removeLastFileSeparatorIfNeeds(path) {
    if(path.length === 0){
        return "";
    }
    let lastChar = path.substr(-1);
    if(lastChar === '/'){
        return path.substr(0, path.length - 1);
    }
    return path;
}

function buildCodeGenCommand(graphqlPath, swiftPath){
    const mainCommand = "apollo client:codegen";
    const parameters = {
        "includes": graphqlPath,
        "localSchemaFile": `${settings.schemaPath}`,
        "target": "swift",
        "passthroughCustomScalars" : null
    };
    const output = swiftPath;

    let result = mainCommand;
    result += ` ${output}`;
    for(let key in parameters){
        if(parameters[key] === null) {
            result += ` --${key}`;
        } else {
            result += ` --${key}="${parameters[key]}"`;
        }
    }
    return result;
}

async function executeCommand(command) {
    return new Promise((resolve, reject) => {
        exec(command, (err, stdout, stderr) => {
            if (stderr) {
                //console.log(stderr);
                reject(stderr);
                return;
            }
            //console.log(stdout);
            resolve(stdout);
        });
    });
}

async function removeFileIfExists(filePath) {
    return new Promise((resolve, reject) => {
        fs.access(filePath, fs.F_OK, (accessError)=>{
            if (accessError) {
                // 存在しないので成功とする
                resolve(true);
                return;
            }
            fs.unlink(filePath, (removeError)=>{
                if(removeError){
                    reject(removeError);
                    return;
                }
                resolve(true);
            })
        });
    });
}

async function concatGraphQLFiles(inputDir) {
    return new Promise(async (resolve, reject) => {
        let modifiedInputPath = removeLastFileSeparatorIfNeeds(inputDir);
        let concatCommand = `cat ${modifiedInputPath}/*.graphql > ${WORKING_FILE_PATH}`;
        console.log('command: ', concatCommand);
        await executeCommand(concatCommand);
        resolve(WORKING_FILE_PATH);
    });
}

async function main() {
    await removeFileIfExists(WORKING_FILE_PATH);
    await concatGraphQLFiles(settings.inputDirPath);
    const swiftPath = removeLastFileSeparatorIfNeeds(settings.outputSwiftDirPath) + "/" + settings.outputSwiftFileName;
    const command = buildCodeGenCommand(WORKING_FILE_PATH, swiftPath);
    console.log(command);
    const result = await executeCommand(command);
    console.log(result);
    await removeFileIfExists(WORKING_FILE_PATH);
}

main();

使い方

Schemaファイルダウンロード

npm run schema

Swiftファイル生成

npm run codegen

注意点

Apollo CLIはよく引数やメソッド名が変わるらしいです。今回も諏訪氏の記事から4ヶ月くらいしか経っていないのですが、 --queries オプションがDeprecated扱いになっていました。

なので、うまく動かなければ公式ドキュメントを読んでみてくださいまし。

ではでは。