Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

設定ファイルを受取りjinja2で生成したJSONファイルがvalidなJSONかチェックしたい

More than 3 years have passed since last update.

設定ファイルを受取りjinja2で生成したJSONファイルがデプロイの際に使われる。この生成されたJSONファイルがvalidなJSONかチェックしたい。

利用しているコマンド

j2cliみたいなイメージ。

以下の2つのファイルを使う。

  • 設定ファイル(.json)
  • templateファイル(.json.j2)

例えば、設定ファイルが以下のときに、

{
  "name": "staging"
}

このようなtemplateを使う。

{
  "app-name": "{{name}}-app",
  "batch-name": "{{name}}-batch"
}

結果として以下のようなJSONを出力する。これが実際の設定ファイルとして使われる。

{
  "app-name": "staging-app",
  "batch-name": "staging-batch"
}

dev的な設定の場合は以下のような出力になるかもしれない。

{
  "app-name": "dev-app",
  "batch-name": "dev-batch"
}

問題

生成されたJSONがinvalidな場合がある。本番にデプロイされたタイミングでようやく気づくということになるとかなしい。事前にチェックしたい。

やること

方針は以下。

  1. templateをjinja2でemit
  2. emitされたJSONを再度json.loads
  3. エラーが発生したらinvalidなJSONなはず

pythonのコードにするとこんな感じ。

import jinja2
import json


env = jinja2.Environment(loader=jinja2.FileSystemLoader(["."]), undefined=jinja2.StrictUndefined)
t = env.get_or_select_template(template_path)
json.loads(t.render(**config))  # invalidならエラー

ただいくつか注意点がある。

注意点1 期待した値が設定ファイル中に含まれてない場合

{"foo": "foo-{{name}}"}

このようなtemplateに何も設定せずにjinja2でemitすると以下の様になるがエラーにならない。

{"foo": "foo-"}

これはjinja2のundefinedの設定を変えれば検知できる。undefined=jinja2.StrictUndefined を付けてあげれば UndefinedError が発生するようになる。

注意点2 templateに "}" が過剰に含まれていた場合

templateに不備があり、"}" が過剰に含まれている場合がまずい。生成されるJSONはJSONとしてはvalidではあるけれど。期待した出力ではない。

誤って以下のようなtemplateを書いてしまった場合。

{
  "app-name": "{{name}}}-app"
}

以下はvalidなJSONではあるけれど。期待した出力ではない。

{
  "app-name": "staging}-app"
}

一応dictのvalueの値もチェックすると良い。

jinja2でemitした結果がvalidなJSONか調べてくれるスクリプト

以上を踏まえてjinja2でemitした結果がvalidなJSONか調べてくれるスクリプトを作った。こんな感じで使う。environという変数が存在しなかった場合には以下の様にエラーになる。したの利用例は environ という名前の設定が設定ファイル中二存在していなかった場合のエラー。

$ python check.py --conf-dir conf/ --template-dir template/ || echo ng
template/back.json.j2(conf/master.json): UndefinedError 'environ' is undefined
template/back.json.j2(conf/production.json): UndefinedError 'environ' is undefined
template/back.json.j2(conf/rook.json): UndefinedError 'environ' is undefined
ng

実装は以下のようなもの。

import sys
import os.path
import argparse
import jinja2
import json


def validate_conf(d, path=None):
    path = path or []
    if hasattr(d, "items"):
        for k, v in d.items():
            path.append(k)
            validate_conf(v, path=path)
            path.pop()
    elif isinstance(d, (list, tuple)):
        for i, x in enumerate(d):
            path.append(i)
            validate_conf(x, path=path)
            path.pop()
    elif isinstance(d, str):
        v = d
        if "{" in v:
            raise ValueError("invalid value: '{{' is included. path={}".format(path))
        if "}" in v:
            raise ValueError("invalid value: '}}' is included. path={}".format(path))
    return d


def check(env, config, template_path):
    t = env.get_or_select_template(template_path)
    data = json.loads(t.render(**config))
    return validate_conf(data)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--conf-dir", required=True)
    parser.add_argument("--template-dir", required=True)
    args = parser.parse_args()

    env = jinja2.Environment(loader=jinja2.FileSystemLoader(["."]), undefined=jinja2.StrictUndefined)
    status = 0
    for root, _, conf_files in os.walk(args.conf_dir):
        for conf_file in conf_files:
            try:
                confpath = os.path.join(root, conf_file)
                with open(confpath) as rf:
                    conf = json.load(rf)
            except Exception as e:
                sys.stderr.write("{confpath}: {e.__class__.__name__} {e}\n".format(confpath=confpath, e=e))
                status = 1
                continue

            for root2, _, template_files in os.walk(args.template_dir):
                for template_file in template_files:
                    try:
                        filepath = os.path.join(root2, template_file)
                        check(env, conf, filepath)
                    except Exception as e:
                        sys.stderr.write("{filepath}({confpath}): {e.__class__.__name__} {e}\n".format(
                            confpath=confpath, filepath=filepath, e=e)
                        )
                        status = 1
    sys.exit(status)


if __name__ == "__main__":
    main()
podhmo
wacul
人工知能でWebサイトの課題を発見する AIアナリスト https://wacul-ai.com を開発しています
https://wacul.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away