|
3 | 3 | import yaml |
4 | 4 | import os |
5 | 5 | import re |
6 | | -import subprocess |
7 | 6 | from pathlib import Path |
| 7 | +from rich import print as rprint |
8 | 8 | from . import owasp |
9 | 9 | from .schema_handler import SchemaHandler |
10 | 10 | from . import pydantic_handler |
@@ -41,7 +41,7 @@ def _resolve_file_references(self, value): |
41 | 41 | else: |
42 | 42 | return yaml.safe_load(f) |
43 | 43 | except (IOError, yaml.YAMLError, json.JSONDecodeError) as e: |
44 | | - print(f"Warning: Could not read or parse file reference {abs_path}: {e}") |
| 44 | + rprint(f"[yellow]Warning: Could not read or parse file reference {abs_path}: {e}[/yellow]") |
45 | 45 | return value |
46 | 46 | return value |
47 | 47 |
|
@@ -164,12 +164,21 @@ def create_operation_object(self, documentation, func, http_event): |
164 | 164 | func_name = func['name'] |
165 | 165 | operation_id = f"{service_name}-{stage}-{func_name}" |
166 | 166 |
|
| 167 | + tags = documentation.get('tags', []) |
| 168 | + if not tags: |
| 169 | + handler_path = func.get('details', {}).get('handler', '') |
| 170 | + if handler_path: |
| 171 | + # e.g. src.api.users.handler -> users |
| 172 | + parts = handler_path.split('.') |
| 173 | + if len(parts) > 2: |
| 174 | + tags.append(parts[-2]) |
| 175 | + |
167 | 176 | obj = { |
168 | 177 | 'summary': documentation.get('summary', ''), |
169 | 178 | 'description': documentation.get('description', func['details'].get('description', '')), |
170 | 179 | 'operationId': operation_id, |
171 | 180 | 'parameters': [], |
172 | | - 'tags': documentation.get('tags', []), |
| 181 | + 'tags': tags, |
173 | 182 | } |
174 | 183 |
|
175 | 184 | if documentation.get('pathParams'): |
@@ -291,8 +300,6 @@ def create_media_type_object(self, models): |
291 | 300 | content = {} |
292 | 301 | if models: |
293 | 302 | for media_type, schema_name in models.items(): |
294 | | - print(f"Schema name: {schema_name}") |
295 | | - print(f"Models: {self.schema_handler.models}") |
296 | 303 | model_info = next((model for model in self.schema_handler.models if model['name'] == schema_name), None) |
297 | 304 | if model_info: |
298 | 305 | schema_ref = self.schema_handler.create_schema(schema_name, model_info.get('schema')) |
@@ -402,77 +409,94 @@ def create_response_headers(self, headers_doc): |
402 | 409 | headers[header_name] = header_obj |
403 | 410 | return headers |
404 | 411 |
|
405 | | -def main(): |
406 | | - parser = argparse.ArgumentParser(description='Generate OpenAPI v3 documentation from a serverless.yml file.') |
407 | | - parser.add_argument('output_file_path', type=str, help='Path to the output OpenAPI JSON file') |
408 | | - parser.add_argument('--serverless-yml-path', type=str, help='Path to the serverless.yml file. Required if --pydantic-source is not used.') |
409 | | - parser.add_argument('--openApiVersion', type=str, default='3.0.3', help='OpenAPI version to use') |
410 | | - parser.add_argument('--pre-hook', type=str, help='Path to a Python script to run before generation') |
411 | | - parser.add_argument('--pydantic-source', type=str, help='Path to the Pydantic models source directory') |
412 | | - parser.add_argument('--validate', action='store_true', help='Validate the generated OpenAPI spec') |
413 | | - args = parser.parse_args() |
414 | | - |
415 | | - if not args.serverless_yml_path and not args.pydantic_source: |
416 | | - parser.error("Either --serverless-yml-path or --pydantic-source must be provided.") |
417 | | - |
418 | | - # Execute the pre-hook script if provided |
419 | | - if args.pre_hook: |
420 | | - print(f"--- Running pre-hook script: {args.pre_hook} ---") |
421 | | - try: |
422 | | - subprocess.run(['python', args.pre_hook], check=True, text=True) |
423 | | - print("--- Pre-hook script finished successfully ---") |
424 | | - except FileNotFoundError: |
425 | | - print(f"Error: Pre-hook script not found at {args.pre_hook}") |
426 | | - return |
427 | | - except subprocess.CalledProcessError as e: |
428 | | - print(f"Error executing pre-hook script: {e}") |
429 | | - return |
430 | | - |
431 | | - # Execute the Pydantic schema generation if the source is provided |
432 | | - if args.pydantic_source: |
433 | | - print(f"--- Running Pydantic schema generation from: {args.pydantic_source} ---") |
434 | | - project_root = Path(args.pydantic_source) |
435 | | - output_dir = project_root / "openapi_models" |
436 | | - |
437 | | - generated_schemas = pydantic_handler.generate_dto_schemas(project_root, output_dir, project_root) |
438 | | - project_meta = pydantic_handler.load_project_meta(project_root) |
439 | | - |
440 | | - serverless_config = pydantic_handler.generate_serverless_config(generated_schemas, project_meta, project_root) |
441 | | - |
442 | | - # Use a virtual file path in the project root for correct base directory resolution |
443 | | - effective_sls_path = project_root / "serverless.yml" |
444 | | - |
445 | | - print("--- Pydantic schema generation finished successfully ---") |
446 | | - else: |
447 | | - try: |
448 | | - with open(args.serverless_yml_path, 'r') as f: |
449 | | - serverless_config = yaml.safe_load(f) |
450 | | - effective_sls_path = args.serverless_yml_path |
451 | | - except FileNotFoundError: |
452 | | - print(f"Error: The file {args.serverless_yml_path} was not found.") |
453 | | - return |
454 | | - except yaml.YAMLError as e: |
455 | | - print(f"Error parsing YAML file: {e}") |
456 | | - return |
| 412 | +def generate_schemas(args): |
| 413 | + rprint(f"[bold]--- Running Pydantic schema generation from: {args.pydantic_source} ---[/bold]") |
| 414 | + project_root = Path(args.pydantic_source) |
| 415 | + output_dir = Path(args.output_dir) |
| 416 | + |
| 417 | + pydantic_handler.generate_dto_schemas(project_root, output_dir, project_root) |
| 418 | + rprint(f"[bold green]--- Pydantic schema generation finished successfully. Schemas are in {args.output_dir} ---[/bold green]") |
| 419 | + |
| 420 | +def generate_serverless(args): |
| 421 | + rprint(f"[bold]--- Generating serverless.yml from schemas in {args.schema_dir} ---[/bold]") |
| 422 | + schema_dir = Path(args.schema_dir) |
| 423 | + project_root = Path(args.project_dir) if args.project_dir else schema_dir.parent |
| 424 | + |
| 425 | + schemas = [] |
| 426 | + for file_path in schema_dir.glob("*.json"): |
| 427 | + with open(file_path, 'r') as f: |
| 428 | + schema = json.load(f) |
| 429 | + schemas.append({ |
| 430 | + "name": file_path.stem, |
| 431 | + "schema": schema |
| 432 | + }) |
| 433 | + |
| 434 | + project_meta = pydantic_handler.load_project_meta(project_root) |
| 435 | + serverless_config = pydantic_handler.generate_serverless_config(schemas, project_meta, project_root) |
| 436 | + |
| 437 | + output_path = project_root / "serverless.yml" |
| 438 | + with open(output_path, 'w') as f: |
| 439 | + yaml.dump(serverless_config, f) |
| 440 | + |
| 441 | + rprint(f"[bold green]--- serverless.yml generated at {output_path} ---[/bold green]") |
| 442 | + |
| 443 | +def generate_spec(args): |
| 444 | + try: |
| 445 | + with open(args.serverless_yml_path, 'r') as f: |
| 446 | + serverless_config = yaml.safe_load(f) |
| 447 | + effective_sls_path = args.serverless_yml_path |
| 448 | + except FileNotFoundError: |
| 449 | + rprint(f"[red]Error: The file {args.serverless_yml_path} was not found.[/red]") |
| 450 | + return |
| 451 | + except yaml.YAMLError as e: |
| 452 | + rprint(f"[red]Error parsing YAML file: {e}[/red]") |
| 453 | + return |
457 | 454 |
|
458 | 455 | generator = DefinitionGenerator(serverless_config, str(effective_sls_path), args.openApiVersion) |
459 | 456 | open_api_spec = generator.generate() |
460 | 457 |
|
461 | 458 | try: |
462 | 459 | with open(args.output_file_path, 'w') as f: |
463 | 460 | json.dump(open_api_spec, f, indent=2) |
464 | | - print(f"OpenAPI specification successfully written to {args.output_file_path}") |
| 461 | + rprint(f"[bold green]OpenAPI specification successfully written to {args.output_file_path}[/bold green]") |
465 | 462 | except IOError as e: |
466 | | - print(f"Error writing to output file: {e}") |
| 463 | + rprint(f"[red]Error writing to output file: {e}[/red]") |
467 | 464 |
|
468 | 465 | if args.validate: |
469 | 466 | from openapi_spec_validator import validate |
470 | | - print("Validating generated spec...") |
| 467 | + rprint("[bold]Validating generated spec...[/bold]") |
471 | 468 | try: |
472 | 469 | validate(open_api_spec) |
473 | | - print("Validation successful.") |
| 470 | + rprint("[bold green]Validation successful.[/bold green]") |
474 | 471 | except Exception as e: |
475 | | - print(f"Validation failed: {e}") |
| 472 | + rprint(f"[bold red]Validation failed: {e}[/bold red]") |
| 473 | + |
| 474 | +def main(): |
| 475 | + parser = argparse.ArgumentParser(description='Generate OpenAPI v3 documentation from a serverless.yml file.') |
| 476 | + subparsers = parser.add_subparsers(dest='command', required=True) |
| 477 | + |
| 478 | + # Sub-parser for generating schemas |
| 479 | + parser_schemas = subparsers.add_parser('generate-schemas', help='Generate JSON schemas from Pydantic models.') |
| 480 | + parser_schemas.add_argument('--pydantic-source', type=str, required=True, help='Path to the Pydantic models source directory') |
| 481 | + parser_schemas.add_argument('--output-dir', type=str, required=True, help='Directory to save the generated JSON schemas') |
| 482 | + parser_schemas.set_defaults(func=generate_schemas) |
| 483 | + |
| 484 | + # Sub-parser for generating serverless.yml |
| 485 | + parser_serverless = subparsers.add_parser('generate-serverless', help='Generate serverless.yml from JSON schemas.') |
| 486 | + parser_serverless.add_argument('--schema-dir', type=str, required=True, help='Directory containing the JSON schemas') |
| 487 | + parser_serverless.add_argument('--project-dir', type=str, help='Path to the project root directory (for pyproject.toml)') |
| 488 | + parser_serverless.set_defaults(func=generate_serverless) |
| 489 | + |
| 490 | + # Sub-parser for generating the OpenAPI spec |
| 491 | + parser_spec = subparsers.add_parser('generate-spec', help='Generate the full OpenAPI spec from a serverless.yml file.') |
| 492 | + parser_spec.add_argument('output_file_path', type=str, help='Path to the output OpenAPI JSON file') |
| 493 | + parser_spec.add_argument('--serverless-yml-path', type=str, required=True, help='Path to the serverless.yml file.') |
| 494 | + parser_spec.add_argument('--openApiVersion', type=str, default='3.0.3', help='OpenAPI version to use') |
| 495 | + parser_spec.add_argument('--validate', action='store_true', help='Validate the generated OpenAPI spec') |
| 496 | + parser_spec.set_defaults(func=generate_spec) |
| 497 | + |
| 498 | + args = parser.parse_args() |
| 499 | + args.func(args) |
476 | 500 |
|
477 | 501 | if __name__ == '__main__': |
478 | 502 | main() |
0 commit comments