From 2d7a41aa531f5564fe711ba4c79769ac03f2c91b Mon Sep 17 00:00:00 2001 From: Stig Ofstad Date: Wed, 1 Nov 2023 09:42:41 +0100 Subject: [PATCH] fix: don't validate primitive attributes of type any --- src/common/entity/validators.py | 14 +++--- src/domain_classes/dimension.py | 6 ++- src/enums.py | 6 ++- .../entity/validate_default_entity.feature | 37 ++++++++++++++ .../unit/common/tree/test_tree_node_update.py | 50 +++++++++++++++++++ 5 files changed, 105 insertions(+), 8 deletions(-) diff --git a/src/common/entity/validators.py b/src/common/entity/validators.py index 0fe6ccfec..d544163d2 100644 --- a/src/common/entity/validators.py +++ b/src/common/entity/validators.py @@ -37,6 +37,8 @@ def is_blueprint_instance_of( def _validate_primitive_attribute(attribute: BlueprintAttribute, value: bool | int | float | str, key: str): + if attribute.attribute_type == BuiltinDataTypes.ANY.value: + return # If type is "any", no need to validate further python_type = BuiltinDataTypes(attribute.attribute_type).to_py_type() if attribute.attribute_type == "number" and isinstance(value, int): # float is considered a superset containing int return @@ -143,12 +145,6 @@ def _validate_entity( len(attributeDefinition.dimensions.dimensions) if attributeDefinition.dimensions else 0, implementation_mode, ) - elif attributeDefinition.is_primitive: - _validate_primitive_attribute( - attributeDefinition, - entity[attributeDefinition.name], - f"{key}.{attributeDefinition.name}", - ) elif attributeDefinition.attribute_type == "any" and attributeDefinition.name == "default": default_attribute_definition = BlueprintAttribute( name=attributeDefinition.name, @@ -178,6 +174,12 @@ def _validate_entity( f"{key}.{default_attribute_definition.name}", "extend", ) + elif attributeDefinition.is_primitive: + _validate_primitive_attribute( + attributeDefinition, + entity[attributeDefinition.name], + f"{key}.{attributeDefinition.name}", + ) else: _validate_complex_attribute( attributeDefinition, diff --git a/src/domain_classes/dimension.py b/src/domain_classes/dimension.py index 00d3962b1..6ce82ad7d 100644 --- a/src/domain_classes/dimension.py +++ b/src/domain_classes/dimension.py @@ -1,4 +1,6 @@ # Convert dmt attribute_types to python types. If complex, return type as string. +from typing import Any + from enums import BuiltinDataTypes @@ -27,7 +29,9 @@ def __init__(self, dimensions: str, attribute_type: str): a string value is stored. """ self.dimensions: list[str] = dimensions.split(",") - self.type: type[bool] | type[int] | type[float] | type[str] | str = _get_data_type_from_dmt_type(attribute_type) + self.type: type[bool] | type[int] | type[float] | type[str] | str | Any = _get_data_type_from_dmt_type( + attribute_type + ) self.value = None def is_array(self) -> bool: diff --git a/src/enums.py b/src/enums.py index b4c436572..e4bf759e5 100644 --- a/src/enums.py +++ b/src/enums.py @@ -1,6 +1,7 @@ from enum import Enum +from typing import Any -PRIMITIVES = {"string", "number", "integer", "boolean"} +PRIMITIVES = {"string", "number", "integer", "boolean", "any"} class Protocols(Enum): @@ -14,6 +15,7 @@ class BuiltinDataTypes(Enum): BOOL = "boolean" OBJECT = "object" # Any complex type (i.e. any blueprint type) BINARY = "binary" + ANY = "any" def to_py_type(self): if self is BuiltinDataTypes.BOOL: @@ -26,6 +28,8 @@ def to_py_type(self): return str elif self is BuiltinDataTypes.OBJECT: return dict + elif self is BuiltinDataTypes.ANY: + return Any class RepositoryType(Enum): diff --git a/src/tests/bdd/entity/validate_default_entity.feature b/src/tests/bdd/entity/validate_default_entity.feature index 0756c6b38..9ec3e227e 100644 --- a/src/tests/bdd/entity/validate_default_entity.feature +++ b/src/tests/bdd/entity/validate_default_entity.feature @@ -36,6 +36,11 @@ Feature: Validate Default Entity "address": "$AnimalBlueprint", "type": "dmss://system/SIMOS/Reference", "referenceType": "storage" + }, + { + "address": "$SomeBlueprint", + "type": "dmss://system/SIMOS/Reference", + "referenceType": "storage" } ] } @@ -189,6 +194,24 @@ Feature: Validate Default Entity } """ + Given there exist document with id "SomeBlueprint" in data source "data-source-name" + """ + { + "name": "SomeBlueprint", + "type": "dmss://system/SIMOS/Blueprint", + "attributes": [ + { + "name": "pets", + "type": "dmss://system/SIMOS/BlueprintAttribute", + "attributeType": "number", + "label": "Number of pets", + "optional": false, + "default": "five" + } + ] + } + """ + Scenario: Validate existing simple example Given i access the resource url "/api/entity/validate-existing-entity/data-source-name/root_package/PersonBlueprint" @@ -207,4 +230,18 @@ Feature: Validate Default Entity "debug": "Location: Entity in key '^.attributes.0.default'", "data": null } + """ + + Given i access the resource url "/api/entity/validate-existing-entity/data-source-name/root_package/SomeBlueprint" + When i make a "POST" request + Then the response status should be "Bad Request" + And the response should contain + """ + { + "status": 400, + "type": "ValidationException", + "message": "Attribute 'default' should be type 'float'. Got 'str'. Value: five", + "debug": "Location: Entity in key '^.attributes.0.default'", + "data": null + } """ \ No newline at end of file diff --git a/src/tests/unit/common/tree/test_tree_node_update.py b/src/tests/unit/common/tree/test_tree_node_update.py index 7455c5e07..5a14291f4 100644 --- a/src/tests/unit/common/tree/test_tree_node_update.py +++ b/src/tests/unit/common/tree/test_tree_node_update.py @@ -6,6 +6,7 @@ from common.address import Address from common.exceptions import BadRequestException, ValidationException from common.tree.tree_node import Node +from common.tree.tree_node_serializer import tree_node_from_dict from enums import REFERENCE_TYPES, SIMOS from features.document.use_cases.add_document_use_case import add_document_use_case from features.document.use_cases.update_document_use_case import ( @@ -22,6 +23,9 @@ def setUp(self) -> None: simos_blueprints = [ "dmss://system/SIMOS/NamedEntity", "dmss://system/SIMOS/Reference", + "dmss://system/SIMOS/Action", + "dmss://system/SIMOS/Blueprint", + "dmss://system/SIMOS/BlueprintAttribute", ] mock_blueprint_folder = "src/tests/unit/common/tree/mock_data/mock_blueprints" mock_blueprints_and_file_names = { @@ -163,6 +167,52 @@ def test_add_optional_nested(self): self.assertDictEqual(self.doc_storage["1"], entity_after) + def test_update_blueprint_attribute_default_with_int(self): + blueprint = self.mock_blueprint_provider.get_blueprint("dmss://system/SIMOS/Action").to_dict() + blueprint_node = tree_node_from_dict(blueprint, self.mock_blueprint_provider.get_blueprint) + attribute_node = blueprint_node.get_by_path(["attributes", "7"]) + + new_value = { + "type": "dmss://system/SIMOS/BlueprintAttribute", + "name": "method", + "description": "What method to call inside runnable.tsx", + "attributeType": "number", + "default": 67, + } + attribute_node.update(new_value) + self.assertEqual(attribute_node.entity["default"], 67) + + def test_update_blueprint_attribute_default_with_dict(self): + blueprint = self.mock_blueprint_provider.get_blueprint("dmss://system/SIMOS/Action").to_dict() + blueprint_node = tree_node_from_dict(blueprint, self.mock_blueprint_provider.get_blueprint) + attribute_node = blueprint_node.get_by_path(["attributes", "7"]) + + new_value = { + "type": "dmss://system/SIMOS/BlueprintAttribute", + "name": "method", + "description": "What method to call inside runnable.tsx", + # There is no check if default value matches 'attributeType' in node.update(). It should happen before... + "attributeType": "number", + "default": {"type": "test"}, + } + attribute_node.update(new_value) + self.assertDictEqual(attribute_node.entity["default"], {"type": "test"}) + + def test_update_blueprint_attribute_default_with_bool(self): + blueprint = self.mock_blueprint_provider.get_blueprint("dmss://system/SIMOS/Action").to_dict() + blueprint_node = tree_node_from_dict(blueprint, self.mock_blueprint_provider.get_blueprint) + attribute_node = blueprint_node.get_by_path(["attributes", "7"]) + + new_value = { + "type": "dmss://system/SIMOS/BlueprintAttribute", + "name": "method", + "description": "What method to call inside runnable.tsx", + "attributeType": "number", + "default": True, + } + attribute_node.update(new_value) + self.assertEqual(attribute_node.entity["default"], True) + def test_add_duplicate(self): self.doc_storage = { "1": {