Skip to content

Commit

Permalink
refactor: split out @Copyable
Browse files Browse the repository at this point in the history
  • Loading branch information
felangel committed May 18, 2024
1 parent f89cf06 commit ee58cf8
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 104 deletions.
1 change: 1 addition & 0 deletions lib/data_class_macro.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:collection/collection.dart';

export 'src/constructable_macro.dart' show Constructable;
export 'src/copyable_macro.dart' show Copyable;
export 'src/data_macro.dart' show Data;
export 'src/equatable_macro.dart' show Equatable;
export 'src/stringify_macro.dart' show Stringify;
Expand Down
142 changes: 142 additions & 0 deletions lib/src/copyable_macro.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import 'dart:core';

import 'package:collection/collection.dart';
import 'package:data_class_macro/src/macro_extensions.dart';
import 'package:macros/macros.dart';

/// {@template copyable}
/// An experimental macro which generates a copyWith method.
///
/// ```dart
/// @Copyable()
/// class Person {
/// const Person({required this.name});
/// final String name;
/// }
///
/// void main() {
/// // Generated `copyWith`
/// print(Person(name: 'Dash').copyWith(name: () => 'Sparky').name); // Sparky
/// }
/// ```
/// {@endtemplate}
macro class Copyable implements ClassDeclarationsMacro, ClassDefinitionMacro {
/// {@macro copyable}
const Copyable();

@override
Future<void> buildDeclarationsForClass(
ClassDeclaration clazz,
MemberDeclarationBuilder builder,
) {
return _declareCopyWith(clazz, builder);
}

@override
Future<void> buildDefinitionForClass(
ClassDeclaration clazz,
TypeDefinitionBuilder builder,
) {
return _buildCopyWith(clazz, builder);
}

Future<void> _declareCopyWith(
ClassDeclaration clazz,
MemberDeclarationBuilder builder,
) async {
final fieldDeclarations = await builder.fieldsOf(clazz);
final fields = await Future.wait(
fieldDeclarations.map(
(f) async => (
identifier: f.identifier,
type: checkNamedType(f.type, builder),
),
),
);

final missingType = fields.firstWhereOrNull((f) => f.type == null);
if (missingType != null) return null;

if (fields.isEmpty) {
return builder.declareInType(
DeclarationCode.fromString(
'external ${clazz.identifier.name} copyWith();',
),
);
}

return builder.declareInType(
DeclarationCode.fromParts(
[
'external ${clazz.identifier.name} copyWith({',
for (final field in fields)
...[field.type!.identifier.name, if(field.type!.isNullable) '?', ' Function()? ', field.identifier.name, ',']
,'});',
],
),
);
}

Future<void> _buildCopyWith(
ClassDeclaration clazz,
TypeDefinitionBuilder builder,
) async {
final methods = await builder.methodsOf(clazz);
final copyWith = methods.firstWhereOrNull(
(m) => m.identifier.name == 'copyWith',
);
if (copyWith == null) return;
final copyWithMethod = await builder.buildMethod(copyWith.identifier);
final clazzName = clazz.identifier.name;
final fieldDeclarations = await builder.fieldsOf(clazz);
final fields = await Future.wait(
fieldDeclarations.map(
(f) async => (
identifier: f.identifier,
rawType: f.type,
type: checkNamedType(f.type, builder),
),
),
);
final docComments = CommentCode.fromString('/// Create a copy of [$clazzName] and replace zero or more fields.');

if (fields.isEmpty) {
return copyWithMethod.augment(
FunctionBodyCode.fromParts(
[
'=> ',
clazzName,
'();',
],
),
docComments: docComments,
);
}

final missingType = fields.firstWhereOrNull((f) => f.type == null);

if (missingType != null) {
return copyWithMethod.augment(
FunctionBodyCode.fromString(
'=> throw "Unable to copyWith to due missing type ${missingType.rawType.code.debugString}',
),
docComments: docComments,
);
}

return copyWithMethod.augment(
FunctionBodyCode.fromParts(
[
'=> ',
clazzName,
'(',
for (final field in fields)
...[field.identifier.name, ': ', field.identifier.name, '!= null ? ',field.identifier.name, '.call()', ' : this.',field.identifier.name, ','],
');'
],
),
docComments: docComments,
);
}
}

105 changes: 2 additions & 103 deletions lib/src/data_macro.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'dart:core';

import 'package:collection/collection.dart';
import 'package:data_class_macro/data_class_macro.dart';
import 'package:data_class_macro/src/macro_extensions.dart';
import 'package:macros/macros.dart';

/// {@template data}
Expand Down Expand Up @@ -42,7 +40,7 @@ macro class Data implements ClassDeclarationsMacro, ClassDefinitionMacro {
const Constructable().buildDeclarationsForClass(clazz, builder),
const Equatable().buildDeclarationsForClass(clazz, builder),
const Stringify().buildDeclarationsForClass(clazz, builder),
_declareCopyWith(clazz, builder),
const Copyable().buildDeclarationsForClass(clazz, builder),
]);
}

