• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2018 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Common code for converting proto to other formats, such as JSON."""
18
19import base64
20import collections
21import datetime
22import json
23
24import six
25
26from apitools.base.protorpclite import message_types
27from apitools.base.protorpclite import messages
28from apitools.base.protorpclite import protojson
29from apitools.base.py import exceptions
30
31
32_Codec = collections.namedtuple('_Codec', ['encoder', 'decoder'])
33CodecResult = collections.namedtuple('CodecResult', ['value', 'complete'])
34
35
36class EdgeType(object):
37    """The type of transition made by an edge."""
38    SCALAR = 1
39    REPEATED = 2
40    MAP = 3
41
42
43class ProtoEdge(collections.namedtuple('ProtoEdge',
44                                       ['type_', 'field', 'index'])):
45    """A description of a one-level transition from a message to a value.
46
47    Protobuf messages can be arbitrarily nested as fields can be defined with
48    any "message" type. This nesting property means that there are often many
49    levels of proto messages within a single message instance. This class can
50    unambiguously describe a single step from a message to some nested value.
51
52    Properties:
53      type_: EdgeType, The type of transition represented by this edge.
54      field: str, The name of the message-typed field.
55      index: Any, Additional data needed to make the transition. The semantics
56          of the "index" property change based on the value of "type_":
57            SCALAR: ignored.
58            REPEATED: a numeric index into "field"'s list.
59            MAP: a key into "field"'s mapping.
60    """
61    __slots__ = ()
62
63    def __str__(self):
64        if self.type_ == EdgeType.SCALAR:
65            return self.field
66        else:
67            return '{}[{}]'.format(self.field, self.index)
68
69
70# TODO(craigcitro): Make these non-global.
71_UNRECOGNIZED_FIELD_MAPPINGS = {}
72_CUSTOM_MESSAGE_CODECS = {}
73_CUSTOM_FIELD_CODECS = {}
74_FIELD_TYPE_CODECS = {}
75
76
77def MapUnrecognizedFields(field_name):
78    """Register field_name as a container for unrecognized fields."""
79    def Register(cls):
80        _UNRECOGNIZED_FIELD_MAPPINGS[cls] = field_name
81        return cls
82    return Register
83
84
85def RegisterCustomMessageCodec(encoder, decoder):
86    """Register a custom encoder/decoder for this message class."""
87    def Register(cls):
88        _CUSTOM_MESSAGE_CODECS[cls] = _Codec(encoder=encoder, decoder=decoder)
89        return cls
90    return Register
91
92
93def RegisterCustomFieldCodec(encoder, decoder):
94    """Register a custom encoder/decoder for this field."""
95    def Register(field):
96        _CUSTOM_FIELD_CODECS[field] = _Codec(encoder=encoder, decoder=decoder)
97        return field
98    return Register
99
100
101def RegisterFieldTypeCodec(encoder, decoder):
102    """Register a custom encoder/decoder for all fields of this type."""
103    def Register(field_type):
104        _FIELD_TYPE_CODECS[field_type] = _Codec(
105            encoder=encoder, decoder=decoder)
106        return field_type
107    return Register
108
109
110def CopyProtoMessage(message):
111    """Make a deep copy of a message."""
112    return JsonToMessage(type(message), MessageToJson(message))
113
114
115def MessageToJson(message, include_fields=None):
116    """Convert the given message to JSON."""
117    result = _ProtoJsonApiTools.Get().encode_message(message)
118    return _IncludeFields(result, message, include_fields)
119
120
121def JsonToMessage(message_type, message):
122    """Convert the given JSON to a message of type message_type."""
123    return _ProtoJsonApiTools.Get().decode_message(message_type, message)
124
125
126# TODO(craigcitro): Do this directly, instead of via JSON.
127def DictToMessage(d, message_type):
128    """Convert the given dictionary to a message of type message_type."""
129    return JsonToMessage(message_type, json.dumps(d))
130
131
132def MessageToDict(message):
133    """Convert the given message to a dictionary."""
134    return json.loads(MessageToJson(message))
135
136
137def DictToAdditionalPropertyMessage(properties, additional_property_type,
138                                    sort_items=False):
139    """Convert the given dictionary to an AdditionalProperty message."""
140    items = properties.items()
141    if sort_items:
142        items = sorted(items)
143    map_ = []
144    for key, value in items:
145        map_.append(additional_property_type.AdditionalProperty(
146            key=key, value=value))
147    return additional_property_type(additionalProperties=map_)
148
149
150def PyValueToMessage(message_type, value):
151    """Convert the given python value to a message of type message_type."""
152    return JsonToMessage(message_type, json.dumps(value))
153
154
155def MessageToPyValue(message):
156    """Convert the given message to a python value."""
157    return json.loads(MessageToJson(message))
158
159
160def MessageToRepr(msg, multiline=False, **kwargs):
161    """Return a repr-style string for a protorpc message.
162
163    protorpc.Message.__repr__ does not return anything that could be considered
164    python code. Adding this function lets us print a protorpc message in such
165    a way that it could be pasted into code later, and used to compare against
166    other things.
167
168    Args:
169      msg: protorpc.Message, the message to be repr'd.
170      multiline: bool, True if the returned string should have each field
171          assignment on its own line.
172      **kwargs: {str:str}, Additional flags for how to format the string.
173
174    Known **kwargs:
175      shortstrings: bool, True if all string values should be
176          truncated at 100 characters, since when mocking the contents
177          typically don't matter except for IDs, and IDs are usually
178          less than 100 characters.
179      no_modules: bool, True if the long module name should not be printed with
180          each type.
181
182    Returns:
183      str, A string of valid python (assuming the right imports have been made)
184      that recreates the message passed into this function.
185
186    """
187
188    # TODO(jasmuth): craigcitro suggests a pretty-printer from apitools/gen.
189
190    indent = kwargs.get('indent', 0)
191
192    def IndentKwargs(kwargs):
193        kwargs = dict(kwargs)
194        kwargs['indent'] = kwargs.get('indent', 0) + 4
195        return kwargs
196
197    if isinstance(msg, list):
198        s = '['
199        for item in msg:
200            if multiline:
201                s += '\n' + ' ' * (indent + 4)
202            s += MessageToRepr(
203                item, multiline=multiline, **IndentKwargs(kwargs)) + ','
204        if multiline:
205            s += '\n' + ' ' * indent
206        s += ']'
207        return s
208
209    if isinstance(msg, messages.Message):
210        s = type(msg).__name__ + '('
211        if not kwargs.get('no_modules'):
212            s = msg.__module__ + '.' + s
213        names = sorted([field.name for field in msg.all_fields()])
214        for name in names:
215            field = msg.field_by_name(name)
216            if multiline:
217                s += '\n' + ' ' * (indent + 4)
218            value = getattr(msg, field.name)
219            s += field.name + '=' + MessageToRepr(
220                value, multiline=multiline, **IndentKwargs(kwargs)) + ','
221        if multiline:
222            s += '\n' + ' ' * indent
223        s += ')'
224        return s
225
226    if isinstance(msg, six.string_types):
227        if kwargs.get('shortstrings') and len(msg) > 100:
228            msg = msg[:100]
229
230    if isinstance(msg, datetime.datetime):
231
232        class SpecialTZInfo(datetime.tzinfo):
233
234            def __init__(self, offset):
235                super(SpecialTZInfo, self).__init__()
236                self.offset = offset
237
238            def __repr__(self):
239                s = 'TimeZoneOffset(' + repr(self.offset) + ')'
240                if not kwargs.get('no_modules'):
241                    s = 'apitools.base.protorpclite.util.' + s
242                return s
243
244        msg = datetime.datetime(
245            msg.year, msg.month, msg.day, msg.hour, msg.minute, msg.second,
246            msg.microsecond, SpecialTZInfo(msg.tzinfo.utcoffset(0)))
247
248    return repr(msg)
249
250
251def _GetField(message, field_path):
252    for field in field_path:
253        if field not in dir(message):
254            raise KeyError('no field "%s"' % field)
255        message = getattr(message, field)
256    return message
257
258
259def _SetField(dictblob, field_path, value):
260    for field in field_path[:-1]:
261        dictblob = dictblob.setdefault(field, {})
262    dictblob[field_path[-1]] = value
263
264
265def _IncludeFields(encoded_message, message, include_fields):
266    """Add the requested fields to the encoded message."""
267    if include_fields is None:
268        return encoded_message
269    result = json.loads(encoded_message)
270    for field_name in include_fields:
271        try:
272            value = _GetField(message, field_name.split('.'))
273            nullvalue = None
274            if isinstance(value, list):
275                nullvalue = []
276        except KeyError:
277            raise exceptions.InvalidDataError(
278                'No field named %s in message of type %s' % (
279                    field_name, type(message)))
280        _SetField(result, field_name.split('.'), nullvalue)
281    return json.dumps(result)
282
283
284def _GetFieldCodecs(field, attr):
285    result = [
286        getattr(_CUSTOM_FIELD_CODECS.get(field), attr, None),
287        getattr(_FIELD_TYPE_CODECS.get(type(field)), attr, None),
288    ]
289    return [x for x in result if x is not None]
290
291
292class _ProtoJsonApiTools(protojson.ProtoJson):
293
294    """JSON encoder used by apitools clients."""
295    _INSTANCE = None
296
297    @classmethod
298    def Get(cls):
299        if cls._INSTANCE is None:
300            cls._INSTANCE = cls()
301        return cls._INSTANCE
302
303    def decode_message(self, message_type, encoded_message):
304        if message_type in _CUSTOM_MESSAGE_CODECS:
305            return _CUSTOM_MESSAGE_CODECS[
306                message_type].decoder(encoded_message)
307        result = _DecodeCustomFieldNames(message_type, encoded_message)
308        result = super(_ProtoJsonApiTools, self).decode_message(
309            message_type, result)
310        result = _ProcessUnknownEnums(result, encoded_message)
311        result = _ProcessUnknownMessages(result, encoded_message)
312        return _DecodeUnknownFields(result, encoded_message)
313
314    def decode_field(self, field, value):
315        """Decode the given JSON value.
316
317        Args:
318          field: a messages.Field for the field we're decoding.
319          value: a python value we'd like to decode.
320
321        Returns:
322          A value suitable for assignment to field.
323        """
324        for decoder in _GetFieldCodecs(field, 'decoder'):
325            result = decoder(field, value)
326            value = result.value
327            if result.complete:
328                return value
329        if isinstance(field, messages.MessageField):
330            field_value = self.decode_message(
331                field.message_type, json.dumps(value))
332        elif isinstance(field, messages.EnumField):
333            value = GetCustomJsonEnumMapping(
334                field.type, json_name=value) or value
335            try:
336                field_value = super(
337                    _ProtoJsonApiTools, self).decode_field(field, value)
338            except messages.DecodeError:
339                if not isinstance(value, six.string_types):
340                    raise
341                field_value = None
342        else:
343            field_value = super(
344                _ProtoJsonApiTools, self).decode_field(field, value)
345        return field_value
346
347    def encode_message(self, message):
348        if isinstance(message, messages.FieldList):
349            return '[%s]' % (', '.join(self.encode_message(x)
350                                       for x in message))
351
352        # pylint: disable=unidiomatic-typecheck
353        if type(message) in _CUSTOM_MESSAGE_CODECS:
354            return _CUSTOM_MESSAGE_CODECS[type(message)].encoder(message)
355
356        message = _EncodeUnknownFields(message)
357        result = super(_ProtoJsonApiTools, self).encode_message(message)
358        result = _EncodeCustomFieldNames(message, result)
359        return json.dumps(json.loads(result), sort_keys=True)
360
361    def encode_field(self, field, value):
362        """Encode the given value as JSON.
363
364        Args:
365          field: a messages.Field for the field we're encoding.
366          value: a value for field.
367
368        Returns:
369          A python value suitable for json.dumps.
370        """
371        for encoder in _GetFieldCodecs(field, 'encoder'):
372            result = encoder(field, value)
373            value = result.value
374            if result.complete:
375                return value
376        if isinstance(field, messages.EnumField):
377            if field.repeated:
378                remapped_value = [GetCustomJsonEnumMapping(
379                    field.type, python_name=e.name) or e.name for e in value]
380            else:
381                remapped_value = GetCustomJsonEnumMapping(
382                    field.type, python_name=value.name)
383            if remapped_value:
384                return remapped_value
385        if (isinstance(field, messages.MessageField) and
386                not isinstance(field, message_types.DateTimeField)):
387            value = json.loads(self.encode_message(value))
388        return super(_ProtoJsonApiTools, self).encode_field(field, value)
389
390
391# TODO(craigcitro): Fold this and _IncludeFields in as codecs.
392def _DecodeUnknownFields(message, encoded_message):
393    """Rewrite unknown fields in message into message.destination."""
394    destination = _UNRECOGNIZED_FIELD_MAPPINGS.get(type(message))
395    if destination is None:
396        return message
397    pair_field = message.field_by_name(destination)
398    if not isinstance(pair_field, messages.MessageField):
399        raise exceptions.InvalidDataFromServerError(
400            'Unrecognized fields must be mapped to a compound '
401            'message type.')
402    pair_type = pair_field.message_type
403    # TODO(craigcitro): Add more error checking around the pair
404    # type being exactly what we suspect (field names, etc).
405    if isinstance(pair_type.value, messages.MessageField):
406        new_values = _DecodeUnknownMessages(
407            message, json.loads(encoded_message), pair_type)
408    else:
409        new_values = _DecodeUnrecognizedFields(message, pair_type)
410    setattr(message, destination, new_values)
411    # We could probably get away with not setting this, but
412    # why not clear it?
413    setattr(message, '_Message__unrecognized_fields', {})
414    return message
415
416
417def _DecodeUnknownMessages(message, encoded_message, pair_type):
418    """Process unknown fields in encoded_message of a message type."""
419    field_type = pair_type.value.type
420    new_values = []
421    all_field_names = [x.name for x in message.all_fields()]
422    for name, value_dict in six.iteritems(encoded_message):
423        if name in all_field_names:
424            continue
425        value = PyValueToMessage(field_type, value_dict)
426        if pair_type.value.repeated:
427            value = _AsMessageList(value)
428        new_pair = pair_type(key=name, value=value)
429        new_values.append(new_pair)
430    return new_values
431
432
433def _DecodeUnrecognizedFields(message, pair_type):
434    """Process unrecognized fields in message."""
435    new_values = []
436    codec = _ProtoJsonApiTools.Get()
437    for unknown_field in message.all_unrecognized_fields():
438        # TODO(craigcitro): Consider validating the variant if
439        # the assignment below doesn't take care of it. It may
440        # also be necessary to check it in the case that the
441        # type has multiple encodings.
442        value, _ = message.get_unrecognized_field_info(unknown_field)
443        value_type = pair_type.field_by_name('value')
444        if isinstance(value_type, messages.MessageField):
445            decoded_value = DictToMessage(value, pair_type.value.message_type)
446        else:
447            decoded_value = codec.decode_field(
448                pair_type.value, value)
449        try:
450            new_pair_key = str(unknown_field)
451        except UnicodeEncodeError:
452            new_pair_key = protojson.ProtoJson().decode_field(
453                pair_type.key, unknown_field)
454        new_pair = pair_type(key=new_pair_key, value=decoded_value)
455        new_values.append(new_pair)
456    return new_values
457
458
459def _CopyProtoMessageVanillaProtoJson(message):
460    codec = protojson.ProtoJson()
461    return codec.decode_message(type(message), codec.encode_message(message))
462
463
464def _EncodeUnknownFields(message):
465    """Remap unknown fields in message out of message.source."""
466    source = _UNRECOGNIZED_FIELD_MAPPINGS.get(type(message))
467    if source is None:
468        return message
469    # CopyProtoMessage uses _ProtoJsonApiTools, which uses this message. Use
470    # the vanilla protojson-based copy function to avoid infinite recursion.
471    result = _CopyProtoMessageVanillaProtoJson(message)
472    pairs_field = message.field_by_name(source)
473    if not isinstance(pairs_field, messages.MessageField):
474        raise exceptions.InvalidUserInputError(
475            'Invalid pairs field %s' % pairs_field)
476    pairs_type = pairs_field.message_type
477    value_field = pairs_type.field_by_name('value')
478    value_variant = value_field.variant
479    pairs = getattr(message, source)
480    codec = _ProtoJsonApiTools.Get()
481    for pair in pairs:
482        encoded_value = codec.encode_field(value_field, pair.value)
483        result.set_unrecognized_field(pair.key, encoded_value, value_variant)
484    setattr(result, source, [])
485    return result
486
487
488def _SafeEncodeBytes(field, value):
489    """Encode the bytes in value as urlsafe base64."""
490    try:
491        if field.repeated:
492            result = [base64.urlsafe_b64encode(byte) for byte in value]
493        else:
494            result = base64.urlsafe_b64encode(value)
495        complete = True
496    except TypeError:
497        result = value
498        complete = False
499    return CodecResult(value=result, complete=complete)
500
501
502def _SafeDecodeBytes(unused_field, value):
503    """Decode the urlsafe base64 value into bytes."""
504    try:
505        result = base64.urlsafe_b64decode(str(value))
506        complete = True
507    except TypeError:
508        result = value
509        complete = False
510    return CodecResult(value=result, complete=complete)
511
512
513def _ProcessUnknownEnums(message, encoded_message):
514    """Add unknown enum values from encoded_message as unknown fields.
515
516    ProtoRPC diverges from the usual protocol buffer behavior here and
517    doesn't allow unknown fields. Throwing on unknown fields makes it
518    impossible to let servers add new enum values and stay compatible
519    with older clients, which isn't reasonable for us. We simply store
520    unrecognized enum values as unknown fields, and all is well.
521
522    Args:
523      message: Proto message we've decoded thus far.
524      encoded_message: JSON string we're decoding.
525
526    Returns:
527      message, with any unknown enums stored as unrecognized fields.
528    """
529    if not encoded_message:
530        return message
531    decoded_message = json.loads(six.ensure_str(encoded_message))
532    for field in message.all_fields():
533        if (isinstance(field, messages.EnumField) and
534                field.name in decoded_message):
535            value = message.get_assigned_value(field.name)
536            if ((field.repeated and len(value) != len(decoded_message[field.name])) or
537                    value is None):
538                message.set_unrecognized_field(
539                    field.name, decoded_message[field.name], messages.Variant.ENUM)
540    return message
541
542
543def _ProcessUnknownMessages(message, encoded_message):
544    """Store any remaining unknown fields as strings.
545
546    ProtoRPC currently ignores unknown values for which no type can be
547    determined (and logs a "No variant found" message). For the purposes
548    of reserializing, this is quite harmful (since it throws away
549    information). Here we simply add those as unknown fields of type
550    string (so that they can easily be reserialized).
551
552    Args:
553      message: Proto message we've decoded thus far.
554      encoded_message: JSON string we're decoding.
555
556    Returns:
557      message, with any remaining unrecognized fields saved.
558    """
559    if not encoded_message:
560        return message
561    decoded_message = json.loads(six.ensure_str(encoded_message))
562    message_fields = [x.name for x in message.all_fields()] + list(
563        message.all_unrecognized_fields())
564    missing_fields = [x for x in decoded_message.keys()
565                      if x not in message_fields]
566    for field_name in missing_fields:
567        message.set_unrecognized_field(field_name, decoded_message[field_name],
568                                       messages.Variant.STRING)
569    return message
570
571
572RegisterFieldTypeCodec(_SafeEncodeBytes, _SafeDecodeBytes)(messages.BytesField)
573
574
575# Note that these could share a dictionary, since they're keyed by
576# distinct types, but it's not really worth it.
577_JSON_ENUM_MAPPINGS = {}
578_JSON_FIELD_MAPPINGS = {}
579
580
581def AddCustomJsonEnumMapping(enum_type, python_name, json_name,
582                             package=None):  # pylint: disable=unused-argument
583    """Add a custom wire encoding for a given enum value.
584
585    This is primarily used in generated code, to handle enum values
586    which happen to be Python keywords.
587
588    Args:
589      enum_type: (messages.Enum) An enum type
590      python_name: (basestring) Python name for this value.
591      json_name: (basestring) JSON name to be used on the wire.
592      package: (NoneType, optional) No effect, exists for legacy compatibility.
593    """
594    if not issubclass(enum_type, messages.Enum):
595        raise exceptions.TypecheckError(
596            'Cannot set JSON enum mapping for non-enum "%s"' % enum_type)
597    if python_name not in enum_type.names():
598        raise exceptions.InvalidDataError(
599            'Enum value %s not a value for type %s' % (python_name, enum_type))
600    field_mappings = _JSON_ENUM_MAPPINGS.setdefault(enum_type, {})
601    _CheckForExistingMappings('enum', enum_type, python_name, json_name)
602    field_mappings[python_name] = json_name
603
604
605def AddCustomJsonFieldMapping(message_type, python_name, json_name,
606                              package=None):  # pylint: disable=unused-argument
607    """Add a custom wire encoding for a given message field.
608
609    This is primarily used in generated code, to handle enum values
610    which happen to be Python keywords.
611
612    Args:
613      message_type: (messages.Message) A message type
614      python_name: (basestring) Python name for this value.
615      json_name: (basestring) JSON name to be used on the wire.
616      package: (NoneType, optional) No effect, exists for legacy compatibility.
617    """
618    if not issubclass(message_type, messages.Message):
619        raise exceptions.TypecheckError(
620            'Cannot set JSON field mapping for '
621            'non-message "%s"' % message_type)
622    try:
623        _ = message_type.field_by_name(python_name)
624    except KeyError:
625        raise exceptions.InvalidDataError(
626            'Field %s not recognized for type %s' % (
627                python_name, message_type))
628    field_mappings = _JSON_FIELD_MAPPINGS.setdefault(message_type, {})
629    _CheckForExistingMappings('field', message_type, python_name, json_name)
630    field_mappings[python_name] = json_name
631
632
633def GetCustomJsonEnumMapping(enum_type, python_name=None, json_name=None):
634    """Return the appropriate remapping for the given enum, or None."""
635    return _FetchRemapping(enum_type, 'enum',
636                           python_name=python_name, json_name=json_name,
637                           mappings=_JSON_ENUM_MAPPINGS)
638
639
640def GetCustomJsonFieldMapping(message_type, python_name=None, json_name=None):
641    """Return the appropriate remapping for the given field, or None."""
642    return _FetchRemapping(message_type, 'field',
643                           python_name=python_name, json_name=json_name,
644                           mappings=_JSON_FIELD_MAPPINGS)
645
646
647def _FetchRemapping(type_name, mapping_type, python_name=None, json_name=None,
648                    mappings=None):
649    """Common code for fetching a key or value from a remapping dict."""
650    if python_name and json_name:
651        raise exceptions.InvalidDataError(
652            'Cannot specify both python_name and json_name '
653            'for %s remapping' % mapping_type)
654    if not (python_name or json_name):
655        raise exceptions.InvalidDataError(
656            'Must specify either python_name or json_name for %s remapping' % (
657                mapping_type,))
658    field_remappings = mappings.get(type_name, {})
659    if field_remappings:
660        if python_name:
661            return field_remappings.get(python_name)
662        elif json_name:
663            if json_name in list(field_remappings.values()):
664                return [k for k in field_remappings
665                        if field_remappings[k] == json_name][0]
666    return None
667
668
669def _CheckForExistingMappings(mapping_type, message_type,
670                              python_name, json_name):
671    """Validate that no mappings exist for the given values."""
672    if mapping_type == 'field':
673        getter = GetCustomJsonFieldMapping
674    elif mapping_type == 'enum':
675        getter = GetCustomJsonEnumMapping
676    remapping = getter(message_type, python_name=python_name)
677    if remapping is not None and remapping != json_name:
678        raise exceptions.InvalidDataError(
679            'Cannot add mapping for %s "%s", already mapped to "%s"' % (
680                mapping_type, python_name, remapping))
681    remapping = getter(message_type, json_name=json_name)
682    if remapping is not None and remapping != python_name:
683        raise exceptions.InvalidDataError(
684            'Cannot add mapping for %s "%s", already mapped to "%s"' % (
685                mapping_type, json_name, remapping))
686
687
688def _EncodeCustomFieldNames(message, encoded_value):
689    field_remappings = list(_JSON_FIELD_MAPPINGS.get(type(message), {})
690                            .items())
691    if field_remappings:
692        decoded_value = json.loads(encoded_value)
693        for python_name, json_name in field_remappings:
694            if python_name in encoded_value:
695                decoded_value[json_name] = decoded_value.pop(python_name)
696        encoded_value = json.dumps(decoded_value)
697    return encoded_value
698
699
700def _DecodeCustomFieldNames(message_type, encoded_message):
701    field_remappings = _JSON_FIELD_MAPPINGS.get(message_type, {})
702    if field_remappings:
703        decoded_message = json.loads(encoded_message)
704        for python_name, json_name in list(field_remappings.items()):
705            if json_name in decoded_message:
706                decoded_message[python_name] = decoded_message.pop(json_name)
707        encoded_message = json.dumps(decoded_message)
708    return encoded_message
709
710
711def _AsMessageList(msg):
712    """Convert the provided list-as-JsonValue to a list."""
713    # This really needs to live in extra_types, but extra_types needs
714    # to import this file to be able to register codecs.
715    # TODO(craigcitro): Split out a codecs module and fix this ugly
716    # import.
717    from apitools.base.py import extra_types
718
719    def _IsRepeatedJsonValue(msg):
720        """Return True if msg is a repeated value as a JsonValue."""
721        if isinstance(msg, extra_types.JsonArray):
722            return True
723        if isinstance(msg, extra_types.JsonValue) and msg.array_value:
724            return True
725        return False
726
727    if not _IsRepeatedJsonValue(msg):
728        raise ValueError('invalid argument to _AsMessageList')
729    if isinstance(msg, extra_types.JsonValue):
730        msg = msg.array_value
731    if isinstance(msg, extra_types.JsonArray):
732        msg = msg.entries
733    return msg
734
735
736def _IsMap(message, field):
737    """Returns whether the "field" is actually a map-type."""
738    value = message.get_assigned_value(field.name)
739    if not isinstance(value, messages.Message):
740        return False
741    try:
742        additional_properties = value.field_by_name('additionalProperties')
743    except KeyError:
744        return False
745    else:
746        return additional_properties.repeated
747
748
749def _MapItems(message, field):
750    """Yields the (key, value) pair of the map values."""
751    assert _IsMap(message, field)
752    map_message = message.get_assigned_value(field.name)
753    additional_properties = map_message.get_assigned_value(
754        'additionalProperties')
755    for kv_pair in additional_properties:
756        yield kv_pair.key, kv_pair.value
757
758
759def UnrecognizedFieldIter(message, _edges=()):  # pylint: disable=invalid-name
760    """Yields the locations of unrecognized fields within "message".
761
762    If a sub-message is found to have unrecognized fields, that sub-message
763    will not be searched any further. We prune the search of the sub-message
764    because we assume it is malformed and further checks will not yield
765    productive errors.
766
767    Args:
768      message: The Message instance to search.
769      _edges: Internal arg for passing state.
770
771    Yields:
772      (edges_to_message, field_names):
773        edges_to_message: List[ProtoEdge], The edges (relative to "message")
774            describing the path to the sub-message where the unrecognized
775            fields were found.
776        field_names: List[Str], The names of the field(s) that were
777            unrecognized in the sub-message.
778    """
779    if not isinstance(message, messages.Message):
780        # This is a primitive leaf, no errors found down this path.
781        return
782
783    field_names = message.all_unrecognized_fields()
784    if field_names:
785        # This message is malformed. Stop recursing and report it.
786        yield _edges, field_names
787        return
788
789    # Recurse through all fields in the current message.
790    for field in message.all_fields():
791        value = message.get_assigned_value(field.name)
792        if field.repeated:
793            for i, item in enumerate(value):
794                repeated_edge = ProtoEdge(EdgeType.REPEATED, field.name, i)
795                iter_ = UnrecognizedFieldIter(item, _edges + (repeated_edge,))
796                for (e, y) in iter_:
797                    yield e, y
798        elif _IsMap(message, field):
799            for key, item in _MapItems(message, field):
800                map_edge = ProtoEdge(EdgeType.MAP, field.name, key)
801                iter_ = UnrecognizedFieldIter(item, _edges + (map_edge,))
802                for (e, y) in iter_:
803                    yield e, y
804        else:
805            scalar_edge = ProtoEdge(EdgeType.SCALAR, field.name, None)
806            iter_ = UnrecognizedFieldIter(value, _edges + (scalar_edge,))
807            for (e, y) in iter_:
808                yield e, y
809