1"""This module defines data parser
2which can be used to exchange information between processes.
3"""
4import json
5import abc
6import shlex
7import itertools
8import enum
9
10
11class Parser:
12    """Subclasses of this abstract class define
13    how to input will be parsed.
14    """
15
16    @staticmethod
17    @abc.abstractmethod
18    def get_name():
19        """Returns the constant name which is associated to this parser."""
20        raise NotImplementedError()
21
22    @abc.abstractmethod
23    def parse(self, line):
24        """Parses a line.
25
26        Args:
27            line (str): read line
28
29        Returns:
30            dict containing the key value pairs of the line
31        """
32        raise NotImplementedError()
33
34    @abc.abstractmethod
35    def unparse(self, data):
36        """Composes the data to a string
37        whichs follows the syntax of the parser.
38
39        Args:
40            data (dict): data as key value pairs
41
42        Returns:
43            string
44        """
45        raise NotImplementedError()
46
47
48class JsonParser(Parser):
49    """Parses json input"""
50
51    @staticmethod
52    def get_name():
53        return 'json'
54
55    def parse(self, line):
56        try:
57            data = json.loads(line)
58            if not isinstance(data, dict):
59                raise ValueError(
60                    'Expected to parse an json object, got ' + line)
61            return data
62        except json.JSONDecodeError as error:
63            raise ValueError(error)
64
65    def unparse(self, data):
66        return json.dumps(data)
67
68
69class SimpleParser(Parser):
70    """Parses key value pairs separated by a tab.
71    Does not support escaping spaces.
72    """
73    SEPARATOR = '\t'
74
75    @staticmethod
76    def get_name():
77        return 'simple'
78
79    def parse(self, line):
80        components = line.split(SimpleParser.SEPARATOR)
81
82        if len(components) % 2 != 0:
83            raise ValueError(
84                'Expected key value pairs, ' +
85                'but at least one key has no value: ' +
86                line)
87
88        return {
89            key: value
90            for key, value in itertools.zip_longest(
91                components[::2], components[1::2])
92        }
93
94    def unparse(self, data):
95        return SimpleParser.SEPARATOR.join(
96            str(key) + SimpleParser.SEPARATOR + str(value.replace('\n', ''))
97            for key, value in data.items())
98
99
100class BashParser(Parser):
101    """Parses input generated
102    by dumping associative arrays with `declare -p`.
103    """
104
105    @staticmethod
106    def get_name():
107        return 'bash'
108
109    def parse(self, line):
110        # remove 'typeset -A varname=( ' and ')'
111        start = line.find('(')
112        end = line.rfind(')')
113
114        if not 0 <= start < end:
115            raise ValueError(
116                "Expected input to be formatted like "
117                "the output of bashs `declare -p` function. "
118                "Got: " + line)
119
120        components = itertools.dropwhile(
121            lambda text: not text or text[0] != '[',
122            shlex.split(line[start + 1:end]))
123        return {
124            key[1:-1]: value
125            for pair in components
126            for key, value in (pair.split('=', maxsplit=1),)
127        }
128
129    def unparse(self, data):
130        return ' '.join(
131            '[' + str(key) + ']=' + shlex.quote(value)
132            for key, value in data.items())
133
134
135@enum.unique
136class ParserOption(str, enum.Enum):
137    JSON = JsonParser
138    SIMPLE = SimpleParser
139    BASH = BashParser
140
141    def __new__(cls, parser_class):
142        inst = str.__new__(cls)
143        inst._value_ = parser_class.get_name()
144        inst.parser_class = parser_class
145        return inst
146