Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom converters #49

Closed
tnc1997 opened this issue Oct 7, 2023 · 3 comments · Fixed by #56
Closed

Add support for custom converters #49

tnc1997 opened this issue Oct 7, 2023 · 3 comments · Fixed by #56
Assignees

Comments

@tnc1997
Copy link
Owner

tnc1997 commented Oct 7, 2023

Is your feature request related to a problem? Please describe.
As a developer I would like to provide an XmlConverter to control the serialization of data types.

Describe the solution you'd like
I would like a solution that is similar to the JsonConverter from the json_annotation package.

Additional context
https://github.com/google/json_serializable.dart/blob/master/json_annotation/lib/src/json_converter.dart
https://github.com/google/json_serializable.dart/blob/master/example/lib/json_converter_example.dart

@tnc1997
Copy link
Owner Author

tnc1997 commented Oct 8, 2023

Hi @Matej-Hlatky, I would be interested in your opinion on an XmlConverter annotation vs fromXml... and toXml... fields on the existing annotations. I am not entirely sure that XmlConverter would be able to work quite like JsonConverter. The field being serialized could be an XmlAttribute or an XmlElement to name a few, and whilst the developer is likely only interested in the value of the XmlAttribute, they might be interested in the entire XmlElement instead of just its text, especially if the XmlElement contains nested children. Here are some of the different proposals that I have considered so far:

@annotation.XmlSerializable()
class One {
  @annotation.XmlAttribute(fromXmlAttribute: base64BinaryFromXmlAttribute, toXmlAttribute: base64BinaryToXmlAttribute)
  Uint8List attribute;

  @annotation.XmlElement(fromXmlElement: base64BinaryFromXmlElement, toXmlElement: base64BinaryToXmlElement)
  Uint8List element;

  ...
}

Uint8List base64BinaryFromXmlAttribute(XmlAttribute attribute) {
  return base64.decode(attribute.value);
}

XmlAttribute base64BinaryToXmlAttribute(Uint8List value) {
  return XmlAttribute(XmlName('???'), base64.encode(value));
}

Uint8List base64BinaryFromXmlElement(XmlElement element) {
  final text = element.getText()!;

  return base64.decode(text);
}

XmlElement base64BinaryToXmlElement(Uint8List value) {
  final text = base64.encode(value);

  return XmlElement(XmlName('???'), [], [XmlText(text)]);
}
@annotation.XmlSerializable()
class Two {
  @annotation.XmlAttribute(fromXml: base64BinaryFromXml, toXml: base64BinaryToXml)
  Uint8List attribute;

  @annotation.XmlElement(fromXml: base64BinaryFromXml, toXml: base64BinaryToXml)
  Uint8List element;

  ...
}

Uint8List base64BinaryFromXml(String value) => base64.decode(value);

String base64BinaryToXml(Uint8List value) => base64.encode(value);
// xml_annotation/lib/src/annotations/xml_attribute_converter.dart
abstract class XmlAttributeConverter<T> {
  const XmlAttributeConverter();

  T fromXmlAttribute(XmlAttribute attribute);

  XmlAttribute toXmlAttribute(T object);
}

// xml_annotation/lib/src/annotations/xml_element_converter.dart
abstract class XmlElementConverter<T> {
  const XmlElementConverter();

  T fromXmlElement(XmlElement element);

  XmlElement toXmlElement(T object);
}

@annotation.XmlSerializable()
class Three {
  @annotation.XmlAttribute()
  @Base64BinaryConverter()
  Uint8List attribute;

  @annotation.XmlElement()
  @Base64BinaryConverter()
  Uint8List element;

  ...
}

class Base64BinaryConverter implements annotation.XmlAttributeConverter<Uint8List>, annotation.XmlElementConverter<Uint8List> {
  const Base64BinaryConverter();

  @override
  Uint8List fromXmlAttribute(XmlAttribute attribute) {
    return base64.decode(attribute.value);
  }

  @override
  XmlAttribute toXmlAttribute(Uint8List object) {
    return XmlAttribute(XmlName('???'), base64.encode(object));
  }

  @override
  Uint8List fromXmlElement(XmlElement element) {
    final text = element.getText()!;

    return base64.decode(text);
  }

  @override
  XmlElement toXmlElement(Uint8List object) {
    final text = base64.encode(object);

    return XmlElement(XmlName('???'), [], [XmlText(text)]);
  }
}
// xml_annotation/lib/src/annotations/xml_converter.dart
abstract class XmlConverter<T> {
  const XmlConverter();

  T fromXml(String value);

