Skip to content
Merged
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
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ smoke-test:

lint:
# Linter performs static analysis to catch latent bugs
ruff samcli
ruff samcli schema
# mypy performs type check
mypy --exclude /testdata/ --exclude /init/templates/ --no-incremental setup.py samcli tests
mypy --exclude /testdata/ --exclude /init/templates/ --no-incremental setup.py samcli tests schema

# Command to run everytime you make changes to verify everything works
dev: lint test

black:
black setup.py samcli tests
black setup.py samcli tests schema

black-check:
black --check setup.py samcli tests
black --check setup.py samcli tests schema

format: black
ruff samcli --fix
Expand Down
6 changes: 3 additions & 3 deletions samcli/lib/config/samconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def get_all(self, cmd_names, section, env=DEFAULT_ENV):
self._read()
if isinstance(self.document, dict):
toml_content = self.document.get(env, {})
params = toml_content.get(self._to_key(cmd_names), {}).get(section, {})
params = toml_content.get(self.to_key(cmd_names), {}).get(section, {})
if DEFAULT_GLOBAL_CMDNAME in toml_content:
global_params = toml_content.get(DEFAULT_GLOBAL_CMDNAME, {}).get(section, {})
global_params.update(params.copy())
Expand Down Expand Up @@ -119,7 +119,7 @@ def put(self, cmd_names, section, key, value, env=DEFAULT_ENV):
# Empty document prepare the initial structure.
# self.document is a nested dict, we need to check each layer and add new tables, otherwise duplicated key
# in parent layer will override the whole child layer
cmd_name_key = self._to_key(cmd_names)
cmd_name_key = self.to_key(cmd_names)
env_content = self.document.get(env, {})
cmd_content = env_content.get(cmd_name_key, {})
param_content = cmd_content.get(section, {})
Expand Down Expand Up @@ -267,6 +267,6 @@ def _version_sanity_check(version: Any) -> None:
raise SamConfigVersionException(f"'{VERSION_KEY}' key is not present or is in unrecognized format. ")

@staticmethod
def _to_key(cmd_names: Iterable[str]) -> str:
def to_key(cmd_names: Iterable[str]) -> str:
# construct a parsed name that is of the format: a_b_c_d
return "_".join([cmd.replace("-", "_").replace(" ", "_") for cmd in cmd_names])
105 changes: 105 additions & 0 deletions schema/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Handles JSON schema generation logic"""


import importlib
from typing import Dict, List, Optional, Union

import click

from samcli.cli.command import _SAM_CLI_COMMAND_PACKAGES
from samcli.lib.config.samconfig import SamConfig


def format_param(param: click.core.Option) -> Dict[str, Union[Optional[str], List[str]]]:
"""Format a click Option parameter to a dictionary object.

A parameter object should contain the following information that will be
necessary for including in the JSON schema:
* name - The name of the parameter
* help - The parameter's description (may vary between commands)
* type - The data type accepted by the parameter
* type.choices - If there are only a certain number of options allowed,
a list of those allowed options
* default - The default option for that parameter
"""
formatted_param: Dict[str, Union[Optional[str], List[str]]] = {"name": param.name, "help": param.help}

# NOTE: Params do not have explicit "string" type; either "text" or "path".
# All choice options are from a set of strings.
if param.type.name in ["text", "path", "choice"]:
formatted_param["type"] = "string"
else:
formatted_param["type"] = param.type.name

if param.default:
formatted_param["default"] = str(param.default)

if param.type.name == "choice" and isinstance(param.type, click.Choice):
formatted_param["choices"] = list(param.type.choices)

return formatted_param


def get_params_from_command(cli, main_command_name: str = "") -> Dict[str, dict]:
"""Given a command CLI, return it in a dictionary, pointing to its parameters as dictionary objects."""
params = [
param
for param in cli.params
if param.name and isinstance(param, click.core.Option) # exclude None and non-Options
]
cmd_name = SamConfig.to_key([main_command_name, cli.name]) if main_command_name else cli.name
return {cmd_name: {param.name: format_param(param) for param in params}}


def retrieve_command_structure(package_name: str) -> Dict[str, dict]:
"""Given a SAM CLI package name, retrieve its structure.

Parameters
----------
package_name: str
The name of the command package to retrieve.

Returns
-------
dict
A dictionary that maps the name of the command to its relevant click options.
"""
module = importlib.import_module(package_name)
command = {}

if isinstance(module.cli, click.core.Group): # command has subcommands (e.g. local invoke)
for subcommand in module.cli.commands.values():
command.update(
get_params_from_command(
subcommand,
module.__name__.split(".")[-1], # if Group CLI, get last section of module name for cmd name
)
)
else:
command.update(get_params_from_command(module.cli))
return command


def generate_schema() -> dict:
"""Generate a JSON schema for all SAM CLI commands.

Returns
-------
dict
A dictionary representation of the JSON schema.
"""
schema: dict = {}
commands = {}
params = set() # NOTE(leogama): Currently unused due to some params having different help values
# TODO: Populate schema with relevant attributes
for package_name in _SAM_CLI_COMMAND_PACKAGES:
new_command = retrieve_command_structure(package_name)
commands.update(new_command)
for param_list in new_command.values():
command_params = [param for param in param_list]
params.update(command_params)
return schema


if __name__ == "__main__":
generate_schema()