• 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 and
535                message.get_assigned_value(field.name) is None):
536            message.set_unrecognized_field(
537                field.name, decoded_message[field.name], messages.Variant.ENUM)
538    return message
539
540
541def _ProcessUnknownMessages(message, encoded_message):
542    """Store any remaining unknown fields as strings.
543
544    ProtoRPC currently ignores unknown values for which no type can be
545    determined (and logs a "No variant found" message). For the purposes
546    of reserializing, this is quite harmful (since it throws away
547    information). Here we simply add those as unknown fields of type
548    string (so that they can easily be reserialized).
549
550    Args:
551      message: Proto message we've decoded thus far.
552      encoded_message: JSON string we're decoding.
553
554    Returns:
555      message, with any remaining unrecognized fields saved.
556    """
557    if not encoded_message:
558        return message
559    decoded_message = json.loads(six.ensure_str(encoded_message))
560    message_fields = [x.name for x in message.all_fields()] + list(
561        message.all_unrecognized_fields())
562    missing_fields = [x for x in decoded_message.keys()
563                      if x not in message_fields]
564    for field_name in missing_fields:
565        message.set_unrecognized_field(field_name, decoded_message[field_name],
566                                       messages.Variant.STRING)
567    return message
568
569
570RegisterFieldTypeCodec(_SafeEncodeBytes, _SafeDecodeBytes)(messages.BytesField)
571
572
573# Note that these could share a dictionary, since they're keyed by
574# distinct types, but it's not really worth it.
575_JSON_ENUM_MAPPINGS = {}
576_JSON_FIELD_MAPPINGS = {}
577
578
579def AddCustomJsonEnumMapping(enum_type, python_name, json_name,
580                             package=None):  # pylint: disable=unused-argument
581    """Add a custom wire encoding for a given enum value.
582
583    This is primarily used in generated code, to handle enum values
584    which happen to be Python keywords.
585
586    Args:
587      enum_type: (messages.Enum) An enum type
588      python_name: (basestring) Python name for this value.
589      json_name: (basestring) JSON name to be used on the wire.
590      package: (NoneType, optional) No effect, exists for legacy compatibility.
591    """
592    if not issubclass(enum_type, messages.Enum):
593        raise exceptions.TypecheckError(
594            'Cannot set JSON enum mapping for non-enum "%s"' % enum_type)
595    if python_name not in enum_type.names():
596        raise exceptions.InvalidDataError(
597            'Enum value %s not a value for type %s' % (python_name, enum_type))
598    field_mappings = _JSON_ENUM_MAPPINGS.setdefault(enum_type, {})
599    _CheckForExistingMappings('enum', enum_type, python_name, json_name)
600    field_mappings[python_name] = json_name
601
602
603def AddCustomJsonFieldMapping(message_type, python_name, json_name,
604                              package=None):  # pylint: disable=unused-argument
605    """Add a custom wire encoding for a given message field.
606
607    This is primarily used in generated code, to handle enum values
608    which happen to be Python keywords.
609
610    Args:
611      message_type: (messages.Message) A message type
612      python_name: (basestring) Python name for this value.
613      json_name: (basestring) JSON name to be used on the wire.
614      package: (NoneType, optional) No effect, exists for legacy compatibility.
615    """
616    if not issubclass(message_type, messages.Message):
617        raise exceptions.TypecheckError(
618            'Cannot set JSON field mapping for '
619            'non-message "%s"' % message_type)
620    try:
621        _ = message_type.field_by_name(python_name)
622    except KeyError:
623        raise exceptions.InvalidDataError(
624            'Field %s not recognized for type %s' % (
625                python_name, message_type))
626    field_mappings = _JSON_FIELD_MAPPINGS.setdefault(message_type, {})
627    _CheckForExistingMappings('field', message_type, python_name, json_name)
628    field_mappings[python_name] = json_name
629
630
631def GetCustomJsonEnumMapping(enum_type, python_name=None, json_name=None):
632    """Return the appropriate remapping for the given enum, or None."""
633    return _FetchRemapping(enum_type, 'enum',
634                           python_name=python_name, json_name=json_name,
635                           mappings=_JSON_ENUM_MAPPINGS)
636
637
638def GetCustomJsonFieldMapping(message_type, python_name=None, json_name=None):
639    """Return the appropriate remapping for the given field, or None."""
640    return _FetchRemapping(message_type, 'field',
641                           python_name=python_name, json_name=json_name,
642                           mappings=_JSON_FIELD_MAPPINGS)
643
644
645def _FetchRemapping(type_name, mapping_type, python_name=None, json_name=None,
646                    mappings=None):
647    """Common code for fetching a key or value from a remapping dict."""
648    if python_name and json_name:
649        raise exceptions.InvalidDataError(
650            'Cannot specify both python_name and json_name '
651            'for %s remapping' % mapping_type)
652    if not (python_name or json_name):
653        raise exceptions.InvalidDataError(
654            'Must specify either python_name or json_name for %s remapping' % (
655                mapping_type,))
656    field_remappings = mappings.get(type_name, {})
657    if field_remappings:
658        if python_name:
659            return field_remappings.get(python_name)
660        elif json_name:
661            if json_name in list(field_remappings.values()):
662                return [k for k in field_remappings
663                        if field_remappings[k] == json_name][0]
664    return None
665
666
667def _CheckForExistingMappings(mapping_type, message_type,
668                              python_name, json_name):
669    """Validate that no mappings exist for the given values."""
670    if mapping_type == 'field':
671        getter = GetCustomJsonFieldMapping
672    elif mapping_type == 'enum':
673        getter = GetCustomJsonEnumMapping
674    remapping = getter(message_type, python_name=python_name)
675    if remapping is not None and remapping != json_name:
676        raise exceptions.InvalidDataError(
677            'Cannot add mapping for %s "%s", already mapped to "%s"' % (
678                mapping_type, python_name, remapping))
679    remapping = getter(message_type, json_name=json_name)
680    if remapping is not None and remapping != python_name:
681        raise exceptions.InvalidDataError(
682            'Cannot add mapping for %s "%s", already mapped to "%s"' % (
683                mapping_type, json_name, remapping))
684
685
686def _EncodeCustomFieldNames(message, encoded_value):
687    field_remappings = list(_JSON_FIELD_MAPPINGS.get(type(message), {})
688                            .items())
689    if field_remappings:
690        decoded_value = json.loads(encoded_value)
691        for python_name, json_name in field_remappings:
692            if python_name in encoded_value:
693                decoded_value[json_name] = decoded_value.pop(python_name)
694        encoded_value = json.dumps(decoded_value)
695    return encoded_value
696
697
698def _DecodeCustomFieldNames(message_type, encoded_message):
699    field_remappings = _JSON_FIELD_MAPPINGS.get(message_type, {})
700    if field_remappings:
701        decoded_message = json.loads(encoded_message)
702        for python_name, json_name in list(field_remappings.items()):
703            if json_name in decoded_message:
704                decoded_message[python_name] = decoded_message.pop(json_name)
705        encoded_message = json.dumps(decoded_message)
706    return encoded_message
707
708
709def _AsMessageList(msg):
710    """Convert the provided list-as-JsonValue to a list."""
711    # This really needs to live in extra_types, but extra_types needs
712    # to import this file to be able to register codecs.
713    # TODO(craigcitro): Split out a codecs module and fix this ugly
714    # import.
715    from apitools.base.py import extra_types
716
717    def _IsRepeatedJsonValue(msg):
718        """Return True if msg is a repeated value as a JsonValue."""
719        if isinstance(msg, extra_types.JsonArray):
720            return True
721        if isinstance(msg, extra_types.JsonValue) and msg.array_value:
722            return True
723        return False
724
725    if not _IsRepeatedJsonValue(msg):
726        raise ValueError('invalid argument to _AsMessageList')
727    if isinstance(msg, extra_types.JsonValue):
728        msg = msg.array_value
729    if isinstance(msg, extra_types.JsonArray):
730        msg = msg.entries
731    return msg
732
733
734def _IsMap(message, field):
735    """Returns whether the "field" is actually a map-type."""
736    value = message.get_assigned_value(field.name)
737    if not isinstance(value, messages.Message):
738        return False
739    try:
740        additional_properties = value.field_by_name('additionalProperties')
741    except KeyError:
742        return False
743    else:
744        return additional_properties.repeated
745
746
747def _MapItems(message, field):
748    """Yields the (key, value) pair of the map values."""
749    assert _IsMap(message, field)
750    map_message = message.get_assigned_value(field.name)
751    additional_properties = map_message.get_assigned_value(
752        'additionalProperties')
753    for kv_pair in additional_properties:
754        yield kv_pair.key, kv_pair.value
755
756
757def UnrecognizedFieldIter(message, _edges=()):  # pylint: disable=invalid-name
758    """Yields the locations of unrecognized fields within "message".
759
760    If a sub-message is found to have unrecognized fields, that sub-message
761    will not be searched any further. We prune the search of the sub-message
762    because we assume it is malformed and further checks will not yield
763    productive errors.
764
765    Args:
766      message: The Message instance to search.
767      _edges: Internal arg for passing state.
768
769    Yields:
770      (edges_to_message, field_names):
771        edges_to_message: List[ProtoEdge], The edges (relative to "message")
772            describing the path to the sub-message where the unrecognized
773            fields were found.
774        field_names: List[Str], The names of the field(s) that were
775            unrecognized in the sub-message.
776    """
777    if not isinstance(message, messages.Message):
778        # This is a primitive leaf, no errors found down this path.
779        return
780
781    field_names = message.all_unrecognized_fields()
782    if field_names:
783        # This message is malformed. Stop recursing and report it.
784        yield _edges, field_names
785        return
786
787    # Recurse through all fields in the current message.
788    for field in message.all_fields():
789        value = message.get_assigned_value(field.name)
790        if field.repeated:
791            for i, item in enumerate(value):
792                repeated_edge = ProtoEdge(EdgeType.REPEATED, field.name, i)
793                iter_ = UnrecognizedFieldIter(item, _edges + (repeated_edge,))
794                for (e, y) in iter_:
795                    yield e, y
796        elif _IsMap(message, field):
797            for key, item in _MapItems(message, field):
798                map_edge = ProtoEdge(EdgeType.MAP, field.name, key)
799                iter_ = UnrecognizedFieldIter(item, _edges + (map_edge,))
800                for (e, y) in iter_:
801                    yield e, y
802        else:
803            scalar_edge = ProtoEdge(EdgeType.SCALAR, field.name, None)
804            iter_ = UnrecognizedFieldIter(value, _edges + (scalar_edge,))
805            for (e, y) in iter_:
806                yield e, y
807