OpServ-Monitoring/opserv-backend

View on GitHub
app/server/restful_api/general/endpoint.py

Summary

Maintainability
A
3 hrs
Test Coverage
import re
from abc import ABCMeta, abstractmethod

from server.data_gates.default_data_gate import DefaultDataGate
from .requestholder import RequestHolder
from .responseholder import ResponseHolder


class Endpoint(metaclass=ABCMeta):
    _outbound_gate = DefaultDataGate

    _request_holder = None
    _response_holder = None

    def __init__(self):
        self._response_holder = ResponseHolder()

    def handle_request(self, request_holder):
        """
        Handles the processing of a incoming http request by executing the _pre_process() function,
        any of the processing functions _get(), _post(), _put() or _delete() and finally the _pre_process() function
        :return: The final Response object to answer the request with
        """
        self._request_holder = request_holder

        keep_processing = self._pre_process()
        if keep_processing:
            keep_processing = keep_processing and self.__main_process()

            if keep_processing:
                self._post_process()

        return self._response_holder

    def _pre_process(self) -> bool:
        """
        This method is called before any of the processing functions _get(), _post(), _put() or _delete() is executed.
        Override this method in any subclass of Endpoint to manipulate the Request or Response object beforehand.
        :return: A boolean indicating whether to carry on processing the request or not
        """
        body = {
            'data': {},
            'links': self.__generate_links()
        }

        self._response_holder.set_body(body)

        return True

    def __main_process(self):
        method = self._request_holder.get_http_method()

        if method == RequestHolder.METHOD_GET():
            return self._get()
        elif method == RequestHolder.METHOD_POST():
            return self._post()
        elif method == RequestHolder.METHOD_PUT():
            return self._put()
        elif method == RequestHolder.METHOD_DELETE():
            return self._delete()
        else:
            self._response_holder.set_bad_request_response(self._response_holder)

            return False

    @abstractmethod
    def _get(self) -> bool:
        """
        Override this method in any subclass of Endpoint to manipulate the Response object
        in case the request is a GET request
        :return: A boolean indicating whether to carry on processing the request or not
        """

    @abstractmethod
    def _post(self) -> bool:
        """
        Override this method in any subclass of Endpoint to manipulate the Response object
        in case the request is a POST request
        :return: A boolean indicating whether to carry on processing the request or not
        """

    @abstractmethod
    def _put(self) -> bool:
        """
        Override this method in any subclass of Endpoint to manipulate the Response object
        in case the request is a PUT request
        :return: A boolean indicating whether to carry on processing the request or not
        """

    @abstractmethod
    def _delete(self) -> bool:
        """
        Override this method in any subclass of Endpoint to manipulate the Response object
        in case the request is a DELETE request
        :return: A boolean indicating whether to carry on processing the request or not
        """

    @abstractmethod
    def _post_process(self) -> bool:
        """
        This method is called after one of the processing functions _get(), _post(), _put() or _delete() is executed.
        Override this method in any subclass of Endpoint to manipulate the Response object afterwards.
        :return: A boolean indicating whether to carry on processing the request or not
        """

    @classmethod
    @abstractmethod
    def get_paths(cls):
        """
        Override this method in any subclass of Endpoint to set the paths this endpoint should operate on
        :return: A tupel of paths this endpoint should operate on
        """

    def __generate_links(self):
        links = {}

        self.__generate_self_reference(links)
        self.__generate_parent_reference(links)
        self.__generate_children_references(links)
        self.__generate_doc_reference(links)

        return links

    def __generate_self_reference(self, links):
        uri = self._request_holder.get_uri()
        name = self.get_name()
        if name is not None:
            links['self'] = self._get_link_element(uri, name)

    def __generate_parent_reference(self, links):
        parent_uri = self.__get_parent_uri()
        parent_name = self.__get_parent_name()

        if parent_uri is not None and parent_name is not None:
            links['parent'] = self._get_link_element(parent_uri, parent_name)

    def __generate_children_references(self, links):
        children = self.__get_children()
        if children is not None and isinstance(children, list) and len(children) > 0:
            links['children'] = children

    def __generate_doc_reference(self, links):
        uri = self._request_holder.get_uri().replace("/api", "/apiref")

        links['docs'] = self._get_link_element(uri, "api reference article")

    @classmethod
    @abstractmethod
    def get_name(cls):
        """
        Each type of endpoint should have a name which is displayed in the api as part of the links section
        :return: A string indicating the type of resource this endpoint represents
        """

    @classmethod
    @abstractmethod
    def _get_parent(cls):
        """
        To support HATEOAS each type of endpoint should define a parent endpoint.
        :return: A (subclass of) endpoint that is the direct api-parent of this endpoint or None
        """

    def __get_parent_uri(self):
        found_match = self.__match_for_parent_path()

        if found_match:
            return found_match.group(1)
        else:
            return None

    def __get_parent_name(self):
        parent = self._get_parent()

        parent_name = None
        if parent is not None:
            parent_name = parent.get_name()

        return parent_name

    def __get_children(self):
        """
        Returns an array holding reference objects to append as children to the link section of the response
        :return: An array holding reference objects to append as children to the response
        """
        children = []

        found_match = self.__match_for_children_base_path()
        if found_match:
            base_path = found_match.group(1)

            children_data = self._get_children()

            for child_data in children_data:
                uri = base_path + child_data[0]
                endpoint = child_data[1]

                children.append(self._get_link_element(uri, endpoint.get_name()))

        return children

    @classmethod
    @abstractmethod
    def _get_children(cls) -> list:
        pass

    def __match_uri_with_regex(self, regex):
        uri = self._request_holder.get_uri()

        return re.match(regex, uri)

    def __match_for_children_base_path(self):
        regex = "(.+\/api.*\/*.*)(\?.*){0,1}"

        return self.__match_uri_with_regex(regex)

    def __match_for_parent_path(self):
        regex = "(.+\/api.*)\/.+(\?.*){0,1}"

        return self.__match_uri_with_regex(regex)

    @classmethod
    def _get_link_element(cls, uri, name):
        """
        Static helper that returns a reference dictionary object to use in the links section of the response
        :param uri: The uri of the referenced endpoint
        :param name: The name of the references endpoint
        :return: a dictionary including the passed uri and name
        """
        return {'href': uri, 'name': name}

    @classmethod
    def KEEP_PROCESSING(cls):
        return True

    @classmethod
    def STOP_PROCESSING(cls):
        return False