From 95b5666ea02487ad1f1d61474a856c81b6e4960d Mon Sep 17 00:00:00 2001 From: Anshika Garg Date: Fri, 19 Feb 2021 16:52:18 -0800 Subject: [PATCH] Add Regex and OR support for transform (#687) * Add Regex and OR support for transform * Update regex, unit test and --- src/rpdk/core/contract/resource_client.py | 73 ++++++++++++++----- .../templates/transformation.template | 2 +- tests/contract/test_resource_client.py | 53 ++++++++++++-- 3 files changed, 103 insertions(+), 25 deletions(-) diff --git a/src/rpdk/core/contract/resource_client.py b/src/rpdk/core/contract/resource_client.py index e76eb631..9676dc17 100644 --- a/src/rpdk/core/contract/resource_client.py +++ b/src/rpdk/core/contract/resource_client.py @@ -215,28 +215,65 @@ def get_metadata(self): and properties[prop]["insertionOrder"] == "false" } - def transform_model(self, input_model, output_model): + def transform_model(self, input_model, output_model): # pylint: disable=R0914 if self.property_transform_keys: self.check_npm() - for prop in self.property_transform_keys: - document_input, _path_input, _parent_input = traverse( - input_model, list(prop)[1:] - ) - document_output, _path_output, _parent_output = traverse( - output_model, list(prop)[1:] - ) - if document_input != document_output: - transformed_property = self.transform(prop, input_model) - self.update_transformed_property( - prop, transformed_property, input_model + for prop in self.property_transform_keys: # pylint: disable=R1702 + try: + document_input, _path_input, _parent_input = traverse( + input_model, list(prop)[1:] + ) + document_output, _path_output, _parent_output = traverse( + output_model, list(prop)[1:] ) + if not self.compare(document_input, document_output): + path = "/" + "/".join(prop) + property_transform_value = self.property_transform[path].replace( + '"', '\\"' + ) + if "$OR" in property_transform_value: + transform_functions = property_transform_value.split("$OR") + for transform_function in transform_functions: + transformed_property = self.transform( + transform_function, input_model + ) + value = self.convert_type( + document_input, transformed_property + ) + if self.compare(value, document_output): + self.update_transformed_property( + prop, value, input_model + ) + else: + transformed_property = self.transform( + property_transform_value, input_model + ) + value = self.convert_type(document_input, transformed_property) + self.update_transformed_property(prop, value, input_model) + + except KeyError: + pass - def transform(self, property_path, input_model): + @staticmethod + def convert_type(document_input, transformed_property): + if isinstance(document_input, bool): + return bool(transformed_property) + if isinstance(document_input, int): + return int(transformed_property) + if isinstance(document_input, float): + return float(transformed_property) + return transformed_property - path = "/" + "/".join(property_path) - property_transform_value = json.dumps(self.property_transform[path]) - # self.property_transform[path].replace('"', '\\"') + @staticmethod + def compare(document_input, document_output): + try: + if isinstance(document_input, str): + return re.match(f"^{document_input}$", document_output) + return document_input == document_output + except re.error: + return document_input == document_output + def transform(self, property_transform_value, input_model): LOG.warning("This is the transform %s", property_transform_value) content = self.transformation_template.render( input_model=input_model, jsonata_expression=property_transform_value @@ -458,7 +495,7 @@ def make_request( creds, token, callback_context=None, - **kwargs + **kwargs, ): return { "requestData": { @@ -534,7 +571,7 @@ def _make_payload(self, action, current_model, previous_model=None, **kwargs): self._session, LOWER_CAMEL_CRED_KEYS, self._role_arn ), self.generate_token(), - **kwargs + **kwargs, ) def _call(self, payload): diff --git a/src/rpdk/core/contract/templates/transformation.template b/src/rpdk/core/contract/templates/transformation.template index 586b3038..84bc1f71 100644 --- a/src/rpdk/core/contract/templates/transformation.template +++ b/src/rpdk/core/contract/templates/transformation.template @@ -1,5 +1,5 @@ var jsonata = require('jsonata') var model = {{input_model}} -var expression = jsonata({{jsonata_expression}}); +var expression = jsonata("{{jsonata_expression}}"); var result = expression.evaluate(model); console.log(result); diff --git a/tests/contract/test_resource_client.py b/tests/contract/test_resource_client.py index 3fb2da20..ecc98bd2 100644 --- a/tests/contract/test_resource_client.py +++ b/tests/contract/test_resource_client.py @@ -85,15 +85,21 @@ "b": {"type": "number"}, "c": {"type": "number"}, "d": {"type": "number"}, + "e": {"type": "string"}, }, "readOnlyProperties": ["/properties/b"], "createOnlyProperties": ["/properties/c"], - "primaryIdentifier": ["/properties/c"], + "primaryIdentifier": ["/properties/b"], "writeOnlyProperties": ["/properties/d"], - "propertyTransform": {"/properties/a": '$join([a, "Test"])'}, + "propertyTransform": { + "/properties/a": '$join([a, "Test"])', + "/properties/c": "$power(2, 2)", + "/properties/e": '$join([e, "Test"]) $OR $join([e, "Value"])', + }, } -TRANSFORM_OUTPUT = {"a": "ValueATest", "c": 1} +TRANSFORM_OUTPUT = {"a": "ValueATest", "c": 4} +TRANSFORM_OUTPUT_WITHOUT_C = {"a": "ValueATest", "c": 1} INPUT = {"a": "ValueA", "c": 1} INVALID_OUTPUT = {"a": "ValueB", "c": 1} @@ -834,7 +840,11 @@ def test_call_async(resource_client, action): ) mock_client.invoke.side_effect = [ - {"Payload": StringIO('{"status": "IN_PROGRESS", "resourceModel": {"c": 3} }')}, + { + "Payload": StringIO( + '{"status": "IN_PROGRESS", "resourceModel": {"c": 3, "b": 1} }' + ) + }, {"Payload": StringIO('{"status": "SUCCESS"}')}, ] @@ -1238,7 +1248,27 @@ def test_transform_model_equal_output(resource_client): output_model = TRANSFORM_OUTPUT.copy() resource_client.transform_model(input_model, output_model) - assert input_model == TRANSFORM_OUTPUT + assert input_model == output_model + + +@pytest.mark.parametrize( + "output", + [{"e": "newValue", "a": "ValueA", "c": 1}, {"e": "newTest", "a": "ValueA", "c": 1}], +) +def test_transform_model_equal_output_OR(resource_client, output): + input_model = {"e": "new", "a": "ValueA", "c": 1} + + resource_client.transform_model(input_model, output) + assert input_model == output + + +def test_transform_model_not_equal_output_OR(resource_client): + input_model = {"e": "new", "a": "ValueA", "c": 1} + output_model = {"e": "not-newValue", "a": "ValueA", "c": 4} + + resource_client.transform_model(input_model, output_model) + assert input_model != output_model + assert input_model == {"a": "ValueA", "c": 4, "e": "new"} def test_transform_model_unequal_models(resource_client): @@ -1247,7 +1277,7 @@ def test_transform_model_unequal_models(resource_client): resource_client.transform_model(input_model, output_model) assert input_model != output_model - assert input_model == TRANSFORM_OUTPUT + assert input_model == TRANSFORM_OUTPUT_WITHOUT_C def test_non_transform_model_not_equal(resource_client_inputs_schema): @@ -1256,3 +1286,14 @@ def test_non_transform_model_not_equal(resource_client_inputs_schema): resource_client_inputs_schema.transform_model(input_model, output_model) assert input_model != output_model + + +def test_compare_raise_exception(resource_client): + assert not resource_client.compare("he(lo", "hello") + + +@pytest.mark.parametrize("value", [1, 2.3, True, "test"]) +def test_convert_type(resource_client, value): + converted_value = resource_client.convert_type(value, str(value)) + assert isinstance(converted_value, type(value)) + assert converted_value == value