Files
edubinskaya 15a660fe20 introduce json_validator.py
commit_hash:0846f3da46af520004ec4b4bba0abf3bbc6511d6
2024-09-18 13:37:21 +03:00

309 lines
9.9 KiB
Python

import argparse
import json
import re
used_to_build_card_template_field = "USED_TO_BUILD_CARD"
def print_warning(message, template_name=None, id=None, path=None):
if template_name is not None:
print(f"Warning: {message}. template='{template_name}'.")
elif id is not None:
print(f"Warning: {message}. id='{id}'.")
elif path is not None:
print(f"Warning: {message}. path='{path}'.")
else:
print(f"Warning: {message}.")
def print_error(message, template_name=None, id=None, path=None):
if template_name is not None:
print(f"ERROR: {message}. template='{template_name}'.")
elif id is not None:
print(f"ERROR: {message}. id='{id}'.")
elif path is not None:
print(f"ERROR: {message}. path='{path}'.")
else:
print(f"ERROR: {message}.")
def validate_no_unused_templates(templates_map):
for key, value in templates_map.items():
if used_to_build_card_template_field not in value:
print_warning(f"template named '{key}' is never used")
def validate_string_field(s, template_name=None, id=None):
evaluable_pattern = r'@{([^}]*)}'
evaluable_match = re.search(evaluable_pattern, s)
if evaluable_match:
raw_value = evaluable_match.group(1)
if (re.fullmatch(r'\d+', raw_value)
or re.fullmatch(r'\'[^\']+\'', raw_value)
or re.fullmatch(r'[+-]? *(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?', raw_value)):
print_warning(
f"evaluable expression {s} wrapping raw value. Use a raw value instead",
template_name=template_name,
id=id
)
quoted_values = re.findall(r'\'([^\']{200,})\'', raw_value)
if quoted_values:
print_warning(
f"evaluable expression '{s[:50]}..' with a string longer than 200 "
f"characters. It will be better for performance if you store long strings as "
f"variables",
template_name=template_name,
id=id
)
for value in quoted_values:
print(f"\tLong string: '{value[:50]}..'")
elif len(s) >= 200:
print_warning(
f"string '{s[:50]}..' longer than 200 characters. It will be better for "
f"performance if you store long strings as variables",
template_name=template_name,
id=id
)
def get_size(item, name, default, id=None):
if name in item:
size_block = item[name]
if 'type' in size_block:
type = size_block['type']
if type == 'match_parent':
return 'match_parent'
elif type == 'wrap_content':
return 'wrap_content'
elif type == 'fixed':
return 'fixed'
else:
print_error(
f"{name} with 'type'={type}. Use 'wrap_content', 'match_parent' or 'fixed'", id
)
return default
def validate_size(item, parent, path):
match_parent = "match_parent"
wrap_content = "wrap_content"
id = item.get('id', None)
parent_id = parent.get('id', None)
width = get_size(item, 'width', match_parent, id)
height = get_size(item, 'height', wrap_content, id)
parent_width = get_size(parent, 'width', match_parent, parent_id)
parent_height = get_size(parent, 'height', wrap_content)
orientation = 'horizontal'
if orientation in parent:
orientation = parent['orientation']
if width == match_parent and parent_width == wrap_content:
print_warning(
f"{orientation} container with 'wrap_content' width contains item with 'match_parent' "
f"width. child_id='{item.get('id', 'NO_ID')}'",
id=parent_id,
path=path
)
if height == match_parent and parent_height == wrap_content:
print_warning(
f"{orientation} container with 'wrap_content' height contains item with "
f"'match_parent' width. child_id='{item.get('id', 'NO_ID')}'",
id=parent_id,
path=path
)
def validate_item_raw(div):
for key, value in div.items():
if isinstance(value, str):
if 'id' in div:
validate_string_field(value, id=div['id'])
else:
validate_string_field(value)
def validate_raw_template(template, name):
# checking fields before joining all the templates to avoid warning duplicates.
for key, value in template.items():
if isinstance(value, str):
validate_string_field(value, name)
if 'type' not in template:
print_error("'type' is not specified", template_name=name)
if 'items' in template:
# checking items inside the container
for item in template['items']:
validate_raw_template(item, name)
def build_template(template, checked_types, templates_map):
type = template.get("type")
if type in checked_types:
# some of the templates can inherit others
# in this case templates would be resolved recursively
return
checked_types.add(type)
if type in templates_map:
original_template = templates_map[type]
build_template(original_template, checked_types, templates_map)
for key, value in original_template.items():
if key not in template and key != used_to_build_card_template_field:
template[key] = value
if key == 'type':
template[key] = value
original_template[used_to_build_card_template_field] = 1
def process_templates(templates, templates_map):
for template_id, template in templates.items():
templates_map[template_id] = template
validate_raw_template(template, template_id)
checked_types = set()
for key in templates_map:
build_template(templates_map[key], checked_types, templates_map)
def substitute_fields(template_name, template, div, to_modify_div, path):
if to_modify_div is None:
return
substitutions = []
for key, value in template.items():
if isinstance(value, dict):
substitute_fields(template_name, value, div, to_modify_div.get(key, None), path)
elif isinstance(value, list):
for index, item in enumerate(value):
if isinstance(item, dict):
to_modify_item = to_modify_div.get(key, [{}] * len(value))[index]
substitute_fields(template_name, item, div, to_modify_item, f"{path}[{index}]")
elif key.startswith("$"):
real_field = value
if real_field in div:
substitutions.append((key[1:], div.pop(real_field)))
elif key[1:] in to_modify_div:
print_warning(
f"Unnecessary substitution field '{key}': '{real_field}' in "
f"template={template_name}. Field '{key[1:]}' already exists",
id=div.get('id', None), path=path
)
for new_key, new_value in substitutions:
to_modify_div[new_key] = new_value
def process_item(div, templates_map, path):
if 'type' not in div and 'div' not in div:
print_error("'type' is not specified", id=div.get('id', None), path=path)
return
if 'div' in div:
process_item(div['div'], templates_map, path)
return
type = div['type']
# validate fields before merge with template data
validate_item_raw(div)
if type in templates_map:
template = templates_map[type]
template[used_to_build_card_template_field] = 1
for key, value in template.items():
if key not in div and key != used_to_build_card_template_field:
div[key] = value
if key == 'type':
div[key] = value
substitute_fields(type, template, div, div, path)
if type == 'state':
process_state(type, templates_map, path)
elif 'items' in div:
process_container(div, templates_map, path)
def process_container(container, templates_map, path):
items = container['items']
if len(items) == 0:
print_error("container without items", id=container.get("id", None), path=path)
return
if len(items) == 1:
print_warning(
"container with only one element. Please avoid unnecessary nesting",
id=container.get("id", None), path=path
)
for div in items:
if 'id' in div:
item_path = f"{path}.id={div['id']}"
else:
item_path = f"{path}.item#{items.index(div)}"
process_item(div, templates_map, item_path)
if container['type'] == 'container':
validate_size(div, container, item_path)
def process_state(states, templates_map, path):
for state in states:
state_id = state.get("state_id", None)
if state_id is None:
print_error("state_id not specified", path=path)
return
path = f"{path}.state_id={state_id}"
process_item(state['div'], templates_map, path)
def process_card(card, templates_map):
states = card['states']
process_state(states, templates_map, "")
def load_json(file_path):
try:
with open(file_path, 'r') as file:
data = json.load(file)
return data
except Exception as e:
print(f"Error loading JSON file: {e}")
return None
def main(json_file_path):
data = load_json(json_file_path)
if data is None:
return
templates_map = {}
if 'card' not in data:
process_card(data, templates_map)
else:
cards = data['card']
if 'templates' in data:
process_templates(data['templates'], templates_map)
process_card(cards, templates_map)
validate_no_unused_templates(templates_map)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('file_name', type=str, help='Path to the JSON file to check')
args = parser.parse_args()
main(args.file_name)