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

[Bug] Code Generation Attempt Unintendedly Triggered #1303

Open jlambright opened 7 months ago

jlambright commented 7 months ago

Given the abstract class (as well as a mixin version of the code) any sub-classes seem to trigger code generation attempts. However, I'm creating a wrapper class that consumes Built generated classes. This seems like buggy behavior. Either that, or it highlights the need for some kind of @built_ignore() annotation.

abstract class BuiltJsonSerializable<T extends Built<T, V>,
    V extends Builder<T, V>> {
  Serializer<T> get serializer;

  Map<String, dynamic> toJsonMap() => jsonDecode(toJsonString());

  String toJsonString() => standardSerializers.toJson<T>(serializer, toModel());

  T? toModel();

  T? modelFromJsonString(String serialized) {
    try {
      T? model = standardSerializers.fromJson<T>(serializer, serialized);
      return model;
    } on DeserializationError catch (e) {
      Get.log('[${T.toString()} DESERIALIZATION ERROR] ${e.toString()}');
      return null;
    }
  }
}
davidmorgan commented 7 months ago

The trigger for codegen is supposed to be implementing Built.

Could you please give an example of a class that triggers codegen that you expect to not trigger codegen?

Thanks :)

jlambright commented 7 months ago

Below you'll see one of the sub-classes. When I run dart run build_runner build (as I do have some codegen in this project), it throws the following errors.

[SEVERE] built_value_generator:built_value on lib/data/wrappers/wrappers.dart:
Error in BuiltValueGenerator for /dsk360_command/lib/data/wrappers/wrappers.dart.
Please make the following changes to use built_value serialization:
<I'm skipping a bunch of errors since they're almost all the same.>
10. Declare TicketWrapper.serializer as: static Serializer<TicketWrapper> get serializer => _$ticketWrapperSerializer; got @override Serializer<TicketModel> get serializer => _$ticketWrapperSerializer;

part of 'wrappers.dart';

TicketWrapper ticketFromJson(String str) => TicketWrapper.fromJsonString(str);
TicketModel? ticketToModel(TicketWrapper data) => data.toModel();
String ticketToJson(TicketWrapper data) => standardSerializers
    .toJson<TicketModel>(TicketModel.serializer, data.toModel());

List<TicketWrapper> ticketsFromModelList(List<TicketModel>? tickets) {
  if (tickets == null) {
    return [];
  } else {
    return tickets
        .asMap()
        .entries
        .map((entry) => TicketWrapper.fromModel(
            ticketModel: entry.value, number: entry.key))
        .toList();
  }
}

Map<String, TicketWrapper> ticketsMapFromTicketModelList(
    List<TicketModel>? tickets) {
  if (tickets == null) {
    return <String, TicketWrapper>{};
  } else {
    return Map.fromEntries(
        tickets.map<MapEntry<String, TicketWrapper>>((ticketModel) {
      final TicketWrapper ticket =
          TicketWrapper.fromModel(ticketModel: ticketModel);
      return MapEntry(ticket.uuid, ticket);
    }));
  }
}

Map<String, TicketWrapper> ticketsMapFromList(List<TicketWrapper>? tickets) =>
    {for (TicketWrapper ticket in tickets ?? []) ticket.uuid: ticket};

List<TicketModel?> ticketWrapperListToModel(
    List<TicketWrapper> ticketWrappers) {
  return ticketWrappers
      .map((ticketWrapper) => ticketWrapper.toModel())
      .toList();
}

List<Map<String, dynamic>> toJsonMapTicketList(List<TicketWrapper> tickets) =>
    tickets.map((ticket) => ticket.toJsonMap()).toList();

class TicketWrapper
    extends BuiltJsonSerializable<TicketModel, TicketModelBuilder> {
  @override
  Serializer<TicketModel> get serializer => TicketModel.serializer;

  late final int dataHash;

  late final String uuid;
  late final DateTime lastModified;
  late final ContactWrapper contact;
  late final TicketStatusEnum status;
  late final Map<String, NoteWrapper> notes;
  List<NoteWrapper> get notesList => notes.values.toList();
  List<String> get notesUuids => notes.keys.toList();

  late final Map<String, TagWrapper> tags;
  List<TagWrapper> get tagList => tags.values.toList();
  List<String> get tagUuids => tags.keys.toList();

  List<String> get tagNames => [for (TagWrapper tag in tagList) tag.name];
  bool hasTagByUuid(String tagUuid) => tagUuids.contains(tagUuid);

  bool get isMaintenance => tagNames.contains("Maintenance");
  bool get isSecurity => tagNames.contains("Security");

  bool get hasImageUrls => contact.hasLatestUrls;
  String? get latestImageUrl => contact.latestImageUrl;
  List<String>? get latestImageUrls => contact.latestImageUrls;
  BoundingSphereWrapper get boundingSphere => contact.boundingSphere;

  @override
  int get hashCode => toJsonString().hashCode;

  TicketWrapper(
      {required this.uuid,
      required this.contact,
      DateTime? lastModified,
      this.status = TicketStatusEnum.unknown,
      this.notes = const {},
      this.tags = const {}})
      : lastModified = lastModified ?? epochTime;

  TicketWrapper.fromModel({TicketModel? ticketModel, int? number}) {
    uuid = ticketModel?.uuid ?? _uuid.v4();
    lastModified = ticketModel?.lastModified ?? epochTime;
    contact = ContactWrapper.fromModel(ticketModel?.contact);
    status = TicketStatusEnum.fromDkpQualityEnum(ticketModel?.status);
    tags = fromTagModelList(ticketModel?.tags.toList());
    notes = fromNoteModelList(ticketModel?.notes?.toList());
  }

  TicketWrapper.createEmptyWrapper() {
    uuid = _uuid.v4();
    lastModified = epochTime;
    contact = ContactWrapper.createEmptyWrapper();
    status = TicketStatusEnum.unknown;
    tags = {};
    notes = {};
  }

  TicketWrapper.fromJsonString(String serialized) {
    try {
      TicketModel? ticketModel =
          standardSerializers.fromJson<TicketModel>(serializer, serialized);
      TicketWrapper.fromModel(ticketModel: ticketModel);
    } on AssertionError {
      throw ArgumentError.notNull(serialized);
    }
  }

  @override
  Map<String, dynamic> toJsonMap() => jsonDecode(toJsonString());

  @override
  TicketModel? toModel() {
    return modelFromJsonString(jsonEncode({
      "uuid": uuid,
      "lastModified": lastModified,
      "contact": contact.toModel(),
      "status": status == TicketStatusEnum.unknown ? "Open" : status.value,
      "notes": noteWrapperListToModel(notesList),
      "tags": tagWrapperListToModel(tagList)
    }));
  }

  @override
  bool operator ==(Object other) {
    return super.hashCode == other.hashCode;
  }
}
davidmorgan commented 7 months ago

Oh, I see the problem.

The serializer codegen triggers for a class if any of its supertypes starts with Built, and there is a field called serializer.

I have no idea why it's like this, the value type generation only triggers if the interface is exactly Built.

That looks like it should just be fixed, until then you can work around by renaming your class to anything that doesn't start with Built or renaming serializer. (Whoops).