Initial commit
This commit is contained in:
448
commands/schema-validation/.scripts/format-validator.py
Executable file
448
commands/schema-validation/.scripts/format-validator.py
Executable file
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# ============================================================================
|
||||
# Format Validator Script
|
||||
# ============================================================================
|
||||
# Purpose: Validate format compliance for semver, URLs, emails, naming
|
||||
# Version: 1.0.0
|
||||
# Usage: ./format-validator.py --file <path> --type <plugin|marketplace> [--strict]
|
||||
# Returns: 0=all valid, 1=format violations, 2=error
|
||||
# ============================================================================
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
|
||||
# ====================
|
||||
# Color Support
|
||||
# ====================
|
||||
|
||||
class Colors:
|
||||
"""ANSI color codes for terminal output"""
|
||||
RED = '\033[0;31m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[1;33m'
|
||||
BLUE = '\033[0;34m'
|
||||
CYAN = '\033[0;36m'
|
||||
BOLD = '\033[1m'
|
||||
NC = '\033[0m'
|
||||
|
||||
@classmethod
|
||||
def disable(cls):
|
||||
"""Disable colors for non-TTY output"""
|
||||
cls.RED = cls.GREEN = cls.YELLOW = cls.BLUE = cls.CYAN = cls.BOLD = cls.NC = ''
|
||||
|
||||
|
||||
if not sys.stdout.isatty():
|
||||
Colors.disable()
|
||||
|
||||
|
||||
# ====================
|
||||
# Format Patterns
|
||||
# ====================
|
||||
|
||||
# Semantic versioning: X.Y.Z
|
||||
SEMVER_PATTERN = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$')
|
||||
|
||||
# Lowercase-hyphen naming: plugin-name
|
||||
LOWERCASE_HYPHEN_PATTERN = re.compile(r'^[a-z0-9]+(-[a-z0-9]+)*$')
|
||||
|
||||
# Email: RFC 5322 simplified
|
||||
EMAIL_PATTERN = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
|
||||
|
||||
# URL: http or https
|
||||
URL_PATTERN = re.compile(r'^https?://')
|
||||
|
||||
# HTTPS only
|
||||
HTTPS_PATTERN = re.compile(r'^https://')
|
||||
|
||||
# SPDX License Identifiers (common ones)
|
||||
SPDX_LICENSES = [
|
||||
'MIT', 'Apache-2.0', 'GPL-3.0', 'GPL-2.0', 'LGPL-3.0', 'LGPL-2.1',
|
||||
'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MPL-2.0', 'AGPL-3.0',
|
||||
'Unlicense', 'CC0-1.0', 'Proprietary'
|
||||
]
|
||||
|
||||
# Approved categories (10 standard)
|
||||
APPROVED_CATEGORIES = [
|
||||
'development', 'testing', 'deployment', 'documentation', 'security',
|
||||
'database', 'monitoring', 'productivity', 'quality', 'collaboration'
|
||||
]
|
||||
|
||||
|
||||
# ====================
|
||||
# Validation Functions
|
||||
# ====================
|
||||
|
||||
class FormatValidator:
|
||||
"""Format validation logic"""
|
||||
|
||||
def __init__(self, strict_https: bool = False):
|
||||
self.strict_https = strict_https
|
||||
self.errors: List[Tuple[str, str, str]] = []
|
||||
self.warnings: List[Tuple[str, str]] = []
|
||||
self.passed: List[Tuple[str, str]] = []
|
||||
|
||||
def validate_semver(self, field: str, value: str) -> bool:
|
||||
"""Validate semantic versioning"""
|
||||
if not value:
|
||||
return True # Skip empty (handled by required fields check)
|
||||
|
||||
if SEMVER_PATTERN.match(value):
|
||||
self.passed.append((field, f'"{value}" (semver)'))
|
||||
return True
|
||||
else:
|
||||
error = (
|
||||
field,
|
||||
f'"{value}"',
|
||||
'Invalid: Must use semantic versioning (X.Y.Z)\n'
|
||||
' Pattern: MAJOR.MINOR.PATCH\n'
|
||||
' Example: 1.0.0, 2.1.5'
|
||||
)
|
||||
self.errors.append(error)
|
||||
return False
|
||||
|
||||
def validate_lowercase_hyphen(self, field: str, value: str) -> bool:
|
||||
"""Validate lowercase-hyphen naming"""
|
||||
if not value:
|
||||
return True
|
||||
|
||||
if LOWERCASE_HYPHEN_PATTERN.match(value):
|
||||
self.passed.append((field, f'"{value}" (lowercase-hyphen)'))
|
||||
return True
|
||||
else:
|
||||
error = (
|
||||
field,
|
||||
f'"{value}"',
|
||||
'Invalid: Must use lowercase-hyphen format\n'
|
||||
' Pattern: ^[a-z0-9]+(-[a-z0-9]+)*$\n'
|
||||
' Example: my-plugin, test-tool, plugin123'
|
||||
)
|
||||
self.errors.append(error)
|
||||
return False
|
||||
|
||||
def validate_email(self, field: str, value: str) -> bool:
|
||||
"""Validate email address"""
|
||||
if not value:
|
||||
return True
|
||||
|
||||
if EMAIL_PATTERN.match(value):
|
||||
self.passed.append((field, f'"{value}" (valid email)'))
|
||||
return True
|
||||
else:
|
||||
error = (
|
||||
field,
|
||||
f'"{value}"',
|
||||
'Invalid: Must be valid email address\n'
|
||||
' Pattern: user@domain.tld\n'
|
||||
' Example: developer@example.com'
|
||||
)
|
||||
self.errors.append(error)
|
||||
return False
|
||||
|
||||
def validate_url(self, field: str, value: str) -> bool:
|
||||
"""Validate URL format"""
|
||||
if not value:
|
||||
return True
|
||||
|
||||
if self.strict_https and not HTTPS_PATTERN.match(value):
|
||||
error = (
|
||||
field,
|
||||
f'"{value}"',
|
||||
'Invalid: HTTPS required in strict mode\n'
|
||||
f' Current: {value}\n'
|
||||
f' Required: {value.replace("http://", "https://", 1)}'
|
||||
)
|
||||
self.errors.append(error)
|
||||
return False
|
||||
elif URL_PATTERN.match(value):
|
||||
if value.startswith('http://'):
|
||||
self.warnings.append((
|
||||
field,
|
||||
f'"{value}" - Consider using HTTPS for security'
|
||||
))
|
||||
self.passed.append((field, f'"{value}" (valid URL)'))
|
||||
return True
|
||||
else:
|
||||
error = (
|
||||
field,
|
||||
f'"{value}"',
|
||||
'Invalid: Must be valid URL\n'
|
||||
' Pattern: https://domain.tld/path\n'
|
||||
' Example: https://github.com/user/repo'
|
||||
)
|
||||
self.errors.append(error)
|
||||
return False
|
||||
|
||||
def validate_license(self, field: str, value: str) -> bool:
|
||||
"""Validate SPDX license identifier"""
|
||||
if not value:
|
||||
return True
|
||||
|
||||
if value in SPDX_LICENSES:
|
||||
self.passed.append((field, f'"{value}" (SPDX identifier)'))
|
||||
return True
|
||||
else:
|
||||
error = (
|
||||
field,
|
||||
f'"{value}"',
|
||||
'Invalid: Must be SPDX license identifier\n'
|
||||
' Common: MIT, Apache-2.0, GPL-3.0, BSD-3-Clause, ISC\n'
|
||||
' See: https://spdx.org/licenses/'
|
||||
)
|
||||
self.errors.append(error)
|
||||
return False
|
||||
|
||||
def validate_category(self, field: str, value: str) -> bool:
|
||||
"""Validate category against approved list"""
|
||||
if not value:
|
||||
return True
|
||||
|
||||
if value in APPROVED_CATEGORIES:
|
||||
self.passed.append((field, f'"{value}" (approved category)'))
|
||||
return True
|
||||
else:
|
||||
error = (
|
||||
field,
|
||||
f'"{value}"',
|
||||
'Invalid: Must be one of 10 approved categories\n'
|
||||
' Valid: development, testing, deployment, documentation,\n'
|
||||
' security, database, monitoring, productivity,\n'
|
||||
' quality, collaboration'
|
||||
)
|
||||
self.errors.append(error)
|
||||
return False
|
||||
|
||||
def validate_description_length(self, field: str, value: str) -> bool:
|
||||
"""Validate description length (50-200 chars recommended)"""
|
||||
if not value:
|
||||
return True
|
||||
|
||||
length = len(value)
|
||||
if 50 <= length <= 200:
|
||||
self.passed.append((field, f'Valid length ({length} chars)'))
|
||||
return True
|
||||
elif length < 50:
|
||||
self.warnings.append((
|
||||
field,
|
||||
f'Short description ({length} chars) - consider 50-200 characters for clarity'
|
||||
))
|
||||
return True
|
||||
else:
|
||||
self.warnings.append((
|
||||
field,
|
||||
f'Long description ({length} chars) - consider keeping under 200 characters'
|
||||
))
|
||||
return True
|
||||
|
||||
|
||||
# ====================
|
||||
# Plugin Validation
|
||||
# ====================
|
||||
|
||||
def validate_plugin_formats(data: Dict, validator: FormatValidator) -> int:
|
||||
"""Validate plugin format compliance"""
|
||||
print(f"{Colors.CYAN}Format Checks:{Colors.NC}\n")
|
||||
|
||||
# name: lowercase-hyphen
|
||||
if 'name' in data:
|
||||
validator.validate_lowercase_hyphen('name', data['name'])
|
||||
|
||||
# version: semver
|
||||
if 'version' in data:
|
||||
validator.validate_semver('version', data['version'])
|
||||
|
||||
# description: length check
|
||||
if 'description' in data:
|
||||
validator.validate_description_length('description', data['description'])
|
||||
|
||||
# license: SPDX
|
||||
if 'license' in data:
|
||||
validator.validate_license('license', data['license'])
|
||||
|
||||
# homepage: URL
|
||||
if 'homepage' in data:
|
||||
validator.validate_url('homepage', data['homepage'])
|
||||
|
||||
# repository: URL or object
|
||||
if 'repository' in data:
|
||||
repo = data['repository']
|
||||
if isinstance(repo, str):
|
||||
validator.validate_url('repository', repo)
|
||||
elif isinstance(repo, dict) and 'url' in repo:
|
||||
validator.validate_url('repository.url', repo['url'])
|
||||
|
||||
# category: approved list
|
||||
if 'category' in data:
|
||||
validator.validate_category('category', data['category'])
|
||||
|
||||
# author: email if object
|
||||
if 'author' in data:
|
||||
author = data['author']
|
||||
if isinstance(author, dict) and 'email' in author:
|
||||
validator.validate_email('author.email', author['email'])
|
||||
|
||||
return 0 if not validator.errors else 1
|
||||
|
||||
|
||||
# ====================
|
||||
# Marketplace Validation
|
||||
# ====================
|
||||
|
||||
def validate_marketplace_formats(data: Dict, validator: FormatValidator) -> int:
|
||||
"""Validate marketplace format compliance"""
|
||||
print(f"{Colors.CYAN}Format Checks:{Colors.NC}\n")
|
||||
|
||||
# name: lowercase-hyphen
|
||||
if 'name' in data:
|
||||
validator.validate_lowercase_hyphen('name', data['name'])
|
||||
|
||||
# owner.email: email
|
||||
if 'owner' in data and isinstance(data['owner'], dict):
|
||||
if 'email' in data['owner']:
|
||||
validator.validate_email('owner.email', data['owner']['email'])
|
||||
|
||||
# version: semver (if present)
|
||||
if 'version' in data:
|
||||
validator.validate_semver('version', data['version'])
|
||||
|
||||
# metadata fields
|
||||
if 'metadata' in data and isinstance(data['metadata'], dict):
|
||||
metadata = data['metadata']
|
||||
|
||||
if 'description' in metadata:
|
||||
validator.validate_description_length('metadata.description', metadata['description'])
|
||||
|
||||
if 'homepage' in metadata:
|
||||
validator.validate_url('metadata.homepage', metadata['homepage'])
|
||||
|
||||
if 'repository' in metadata:
|
||||
validator.validate_url('metadata.repository', metadata['repository'])
|
||||
|
||||
return 0 if not validator.errors else 1
|
||||
|
||||
|
||||
# ====================
|
||||
# Output Formatting
|
||||
# ====================
|
||||
|
||||
def print_results(validator: FormatValidator):
|
||||
"""Print validation results"""
|
||||
print()
|
||||
|
||||
# Passed checks
|
||||
if validator.passed:
|
||||
for field, msg in validator.passed:
|
||||
print(f" {Colors.GREEN}✅ {field}: {msg}{Colors.NC}")
|
||||
|
||||
# Errors
|
||||
if validator.errors:
|
||||
print()
|
||||
for field, value, msg in validator.errors:
|
||||
print(f" {Colors.RED}❌ {field}: {value}{Colors.NC}")
|
||||
for line in msg.split('\n'):
|
||||
print(f" {line}")
|
||||
print()
|
||||
|
||||
# Warnings
|
||||
if validator.warnings:
|
||||
print()
|
||||
for field, msg in validator.warnings:
|
||||
print(f" {Colors.YELLOW}⚠️ {field}: {msg}{Colors.NC}")
|
||||
|
||||
# Summary
|
||||
print()
|
||||
print(f"{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
|
||||
|
||||
total = len(validator.passed) + len(validator.errors)
|
||||
passed_count = len(validator.passed)
|
||||
|
||||
if validator.errors:
|
||||
print(f"{Colors.RED}Failed: {len(validator.errors)}{Colors.NC}")
|
||||
if validator.warnings:
|
||||
print(f"{Colors.YELLOW}Warnings: {len(validator.warnings)}{Colors.NC}")
|
||||
print(f"Status: {Colors.RED}FAIL{Colors.NC}")
|
||||
else:
|
||||
print(f"Passed: {passed_count}/{total}")
|
||||
if validator.warnings:
|
||||
print(f"{Colors.YELLOW}Warnings: {len(validator.warnings)}{Colors.NC}")
|
||||
print(f"Status: {Colors.GREEN}PASS{Colors.NC}")
|
||||
|
||||
|
||||
# ====================
|
||||
# Main Logic
|
||||
# ====================
|
||||
|
||||
def main():
|
||||
"""CLI entry point"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Validate format compliance for plugin and marketplace configurations',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--file',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Path to configuration file (plugin.json or marketplace.json)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--type',
|
||||
type=str,
|
||||
choices=['plugin', 'marketplace'],
|
||||
required=True,
|
||||
help='Configuration type'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--strict',
|
||||
action='store_true',
|
||||
help='Enforce HTTPS for all URLs'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load configuration file
|
||||
try:
|
||||
with open(args.file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"{Colors.RED}❌ File not found: {args.file}{Colors.NC}", file=sys.stderr)
|
||||
return 2
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"{Colors.RED}❌ Invalid JSON: {e}{Colors.NC}", file=sys.stderr)
|
||||
print(f"{Colors.BLUE}ℹ️ Run JSON validation first{Colors.NC}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# Print header
|
||||
print(f"{Colors.BOLD}{Colors.BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
|
||||
print(f"{Colors.BOLD}Format Validation{Colors.NC}")
|
||||
print(f"{Colors.BOLD}{Colors.BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.NC}")
|
||||
print(f"Target: {args.file}")
|
||||
print(f"Type: {args.type}")
|
||||
if args.strict:
|
||||
print(f"Strict HTTPS: {Colors.GREEN}Enforced{Colors.NC}")
|
||||
print()
|
||||
|
||||
# Create validator
|
||||
validator = FormatValidator(strict_https=args.strict)
|
||||
|
||||
# Validate based on type
|
||||
if args.type == 'plugin':
|
||||
result = validate_plugin_formats(data, validator)
|
||||
else:
|
||||
result = validate_marketplace_formats(data, validator)
|
||||
|
||||
# Print results
|
||||
print_results(validator)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user