Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions samcli/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"samcli.commands.validate.validate",
"samcli.commands.build",
"samcli.commands.local.local",
"samcli.commands.generate.generate",
"samcli.commands.package",
"samcli.commands.deploy",
"samcli.commands.delete",
Expand Down Expand Up @@ -170,6 +171,10 @@ def format_commands(self, ctx: click.Context, formatter: RootCommandHelpTextForm
name="validate",
text=SAM_CLI_COMMANDS.get("validate", ""),
),
RowDefinition(
name="generate",
text=SAM_CLI_COMMANDS.get("generate", ""),
),
RowDefinition(
name="sync",
text=SAM_CLI_COMMANDS.get("sync", ""),
Expand Down
1 change: 1 addition & 0 deletions samcli/cli/root/command_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"validate": "Validate an AWS SAM template.",
"build": "Build your AWS serverless function code.",
"local": "Run your AWS serverless function locally.",
"generate": "Generate artifacts from SAM templates.",
"remote": "Invoke or send an event to cloud resources in your AWS Cloudformation stack.",
"package": "Package an AWS SAM application.",
"deploy": "Deploy an AWS SAM application.",
Expand Down
3 changes: 3 additions & 0 deletions samcli/commands/generate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Generate Command Group
"""
29 changes: 29 additions & 0 deletions samcli/commands/generate/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
Main CLI group for 'generate' commands
"""

import click

from samcli.cli.main import common_options, pass_context, print_cmdline_args
from samcli.commands._utils.command_exception_handler import command_exception_handler
from samcli.commands.generate.openapi.command import cli as openapi_cli


@click.group()
@common_options
@pass_context
@print_cmdline_args
@command_exception_handler
def cli(ctx):
"""
Generate artifacts from SAM templates.

This command group provides subcommands to generate various artifacts
from your SAM templates, such as OpenAPI specifications, CloudFormation
templates, and more.
"""
pass


# Add openapi subcommand
cli.add_command(openapi_cli)
3 changes: 3 additions & 0 deletions samcli/commands/generate/openapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
OpenAPI Generation Command
"""
124 changes: 124 additions & 0 deletions samcli/commands/generate/openapi/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
CLI command for "generate openapi" command
"""

import click

from samcli.cli.cli_config_file import ConfigProvider, configuration_option
from samcli.cli.main import aws_creds_options, common_options, pass_context, print_cmdline_args
from samcli.commands._utils.command_exception_handler import command_exception_handler
from samcli.commands._utils.options import parameter_override_option, template_click_option
from samcli.lib.telemetry.metric import track_command
from samcli.lib.utils.version_checker import check_newer_version

SHORT_HELP = "Generate OpenAPI specification from SAM template."

DESCRIPTION = """
Generate an OpenAPI (Swagger) specification document from a SAM template.

SAM automatically generates OpenAPI documents for your APIs at deploy time.
This command allows you to access that generated OpenAPI document as part of
your build process, enabling integration with tools like swagger-codegen,
OpenAPI Generator, and other API documentation/client generation tools.
"""


@click.command(
"openapi",
short_help=SHORT_HELP,
help=DESCRIPTION,
context_settings={"max_content_width": 120},
)
@configuration_option(provider=ConfigProvider(section="parameters"))
@template_click_option(include_build=False)
@click.option(
"--api-logical-id",
required=False,
type=str,
help="Logical ID of the API resource to generate OpenAPI for. "
"Required when template contains multiple APIs. "
"Defaults to auto-detection when only one API exists.",
)
@click.option(
"--output-file",
"-o",
required=False,
type=click.Path(),
help="Path to output file for generated OpenAPI document. " "If not specified, outputs to stdout.",
)
@click.option(
"--format",
type=click.Choice(["yaml", "json"], case_sensitive=False),
default="yaml",
help="Output format for the OpenAPI document.",
show_default=True,
)
@click.option(
"--openapi-version",
type=click.Choice(["2.0", "3.0"], case_sensitive=False),
default="3.0",
help="OpenAPI specification version (2.0 = Swagger, 3.0 = OpenAPI).",
show_default=True,
)
@parameter_override_option
@common_options
@aws_creds_options
@pass_context
@track_command
@check_newer_version
@print_cmdline_args
@command_exception_handler
def cli(
ctx,
template_file,
api_logical_id,
output_file,
format,
openapi_version,
parameter_overrides,
config_file,
config_env,
):
"""
`sam generate openapi` command entry point
"""
# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing

do_cli(
template_file=template_file,
api_logical_id=api_logical_id,
output_file=output_file,
output_format=format,
openapi_version=openapi_version,
parameter_overrides=parameter_overrides,
region=ctx.region,
profile=ctx.profile,
) # pragma: no cover


def do_cli(
template_file,
api_logical_id,
output_file,
output_format,
openapi_version,
parameter_overrides,
region,
profile,
):
"""
Implementation of the ``cli`` method
"""
from samcli.commands.generate.openapi.context import OpenApiContext

with OpenApiContext(
template_file=template_file,
api_logical_id=api_logical_id,
output_file=output_file,
output_format=output_format,
openapi_version=openapi_version,
parameter_overrides=parameter_overrides,
region=region,
profile=profile,
) as context:
context.run()
178 changes: 178 additions & 0 deletions samcli/commands/generate/openapi/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""
Context for OpenAPI generation command execution
"""

import json
import logging
from typing import Dict, Optional, cast

import click

from samcli.commands.generate.openapi.exceptions import GenerateOpenApiException
from samcli.lib.generate.openapi_generator import OpenApiGenerator
from samcli.yamlhelper import yaml_dump

LOG = logging.getLogger(__name__)


class OpenApiContext:
"""
Context manager for OpenAPI generation command
"""

MSG_SUCCESS = "\nSuccessfully generated OpenAPI document{output_info}.\n"

def __init__(
self,
template_file: str,
api_logical_id: Optional[str],
output_file: Optional[str],
output_format: str,
openapi_version: str,
parameter_overrides: Optional[Dict],
region: Optional[str],
profile: Optional[str],
):
"""
Initialize OpenAPI generation context

Parameters
----------
template_file : str
Path to SAM template
api_logical_id : str, optional
API resource logical ID
output_file : str, optional
Output file path (None for stdout)
output_format : str
Output format: 'yaml' or 'json'
openapi_version : str
OpenAPI version: '2.0' or '3.0'
parameter_overrides : dict, optional
Template parameter overrides
region : str, optional
AWS region
profile : str, optional
AWS profile
"""
self.template_file = template_file
self.api_logical_id = api_logical_id
self.output_file = output_file
self.output_format = output_format
self.openapi_version = openapi_version
self.parameter_overrides = parameter_overrides
self.region = region
self.profile = profile

def __enter__(self):
return self

def __exit__(self, *args):
pass

def run(self):
"""
Execute OpenAPI generation

Raises
------
GenerateOpenApiException
If generation fails
"""
try:
LOG.debug(
"Generating OpenAPI - Template: %s, API ID: %s, Format: %s",
self.template_file,
self.api_logical_id or "auto-detect",
self.output_format,
)

# Create generator
generator = OpenApiGenerator(
template_file=self.template_file,
api_logical_id=self.api_logical_id,
parameter_overrides=self.parameter_overrides,
region=self.region,
profile=self.profile,
)

# Generate OpenAPI document
openapi_doc = generator.generate()

# Convert to OpenAPI 3.0 if requested
if self.openapi_version == "3.0":
from samcli.lib.generate.openapi_converter import OpenApiConverter

openapi_doc = OpenApiConverter.swagger_to_openapi3(openapi_doc)

# Format output
output_str = self._format_output(openapi_doc)

# Write output
self._write_output(output_str)

# Display success message
self._display_success()

except GenerateOpenApiException:
# Re-raise our specific exceptions
raise
except Exception as e:
# Wrap unexpected exceptions
raise GenerateOpenApiException(f"Unexpected error during OpenAPI generation: {str(e)}") from e

def _format_output(self, openapi_doc: Dict) -> str:
"""
Format OpenAPI document as YAML or JSON

Parameters
----------
openapi_doc : dict
OpenAPI document

Returns
-------
str
Formatted output string
"""
if self.output_format == "json":
return json.dumps(openapi_doc, indent=2, ensure_ascii=False)
else:
# Default to YAML
return cast(str, yaml_dump(openapi_doc))

def _write_output(self, content: str):
"""
Write output to file or stdout

Parameters
----------
content : str
Content to write
"""
if self.output_file:
# Write to file
try:
with open(self.output_file, "w") as f:
f.write(content)
LOG.debug("Wrote OpenAPI document to file: %s", self.output_file)
except IOError as e:
raise GenerateOpenApiException(f"Failed to write to file '{self.output_file}': {str(e)}") from e
else:
# Write to stdout
click.echo(content)

def _display_success(self):
"""
Display success message to user
"""
if self.output_file:
output_info = f" and wrote to file: {self.output_file}"
else:
output_info = ""

msg = self.MSG_SUCCESS.format(output_info=output_info)
if self.output_file:
# Only show success message if writing to file
# (to not clutter stdout when piping)
click.secho(msg, fg="green")
Loading