Feat(Fortune): More json file checker #57
@@ -34,3 +34,19 @@ repos:
|
|||||||
files: fortune_generator/json/static_special.json
|
files: fortune_generator/json/static_special.json
|
||||||
types: [json]
|
types: [json]
|
||||||
pass_filenames: false
|
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
|
||||||
|
|||||||
@@ -169,7 +169,11 @@ event_names = set()
|
|||||||
event_dates = 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(
|
if not require_field_check(
|
||||||
event,
|
event,
|
||||||
idx,
|
idx,
|
||||||
|
|||||||
169
scripts/check-fortune.py
Normal file
169
scripts/check-fortune.py
Normal file
@@ -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)
|
||||||
164
scripts/check-theme.py
Normal file
164
scripts/check-theme.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user