import re import sys from typing import Callable, Dict, List, Tuple class JsonValidationError(Exception): """Json データの比較に失敗した場合に発生する例外クラス""" def __init__(self, message: str) -> None: super().__init__(message) self.message = message class JsonValidator: """Json データの比較を行うクラス""" SPECIAL_COMPARE_PATTERN = r"^(#.*)\((.*)\)$" """ 特殊比較パターン。 """ def __init__(self, expected: Dict | List) -> None: """指定された JSON (辞書型) と合致するか否かを検証するバリデータを構築します。 Args: expected (Dict | List): 期待する JSON """ self.expected = expected self.allow_extra_keys = False self.special_compare_handlers: Dict[str, Callable] = { "#REGEX": self._compare_value_as_regex, "#RANGE": self._compare_value_as_range, } def is_valid(self, actual: Dict | List) -> Tuple[bool, str]: try: self.validate(actual) return True, "成功" except JsonValidationError as e: return False, e.message def validate(self, actual: Dict | List) -> None: """指定された JSON データが期待するデータと一致するか検証します。 Args: actual (Dict | List): JSON データ(辞書型またはリスト) Raises: JsonValidationError: 検証NGの場合に発生します。 """ self._compare(self.expected, actual) def _compare(self, expected: Dict | List, actual: Dict | List) -> None: """指定されたデーさを比較し、一致するか検証します。 Args: expected (Dict | List): 期待する JSON データ(辞書型またはリスト) actual (Dict | List): 実際の JSON データ(辞書型またはリスト) Raises: JsonValidationError: 検証NGの場合に発生します。 """ if isinstance(expected, Dict): self._compare_dict(expected, actual) elif isinstance(expected, List): self._compare_list(expected, actual) else: self._compare_value(expected, actual) def _compare_dict(self, expected: Dict, actual: Dict | List) -> None: """指定された辞書を比較します。 Args: expected (Dict): 期待する辞書 actual (Dict | List): 実際のデータ Raises: JsonValidationError: 検証NGの場合に発生します。 """ if isinstance(actual, List): # 辞書を期待するが、リストの場合、検証NG # raise JsonValidationError(f"Expected dict, Actual list: {actual}") raise JsonValidationError( f"辞書型を期待しているが、リスト型になっています。Actual: {actual}" ) for key, value in expected.items(): if key not in actual: # raise JsonValidationError(f"Expected key: {key}, Actual: {actual.keys()}") raise JsonValidationError( f"期待するキー ({key}) がありません。Actual: {actual.keys()}" ) self._compare(value, actual[key]) if not self.allow_extra_keys: # 追加キーが許容されない場合、追加キーがないか確認する。 expected_keys = set(expected.keys()) actual_keys = set(actual.keys()) if not actual_keys.issubset(expected_keys): # 実際の辞書が、期待する辞書に含まれていなきキーがあるため検証NG diff_keys = actual_keys - expected_keys # raise JsonValidationError(f"Unexpected keys: {diff_keys}") raise JsonValidationError(f"不要なキーがあります。 不要なキー: {diff_keys}") def _compare_list(self, expected: List, actual: Dict | List) -> None: """指定されたリストを比較します。 Args: expected (List): 期待するリスト actual (Dict | List): 実際のデータ Raises: JsonValidationError: 検証NGの場合に発生します。 """ if isinstance(actual, Dict): # リストを期待するが、辞書の場合、検証NG # raise JsonValidationError(f"Expected list, Actual dict: {actual}") raise JsonValidationError( f"リストを期待していますが、辞書型になっています。 Actual: {actual}" ) if len(expected) != len(actual): # リストサイズが異なるため検証NG # raise JsonValidationError( # f"Expected length: {len(expected)}, Actual length: {len(actual)}" # ) raise JsonValidationError( f"リストのサイズが異なります。(期待するサイズ: {len(expected)}, 実際のサイズ: {len(actual)})" ) for expected_item, actual_item in zip(expected, actual): # リスト中のアイテムが一致するか検証 self._compare(expected_item, actual_item) def _compare_value( self, expected: str | int | float | bool | None, actual: str | int | float | bool | None, ) -> None: # 特殊比較 if isinstance(expected, str): match = re.search(JsonValidator.SPECIAL_COMPARE_PATTERN, expected) if match: key = match.group(1) temp_args = match.group(2).split(",") args = list(map(str.strip, temp_args)) self.special_compare_handlers[key](args, actual) return # 通通比較 if expected != actual: # raise JsonValidationError(f"Expected: {expected}, Actual: {actual}") raise JsonValidationError( f"値が異なります。(期待する値: {expected}, 実際の値: {actual}" ) def _compare_value_as_regex( self, expected: List[str], actual: str | int | float | bool | None ) -> None: """指定された値が正規表現に合致するか検証します。 Args: expected: 期待する値(正規表現) actual: 実際の値 Raises: JsonValidationError: 検証NGの場合に発生します。 """ pattern: str = expected[0] if len(expected) >= 1 else "" if not re.match(pattern, str(actual)): # raise JsonValidationError(f"Expected: {expected}, Actual: {actual}") raise JsonValidationError( f"正規表現にマッチしません。(期待する正規表現: {pattern}, 実際の値: {actual})" ) def _compare_value_as_range( self, expected: List[str], actual: str | int | float | bool | None ) -> None: """指定された値が、指定範囲内に入っているか検証します。 Args: expected: 期待する値(範囲) actual: 実際の値 Raises: JsonValidationError: 検証NGの場合に発生します。 """ if not isinstance(actual, (int, float)): # raise JsonValidationError(f"Expected: number, Actual: {actual}") raise JsonValidationError( f"数値を期待していますが、数値型になっていません。 (実際の値:{actual})" ) if len(expected) != 2: # raise JsonValidationError(f"Expected: #RANGE(min,max), Actual: {expected}") raise JsonValidationError( f"期待する値の指定が誤っています。#RANGE(min,max) 形式で指定ください。(指定された期待する値: {expected})" ) min_value = float(expected[0]) if expected[0] else -sys.float_info.max max_value = float(expected[1]) if expected[1] else sys.float_info.max actual_value = float(actual) if (min_value <= actual_value) and (actual_value <= max_value): # 範囲内 pass else: # 範囲外 # raise JsonValidationError( # f"Expected: {min_value} - {max_value}, Actual: {actual_value}" # ) raise JsonValidationError( f"値が範囲外です。(期待する範囲: {expected[0]}~{expected[1]}, 実際の値: {actual})" ) expected = { "name": "John Doe", "age": "#RANGE(20,30)", "email": "#REGEX(^[a-zA-Z0-9]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)", "address": { "street": "123 Main St", "city": "Anytown", "state": "CA", "zip": "#RANGE(10,)", }, } actual = { "name": "John Doe", "age": 25, "email": "H0Pv2@example.com", "address": { "street": "123 Main St", "city": "Anytown", "state": "CA", "zip": 123456, }, } validator = JsonValidator(expected) result, detail = validator.is_valid(actual) print(f"{result}, {detail}")