mirror of https://github.com/AMT-Cheif/drift.git
423 lines
13 KiB
Dart
423 lines
13 KiB
Dart
import '../../analysis/results/results.dart';
|
|
import '../../utils/string_escaper.dart';
|
|
import '../utils/hash_and_equals.dart';
|
|
import '../utils/override_toString.dart';
|
|
import '../writer.dart';
|
|
|
|
class DataClassWriter {
|
|
final DriftElementWithResultSet table;
|
|
final Scope scope;
|
|
|
|
List<DriftColumn> get columns => table.columns;
|
|
|
|
bool get isInsertable => table is DriftTable;
|
|
|
|
final TextEmitter _emitter;
|
|
StringBuffer get _buffer => _emitter.buffer;
|
|
|
|
DataClassWriter(this.table, this.scope) : _emitter = scope.leaf();
|
|
|
|
String get serializerType => _emitter.drift('ValueSerializer?');
|
|
|
|
String _columnType(DriftColumn column) {
|
|
return _emitter.dartCode(_emitter.dartType(column));
|
|
}
|
|
|
|
String _jsonType(DriftColumn column) {
|
|
final converter = column.typeConverter;
|
|
if (converter != null && converter.alsoAppliesToJsonConversion) {
|
|
final nullable = converter.canBeSkippedForNulls && column.nullable;
|
|
final code = AnnotatedDartCode([
|
|
...AnnotatedDartCode.type(converter.jsonType!).elements,
|
|
if (nullable) '?',
|
|
]);
|
|
|
|
return _emitter.dartCode(code);
|
|
} else {
|
|
return _columnType(column);
|
|
}
|
|
}
|
|
|
|
String _converter(DriftColumn column) {
|
|
return _emitter.dartCode(_emitter.writer
|
|
.readConverter(column.typeConverter!, forNullable: column.nullable));
|
|
}
|
|
|
|
void write() {
|
|
final customParent = table.customParentClass;
|
|
final parentClass = customParent != null
|
|
? _emitter.dartCode(customParent.parentClass)
|
|
: _emitter.drift('DataClass');
|
|
_buffer.write('class ${table.nameOfRowClass} extends $parentClass ');
|
|
|
|
if (isInsertable) {
|
|
if (scope.options.writeToColumnsMixins) {
|
|
_buffer.writeln('with ${table.entityInfoName}ToColumns {');
|
|
} else {
|
|
// The data class is only an insertable if we can actually insert rows
|
|
// into the target entity.
|
|
final type = _emitter.dartCode(_emitter.writer.rowType(table));
|
|
|
|
_buffer.writeln('implements ${_emitter.drift('Insertable')}<$type> {');
|
|
}
|
|
} else {
|
|
_buffer.writeln('{');
|
|
}
|
|
|
|
// write individual fields
|
|
for (final column in columns) {
|
|
if (column.documentationComment != null) {
|
|
_buffer.write('${column.documentationComment}\n');
|
|
}
|
|
if (scope.options.writeToColumnsMixins) {
|
|
_buffer.writeln('@override');
|
|
}
|
|
final modifier = scope.options.fieldModifier;
|
|
_buffer.writeln('$modifier ${_columnType(column)} ${column.nameInDart};');
|
|
}
|
|
|
|
// write constructor with named optional fields
|
|
if (!scope.options.generateMutableClasses &&
|
|
customParent?.isConst != false) {
|
|
_buffer.write('const ');
|
|
}
|
|
_emitter
|
|
..write(table.nameOfRowClass)
|
|
..write('({')
|
|
..write(columns.map((column) {
|
|
final nullableDartType = column.typeConverter != null
|
|
? column.typeConverter!.mapsToNullableDart(column.nullable)
|
|
: column.nullable;
|
|
|
|
if (nullableDartType) {
|
|
return 'this.${column.nameInDart}';
|
|
} else {
|
|
return 'required this.${column.nameInDart}';
|
|
}
|
|
}).join(', '))
|
|
..write('});');
|
|
|
|
if (isInsertable) {
|
|
// If we generate mixins for the `toColumns` override, we don't need to
|
|
// generate a duplicate method in the data class.
|
|
if (!scope.options.writeToColumnsMixins) {
|
|
_emitter.writeToColumnsOverride(columns);
|
|
}
|
|
if (scope.options.dataClassToCompanions &&
|
|
scope.generationOptions.writeCompanions) {
|
|
_writeToCompanion();
|
|
}
|
|
}
|
|
|
|
// And a serializer and deserializer method
|
|
_writeFromJson();
|
|
_writeToJson();
|
|
|
|
// And a convenience method to copy data from this class.
|
|
_writeCopyWith();
|
|
|
|
_writeToString();
|
|
_writeHashCode();
|
|
|
|
overrideEquals(
|
|
columns
|
|
.map((c) => EqualityField(c.nameInDart, isList: c.isUint8ListInDart)),
|
|
_emitter.dartCode(_emitter.writer.rowClass(table)),
|
|
_emitter,
|
|
);
|
|
|
|
// finish class declaration
|
|
_buffer.write('}');
|
|
}
|
|
|
|
void _writeFromJson() {
|
|
final dataClassName = table.nameOfRowClass;
|
|
|
|
_buffer
|
|
..write('factory $dataClassName.fromJson('
|
|
'Map<String, dynamic> json, {$serializerType serializer}'
|
|
') {\n')
|
|
..write('serializer ??= ${_emitter.drift('driftRuntimeOptions')}'
|
|
'.defaultSerializer;\n')
|
|
..write('return $dataClassName(');
|
|
|
|
for (final column in columns) {
|
|
final getter = column.nameInDart;
|
|
final jsonKeyString = asDartLiteral(column.getJsonKey(scope.options));
|
|
String deserialized;
|
|
|
|
final typeConverter = column.typeConverter;
|
|
if (typeConverter != null && typeConverter.alsoAppliesToJsonConversion) {
|
|
var type =
|
|
_emitter.dartCode(AnnotatedDartCode.type(typeConverter.jsonType!));
|
|
if (column.nullable && typeConverter.canBeSkippedForNulls) {
|
|
type = '$type?';
|
|
}
|
|
|
|
final fromConverter =
|
|
"serializer.fromJson<$type>(json[$jsonKeyString])";
|
|
final converterField = _converter(column);
|
|
deserialized = '$converterField.fromJson($fromConverter)';
|
|
} else {
|
|
final type = _columnType(column);
|
|
|
|
deserialized = "serializer.fromJson<$type>(json[$jsonKeyString])";
|
|
}
|
|
|
|
_buffer.write('$getter: $deserialized,');
|
|
}
|
|
|
|
_buffer.write(');}\n');
|
|
|
|
if (scope.writer.options.generateFromJsonStringConstructor) {
|
|
// also generate a constructor that only takes a json string
|
|
_buffer.write('factory $dataClassName.fromJsonString(String encodedJson, '
|
|
'{$serializerType serializer}) => '
|
|
'$dataClassName.fromJson('
|
|
'DataClass.parseJson(encodedJson) as Map<String, dynamic>, '
|
|
'serializer: serializer);');
|
|
}
|
|
}
|
|
|
|
void _writeToJson() {
|
|
_buffer.write('@override Map<String, dynamic> toJson('
|
|
'{$serializerType serializer}) {\n'
|
|
'serializer ??= ${_emitter.drift('driftRuntimeOptions')}'
|
|
'.defaultSerializer;\n'
|
|
'return <String, dynamic>{\n');
|
|
|
|
for (final column in columns) {
|
|
final nameLiteral = asDartLiteral(column.getJsonKey(scope.options));
|
|
final getter = column.nameInDart;
|
|
final needsThis = getter == 'serializer';
|
|
var value = needsThis ? 'this.$getter' : getter;
|
|
var dartType = _columnType(column);
|
|
|
|
final typeConverter = column.typeConverter;
|
|
if (typeConverter != null && typeConverter.alsoAppliesToJsonConversion) {
|
|
final converterField = _converter(column);
|
|
value = '$converterField.toJson($value)';
|
|
dartType = _jsonType(column);
|
|
}
|
|
|
|
_buffer.write("$nameLiteral: serializer.toJson<$dartType>($value),");
|
|
}
|
|
|
|
_buffer.write('};}');
|
|
}
|
|
|
|
void _writeCopyWith() {
|
|
final dataClassName = _emitter.dartCode(_emitter.writer.rowClass(table));
|
|
final wrapNullableInValue = scope.options.generateValuesInCopyWith;
|
|
final valueType = _emitter.drift('Value');
|
|
|
|
_buffer.write('$dataClassName copyWith({');
|
|
for (var i = 0; i < columns.length; i++) {
|
|
final column = columns[i];
|
|
final last = i == columns.length - 1;
|
|
final isNullable = column.nullableInDart;
|
|
|
|
final typeName = _columnType(column);
|
|
if (wrapNullableInValue && isNullable) {
|
|
_buffer
|
|
..write('$valueType<$typeName> ${column.nameInDart} ')
|
|
..write('= const $valueType.absent()');
|
|
} else if (!isNullable) {
|
|
// We always use nullable parameters in copyWith, since all parameters
|
|
// are optional. The !isNullable check is there to avoid a duplicate
|
|
// question mark in the type name.
|
|
_buffer.write('$typeName? ${column.nameInDart}');
|
|
} else {
|
|
_buffer.write('$typeName ${column.nameInDart}');
|
|
}
|
|
|
|
if (!last) {
|
|
_buffer.write(',');
|
|
}
|
|
}
|
|
|
|
_buffer.write('}) => $dataClassName(');
|
|
|
|
for (final column in columns) {
|
|
// We also have a method parameter called like the getter, so we can use
|
|
// field: field ?? this.field. If we wrapped the parameter in a `Value`,
|
|
// we can use field.present ? field.value : this.field
|
|
final getter = column.nameInDart;
|
|
|
|
if (wrapNullableInValue && column.nullableInDart) {
|
|
_buffer
|
|
.write('$getter: $getter.present ? $getter.value : this.$getter,');
|
|
} else {
|
|
_buffer.write('$getter: $getter ?? this.$getter,');
|
|
}
|
|
}
|
|
|
|
_buffer.write(');');
|
|
}
|
|
|
|
void _writeToCompanion() {
|
|
final asTable = table as DriftTable;
|
|
final companionType = _emitter.writer.companionType(asTable);
|
|
|
|
_emitter
|
|
..writeDart(companionType)
|
|
..writeln(' toCompanion(bool nullToAbsent) {')
|
|
..write('return ')
|
|
..writeDart(companionType)
|
|
..write('(');
|
|
|
|
for (final column in columns) {
|
|
// Generated columns are not parts of companions.
|
|
if (column.isGenerated) continue;
|
|
|
|
final dartName = column.nameInDart;
|
|
_buffer
|
|
..write(dartName)
|
|
..write(': ');
|
|
|
|
final needsNullCheck = column.nullableInDart;
|
|
if (needsNullCheck) {
|
|
_buffer
|
|
..write(dartName)
|
|
..write(' == null && nullToAbsent ? '
|
|
'const ${_emitter.drift('Value')}.absent() : ');
|
|
// We'll write the non-null case afterwards
|
|
}
|
|
|
|
_buffer
|
|
..write(_emitter.drift('Value'))
|
|
..write('(')
|
|
..write(dartName)
|
|
..write('),');
|
|
}
|
|
|
|
_buffer.writeln(');\n}');
|
|
}
|
|
|
|
void _writeToString() {
|
|
overrideToString(
|
|
table.nameOfRowClass,
|
|
[for (final column in columns) column.nameInDart],
|
|
_buffer,
|
|
);
|
|
}
|
|
|
|
void _writeHashCode() {
|
|
_buffer.write('@override\n int get hashCode => ');
|
|
|
|
final fields = columns
|
|
.map((c) => EqualityField(c.nameInDart, isList: c.isUint8ListInDart))
|
|
.toList();
|
|
writeHashCode(fields, _emitter);
|
|
_buffer.write(';');
|
|
}
|
|
}
|
|
|
|
/// Generates code mapping a row (represented as a `Map`) to positional and
|
|
/// named Dart arguments.
|
|
class RowMappingWriter {
|
|
final List<DriftColumn> positional;
|
|
final Map<DriftColumn, String> named;
|
|
final DriftElementWithResultSet table;
|
|
final Writer writer;
|
|
|
|
/// Code to obtain an instance of a `DatabaseConnectionUser` in the generated
|
|
/// code.
|
|
///
|
|
/// This is used to lookup the connection options necessary for mapping values
|
|
/// from SQL to Dart.
|
|
final String databaseGetter;
|
|
|
|
RowMappingWriter({
|
|
required this.positional,
|
|
required this.table,
|
|
required this.writer,
|
|
required this.databaseGetter,
|
|
this.named = const {},
|
|
});
|
|
|
|
void writeArguments(StringBuffer buffer) {
|
|
String readAndMap(DriftColumn column) {
|
|
final columnName = column.nameInSql;
|
|
final rawData = "data['\${effectivePrefix}$columnName']";
|
|
|
|
String sqlType;
|
|
if (column.sqlType.custom case CustomColumnType custom) {
|
|
sqlType = writer.dartCode(custom.expression);
|
|
} else {
|
|
sqlType = writer.drift(column.sqlType.builtin.toString());
|
|
}
|
|
|
|
var loadType = '$databaseGetter.typeMapping.read($sqlType, $rawData)';
|
|
|
|
if (!column.nullable) {
|
|
loadType += '!';
|
|
}
|
|
|
|
// run the loaded expression though the custom converter for the final
|
|
// result.
|
|
if (column.typeConverter != null) {
|
|
// stored as a static field
|
|
final code = writer.readConverter(column.typeConverter!,
|
|
forNullable: column.nullable);
|
|
final writtenCode = writer.dartCode(code);
|
|
loadType = '$writtenCode.fromSql($loadType)';
|
|
}
|
|
|
|
return loadType;
|
|
}
|
|
|
|
buffer.write('(');
|
|
|
|
for (final column in positional) {
|
|
buffer
|
|
..write(readAndMap(column))
|
|
..write(', ');
|
|
}
|
|
|
|
named.forEach((column, parameterName) {
|
|
final getter = column.nameInDart;
|
|
buffer.write('$getter: ${readAndMap(column)}, ');
|
|
});
|
|
|
|
buffer.write(')');
|
|
}
|
|
}
|
|
|
|
extension WriteToColumns on TextEmitter {
|
|
void writeToColumnsOverride(Iterable<DriftColumn> columns) {
|
|
final expression = drift('Expression');
|
|
|
|
this
|
|
..write('@override\nMap<String, $expression> toColumns'
|
|
'(bool nullToAbsent) {\n')
|
|
..write('final map = <String, $expression> {};');
|
|
|
|
for (final column in columns) {
|
|
// Generated column - cannot be used for inserts or updates
|
|
if (column.isGenerated) continue;
|
|
|
|
// We include all columns that are not null. If nullToAbsent is false, we
|
|
// also include null columns. When generating NNBD code, we can include
|
|
// non-nullable columns without an additional null check since we know
|
|
// the values aren't going to be null.
|
|
final needsNullCheck = column.nullableInDart;
|
|
final needsScope = needsNullCheck || column.typeConverter != null;
|
|
if (needsNullCheck) {
|
|
write('if (!nullToAbsent || ${column.nameInDart} != null)');
|
|
}
|
|
if (needsScope) write('{');
|
|
|
|
write('map[${asDartLiteral(column.nameInSql)}] = ');
|
|
writeDart(
|
|
wrapInVariable(column, AnnotatedDartCode.text(column.nameInDart)));
|
|
writeln(';');
|
|
|
|
// This one closes the optional if from before.
|
|
if (needsScope) write('}');
|
|
}
|
|
|
|
write('return map; \n}\n');
|
|
}
|
|
}
|