|
| 1 | +#!/usr/bin/env python3 |
| 2 | +import argparse |
| 3 | +import json |
| 4 | +import logging |
| 5 | +import os |
| 6 | +import sys |
| 7 | + |
| 8 | +from components.syntax import Command |
| 9 | +from components.markdown import Markdown |
| 10 | + |
| 11 | + |
| 12 | +def command_filename(name: str) -> str: |
| 13 | + """Convert command name to filename format.""" |
| 14 | + return name.lower().replace(' ', '-') |
| 15 | + |
| 16 | + |
| 17 | +def parse_args() -> argparse.Namespace: |
| 18 | + parser = argparse.ArgumentParser(description='Creates new Redis command pages from JSON input') |
| 19 | + parser.add_argument('json_file', type=str, |
| 20 | + help='Path to JSON file containing command definitions') |
| 21 | + parser.add_argument('--loglevel', type=str, |
| 22 | + default='INFO', |
| 23 | + help='Python logging level (overwrites LOGLEVEL env var)') |
| 24 | + return parser.parse_args() |
| 25 | + |
| 26 | + |
| 27 | +def validate_json_structure(data: dict, filename: str) -> None: |
| 28 | + """Validate that the JSON has the expected structure for Redis commands.""" |
| 29 | + if not isinstance(data, dict): |
| 30 | + raise ValueError(f"JSON file {filename} must contain a dictionary at root level") |
| 31 | + |
| 32 | + for command_name, command_data in data.items(): |
| 33 | + if not isinstance(command_data, dict): |
| 34 | + raise ValueError(f"Command '{command_name}' must be a dictionary") |
| 35 | + |
| 36 | + # Check for required fields |
| 37 | + required_fields = ['summary', 'since', 'group'] |
| 38 | + for field in required_fields: |
| 39 | + if field not in command_data: |
| 40 | + logging.warning(f"Command '{command_name}' missing recommended field: {field}") |
| 41 | + |
| 42 | + # Validate arguments structure if present |
| 43 | + if 'arguments' in command_data: |
| 44 | + if not isinstance(command_data['arguments'], list): |
| 45 | + raise ValueError(f"Command '{command_name}' arguments must be a list") |
| 46 | + |
| 47 | + |
| 48 | +def load_and_validate_json(filepath: str) -> dict: |
| 49 | + """Load and validate the JSON file containing command definitions.""" |
| 50 | + if not os.path.exists(filepath): |
| 51 | + raise FileNotFoundError(f"JSON file not found: {filepath}") |
| 52 | + |
| 53 | + try: |
| 54 | + with open(filepath, 'r') as f: |
| 55 | + data = json.load(f) |
| 56 | + except json.JSONDecodeError as e: |
| 57 | + raise ValueError(f"Invalid JSON in file {filepath}: {e}") |
| 58 | + |
| 59 | + validate_json_structure(data, filepath) |
| 60 | + return data |
| 61 | + |
| 62 | + |
| 63 | +def add_standard_categories(fm_data: dict) -> None: |
| 64 | + """Add the standard categories from create.sh script.""" |
| 65 | + standard_categories = [ |
| 66 | + 'docs', 'develop', 'stack', 'oss', 'rs', 'rc', 'oss', 'kubernetes', 'clients' |
| 67 | + ] |
| 68 | + fm_data['categories'] = standard_categories |
| 69 | + |
| 70 | + |
| 71 | +def get_full_command_name(command_name: str, command_data: dict) -> str: |
| 72 | + """Get the full command name, handling container commands.""" |
| 73 | + container = command_data.get('container') |
| 74 | + if container: |
| 75 | + return f"{container} {command_name}" |
| 76 | + return command_name |
| 77 | + |
| 78 | + |
| 79 | +def generate_command_frontmatter(command_name: str, command_data: dict, all_commands: dict) -> dict: |
| 80 | + """Generate complete Hugo frontmatter for a command using existing build infrastructure.""" |
| 81 | + # Get the full command name (handles container commands) |
| 82 | + full_command_name = get_full_command_name(command_name, command_data) |
| 83 | + |
| 84 | + # Create Command object to generate syntax using the full command name |
| 85 | + c = Command(full_command_name, command_data) |
| 86 | + |
| 87 | + # Start with the command data |
| 88 | + fm_data = command_data.copy() |
| 89 | + |
| 90 | + # Add required Hugo frontmatter fields |
| 91 | + fm_data.update({ |
| 92 | + 'title': full_command_name, |
| 93 | + 'linkTitle': full_command_name, |
| 94 | + 'description': command_data.get('summary'), |
| 95 | + 'syntax_str': str(c), |
| 96 | + 'syntax_fmt': c.syntax(), |
| 97 | + 'hidden': False # Default to not hidden |
| 98 | + }) |
| 99 | + |
| 100 | + # Add the standard categories from create.sh |
| 101 | + add_standard_categories(fm_data) |
| 102 | + |
| 103 | + return fm_data |
| 104 | + |
| 105 | + |
| 106 | +def generate_argument_sections(command_data: dict) -> str: |
| 107 | + """Generate placeholder sections for Required arguments and Optional arguments.""" |
| 108 | + content = "" |
| 109 | + |
| 110 | + arguments = command_data.get('arguments', []) |
| 111 | + if not arguments: |
| 112 | + return content |
| 113 | + |
| 114 | + required_args = [] |
| 115 | + optional_args = [] |
| 116 | + |
| 117 | + # Categorize arguments |
| 118 | + for arg in arguments: |
| 119 | + if arg.get('optional', False): |
| 120 | + optional_args.append(arg) |
| 121 | + else: |
| 122 | + required_args.append(arg) |
| 123 | + |
| 124 | + # Generate Required arguments section |
| 125 | + if required_args: |
| 126 | + content += "## Required arguments\n\n" |
| 127 | + for arg in required_args: |
| 128 | + arg_name = arg.get('name', 'unknown') |
| 129 | + arg_type = arg.get('type', 'unknown') |
| 130 | + display_text = arg.get('display_text', arg_name) |
| 131 | + |
| 132 | + content += f"<details open><summary><code>{display_text}</code></summary>\n\n" |
| 133 | + content += f"TODO: Add description for {arg_name} ({arg_type})\n\n" |
| 134 | + content += "</details>\n\n" |
| 135 | + |
| 136 | + # Generate Optional arguments section |
| 137 | + if optional_args: |
| 138 | + content += "## Optional arguments\n\n" |
| 139 | + for arg in optional_args: |
| 140 | + arg_name = arg.get('name', 'unknown') |
| 141 | + arg_type = arg.get('type', 'unknown') |
| 142 | + display_text = arg.get('display_text', arg_name) |
| 143 | + token = arg.get('token', '') |
| 144 | + |
| 145 | + content += f"<details open><summary><code>{token if token else display_text}</code></summary>\n\n" |
| 146 | + content += f"TODO: Add description for {arg_name} ({arg_type})\n\n" |
| 147 | + content += "</details>\n\n" |
| 148 | + |
| 149 | + return content |
| 150 | + |
| 151 | + |
| 152 | +def generate_return_section() -> str: |
| 153 | + """Generate placeholder Return information section.""" |
| 154 | + return '''## Return information |
| 155 | +
|
| 156 | +{{< multitabs id="return-info" |
| 157 | + tab1="RESP2" |
| 158 | + tab2="RESP3" >}} |
| 159 | +
|
| 160 | +TODO: Add RESP2 return information |
| 161 | +
|
| 162 | +-tab-sep- |
| 163 | +
|
| 164 | +TODO: Add RESP3 return information |
| 165 | +
|
| 166 | +{{< /multitabs >}} |
| 167 | +
|
| 168 | +''' |
| 169 | + |
| 170 | + |
| 171 | +def generate_complete_markdown_content(command_name: str, command_data: dict) -> str: |
| 172 | + """Generate the complete markdown content for a command page.""" |
| 173 | + content = "" |
| 174 | + |
| 175 | + # Add command summary as the main description |
| 176 | + summary = command_data.get('summary', f'TODO: Add summary for {command_name}') |
| 177 | + content += f"{summary}\n\n" |
| 178 | + |
| 179 | + # Add argument sections |
| 180 | + content += generate_argument_sections(command_data) |
| 181 | + |
| 182 | + # Add return information section |
| 183 | + content += generate_return_section() |
| 184 | + |
| 185 | + return content |
| 186 | + |
| 187 | + |
| 188 | +def create_command_file(command_name: str, command_data: dict, all_commands: dict) -> str: |
| 189 | + """Create a complete command markdown file with frontmatter and content.""" |
| 190 | + # Get the full command name (handles container commands) |
| 191 | + full_command_name = get_full_command_name(command_name, command_data) |
| 192 | + |
| 193 | + # Generate the file path using the full command name |
| 194 | + filename = command_filename(full_command_name) |
| 195 | + filepath = f'content/commands/{filename}.md' |
| 196 | + |
| 197 | + # Ensure the directory exists |
| 198 | + os.makedirs(os.path.dirname(filepath), exist_ok=True) |
| 199 | + |
| 200 | + # Check if file already exists |
| 201 | + if os.path.exists(filepath): |
| 202 | + logging.warning(f"File {filepath} already exists, skipping...") |
| 203 | + return filepath |
| 204 | + |
| 205 | + # Generate frontmatter |
| 206 | + frontmatter_data = generate_command_frontmatter(command_name, command_data, all_commands) |
| 207 | + |
| 208 | + # Generate content |
| 209 | + content = generate_complete_markdown_content(command_name, command_data) |
| 210 | + |
| 211 | + # Create markdown object and set data |
| 212 | + md = Markdown(filepath) |
| 213 | + md.fm_data = frontmatter_data |
| 214 | + md.payload = content |
| 215 | + |
| 216 | + # Write the file |
| 217 | + md.persist() |
| 218 | + |
| 219 | + logging.info(f"Created command file: {filepath}") |
| 220 | + return filepath |
| 221 | + |
| 222 | + |
| 223 | +if __name__ == '__main__': |
| 224 | + args = parse_args() |
| 225 | + |
| 226 | + # Configure logging BEFORE creating objects |
| 227 | + log_level = getattr(logging, args.loglevel.upper()) |
| 228 | + logging.basicConfig( |
| 229 | + level=log_level, |
| 230 | + format='%(message)s %(filename)s:%(lineno)d - %(funcName)s', |
| 231 | + force=True # Force reconfiguration in case logging was already configured |
| 232 | + ) |
| 233 | + |
| 234 | + try: |
| 235 | + # Load and validate JSON data |
| 236 | + commands_data = load_and_validate_json(args.json_file) |
| 237 | + logging.info(f"Loaded {len(commands_data)} commands from {args.json_file}") |
| 238 | + |
| 239 | + # Process each command and generate markdown files |
| 240 | + created_files = [] |
| 241 | + for command_name in commands_data: |
| 242 | + try: |
| 243 | + logging.info(f"Processing command: {command_name}") |
| 244 | + filepath = create_command_file(command_name, commands_data[command_name], commands_data) |
| 245 | + created_files.append(filepath) |
| 246 | + except Exception as e: |
| 247 | + logging.error(f"Failed to create file for command '{command_name}': {e}") |
| 248 | + # Continue processing other commands |
| 249 | + continue |
| 250 | + |
| 251 | + # Summary |
| 252 | + logging.info(f"Successfully created {len(created_files)} command files:") |
| 253 | + for filepath in created_files: |
| 254 | + logging.info(f" - {filepath}") |
| 255 | + |
| 256 | + except (FileNotFoundError, ValueError) as e: |
| 257 | + logging.error(f"Error: {e}") |
| 258 | + sys.exit(1) |
| 259 | + except Exception as e: |
| 260 | + logging.error(f"Unexpected error: {e}") |
| 261 | + sys.exit(1) |
0 commit comments