<filename>generators/subservice/subservice_parser.py
"""
@file       subservice_parser.py
@brief      Parses the Subservice definitions for the Mission Information Base.
@details    Used by the MIB Exporter, inherits generic File Parser
@author     <NAME>
@date       14.11.2019

Example Stringset to scan for:

enum Subservice: uint8_t {
		//!< [EXPORT] : [COMMAND] Perform connection test
	    CONNECTION_TEST = 1,
	    //!< [EXPORT] : [REPLY] Connection test reply
	    CONNECTION_TEST_REPORT = 2,
	    EVENT_TRIGGER_TEST = 128, //!<  [EXPORT] : [COMMAND] Trigger test reply and test event
	    MULTIPLE_EVENT_TRIGGER_TEST = 129, //!< [EXPORT] : [COMMAND] Trigger multiple events (5)
	    MULTIPLE_CONNECTION_TEST = 130 //!< [EXPORT] : [COMMAND] Trigger multiple connection tests
	};

"""
import re
from enum import Enum

from fsfwgen.parserbase.file_list_parser import FileListParser
from fsfwgen.parserbase.parser import FileParser
from fsfwgen.utility.csv_writer import CsvWriter
from fsfwgen.utility.printer import Printer

SUBSERVICE_DEFINITION_DESTINATION = "../../mission/pus/"
SUBSERVICE_CSV_NAME = "mib_subservices.csv"
SUBSERVICE_COLUMN_HEADER = ["Service", "Subservice Name", "Subservice Number", "Type", "Comment"]

SQL_DELETE_SUBSVC_CMD = """
    DROP TABLE IF EXISTS Subservice;
    """

SQL_CREATE_SUBSVC_CMD = """
    CREATE TABLE IF NOT EXISTS Subservice(
    id              INTEGER PRIMARY KEY,
    service         INTEGER,
    subsvcName      TEXT,
    subsvcNumber    INTEGER,
    type            TEXT CHECK( type IN ('TC','TM')),
    comment         TEXT
    )
    """

SQL_INSERT_INTO_SUBSVC_CMD = """
INSERT INTO Subservice(service,subsvcName,subsvcNumber,type,comment)
VALUES(?,?,?,?,?)
"""


class SubserviceColumns(Enum):
    """
    Specifies order of MIB columns
    """
    SERVICE = 0
    NAME = 1
    NUMBER = 2
    TYPE = 3
    COMMENT = 4


Clmns = SubserviceColumns


def main():
    """
    If this file is run separately, this main will be run.
    :return:
    """
    header_parser = FileListParser(SUBSERVICE_DEFINITION_DESTINATION)
    header_file_list = header_parser.parse_header_files(False, "Parsing subservice header files: ")
    packet_subservice_parser = SubserviceParser(header_file_list)
    subservice_table = packet_subservice_parser.parse_files()
    Printer.print_content(subservice_table, "Printing subservice table:")
    print("Found " + str(len(subservice_table)) + " subservice entries.")
    subservice_writer = CsvWriter(SUBSERVICE_CSV_NAME, subservice_table, SUBSERVICE_COLUMN_HEADER)
    subservice_writer.write_to_csv()
    subservice_writer.move_csv("..")


