text: #
  # This file is a part of the normalize python library
  #
  # normalize is free software: you can redistribute it and/or modify
  # it under the terms of the MIT License.
  #
  # normalize 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
  # MIT License for more details.
  #
  # You should have received a copy of the MIT license along with
  # normalize.  If not, refer to the upstream repository at
  # http://github.com/hearsaycorp/normalize
  #
  
  
  from __future__ import absolute_import
  import six
  import decimal
  
  from builtins import str
  from past.builtins import basestring
  import collections
  from copy import deepcopy
  import inspect
  import json
  import re
  
  from normalize.coll import Collection
  from normalize.coll import DictCollection as RecordDict
  from normalize.coll import ListCollection as RecordList
  from normalize.coll import list_of
  from normalize.diff import Diff
  from normalize.diff import DiffInfo
  import normalize.exc as exc
  from normalize.property.json import JsonProperty
  from normalize.record import OhPickle
  from normalize.record import Record
  from normalize.selector import FieldSelector
  
  
  def _json_to_value_initializer(json_val, proptype):
      if proptype:
          if isinstance(proptype, JsonRecord):
              return json_val
          elif hasattr(proptype, "from_json"):
              return proptype.from_json(json_val)
          elif isinstance(proptype, Record) and isinstance(json_val, dict):
              return from_json(proptype, json_val)
  
      return json_val
  
  
  def _box_ingress_error(context, exception):
      error_fs = FieldSelector([context])
      if hasattr(exception, "error_fs"):
          error_fs.extend(exception.error_fs)
      if hasattr(exception, "sub_exception"):
          exception = exception.sub_exception
      return exc.JsonConversionError(
          error_fs=error_fs,
          sub_exception=exception,
      )
  
  
  def json_to_initkwargs(record_type, json_struct, kwargs=None):
      """This function converts a JSON dict (json_struct) to a set of init
      keyword arguments for the passed Record (or JsonRecord).
  
      It is called by the JsonRecord constructor.  This function takes a JSON
      data structure and returns a keyword argument list to be passed to the
      class constructor.  Any keys in the input dictionary which are not known
      are passed as a single ``unknown_json_keys`` value as a dict.
  
      This function should generally not be called directly, except as a part of
      a ``__init__`` or specialized visitor application.
      """
      if kwargs is None:
          kwargs = {}
      if json_struct is None:
          json_struct = {}
      if not isinstance(json_struct, dict):
          raise exc.JsonRecordCoerceError(
              passed=json_struct,
              recordtype=record_type,
          )
      unknown_keys = set(json_struct.keys())
      for propname, prop in record_type.properties.items():
          # think "does" here rather than "is"; the slot does JSON
          if isinstance(prop, JsonProperty):
              json_name = prop.json_name
              if json_name is not None:
                  if json_name in json_struct:
                      if propname not in kwargs:
                          try:
                              kwargs[propname] = _json_to_value_initializer(
                                  prop.from_json(
                                      json_struct[json_name]
                                  ),
                                  prop.valuetype,
                              )
                          except Exception as e:
                              raise _box_ingress_error(json_name, e)
                      unknown_keys.remove(json_name)
          elif prop.name in json_struct:
              json_val = json_struct[prop.name]
              unknown_keys.remove(prop.name)
              if prop.name not in kwargs:
                  proptype = prop.valuetype
                  try:
                      kwargs[propname] = _json_to_value_initializer(
                          json_val, proptype,
                      )
                  except Exception as e:
                      raise _box_ingress_error(prop.name, e)
      if unknown_keys:
          kwargs["unknown_json_keys"] = dict(
              (k, deepcopy(json_struct[k])) for k in unknown_keys
          )
      return kwargs
  
  
  def from_json(record_type, json_struct):
      """JSON marshall in function: a 'visitor' function which looks for JSON
      types/hints on types being converted to, but does not require them.
  
      Args:
          ``record_type=``\ *TYPE*
              Record type to convert data to
  
          ``json_struct=``\ *DICT|LIST*
              a loaded (via ``json.loads``) data structure, normally a
              dict or a list.
      """
      if issubclass(record_type, JsonRecord):
          return record_type(json_struct)
  
      elif issubclass(record_type, Record):
          # do what the default JsonRecord __init__ does
          init_kwargs = json_to_initkwargs(record_type, json_struct)
          instance = record_type(**init_kwargs)
          return instance
      else:
          raise exc.CastTypeError(badtype=record_type)
  
  
  # caches for _json_data
  has_json_data = dict()
  json_data_takes_extraneous = dict()
  
  
  def _json_data(x, extraneous):
      """This function calls a json_json method, if the type has one, otherwise
      calls back into to_json().  It also check whether the method takes an
      'extraneous' argument and passes that through if possible."""
  
      if type(x) in has_json_data and has_json_data[type(x)]:
          if json_data_takes_extraneous[type(x)]:
              return x.json_data(extraneous=extraneous)
          else:
              return x.json_data()
      else:
          htj = hasattr(x, "json_data") and callable(x.json_data)
          has_json_data[type(x)] = htj
          if htj:
              argspec = inspect.getargspec(x.json_data)
              tjte = 'extraneous' in argspec.args or argspec.keywords
              json_data_takes_extraneous[type(x)] = tjte
              if tjte:
                  return x.json_data(extraneous=extraneous)
              else:
                  return x.json_data()
          else:
              return to_json(x, extraneous)
  
  
  def to_json(record, extraneous=True, prop=None):
      """JSON conversion function: a 'visitor' function which implements marshall
      out (to JSON data form), honoring JSON property types/hints but does not
      require them.  To convert to an actual JSON document, pass the return value
      to ``json.dumps`` or a similar function.
  
      args:
          ``record=``\ *anything*
              This object can be of any type; a best-effort attempt is made to
              convert to a form which ``json.dumps`` can accept; this function
              will call itself recursively, respecting any types which define
              ``.json_data()`` as a method and calling that.
  
          ``extraneous=``\ *BOOL*
              This parameter is passed through to any ``json_data()`` methods
              which support it.
  
          ``prop=``\ *PROPNAME*\ |\ *PROPERTY*
              Specifies to return the given property from an object, calling any
              ``to_json`` mapping defined on the property.  Does not catch the
              ``AttributeError`` that is raised by the property not being set.
      """
      if prop:
          if isinstance(prop, basestring):
              prop = type(record).properties[prop]
          val = prop.__get__(record)
          if hasattr(prop, "to_json"):
              return prop.to_json(val, extraneous, _json_data)
          else:
              return _json_data(val, extraneous)
  
      elif isinstance(record, Collection):
          if isinstance(record, RecordDict):
              return dict(
                  (k, _json_data(v, extraneous)) for k, v in list(record.items())
              )
          else:
              return list(_json_data(x, extraneous) for x in record)
  
      elif isinstance(record, Record):
          rv_dict = {}
          for propname, prop in type(record).properties.items():
              if not extraneous and prop.extraneous:
                  pass
              elif prop.slot_is_empty(record):
                  pass
              elif not hasattr(prop, "json_name") or prop.json_name is not None:
                  json_name = getattr(prop, "json_name", prop.name)
                  try:
                      rv_dict[json_name] = to_json(record, extraneous, prop)
                  except AttributeError:
                      pass
          return rv_dict
  
      elif isinstance(record, six.integer_types[-1]):
          return str(record) if abs(record) > 2**50 else record
  
      elif isinstance(record, dict):
          return dict(
              (k, _json_data(v, extraneous)) for k, v in record.items()
          )
  
      elif isinstance(record, (list, tuple, set, frozenset)):
          return list(_json_data(x, extraneous) for x in record)
  
      elif isinstance(record, (basestring, int, float, decimal.Decimal, type(None))):
          return record
  
      else:
          raise TypeError(
              "I don't know how to marshall a %s to JSON" %
              type(record).__name__
          )
  
  
  class JsonRecord(Record):
      """Version of a Record which deals primarily in JSON form.
  
      This means:
  
      1. The first argument to the constructor is assumed to be a JSON data
         dictionary, and passed through the class' ``json_to_initkwargs``
         method before being used to set actual properties
  
      2. Unknown keys are permitted, and saved in the "unknown_json_keys"
         property, which is merged back on output (ie, calling ``.json_data()``
         or ``to_json()``)
      """
      unknown_json_keys = JsonProperty(json_name=None, extraneous=True)
  
      def __init__(self, json_data=None, **kwargs):
          """Build a new JsonRecord sub-class.
  
          args:
              ``json_data=``\ *DICT|other*
                  JSON data (string or already ``json.loads``'d).  If not
                  a JSON dictionary with keys corresponding to the
                  ``json_name`` or the properties within, then
                  ``json_to_initkwargs`` should be overridden to handle
                  the unpacking differently
  
              ``**kwargs``
                  ``JsonRecord`` instances may also be constructed by
                  passing in attribute initializers in keyword form.  The
                  keys here should be the names of the attributes and the
                  python values, not the JSON names or form.
          """
          if isinstance(json_data, OhPickle):
              return
          if isinstance(json_data, basestring):
              json_data = json.loads(json_data)
          if json_data is not None:
              kwargs = type(self).json_to_initkwargs(json_data, kwargs)
          super(JsonRecord, self).__init__(**kwargs)
  
      @classmethod
      def json_to_initkwargs(self, json_data, kwargs):
          """Subclassing hook to specialize how JSON data is converted
          to keyword arguments"""
          if isinstance(json_data, basestring):
              json_data = json.loads(json_data)
          return json_to_initkwargs(self, json_data, kwargs)
  
      @classmethod
      def from_json(self, json_data):
          """This method can be overridden to specialize how the class is loaded
          when marshalling in; however beware that it is not invoked when the
          caller uses the ``from_json()`` function directly."""
          return self(json_data)
  
      def json_data(self, extraneous=False):
          """Returns the JSON data form of this ``JsonRecord``.  The 'unknown'
          JSON keys will be merged back in, if:
  
          1. the ``extraneous=True`` argument is passed.
  
          2. the ``unknown_json_keys`` property on this class is replaced by one
             not marked as ``extraneous``
          """
          jd = to_json(self, extraneous)
          if hasattr(self, "unknown_json_keys"):
              prop = type(self).properties['unknown_json_keys']
              if extraneous or not prop.extraneous:
                  for k, v in self.unknown_json_keys.items():
                      if k not in jd:
                          jd[k] = to_json(v, extraneous)
          return jd
  
      def diff_iter(self, other, **kwargs):
          """Generator method which returns the differences from the invocant to
          the argument.  This specializes :py:meth:`Record.diff_iter` by
          returning :py:class:`JsonDiffInfo` objects.
          """
          for diff in super(JsonRecord, self).diff_iter(other, **kwargs):
              # TODO: object copy/upgrade constructor
              newargs = diff.__getstate__()
              yield JsonDiffInfo(**(newargs))
  
      def diff(self, other, **kwargs):
          """Compare an object with another.  This specializes
          :py:meth:`Record.diff` by returning a :py:class:`JsonDiff` object.
          """
          return JsonDiff(
              base_type_name=type(self).__name__,
              other_type_name=type(other).__name__,
              values=self.diff_iter(other, **kwargs),
          )
  
  
  class JsonRecordList(RecordList, JsonRecord):
      """Version of a RecordList which deals primarily in JSON"""
      json_coll_name = "array"
  
      def __init__(self, json_data=None, **kwargs):
          """Build a new JsonRecord sub-class.
  
          Args:
              ``json_data=``\ *LIST|other*
                  JSON data (string or already ``json.loads``'d)
  
              ``**kwargs``
                  Other initializer attributes, for lists with extra
                  attributes (eg, paging information)
          """
          if isinstance(json_data, OhPickle):
              return
          if isinstance(json_data, basestring):
              json_data = json.loads(json_data)
          if json_data is not None:
              kwargs = type(self).json_to_initkwargs(json_data, kwargs)
          super(JsonRecordList, self).__init__(**kwargs)
  
      @classmethod
      def json_to_initkwargs(cls, json_struct, kwargs):
          member_type = cls.itemtype
          if not member_type:
              raise exc.CollectionDefinitionError(
                  coll="JsonRecordList",
                  property='itemtype',
              )
          if kwargs.get('values', None) is None:
              kwargs['values'] = values = []
              if json_struct is None:
                  json_struct = tuple()
              if not isinstance(json_struct, (list, tuple)):  # sigh
                  raise exc.JsonCollectionCoerceError(
                      passed=json_struct,
                      colltype=cls,
                  )
  
              if hasattr(member_type, "from_json"):
                  for i, x in cls.coll_to_tuples(json_struct):
                      try:
                          values.append(
                              x if isinstance(x, member_type) else
                              member_type.from_json(x)
                          )
                      except Exception as e:
                          raise _box_ingress_error(i, e)
  
              elif issubclass(member_type, Record):
                  for i, x in cls.coll_to_tuples(json_struct):
                      try:
                          values.append(
                              x if isinstance(x, member_type) else
                              from_json(member_type, x)
                          )
                      except Exception as e:
                          raise _box_ingress_error(i, e)
              else:
                  kwargs['values'] = json_struct
          return kwargs
  
      def json_data(self, extraneous=False):
          # this method intentionally does not call the superclass json_data,
          # because this function returns a collection.
          return to_json(self, extraneous)
  
      def __repr__(self):
          super_repr = super(JsonRecordList, self).__repr__()
          return super_repr.replace("[", "values=[", 1)
  
  
  class JsonRecordDict(RecordDict, JsonRecord):
      """Version of a RecordDict which deals primarily in JSON"""
      json_coll_name = "object"
  
      def __init__(self, json_data=None, **kwargs):
          """Build a new JsonRecord sub-class.
  
          Args:
              ``json_data=``\ *DICT|other*
                  JSON data (string or already ``json.loads``'d)
  
              ``**kwargs``
                  Other initializer attributes, for lists with extra
                  attributes (eg, paging information).
          """
          if isinstance(json_data, OhPickle):
              return
          if isinstance(json_data, basestring):
              json_data = json.loads(json_data)
          if json_data is not None:
              kwargs = type(self).json_to_initkwargs(json_data, kwargs)
          super(JsonRecordDict, self).__init__(**kwargs)
  
      @classmethod
      def json_to_initkwargs(cls, json_struct, kwargs):
          member_type = cls.itemtype
          if not member_type:
              raise exc.CollectionDefinitionError(
                  coll="JsonRecordDict",
                  property='itemtype',
              )
          if kwargs.get('values', None) is None:
              kwargs['values'] = values = {}
              if json_struct is None:
                  json_struct = {}
              if not isinstance(json_struct, collections.Mapping):
                  raise exc.JsonCollectionCoerceError(
                      passed=json_struct,
                      colltype=cls,
                  )
  
              if hasattr(member_type, "from_json"):
                  for k, x in cls.coll_to_tuples(json_struct):
                      try:
                          values[k] = (
                              x if isinstance(x, member_type) else
                              member_type.from_json(x)
                          )
                      except Exception as e:
                          raise _box_ingress_error(k, e)
              elif issubclass(member_type, Record):
                  for k, x in cls.coll_to_tuples(json_struct):
                      try:
                          values[k] = (
                              x if isinstance(x, member_type) else
                              from_json(member_type, x)
                          )
                      except Exception as e:
                          raise _box_ingress_error(k, e)
              else:
                  kwargs['values'] = json_struct
          return kwargs
  
      def json_data(self, extraneous=False):
          # this method intentionally does not call the superclass json_data,
          # because this function returns a collection.
          return to_json(self, extraneous)
  
      def __repr__(self):
          super_repr = super(JsonRecordList, self).__repr__()
          return super_repr.replace("{", "values={", 1)
  
  
  class JsonDiffInfo(DiffInfo, JsonRecord):
      """Version of 'DiffInfo' that supports ``.json_data()``"""
      def json_data(self):
          return dict(
              diff_type=self.diff_type.canonical_name,
              base=self.base.selectors,
              other=self.other.selectors,
          )
  
  
  class JsonDiff(Diff, JsonRecordList):
      """Version of 'Diff' that supports ``.json_data()``"""
      itemtype = JsonDiffInfo
  
  
  class AutoJsonRecord(JsonRecord):
      unknown_json_keys = JsonProperty(
          json_name=None, extraneous=False, default=lambda: {},
      )
  
      @classmethod
      def auto_upgrade_dict(cls, thing):
          return AutoJsonRecord(thing)
  
      @classmethod
      def auto_upgrade_list(cls, thing):
          if len(thing) and isinstance(thing[0], dict):
              return list_of(AutoJsonRecord)(thing)
          else:
              return thing
  
      @classmethod
      def auto_upgrade_any(cls, thing):
          if isinstance(thing, dict):
              return cls.auto_upgrade_dict(thing)
          elif isinstance(thing, list):
              return cls.auto_upgrade_list(thing)
          else:
              return thing
  
      @classmethod
      def convert_json_key_in(cls, key):
          return re.sub(
              r'([a-z\d])([A-Z])',
              lambda m: "%s_%s" % (m.group(1), m.group(2).lower()),
              key,
          )
  
      @classmethod
      def convert_json_key_out(cls, key):
          return re.sub(
              r'([a-z\d])_([a-z])',
              lambda m: "%s%s" % (m.group(1), m.group(2).upper()),
              key,
          )
  
      @classmethod
      def json_to_initkwargs(cls, json_data, kwargs):
          kwargs = super(AutoJsonRecord, cls).json_to_initkwargs(
              json_data, kwargs,
          )
          # upgrade any dictionaries to AutoJsonRecord, and
          # any lists of dictionaries to list_of(AutoJsonRecord)
          if 'unknown_json_keys' in kwargs:
              kwargs['unknown_json_keys'] = {
                  cls.convert_json_key_in(k): cls.auto_upgrade_any(v) for
                  k, v in list(kwargs['unknown_json_keys'].items())
              }
          return kwargs
  
      def json_data(self, extraneous=False):
          jd = to_json(self, extraneous)
          if hasattr(self, "unknown_json_keys"):
              prop = type(self).properties['unknown_json_keys']
              if extraneous or not prop.extraneous:
                  for k, v in self.unknown_json_keys.items():
                      k = type(self).convert_json_key_out(k)
                      if k not in jd:
                          jd[k] = to_json(v, extraneous)
          return jd
  
      def __getattr__(self, attr):
          if attr in type(self).properties:
              return type(self).properties[attr].__get__(self)
          else:
              return self.unknown_json_keys[attr]
  
      def __setattr__(self, attr, value):
          if attr in type(self).properties:
              return type(self).properties[attr].__set__(self)
          else:
              self.unknown_json_keys[attr]
  
      def __delattr__(self, attr, value):
          if attr in type(self).properties:
              return type(self).properties[attr].__del__(self)
          else:
              del self.unknown_json_keys[attr]
  
  
  class NCAutoJsonRecord(AutoJsonRecord):
      """A version of AutoJsonRecord which does not convert keys from
      camelCase to python_form"""
      @classmethod
      def convert_json_key_in(cls, key):
          return key
  
      @classmethod
      def convert_json_key_out(cls, key):
          return key
  
      @classmethod
      def auto_upgrade_dict(cls, thing):
          return NCAutoJsonRecord(thing)
  
      @classmethod
      def auto_upgrade_list(cls, thing):
          if len(thing) and isinstance(thing[0], dict):
              return list_of(NCAutoJsonRecord)(thing)
          else:
              return thing
  