schultek/dart_mappable

Are CustomMappers global state?

Closed this issue · 2 comments

I have the following code:

import 'package:dart_mappable/dart_mappable.dart';

part 'model.mapper.dart';

/// This class should use the configured format for DateTime encoding.
/// The default is "Formatted ISO String in UTC Timezone"
@MappableClass()
class DefaultMapperModel with DefaultMapperModelMappable {
  final DateTime dateTime;

  DefaultMapperModel({required this.dateTime});
}

@MappableClass(includeCustomMappers: [_CustomDateTimeMapper()])
class CustomMapperModel with CustomMapperModelMappable {
  final DateTime dateTime;

  CustomMapperModel({required this.dateTime});
}

class _CustomDateTimeMapper extends SimpleMapper<DateTime> {
  const _CustomDateTimeMapper();

  @override
  DateTime decode(dynamic value) {
    return DateTime.parse(value as String);
  }

  @override
  dynamic encode(DateTime self) {
    // some custom formatting, the format does not matter
    // yyyyMMdd HH:mm:ss
    return '${self.year}${_zeroPad(self.month)}${_zeroPad(self.day)} ${_zeroPad(self.hour)}:${_zeroPad(self.minute)}:${_zeroPad(self.second)}';
  }

  String _zeroPad(int value) {
    return value.toString().padLeft(2, '0');
  }
}

If you use it like this:

import 'package:dart_mapable_test/model.dart';

void main(List<String> arguments) {
  final dt = DateTime.utc(2024, 5, 17, 9, 8, 10, 11, 12);

  print(dt.toIso8601String());

  final a = DefaultMapperModel(dateTime: dt);
  final b = CustomMapperModel(dateTime: dt);

  print(a.toJson());
  print(b.toJson());
  print(a.toJson()); // a again
}

You get the following output:

2024-05-17T09:08:10.011012Z
{"dateTime":"2024-05-17T09:08:10.011012Z"}
{"dateTime":"20240517 09:08:10"}
{"dateTime":"20240517 09:08:10"}

Is this intended behavior? My expectation was that the DateTime values of DefaultMapperModel are always converted to ISO-8601 (following the config) and that only CustomMapperModel uses my custom format mapper. But the call to toJson() on a CustomMapperModel seems to alter some sort of global state.

The documentation hints that the annotation is an alternative way to MapperContainer.globals.use(CustomMapper()), and in fact, if the annotation is used, the model's ensureInitialized() method adds the mapper to the global container.

-@MappableClass(includeCustomMappers: [])
+@MappableClass(includeCustomMappers: [_CustomDateTimeMapper()])
 class CustomMapperModel with CustomMapperModelMappable {
   final DateTime dateTime;

diff --git a/lib/model.mapper.dart b/lib/model.mapper.dart
class CustomMapperModelMapper extends ClassMapperBase<CustomMapperModel> {
   static CustomMapperModelMapper ensureInitialized() {
     if (_instance == null) {
       MapperContainer.globals.use(_instance = CustomMapperModelMapper._());
-      MapperContainer.globals.useAll([]);
+      MapperContainer.globals.useAll([_CustomDateTimeMapper()]);
     }
     return _instance!;
   }

So the behavior is intentional and my expectation was wrong.
Still, I find it kinda misleading that the annotation alters the global container, since one could argue that the scope of the annotation should only be the annotated class itself.

Is there a way to apply a CustomerMapper only to the class and not to others? Or do I have to use hooks on the fields?

rtfm ... Hooks are the way to go, just as the documentation says ;)

This gives you the possibility to use different custom mappers on the same type, especially on a field-by-field level.