# pylint: disable=too-few-public-methods
class SubserviceParser(FileParser):
    """
    This parser class can parse the subservice definitions.
    """

    def _handle_file_parsing_moving_window(self, file_name: str, current_line: int, moving_window_size: int,
                                           moving_window: list, *args, **kwargs):
        pass

    def __init__(self, file_list: list):
        super().__init__(file_list)
        # Column System allows reshuffling of table columns in constructor
        self.clmns_len = SubserviceColumns.__len__()
        # this table includes the current new table entry,
        # which will be updated for target parameter
        self.dict_entry_list = list(range(self.clmns_len))
        self.dict_entry_list[Clmns.COMMENT.value] = ""
        self.subservice_enum_found = False
        # This list will store the last three lines for longer comments.
        self.last_line_list = ["", "", ""]
        # If an export command was found, cache the possibility of a match.
        self.possible_match_on_next_lines = False

    # This is called for every file
    def _handle_file_parsing(self, file_name: str, *args: any, **kwargs):
        self_print_parsing_info = False
        if len(args) == 1 and isinstance(args[0], bool):
            self_print_parsing_info = args[0]

        # Read service from file name
        service_match = re.search('Service[^0-9]*([0-9]{1,3})', file_name)
        if service_match:
            self.dict_entry_list[Clmns.SERVICE.value] = service_match.group(1)
        self.dict_entry_list[Clmns.NAME.value] = " "
        file = open(file_name, "r")
        if self_print_parsing_info:
            print("Parsing " + file_name + " ...")
        # Scans each line for possible variables
        for line in file.readlines():
            self.__handle_line_reading(line)

    def __handle_line_reading(self, line):
        """
        Handles the reading of single lines.
        :param line:
        :return:
        """
        # Case insensitive matching
        enum_match = re.search(r'[\s]*enum[\s]*Subservice([^\n]*)', line, re.IGNORECASE)
        if enum_match:
            self.subservice_enum_found = True
        if self.subservice_enum_found:
            self.__handle_enum_scanning(line)
            self.last_line_list[2] = self.last_line_list[1]
            self.last_line_list[1] = self.last_line_list[0]
            self.last_line_list[0] = line

    def __handle_enum_scanning(self, line: str):
        """
        Two-line reading. First check last line. For export command.
        """
        self.__scan_for_export_command(self.last_line_list[0])
        subservice_match = self.__scan_subservices(line)
        if subservice_match:
            self.index = self.index + 1
            dict_entry_tuple = tuple(self.dict_entry_list[:self.clmns_len])
            self.mib_table.update({self.index: dict_entry_tuple})
            self.__clear_tuple()

    def __clear_tuple(self):
        self.dict_entry_list[Clmns.NAME.value] = ""
        self.dict_entry_list[Clmns.TYPE.value] = ""
        self.dict_entry_list[Clmns.NUMBER.value] = ""
        self.dict_entry_list[Clmns.COMMENT.value] = ""
        self.possible_match_on_next_lines = False

    def __scan_for_export_command(self, line: str) -> bool:
        command_string = re.search(r"([^\[]*)\[export\][: ]*\[([\w]*)\][\s]*([^\n]*)",
                                   line, re.IGNORECASE)
        if command_string:
            # Check whether there is a separated export command
            # (export command is not on same line as subservice definition)
            # ugly solution but has worked so far.
            string = command_string.group(1).lstrip()
            if len(string) <= 8:
                self.possible_match_on_next_lines = True
                if self.__scan_for_type(line):
                    self.__scan_for_comment(line)
            return True
        self.__add_possible_comment_string(line)
        return False

    def __add_possible_comment_string(self, line):
        """
        If no command was found, the line might be a continuation of a comment.
        Strip whitespaces and comment symbols and add to comment buffer.
        """
        possible_multiline_comment = line.lstrip()
        possible_multiline_comment = possible_multiline_comment.lstrip('/')
        possible_multiline_comment = possible_multiline_comment.lstrip('<')
        possible_multiline_comment = possible_multiline_comment.lstrip('!')
        possible_multiline_comment = possible_multiline_comment.rstrip()
        if len(possible_multiline_comment) > 0:
            self.dict_entry_list[Clmns.COMMENT.value] += possible_multiline_comment

    def __scan_subservices(self, line):
        """
        Scan for subservice match.
        :param line:
        :return:
        """
        subservice_match = \
            re.search(r"[\s]*([\w]*)[\s]*=[\s]*([0-9]{1,3})(?:,)?(?:[ /!<>]*([^\n]*))?", line)
        if subservice_match:
            self.dict_entry_list[Clmns.NAME.value] = subservice_match.group(1)
            self.dict_entry_list[Clmns.NUMBER.value] = subservice_match.group(2)
            # I am assuming that an export string is longer than 7 chars.
            if len(subservice_match.group(3)) > 7:
                # Export command on same line overrides old commands. Read for comment.
                if self.__process_comment_string(subservice_match.group(3)):
                    return True
            # Check whether exporting was commanded on last lines
            return bool(self.possible_match_on_next_lines)
        if re.search(r'}[\s]*;', line):
            self.subservice_enum_found = False
        return subservice_match

    def __process_comment_string(self, comment_string) -> bool:
        # look for packet type specifier
        export_command_found = self.__scan_for_type(comment_string)
        # Look for everything after [EXPORT] : [TYPESPECIFIER] as comment
        if export_command_found:
            self.__scan_for_comment(comment_string)
        return export_command_found

    def __scan_for_type(self, string) -> bool:
        type_match = re.search(r'\[reply\]|\[tm\]', string, re.IGNORECASE)
        if type_match:
            self.dict_entry_list[Clmns.TYPE.value] = 'TM'
            return True
        type_match = re.search(r'\[command\]|\[tc\]', string, re.IGNORECASE)
        if type_match:
            self.dict_entry_list[Clmns.TYPE.value] = 'TC'
            return True
        self.dict_entry_list[Clmns.TYPE.value] = 'Unspecified'
        return False

    def __scan_for_comment(self, comment_string):
        comment_match = re.search(r':[\s]*\[[\w]*\][\s]*([^\n]*)', comment_string)
        if comment_match:
            self.dict_entry_list[Clmns.COMMENT.value] = comment_match.group(1)

    def _post_parsing_operation(self):
        pass


