knopp / msgpack_dart

MsgPack implementation for dart / msgpack.org[Dart]
MIT License
56 stars 13 forks source link

Support hooks for external Map and Array builders #14

Open wevre opened 1 year ago

wevre commented 1 year ago

@knopp Would you entertain a change that would let a user of the library have more control over how deserialized Arrays and Maps are built?

I'm working on an implementation of Cognitect's transit for Dart and in order to support MessagePack I need some guarantees about map entry orders. If such a hook was available in this API, I would be able to intercept the keys and values as they are being deserialized and be able to preserve their insertion order.

import 'dart:collection';

abstract class MapBuilder<G, M, K, V> {
  // Initialize a new (gestational) map
  G init();
  // Add key and val to the gestational map, return new gestational map, which
  // must be used for the next invocation.
  G add(G m, K key, V val);
  // Convert gestational map into final map and return it.
  M complete(G m);
}

// This is the default builder if the user does not provide a custom one.
class MapBuilderImpl implements MapBuilder<Map, Map, dynamic, dynamic> {
  @override
  init() => Map();

  @override
  add(m, key, val) {
    m[key] = val;
    return m;
  }

  @override
  complete(m) => m;
}

class Deserializer {
  final ExtDecoder? _extDecoder;
  final MapBuilder _mapBuilder;                            //<-- this is new
  final codec = Utf8Codec();
  final Uint8List _list;
  final ByteData _data;
  int _offset = 0;

  Deserializer(
    Uint8List list, {
    ExtDecoder? extDecoder,
    this.copyBinaryData = false,
    MapBuilder? mapBuilder                                 //<-- this is new
  })  : _list = list,
        _data = ByteData.view(list.buffer, list.offsetInBytes),
        _extDecoder = extDecoder,
        _mapBuilder = mapBuilder ?? MapBuilderImpl();      //<-- this is new

  // ... SKIPPING ...

  // Delegates to `mapBuilder` for actual map construction, always passing in
  // the result of the previous call as input to the next call.
  Map _readMap(int length) {
    var mr = _mapBuilder.init();
    while (length > 0) {
      mr = _mapBuilder.add(mr, decode(), decode());
      --length;
    }
    return _mapBuilder.complete(mr);
  }
}

In my case, I would register a MapBuilder that initialized a LinkHashMap in order to preserve insertion order.

If this is something you'd consider, I'll create a PR. If not, I'll just create a fork.