• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2"""Extra types understood by apitools.
3
4This file will be replaced by a .proto file when we switch to proto2
5from protorpc.
6"""
7
8import collections
9import datetime
10import json
11import numbers
12
13from protorpc import message_types
14from protorpc import messages
15from protorpc import protojson
16import six
17
18from apitools.base.py import encoding
19from apitools.base.py import exceptions
20from apitools.base.py import util
21
22__all__ = [
23    'DateField',
24    'DateTimeMessage',
25    'JsonArray',
26    'JsonObject',
27    'JsonValue',
28    'JsonProtoEncoder',
29    'JsonProtoDecoder',
30]
31
32# We import from protorpc.
33# pylint:disable=invalid-name
34DateTimeMessage = message_types.DateTimeMessage
35# pylint:enable=invalid-name
36
37
38class DateField(messages.Field):
39
40    """Field definition for Date values."""
41
42    # We insert our own metaclass here to avoid letting ProtoRPC
43    # register this as the default field type for strings.
44    #  * since ProtoRPC does this via metaclasses, we don't have any
45    #    choice but to use one ourselves
46    #  * since a subclass's metaclass must inherit from its superclass's
47    #    metaclass, we're forced to have this hard-to-read inheritance.
48    #
49    # pylint: disable=invalid-name
50    class __metaclass__(messages.Field.__metaclass__):
51
52        def __init__(cls, name, bases, dct):
53            super(messages.Field.__metaclass__, cls).__init__(name, bases, dct)
54    # pylint: enable=invalid-name
55
56    VARIANTS = frozenset([messages.Variant.STRING])
57    DEFAULT_VARIANT = messages.Variant.STRING
58    type = datetime.date
59
60
61def _ValidateJsonValue(json_value):
62    entries = [(f, json_value.get_assigned_value(f.name))
63               for f in json_value.all_fields()]
64    assigned_entries = [(f, value)
65                        for f, value in entries if value is not None]
66    if len(assigned_entries) != 1:
67        raise exceptions.InvalidDataError(
68            'Malformed JsonValue: %s' % json_value)
69
70
71def _JsonValueToPythonValue(json_value):
72    """Convert the given JsonValue to a json string."""
73    util.Typecheck(json_value, JsonValue)
74    _ValidateJsonValue(json_value)
75    if json_value.is_null:
76        return None
77    entries = [(f, json_value.get_assigned_value(f.name))
78               for f in json_value.all_fields()]
79    assigned_entries = [(f, value)
80                        for f, value in entries if value is not None]
81    field, value = assigned_entries[0]
82    if not isinstance(field, messages.MessageField):
83        return value
84    elif field.message_type is JsonObject:
85        return _JsonObjectToPythonValue(value)
86    elif field.message_type is JsonArray:
87        return _JsonArrayToPythonValue(value)
88
89
90def _JsonObjectToPythonValue(json_value):
91    util.Typecheck(json_value, JsonObject)
92    return dict([(prop.key, _JsonValueToPythonValue(prop.value)) for prop
93                 in json_value.properties])
94
95
96def _JsonArrayToPythonValue(json_value):
97    util.Typecheck(json_value, JsonArray)
98    return [_JsonValueToPythonValue(e) for e in json_value.entries]
99
100
101_MAXINT64 = 2 << 63 - 1
102_MININT64 = -(2 << 63)
103
104
105def _PythonValueToJsonValue(py_value):
106    """Convert the given python value to a JsonValue."""
107    if py_value is None:
108        return JsonValue(is_null=True)
109    if isinstance(py_value, bool):
110        return JsonValue(boolean_value=py_value)
111    if isinstance(py_value, six.string_types):
112        return JsonValue(string_value=py_value)
113    if isinstance(py_value, numbers.Number):
114        if isinstance(py_value, six.integer_types):
115            if _MININT64 < py_value < _MAXINT64:
116                return JsonValue(integer_value=py_value)
117        return JsonValue(double_value=float(py_value))
118    if isinstance(py_value, dict):
119        return JsonValue(object_value=_PythonValueToJsonObject(py_value))
120    if isinstance(py_value, collections.Iterable):
121        return JsonValue(array_value=_PythonValueToJsonArray(py_value))
122    raise exceptions.InvalidDataError(
123        'Cannot convert "%s" to JsonValue' % py_value)
124
125
126def _PythonValueToJsonObject(py_value):
127    util.Typecheck(py_value, dict)
128    return JsonObject(
129        properties=[
130            JsonObject.Property(key=key, value=_PythonValueToJsonValue(value))
131            for key, value in py_value.items()])
132
133
134def _PythonValueToJsonArray(py_value):
135    return JsonArray(entries=list(map(_PythonValueToJsonValue, py_value)))
136
137
138class JsonValue(messages.Message):
139
140    """Any valid JSON value."""
141    # Is this JSON object `null`?
142    is_null = messages.BooleanField(1, default=False)
143
144    # Exactly one of the following is provided if is_null is False; none
145    # should be provided if is_null is True.
146    boolean_value = messages.BooleanField(2)
147    string_value = messages.StringField(3)
148    # We keep two numeric fields to keep int64 round-trips exact.
149    double_value = messages.FloatField(4, variant=messages.Variant.DOUBLE)
150    integer_value = messages.IntegerField(5, variant=messages.Variant.INT64)
151    # Compound types
152    object_value = messages.MessageField('JsonObject', 6)
153    array_value = messages.MessageField('JsonArray', 7)
154
155
156class JsonObject(messages.Message):
157
158    """A JSON object value.
159
160    Messages:
161      Property: A property of a JsonObject.
162
163    Fields:
164      properties: A list of properties of a JsonObject.
165    """
166
167    class Property(messages.Message):
168
169        """A property of a JSON object.
170
171        Fields:
172          key: Name of the property.
173          value: A JsonValue attribute.
174        """
175        key = messages.StringField(1)
176        value = messages.MessageField(JsonValue, 2)
177
178    properties = messages.MessageField(Property, 1, repeated=True)
179
180
181class JsonArray(messages.Message):
182
183    """A JSON array value."""
184    entries = messages.MessageField(JsonValue, 1, repeated=True)
185
186
187_JSON_PROTO_TO_PYTHON_MAP = {
188    JsonArray: _JsonArrayToPythonValue,
189    JsonObject: _JsonObjectToPythonValue,
190    JsonValue: _JsonValueToPythonValue,
191}
192_JSON_PROTO_TYPES = tuple(_JSON_PROTO_TO_PYTHON_MAP.keys())
193
194
195def _JsonProtoToPythonValue(json_proto):
196    util.Typecheck(json_proto, _JSON_PROTO_TYPES)
197    return _JSON_PROTO_TO_PYTHON_MAP[type(json_proto)](json_proto)
198
199
200def _PythonValueToJsonProto(py_value):
201    if isinstance(py_value, dict):
202        return _PythonValueToJsonObject(py_value)
203    if (isinstance(py_value, collections.Iterable) and
204            not isinstance(py_value, six.string_types)):
205        return _PythonValueToJsonArray(py_value)
206    return _PythonValueToJsonValue(py_value)
207
208
209def _JsonProtoToJson(json_proto, unused_encoder=None):
210    return json.dumps(_JsonProtoToPythonValue(json_proto))
211
212
213def _JsonToJsonProto(json_data, unused_decoder=None):
214    return _PythonValueToJsonProto(json.loads(json_data))
215
216
217def _JsonToJsonValue(json_data, unused_decoder=None):
218    result = _PythonValueToJsonProto(json.loads(json_data))
219    if isinstance(result, JsonValue):
220        return result
221    elif isinstance(result, JsonObject):
222        return JsonValue(object_value=result)
223    elif isinstance(result, JsonArray):
224        return JsonValue(array_value=result)
225    else:
226        raise exceptions.InvalidDataError(
227            'Malformed JsonValue: %s' % json_data)
228
229
230# pylint:disable=invalid-name
231JsonProtoEncoder = _JsonProtoToJson
232JsonProtoDecoder = _JsonToJsonProto
233# pylint:enable=invalid-name
234encoding.RegisterCustomMessageCodec(
235    encoder=JsonProtoEncoder, decoder=_JsonToJsonValue)(JsonValue)
236encoding.RegisterCustomMessageCodec(
237    encoder=JsonProtoEncoder, decoder=JsonProtoDecoder)(JsonObject)
238encoding.RegisterCustomMessageCodec(
239    encoder=JsonProtoEncoder, decoder=JsonProtoDecoder)(JsonArray)
240
241
242def _EncodeDateTimeField(field, value):
243    result = protojson.ProtoJson().encode_field(field, value)
244    return encoding.CodecResult(value=result, complete=True)
245
246
247def _DecodeDateTimeField(unused_field, value):
248    result = protojson.ProtoJson().decode_field(
249        message_types.DateTimeField(1), value)
250    return encoding.CodecResult(value=result, complete=True)
251
252
253encoding.RegisterFieldTypeCodec(_EncodeDateTimeField, _DecodeDateTimeField)(
254    message_types.DateTimeField)
255
256
257def _EncodeInt64Field(field, value):
258    """Handle the special case of int64 as a string."""
259    capabilities = [
260        messages.Variant.INT64,
261        messages.Variant.UINT64,
262    ]
263    if field.variant not in capabilities:
264        return encoding.CodecResult(value=value, complete=False)
265
266    if field.repeated:
267        result = [str(x) for x in value]
268    else:
269        result = str(value)
270    return encoding.CodecResult(value=result, complete=True)
271
272
273def _DecodeInt64Field(unused_field, value):
274    # Don't need to do anything special, they're decoded just fine
275    return encoding.CodecResult(value=value, complete=False)
276
277encoding.RegisterFieldTypeCodec(_EncodeInt64Field, _DecodeInt64Field)(
278    messages.IntegerField)
279
280
281def _EncodeDateField(field, value):
282    """Encoder for datetime.date objects."""
283    if field.repeated:
284        result = [d.isoformat() for d in value]
285    else:
286        result = value.isoformat()
287    return encoding.CodecResult(value=result, complete=True)
288
289
290def _DecodeDateField(unused_field, value):
291    date = datetime.datetime.strptime(value, '%Y-%m-%d').date()
292    return encoding.CodecResult(value=date, complete=True)
293
294encoding.RegisterFieldTypeCodec(_EncodeDateField, _DecodeDateField)(DateField)
295