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