1"""
2    This library makes it possible to introspect dataset and aggregate nodes
3    when it is instructed.
4
5    .. note::
6
7        The following examples with be expressed in YAML for convenience's sake:
8
9        - !aggr-scalar will refer to Scalar python function
10        - !aggr-map will refer to Map python object
11        - !aggr-seq will refer for Sequence python object
12
13
14    How to instructs merging
15    ------------------------
16
17    This yaml document has duplicate keys:
18
19    .. code-block:: yaml
20
21        foo: !aggr-scalar first
22        foo: !aggr-scalar second
23        bar: !aggr-map {first: foo}
24        bar: !aggr-map {second: bar}
25        baz: !aggr-scalar 42
26
27    but tagged values instruct Salt that overlapping values they can be merged
28    together:
29
30    .. code-block:: yaml
31
32        foo: !aggr-seq [first, second]
33        bar: !aggr-map {first: foo, second: bar}
34        baz: !aggr-seq [42]
35
36
37    Default merge strategy is keep untouched
38    ----------------------------------------
39
40    For example, this yaml document still has duplicate keys, but does not
41    instruct aggregation:
42
43    .. code-block:: yaml
44
45        foo: first
46        foo: second
47        bar: {first: foo}
48        bar: {second: bar}
49        baz: 42
50
51    So the late found values prevail:
52
53    .. code-block:: yaml
54
55        foo: second
56        bar: {second: bar}
57        baz: 42
58
59
60    Limitations
61    -----------
62
63    Aggregation is permitted between tagged objects that share the same type.
64    If not, the default merge strategy prevails.
65
66    For example, these examples:
67
68    .. code-block:: yaml
69
70        foo: {first: value}
71        foo: !aggr-map {second: value}
72
73        bar: !aggr-map {first: value}
74        bar: 42
75
76        baz: !aggr-seq [42]
77        baz: [fail]
78
79        qux: 42
80        qux: !aggr-scalar fail
81
82    are interpreted like this:
83
84    .. code-block:: yaml
85
86        foo: !aggr-map{second: value}
87
88        bar: 42
89
90        baz: [fail]
91
92        qux: !aggr-seq [fail]
93
94
95    Introspection
96    -------------
97
98    TODO: write this part
99
100"""
101import collections
102import copy
103import logging
104from typing import Iterable
105from typing import Tuple
106
107__all__ = ["aggregate", "Aggregate", "Map", "Scalar", "Sequence"]
108
109log = logging.getLogger(__name__)
110
111
112class Aggregate:
113    """
114    Aggregation base.
115    """
116
117
118class Map(collections.OrderedDict, Aggregate):
119    """
120    Map aggregation.
121    """
122
123
124class Sequence(list, Aggregate):
125    """
126    Sequence aggregation.
127    """
128
129
130def Scalar(obj):
131    """
132    Shortcut for Sequence creation
133
134    >>> Scalar('foo') == Sequence(['foo'])
135    True
136    """
137    return Sequence([obj])
138
139
140def levelise(level: bool or int or Iterable) -> Tuple[bool, bool or int]:
141    """
142    Describe which levels are allowed to do deep merging.
143
144    level can be:
145
146    True
147        all levels are True
148
149    False
150        all levels are False
151
152    an int
153        only the first levels are True, the others are False
154
155    a sequence
156        it describes which levels are True, it can be:
157
158        * a list of bool and int values
159        * a string of 0 and 1 characters
160
161    """
162
163    if not level:  # False, 0, [] ...
164        return False, False
165    if level is True:
166        return True, True
167    if isinstance(level, int):
168        return True, level - 1
169    try:  # a sequence
170        deep, subs = int(level[0]), level[1:]
171        return bool(deep), subs
172    except Exception as error:  # pylint: disable=broad-except
173        log.warning(error)
174        raise
175
176
177def mark(
178    obj: object, map_class: object = Map, sequence_class: object = Sequence
179) -> object:
180    """
181    Convert obj into an Aggregate instance
182    """
183    if isinstance(obj, Aggregate):
184        return obj
185    if isinstance(obj, dict):
186        return map_class(obj)
187    if isinstance(obj, (list, tuple, set)):
188        return sequence_class(obj)
189    else:
190        return sequence_class([obj])
191
192
193def aggregate(
194    obj_a,
195    obj_b,
196    level: bool or int = False,
197    map_class: object = Map,
198    sequence_class: object = Sequence,
199):
200    """
201    Merge obj_b into obj_a.
202
203    >>> aggregate('first', 'second', True) == ['first', 'second']
204    True
205    """
206    deep, subdeep = levelise(level)
207
208    if deep:
209        obj_a = mark(obj_a, map_class=map_class, sequence_class=sequence_class)
210        obj_b = mark(obj_b, map_class=map_class, sequence_class=sequence_class)
211
212    if isinstance(obj_a, dict) and isinstance(obj_b, dict):
213        if isinstance(obj_a, Aggregate) and isinstance(obj_b, Aggregate):
214            # deep merging is more or less a.update(obj_b)
215            response = copy.copy(obj_a)
216        else:
217            # introspection on obj_b keys only
218            response = copy.copy(obj_b)
219
220        for key, value in obj_b.items():
221            if key in obj_a:
222                value = aggregate(obj_a[key], value, subdeep, map_class, sequence_class)
223            response[key] = value
224        return response
225
226    if isinstance(obj_a, Sequence) and isinstance(obj_b, Sequence):
227        response = obj_a.__class__(obj_a[:])
228        for value in obj_b:
229            if value not in obj_a:
230                response.append(value)
231        return response
232
233    response = copy.copy(obj_b)
234
235    if isinstance(obj_a, Aggregate) or isinstance(obj_b, Aggregate):
236        log.info("only one value marked as aggregate. keep `obj_b` value")
237        return response
238
239    log.debug("no value marked as aggregate. keep `obj_b` value")
240    return response
241