From 471cfedb455227f3018276c00201fbf6f8bc825d Mon Sep 17 00:00:00 2001 From: Libor Bucek Date: Tue, 19 Sep 2023 11:09:08 +0200 Subject: [PATCH 1/3] tree-wide: add support for ABAP DDIC Data Element CRUD --- doc/commands.md | 7 +- doc/commands/dataelement.md | 26 ++ sap/adt/__init__.py | 1 + sap/adt/dataelement.py | 186 ++++++++++ sap/cli/__init__.py | 2 + sap/cli/dataelement.py | 117 ++++++ test/unit/fixtures_adt_dataelement.py | 187 ++++++++++ test/unit/test_sap_adt_dataelement.py | 40 +++ test/unit/test_sap_cli_dataelement.py | 496 ++++++++++++++++++++++++++ 9 files changed, 1059 insertions(+), 3 deletions(-) create mode 100644 doc/commands/dataelement.md create mode 100644 sap/adt/dataelement.py create mode 100644 sap/cli/dataelement.py create mode 100644 test/unit/fixtures_adt_dataelement.py create mode 100644 test/unit/test_sap_adt_dataelement.py create mode 100644 test/unit/test_sap_cli_dataelement.py diff --git a/doc/commands.md b/doc/commands.md index e7e4d5f2..13a6b742 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -20,6 +20,7 @@ 18. [flp](commands/flp.md) - Fiori Launchpad 19. [rap](commands/businessservice.md) - RAP Business Services 20. [strust](commands/strust.md) - SSL Certificates -21. [structure](commands/structure.md) - ABAP DDIC structures -22. [table](commands/table.md) - ABAP DDIC transparent tables -23. [badi](commands/badi.md) - New style (Enhancements) BAdI operations +21. [dataelement](commands/dataelement.md) - ABAP DDIC Data Elements +22. [structure](commands/structure.md) - ABAP DDIC structures +23. [table](commands/table.md) - ABAP DDIC transparent tables +24. [badi](commands/badi.md) - New style (Enhancements) BAdI operations \ No newline at end of file diff --git a/doc/commands/dataelement.md b/doc/commands/dataelement.md new file mode 100644 index 00000000..e0228025 --- /dev/null +++ b/doc/commands/dataelement.md @@ -0,0 +1,26 @@ +# Data Element + +- [Data Element](#data-element) + - [define](#define) + +## define + +Define an ABAP DDIC Data Element. + +```bash +sapcli dataelement define DATA_ELEMENT_NAME --type=domain|predefinedAbapType [--corrnr TRANSPORT] [--activate] [--no-error-existing] [--domain_name] [--data_type] [--data_type_length] [--data_type_decimals] [--label_short] [--label_medium] [--label_long] [--label_heading] +``` + +* _DATA\_ELEMENT\_NAME_ specifying the name of the data element +* _--type [domain|predefinedAbapType]_ type kind +* _--domain\_name_ domain name (e.g. BUKRS) [default = ''] - mandatory in case the _--type_=domain **(optional)** +* _--data\_type_ data type (e.g. CHAR) [default = ''] - mandatory in case the _--type_=predefinedAbapType **(optional)** +* _--data\_type\_length_ data type length (e.g. 5) [default = '0'] **(optional)** +* _--data\_type\_decimals_ data type decimals (e.g. 3) [default = '0'] **(optional)** +* _--label\_short_ short label [default = ''] **(optional)** +* _--label\_medium_ medium label [default = ''] **(optional)** +* _--label\_long_ long label [default = ''] **(optional)** +* _--label\_heading_ heading label [default = ''] **(optional)** +* _--corrnr TRANSPORT_ specifies CTS Transport Request Number **(optional)** +* _--activate_ activate after finishing the data element modification **(optional)** +* _--no-error-existing_ do not fail if data element already exists **(optional)** diff --git a/sap/adt/__init__.py b/sap/adt/__init__.py index c173f07a..785e63e9 100644 --- a/sap/adt/__init__.py +++ b/sap/adt/__init__.py @@ -14,3 +14,4 @@ from sap.adt.table import Table # noqa: F401 from sap.adt.enhancement_implementation import EnhancementImplementation # noqa: F401 from sap.adt.structure import Structure # noqa: F401 +from sap.adt.dataelement import DataElement # noqa: F401 diff --git a/sap/adt/dataelement.py b/sap/adt/dataelement.py new file mode 100644 index 00000000..6028372a --- /dev/null +++ b/sap/adt/dataelement.py @@ -0,0 +1,186 @@ +"""ABAP Data Element ADT functionality module""" + +from sap.adt.objects import ADTObject, ADTObjectType, ADTCoreData, ADTObjectSourceEditor +# pylint: disable=unused-import +from sap.adt.annotations import OrderedClassMembers, xml_element, xml_text_node_property +from sap.adt.objects import XMLNamespace, XMLNS_ADTCORE + +XMLNS_DTEL = XMLNamespace('dtel', 'http://www.sap.com/adt/dictionary/dataelements') + +LABELS_LENGTH = { + 'short': '10', + 'medium': '20', + 'long': '40', + 'heading': '55' +} + +VALIDATION_ISSUE_KEYS = { + 'none': 'none', + 'domain_name_not_defined': 'domain_name_not_defined', + 'data_type_not_defined': 'data_type_not_defined' +} + + +class ADTDataElementData(ADTCoreData): + """Data Element nodes data. + """ + + # pylint: disable=too-many-instance-attributes + # pylint: disable=too-few-public-methods + class DataElement(metaclass=OrderedClassMembers): + """ADT Data Element data collector""" + + type = xml_text_node_property('dtel:typeKind') + type_name = xml_text_node_property('dtel:typeName') + data_type = xml_text_node_property('dtel:dataType') + data_type_length = xml_text_node_property('dtel:dataTypeLength') + data_type_decimals = xml_text_node_property('dtel:dataTypeDecimals') + label_short = xml_text_node_property('dtel:shortFieldLabel') + label_short_length = xml_text_node_property('dtel:shortFieldLength') + label_short_max_length = xml_text_node_property('dtel:shortFieldMaxLength') + label_medium = xml_text_node_property('dtel:mediumFieldLabel') + label_medium_length = xml_text_node_property('dtel:mediumFieldLength') + label_medium_max_length = xml_text_node_property('dtel:mediumFieldMaxLength') + label_long = xml_text_node_property('dtel:longFieldLabel') + label_long_length = xml_text_node_property('dtel:longFieldLength') + label_long_max_length = xml_text_node_property('dtel:longFieldMaxLength') + label_heading = xml_text_node_property('dtel:headingFieldLabel') + label_heading_length = xml_text_node_property('dtel:headingFieldLength') + label_heading_max_length = xml_text_node_property('dtel:headingFieldMaxLength') + search_help = xml_text_node_property('dtel:searchHelp') + search_help_parameter = xml_text_node_property('dtel:searchHelpParameter') + set_get_parameter = xml_text_node_property('dtel:setGetParameter') + default_component_name = xml_text_node_property('dtel:defaultComponentName') + deactivate_input_history = xml_text_node_property('dtel:deactivateInputHistory') + change_document = xml_text_node_property('dtel:changeDocument') + left_to_right_direction = xml_text_node_property('dtel:leftToRightDirection') + deactivate_bidi_filtering = xml_text_node_property('dtel:deactivateBIDIFiltering') + + # pylint: disable=too-many-arguments + def __init__(self, package=None, description=None, language=None, + master_language=None, master_system=None, responsible=None, + package_reference=None, abap_language_version=None): + super().__init__(package, description, language, + master_language, master_system, responsible, + package_reference, abap_language_version) + + self._data_element = ADTDataElementData.DataElement() + + @property + def data_element(self): + """The Data Element's reference""" + + return self._data_element + + +class DataElement(ADTObject): + """ABAP Data Element""" + + OBJTYPE = ADTObjectType( + 'DTEL/DE', + 'ddic/dataelements', + XMLNamespace('blue', 'http://www.sap.com/wbobj/dictionary/dtel', parents=[XMLNS_ADTCORE, XMLNS_DTEL]), + 'application/vnd.sap.adt.dataelements.v2+xml', + { + 'application/vnd.sap.adt.dataelements.v2+xml': '', + 'application/vnd.sap.adt.dataelements.v1+xml': '' + }, + 'wbobj', + editor_factory=ADTObjectSourceEditor + ) + + def __init__(self, connection, name, package=None, metadata=None): + super().__init__(connection, name, metadata, active_status='inactive') + + self._metadata = ADTDataElementData( + metadata.package, metadata.description, + metadata.language, metadata.master_language, + metadata.master_system, metadata.responsible, + metadata.package_reference.name if metadata.package_reference is not None else None, + metadata.abap_language_version + ) if metadata is not None else ADTDataElementData() + + self._metadata.package_reference.name = package + + @xml_element('dtel:dataElement') + def data_element(self): + """The Data Element's reference""" + + return self._metadata.data_element + + def set_type(self, value): + """Setter for Type Kind element""" + + self._metadata.data_element.type = value + + def set_type_name(self, value): + """Setter for Type Name element""" + + self._metadata.data_element.type_name = value.upper() if value is not None else None + + def set_data_type(self, value): + """Setter for Data Type element""" + + self._metadata.data_element.data_type = value.upper() if value is not None else None + + def set_data_type_length(self, value): + """Setter for Data Type Length element""" + + self._metadata.data_element.data_type_length = value + + def set_data_type_decimals(self, value): + """Setter for Data Type Decimals element""" + + self._metadata.data_element.data_type_decimals = value + + def set_label_short(self, value): + """Setter for Label Short element""" + + self._metadata.data_element.label_short = value + + def set_label_medium(self, value): + """Setter for Label Medium element""" + + self._metadata.data_element.label_medium = value + + def set_label_long(self, value): + """Setter for Label Long element""" + + self._metadata.data_element.label_long = value + + def set_label_heading(self, value): + """Setter for Label Heading element""" + + self._metadata.data_element.label_heading = value + + def normalize(self): + """Validate Data Element setup before save""" + de = self._metadata.data_element + + if de.type == 'domain': + de.data_type = '' + de.data_type_length = '0' + de.data_type_decimals = '0' + if de.type == 'predefinedAbapType': + de.type_name = '' + + de.label_short_length = de.label_short_length if not de.label_short_length else LABELS_LENGTH['short'] + de.label_medium_length = de.label_medium_length if not de.label_medium_length else LABELS_LENGTH['medium'] + de.label_long_length = de.label_long_length if not de.label_long_length else LABELS_LENGTH['long'] + de.label_heading_length = de.label_heading_length if not de.label_heading_length else LABELS_LENGTH['heading'] + + de.deactivate_input_history = de.deactivate_input_history if de.deactivate_input_history is not None else False + de.change_document = de.change_document if de.change_document is not None else False + de.left_to_right_direction = de.left_to_right_direction if de.left_to_right_direction is not None else False + # pylint: disable=line-too-long + de.deactivate_bidi_filtering = de.deactivate_bidi_filtering if de.deactivate_bidi_filtering is not None else False + + def validate(self): + """Validate Data Element setup""" + + if self._metadata.data_element.type == 'domain' and not self._metadata.data_element.type_name: + return VALIDATION_ISSUE_KEYS['domain_name_not_defined'] + if self._metadata.data_element.type == 'predefinedAbapType' and not self._metadata.data_element.data_type: + return VALIDATION_ISSUE_KEYS['data_type_not_defined'] + + return VALIDATION_ISSUE_KEYS['none'] diff --git a/sap/cli/__init__.py b/sap/cli/__init__.py index 09d71495..62c8a3ef 100644 --- a/sap/cli/__init__.py +++ b/sap/cli/__init__.py @@ -47,6 +47,7 @@ def commands(): import sap.cli.table import sap.cli.badi import sap.cli.structure + import sap.cli.dataelement if CommandsCache.adt is None: CommandsCache.adt = [ @@ -69,6 +70,7 @@ def commands(): (adt_connection_from_args, sap.cli.rap.CommandGroup()), (adt_connection_from_args, sap.cli.table.CommandGroup()), (adt_connection_from_args, sap.cli.structure.CommandGroup()), + (adt_connection_from_args, sap.cli.dataelement.CommandGroup()), (adt_connection_from_args, sap.cli.checkin.CommandGroup()), (adt_connection_from_args, sap.cli.badi.CommandGroup()), ] diff --git a/sap/cli/dataelement.py b/sap/cli/dataelement.py new file mode 100644 index 00000000..cb26b76d --- /dev/null +++ b/sap/cli/dataelement.py @@ -0,0 +1,117 @@ +"""ADT proxy for ABAP DDIC Data Element""" + +import sap.adt +from sap.adt.dataelement import VALIDATION_ISSUE_KEYS +from sap.adt.errors import ExceptionResourceAlreadyExists +import sap.cli.object +import sap.cli.wb +import sap.cli.core + + +class CommandGroup(sap.cli.object.CommandGroupObjectMaster): + """Adapter converting command line parameters to sap.adt.DataElement methods + calls. + """ + + def __init__(self): + super().__init__('dataelement') + + self.define() + + def instance(self, connection, name, args, metadata=None): + package = None + if hasattr(args, 'package'): + package = args.package + + return sap.adt.DataElement(connection, name.upper(), package=package, metadata=metadata) + + +@CommandGroup.argument_corrnr() +@CommandGroup.argument('--no-error-existing', action='store_true', default=False, + help='Do not fail if data element already exists') +@CommandGroup.argument('-a', '--activate', action='store_true', default=False, help='Activate after modification') +@CommandGroup.argument('-t', '--type', required=True, choices=['domain', 'predefinedAbapType'], + type=str, help='Type kind') +@CommandGroup.argument('-d', '--domain_name', default=None, type=str, help='Domain name') +@CommandGroup.argument('-dt', '--data_type', default=None, type=str, help='Data type') +@CommandGroup.argument('-dtl', '--data_type_length', default='0', type=str, help='Data type length') +@CommandGroup.argument('-dtd', '--data_type_decimals', default='0', type=str, help='Data type decimals') +@CommandGroup.argument('-ls', '--label_short', default='', type=str, help='Short label') +@CommandGroup.argument('-lm', '--label_medium', default='', type=str, help='Medium label') +@CommandGroup.argument('-ll', '--label_long', default='', type=str, help='Long label') +@CommandGroup.argument('-lh', '--label_heading', default='', type=str, help='Heading label') +@CommandGroup.argument('package', help='Package assignment') +@CommandGroup.argument('description', help='Data element description') +@CommandGroup.argument('name', help='Data element name') +@CommandGroup.command() +# pylint: disable=too-many-branches +def define(connection, args): + """Changes attributes of the given Data Element""" + + console = sap.cli.core.get_console() + + metadata = sap.adt.ADTCoreData(language='EN', master_language='EN', responsible=connection.user, + description=args.description) + + dataelement = sap.adt.DataElement(connection, args.name.upper(), args.package, metadata=metadata) + + # Create Data Element + console.printout(f'Creating data element {args.name}') + try: + dataelement.create(args.corrnr) + except ExceptionResourceAlreadyExists as error: + # Date Element already exists + console.printout(f'Data element {args.name} already exists') + if not args.no_error_existing: + raise error + + # Fetch data element's content + dataelement.fetch() + + if hasattr(args, 'type'): + dataelement.set_type(args.type) + + if hasattr(args, 'domain_name'): + dataelement.set_type_name(args.domain_name) + + if hasattr(args, 'data_type'): + dataelement.set_data_type(args.data_type) + + if hasattr(args, 'data_type_length'): + dataelement.set_data_type_length(args.data_type_length) + + if hasattr(args, 'data_type_decimals'): + dataelement.set_data_type_decimals(args.data_type_decimals) + + if hasattr(args, 'label_short'): + dataelement.set_label_short(args.label_short) + + if hasattr(args, 'label_medium'): + dataelement.set_label_medium(args.label_medium) + + if hasattr(args, 'label_long'): + dataelement.set_label_long(args.label_long) + + if hasattr(args, 'label_heading'): + dataelement.set_label_heading(args.label_heading) + + dataelement.normalize() + + validation_issue_key = dataelement.validate() + if validation_issue_key == VALIDATION_ISSUE_KEYS['domain_name_not_defined']: + console.printerr('Domain name must be provided (--domain_name) if the type (--type) is "domain"') + return + if validation_issue_key == VALIDATION_ISSUE_KEYS['data_type_not_defined']: + console.printerr('Data type name must be provided (--data_type) if the type (--type) is "predefinedAbapType"') + return + + # Push Data Element changes + console.printout(f'Data element {args.name} setup performed') + with dataelement.open_editor(corrnr=args.corrnr) as editor: + editor.push() + + # Activate Data Element + if args.activate: + console.printout(f'Data element {args.name} activation performed') + activator = sap.cli.wb.ObjectActivationWorker() + sap.cli.object.activate_object_list(activator, ((args.name, dataelement),), count=1) diff --git a/test/unit/fixtures_adt_dataelement.py b/test/unit/fixtures_adt_dataelement.py new file mode 100644 index 00000000..ca92cf34 --- /dev/null +++ b/test/unit/fixtures_adt_dataelement.py @@ -0,0 +1,187 @@ +DATA_ELEMENT_NAME = 'TEST_DATA_ELEMENT' + +DATA_ELEMENT_DEFINITION_ADT_XML = f''' + + + + + + + + + predefinedAbapType + + STRING + 000000 + 000000 + + 10 + 10 + + 20 + 20 + + 40 + 40 + + 55 + 55 + + + + + false + false + false + false + +''' + +CREATE_DATA_ELEMENT_ADT_XML = f''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +''' + +READ_DATA_ELEMENT_BODY = DATA_ELEMENT_DEFINITION_ADT_XML + +WRITE_DATA_ELEMENT_BODY = f''' + + + + + + + + + + predefinedAbapType + + STRING + 6 + 0 + Req. Add. + 10 + 10 + Request Address + 15 + 20 + Request Address + 20 + 40 + Request Address + 55 + 55 + + + + + false + false + false + false + +''' + +DEFINE_DATA_ELEMENT_W_DOMAIN_BODY = f''' + + + +domain +ABC + +0 +0 +Tst DTEL +10 +10 +Test Label Medium +20 +20 +Test Label Long +40 +40 +Test Label Heading +55 +55 + + + + +false +false +false +false + +''' + +DEFINE_DATA_ELEMENT_W_PREDEFINED_ABAP_TYPE_BODY = f''' + + + +predefinedAbapType + +STRING +200 +0 +Tst DTEL +10 +10 +Test Label Medium +20 +20 +Test Label Long +40 +40 +Test Label Heading +55 +55 + + + + +false +false +false +false + +''' + +FAKE_LOCK_HANDLE = 'lock_handle' + +ACTIVATE_DATA_ELEMENT_BODY = f''' + + +''' + +ERROR_XML_DATA_ELEMENT_ALREADY_EXISTS=f'''Resource Data Element {DATA_ELEMENT_NAME} does already exist.Resource Package $SAPCLI_TEST_ROOT does already exist.''' diff --git a/test/unit/test_sap_adt_dataelement.py b/test/unit/test_sap_adt_dataelement.py new file mode 100644 index 00000000..4f6879c8 --- /dev/null +++ b/test/unit/test_sap_adt_dataelement.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +import unittest +import sap.adt +import sap.adt.dataelement + +from mock import Connection, Response, Request +from fixtures_adt_dataelement import DATA_ELEMENT_DEFINITION_ADT_XML, DATA_ELEMENT_NAME, CREATE_DATA_ELEMENT_ADT_XML + + +class TestADTDataElement(unittest.TestCase): + + def test_data_element_fetch(self): + connection = Connection([Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={})]) + + data_element = sap.adt.DataElement(connection, DATA_ELEMENT_NAME) + data_element.fetch() + + self.assertEqual(data_element.name, DATA_ELEMENT_NAME) + self.assertEqual(data_element.active, 'active') + self.assertEqual(data_element.master_language, 'EN') + self.assertEqual(data_element.description, 'Test data element') + + def test_data_element_serialize(self): + connection = Connection() + + metadata = sap.adt.dataelement.ADTDataElementData(description='Test data element', language='EN', master_language='EN', + responsible='ANZEIGER') + data_element = sap.adt.DataElement(connection, DATA_ELEMENT_NAME, package='PACKAGE', metadata=metadata) + data_element.create() + + expected_request = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], expected_request) diff --git a/test/unit/test_sap_cli_dataelement.py b/test/unit/test_sap_cli_dataelement.py new file mode 100644 index 00000000..e804f010 --- /dev/null +++ b/test/unit/test_sap_cli_dataelement.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 + +import unittest +from unittest.mock import call +from sap.adt.errors import ExceptionResourceAlreadyExists + +import sap.cli.dataelement +from sap.errors import SAPCliError + +from mock import ( + Connection, + Response, + Request, + patch, +) +from io import StringIO + +from infra import generate_parse_args +from fixtures_adt_dataelement import ( + DATA_ELEMENT_NAME, + CREATE_DATA_ELEMENT_ADT_XML, + READ_DATA_ELEMENT_BODY, + FAKE_LOCK_HANDLE, + ACTIVATE_DATA_ELEMENT_BODY, + DATA_ELEMENT_DEFINITION_ADT_XML, + DEFINE_DATA_ELEMENT_W_DOMAIN_BODY, + DEFINE_DATA_ELEMENT_W_PREDEFINED_ABAP_TYPE_BODY, + ERROR_XML_DATA_ELEMENT_ALREADY_EXISTS +) +from fixtures_adt_wb import RESPONSE_ACTIVATION_OK + +parse_args = generate_parse_args(sap.cli.dataelement.CommandGroup()) + + +class TestDataElementCreate(unittest.TestCase): + + def data_element_create_cmd(self, *args, **kwargs): + return parse_args('create', *args, **kwargs) + + def test_create(self): + connection = Connection() + + the_cmd = self.data_element_create_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package') + the_cmd.execute(connection, the_cmd) + + exptected_request = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request) + + +class TestDataElementActivate(unittest.TestCase): + + def data_element_activate_cmd(self, *args, **kwargs): + return parse_args('activate', *args, **kwargs) + + def test_activate(self): + connection = Connection([ + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ]) + + the_cmd = self.data_element_activate_cmd(DATA_ELEMENT_NAME) + the_cmd.execute(connection, the_cmd) + + expected_request = Request( + adt_uri='/sap/bc/adt/activation', + method='POST', + headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, + body=ACTIVATE_DATA_ELEMENT_BODY, + params={'method': 'activate', 'preauditRequested': 'true'} + ) + + self.assertEqual(connection.execs[0], expected_request) + + +class TestDataElementDefine(unittest.TestCase): + + def data_element_define_cmd(self, *args, **kwargs): + return parse_args('define', *args, **kwargs) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_w_domain(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text='', + status_code=200, + headers={} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=domain', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_called_once() + fake_unlock.assert_called_once() + fake_console_print_err.assert_not_called() + + self.assertEqual(len(connection.execs), 5) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + expected_request_push = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='PUT', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(DEFINE_DATA_ELEMENT_W_DOMAIN_BODY, 'utf-8'), + params={'lockHandle': FAKE_LOCK_HANDLE} + ) + + self.assertEqual(connection.execs[2], expected_request_push) + + expected_request_activate = Request( + adt_uri='/sap/bc/adt/activation', + method='POST', + headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, + body=ACTIVATE_DATA_ELEMENT_BODY, + params={'method': 'activate', 'preauditRequested': 'true'} + ) + + self.assertEqual(connection.execs[3], expected_request_activate) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[4], expected_request_fetch) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_w_predefined_abap_type(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text='', + status_code=200, + headers={} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=predefinedAbapType', '--data_type=STRING', '--data_type_length=200', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_called_once() + fake_unlock.assert_called_once() + fake_console_print_err.assert_not_called() + + self.assertEqual(len(connection.execs), 5) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + expected_request_push = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='PUT', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(DEFINE_DATA_ELEMENT_W_PREDEFINED_ABAP_TYPE_BODY, 'utf-8'), + params={'lockHandle': FAKE_LOCK_HANDLE} + ) + + self.assertEqual(connection.execs[2], expected_request_push) + + expected_request_activate = Request( + adt_uri='/sap/bc/adt/activation', + method='POST', + headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, + body=ACTIVATE_DATA_ELEMENT_BODY, + params={'method': 'activate', 'preauditRequested': 'true'} + ) + + self.assertEqual(connection.execs[3], expected_request_activate) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[4], expected_request_fetch) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_domain_not_provided(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text='', + status_code=200, + headers={} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + # --domain_name argument is missing + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=domain', '--data_type=STRING', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_not_called() + fake_unlock.assert_not_called() + fake_console_print_err.assert_called_once_with('Domain name must be provided (--domain_name) if the type (--type) is "domain"') + + self.assertEqual(len(connection.execs), 2) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_predefined_abap_type_not_provided(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text='', + status_code=200, + headers={} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + # --data_type argument is missing + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=predefinedAbapType', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_not_called() + fake_unlock.assert_not_called() + fake_console_print_err.assert_called_once_with('Data type name must be provided (--data_type) if the type (--type) is "predefinedAbapType"') + + self.assertEqual(len(connection.execs), 2) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_data_element_already_exists(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text=ERROR_XML_DATA_ELEMENT_ALREADY_EXISTS, + status_code=500, + headers={'content-type': 'application/xml'} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--type=domain', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + try: + the_cmd.execute(connection, the_cmd) + + self.fail('Exception should be raised but it has not been') + except ExceptionResourceAlreadyExists as e: + self.assertEqual('Resource Data Element TEST_DATA_ELEMENT does already exist.', str(e)) + + fake_lock.assert_not_called() + fake_unlock.assert_not_called() + fake_console_print_err.assert_not_called() + + self.assertEqual(len(connection.execs), 1) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + @patch('sap.cli.core.PrintConsole.printerr') + @patch('sap.adt.objects.ADTObject.unlock') + @patch('sap.adt.objects.ADTObject.lock', return_value=FAKE_LOCK_HANDLE) + def test_define_data_element_already_exists_but_skipped(self, fake_lock, fake_unlock, fake_console_print_err): + connection = Connection([ + Response( + text=ERROR_XML_DATA_ELEMENT_ALREADY_EXISTS, + status_code=500, + headers={'content-type': 'application/xml'} + ), + Response( + text=READ_DATA_ELEMENT_BODY, + status_code=200, + headers={} + ), + Response( + text='', + status_code=200, + headers={} + ), + RESPONSE_ACTIVATION_OK, + Response(text=DATA_ELEMENT_DEFINITION_ADT_XML, status_code=200, headers={}) + ], asserter=self) + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--no-error-existing', '--type=domain', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + with patch('sys.stdin', StringIO(DATA_ELEMENT_DEFINITION_ADT_XML)): + the_cmd.execute(connection, the_cmd) + + fake_lock.assert_called_once() + fake_unlock.assert_called_once() + fake_console_print_err.assert_not_called() + + self.assertEqual(len(connection.execs), 5) + + exptected_request_create = Request( + adt_uri='/sap/bc/adt/ddic/dataelements', + method='POST', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(CREATE_DATA_ELEMENT_ADT_XML, 'utf-8'), + params=None + ) + + self.assertEqual(connection.execs[0], exptected_request_create) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[1], expected_request_fetch) + + expected_request_push = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='PUT', + headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, + body=bytes(DEFINE_DATA_ELEMENT_W_DOMAIN_BODY, 'utf-8'), + params={'lockHandle': FAKE_LOCK_HANDLE} + ) + + self.assertEqual(connection.execs[2], expected_request_push) + + expected_request_activate = Request( + adt_uri='/sap/bc/adt/activation', + method='POST', + headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'}, + body=ACTIVATE_DATA_ELEMENT_BODY, + params={'method': 'activate', 'preauditRequested': 'true'} + ) + + self.assertEqual(connection.execs[3], expected_request_activate) + + expected_request_fetch = Request( + adt_uri=f'/sap/bc/adt/ddic/dataelements/{DATA_ELEMENT_NAME.lower()}', + method='GET', + headers=None, + body=None, + params=None + ) + + self.assertEqual(connection.execs[4], expected_request_fetch) From 760ad1e01089429f6a04816ba84459c1f9056c50 Mon Sep 17 00:00:00 2001 From: Libor Bucek Date: Tue, 19 Sep 2023 11:17:53 +0200 Subject: [PATCH 2/3] adt: avoid adding trailing slash into uri if it is not necessary - There is a case when the mimetype is defined, but empty uri extension is provided (it is not needed). When the XML for DTEL needs to be fetched, there is noextended ADT source like .../main/sources that needs to be added to the URI. Trailing slashshould not be added for the case the required uri extension is empty --- sap/adt/objects.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sap/adt/objects.py b/sap/adt/objects.py index a192a046..e7701530 100644 --- a/sap/adt/objects.py +++ b/sap/adt/objects.py @@ -233,7 +233,9 @@ def get_uri_for_type(self, mimetype): """ try: - return '/' + self._typeuris[mimetype] + mimetype_uri = self._typeuris[mimetype] + + return '/' + mimetype_uri if mimetype_uri != '' else '' except KeyError: # pylint: disable=raise-missing-from raise SAPCliError('Object {type} does not support plain \'text\' format') From 7ca8567f4b1a35eb91ec4853e711a24d7b699919 Mon Sep 17 00:00:00 2001 From: Libor Bucek Date: Tue, 19 Sep 2023 15:15:44 +0200 Subject: [PATCH 3/3] review changes --- sap/adt/dataelement.py | 157 +++++++++++--------------- sap/cli/dataelement.py | 28 +++-- test/unit/test_sap_adt_dataelement.py | 7 +- test/unit/test_sap_cli_dataelement.py | 21 +++- 4 files changed, 109 insertions(+), 104 deletions(-) diff --git a/sap/adt/dataelement.py b/sap/adt/dataelement.py index 6028372a..042a0896 100644 --- a/sap/adt/dataelement.py +++ b/sap/adt/dataelement.py @@ -1,8 +1,14 @@ """ABAP Data Element ADT functionality module""" -from sap.adt.objects import ADTObject, ADTObjectType, ADTCoreData, ADTObjectSourceEditor +from enum import Enum + +from sap.adt.objects import ADTObject, ADTObjectType, ADTObjectSourceEditor # pylint: disable=unused-import -from sap.adt.annotations import OrderedClassMembers, xml_element, xml_text_node_property +from sap.adt.annotations import ( + OrderedClassMembers, + xml_text_node_property, + XmlNodeProperty, +) from sap.adt.objects import XMLNamespace, XMLNS_ADTCORE XMLNS_DTEL = XMLNamespace('dtel', 'http://www.sap.com/adt/dictionary/dataelements') @@ -14,63 +20,45 @@ 'heading': '55' } -VALIDATION_ISSUE_KEYS = { - 'none': 'none', - 'domain_name_not_defined': 'domain_name_not_defined', - 'data_type_not_defined': 'data_type_not_defined' -} - -class ADTDataElementData(ADTCoreData): - """Data Element nodes data. - """ - - # pylint: disable=too-many-instance-attributes - # pylint: disable=too-few-public-methods - class DataElement(metaclass=OrderedClassMembers): - """ADT Data Element data collector""" - - type = xml_text_node_property('dtel:typeKind') - type_name = xml_text_node_property('dtel:typeName') - data_type = xml_text_node_property('dtel:dataType') - data_type_length = xml_text_node_property('dtel:dataTypeLength') - data_type_decimals = xml_text_node_property('dtel:dataTypeDecimals') - label_short = xml_text_node_property('dtel:shortFieldLabel') - label_short_length = xml_text_node_property('dtel:shortFieldLength') - label_short_max_length = xml_text_node_property('dtel:shortFieldMaxLength') - label_medium = xml_text_node_property('dtel:mediumFieldLabel') - label_medium_length = xml_text_node_property('dtel:mediumFieldLength') - label_medium_max_length = xml_text_node_property('dtel:mediumFieldMaxLength') - label_long = xml_text_node_property('dtel:longFieldLabel') - label_long_length = xml_text_node_property('dtel:longFieldLength') - label_long_max_length = xml_text_node_property('dtel:longFieldMaxLength') - label_heading = xml_text_node_property('dtel:headingFieldLabel') - label_heading_length = xml_text_node_property('dtel:headingFieldLength') - label_heading_max_length = xml_text_node_property('dtel:headingFieldMaxLength') - search_help = xml_text_node_property('dtel:searchHelp') - search_help_parameter = xml_text_node_property('dtel:searchHelpParameter') - set_get_parameter = xml_text_node_property('dtel:setGetParameter') - default_component_name = xml_text_node_property('dtel:defaultComponentName') - deactivate_input_history = xml_text_node_property('dtel:deactivateInputHistory') - change_document = xml_text_node_property('dtel:changeDocument') - left_to_right_direction = xml_text_node_property('dtel:leftToRightDirection') - deactivate_bidi_filtering = xml_text_node_property('dtel:deactivateBIDIFiltering') - - # pylint: disable=too-many-arguments - def __init__(self, package=None, description=None, language=None, - master_language=None, master_system=None, responsible=None, - package_reference=None, abap_language_version=None): - super().__init__(package, description, language, - master_language, master_system, responsible, - package_reference, abap_language_version) - - self._data_element = ADTDataElementData.DataElement() - - @property - def data_element(self): - """The Data Element's reference""" - - return self._data_element +class DataElementValidationIssues(Enum): + """"DataElement validation issues""" + + NO_ISSUE = 0 # DataElement is correctly defined + MISSING_DOMAIN_NAME = 1 # DataElement is Domain based by Domain Name was not provided + MISSING_TYPE_NAME = 2 # DataElement is created from a predefined type by it was not provided + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-few-public-methods +class DataElementDefinition(metaclass=OrderedClassMembers): + """ADT Data Element data collector""" + + typ = xml_text_node_property('dtel:typeKind') + typ_name = xml_text_node_property('dtel:typeName') + data_type = xml_text_node_property('dtel:dataType') + data_type_length = xml_text_node_property('dtel:dataTypeLength') + data_type_decimals = xml_text_node_property('dtel:dataTypeDecimals') + label_short = xml_text_node_property('dtel:shortFieldLabel') + label_short_length = xml_text_node_property('dtel:shortFieldLength') + label_short_max_length = xml_text_node_property('dtel:shortFieldMaxLength') + label_medium = xml_text_node_property('dtel:mediumFieldLabel') + label_medium_length = xml_text_node_property('dtel:mediumFieldLength') + label_medium_max_length = xml_text_node_property('dtel:mediumFieldMaxLength') + label_long = xml_text_node_property('dtel:longFieldLabel') + label_long_length = xml_text_node_property('dtel:longFieldLength') + label_long_max_length = xml_text_node_property('dtel:longFieldMaxLength') + label_heading = xml_text_node_property('dtel:headingFieldLabel') + label_heading_length = xml_text_node_property('dtel:headingFieldLength') + label_heading_max_length = xml_text_node_property('dtel:headingFieldMaxLength') + search_help = xml_text_node_property('dtel:searchHelp') + search_help_parameter = xml_text_node_property('dtel:searchHelpParameter') + set_get_parameter = xml_text_node_property('dtel:setGetParameter') + default_component_name = xml_text_node_property('dtel:defaultComponentName') + deactivate_input_history = xml_text_node_property('dtel:deactivateInputHistory') + change_document = xml_text_node_property('dtel:changeDocument') + left_to_right_direction = xml_text_node_property('dtel:leftToRightDirection') + deactivate_bidi_filtering = xml_text_node_property('dtel:deactivateBIDIFiltering') class DataElement(ADTObject): @@ -89,80 +77,69 @@ class DataElement(ADTObject): editor_factory=ADTObjectSourceEditor ) + definition = XmlNodeProperty('dtel:dataElement') + def __init__(self, connection, name, package=None, metadata=None): super().__init__(connection, name, metadata, active_status='inactive') - self._metadata = ADTDataElementData( - metadata.package, metadata.description, - metadata.language, metadata.master_language, - metadata.master_system, metadata.responsible, - metadata.package_reference.name if metadata.package_reference is not None else None, - metadata.abap_language_version - ) if metadata is not None else ADTDataElementData() - self._metadata.package_reference.name = package - - @xml_element('dtel:dataElement') - def data_element(self): - """The Data Element's reference""" - - return self._metadata.data_element + self.definition = DataElementDefinition() def set_type(self, value): """Setter for Type Kind element""" - self._metadata.data_element.type = value + self.definition.typ = value def set_type_name(self, value): """Setter for Type Name element""" - self._metadata.data_element.type_name = value.upper() if value is not None else None + self.definition.typ_name = value.upper() if value is not None else None def set_data_type(self, value): """Setter for Data Type element""" - self._metadata.data_element.data_type = value.upper() if value is not None else None + self.definition.data_type = value.upper() if value is not None else None def set_data_type_length(self, value): """Setter for Data Type Length element""" - self._metadata.data_element.data_type_length = value + self.definition.data_type_length = value def set_data_type_decimals(self, value): """Setter for Data Type Decimals element""" - self._metadata.data_element.data_type_decimals = value + self.definition.data_type_decimals = value def set_label_short(self, value): """Setter for Label Short element""" - self._metadata.data_element.label_short = value + self.definition.label_short = value def set_label_medium(self, value): """Setter for Label Medium element""" - self._metadata.data_element.label_medium = value + self.definition.label_medium = value def set_label_long(self, value): """Setter for Label Long element""" - self._metadata.data_element.label_long = value + self.definition.label_long = value def set_label_heading(self, value): """Setter for Label Heading element""" - self._metadata.data_element.label_heading = value + self.definition.label_heading = value def normalize(self): """Validate Data Element setup before save""" - de = self._metadata.data_element + de = self.definition - if de.type == 'domain': + if de.typ == 'domain': de.data_type = '' de.data_type_length = '0' de.data_type_decimals = '0' - if de.type == 'predefinedAbapType': - de.type_name = '' + if de.typ == 'predefinedAbapType': + de.typ_name = '' de.label_short_length = de.label_short_length if not de.label_short_length else LABELS_LENGTH['short'] de.label_medium_length = de.label_medium_length if not de.label_medium_length else LABELS_LENGTH['medium'] @@ -178,9 +155,9 @@ def normalize(self): def validate(self): """Validate Data Element setup""" - if self._metadata.data_element.type == 'domain' and not self._metadata.data_element.type_name: - return VALIDATION_ISSUE_KEYS['domain_name_not_defined'] - if self._metadata.data_element.type == 'predefinedAbapType' and not self._metadata.data_element.data_type: - return VALIDATION_ISSUE_KEYS['data_type_not_defined'] + if self.definition.typ == 'domain' and not self.definition.typ_name: + return DataElementValidationIssues.MISSING_DOMAIN_NAME + if self.definition.typ == 'predefinedAbapType' and not self.definition.data_type: + return DataElementValidationIssues.MISSING_TYPE_NAME - return VALIDATION_ISSUE_KEYS['none'] + return DataElementValidationIssues.NO_ISSUE diff --git a/sap/cli/dataelement.py b/sap/cli/dataelement.py index cb26b76d..7a4d4841 100644 --- a/sap/cli/dataelement.py +++ b/sap/cli/dataelement.py @@ -1,7 +1,8 @@ """ADT proxy for ABAP DDIC Data Element""" +from sap.errors import SAPCliError import sap.adt -from sap.adt.dataelement import VALIDATION_ISSUE_KEYS +from sap.adt.dataelement import DataElementValidationIssues from sap.adt.errors import ExceptionResourceAlreadyExists import sap.cli.object import sap.cli.wb @@ -98,12 +99,25 @@ def define(connection, args): dataelement.normalize() validation_issue_key = dataelement.validate() - if validation_issue_key == VALIDATION_ISSUE_KEYS['domain_name_not_defined']: - console.printerr('Domain name must be provided (--domain_name) if the type (--type) is "domain"') - return - if validation_issue_key == VALIDATION_ISSUE_KEYS['data_type_not_defined']: - console.printerr('Data type name must be provided (--data_type) if the type (--type) is "predefinedAbapType"') - return + + match validation_issue_key: + case DataElementValidationIssues.MISSING_DOMAIN_NAME: + console.printerr( + 'Domain name must be provided (--domain_name) if the type (--type) is "domain"') + + case DataElementValidationIssues.MISSING_TYPE_NAME: + console.printerr( + 'Data type name must be provided (--data_type) if the type (--type) is "predefinedAbapType"') + + case DataElementValidationIssues.NO_ISSUE: + pass + + case _: + raise SAPCliError( + f'BUG: please report a forgotten case DataElementValidationIssues({validation_issue_key})') + + if validation_issue_key != DataElementValidationIssues.NO_ISSUE: + return 1 # Push Data Element changes console.printout(f'Data element {args.name} setup performed') diff --git a/test/unit/test_sap_adt_dataelement.py b/test/unit/test_sap_adt_dataelement.py index 4f6879c8..0d1a9394 100644 --- a/test/unit/test_sap_adt_dataelement.py +++ b/test/unit/test_sap_adt_dataelement.py @@ -24,8 +24,8 @@ def test_data_element_fetch(self): def test_data_element_serialize(self): connection = Connection() - metadata = sap.adt.dataelement.ADTDataElementData(description='Test data element', language='EN', master_language='EN', - responsible='ANZEIGER') + metadata = sap.adt.ADTCoreData(description='Test data element', language='EN', master_language='EN', + responsible='ANZEIGER') data_element = sap.adt.DataElement(connection, DATA_ELEMENT_NAME, package='PACKAGE', metadata=metadata) data_element.create() @@ -37,4 +37,5 @@ def test_data_element_serialize(self): params=None ) - self.assertEqual(connection.execs[0], expected_request) + self.maxDiff = None + expected_request.assertEqual(connection.execs[0], self) diff --git a/test/unit/test_sap_cli_dataelement.py b/test/unit/test_sap_cli_dataelement.py index e804f010..2198455c 100644 --- a/test/unit/test_sap_cli_dataelement.py +++ b/test/unit/test_sap_cli_dataelement.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import unittest -from unittest.mock import call +from unittest.mock import call, patch, Mock from sap.adt.errors import ExceptionResourceAlreadyExists import sap.cli.dataelement @@ -43,7 +43,7 @@ def test_create(self): the_cmd = self.data_element_create_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package') the_cmd.execute(connection, the_cmd) - exptected_request = Request( + expected_request = Request( adt_uri='/sap/bc/adt/ddic/dataelements', method='POST', headers={'Content-Type': 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8'}, @@ -51,7 +51,7 @@ def test_create(self): params=None ) - self.assertEqual(connection.execs[0], exptected_request) + expected_request.assertEqual(connection.execs[0], self) class TestDataElementActivate(unittest.TestCase): @@ -146,7 +146,7 @@ def test_define_w_domain(self, fake_lock, fake_unlock, fake_console_print_err): params={'lockHandle': FAKE_LOCK_HANDLE} ) - self.assertEqual(connection.execs[2], expected_request_push) + expected_request_push.assertEqual(connection.execs[2], self) expected_request_activate = Request( adt_uri='/sap/bc/adt/activation', @@ -494,3 +494,16 @@ def test_define_data_element_already_exists_but_skipped(self, fake_lock, fake_un ) self.assertEqual(connection.execs[4], expected_request_fetch) + + @patch('sap.adt.DataElement') + def test_define_element_forgotten_issue(self, mock_data_element): + mock_data_element.return_value.validate.return_value = 9999 + + the_cmd = self.data_element_define_cmd(DATA_ELEMENT_NAME, 'Test data element', 'package', '--activate', '--no-error-existing', '--type=domain', '--domain_name=ABC', '--label_short=Tst DTEL', '--label_medium=Test Label Medium', '--label_long=Test Label Long', '--label_heading=Test Label Heading') + + with self.assertRaises(SAPCliError) as caught: + the_cmd.execute(Mock(), the_cmd) + + self.assertEqual( + 'BUG: please report a forgotten case DataElementValidationIssues(9999)', + str(caught.exception))