1"""
2This module's purpose is to enable us to present internals of objects
3in well-defined way to operator. To do this we can define "views"
4on some objects. View is a definition of how to present object
5and relations to other objects which also have their views defined.
6
7By using views we can avoid making all interesting internal values
8public. They will stay private and only "view" will access them
9(think friend-class from C++)
10"""
11import logging
12
13from ryu.services.protocols.bgp.operator.views import fields
14
15LOG = logging.getLogger('bgpspeaker.operator.views.base')
16
17
18class RdyToFlattenCollection(object):
19    pass
20
21
22class RdyToFlattenList(list, RdyToFlattenCollection):
23    pass
24
25
26class RdyToFlattenDict(dict, RdyToFlattenCollection):
27    pass
28
29
30class OperatorAbstractView(object):
31    """Abstract base class for operator views. It isn't meant to be
32    instantiated.
33    """
34
35    def __init__(self, obj, filter_func=None):
36        """Init
37
38        :param obj: data model for view. In other words object we
39            are creating view for. In case of ListView it should be
40            a list and in case of DictView it should be a dict.
41        :param filter_func: function to filter models
42        """
43        self._filter_func = filter_func
44        self._fields = self._collect_fields()
45        self._obj = obj
46
47    @classmethod
48    def _collect_fields(cls):
49        names = [attr for attr in dir(cls)
50                 if isinstance(getattr(cls, attr), fields.Field)]
51        return dict([(name, getattr(cls, name)) for name in names])
52
53    def combine_related(self, field_name):
54        """Combines related views. In case of DetailView it just returns
55            one-element list containing related view wrapped in
56            CombinedViewsWrapper.
57
58            In case of ListView and DictView it returns a list of related views
59            for every element of model collection also wrapped
60            in CombinedViewsWrapper.
61
62        :param field_name: field name of related view
63        :returns: vectorized form of related views. You can access them
64            as if you had only one view and you will receive flattened list
65            of responses from related views. Look at docstring of
66            CombinedViewsWrapper
67        """
68        raise NotImplementedError()
69
70    def c_rel(self, *args, **kwargs):
71        """Shortcut for combine_related. Look above
72        """
73        return self.combine_related(*args, **kwargs)
74
75    def get_field(self, field_name):
76        """Get value of data field.
77
78        :return: value of data-field of this view
79        """
80        raise NotImplementedError()
81
82    def encode(self):
83        """Representation of view which is using only python standard types.
84
85        :return: dict representation of this views data. However it
86            doesn't have to be a dict. In case of ListView it would
87            return a list. It should return wrapped types
88            for list - RdyToFlattenList, for dict - RdyToFlattenDict
89        """
90        raise NotImplementedError()
91
92    @property
93    def model(self):
94        """Getter for data model being presented by this view. Every view is
95        associated with some data model.
96
97        :return: underlaying data of this view
98        """
99        raise NotImplementedError()
100
101    def apply_filter(self, filter_func):
102        """Sets filter function to apply on model
103
104        :param filter_func: function which takes the model and returns it
105            filtered
106        """
107        self._filter_func = filter_func
108
109    def clear_filter(self):
110        self._filter_func = None
111
112
113class OperatorDetailView(OperatorAbstractView):
114    def combine_related(self, field_name):
115        f = self._fields[field_name]
116        return CombinedViewsWrapper([f.retrieve_and_wrap(self._obj)])
117
118    def get_field(self, field_name):
119        f = self._fields[field_name]
120        return f.get(self._obj)
121
122    def encode(self):
123        encoded = {}
124        for field_name, field in self._fields.items():
125            if isinstance(field, fields.DataField):
126                encoded[field_name] = field.get(self._obj)
127        return encoded
128
129    def rel(self, field_name):
130        f = self._fields[field_name]
131        return f.retrieve_and_wrap(self._obj)
132
133    @property
134    def model(self):
135        return self._obj
136
137
138class OperatorListView(OperatorAbstractView):
139    def __init__(self, obj, filter_func=None):
140        assert isinstance(obj, list)
141        obj = RdyToFlattenList(obj)
142        super(OperatorListView, self).__init__(obj, filter_func)
143
144    def combine_related(self, field_name):
145        f = self._fields[field_name]
146        return CombinedViewsWrapper(RdyToFlattenList(
147            [f.retrieve_and_wrap(obj) for obj in self.model]
148        ))
149
150    def get_field(self, field_name):
151        f = self._fields[field_name]
152        return RdyToFlattenList([f.get(obj) for obj in self.model])
153
154    def encode(self):
155        encoded_list = []
156        for obj in self.model:
157            encoded_item = {}
158            for field_name, field in self._fields.items():
159                if isinstance(field, fields.DataField):
160                    encoded_item[field_name] = field.get(obj)
161            encoded_list.append(encoded_item)
162        return RdyToFlattenList(encoded_list)
163
164    @property
165    def model(self):
166        if self._filter_func is not None:
167            return RdyToFlattenList(filter(self._filter_func, self._obj))
168        else:
169            return self._obj
170
171
172class OperatorDictView(OperatorAbstractView):
173    def __init__(self, obj, filter_func=None):
174        assert isinstance(obj, dict)
175        obj = RdyToFlattenDict(obj)
176        super(OperatorDictView, self).__init__(obj, filter_func)
177
178    def combine_related(self, field_name):
179        f = self._fields[field_name]
180        return CombinedViewsWrapper(RdyToFlattenList(
181            [f.retrieve_and_wrap(obj) for obj in self.model.values()])
182        )
183
184    def get_field(self, field_name):
185        f = self._fields[field_name]
186        dict_to_flatten = {}
187        for key, obj in self.model.items():
188            dict_to_flatten[key] = f.get(obj)
189        return RdyToFlattenDict(dict_to_flatten)
190
191    def encode(self):
192        outer_dict_to_flatten = {}
193        for key, obj in self.model.items():
194            inner_dict_to_flatten = {}
195            for field_name, field in self._fields.items():
196                if isinstance(field, fields.DataField):
197                    inner_dict_to_flatten[field_name] = field.get(obj)
198            outer_dict_to_flatten[key] = inner_dict_to_flatten
199        return RdyToFlattenDict(outer_dict_to_flatten)
200
201    @property
202    def model(self):
203        if self._filter_func is not None:
204            new_model = RdyToFlattenDict()
205            for k, v in self._obj.items():
206                if self._filter_func(k, v):
207                    new_model[k] = v
208            return new_model
209        else:
210            return self._obj
211
212
213class CombinedViewsWrapper(RdyToFlattenList):
214    """List-like wrapper for views. It provides same interface as any other
215    views but enables as to access all views in bulk.
216    It wraps and return responses from all views as a list. Be aware that
217    in case of DictViews wrapped in CombinedViewsWrapper you loose
218    information about dict keys.
219    """
220
221    def __init__(self, obj):
222        super(CombinedViewsWrapper, self).__init__(obj)
223        self._obj = obj
224
225    def combine_related(self, field_name):
226        return CombinedViewsWrapper(
227            list(_flatten(
228                [obj.combine_related(field_name) for obj in self._obj]
229            ))
230        )
231
232    def c_rel(self, *args, **kwargs):
233        return self.combine_related(*args, **kwargs)
234
235    def encode(self):
236        return list(_flatten([obj.encode() for obj in self._obj]))
237
238    def get_field(self, field_name):
239        return list(_flatten([obj.get_field(field_name) for obj in self._obj]))
240
241    @property
242    def model(self):
243        return list(_flatten([obj.model for obj in self._obj]))
244
245    def apply_filter(self, filter_func):
246        for obj in self._obj:
247            obj.apply_filter(filter_func)
248
249    def clear_filter(self):
250        for obj in self._obj:
251            obj.clear_filter()
252
253
254def _flatten(l, max_level=10):
255    """Generator function going deep in tree-like structures
256    (i.e. dicts in dicts or lists in lists etc.) and returning all elements as
257    a flat list. It's flattening only lists and dicts which are subclasses of
258    RdyToFlattenCollection. Regular lists and dicts are treated as a
259    single items.
260
261    :param l: some iterable to be flattened
262    :return: flattened iterator
263    """
264    if max_level >= 0:
265        _iter = l.values() if isinstance(l, dict) else l
266        for el in _iter:
267            if isinstance(el, RdyToFlattenCollection):
268                for sub in _flatten(el, max_level=max_level - 1):
269                    yield sub
270            else:
271                yield el
272    else:
273        yield l
274
275
276def _create_collection_view(detail_view_class, name, encode=None,
277                            view_class=None):
278    assert issubclass(detail_view_class, OperatorDetailView)
279    class_fields = detail_view_class._collect_fields()
280    if encode is not None:
281        class_fields.update({'encode': encode})
282    return type(name, (view_class,), class_fields)
283
284
285# function creating ListView from DetailView
286def create_dict_view_class(detail_view_class, name):
287    encode = None
288    if 'encode' in dir(detail_view_class):
289        def encode(self):
290            dict_to_flatten = {}
291            for key, obj in self.model.items():
292                dict_to_flatten[key] = detail_view_class(obj).encode()
293            return RdyToFlattenDict(dict_to_flatten)
294
295    return _create_collection_view(
296        detail_view_class, name, encode, OperatorDictView
297    )
298
299
300# function creating DictView from DetailView
301def create_list_view_class(detail_view_class, name):
302    encode = None
303    if 'encode' in dir(detail_view_class):
304        def encode(self):
305            return RdyToFlattenList([detail_view_class(obj).encode()
306                                     for obj in self.model])
307
308    return _create_collection_view(
309        detail_view_class, name, encode, OperatorListView
310    )
311