005: 「JSONは誰の息子」の解答¶
難易度: ☆☆
方針¶
JSON文字列をPythonの値へ変換したあと、辞書であることを確認します。 そこから、未知キー、必須キー、省略可能なキー、入れ子の辞書を順に検査します。
検査の順番を固定すると、どの入力でどのエラーを返すかが安定します。 これはテストを書きやすくするためにも有効です。
実装¶
import json
ALLOWED_KEYS = {"report_name", "group_by", "min_total", "include_zero", "aliases"}
ALLOWED_GROUPS = {"item", "date"}
def load_report_settings(text):
data = json.loads(text)
if not isinstance(data, dict):
raise ValueError("settings must be an object")
unknown_keys = set(data) - ALLOWED_KEYS
if unknown_keys:
key = sorted(unknown_keys)[0]
raise ValueError(f"unknown setting: {key}")
report_name = data.get("report_name")
if type(report_name) is not str or report_name == "":
raise ValueError("report_name is required")
group_by = data.get("group_by", "item")
if group_by not in ALLOWED_GROUPS:
raise ValueError("group_by must be item or date")
min_total = data.get("min_total", 0)
if type(min_total) is not int or min_total < 0:
raise ValueError("min_total must be a non-negative int")
include_zero = data.get("include_zero", False)
if type(include_zero) is not bool:
raise ValueError("include_zero must be a bool")
aliases = data.get("aliases", {})
if not isinstance(aliases, dict):
raise ValueError("aliases must be an object")
for key, value in aliases.items():
if type(key) is not str or type(value) is not str:
raise ValueError("aliases must map strings to strings")
return {
"report_name": report_name,
"group_by": group_by,
"min_total": min_total,
"include_zero": include_zero,
"aliases": dict(aliases),
}
確認¶
assert load_report_settings('{"report_name": "monthly"}') == {
"report_name": "monthly",
"group_by": "item",
"min_total": 0,
"include_zero": False,
"aliases": {},
}
assert load_report_settings(
'{"report_name": "monthly", "group_by": "date", "min_total": 1000, '
'"include_zero": true, "aliases": {"pen": "文具"}}'
) == {
"report_name": "monthly",
"group_by": "date",
"min_total": 1000,
"include_zero": True,
"aliases": {"pen": "文具"},
}
try:
load_report_settings("[]")
except ValueError as error:
assert str(error) == "settings must be an object"
else:
raise AssertionError("ValueError was not raised")
try:
load_report_settings('{"report_name": "monthly", "min_total": true}')
except ValueError as error:
assert str(error) == "min_total must be a non-negative int"
else:
raise AssertionError("ValueError was not raised")
発展¶
設定項目が増えると、1つの関数に検査が集中します。 その場合は、項目ごとの検査関数を作り、どのキーをどの関数で検査するかを辞書で持つと見通しを保てます。