|
| 1 | +import yaml |
| 2 | +import re |
| 3 | +import sys |
| 4 | + |
| 5 | +REQUIRED_FIELDS = { |
| 6 | + 'name': str, |
| 7 | + 'repo': str, |
| 8 | + 'description': str, |
| 9 | + 'creator': dict, |
| 10 | + 'latest_release_tag': str, |
| 11 | + 'screenshots': list, |
| 12 | +} |
| 13 | + |
| 14 | +REQUIRED_CREATOR_FIELDS = { |
| 15 | + 'name': str, |
| 16 | + 'url': str, |
| 17 | + 'avatar': str, |
| 18 | +} |
| 19 | + |
| 20 | +YOUTUBE_REGEX = re.compile(r'https://img\.youtube\.com/vi/([^/]+)/[^/]+\.jpg') |
| 21 | +REPO_REGEX = re.compile(r'^[\w\-]+/[\w\-\.]+$') |
| 22 | + |
| 23 | +def validate_entry(entry, is_plugin=True, index=0): |
| 24 | + name = entry.get('name', f'<Unnamed {index}>') |
| 25 | + errors = [] |
| 26 | + |
| 27 | + for field, field_type in REQUIRED_FIELDS.items(): |
| 28 | + if field not in entry: |
| 29 | + errors.append(f"❌ Missing field `{field}` in entry '{name}'") |
| 30 | + elif not isinstance(entry[field], field_type): |
| 31 | + errors.append(f"❌ Field `{field}` in entry '{name}' must be {field_type.__name__}") |
| 32 | + |
| 33 | + repo = entry.get('repo') |
| 34 | + if repo and not REPO_REGEX.match(repo): |
| 35 | + errors.append(f"❌ Invalid `repo` format in '{name}', must be 'owner/repo'") |
| 36 | + |
| 37 | + creator = entry.get('creator', {}) |
| 38 | + if not isinstance(creator, dict): |
| 39 | + errors.append(f"❌ `creator` must be a dictionary in entry '{name}'") |
| 40 | + else: |
| 41 | + for cfield, ctype in REQUIRED_CREATOR_FIELDS.items(): |
| 42 | + if cfield not in creator: |
| 43 | + errors.append(f"❌ Missing `creator.{cfield}` in entry '{name}'") |
| 44 | + elif not isinstance(creator[cfield], ctype): |
| 45 | + errors.append(f"❌ `creator.{cfield}` in entry '{name}' must be {ctype.__name__}") |
| 46 | + elif isinstance(creator[cfield], str) and not creator[cfield].startswith("https://"): |
| 47 | + errors.append(f"❌ `creator.{cfield}` URL must start with https:// in '{name}'") |
| 48 | + |
| 49 | + for ss in entry.get('screenshots', []): |
| 50 | + if not isinstance(ss, dict): |
| 51 | + errors.append(f"❌ Screenshot must be a dictionary in '{name}'") |
| 52 | + continue |
| 53 | + if 'url' not in ss or not isinstance(ss['url'], str): |
| 54 | + errors.append(f"❌ Missing or invalid `url` in screenshot of '{name}'") |
| 55 | + elif not ss['url'].startswith("https://"): |
| 56 | + errors.append(f"❌ Screenshot URL must start with https:// in '{name}'") |
| 57 | + elif ss['url'].startswith("https://img.youtube.com/") and not YOUTUBE_REGEX.match(ss['url']): |
| 58 | + errors.append(f"❌ Invalid YouTube thumbnail format in screenshot of '{name}'") |
| 59 | + |
| 60 | + if 'alt' in ss and not isinstance(ss['alt'], str): |
| 61 | + errors.append(f"❌ `alt` must be a string in screenshot of '{name}'") |
| 62 | + |
| 63 | + if 'width' in ss and not isinstance(ss['width'], int): |
| 64 | + errors.append(f"❌ `width` must be an integer in screenshot of '{name}'") |
| 65 | + |
| 66 | + if is_plugin: |
| 67 | + if 'mc_versions' not in entry: |
| 68 | + errors.append(f"❌ Missing `mc_versions` in plugin '{name}'") |
| 69 | + elif not isinstance(entry['mc_versions'], str): |
| 70 | + errors.append(f"❌ `mc_versions` must be a string in plugin '{name}'") |
| 71 | + |
| 72 | + if 'is_core' in entry and not isinstance(entry['is_core'], bool): |
| 73 | + errors.append(f"❌ `is_core` must be a boolean in plugin '{name}'") |
| 74 | + |
| 75 | + return errors |
| 76 | + |
| 77 | +def main(): |
| 78 | + try: |
| 79 | + with open('data/plugins-and-themes.yml', 'r') as f: |
| 80 | + data = yaml.safe_load(f) |
| 81 | + except Exception as e: |
| 82 | + print(f"❌ Failed to load YAML: {e}") |
| 83 | + sys.exit(1) |
| 84 | + |
| 85 | + if not isinstance(data, dict): |
| 86 | + print("❌ Top-level YAML must be a dictionary with 'plugins' and 'themes'") |
| 87 | + sys.exit(1) |
| 88 | + |
| 89 | + all_errors = [] |
| 90 | + |
| 91 | + for i, plugin in enumerate(data.get("plugins", [])): |
| 92 | + all_errors.extend(validate_entry(plugin, is_plugin=True, index=i)) |
| 93 | + |
| 94 | + for i, theme in enumerate(data.get("themes", [])): |
| 95 | + all_errors.extend(validate_entry(theme, is_plugin=False, index=i)) |
| 96 | + |
| 97 | + if all_errors: |
| 98 | + print("\n--- VALIDATION ERRORS ---\n") |
| 99 | + for err in all_errors: |
| 100 | + print(err) |
| 101 | + print(f"\n❌ {len(all_errors)} issue(s) found.") |
| 102 | + sys.exit(1) |
| 103 | + else: |
| 104 | + print("✅ plugins-and-themes.yml validation passed with no issues.") |
| 105 | + |
| 106 | +if __name__ == "__main__": |
| 107 | + main() |
0 commit comments