if __name__ == "__main__":
    main()

<filename>generators/subservice/subservice_parser.py
"""
@file       subservice_parser.py
@brief      Parses the Subservice definitions for the Mission Information Base.
@details    Used by the MIB Exporter, inherits generic File Parser
@author     <NAME>
@date       14.11.2019

Example Stringset to scan for:

enum Subservice: uint8_t {
		//!< [EXPORT] : [COMMAND] Perform connection test
	    CONNECTION_TEST = 1,
	    //!< [EXPORT] : [REPLY] Connection test reply
	    CONNECTION_TEST_REPORT = 2,
	    EVENT_TRIGGER_TEST = 128, //!<  [EXPORT] : [COMMAND] Trigger test reply and test event
	    MULTIPLE_EVENT_TRIGGER_TEST = 129, //!< [EXPORT] : [COMMAND] Trigger multiple events (5)
	    MULTIPLE_CONNECTION_TEST = 130 //!< [EXPORT] : [COMMAND] Trigger multiple connection tests
	};

"""
import re
from enum import Enum

from fsfwgen.parserbase.file_list_parser import FileListParser
from fsfwgen.parserbase.parser import FileParser
from fsfwgen.utility.csv_writer import CsvWriter
from fsfwgen.utility.printer import Printer

SUBSERVICE_DEFINITION_DESTINATION = "../../mission/pus/"
SUBSERVICE_CSV_NAME = "mib_subservices.csv"
SUBSERVICE_COLUMN_HEADER = ["Service", "Subservice Name", "Subservice Number", "Type", "Comment"]

SQL_DELETE_SUBSVC_CMD = """
    DROP TABLE IF EXISTS Subservice;
    """

SQL_CREATE_SUBSVC_CMD = """
    CREATE TABLE IF NOT EXISTS Subservice(
    id              INTEGER PRIMARY KEY,
    service         INTEGER,
    subsvcName      TEXT,
    subsvcNumber    INTEGER,
    type            TEXT CHECK( type IN ('TC','TM')),
    comment         TEXT
    )
    """

SQL_INSERT_INTO_SUBSVC_CMD = """
INSERT INTO Subservice(service,subsvcName,subsvcNumber,type,comment)
VALUES(?,?,?,?,?)
"""


class SubserviceColumns(Enum):
    """
    Specifies order of MIB columns
    """
    SERVICE = 0
    NAME = 1
    NUMBER = 2
    TYPE = 3
    COMMENT = 4


Clmns = SubserviceColumns


def main():
    """
    If this file is run separately, this main will be run.
    :return:
    """
    header_parser = FileListParser(SUBSERVICE_DEFINITION_DESTINATION)
    header_file_list = header_parser.parse_header_files(False, "Parsing subservice header files: ")
    packet_subservice_parser = SubserviceParser(header_file_list)
    subservice_table = packet_subservice_parser.parse_files()
    Printer.print_content(subservice_table, "Printing subservice table:")
    print("Found " + str(len(subservice_table)) + " subservice entries.")
    subservice_writer = CsvWriter(SUBSERVICE_CSV_NAME, subservice_table, SUBSERVICE_COLUMN_HEADER)
    subservice_writer.write_to_csv()
    subservice_writer.move_csv("..")


