1# encoding=utf-8
2
3import collections
4import itertools
5
6from six import iteritems
7
8from .exceptions import ConversionError, ModelConversionError, ValidationError
9from .datastructures import OrderedDict
10
11try:
12    basestring #PY2
13except NameError:
14    basestring = str #PY3
15
16def _list_or_string(lors):
17    if lors is None:
18        return []
19    if isinstance(lors, basestring):
20        return [lors]
21    return list(lors)
22
23try:
24    unicode #PY2
25except:
26    import codecs
27    unicode = str #PY3
28
29###
30# Transform Loops
31###
32
33def import_loop(cls, instance_or_dict, field_converter, context=None,
34                partial=False, strict=False, mapping=None):
35    """
36    The import loop is designed to take untrusted data and convert it into the
37    native types, as described in ``cls``.  It does this by calling
38    ``field_converter`` on every field.
39
40    Errors are aggregated and returned by throwing a ``ModelConversionError``.
41
42    :param cls:
43        The class for the model.
44    :param instance_or_dict:
45        A dict of data to be converted into types according to ``cls``.
46    :param field_convert:
47        This function is applied to every field found in ``instance_or_dict``.
48    :param context:
49        A ``dict``-like structure that may contain already validated data.
50    :param partial:
51        Allow partial data to validate; useful for PATCH requests.
52        Essentially drops the ``required=True`` arguments from field
53        definitions. Default: False
54    :param strict:
55        Complain about unrecognized keys. Default: False
56    """
57    is_dict = isinstance(instance_or_dict, dict)
58    is_cls = isinstance(instance_or_dict, cls)
59    if not is_cls and not is_dict:
60        error_msg = 'Model conversion requires a model or dict'
61        raise ModelConversionError(error_msg)
62    if mapping is None:
63        mapping = {}
64    data = dict(context) if context is not None else {}
65    errors = {}
66
67    # Determine all acceptable field input names
68    all_fields = set(cls._fields) ^ set(cls._serializables)
69    for field_name, field, in iteritems(cls._fields):
70        if hasattr(field, 'serialized_name'):
71            all_fields.add(field.serialized_name)
72        if hasattr(field, 'deserialize_from'):
73            all_fields.update(set(_list_or_string(field.deserialize_from)))
74        if field_name in mapping:
75            all_fields.update(set(_list_or_string(mapping[field_name])))
76
77    # Check for rogues if strict is set
78    rogue_fields = set(instance_or_dict) - set(all_fields)
79    if strict and len(rogue_fields) > 0:
80        for field in rogue_fields:
81            errors[field] = 'Rogue field'
82
83    for field_name, field in iteritems(cls._fields):
84        serialized_field_name = field.serialized_name or field_name
85
86        trial_keys = _list_or_string(field.deserialize_from)
87        trial_keys.extend(mapping.get(field_name, []))
88        trial_keys.extend([serialized_field_name, field_name])
89
90        raw_value = None
91        for key in trial_keys:
92            if key and key in instance_or_dict:
93                raw_value = instance_or_dict[key]
94        if raw_value is None:
95            if field_name in data:
96                continue
97            raw_value = field.default
98
99        try:
100            if raw_value is None:
101                if field.required and not partial:
102                    errors[serialized_field_name] = [field.messages['required']]
103            else:
104                try:
105                    mapping_by_model = mapping.get('model_mapping', {})
106                    model_mapping = mapping_by_model.get(field_name, {})
107                    raw_value = field_converter(field, raw_value, mapping=model_mapping)
108                except Exception:
109                    raw_value = field_converter(field, raw_value)
110
111            data[field_name] = raw_value
112
113        except ConversionError as exc:
114            errors[serialized_field_name] = exc.messages
115        except ValidationError as exc:
116            errors[serialized_field_name] = exc.messages
117
118    if errors:
119        raise ModelConversionError(errors, data)
120
121    return data
122
123
124def export_loop(cls, instance_or_dict, field_converter,
125                role=None, raise_error_on_role=False, print_none=False):
126    """
127    The export_loop function is intended to be a general loop definition that
128    can be used for any form of data shaping, such as application of roles or
129    how a field is transformed.
130
131    :param cls:
132        The model definition.
133    :param instance_or_dict:
134        The structure where fields from cls are mapped to values. The only
135        expectionation for this structure is that it implements a ``dict``
136        interface.
137    :param field_converter:
138        This function is applied to every field found in ``instance_or_dict``.
139    :param role:
140        The role used to determine if fields should be left out of the
141        transformation.
142    :param raise_error_on_role:
143        This parameter enforces strict behavior which requires substructures
144        to have the same role definition as their parent structures.
145    :param print_none:
146        This function overrides ``serialize_when_none`` values found either on
147        ``cls`` or an instance.
148    """
149    data = {}
150
151    # Translate `role` into `gottago` function
152    gottago = wholelist()
153    if hasattr(cls, '_options') and role in cls._options.roles:
154        gottago = cls._options.roles[role]
155    elif role and raise_error_on_role:
156        error_msg = u'%s Model has no role "%s"'
157        raise ValueError(error_msg % (cls.__name__, role))
158    else:
159        gottago = cls._options.roles.get("default", gottago)
160
161    fields_order = (getattr(cls._options, 'fields_order', None)
162                    if hasattr(cls, '_options') else None)
163
164    for field_name, field, value in atoms(cls, instance_or_dict):
165        serialized_name = field.serialized_name or field_name
166
167        # Skipping this field was requested
168        if gottago(field_name, value):
169            continue
170
171        # Value found, apply transformation and store it
172        elif value is not None:
173            if hasattr(field, 'export_loop'):
174                shaped = field.export_loop(value, field_converter,
175                                           role=role,
176                                           print_none=print_none)
177            else:
178                shaped = field_converter(field, value)
179
180            # Print if we want none or found a value
181            if shaped is None and allow_none(cls, field):
182                data[serialized_name] = shaped
183            elif shaped is not None:
184                data[serialized_name] = shaped
185            elif print_none:
186                data[serialized_name] = shaped
187
188        # Store None if reqeusted
189        elif value is None and allow_none(cls, field):
190            data[serialized_name] = value
191        elif print_none:
192            data[serialized_name] = value
193
194    # Return data if the list contains anything
195    if len(data) > 0:
196        if fields_order:
197            return sort_dict(data, fields_order)
198        return data
199    elif print_none:
200        return data
201
202
203def sort_dict(dct, based_on):
204    """
205    Sorts provided dictionary based on order of keys provided in ``based_on``
206    list.
207
208    Order is not guarantied in case if ``dct`` has keys that are not present
209    in ``based_on``
210
211    :param dct:
212        Dictionary to be sorted.
213    :param based_on:
214        List of keys in order that resulting dictionary should have.
215    :return:
216        OrderedDict with keys in the same order as provided ``based_on``.
217    """
218    return OrderedDict(
219        sorted(
220            dct.items(),
221            key=lambda el: based_on.index(el[0] if el[0] in based_on else -1))
222    )
223
224
225def atoms(cls, instance_or_dict):
226    """
227    Iterator for the atomic components of a model definition and relevant data
228    that creates a threeple of the field's name, the instance of it's type, and
229    it's value.
230
231    :param cls:
232        The model definition.
233    :param instance_or_dict:
234        The structure where fields from cls are mapped to values. The only
235        expectionation for this structure is that it implements a ``dict``
236        interface.
237    """
238    all_fields = itertools.chain(iteritems(cls._fields),
239                                 iteritems(cls._serializables))
240
241    return ((field_name, field, instance_or_dict[field_name])
242            for field_name, field in all_fields)
243
244
245def allow_none(cls, field):
246    """
247    This function inspects a model and a field for a setting either at the
248    model or field level for the ``serialize_when_none`` setting.
249
250    The setting defaults to the value of the class.  A field can override the
251    class setting with it's own ``serialize_when_none`` setting.
252
253    :param cls:
254        The model definition.
255    :param field:
256        The field in question.
257    """
258    allowed = cls._options.serialize_when_none
259    if field.serialize_when_none is not None:
260        allowed = field.serialize_when_none
261    return allowed
262
263
264###
265# Field Filtering
266###
267
268class Role(collections.Set):
269
270    """
271    A ``Role`` object can be used to filter specific fields against a sequence.
272
273    The ``Role`` is two things: a set of names and a function.  The function
274    describes how filter taking a field name as input and then returning either
275    ``True`` or ``False``, indicating that field should or should not be
276    skipped.
277
278    A ``Role`` can be operated on as a ``Set`` object representing the fields
279    is has an opinion on.  When Roles are combined with other roles, the
280    filtering behavior of the first role is used.
281    """
282
283    def __init__(self, function, fields):
284        self.function = function
285        self.fields = set(fields)
286
287    def _from_iterable(self, iterable):
288        return Role(self.function, iterable)
289
290    def __contains__(self, value):
291        return value in self.fields
292
293    def __iter__(self):
294        return iter(self.fields)
295
296    def __len__(self):
297        return len(self.fields)
298
299    def __eq__(self, other):
300        print(dir(self.function))
301        return (self.function.__name__ == other.function.__name__ and
302                self.fields == other.fields)
303
304    def __str__(self):
305        return '%s(%s)' % (self.function.__name__,
306                           ', '.join("'%s'" % f for f in self.fields))
307
308    def __repr__(self):
309        return '<Role %s>' % str(self)
310
311    # edit role fields
312    def __add__(self, other):
313        fields = self.fields.union(other)
314        return self._from_iterable(fields)
315
316    def __sub__(self, other):
317        fields = self.fields.difference(other)
318        return self._from_iterable(fields)
319
320    # apply role to field
321    def __call__(self, name, value):
322        return self.function(name, value, self.fields)
323
324    # static filter functions
325    @staticmethod
326    def wholelist(name, value, seq):
327        """
328        Accepts a field name, value, and a field list.  This functions
329        implements acceptance of all fields by never requesting a field be
330        skipped, thus returns False for all input.
331
332        :param name:
333            The field name to inspect.
334        :param value:
335            The field's value.
336        :param seq:
337            The list of fields associated with the ``Role``.
338        """
339        return False
340
341    @staticmethod
342    def whitelist(name, value, seq):
343        """
344        Implements the behavior of a whitelist by requesting a field be skipped
345        whenever it's name is not in the list of fields.
346
347        :param name:
348            The field name to inspect.
349        :param value:
350            The field's value.
351        :param seq:
352            The list of fields associated with the ``Role``.
353        """
354
355        if seq is not None and len(seq) > 0:
356            return name not in seq
357        return True
358
359    @staticmethod
360    def blacklist(name, value, seq):
361        """
362        Implements the behavior of a blacklist by requesting a field be skipped
363        whenever it's name is found in the list of fields.
364
365        :param k:
366            The field name to inspect.
367        :param v:
368            The field's value.
369        :param seq:
370            The list of fields associated with the ``Role``.
371        """
372        if seq is not None and len(seq) > 0:
373            return name in seq
374        return False
375
376
377def wholelist(*field_list):
378    """
379    Returns a function that evicts nothing. Exists mainly to be an explicit
380    allowance of all fields instead of a using an empty blacklist.
381    """
382    return Role(Role.wholelist, field_list)
383
384
385def whitelist(*field_list):
386    """
387    Returns a function that operates as a whitelist for the provided list of
388    fields.
389
390    A whitelist is a list of fields explicitly named that are allowed.
391    """
392    return Role(Role.whitelist, field_list)
393
394
395def blacklist(*field_list):
396    """
397    Returns a function that operates as a blacklist for the provided list of
398    fields.
399
400    A blacklist is a list of fields explicitly named that are not allowed.
401    """
402    return Role(Role.blacklist, field_list)
403
404
405###
406# Import and export functions
407###
408
409
410def convert(cls, instance_or_dict, context=None, partial=True, strict=False,
411            mapping=None):
412    def field_converter(field, value, mapping=None):
413        try:
414            return field.to_native(value, mapping=mapping)
415        except Exception:
416            return field.to_native(value)
417#   field_converter = lambda field, value: field.to_native(value)
418    data = import_loop(cls, instance_or_dict, field_converter, context=context,
419                       partial=partial, strict=strict, mapping=mapping)
420    return data
421
422
423def to_native(cls, instance_or_dict, role=None, raise_error_on_role=True,
424              context=None):
425    field_converter = lambda field, value: field.to_native(value,
426                                                           context=context)
427    data = export_loop(cls, instance_or_dict, field_converter,
428                       role=role, raise_error_on_role=raise_error_on_role)
429    return data
430
431
432def to_primitive(cls, instance_or_dict, role=None, raise_error_on_role=True,
433                 context=None):
434    """
435    Implements serialization as a mechanism to convert ``Model`` instances into
436    dictionaries keyed by field_names with the converted data as the values.
437
438    The conversion is done by calling ``to_primitive`` on both model and field
439    instances.
440
441    :param cls:
442        The model definition.
443    :param instance_or_dict:
444        The structure where fields from cls are mapped to values. The only
445        expectionation for this structure is that it implements a ``dict``
446        interface.
447    :param role:
448        The role used to determine if fields should be left out of the
449        transformation.
450    :param raise_error_on_role:
451        This parameter enforces strict behavior which requires substructures
452        to have the same role definition as their parent structures.
453    """
454    field_converter = lambda field, value: field.to_primitive(value,
455                                                              context=context)
456    data = export_loop(cls, instance_or_dict, field_converter,
457                       role=role, raise_error_on_role=raise_error_on_role)
458    return data
459
460
461def serialize(cls, instance_or_dict, role=None, raise_error_on_role=True,
462              context=None):
463    return to_primitive(cls, instance_or_dict, role, raise_error_on_role,
464                        context)
465
466
467EMPTY_LIST = "[]"
468EMPTY_DICT = "{}"
469
470
471def expand(data, context=None):
472    """
473    Expands a flattened structure into it's corresponding layers.  Essentially,
474    it is the counterpart to ``flatten_to_dict``.
475
476    :param data:
477        The data to expand.
478    :param context:
479        Existing expanded data that this function use for output
480    """
481    expanded_dict = {}
482
483    if context is None:
484        context = expanded_dict
485
486    for key, value in iteritems(data):
487        try:
488            key, remaining = key.split(".", 1)
489        except ValueError:
490            if not (value in (EMPTY_DICT, EMPTY_LIST) and key in expanded_dict):
491                expanded_dict[key] = value
492        else:
493            current_context = context.setdefault(key, {})
494            if current_context in (EMPTY_DICT, EMPTY_LIST):
495                current_context = {}
496                context[key] = current_context
497
498            current_context.update(expand({remaining: value}, current_context))
499    return expanded_dict
500
501
502def flatten_to_dict(instance_or_dict, prefix=None, ignore_none=True):
503    """
504    Flattens an iterable structure into a single layer dictionary.
505
506    For example:
507
508        {
509            's': 'jms was hrrr',
510            'l': ['jms was here', 'here', 'and here']
511        }
512
513        becomes
514
515        {
516            's': 'jms was hrrr',
517            u'l.1': 'here',
518            u'l.0': 'jms was here',
519            u'l.2': 'and here'
520        }
521
522    :param instance_or_dict:
523        The structure where fields from cls are mapped to values. The only
524        expectionation for this structure is that it implements a ``dict``
525        interface.
526    :param ignore_none:
527        This ignores any ``serialize_when_none`` settings and forces the empty
528        fields to be printed as part of the flattening.
529        Default: True
530    :param prefix:
531        This puts a prefix in front of the field names during flattening.
532        Default: None
533    """
534    if isinstance(instance_or_dict, dict):
535        iterator = iteritems(instance_or_dict)
536    # if hasattr(instance_or_dict, "iteritems"):
537    #     iterator = instance_or_dict.iteritems()
538    else:
539        iterator = enumerate(instance_or_dict)
540
541    flat_dict = {}
542    for key, value in iterator:
543        if prefix:
544            key = ".".join(map(unicode, (prefix, key)))
545
546        if value == []:
547            value = EMPTY_LIST
548        elif value == {}:
549            value = EMPTY_DICT
550
551        if isinstance(value, (dict, list)):
552            flat_dict.update(flatten_to_dict(value, prefix=key))
553        elif value is not None:
554            flat_dict[key] = value
555        elif not ignore_none:
556            flat_dict[key] = None
557
558    return flat_dict
559
560
561def flatten(cls, instance_or_dict, role=None, raise_error_on_role=True,
562            ignore_none=True, prefix=None, context=None):
563    """
564    Produces a flat dictionary representation of the model.  Flat, in this
565    context, means there is only one level to the dictionary.  Multiple layers
566    are represented by the structure of the key.
567
568    Example:
569
570        >>> class Foo(Model):
571        ...    s = StringType()
572        ...    l = ListType(StringType)
573
574        >>> f = Foo()
575        >>> f.s = 'string'
576        >>> f.l = ['jms', 'was here', 'and here']
577
578        >>> flatten(Foo, f)
579        {'s': 'string', u'l.1': 'jms', u'l.0': 'was here', u'l.2': 'and here'}
580
581    :param cls:
582        The model definition.
583    :param instance_or_dict:
584        The structure where fields from cls are mapped to values. The only
585        expectionation for this structure is that it implements a ``dict``
586        interface.
587    :param role:
588        The role used to determine if fields should be left out of the
589        transformation.
590    :param raise_error_on_role:
591        This parameter enforces strict behavior which requires substructures
592        to have the same role definition as their parent structures.
593    :param ignore_none:
594        This ignores any ``serialize_when_none`` settings and forces the empty
595        fields to be printed as part of the flattening.
596        Default: True
597    :param prefix:
598        This puts a prefix in front of the field names during flattening.
599        Default: None
600    """
601    field_converter = lambda field, value: field.to_primitive(value,
602                                                              context=context)
603
604    data = export_loop(cls, instance_or_dict, field_converter,
605                       role=role, print_none=True)
606
607    flattened = flatten_to_dict(data, prefix=prefix, ignore_none=ignore_none)
608
609    return flattened
610