samtranslator/internal/schema_source/schema.py (117 lines of code) (raw):
from __future__ import annotations
import argparse
import json
from copy import deepcopy
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Type, Union
from samtranslator.compat import pydantic
from samtranslator.internal.schema_source import (
any_cfn_resource,
aws_serverless_api,
aws_serverless_application,
aws_serverless_connector,
aws_serverless_function,
aws_serverless_graphqlapi,
aws_serverless_httpapi,
aws_serverless_layerversion,
aws_serverless_simpletable,
aws_serverless_statemachine,
)
from samtranslator.internal.schema_source.common import BaseModel, LenientBaseModel
class Globals(BaseModel):
Function: Optional[aws_serverless_function.Globals]
Api: Optional[aws_serverless_api.Globals]
HttpApi: Optional[aws_serverless_httpapi.Globals]
SimpleTable: Optional[aws_serverless_simpletable.Globals]
StateMachine: Optional[aws_serverless_statemachine.Globals]
LayerVersion: Optional[aws_serverless_layerversion.Globals]
Resources = Union[
aws_serverless_connector.Resource,
aws_serverless_function.Resource,
aws_serverless_simpletable.Resource,
aws_serverless_statemachine.Resource,
aws_serverless_layerversion.Resource,
aws_serverless_api.Resource,
aws_serverless_httpapi.Resource,
aws_serverless_application.Resource,
aws_serverless_graphqlapi.Resource,
]
class _ModelWithoutResources(LenientBaseModel):
Globals: Optional[Globals]
class SamModel(_ModelWithoutResources):
Resources: Dict[
str,
Union[
Resources,
# Ignore resources that are not AWS::Serverless::*
any_cfn_resource.Resource,
],
]
class Model(_ModelWithoutResources):
Resources: Dict[str, Resources]
def get_schema(model: Type[pydantic.BaseModel]) -> Dict[str, Any]:
obj = model.schema()
# http://json-schema.org/understanding-json-schema/reference/schema.html#schema
# https://github.com/pydantic/pydantic/issues/1478
# Validated in https://github.com/aws/serverless-application-model/blob/5c82f5d2ae95adabc9827398fba8ccfc3dbe101a/tests/schema/test_validate_schema.py#L91
obj["$schema"] = "http://json-schema.org/draft-04/schema#"
# Pydantic automatically adds title to model (https://github.com/pydantic/pydantic/issues/1051),
# and the YAML extension for VS Code then shows 'PassThroughProp' as title for pass-through
# properties (instead of the title of the property itself)... so manually deleting it.
del obj["definitions"]["PassThroughProp"]["title"]
return obj
def json_dumps(obj: Any) -> str:
return json.dumps(obj, indent=2, sort_keys=True) + "\n"
def _replace_in_dict(d: Dict[str, Any], keyword: str, replace: Callable[[Dict[str, Any]], Any]) -> Dict[str, Any]:
"""
Replace any dict containing keyword.
replace() takes the containing dict as input, and returns its replacement.
"""
if keyword in d:
d = replace(d)
for k, v in d.items():
if isinstance(v, dict):
d[k] = _replace_in_dict(v, keyword, replace)
return d
def _deep_get(d: Dict[str, Any], path: List[str]) -> Dict[str, Any]:
"""
Returns value at path defined by the keys in `path`.
"""
for k in path:
d = d[k]
return d
def _add_embedded_connectors(schema: Dict[str, Any]) -> None:
"""
Add embedded Connectors resource attribute to supported CloudFormation resources.
"""
# We get the definition from an existing SAM resource
embedded_connector = schema["definitions"][
"samtranslator__internal__schema_source__aws_serverless_function__Resource"
]["properties"]["Connectors"]
profiles = json.loads(Path("samtranslator/model/connector_profiles/profiles.json").read_text())
# Only add the resource attributes to resources that support it
source_resources = profiles["Permissions"].keys()
for resource in source_resources:
schema["definitions"][resource]["properties"]["Connectors"] = embedded_connector
def extend_with_cfn_schema(sam_schema: Dict[str, Any], cfn_schema: Dict[str, Any]) -> None:
"""
Add CloudFormation resources and template syntax to SAM schema.
"""
sam_defs = sam_schema["definitions"]
cfn_defs = cfn_schema["definitions"]
sam_props = sam_schema["properties"]
cfn_props = cfn_schema["properties"]
# Add Resources from CloudFormation schema to SAM schema
cfn_resources = cfn_props["Resources"]["patternProperties"]["^[a-zA-Z0-9]+$"]["anyOf"]
sam_props["Resources"]["additionalProperties"]["anyOf"].extend(cfn_resources)
# Add any other top-level properties from CloudFormation schema to SAM schema
for k in cfn_props:
if k not in sam_props:
sam_props[k] = cfn_props[k]
# Add definitions from CloudFormation schema to SAM schema
for k in cfn_defs:
if k in sam_defs:
raise Exception(f"Key {k} already in SAM schema definitions")
sam_defs[k] = cfn_defs[k]
_add_embedded_connectors(sam_schema)
# Inject CloudFormation documentation to SAM pass-through properties
def replace_passthrough(d: Dict[str, Any]) -> Dict[str, Any]:
passthrough = d["__samPassThrough"]
schema = deepcopy(_deep_get(cfn_schema, passthrough["schemaPath"]))
schema["markdownDescription"] = passthrough["markdownDescriptionOverride"]
schema["title"] = d["title"] # Still want the original title, CFN property name could be different
return schema
_replace_in_dict(
sam_schema,
"__samPassThrough",
replace_passthrough,
)
# The unified schema should include all supported properties
sam_schema["additionalProperties"] = False
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--cfn-schema", help="input CloudFormation schema", type=Path, required=True)
parser.add_argument("--sam-schema", help="output SAM schema", type=Path, required=True)
parser.add_argument("--unified-schema", help="output unified schema", type=Path, required=True)
args = parser.parse_args()
sam_schema = get_schema(SamModel)
args.sam_schema.write_text(json_dumps(sam_schema))
unified_schema = get_schema(Model)
cfn_schema = json.loads(args.cfn_schema.read_text())
extend_with_cfn_schema(unified_schema, cfn_schema)
args.unified_schema.write_text(json_dumps(unified_schema))
if __name__ == "__main__":
main()