Expand All @@ -54,107 +52,8 @@ macro class Data implements ClassDeclarationsMacro, ClassDefinitionMacro {
return Future.wait([
const Equatable().buildDefinitionForClass(clazz, builder),
const Stringify().buildDefinitionForClass(clazz, builder),
_buildCopyWith(clazz, builder),
const Copyable().buildDefinitionForClass(clazz, builder),
]);
}

Future<void> _declareCopyWith(
ClassDeclaration clazz,
MemberDeclarationBuilder builder,
) async {
final fieldDeclarations = await builder.fieldsOf(clazz);
final fields = await Future.wait(
fieldDeclarations.map(
(f) async => (
identifier: f.identifier,
type: checkNamedType(f.type, builder),
),
),
);

final missingType = fields.firstWhereOrNull((f) => f.type == null);
if (missingType != null) return null;

if (fields.isEmpty) {
return builder.declareInType(
DeclarationCode.fromString(
'external ${clazz.identifier.name} copyWith();',
),
);
}

return builder.declareInType(
DeclarationCode.fromParts(
[
'external ${clazz.identifier.name} copyWith({',
for (final field in fields)
...[field.type!.identifier.name, if(field.type!.isNullable) '?', ' Function()? ', field.identifier.name, ',']
,'});',
],
),
);
}

Future<void> _buildCopyWith(
ClassDeclaration clazz,
TypeDefinitionBuilder builder,
) async {
final methods = await builder.methodsOf(clazz);
final copyWith = methods.firstWhereOrNull(
(m) => m.identifier.name == 'copyWith',
);
if (copyWith == null) return;
final copyWithMethod = await builder.buildMethod(copyWith.identifier);
final clazzName = clazz.identifier.name;
final fieldDeclarations = await builder.fieldsOf(clazz);
final fields = await Future.wait(
fieldDeclarations.map(
(f) async => (
identifier: f.identifier,
rawType: f.type,
type: checkNamedType(f.type, builder),
),
),
);
final docComments = CommentCode.fromString('/// Create a copy of [$clazzName] and replace zero or more fields.');

if (fields.isEmpty) {
return copyWithMethod.augment(
FunctionBodyCode.fromParts(
[
'=> ',
clazzName,
'();',
],
),
docComments: docComments,
);
}

final missingType = fields.firstWhereOrNull((f) => f.type == null);

if (missingType != null) {
return copyWithMethod.augment(
FunctionBodyCode.fromString(
'=> throw "Unable to copyWith to due missing type ${missingType.rawType.code.debugString}',
),
docComments: docComments,
);
}

return copyWithMethod.augment(
FunctionBodyCode.fromParts(
[
'=> ',
clazzName,
'(',
for (final field in fields)
...[field.identifier.name, ': ', field.identifier.name, '!= null ? ',field.identifier.name, '.call()', ' : this.',field.identifier.name, ','],
');'
],
),
docComments: docComments,
);
}
}

57 changes: 57 additions & 0 deletions test/src/copyable_macro_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:data_class_macro/data_class_macro.dart';
import 'package:test/test.dart';

@Copyable()
class EmptyClass {
const EmptyClass();
}

@Copyable()
class StringFieldClass {
const StringFieldClass({required this.value});
final String value;
}

@Copyable()
class NullableStringFieldClass {
const NullableStringFieldClass({this.value});
final String? value;
}

void main() {
group(EmptyClass, () {
test('copyWith', () {
expect(const EmptyClass().copyWith(), isA<EmptyClass>());
});
});

group(StringFieldClass, () {
test('copyWith', () {
const instance = const StringFieldClass(value: 'hello');
expect(instance.copyWith().value, equals('hello'));
expect(
instance.copyWith(value: () => 'bye').value,
equals('bye'),
);
});
});

group(NullableStringFieldClass, () {
test('copyWith', () {
final nonNullInstance = const NullableStringFieldClass(value: 'hello');
final nullInstance = const NullableStringFieldClass(value: null);
expect(nullInstance.copyWith().value, isNull);
expect(nonNullInstance.copyWith().value, equals('hello'));
expect(nullInstance.copyWith(value: null).value, isNull);
expect(nonNullInstance.copyWith(value: null).value, equals('hello'));
expect(
nullInstance.copyWith(value: () => 'hello').value,
equals('hello'),
);
expect(
nonNullInstance.copyWith(value: () => null).value,
isNull,
);
});
});
}
2 changes: 1 addition & 1 deletion test/src/data_macro_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:data_class_macro/src/data_macro.dart';
import 'package:data_class_macro/data_class_macro.dart';
import 'package:test/test.dart';

@Data()
Expand Down

0 comments on commit ee58cf8

Please sign in to comment.