Merge pull request #2102 from North101/develop

Decouple JsonTypeConverter from TypeConverter
This commit is contained in:
Simon Binder 2022-10-18 20:45:33 +02:00 committed by GitHub
commit eb840db4be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 140 additions and 41 deletions

View File

@ -10,7 +10,6 @@ extension Expressions on MyDatabase {
return (select(categories)..where((row) => hasNoTodo)).get();
}
// #enddocregion emptyCategories
}
// #docregion bitwise

View File

@ -123,6 +123,13 @@ If you want to apply the same conversion to JSON as well, make your type convert
You can also override the `toJson` and `fromJson` methods to customize serialization as long as the types
stay the compatible.
If you want to serialize to a different JSON type (e.g. you have a type converter `<MyObject, int>` in SQL but
want to map to a string in JSON), you'll have to write a custom [`ValueSerializer`](https://drift.simonbinder.eu/api/drift/valueserializer-class)
If the JSON type you want to serialize to is different to the SQL type you're
mapping to, you can mix-in `JsonTypeConverter2` instead.
For instance, say you have a type converter mapping to a complex Dart type
`MyObject`. In SQL, you might want to store this as an `String`. But when
serializing to JSON, you may want to use a `Map<String, Object?>`. Here, simply
add the `JsonTypeConverter2<MyObject, String, Map<String, Object?>>` mixin to
your type converter.
As an alternative to using JSON type converters, you can use a custom [`ValueSerializer`](https://drift.simonbinder.eu/api/drift/valueserializer-class)
and pass it to the serialization methods.

View File

@ -1,3 +1,8 @@
## 2.3.0-dev
- Add the `JsonTypeConverter2` mixin. It behaves similar to the existing json
type converters, but can use a different SQL and JSON type.
## 2.2.0
- Always escape column names, avoiding the costs of using a regular expression

View File

@ -1,2 +1,3 @@
// This field is analyzed by drift_dev to easily obtain common types.
export 'runtime/types/converters.dart' show TypeConverter, JsonTypeConverter;
export 'runtime/types/converters.dart'
show TypeConverter, JsonTypeConverter, JsonTypeConverter2;

View File

@ -29,22 +29,21 @@ abstract class TypeConverter<D, S> {
/// A mixin for [TypeConverter]s that should also apply to drift's builtin
/// JSON serialization of data classes.
///
/// By default, a [TypeConverter] only applies to the serialization from Dart
/// to SQL (and vice-versa).
/// When a [BuildGeneralColumn.map] column (or a `MAPPED BY` constraint in
/// `.drift` files) refers to a type converter that inherits from
/// [JsonTypeConverter], it will also be used for the conversion from and to
/// JSON.
mixin JsonTypeConverter<D, S> on TypeConverter<D, S> {
/// Unlike the old [JsonTypeConverter] mixin, this more general mixin allows
/// using a different type when serializing to JSON ([J]) than the type used in
/// SQL ([S]).
/// For the cases where the JSON serialization and the mapping to SQL use the
/// same types, it may be more convenient to mix-in [JsonTypeConverter] instead.
mixin JsonTypeConverter2<D, S, J> on TypeConverter<D, S> {
/// Map a value from the Data class to json.
///
/// Defaults to doing the same conversion as for Dart -> SQL, [toSql].
S toJson(D value) => toSql(value);
J toJson(D value);
/// Map a value from json to something understood by the data class.
///
/// Defaults to doing the same conversion as for SQL -> Dart, [toSql].
D fromJson(S json) => fromSql(json);
D fromJson(J json);
/// Wraps an [inner] type converter that only considers non-nullable values
/// as a type converter that handles null values too.
@ -52,12 +51,35 @@ mixin JsonTypeConverter<D, S> on TypeConverter<D, S> {
/// The returned type converter will use the [inner] type converter for non-
/// null values. Further, `null` is mapped to `null` in both directions (from
/// Dart to SQL and vice-versa).
static JsonTypeConverter<D?, S?> asNullable<D, S extends Object>(
TypeConverter<D, S> inner) {
static JsonTypeConverter2<D?, S?, J?>
asNullable<D, S extends Object, J extends Object>(
JsonTypeConverter2<D, S, J> inner) {
return _NullWrappingTypeConverterWithJson(inner);
}
}
/// A mixin for [TypeConverter]s that should also apply to drift's builtin
/// JSON serialization of data classes.
///
/// By default, a [TypeConverter] only applies to the serialization from Dart
/// to SQL (and vice-versa).
/// When a [BuildGeneralColumn.map] column (or a `MAPPED BY` constraint in
/// `.drift` files) refers to a type converter that inherits from
/// [JsonTypeConverter], it will also be used for the conversion from and to
/// JSON.
///
/// If the serialized JSON has a different type than the type in SQL ([S]), use
/// a [JsonTypeConverter2]. For instance, this could be useful if your type
/// converter between Dart and SQL maps to a string in SQL, but to a `Map` in
/// JSON.
mixin JsonTypeConverter<D, S> implements JsonTypeConverter2<D, S, S> {
@override
S toJson(D value) => toSql(value);
@override
D fromJson(S json) => fromSql(json);
}
/// Implementation for an enum to int converter that uses the index of the enum
/// as the value stored in the database.
class EnumIndexConverter<T extends Enum> extends TypeConverter<T, int> {
@ -150,9 +172,10 @@ class _NullWrappingTypeConverter<D, S extends Object>
S requireToSql(D value) => _inner.toSql(value);
}
class _NullWrappingTypeConverterWithJson<D, S extends Object>
extends NullAwareTypeConverter<D, S> with JsonTypeConverter<D?, S?> {
final TypeConverter<D, S> _inner;
class _NullWrappingTypeConverterWithJson<D, S extends Object, J extends Object>
extends NullAwareTypeConverter<D, S>
implements JsonTypeConverter2<D?, S?, J?> {
final JsonTypeConverter2<D, S, J> _inner;
const _NullWrappingTypeConverterWithJson(this._inner);
@ -161,4 +184,18 @@ class _NullWrappingTypeConverterWithJson<D, S extends Object>
@override
S requireToSql(D value) => _inner.toSql(value);
D requireFromJson(J json) => _inner.fromJson(json);
@override
D? fromJson(J? json) {
return json == null ? null : requireFromJson(json);
}
J? requireToJson(D? value) => _inner.toJson(value as D);
@override
J? toJson(D? value) {
return value == null ? null : requireToJson(value);
}
}

View File

@ -1,6 +1,6 @@
name: drift
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
version: 2.2.0
version: 2.3.0-dev
repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues

View File

@ -135,8 +135,18 @@ class CustomConverter extends TypeConverter<MyCustomObject, String> {
}
class CustomJsonConverter extends CustomConverter
with JsonTypeConverter<MyCustomObject, String> {
with JsonTypeConverter2<MyCustomObject, String, Map> {
const CustomJsonConverter();
@override
MyCustomObject fromJson(Map json) {
return MyCustomObject(json['data'] as String);
}
@override
Map toJson(MyCustomObject value) {
return {'data': value.data};
}
}
abstract class CategoryTodoCountView extends View {

View File

@ -1252,7 +1252,7 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
serializer ??= driftRuntimeOptions.defaultSerializer;
return PureDefault(
txt: $PureDefaultsTable.$converter0n
.fromJson(serializer.fromJson<String?>(json['txt'])),
.fromJson(serializer.fromJson<Map<dynamic, dynamic>>(json['txt'])),
);
}
factory PureDefault.fromJsonString(String encodedJson,
@ -1264,8 +1264,8 @@ class PureDefault extends DataClass implements Insertable<PureDefault> {
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'txt': serializer
.toJson<String?>($PureDefaultsTable.$converter0n.toJson(txt)),
'txt': serializer.toJson<Map<dynamic, dynamic>?>(
$PureDefaultsTable.$converter0n.toJson(txt)),
};
}
@ -1373,10 +1373,10 @@ class $PureDefaultsTable extends PureDefaults
return $PureDefaultsTable(attachedDatabase, alias);
}
static JsonTypeConverter<MyCustomObject, String> $converter0 =
const CustomJsonConverter();
static JsonTypeConverter<MyCustomObject?, String?> $converter0n =
JsonTypeConverter.asNullable($converter0);
static JsonTypeConverter2<MyCustomObject, String, Map<dynamic, dynamic>>
$converter0 = const CustomJsonConverter();
static JsonTypeConverter2<MyCustomObject?, String?, Map<dynamic, dynamic>?>
$converter0n = JsonTypeConverter2.asNullable($converter0);
}
class CategoryTodoCountViewData extends DataClass {

View File

@ -82,8 +82,12 @@ void main() {
});
test('applies json type converter', () {
expect(PureDefault(txt: MyCustomObject('foo')).toJson(), {'txt': 'foo'});
expect(PureDefault.fromJson({'txt': 'foo'}),
const serialized = {
'txt': {'data': 'foo'}
};
expect(PureDefault(txt: MyCustomObject('foo')).toJson(), serialized);
expect(PureDefault.fromJson(serialized),
PureDefault(txt: MyCustomObject('foo')));
});

View File

@ -152,6 +152,8 @@ UsedTypeConverter? readTypeConverter(
final staticType = dartExpression.staticType;
final asTypeConverter =
staticType != null ? helper.asTypeConverter(staticType) : null;
final asJsonTypeConverter =
staticType != null ? helper.asJsonTypeConverter(staticType) : null;
if (asTypeConverter == null) {
reportError('Not a type converter');
@ -160,6 +162,7 @@ UsedTypeConverter? readTypeConverter(
final dartType = asTypeConverter.typeArguments[0];
final sqlType = asTypeConverter.typeArguments[1];
final jsonType = asJsonTypeConverter?.typeArguments[2] ?? sqlType;
final typeSystem = library.typeSystem;
final dartTypeNullable = typeSystem.isNullable(dartType);
@ -178,7 +181,7 @@ UsedTypeConverter? readTypeConverter(
"potentially map to `null` which can't be stored in the database.");
} else if (!canBeSkippedForNulls) {
final alternative = appliesToJsonToo
? 'JsonTypeConverter.asNullable'
? 'JsonTypeConverter2.asNullable'
: 'NullAwareTypeConverter.wrap';
reportError('This column is nullable, but the type converter has a non-'
@ -195,6 +198,7 @@ UsedTypeConverter? readTypeConverter(
expression: dartExpression.toSource(),
dartType: resolvedDartType ?? DriftDartType.of(dartType),
sqlType: sqlType,
jsonType: jsonType,
dartTypeIsNullable: dartTypeNullable,
sqlTypeIsNullable: sqlTypeNullable,
alsoAppliesToJsonConversion: appliesToJsonToo,

View File

@ -26,13 +26,24 @@ class HelperLibrary {
return type.asInstanceOf(converter);
}
/// Converts the given Dart [type] into an instantiation of the
/// `JsonTypeConverter` class from drift.
///
/// Returns `null` if [type] is not a subtype of `TypeConverter`.
InterfaceType? asJsonTypeConverter(DartType type) {
final converter = helperLibrary.exportNamespace.get('JsonTypeConverter2')
as InterfaceElement;
return type.asInstanceOf(converter);
}
bool isJsonAwareTypeConverter(DartType? type, LibraryElement context) {
final jsonMixin = helperLibrary.exportNamespace.get('JsonTypeConverter')
final jsonMixin = helperLibrary.exportNamespace.get('JsonTypeConverter2')
as InterfaceElement;
final jsonConverterType = jsonMixin.instantiate(
typeArguments: [
context.typeProvider.dynamicType,
context.typeProvider.dynamicType
context.typeProvider.dynamicType,
context.typeProvider.dynamicType,
],
nullabilitySuffix: NullabilitySuffix.none,
);

View File

@ -136,6 +136,19 @@ extension OperationOnTypes on HasType {
return variableTypeCode();
}
/// The Dart type that matches the values of this column when serialized to
/// JSON.
String jsonTypeCode() {
final converter = typeConverter;
if (converter != null) {
var inner = converter.jsonType.codeString();
if (converter.canBeSkippedForNulls && nullable) inner += '?';
return isArray ? 'List<$inner>' : inner;
}
return variableTypeCode();
}
}
const Map<DriftSqlType, String> dartTypeNames = {

View File

@ -31,6 +31,10 @@ class UsedTypeConverter {
/// mapped values in the database.
final DartType sqlType;
/// The "JSON" type of this type converter. This is the type used to represent
/// mapped values in the database.
final DartType jsonType;
/// Whether the Dart-value output of this type converter is nullable.
///
/// In other words, [dartType] is potentially nullable.
@ -80,6 +84,7 @@ class UsedTypeConverter {
required this.expression,
required this.dartType,
required this.sqlType,
required this.jsonType,
required this.dartTypeIsNullable,
required this.sqlTypeIsNullable,
this.alsoAppliesToJsonConversion = false,
@ -115,6 +120,7 @@ class UsedTypeConverter {
sqlTypeIsNullable: false,
dartTypeIsNullable: false,
sqlType: typeProvider.intType,
jsonType: typeProvider.intType,
);
}
@ -133,11 +139,13 @@ class UsedTypeConverter {
String converterNameInCode({bool makeNullable = false}) {
var sqlDartType = sqlType.getDisplayString(withNullability: true);
if (makeNullable) sqlDartType += '?';
var jsonDartType = jsonType.getDisplayString(withNullability: true);
if (makeNullable) jsonDartType += '?';
final className =
alsoAppliesToJsonConversion ? 'JsonTypeConverter' : 'TypeConverter';
return '$className<${dartTypeCode(makeNullable)}, $sqlDartType>';
if (alsoAppliesToJsonConversion) {
return 'JsonTypeConverter2<${dartTypeCode(makeNullable)}, $sqlDartType, $jsonDartType>';
}
return 'TypeConverter<${dartTypeCode(makeNullable)}, $sqlDartType>';
}
}

View File

@ -107,7 +107,7 @@ class DataClassWriter {
final typeConverter = column.typeConverter;
if (typeConverter != null && typeConverter.alsoAppliesToJsonConversion) {
final type = column.innerColumnType(nullable: column.nullable);
final type = typeConverter.jsonType;
final fromConverter = "serializer.fromJson<$type>(json['$jsonKey'])";
final converterField =
typeConverter.tableAndField(forNullableColumn: column.nullable);
@ -151,7 +151,7 @@ class DataClassWriter {
final converterField =
typeConverter.tableAndField(forNullableColumn: column.nullable);
value = '$converterField.toJson($value)';
dartType = column.innerColumnType(nullable: true);
dartType = column.jsonTypeCode();
}
_buffer.write("'$name': serializer.toJson<$dartType>($value),");

View File

@ -336,7 +336,7 @@ class TableWriter extends TableOrViewWriter {
converter.converterNameInCode(makeNullable: true);
final wrap = converter.alsoAppliesToJsonConversion
? 'JsonTypeConverter.asNullable'
? 'JsonTypeConverter2.asNullable'
: 'NullAwareTypeConverter.wrap';
final code = '$wrap(${converter.fieldName})';

View File

@ -1,6 +1,6 @@
name: drift_dev
description: Dev-dependency for users of drift. Contains a the generator and development tools.
version: 2.2.0+1
version: 2.3.0-dev
repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues
@ -25,7 +25,7 @@ dependencies:
io: ^1.0.3
# Drift-specific analysis and apis
drift: '>=2.0.0 <2.3.0'
drift: '>=2.3.0 <2.4.0'
sqlite3: '>=0.1.6 <2.0.0'
sqlparser: ^0.23.3