diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e01afb6..a1c4588 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,3 +34,19 @@ repos: files: fortune_generator/json/static_special.json types: [json] pass_filenames: false + + - id: check-fortune + name: check-fortune + entry: python3 scripts/check-fortune.py fortune_generator/json/fortune.json + language: python + files: fortune_generator/json/fortune.json + types: [json] + pass_filenames: false + + - id: check-theme + name: check-theme + entry: python3 scripts/check-theme.py fortune_generator/json/themes.json + language: python + files: fortune_generator/json/themes.json + types: [json] + pass_filenames: false diff --git a/scripts/check-events.py b/scripts/check-events.py index 76d2256..707712e 100644 --- a/scripts/check-events.py +++ b/scripts/check-events.py @@ -169,7 +169,11 @@ event_names = set() event_dates = set() -def check_structure(event: dict, idx: int): +def check_structure(event, idx: int): + if not isinstance(event, dict): + errors[idx].append("should be a dict") + return False + if not require_field_check( event, idx, diff --git a/scripts/check-fortune.py b/scripts/check-fortune.py new file mode 100644 index 0000000..f689720 --- /dev/null +++ b/scripts/check-fortune.py @@ -0,0 +1,169 @@ +#!/bin/python3 + +import json +import logging +import argparse +import collections + +args_parser = argparse.ArgumentParser(description="fortune checker") +args_parser.add_argument("path", type=str, help="event json file path") + +args = args_parser.parse_args() +errors: dict[tuple[str, int], list[str]] = collections.defaultdict(list) +good_fortunes: list[dict] = None +bad_fortunes: list[dict] = None +all_fortunes = None + +try: + with open(args.path) as f: + all_fortunes = json.loads(f.read()) +except json.JSONDecodeError: + print(f"`{args.path}` json syntax error.") + exit(-1) + +except FileNotFoundError: + print(f"`{args.path}` not found.") + print("Please contact developer to solve this problem.") + exit(-1) + +if not isinstance(all_fortunes, dict): + print(f"`{args.path}` should contain a dict") + exit(-1) + +try: + good_fortunes = all_fortunes["goodFortunes"] +except KeyError: + print(f"`{args.path}` should contain `goodFortunes`") + +if not isinstance(good_fortunes, list): + print("`goodFortunes` should be a list.") + +try: + bad_fortunes = all_fortunes["badFortunes"] +except KeyError: + print(f"`{args.path}` should contain `badFortunes`") + +if not isinstance(bad_fortunes, list): + print("`badFortunes` should be a list.") + + +def require_field_check( + obj: dict, + fortune_idx: tuple[str, int], + fields: list[tuple[str, type]], + required_field: str = "", +) -> bool: + """ + Validates the presence and types of required fields in a given object. + + Args: + obj (dict): The object (dictionary) to validate. + fortune_idx (tuple[str, int]): The index of the fortune for associating validation errors. + fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type. + required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "". + + Returns: + bool: True if all required fields are present and have the correct types, otherwise False. + + Validation Rules: + - If a required field is missing, an error message is recorded. + - If a field is present but its type does not match the expected type, an error message is recorded. + - The `required_field` parameter, if provided, is prepended to error messages for context. + """ + + error_found = False + for field_name, field_type in fields: + if field_name not in obj: + error_found = True + msg = "" + if required_field != "": + msg = f"`{required_field}` " + + msg += f"missing `{field_name}`." + errors[fortune_idx].append(msg) + + elif not isinstance(obj[field_name], field_type): + error_found = True + errors[fortune_idx].append( + f"`{field_name}` should be a `{field_type}` type." + ) + + if error_found: + return False + return True + + +fortune_names = set() + +def check_fortune(fortune, idx: tuple[str, int]): + if not isinstance(fortune, dict): + errors[idx].append("fortune should be a dict.") + return False + + if not require_field_check(fortune, idx, [ + ("event", str), + ("description", list) + ]): + return False + + fortune_name = fortune["event"] + if fortune_name in fortune_names: + errors[idx].append(f"fortune `{fortune_name}` already exists.") + + if not fortune_name: + errors[idx].append("fortune name should not be empty.") + + + if not fortune["description"]: + errors[idx].append("fortune description should not be empty.") + return False + + descriptions = set() + for desc in fortune["description"]: + if not isinstance(desc, str): + errors[idx].append(f"fortune description {desc} should be a string.") + continue + + if not desc: + errors[idx].append(f"fortune description {desc} should not be empty.") + continue + + if desc in descriptions: + errors[idx].append(f"fortune description {desc} already exists.") + continue + else: + descriptions.add(desc) + + fortune_names.add(fortune_name) + + return True + +if good_fortunes: + for idx, fortune in enumerate(good_fortunes): + check_fortune(fortune, ("goodFortunes", idx)) + +fortune_names.clear() +if bad_fortunes: + for idx, fortune in enumerate(bad_fortunes): + check_fortune(fortune, ("badFortunes", idx)) + +if errors: + logging.error(args.path) + for idx, error_msgs in errors.items(): + fortunes = None + if idx[0] == "goodFortunes": + fortunes = good_fortunes + elif idx[0] == "badFortunes": + fortunes = bad_fortunes + + if not fortunes: + continue + + logging.error( + json.dumps( + fortunes[idx[1]], indent=4, ensure_ascii=False + ) + ) + for msg in error_msgs: + logging.error(msg) + exit(-1) diff --git a/scripts/check-theme.py b/scripts/check-theme.py new file mode 100644 index 0000000..b6b1584 --- /dev/null +++ b/scripts/check-theme.py @@ -0,0 +1,164 @@ +#!/bin/python3 + +import json +import logging +import argparse +import collections + +args_parser = argparse.ArgumentParser(description="theme checker") +args_parser.add_argument("path", type=str, help="event json file path") + +args = args_parser.parse_args() +errors: dict[int, list[str]] = collections.defaultdict(list) +themes: list[dict[str]] = None +j = None + +try: + with open(args.path) as f: + j = json.loads(f.read()) +except json.JSONDecodeError: + print(f"`{args.path}` json syntax error.") + exit(-1) + +except FileNotFoundError: + print(f"`{args.path}` not found.") + print("Please contact developer to solve this problem.") + exit(-1) + +if not isinstance(j, dict): + print(f"`{args.path}` should contain a dict") + exit(-1) + +try: + themes = j["themes"] +except KeyError: + print(f"`{args.path}` should contain `themes`") + exit(-1) + +if not isinstance(themes, list): + print("`themes` should be a list.") + exit(-1) + +def require_field_check( + obj: dict, + theme_idx: int, + fields: list[tuple[str, type]], + required_field: str = "", +) -> bool: + """ + Validates the presence and types of required fields in a given object. + + Args: + obj (dict): The object (dictionary) to validate. + theme_idx (int): The index of the fortune for associating validation errors. + fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type. + required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "". + + Returns: + bool: True if all required fields are present and have the correct types, otherwise False. + + Validation Rules: + - If a required field is missing, an error message is recorded. + - If a field is present but its type does not match the expected type, an error message is recorded. + - The `required_field` parameter, if provided, is prepended to error messages for context. + """ + + error_found = False + for field_name, field_type in fields: + if field_name not in obj: + error_found = True + msg = "" + if required_field != "": + msg = f"`{required_field}` " + + msg += f"missing `{field_name}`." + errors[theme_idx].append(msg) + + elif not isinstance(obj[field_name], field_type): + error_found = True + errors[theme_idx].append( + f"`{field_name}` should be a `{field_type}` type." + ) + + if error_found: + return False + return True + + +theme_names = set() + +def check_theme(theme, idx: int): + if not isinstance(theme, dict): + errors[idx].append("theme should be a dict.") + return False + + if not require_field_check(theme, idx, [ + ("name", str), + ("properties", dict) + ]): + return False + + theme_name = theme["name"] + if theme_name in theme_names: + errors[idx].append(f"theme `{theme_name}` already exists.") + + if not theme_name: + errors[idx].append("theme name should not be empty.") + + properties = theme["properties"] + properties_field_required = [ + ("bg-color", str), + ("good-fortune-color", str), + ("bad-fortune-color", str), + ("middle-fortune-color", str), + ("title-color", str), + ("desc-color", str), + ("button-color", str), + ("button-hover-color", str), + ("toggle-theme-button-color", str), + ("copy-result-button-color", str), + ("date-color", str), + ("special-event-color", str), + ] + if not require_field_check(properties, idx, properties_field_required): + return False + + for field_name in (v[0] for v in properties_field_required): + color: str = properties[field_name] + if color[0] != "#": + errors[idx].append(f"color {color} should starts with `#`.") + continue + + color = color[1:] + if any(not ch.isdigit() and not ch.islower() for ch in color): + errors[idx].append(f"color {color} should be all lowercase.") + continue + + hex = set("0123456789abcdef") + if any(ch not in hex for ch in color): + errors[idx].append(f"color {color} should be a hex value.") + continue + + if len(color) != len("rrggbb") and len(color) != len("rrggbbaa"): + errors[idx].append(f"color {color} should be in `rrggbb` or `rrggbbaa` format.") + continue + + + theme_names.add(theme_name) + + return True + +for idx, theme in enumerate(themes): + check_theme(theme, idx) + +if errors: + logging.error(args.path) + for idx, error_msgs in errors.items(): + logging.error( + json.dumps( + themes[idx], indent=4, ensure_ascii=False + ) + ) + for msg in error_msgs: + logging.error(msg) + exit(-1)