# pylint: disable=too-few-public-methods
class SubserviceParser(FileParser):
    """
    This parser class can parse the subservice definitions.
    """

    def _handle_file_parsing_moving_window(self, file_name: str, current_line: int, moving_window_size: int,
                                           moving_window: list, *args, **kwargs):
        pass

    def __init__(self, file_list: list):
        super().__init__(file_list)
        # Column System allows reshuffling of table columns in constructor
        self.clmns_len = SubserviceColumns.__len__()
        # this table includes the current new table entry,
        # which will be updated for target parameter
        self.dict_entry_list = list(range(self.clmns_len))
        self.dict_entry_list[Clmns.COMMENT.value] = ""
        self.subservice_enum_found = False
        # This list will store the last three lines for longer comments.
        self.last_line_list = ["", "", ""]
        # If an export command was found, cache the possibility of a match.
        self.possible_match_on_next_lines = False

    # This is called for every file
    def _handle_file_parsing(self, file_name: str, *args: any, **kwargs):
        self_print_parsing_info = False
        if len(args) == 1 and isinstance(args[0], bool):
            self_print_parsing_info = args[0]

        # Read service from file name
        service_match = re.search('Service[^0-9]*([0-9]{1,3})', file_name)
        if service_match:
            self.dict_entry_list[Clmns.SERVICE.value] = service_match.group(1)
        self.dict_entry_list[Clmns.NAME.value] = " "
        file = open(file_name, "r")
        if self_print_parsing_info:
            print("Parsing " + file_name + " ...")
        # Scans each line for possible variables
        for line in file.readlines():
            self.__handle_line_reading(line)

    def __handle_line_reading(self, line):
        """
        Handles the reading of single lines.
        :param line:
        :return:
        """
        # Case insensitive matching
        enum_match = re.search(r'[\s]*enum[\s]*Subservice([^\n]*)', line, re.IGNORECASE)
        if enum_match:
            self.subservice_enum_found = True
        if self.subservice_enum_found:
            self.__handle_enum_scanning(line)
            self.last_line_list[2] = self.last_line_list[1]
            self.last_line_list[1] = self.last_line_list[0]
            self.last_line_list[0] = line

    def __handle_enum_scanning(self, line: str):
        """
        Two-line reading. First check last line. For export command.
        """
        self.__scan_for_export_command(self.last_line_list[0])
        subservice_match = self.__scan_subservices(line)
        if subservice_match:
            self.index = self.index + 1
            dict_entry_tuple = tuple(self.dict_entry_list[:self.clmns_len])
            self.mib_table.update({self.index: dict_entry_tuple})
            self.__clear_tuple()

    def __clear_tuple(self):
        self.dict_entry_list[Clmns.NAME.value] = ""
        self.dict_entry_list[Clmns.TYPE.value] = ""
        self.dict_entry_list[Clmns.NUMBER.value] = ""
        self.dict_entry_list[Clmns.COMMENT.value] = ""
        self.possible_match_on_next_lines = False

    def __scan_for_export_command(self, line: str) -> bool:
        command_string = re.search(r"([^\[]*)\[export\][: ]*\[([\w]*)\][\s]*([^\n]*)",
                                   line, re.IGNORECASE)
        if command_string:
            # Check whether there is a separated export command
            # (export command is not on same line as subservice definition)
            # ugly solution but has worked so far.
            string = command_string.group(1).lstrip()
            if len(string) <= 8:
                self.possible_match_on_next_lines = True
                if self.__scan_for_type(line):
                    self.__scan_for_comment(line)
            return True
        self.__add_possible_comment_string(line)
        return False

    def __add_possible_comment_string(self, line):
        """
        If no command was found, the line might be a continuation of a comment.
        Strip whitespaces and comment symbols and add to comment buffer.
        """
        possible_multiline_comment = line.lstrip()
        possible_multiline_comment = possible_multiline_comment.lstrip('/')
        possible_multiline_comment = possible_multiline_comment.lstrip('<')
        possible_multiline_comment = possible_multiline_comment.lstrip('!')
        possible_multiline_comment = possible_multiline_comment.rstrip()
        if len(possible_multiline_comment) > 0:
            self.dict_entry_list[Clmns.COMMENT.value] += possible_multiline_comment

    def __scan_subservices(self, line):
        """
        Scan for subservice match.
        :param line:
        :return:
        """
        subservice_match = \
            re.search(r"[\s]*([\w]*)[\s]*=[\s]*([0-9]{1,3})(?:,)?(?:[ /!<>]*([^\n]*))?", line)
        if subservice_match:
            self.dict_entry_list[Clmns.NAME.value] = subservice_match.group(1)
            self.dict_entry_list[Clmns.NUMBER.value] = subservice_match.group(2)
            # I am assuming that an export string is longer than 7 chars.
            if len(subservice_match.group(3)) > 7:
                # Export command on same line overrides old commands. Read for comment.
                if self.__process_comment_string(subservice_match.group(3)):
                    return True
            # Check whether exporting was commanded on last lines
            return bool(self.possible_match_on_next_lines)
        if re.search(r'}[\s]*;', line):
            self.subservice_enum_found = False
        return subservice_match

    def __process_comment_string(self, comment_string) -> bool:
        # look for packet type specifier
        export_command_found = self.__scan_for_type(comment_string)
        # Look for everything after [EXPORT] : [TYPESPECIFIER] as comment
        if export_command_found:
            self.__scan_for_comment(comment_string)
        return export_command_found

    def __scan_for_type(self, string) -> bool:
        type_match = re.search(r'\[reply\]|\[tm\]', string, re.IGNORECASE)
        if type_match:
            self.dict_entry_list[Clmns.TYPE.value] = 'TM'
            return True
        type_match = re.search(r'\[command\]|\[tc\]', string, re.IGNORECASE)
        if type_match:
            self.dict_entry_list[Clmns.TYPE.value] = 'TC'
            return True
        self.dict_entry_list[Clmns.TYPE.value] = 'Unspecified'
        return False

    def __scan_for_comment(self, comment_string):
        comment_match = re.search(r':[\s]*\[[\w]*\][\s]*([^\n]*)', comment_string)
        if comment_match:
            self.dict_entry_list[Clmns.COMMENT.value] = comment_match.group(1)

    def _post_parsing_operation(self):
        pass


