diff --git a/Gems/AWSCore/Code/CMakeLists.txt b/Gems/AWSCore/Code/CMakeLists.txt index 7559f4720b..624f61d88a 100644 --- a/Gems/AWSCore/Code/CMakeLists.txt +++ b/Gems/AWSCore/Code/CMakeLists.txt @@ -154,10 +154,20 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AWSNativeSDKInit Gem::AWSCore.Static ) + ly_add_googletest( NAME Gem::AWSCore.Tests ) + ly_add_target_files( + TARGETS + AWSCore.Tests + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/Tools/ResourceMappingTool/resource_mapping_schema.json + OUTPUT_SUBDIRECTORY + Gems/AWSCore + ) + if (PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( NAME AWSCore.Editor.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} @@ -189,4 +199,13 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) endif() endif() +ly_add_target_files( + TARGETS + AWSCore + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/Tools/ResourceMappingTool/resource_mapping_schema.json + OUTPUT_SUBDIRECTORY + Gems/AWSCore +) + ly_install_directory(DIRECTORIES Tools/ResourceMappingTool) diff --git a/Gems/AWSCore/Code/Include/Private/ResourceMapping/AWSResourceMappingConstants.h b/Gems/AWSCore/Code/Include/Private/ResourceMapping/AWSResourceMappingConstants.h index a3d18c1ea1..e9d063eb5f 100644 --- a/Gems/AWSCore/Code/Include/Private/ResourceMapping/AWSResourceMappingConstants.h +++ b/Gems/AWSCore/Code/Include/Private/ResourceMapping/AWSResourceMappingConstants.h @@ -22,63 +22,6 @@ namespace AWSCore static constexpr const char ResourceMappingTypeKeyName[] = "Type"; static constexpr const char ResourceMappingVersionKeyName[] = "Version"; - // TODO: move this into an independent file under AWSCore gem, if resource mapping tool can reuse it - static constexpr const char ResourceMappingJsonSchema[] = - R"({ - "$schema": "http://json-schema.org/draft-04/schema", - "type": "object", - "title": "The AWS Resource Mapping Root schema", - "required": ["AWSResourceMappings", "AccountId", "Region", "Version"], - "properties": { - "AWSResourceMappings": { - "type": "object", - "title": "The AWSResourceMappings schema", - "patternProperties": { - "^.+$": { - "type": "object", - "title": "The AWS Resource Entry schema", - "required": ["Type", "Name/ID"], - "properties": { - "Type": { - "$ref": "#/NonEmptyString" - }, - "Name/ID": { - "$ref": "#/NonEmptyString" - }, - "AccountId": { - "$ref": "#/AccountIdString" - }, - "Region": { - "$ref": "#/RegionString" - } - } - } - }, - "additionalProperties": false - }, - "AccountId": { - "$ref": "#/AccountIdString" - }, - "Region": { - "$ref": "#/RegionString" - }, - "Version": { - "pattern": "^[0-9]{1}.[0-9]{1}.[0-9]{1}$" - } - }, - "AccountIdString": { - "type": "string", - "pattern": "^[0-9]{12}$|EMPTY|^$" - }, - "NonEmptyString": { - "type": "string", - "minLength": 1 - }, - "RegionString": { - "type": "string", - "pattern": "^[a-z]{2}-[a-z]{4,9}-[0-9]{1}$" - }, - "additionalProperties": false -})"; - + static constexpr const char ResourceMapppingJsonSchemaFilePath[] = + "Gems/AWSCore/resource_mapping_schema.json"; } // namespace AWSCore diff --git a/Gems/AWSCore/Code/Source/ResourceMapping/AWSResourceMappingManager.cpp b/Gems/AWSCore/Code/Source/ResourceMapping/AWSResourceMappingManager.cpp index faec07b19e..d6a3912523 100644 --- a/Gems/AWSCore/Code/Source/ResourceMapping/AWSResourceMappingManager.cpp +++ b/Gems/AWSCore/Code/Source/ResourceMapping/AWSResourceMappingManager.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -244,14 +245,16 @@ namespace AWSCore bool AWSResourceMappingManager::ValidateJsonDocumentAgainstSchema(const rapidjson::Document& jsonDocument) { - rapidjson::Document jsonSchemaDocument; - if (jsonSchemaDocument.Parse(ResourceMappingJsonSchema).HasParseError()) + AZ::IO::Path executablePath = AZ::IO::PathView(AZ::Utils::GetExecutableDirectory()); + AZ::IO::Path jsonSchemaPath = (executablePath / ResourceMapppingJsonSchemaFilePath).LexicallyNormal(); + AZ::Outcome readJsonOutcome = AZ::JsonSerializationUtils::ReadJsonFile(jsonSchemaPath.c_str()); + if (!readJsonOutcome.IsSuccess() || readJsonOutcome.TakeValue().ObjectEmpty()) { AZ_Error(AWSResourceMappingManagerName, false, ResourceMappingFileInvalidSchemaErrorMessage); return false; } - auto jsonSchema = rapidjson::SchemaDocument(jsonSchemaDocument); + auto jsonSchema = rapidjson::SchemaDocument(readJsonOutcome.TakeValue()); rapidjson::SchemaValidator validator(jsonSchema); if (!jsonDocument.Accept(validator)) diff --git a/Gems/AWSCore/Code/Tests/ResourceMapping/AWSResourceMappingManagerTest.cpp b/Gems/AWSCore/Code/Tests/ResourceMapping/AWSResourceMappingManagerTest.cpp index 81ea166848..fd37e6e228 100644 --- a/Gems/AWSCore/Code/Tests/ResourceMapping/AWSResourceMappingManagerTest.cpp +++ b/Gems/AWSCore/Code/Tests/ResourceMapping/AWSResourceMappingManagerTest.cpp @@ -121,7 +121,7 @@ public: void SetUp() override { - AWSCoreFixture::SetUp(); + AWSCoreFixture::SetUpFixture(false); m_normalizedSourceProjectFolder = AZStd::string::format("%s/%s%s/", AZ::Test::GetCurrentExecutablePath().c_str(), "AWSResourceMappingManager", AZ::Uuid::CreateRandom().ToString(false, false).c_str()); @@ -142,7 +142,7 @@ public: m_resourceMappingManager->DeactivateManager(); m_resourceMappingManager.reset(); - AWSCoreFixture::TearDown(); + AWSCoreFixture::TearDownFixture(false); } // AWSCoreInternalRequestBus interface implementation diff --git a/Gems/AWSCore/Code/Tests/TestFramework/AWSCoreFixture.h b/Gems/AWSCore/Code/Tests/TestFramework/AWSCoreFixture.h index 1d14484387..65e609b490 100644 --- a/Gems/AWSCore/Code/Tests/TestFramework/AWSCoreFixture.h +++ b/Gems/AWSCore/Code/Tests/TestFramework/AWSCoreFixture.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -111,6 +112,11 @@ public: ~AWSCoreFixture() override = default; void SetUp() override + { + SetUpFixture(); + } + + void SetUpFixture(bool mockSettingsRegistry = true) { AZ::AllocatorInstance::Create(); AZ::AllocatorInstance::Create(); @@ -120,14 +126,33 @@ public: AZ::IO::FileIOBase::SetInstance(nullptr); AZ::IO::FileIOBase::SetInstance(m_localFileIO); - m_settingsRegistry = AZStd::make_unique(); - AZ::SettingsRegistry::Register(m_settingsRegistry.get()); + if (mockSettingsRegistry) + { + m_settingsRegistry = AZStd::make_unique(); + AZ::SettingsRegistry::Register(m_settingsRegistry.get()); + } + else + { + m_app = AZStd::make_unique(); + } } void TearDown() override { - AZ::SettingsRegistry::Unregister(m_settingsRegistry.get()); - m_settingsRegistry.reset(); + TearDownFixture(); + } + + void TearDownFixture(bool mockSettingsRegistry = true) + { + if (mockSettingsRegistry) + { + AZ::SettingsRegistry::Unregister(m_settingsRegistry.get()); + m_settingsRegistry.reset(); + } + else + { + m_app.reset(); + } AZ::IO::FileIOBase::SetInstance(nullptr); @@ -173,4 +198,5 @@ private: protected: AZStd::unique_ptr m_settingsRegistry; + AZStd::unique_ptr m_app; }; diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_schema.json b/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_schema.json new file mode 100644 index 0000000000..21bc3334ce --- /dev/null +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "title": "O3DE AWS Resource mapping file schema", + "required": ["AWSResourceMappings", "AccountId", "Region", "Version"], + "properties": { + "AWSResourceMappings": { + "type": "object", + "title": "AWS resource mappings schema", + "patternProperties": { + "^.+$": { + "type": "object", + "title": "AWS resource entry schema", + "required": ["Type", "Name/ID"], + "properties": { + "Type": { + "$ref": "#/NonEmptyString" + }, + "Name/ID": { + "$ref": "#/NonEmptyString" + }, + "AccountId": { + "$ref": "#/AccountIdString" + }, + "Region": { + "$ref": "#/RegionString" + } + } + } + }, + "additionalProperties": false + }, + "AccountId": { + "$ref": "#/AccountIdString" + }, + "Region": { + "$ref": "#/RegionString" + }, + "Version": { + "pattern": "^[0-9]{1}.[0-9]{1}.[0-9]{1}$" + } + }, + "AccountIdString": { + "type": "string", + "pattern": "^[0-9]{12}$|EMPTY|^$" + }, + "NonEmptyString": { + "type": "string", + "minLength": 1 + }, + "RegionString": { + "type": "string", + "pattern": "^[a-z]{2}-[a-z]{4,9}-[0-9]{1}$" + }, + "additionalProperties": false +} diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py index 2351fd001b..7424854d2a 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py @@ -10,6 +10,7 @@ import logging import sys from utils import environment_utils +from utils import json_utils from utils import file_utils # arguments setup @@ -74,6 +75,15 @@ if __name__ == "__main__": except FileNotFoundError: logger.warning("Failed to load style sheet for resource mapping tool") + try: + schema_path: str = file_utils.join_path(file_utils.get_parent_directory_path(__file__), + 'resource_mapping_schema.json') + json_utils.load_resource_mapping_json_schema(schema_path) + except (FileNotFoundError, ValueError, KeyError) as e: + logger.error(f"Failed to load schema file {e}") + environment_utils.cleanup_qt_environment() + exit(-1) + logger.info("Initializing configuration manager ...") configuration_manager: ConfigurationManager = ConfigurationManager() configuration_error: bool = not configuration_manager.setup(arguments.profile, arguments.config_path) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_file_utils.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_file_utils.py index fe0f1754f3..e74771f1ef 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_file_utils.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_file_utils.py @@ -68,12 +68,65 @@ class TestFileUtils(TestCase): self._mock_path.cwd.assert_called_once() assert actual_path_name == TestFileUtils._expected_path_name - def test_get_parent_directory_path_return_expected_path_name(self) -> None: - self._mock_path.return_value.parent = TestFileUtils._expected_path_name + def test_get_parent_directory_path_return_empty_when_invalid_input(self) -> None: + mocked_path: MagicMock = self._mock_path.return_value + mocked_path.exists.return_value = False actual_path_name: str = file_utils.get_parent_directory_path("dummy") self._mock_path.assert_called_once() - assert actual_path_name == TestFileUtils._expected_path_name + assert actual_path_name == "" + + def test_get_parent_directory_path_return_empty_when_parent_invalid(self) -> None: + mocked_path: MagicMock = self._mock_path.return_value + mocked_path.exists.return_value = True + mocked_parent_path: MagicMock = MagicMock() + mocked_path.parent = mocked_parent_path + mocked_parent_path.exists.return_value = False + + actual_path_name: str = file_utils.get_parent_directory_path("dummy") + self._mock_path.assert_called() + assert actual_path_name == "" + + def test_get_parent_directory_path_return_expected_path_when_parent_valid(self) -> None: + mocked_path: MagicMock = self._mock_path.return_value + mocked_path.exists.return_value = True + mocked_parent_path: MagicMock = MagicMock() + mocked_path.parent = mocked_parent_path + mocked_parent_path.exists.return_value = True + mocked_parent_path.resolve.return_value = TestFileUtils._expected_file_name + + actual_path_name: str = file_utils.get_parent_directory_path("dummy") + self._mock_path.assert_called() + assert actual_path_name == TestFileUtils._expected_file_name + + def test_get_parent_directory_path_return_empty_when_level_two_parent_invalid(self) -> None: + mocked_path: MagicMock = self._mock_path.return_value + mocked_path.exists.return_value = True + mocked_parent_path1: MagicMock = MagicMock() + mocked_path.parent = mocked_parent_path1 + mocked_parent_path1.exists.return_value = True + mocked_parent_path2: MagicMock = MagicMock() + mocked_parent_path1.parent = mocked_parent_path2 + mocked_parent_path2.exists.return_value = False + + actual_path_name: str = file_utils.get_parent_directory_path("dummy", 2) + self._mock_path.assert_called() + assert actual_path_name == "" + + def test_get_parent_directory_path_return_expected_path_when_level_two_parent_valid(self) -> None: + mocked_path: MagicMock = self._mock_path.return_value + mocked_path.exists.return_value = True + mocked_parent_path1: MagicMock = MagicMock() + mocked_path.parent = mocked_parent_path1 + mocked_parent_path1.exists.return_value = True + mocked_parent_path2: MagicMock = MagicMock() + mocked_parent_path1.parent = mocked_parent_path2 + mocked_parent_path2.exists.return_value = True + mocked_parent_path2.resolve.return_value = TestFileUtils._expected_file_name + + actual_path_name: str = file_utils.get_parent_directory_path("dummy", 2) + self._mock_path.assert_called() + assert actual_path_name == TestFileUtils._expected_file_name def test_find_files_with_suffix_under_directory_return_expected_file_name(self) -> None: mocked_path: MagicMock = self._mock_path.return_value diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_json_utils.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_json_utils.py index 9fe99461ba..819bfb72fc 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_json_utils.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/utils/test_json_utils.py @@ -10,6 +10,7 @@ from typing import (Dict, List) from unittest import TestCase from unittest.mock import (MagicMock, mock_open, patch) +from utils import file_utils from utils import json_utils from model import constants from model.resource_mapping_attributes import (ResourceMappingAttributes, ResourceMappingAttributesBuilder, @@ -49,6 +50,10 @@ class TestJsonUtils(TestCase): } def setUp(self) -> None: + schema_path: str = file_utils.join_path(file_utils.get_parent_directory_path(__file__, 4), + 'resource_mapping_schema.json') + json_utils.load_resource_mapping_json_schema(schema_path) + self._mock_open = mock_open() open_patcher: patch = patch("utils.json_utils.open", self._mock_open) self.addCleanup(open_patcher.stop) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/file_utils.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/file_utils.py index 9e5dd9617b..9899c5dbb2 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/file_utils.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/file_utils.py @@ -33,8 +33,29 @@ def get_current_directory_path() -> str: return str(pathlib.Path.cwd()) -def get_parent_directory_path(file_path: str) -> str: - return pathlib.Path(file_path).parent +def get_parent_directory_path(file_path: str, level: int = 1) -> str: + """ + Get parent directory path based on requested file path + :param file_path: The requested file path + :param level: The level of parent directory, default value is 1 + :return The string value of parent directory path if exist; otherwise empty string + """ + if not pathlib.Path(file_path).exists(): + return "" + + result: pathlib.Path = pathlib.Path(file_path).parent + current_level: int = 1 + while current_level < level: + current_level += 1 + if result.exists(): + result = result.parent + else: + return "" + + if not result.exists(): + return "" + else: + return result.resolve() def find_files_with_suffix_under_directory(dir_path: str, suffix: str) -> List[str]: diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/json_utils.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/json_utils.py index 8c094a895c..3af9b9ff4a 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/json_utils.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/utils/json_utils.py @@ -19,18 +19,29 @@ Json Utils provide related functions to read/write/serialize/deserialize json fo """ logger = logging.getLogger(__name__) +# resource mapping json content constants _RESOURCE_MAPPING_JSON_KEY_NAME: str = "AWSResourceMappings" _RESOURCE_MAPPING_TYPE_JSON_KEY_NAME: str = "Type" _RESOURCE_MAPPING_NAMEID_JSON_KEY_NAME: str = "Name/ID" _RESOURCE_MAPPING_REGION_JSON_KEY_NAME: str = "Region" _RESOURCE_MAPPING_VERSION_JSON_KEY_NAME: str = "Version" _RESOURCE_MAPPING_JSON_FORMAT_VERSION: str = "1.1.0" - RESOURCE_MAPPING_ACCOUNTID_JSON_KEY_NAME: str = "AccountId" RESOURCE_MAPPING_ACCOUNTID_TEMPLATE_VALUE: str = "EMPTY" -_RESOURCE_MAPPING_ACCOUNTID_PATTERN: str = f"^[0-9]{{12}}$|{RESOURCE_MAPPING_ACCOUNTID_TEMPLATE_VALUE}|^$" -_RESOURCE_MAPPING_REGION_PATTERN: str = "^[a-z]{2}-[a-z]{4,9}-[0-9]{1}$" -_RESOURCE_MAPPING_VERSION_PATTERN: str = "^[0-9]{1}.[0-9]{1}.[0-9]{1}$" + +# resource mapping json schema constants +_RESOURCE_MAPPING_SCHEMA_ACCOUNTID_KEY_NAME: str = "AccountIdString" +_RESOURCE_MAPPING_SCHEMA_REGION_KEY_NAME: str = "RegionString" +_RESOURCE_MAPPING_SCHEMA_REQUIRED_PROPERTIES_KEY_NAME: str = "required" +_RESOURCE_MAPPING_SCHEMA_PROPERTIES_KEY_NAME: str = "properties" +_RESOURCE_MAPPING_SCHEMA_PATTERN_PROPERTIES_KEY_NAME: str = "patternProperties" +_RESOURCE_MAPPING_SCHEMA_PROPERTY_PATTERN_KEY_NAME: str = "pattern" +_RESOURCE_MAPPING_SCHEMA: Dict[str, any] = {} +_RESOURCE_MAPPING_SCHEMA_REQUIRED_ROOT_PROPERTIES: List[str] = [] +_RESOURCE_MAPPING_SCHEMA_REQUIRED_RESOURCE_PROPERTIES: List[str] = [] +_RESOURCE_MAPPING_SCHEMA_ACCOUNTID_PATTERN: str = "" +_RESOURCE_MAPPING_SCHEMA_REGION_PATTERN: str = "" +_RESOURCE_MAPPING_SCHEMA_VERSION_PATTERN: str = "" def _add_validation_error_message(errors: Dict[int, List[str]], row: int, error_message: str) -> None: if row in errors.keys(): @@ -119,6 +130,26 @@ def convert_json_dict_to_resources(json_dict: Dict[str, any]) -> List[ResourceMa return resources +def load_resource_mapping_json_schema(schema_path: str) -> None: + global _RESOURCE_MAPPING_SCHEMA, _RESOURCE_MAPPING_SCHEMA_REQUIRED_ROOT_PROPERTIES, _RESOURCE_MAPPING_SCHEMA_REQUIRED_RESOURCE_PROPERTIES,\ + _RESOURCE_MAPPING_SCHEMA_ACCOUNTID_PATTERN, _RESOURCE_MAPPING_SCHEMA_REGION_PATTERN, _RESOURCE_MAPPING_SCHEMA_VERSION_PATTERN + + if not _RESOURCE_MAPPING_SCHEMA: + # assume schema should be in correct format, and manually load expected pattern; tool will log error if schema is invalid + _RESOURCE_MAPPING_SCHEMA = read_from_json_file(schema_path) + _RESOURCE_MAPPING_SCHEMA_REQUIRED_ROOT_PROPERTIES = _RESOURCE_MAPPING_SCHEMA[_RESOURCE_MAPPING_SCHEMA_REQUIRED_PROPERTIES_KEY_NAME] + schema_properties: Dict[str, any] = _RESOURCE_MAPPING_SCHEMA[_RESOURCE_MAPPING_SCHEMA_PROPERTIES_KEY_NAME] + schema_pattern_properties: Dict[str, any] = \ + schema_properties[_RESOURCE_MAPPING_JSON_KEY_NAME][_RESOURCE_MAPPING_SCHEMA_PATTERN_PROPERTIES_KEY_NAME] + _RESOURCE_MAPPING_SCHEMA_REQUIRED_RESOURCE_PROPERTIES = list(schema_pattern_properties.values())[0][_RESOURCE_MAPPING_SCHEMA_REQUIRED_PROPERTIES_KEY_NAME] + _RESOURCE_MAPPING_SCHEMA_ACCOUNTID_PATTERN = \ + _RESOURCE_MAPPING_SCHEMA[_RESOURCE_MAPPING_SCHEMA_ACCOUNTID_KEY_NAME][_RESOURCE_MAPPING_SCHEMA_PROPERTY_PATTERN_KEY_NAME] + _RESOURCE_MAPPING_SCHEMA_REGION_PATTERN = \ + _RESOURCE_MAPPING_SCHEMA[_RESOURCE_MAPPING_SCHEMA_REGION_KEY_NAME][_RESOURCE_MAPPING_SCHEMA_PROPERTY_PATTERN_KEY_NAME] + _RESOURCE_MAPPING_SCHEMA_VERSION_PATTERN = \ + schema_properties[_RESOURCE_MAPPING_VERSION_JSON_KEY_NAME][_RESOURCE_MAPPING_SCHEMA_PROPERTY_PATTERN_KEY_NAME] + + def read_from_json_file(file_name: str) -> Dict[str, any]: try: json_dict: Dict[str, any] = {} @@ -166,20 +197,20 @@ def validate_resources_according_to_json_schema(resources: List[ResourceMappingA invalid_resources, row_count, error_messages.INVALID_FORMAT_DUPLICATED_KEY_ERROR_MESSAGE.format(resource.key_name)) - if not re.match(_RESOURCE_MAPPING_ACCOUNTID_PATTERN, resource.account_id): + if not re.match(_RESOURCE_MAPPING_SCHEMA_ACCOUNTID_PATTERN, resource.account_id): _add_validation_error_message( invalid_resources, row_count, error_messages.INVALID_FORMAT_UNEXPECTED_VALUE_IN_TABLE_ERROR_MESSAGE.format( resource.account_id, RESOURCE_MAPPING_ACCOUNTID_JSON_KEY_NAME, - _RESOURCE_MAPPING_ACCOUNTID_PATTERN)) + _RESOURCE_MAPPING_SCHEMA_ACCOUNTID_PATTERN)) - if not re.match(_RESOURCE_MAPPING_REGION_PATTERN, resource.region): + if not re.match(_RESOURCE_MAPPING_SCHEMA_REGION_PATTERN, resource.region): _add_validation_error_message( invalid_resources, row_count, error_messages.INVALID_FORMAT_UNEXPECTED_VALUE_IN_TABLE_ERROR_MESSAGE.format( resource.region, _RESOURCE_MAPPING_REGION_JSON_KEY_NAME, - _RESOURCE_MAPPING_REGION_PATTERN)) + _RESOURCE_MAPPING_SCHEMA_REGION_PATTERN)) row_count += 1 @@ -187,30 +218,32 @@ def validate_resources_according_to_json_schema(resources: List[ResourceMappingA def validate_json_dict_according_to_json_schema(json_dict: Dict[str, any]) -> None: - _validate_required_key_in_json_dict(json_dict, "root", _RESOURCE_MAPPING_VERSION_JSON_KEY_NAME) - _validate_required_key_in_json_dict(json_dict, "root", _RESOURCE_MAPPING_JSON_KEY_NAME) + # The reason we keep this manual json schema validation is python missing supportive feature in default libs + # When it is ready, we should be able to replace this with straightforward lib function call + root_property: str + for root_property in _RESOURCE_MAPPING_SCHEMA_REQUIRED_ROOT_PROPERTIES: + _validate_required_key_in_json_dict(json_dict, "root", root_property) - _validate_required_key_in_json_dict(json_dict, "root", RESOURCE_MAPPING_ACCOUNTID_JSON_KEY_NAME) - if not re.match(_RESOURCE_MAPPING_ACCOUNTID_PATTERN, json_dict[RESOURCE_MAPPING_ACCOUNTID_JSON_KEY_NAME]): + if not re.match(_RESOURCE_MAPPING_SCHEMA_ACCOUNTID_PATTERN, json_dict[RESOURCE_MAPPING_ACCOUNTID_JSON_KEY_NAME]): raise ValueError(error_messages.INVALID_FORMAT_UNEXPECTED_VALUE_IN_FILE_ERROR_MESSAGE.format( json_dict[RESOURCE_MAPPING_ACCOUNTID_JSON_KEY_NAME], f"root/{RESOURCE_MAPPING_ACCOUNTID_JSON_KEY_NAME}", - _RESOURCE_MAPPING_ACCOUNTID_PATTERN)) + _RESOURCE_MAPPING_SCHEMA_ACCOUNTID_PATTERN)) - _validate_required_key_in_json_dict(json_dict, "root", _RESOURCE_MAPPING_REGION_JSON_KEY_NAME) - if not re.match(_RESOURCE_MAPPING_REGION_PATTERN, json_dict[_RESOURCE_MAPPING_REGION_JSON_KEY_NAME]): + if not re.match(_RESOURCE_MAPPING_SCHEMA_REGION_PATTERN, json_dict[_RESOURCE_MAPPING_REGION_JSON_KEY_NAME]): raise ValueError(error_messages.INVALID_FORMAT_UNEXPECTED_VALUE_IN_FILE_ERROR_MESSAGE.format( json_dict[_RESOURCE_MAPPING_REGION_JSON_KEY_NAME], f"root/{_RESOURCE_MAPPING_REGION_JSON_KEY_NAME}", - _RESOURCE_MAPPING_REGION_PATTERN)) + _RESOURCE_MAPPING_SCHEMA_REGION_PATTERN)) json_resources: Dict[str, any] = json_dict[_RESOURCE_MAPPING_JSON_KEY_NAME] if json_resources: resource_key: str resource_value: Dict[str, str] for resource_key, resource_value in json_resources.items(): - _validate_required_key_in_json_dict(resource_value, resource_key, _RESOURCE_MAPPING_TYPE_JSON_KEY_NAME) - _validate_required_key_in_json_dict(resource_value, resource_key, _RESOURCE_MAPPING_NAMEID_JSON_KEY_NAME) + resource_property: str + for resource_property in _RESOURCE_MAPPING_SCHEMA_REQUIRED_RESOURCE_PROPERTIES: + _validate_required_key_in_json_dict(resource_value, resource_key, resource_property) def write_into_json_file(file_name: str, json_dict: Dict[str, any]) -> None: