diff --git a/src/rpdk/core/project.py b/src/rpdk/core/project.py index 5b5c1b4e..1649bdd8 100644 --- a/src/rpdk/core/project.py +++ b/src/rpdk/core/project.py @@ -959,6 +959,22 @@ def __set_property_type(prop_type, single_type=True): lambda markdown_value: f"List of {markdown_value}" ) # noqa: E731 + if "items" not in prop.keys(): + LOG.warning( + 'Warning: found schema property of array type with no "items" key; \n' + 'defaulting data types for array items to "Map" in generated docs.\n' + 'If "Map" is not what you need instead, specify the expected data type \n' + "for array items such as (example with items of string type):\n" + ' "ExampleProperty" : {\n' + ' "description" : "Example description.",\n' + ' "type": "array",\n' + ' "items": {\n' + ' "type": "string"\n' + " }\n" + " }\n" + ) + prop["items"] = {} + # potential circular ref # setting up markdown before going deep in the heap to reuse markdown if "$ref" in prop["items"]: diff --git a/tests/data/schema/hook/valid/valid_hook_configuration_with_array_items.json b/tests/data/schema/hook/valid/valid_hook_configuration_with_array_items.json new file mode 100644 index 00000000..8750a455 --- /dev/null +++ b/tests/data/schema/hook/valid/valid_hook_configuration_with_array_items.json @@ -0,0 +1,40 @@ +{ + "typeName": "TestOnly::Sample::Hook", + "description": "Example hook schema for unit tests.", + "sourceUrl": "https://github.com/aws-cloudformation/SAMPLE-URL-SUFFIX", + "documentationUrl": "https://github.com/aws-cloudformation/SAMPLE-URL-SUFFIX/blob/main/README.md", + "typeConfiguration": { + "properties": { + "exampleArrayProperty": { + "type": "array", + "description": "Example property of array type with items of string type.", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "required": [], + "handlers": { + "preCreate": { + "targetNames": [ + "AWS::S3::Bucket" + ], + "permissions": [] + }, + "preUpdate": { + "targetNames": [ + "AWS::S3::Bucket" + ], + "permissions": [] + }, + "preDelete": { + "targetNames": [ + "AWS::S3::Bucket" + ], + "permissions": [] + } + }, + "additionalProperties": false +} diff --git a/tests/data/schema/hook/valid/valid_hook_configuration_without_array_items.json b/tests/data/schema/hook/valid/valid_hook_configuration_without_array_items.json new file mode 100644 index 00000000..bf614ecf --- /dev/null +++ b/tests/data/schema/hook/valid/valid_hook_configuration_without_array_items.json @@ -0,0 +1,37 @@ +{ + "typeName": "TestOnly::Sample::Hook", + "description": "Example hook schema for unit tests.", + "sourceUrl": "https://github.com/aws-cloudformation/SAMPLE-URL-SUFFIX", + "documentationUrl": "https://github.com/aws-cloudformation/SAMPLE-URL-SUFFIX/blob/main/README.md", + "typeConfiguration": { + "properties": { + "exampleArrayProperty": { + "type": "array", + "description": "Example property of array type without items (that is, an 'items` key at this same level)." + } + }, + "additionalProperties": false + }, + "required": [], + "handlers": { + "preCreate": { + "targetNames": [ + "AWS::S3::Bucket" + ], + "permissions": [] + }, + "preUpdate": { + "targetNames": [ + "AWS::S3::Bucket" + ], + "permissions": [] + }, + "preDelete": { + "targetNames": [ + "AWS::S3::Bucket" + ], + "permissions": [] + } + }, + "additionalProperties": false +} diff --git a/tests/test_project.py b/tests/test_project.py index d9337ca5..dbbb5eda 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -601,6 +601,122 @@ def test_generate_docs_with_multiref_property(project, tmp_path_factory): assert read_me_stripped == read_me_target_stripped +def test_when_array_property_has_items_then_generated_docs_should_use_specified_items_type( + project, tmp_path_factory, session +): + project.artifact_type = ARTIFACT_TYPE_HOOK + project.schema = resource_json( + __name__, + "data/schema/hook/valid/valid_hook_configuration_with_array_items.json", + ) + project.type_name = "TestOnly::Sample::Hook" + # tmpdir conflicts with other tests, make a unique one + project.root = tmp_path_factory.mktemp( + "generate_docs_when_array_property_has_items" + ) + + project.load_configuration_schema() + + mock_plugin = MagicMock(spec=["generate"]) + patch_session = patch("rpdk.core.boto_helpers.Boto3Session") + + def get_test_schema(): + return { + "typeName": "AWS::S3::Bucket", + "description": "test schema", + "properties": {"foo": {"type": "string"}}, + "primaryIdentifier": ["/properties/foo"], + "additionalProperties": False, + } + + mock_cfn_client = MagicMock(spec=["describe_type"]) + with patch.object(project, "_plugin", mock_plugin), patch_session as mock_session: + mock_cfn_client.describe_type.return_value = { + "Schema": json.dumps(get_test_schema()), + "Type": "", + "ProvisioningType": "", + } + session.client.side_effect = [mock_cfn_client, MagicMock()] + mock_session.return_value = session + project.generate() + project.generate_docs() + mock_plugin.generate.assert_called_once_with(project) + + docs_dir = project.root / "docs" + readme_file = project.root / "docs" / "README.md" + + assert docs_dir.is_dir() + assert readme_file.is_file() + with patch.object(project, "_plugin", mock_plugin), patch_session as mock_session: + session.client.side_effect = [mock_cfn_client, MagicMock()] + mock_session.return_value = session + project.generate() + readme_contents = readme_file.read_text(encoding="utf-8").strip().replace("\n", " ") + assert project.type_name in readme_contents + assert ( + "exampleArrayProperty Example property of array type with items of string type. _Required_: No _Type_: List of String" + in readme_contents + ) + + +def test_when_array_property_has_no_items_then_generated_docs_should_default_to_map_items_type( + project, tmp_path_factory, session +): + project.artifact_type = ARTIFACT_TYPE_HOOK + project.schema = resource_json( + __name__, + "data/schema/hook/valid/valid_hook_configuration_without_array_items.json", + ) + project.type_name = "TestOnly::Sample::Hook" + # tmpdir conflicts with other tests, make a unique one + project.root = tmp_path_factory.mktemp( + "generate_docs_when_array_property_has_no_items" + ) + + project.load_configuration_schema() + + mock_plugin = MagicMock(spec=["generate"]) + patch_session = patch("rpdk.core.boto_helpers.Boto3Session") + + def get_test_schema(): + return { + "typeName": "AWS::S3::Bucket", + "description": "test schema", + "properties": {"foo": {"type": "string"}}, + "primaryIdentifier": ["/properties/foo"], + "additionalProperties": False, + } + + mock_cfn_client = MagicMock(spec=["describe_type"]) + with patch.object(project, "_plugin", mock_plugin), patch_session as mock_session: + mock_cfn_client.describe_type.return_value = { + "Schema": json.dumps(get_test_schema()), + "Type": "", + "ProvisioningType": "", + } + session.client.side_effect = [mock_cfn_client, MagicMock()] + mock_session.return_value = session + project.generate() + project.generate_docs() + mock_plugin.generate.assert_called_once_with(project) + + docs_dir = project.root / "docs" + readme_file = project.root / "docs" / "README.md" + + assert docs_dir.is_dir() + assert readme_file.is_file() + with patch.object(project, "_plugin", mock_plugin), patch_session as mock_session: + session.client.side_effect = [mock_cfn_client, MagicMock()] + mock_session.return_value = session + project.generate() + readme_contents = readme_file.read_text(encoding="utf-8").strip().replace("\n", " ") + assert project.type_name in readme_contents + assert ( + "exampleArrayProperty Example property of array type without items (that is, an 'items` key at this same level). _Required_: No _Type_: List of Map" + in readme_contents + ) + + def test_generate_with_docs_invalid_property_type(project, tmp_path_factory): project.schema = resource_json( __name__, "data/schema/invalid/invalid_property_type_invalid.json"