jolleekin / jsonx

An extended JSON library that supports the encoding and decoding of arbitrary objects.
BSD 2-Clause "Simplified" License
11 stars 6 forks source link

Support ObservableList and ObservableMap #3

Closed ErikGrimes closed 10 years ago

ErikGrimes commented 10 years ago

Using ObservableList and ObservableMap properties results in the wrong type getting created and a runtime exception being thrown (Exception: type '_LinkedHashMap' is not a subtype of type 'ObservableMap).

jolleekin commented 10 years ago

I tried to change _jsonToObject to the following so that all complex objects, including List and Map, will be instantiated via mirror.newInstance(_EMTPY_SYMBOL, []), which invokes the default constructor with no arguments.

_jsonToObject(json, mirror) {
  if (json == null) return null;

  var convert = jsonToObjects[mirror.reflectedType];
  if (convert != null) return convert(json);

  if (_isPrimitive(json)) return json;

  TypeMirror type;
  var instance = mirror.newInstance(_EMTPY_SYMBOL, []);
  var reflectee = instance.reflectee;

  if (reflectee is List) {
    type = mirror.typeArguments.single;
    for (var value in json) {
      reflectee.add(_jsonToObject(value, type));
    }
  } else if (reflectee is Map) {
    type = mirror.typeArguments.last;
    for (var key in json.keys) {
      reflectee[key] = _jsonToObject(json[key], type);
    }
  } else {

    // TODO: Consider using [mirror.instanceMembers].
    var setters = _getPublicSetters(mirror);

    for (var key in json.keys) {
      var name = new Symbol(key);
      var decl = setters[name];
      if (decl != null) {
        type = decl.type;
      } else {
        decl = setters[new Symbol('$key=')];
        if (decl != null) {
          type = decl.parameters.first.type;
        } else {
          continue;
        }
      }

      instance.setField(name, _jsonToObject(json[key], type));
    }
  }
  return reflectee;
}

However, an exception occurs when decoding a List. The problem is because the actual List type is _GrowableList, which doesn't have a default constructor. Interestingly, when looking at the code of _GrowableList, a default constructor was used!!!

  List < T > result = new _GrowableList < T >();

The two solutions I can think of right now are

  1. File a bug so that the Dart team can provide a default constructor for _GrowableList
  2. Provide a hack to _jsonToObject to handle List
// dart:core-patch_growable_array.dart

factory _GrowableList(int length) {
  var data = new _List((length == 0) ? 4 : length);
  var result = new _GrowableList < T >.withData(data);
  if (length > 0) {
    result._setLength(length);
  }
  return result;
}

factory _GrowableList.withCapacity(int capacity) {
  var data = new _List((capacity == 0) ? 4 : capacity);
  return new _GrowableList < T >.withData(data);
}

factory _GrowableList.from(Iterable < T > other) {
  List < T > result = new _GrowableList < T >();
  result.addAll(other);
  return result;
}

factory _GrowableList.withData(_List data)
native "GrowableList_allocate";
import 'package:observe/observe.dart';
import 'dart:mirrors';

main() {
  var list = [
    <int>[],
    <int, int>{},
    new ObservableList<int>(),
    new ObservableMap<int, int>()
  ];

  for (var e in list) {
    var t = e.runtimeType;
    var m = reflectType(t);
    print('type: $t');
    print('mirror: $m');
    print('');
  }
}

Output

type: List<int>
mirror: ClassMirror on '_GrowableList'

type: _LinkedHashMap<int, int>
mirror: ClassMirror on '_LinkedHashMap'

type: ObservableList<int>
mirror: ClassMirror on 'ObservableList'

type: ObservableMap<int, int>
mirror: ClassMirror on 'ObservableMap'
ErikGrimes commented 10 years ago

I went down the same path with _jsonToObject yesterday and ran into the same issue. There's seems to be a more general issue of default factory constructors which return implementations that don't have default constructors, which is the case with the core List implementation. The mirrors api has no know knowledge of the actual constructor that was used to create the object it is reflecting on. The sdk uses factory constructors to hide internal implementations in a number of places, so List is only the first of many potential problems.

I'm wondering if maybe the way to go is to allow the user to specify or provide a constructor somehow and pre-populate that registry to handle the sdk?

Jackson (https://github.com/FasterXML/jackson-databind) and other similar libraries may be able to provide some inspiration. The serialization package may also be worth a look. It has to handle similar issues.

jolleekin commented 10 years ago

I can add the following helper class to the library

class Typer<T> {
  Type get type => T;
}

Then, this works.

List<String> list = decode('["green", "yellow", "orange"]',
    type: new Typer<List<String>>().type);
ErikGrimes commented 10 years ago

Would that work with this?

 class Example {
   List<String> strings;
   List<int> ints
   }
jolleekin commented 10 years ago

Sure.

Sent from my Windows Phone


From: ErikGrimesmailto:notifications@github.com Sent: ‎5/‎14/‎2014 11:22 PM To: jolleekin/jsonxmailto:jsonx@noreply.github.com Cc: jolleekinmailto:jolleekin@outlook.com Subject: Re: [jsonx] Support ObservableList and ObservableMap (#3)

Would that work with this?

 class Example {
   List<String> strings;
   List<int> ints
   }

Reply to this email directly or view it on GitHub: https://github.com/jolleekin/jsonx/issues/3#issuecomment-43102995

jolleekin commented 10 years ago

Your example works as Example is not a generic type. The purpose of TypeHelper (renamed from Typer) is to deal with factory constructors of the built-in types such as List, Map, and Set. For example,

// Doesn't work since the runtime type of <int>[] is _GrowableList<T>, not List<T>.
var x = decode('[1, 2, 3]', type: <int>[].runtimeType);

// Works as [type] is List<int>.
var x = decode('[1, 2, 3]', type: new TypeHelper<List<int>>().type);

// A generic class whose default constructor is NOT a factory constructor.
class ListResponse<T> {
  List<T> items;
  int total;
}

// Works as [runtimeType] is ListResponse<int>.
var x = decode('{items: [1, 2, 3], total: 10}', type: new ListResponse<int>().runtimeType);

// Works as [type] is ListResponse<int>.
var x = decode('{items: [1, 2, 3], total: 10}', type: new TypeHelper<ListResponse<int>>().type);

As you can see, runtimeType only works with types that don't use factory constructors while TypeHelper works with any type. Therefore, it is better to use TypeHelper.

jolleekin commented 10 years ago

I just published another library called hotkey (http://pub.dartlang.org/packages/hotkey, https://github.com/jolleekin/hotkey). Please try it out and let me know what you think. Thanks.

ErikGrimes commented 10 years ago

Thanks for the fix!