  String toXml(T object);
}

@annotation.XmlSerializable()
class Four {
  @annotation.XmlAttribute()
  @Base64BinaryConverter()
  Uint8List attribute;

  @annotation.XmlElement()
  @Base64BinaryConverter()
  Uint8List element;

  ...
}

class Base64BinaryConverter implements annotation.XmlConverter<Uint8List> {
  const Base64BinaryConverter();

  @override
  Uint8List fromXml(String value) => base64.decode(value);

  @override
  String toXml(Uint8List object) => base64.encode(object);
}

Two is simpler than One but it doesn't support creating an instance of the custom type using nested children.

Four is the equivalent of Two using a custom XmlConverter annotation instead of fromXml and toXml fields.

@Matej-Hlatky
Copy link

Hi @tnc1997 ,
personally, I couldn't find any real use case for XML element conversion, but only attribute (aka scalar type) --
String to base64binary, time, ...

Of course, sometimes you need to map whole XML document (or JSON Object, GraphQL Fragment) to your specific model,
however this shoudl be dealt with in Domain layer.

Also usage of types of converted types instead of "raw" XML elements from XSD schema or other documentation could make it not only harder to write and validate, but then also harder to debug just by comparing the structure "optically" and also writing tests.

So let's stick to the attribute (scalar) mapping; same as in json_convert for JSON and GraphQL does it.

@tnc1997
Copy link
Owner Author

tnc1997 commented Oct 8, 2023

I couldn't find any real use case for XML element conversion.

It looks like JsonConvert allows conversion to/from Map<String, dynamic>, which would allow converting a nested object (e.g. {"date": "2023-10-08", "time": "12:00:00"}) to/from a type (e.g. DateTime). In theory a Map<String, dynamic> is somewhat synonymous with an XmlElement. That being said, I do agree that opting for just scalar mapping is easier and simpler.

{
  "a": { "year": 2023, "month": 10, "day": 8 },
  "b": 1696793104943
}
@JsonSerializable()
class Example {
  @_CustomDateTimeConverter()
  final DateTime a;

  @_EpochDateTimeConverter()
  final DateTime b;

  ...
}

class _CustomDateTimeConverter implements JsonConverter<DateTime, Map<String, dynamic>> {
  const _CustomDateTimeConverter();

  @override
  DateTime fromJson(Map<String, dynamic> json) => DateTime(json['year'] as int, json['month'] as int, json['day'] as int);

  @override
  Map<String, dynamic> toJson(DateTime object) => {'year': object.year, 'month': object.month, 'day': object.day};
}

class _EpochDateTimeConverter implements JsonConverter<DateTime, int> {
  const _EpochDateTimeConverter();

  @override
  DateTime fromJson(int json) => DateTime.fromMillisecondsSinceEpoch(json);

  @override
  int toJson(DateTime object) => object.millisecondsSinceEpoch;
}
<?xml version="1.0" encoding="UTF-8"?>
<example b="1696793104943">
    <a>
        <year>2023</year>
        <month>10</month>
        <day>8</day>
    </a>
</example>
@annotation.XmlSerializable()
class Example {
  @annotation.XmlElement()
  @_CustomDateTimeConverter()
  final DateTime a;

  @annotation.XmlAttribute()
  @_EpochDateTimeConverter()
  final DateTime b;

  ...
}

class _CustomDateTimeConverter implements annotation.XmlElementConverter<DateTime> {
  const _CustomDateTimeConverter();

  @override
  DateTime fromXmlElement(XmlElement element) => DateTime(
    int.parse(element.getElement('year')!.getText()!),
    int.parse(element.getElement('month')!.getText()!),
    int.parse(element.getElement('day')!.getText()!),
  );

  @override
  XmlElement toXmlElement(DateTime object) => XmlElement(
    XmlName('???'),
    [],
    [
      XmlElement(XmlName('year'), [], [XmlText(object.year.toString())]),
      XmlElement(XmlName('month'), [], [XmlText(object.month.toString())]),
      XmlElement(XmlName('day'), [], [XmlText(object.day.toString())]),
    ],
  );
}

class _EpochDateTimeConverter implements annotation.XmlAttributeConverter<DateTime> {
  const _EpochDateTimeConverter();

  @override
  DateTime fromXmlAttribute(XmlAttribute attribute) => DateTime.fromMillisecondsSinceEpoch(int.parse(attribute.value));

  @override
  XmlAttribute toXmlAttribute(DateTime object) => XmlAttribute(XmlName('???'), object.millisecondsSinceEpoch.toString());
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants