File size: 9,674 Bytes
d1ceb73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.

"""Improved JSON serialization.
"""

import builtins
import json
import numbers
import operator


JsonDecoder = json.JSONDecoder


class JsonEncoder(json.JSONEncoder):
    """Customizable JSON encoder.

    If the object implements __getstate__, then that method is invoked, and its
    result is serialized instead of the object itself.
    """

    def default(self, value):
        try:
            get_state = value.__getstate__
        except AttributeError:
            pass
        else:
            return get_state()
        return super().default(value)


class JsonObject(object):
    """A wrapped Python object that formats itself as JSON when asked for a string
    representation via str() or format().
    """

    json_encoder_factory = JsonEncoder
    """Used by __format__ when format_spec is not empty."""

    json_encoder = json_encoder_factory(indent=4)
    """The default encoder used by __format__ when format_spec is empty."""

    def __init__(self, value):
        assert not isinstance(value, JsonObject)
        self.value = value

    def __getstate__(self):
        raise NotImplementedError

    def __repr__(self):
        return builtins.repr(self.value)

    def __str__(self):
        return format(self)

    def __format__(self, format_spec):
        """If format_spec is empty, uses self.json_encoder to serialize self.value
        as a string. Otherwise, format_spec is treated as an argument list to be
        passed to self.json_encoder_factory - which defaults to JSONEncoder - and
        then the resulting formatter is used to serialize self.value as a string.

        Example::

            format("{0} {0:indent=4,sort_keys=True}", json.repr(x))
        """
        if format_spec:
            # At this point, format_spec is a string that looks something like
            # "indent=4,sort_keys=True". What we want is to build a function call
            # from that which looks like:
            #
            #   json_encoder_factory(indent=4,sort_keys=True)
            #
            # which we can then eval() to create our encoder instance.
            make_encoder = "json_encoder_factory(" + format_spec + ")"
            encoder = eval(
                make_encoder, {"json_encoder_factory": self.json_encoder_factory}
            )
        else:
            encoder = self.json_encoder
        return encoder.encode(self.value)


# JSON property validators, for use with MessageDict.
#
# A validator is invoked with the actual value of the JSON property passed to it as
# the sole argument; or if the property is missing in JSON, then () is passed. Note
# that None represents an actual null in JSON, while () is a missing value.
#
# The validator must either raise TypeError or ValueError describing why the property
# value is invalid, or else return the value of the property, possibly after performing
# some substitutions - e.g. replacing () with some default value.


def _converter(value, classinfo):
    """Convert value (str) to number, otherwise return None if is not possible"""
    for one_info in classinfo:
        if issubclass(one_info, numbers.Number):
            try:
                return one_info(value)
            except ValueError:
                pass


def of_type(*classinfo, **kwargs):
    """Returns a validator for a JSON property that requires it to have a value of
    the specified type. If optional=True, () is also allowed.

    The meaning of classinfo is the same as for isinstance().
    """

    assert len(classinfo)
    optional = kwargs.pop("optional", False)
    assert not len(kwargs)

    def validate(value):
        if (optional and value == ()) or isinstance(value, classinfo):
            return value
        else:
            converted_value = _converter(value, classinfo)
            if converted_value:
                return converted_value

            if not optional and value == ():
                raise ValueError("must be specified")
            raise TypeError("must be " + " or ".join(t.__name__ for t in classinfo))

    return validate


def default(default):
    """Returns a validator for a JSON property with a default value.

    The validator will only allow property values that have the same type as the
    specified default value.
    """

    def validate(value):
        if value == ():
            return default
        elif isinstance(value, type(default)):
            return value
        else:
            raise TypeError("must be {0}".format(type(default).__name__))

    return validate


def enum(*values, **kwargs):
    """Returns a validator for a JSON enum.

    The validator will only allow the property to have one of the specified values.

    If optional=True, and the property is missing, the first value specified is used
    as the default.
    """

    assert len(values)
    optional = kwargs.pop("optional", False)
    assert not len(kwargs)

    def validate(value):
        if optional and value == ():
            return values[0]
        elif value in values:
            return value
        else:
            raise ValueError("must be one of: {0!r}".format(list(values)))

    return validate


def array(validate_item=False, vectorize=False, size=None):
    """Returns a validator for a JSON array.

    If the property is missing, it is treated as if it were []. Otherwise, it must
    be a list.

    If validate_item=False, it's treated as if it were (lambda x: x) - i.e. any item
    is considered valid, and is unchanged. If validate_item is a type or a tuple,
    it's treated as if it were json.of_type(validate).

    Every item in the list is replaced with validate_item(item) in-place, propagating
    any exceptions raised by the latter. If validate_item is a type or a tuple, it is
    treated as if it were json.of_type(validate_item).

    If vectorize=True, and the value is neither a list nor a dict, it is treated as
    if it were a single-element list containing that single value - e.g. "foo" is
    then the same as ["foo"]; but {} is an error, and not [{}].

    If size is not None, it can be an int, a tuple of one int, a tuple of two ints,
    or a set. If it's an int, the array must have exactly that many elements. If it's
    a tuple of one int, it's the minimum length. If it's a tuple of two ints, they
    are the minimum and the maximum lengths. If it's a set, it's the set of sizes that
    are valid - e.g. for {2, 4}, the array can be either 2 or 4 elements long.
    """

    if not validate_item:
        validate_item = lambda x: x
    elif isinstance(validate_item, type) or isinstance(validate_item, tuple):
        validate_item = of_type(validate_item)

    if size is None:
        validate_size = lambda _: True
    elif isinstance(size, set):
        size = {operator.index(n) for n in size}
        validate_size = lambda value: (
            len(value) in size
            or "must have {0} elements".format(
                " or ".join(str(n) for n in sorted(size))
            )
        )
    elif isinstance(size, tuple):
        assert 1 <= len(size) <= 2
        size = tuple(operator.index(n) for n in size)
        min_len, max_len = (size + (None,))[0:2]
        validate_size = lambda value: (
            "must have at least {0} elements".format(min_len)
            if len(value) < min_len
            else "must have at most {0} elements".format(max_len)
            if max_len is not None and len(value) < max_len
            else True
        )
    else:
        size = operator.index(size)
        validate_size = lambda value: (
            len(value) == size or "must have {0} elements".format(size)
        )

    def validate(value):
        if value == ():
            value = []
        elif vectorize and not isinstance(value, (list, dict)):
            value = [value]

        of_type(list)(value)

        size_err = validate_size(value)  # True if valid, str if error
        if size_err is not True:
            raise ValueError(size_err)

        for i, item in enumerate(value):
            try:
                value[i] = validate_item(item)
            except (TypeError, ValueError) as exc:
                raise type(exc)(f"[{repr(i)}] {exc}")
        return value

    return validate


def object(validate_value=False):
    """Returns a validator for a JSON object.

    If the property is missing, it is treated as if it were {}. Otherwise, it must
    be a dict.

    If validate_value=False, it's treated as if it were (lambda x: x) - i.e. any
    value is considered valid, and is unchanged. If validate_value is a type or a
    tuple, it's treated as if it were json.of_type(validate_value).

    Every value in the dict is replaced with validate_value(value) in-place, propagating
    any exceptions raised by the latter. If validate_value is a type or a tuple, it is
    treated as if it were json.of_type(validate_value). Keys are not affected.
    """

    if isinstance(validate_value, type) or isinstance(validate_value, tuple):
        validate_value = of_type(validate_value)

    def validate(value):
        if value == ():
            return {}

        of_type(dict)(value)
        if validate_value:
            for k, v in value.items():
                try:
                    value[k] = validate_value(v)
                except (TypeError, ValueError) as exc:
                    raise type(exc)(f"[{repr(k)}] {exc}")
        return value

    return validate


def repr(value):
    return JsonObject(value)


dumps = json.dumps
loads = json.loads