summaryrefslogtreecommitdiff
path: root/src/leap/mail/adaptors/models.py
blob: 2bf9e601238002c133eda96f058db17bd8850e0a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# -*- coding: utf-8 -*-
# models.py
# Copyright (C) 2014 LEAP
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Generic Models to be used by the Document Adaptors.
"""
import copy


class SerializableModel(object):
    """
    A Generic document model, that can be serialized into a dictionary.

    Subclasses of this `SerializableModel` are meant to be added as class
    attributes of classes inheriting from DocumentWrapper.

    A subclass __meta__ of this SerializableModel might exist, and contain info
    relative to particularities of this model.

    For instance, the use of `__meta__.index` marks the existence of a primary
    index in the model, which will be used to do unique queries (in which case
    all the other indexed fields in the underlying document will be filled with
    the default info contained in the model definition).
    """

    @classmethod
    def serialize(klass):
        """
        Get a dictionary representation of the public attributes in the model
        class. To avoid collisions with builtin functions, any occurrence of an
        attribute ended in '_' (like 'type_') will be normalized by removing
        the trailing underscore.

        This classmethod is used from within the serialized method of a
        DocumentWrapper instance: it provides defaults for the
        empty document.
        """
        assert isinstance(klass, type)
        return _normalize_dict(klass.__dict__)


class DocumentWrapper(object):
    """
    A Wrapper object that can be manipulated, passed around, and serialized in
    a format that the store understands.
    It is related to a SerializableModel, which must be specified as the
    ``model`` class attribute.  The instance of this DocumentWrapper will not
    allow any other *public* attributes than those defined in the corresponding
    model.
    """
    # TODO we could do some very basic type checking here
    # TODO set a dirty flag (on __setattr__, whenever the value is != from
    # before)
    # TODO we could enforce the existence of a correct "model" attribute
    # in some other way (other than in the initializer)

    def __init__(self, **kwargs):
        if not getattr(self, 'model', None):
            raise RuntimeError(
                'DocumentWrapper class needs a model attribute')

        defaults = self.model.serialize()

        if kwargs:
            values = copy.deepcopy(defaults)
            values.update(kwargs)
        else:
            values = defaults

        for k, v in values.items():
            k = k.replace('-', '_')
            setattr(self, k, v)

    def __setattr__(self, attr, value):
        normalized = _normalize_dict(self.model.__dict__)
        if not attr.startswith('_') and attr not in normalized:
            raise RuntimeError(
                "Cannot set attribute because it's not defined "
                "in the model %s: %s" % (self.__class__, attr))
        object.__setattr__(self, attr, value)

    def serialize(self):
        return _normalize_dict(self.__dict__)

    def create(self):
        raise NotImplementedError()

    def update(self):
        raise NotImplementedError()

    def delete(self):
        raise NotImplementedError()

    @classmethod
    def get_or_create(self):
        raise NotImplementedError()

    @classmethod
    def get_all(self):
        raise NotImplementedError()


def _normalize_dict(_dict):
    items = _dict.items()
    items = filter(lambda (k, v): not callable(v), items)
    items = filter(lambda (k, v): not k.startswith('_'), items)
    items = [(k, v) if not k.endswith('_') else (k[:-1], v)
             for (k, v) in items]
    items = [(k.replace('-', '_'), v) for (k, v) in items]
    return dict(items)