google / built_value.dart

Immutable value types, enum classes, and serialization.
https://pub.dev/packages/built_value
BSD 3-Clause "New" or "Revised" License
861 stars 183 forks source link

Supporting Firestore GeoPoint data type #615

Open jimmyff opened 5 years ago

jimmyff commented 5 years ago

Hey @davidmorgan, I'm trying to integrate with Firestore's GeoPoint data type. I'm having difficulty, could you point me in the right direction?

This is what is coming to and from Firestore:

{
    'name': 'Jimmy',
    'location': GeoPoint(55.424359, -5.605486)
}

I've created GeoPoint built_value class:

part 'geo_point.g.dart';

abstract class GeoPoint implements Built<GeoPoint, GeoPointBuilder> {
  static Serializer<GeoPoint> get serializer => _$geoPointSerializer;

  num get longitiude;

  num get latitude;

  factory GeoPoint([updates(GeoPointBuilder b)]) = _$GeoPoint;
  GeoPoint._();

  factory GeoPoint.fromJsonMap(Map<String, dynamic> data) {
    print('GeoPoint: from JOSN map: $data');
    return serializers.deserializeWith(GeoPoint.serializer, data);
  }
  factory GeoPoint.withCoordinates(num latitude, num longitiude) {
    print('GeoPoint: from lat: $latitude long: $longitiude ');
    return GeoPoint((b) => b
      ..latitude = latitude
      ..longitiude = longitiude);
  }

  Map<String, dynamic> toJsonMap() {
    return new Map.of(serializers.serialize(this,
            specifiedType: const FullType(GeoPoint)))
        .cast<String, dynamic>();
  }
}

I've created a serializer:


import '../geo_point.dart';

class GeoPointPlugin implements SerializerPlugin {
  /// The field used to specify the value type if needed. Defaults to `$`.
  final String discriminator;

  // The key used when there is just a single value, for example if serializing
  // an `int`.
  final String valueKey;

  GeoPointPlugin({this.discriminator = r'$', this.valueKey = ''});

  @override
  Object beforeSerialize(Object object, FullType specifiedType) => object;

  @override
  Object afterSerialize(Object object, FullType specifiedType) {
    if (object is GeoPoint && specifiedType.root != JsonObject)
      return _toGeoPointString(object);
    else
      return object;
  }

  @override
  Object beforeDeserialize(Object object, FullType specifiedType) {
    if (object is String && object.substring(0, 8) == 'GeoPoint')
      return _toGeoPoint(object);
    else
      return object;
  }

  @override
  Object afterDeserialize(Object object, FullType specifiedType) {
    return object;
  }

  GeoPoint _toGeoPoint(String serializedString) {
    Match match = new RegExp(r"\((.*),(.*)\)").firstMatch(serializedString);
    return GeoPoint.withCoordinates(
        num.parse(match.group(1)), num.parse(match.group(2)));
  }

  String _toGeoPointString(GeoPoint gp) {
    return 'GeoPoint(${gp.latitude}, ${gp.longitiude})';
  }
}

I've then created a test:

  group('GeoPoint with known specifiedType', () {
    final data = GeoPoint.withCoordinates(55.424359, -5.605486);
    final serialized = 'GeoPoint(55.424359, -5.605486)';
    final specifiedType = const FullType(GeoPoint);

    test('can be serialized', () {
      expect(serializers.serialize(data, specifiedType: specifiedType),
          serialized);
    });

    test('can be deserialized', () {
      expect(serializers.deserialize(serialized, specifiedType: specifiedType),
          data);
    });
  });

The test is failing with the following output:

Expected: 'GeoPoint(55.424359, -5.605486)'
  Actual: {'longitiude': -5.605486, 'latitude': 55.424359}
   Which: not an <Instance of 'String'>
package:test_api                                 expect
test/models/geo_point_serializer_test.dart 15:7  main.<fn>.<fn>
✖ GeoPoint with known specifiedType can be serialized 
Deserializing 'GeoPoint {
  longitiude=-5.605486,
  latitude=55.424359,
}' to 'GeoPoint' failed due to: type '_$GeoPoint' is not a subtype of type 'Iterable<dynamic>' in type cast
package:built_value/src/built_json_serializers.dart 154:11  BuiltJsonSerializers._deserialize
package:built_value/src/built_json_serializers.dart 105:18  BuiltJsonSerializers.deserialize
test/models/geo_point_serializer_test.dart 20:26            main.<fn>.<fn>
✖ GeoPoint with known specifiedType can be deserialized

Any help would be great. Thanks

davidmorgan commented 5 years ago

Sorry for the slow response.

I believe the problem is that the GeoPoint is not included in the data as a String, rather it's the actual GeoPoint class.

You should be able to handle this by adding a custom Serializer that just passes the value through without changing it, as recommended here:

https://github.com/google/built_value.dart/issues/417#issuecomment-391661750

Hope that helps!