if __name__ == "__main__":
    main()

@file       subservice_parser.py @brief      Parses the Subservice definitions for the Mission Information Base. @details    Used by the MIB Exporter, inherits generic File Parser @author     <NAME> @date       14.11.2019  Example Stringset to scan for:  enum Subservice: uint8_t { 		//!< [EXPORT] : [COMMAND] Perform connection test 	    CONNECTION_TEST = 1, 	    //!< [EXPORT] : [REPLY] Connection test reply 	    CONNECTION_TEST_REPORT = 2, 	    EVENT_TRIGGER_TEST = 128, //!<  [EXPORT] : [COMMAND] Trigger test reply and test event 	    MULTIPLE_EVENT_TRIGGER_TEST = 129, //!< [EXPORT] : [COMMAND] Trigger multiple events (5) 	    MULTIPLE_CONNECTION_TEST = 130 //!< [EXPORT] : [COMMAND] Trigger multiple connection tests 	}; DROP TABLE IF EXISTS Subservice; CREATE TABLE IF NOT EXISTS Subservice(     id              INTEGER PRIMARY KEY,     service         INTEGER,     subsvcName      TEXT,     subsvcNumber    INTEGER,     type            TEXT CHECK( type IN ('TC','TM')),     comment         TEXT     ) INSERT INTO Subservice(service,subsvcName,subsvcNumber,type,comment) VALUES(?,?,?,?,?) Specifies order of MIB columns If this file is run separately, this main will be run.     :return: # pylint: disable=too-few-public-methods This parser class can parse the subservice definitions. # Column System allows reshuffling of table columns in constructor # this table includes the current new table entry, # which will be updated for target parameter # This list will store the last three lines for longer comments. # If an export command was found, cache the possibility of a match. # This is called for every file # Read service from file name # Scans each line for possible variables Handles the reading of single lines.         :param line:         :return: # Case insensitive matching Two-line reading. First check last line. For export command. # Check whether there is a separated export command # (export command is not on same line as subservice definition) # ugly solution but has worked so far. If no command was found, the line might be a continuation of a comment.         Strip whitespaces and comment symbols and add to comment buffer. Scan for subservice match.         :param line:         :return: # I am assuming that an export string is longer than 7 chars. # Export command on same line overrides old commands. Read for comment. # Check whether exporting was commanded on last lines # look for packet type specifier # Look for everything after [EXPORT] : [TYPESPECIFIER] as comment