dgk/django-business-logic

View on GitHub
business_logic/blockly/build.py

Summary

Maintainability
A
3 hrs
Test Coverage
# -*- coding: utf-8 -*-

import inspect
import logging
import re

from lxml import etree

from django.db.models import Model

from ..models import *

from .data import OPERATOR_TABLE
from .exceptions import BlocklyXmlBuilderException

logger = logging.getLogger(__name__)


def camel_case_to_snake_case(name):
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()


class BlocklyXmlBuilder(NodeCacheHolder):

    def build(self, tree_root):
        xml = etree.Element('xml')
        self.visit(tree_root, parent_xml=xml)
        return etree.tostring(xml, pretty_print=True).decode('utf-8')

    def visit(self, node, parent_xml):
        content_object = node.content_object

        if content_object is None:
            last_xml = None
            for child in self.get_children(node):
                if last_xml is not None:
                    next_element = etree.Element('next')
                    last_xml.append(next_element)
                    parent_xml = next_element
                last_xml = self.visit(child, parent_xml)
            return

        for cls in inspect.getmro(content_object.__class__):
            if cls == Model:
                break

            method_name = 'visit_{}'.format(camel_case_to_snake_case(cls.__name__))
            method = getattr(self, method_name, None)

            if not method:
                continue

            node_xml = method(node, parent_xml)

            if not getattr(method, 'process_children', None):
                for child in self.get_children(node):
                    self.visit(child, parent_xml)

            node_xml.set('id', str(node.id))
            return node_xml

        if content_object.__class__ not in (VariableDefinition,):
            logger.debug('Unsupported content_object: {}'.format(content_object.__class__))

    def visit_constant(self, node, parent_xml):
        block_type = {
            NumberConstant: 'math_number',
            StringConstant: 'text',
            BooleanConstant: 'logic_boolean',
            DateConstant: 'business_logic_date',
        }
        field_name = {
            NumberConstant: 'NUM',
            StringConstant: 'TEXT',
            BooleanConstant: 'BOOL',
            DateConstant: 'DATE',
        }
        content_object = node.content_object
        cls = content_object.__class__
        block = etree.SubElement(parent_xml, 'block', type=block_type[cls])
        field_element = etree.SubElement(block, 'field', name=field_name[cls])
        if isinstance(content_object, BooleanConstant):
            field_element.text = str(content_object).upper()
        else:
            field_element.text = str(content_object)
        return block

    def visit_reference_constant(self, node, parent_xml):
        children = self.get_children(node)

        if len(children) != 1:
            raise BlocklyXmlBuilderException('Incorrect number of ReferenceConstant node children: {}'.format(
                len(children)))

        value_object_node = children[0]
        content_type = value_object_node.content_type

        block = etree.SubElement(parent_xml, 'block', type='business_logic_reference')

        type_field = etree.SubElement(block, 'field', name='TYPE')
        type_field.text = '{}.{}'.format(content_type.app_label, content_type.model_class().__name__)

        value_field = etree.SubElement(block, 'field', name='VALUE')
        value_field.text = str(value_object_node.object_id)
        return block

    visit_reference_constant.process_children = True

    def _get_variable_block_type(self, node, action):
        assert action in ('get', 'set')

        if node.content_object.definition.name.find('.') != -1:
            return 'business_logic_argument_field_{}'.format(action)
        return 'variables_{}'.format(action)

    def visit_variable(self, node, parent_xml):
        block_type = self._get_variable_block_type(node, 'get')
        block = etree.SubElement(parent_xml, 'block', type=block_type)
        self._visit_variable(node, block)
        return block

    def visit_assignment(self, node, parent_xml):
        lhs_node, rhs_node = self.get_children(node)
        block_type = self._get_variable_block_type(lhs_node, 'set')
        block = etree.SubElement(parent_xml, 'block', type=block_type)
        self._visit_variable(lhs_node, block)
        value = etree.SubElement(block, 'value', name='VALUE')
        self.visit(rhs_node, value)
        return block

    visit_assignment.process_children = True

    def _visit_variable(self, node, parent_xml):
        variable = node.content_object
        field_element = etree.SubElement(parent_xml, 'field', name='VAR')
        field_element.text = variable.definition.name

    def visit_binary_operator(self, node, parent_xml):

        # determine block_type
        operator_value = node.content_object.operator
        block_type = None
        table = None

        for block_type, table in OPERATOR_TABLE.items():
            if operator_value in table:
                break
        else:
            raise BlocklyXmlBuilderException('Invalid Operator: {}'.format(operator_value))

        block = etree.SubElement(parent_xml, 'block', type=block_type)
        field_element = etree.SubElement(block, 'field', name='OP')
        field_element.text = table[operator_value]

        lhs_node, rhs_node = self.get_children(node)
        for value_name, child_node in (('A', lhs_node), ('B', rhs_node)):
            value = etree.SubElement(block, 'value', name=value_name)
            self.visit(child_node, value)

        return block

    visit_binary_operator.process_children = True

    def visit_if_statement(self, node, parent_xml):
        children = self.get_children(node)
        block = etree.SubElement(parent_xml, 'block', type='controls_if')

        if len(children) > 2:
            mutation = etree.SubElement(block, 'mutation')
            if len(children) % 2:
                mutation.set('else', '1')
            elifs = (len(children) - 2 - len(children) % 2) / 2
            if elifs:
                mutation.set('elseif', str(int(elifs)))

        for i, pair in enumerate(pairs(children)):
            # last "else" branch
            if len(pair) == 1:
                statement = etree.SubElement(block, 'statement', name='ELSE')
                self.visit(pair[0], statement)
                break

            if_condition = pair[0]
            if_value = etree.SubElement(block, 'value', name='IF{}'.format(i))
            self.visit(if_condition, if_value)

            statement = etree.SubElement(block, 'statement', name='DO{}'.format(i))
            self.visit(pair[1], statement)

        return block

    visit_if_statement.process_children = True

    def visit_function(self, node, parent_xml):
        function = node.content_object
        function_definition = function.definition
        children = self.get_children(node)

        block = etree.SubElement(parent_xml, 'block', type='business_logic_function')
        etree.SubElement(block, 'mutation', args='true')
        field_element = etree.SubElement(block, 'field', name='FUNC')
        field_element.text = function_definition.title

        for i, child_node in enumerate(children):
            value = etree.SubElement(block, 'value', name='ARG{}'.format(i))
            self.visit(child_node, value)

        return block

    visit_function.process_children = True