Skip to content

Commit e5059f9

Browse files
committed
start on json serializable macro
1 parent a450d64 commit e5059f9

File tree

3 files changed

+153
-2
lines changed

3 files changed

+153
-2
lines changed

working/macros/example/bin/run.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ void main() async {
2121
var dataClassUri = Uri.parse('package:macro_proposal/data_class.dart');
2222
var observableUri = Uri.parse('package:macro_proposal/observable.dart');
2323
var autoDisposableUri = Uri.parse('package:macro_proposal/auto_dispose.dart');
24+
var jsonSerializableUri =
25+
Uri.parse('package:macro_proposal/json_serializable.dart');
2426
var bootstrapContent = bootstrapMacroIsolate({
2527
dataClassUri.toString(): {
2628
'AutoConstructor': [''],
@@ -35,6 +37,9 @@ void main() async {
3537
autoDisposableUri.toString(): {
3638
'AutoDispose': [''],
3739
},
40+
jsonSerializableUri.toString(): {
41+
'JsonSerializable': [''],
42+
}
3843
}, SerializationMode.byteDataClient);
3944
bootstrapFile.writeAsStringSync(bootstrapContent);
4045
var bootstrapKernelFile =
@@ -95,6 +100,8 @@ void main() async {
95100
'$observableUri;${bootstrapKernelFile.path}',
96101
'--precompiled-macro',
97102
'$autoDisposableUri;${bootstrapKernelFile.path}',
103+
'--precompiled-macro',
104+
'$jsonSerializableUri;${bootstrapKernelFile.path}',
98105
'--macro-serialization-mode=bytedata',
99106
'--input-linked',
100107
bootstrapKernelFile.path,

working/macros/example/bin/user_main.dart

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ import 'dart:math';
77
import 'package:macro_proposal/auto_dispose.dart';
88
import 'package:macro_proposal/data_class.dart';
99
import 'package:macro_proposal/observable.dart';
10+
import 'package:macro_proposal/json_serializable.dart';
1011

1112
void main() {
1213
var rand = Random();
13-
var roger =
14-
User.gen(age: rand.nextInt(100), name: 'Roger', username: 'roger1337');
14+
var rogerJson = {
15+
'age': rand.nextInt(100),
16+
'name': 'Roger',
17+
'username': 'roger1337'
18+
};
19+
var roger = User.fromJson(rogerJson);
1520
print(roger);
1621
var joe = Manager.gen(
1722
age: rand.nextInt(100),
@@ -31,9 +36,18 @@ void main() {
3136

3237
var state = MyState.gen(a: ADisposable(), b: BDisposable(), c: 'hello world');
3338
state.dispose();
39+
40+
var father = Father.fromJson({
41+
'age': roger.age + 25,
42+
'name': 'Rogers Dad',
43+
'username': 'dadJokesAreCool123',
44+
'child': rogerJson,
45+
});
46+
print(father);
3447
}
3548

3649
@DataClass()
50+
@JsonSerializable()
3751
class User {
3852
final int age;
3953
final String name;
@@ -87,3 +101,12 @@ class BDisposable implements Disposable {
87101
print('disposing of BDisposable');
88102
}
89103
}
104+
105+
@DataClass()
106+
@JsonSerializable()
107+
class Father implements User {
108+
final int age;
109+
final String name;
110+
final String username;
111+
final User child;
112+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
//
5+
// ignore_for_file: deprecated_member_use
6+
7+
// There is no public API exposed yet, the in progress api lives here.
8+
import 'package:_fe_analyzer_shared/src/macros/api.dart';
9+
10+
final dartCore = Uri.parse('dart:core');
11+
12+
// TODO: Support `toJson`, collections, and probably some other things :).
13+
macro class JsonSerializable
14+
implements ClassDeclarationsMacro, ClassDefinitionMacro {
15+
const JsonSerializable();
16+
17+
@override
18+
Future<void> buildDeclarationsForClass(
19+
ClassDeclaration clazz, ClassMemberDeclarationBuilder builder) async {
20+
var constructors = await builder.constructorsOf(clazz);
21+
if (constructors.any((c) => c.identifier.name == 'fromJson')) {
22+
throw ArgumentError('There is already a `fromJson` constructor for '
23+
'`${clazz.identifier.name}`, so one could not be added.');
24+
}
25+
26+
var map = await builder.resolveIdentifier(dartCore, 'Map');
27+
var string = NamedTypeAnnotationCode(
28+
name: await builder.resolveIdentifier(dartCore, 'String'));
29+
var dynamic = NamedTypeAnnotationCode(
30+
name: await builder.resolveIdentifier(dartCore, 'dynamic'));
31+
var mapStringDynamic =
32+
NamedTypeAnnotationCode(name: map, typeArguments: [string, dynamic]);
33+
builder.declareInClass(DeclarationCode.fromParts([
34+
'external ',
35+
clazz.identifier.name,
36+
'.fromJson(',
37+
mapStringDynamic,
38+
' json);',
39+
]));
40+
}
41+
42+
@override
43+
Future<void> buildDefinitionForClass(
44+
ClassDeclaration clazz, ClassDefinitionBuilder builder) async {
45+
// TODO: support extending other classes.
46+
if (clazz.superclass != null) {
47+
throw UnsupportedError(
48+
'Serialization of classes that extend other classes is not supported.');
49+
}
50+
51+
var constructors = await builder.constructorsOf(clazz);
52+
var fromJson =
53+
constructors.firstWhere((c) => c.identifier.name == 'fromJson');
54+
var fromJsonBuilder = await builder.buildConstructor(fromJson.identifier);
55+
var fields = await builder.fieldsOf(clazz);
56+
var jsonParam = fromJson.positionalParameters.single.identifier;
57+
fromJsonBuilder.augment(initializers: [
58+
for (var field in fields)
59+
Code.fromParts([
60+
field.identifier,
61+
' = ',
62+
await _convertField(field, jsonParam, builder),
63+
]),
64+
]);
65+
}
66+
67+
// TODO: Support nested List, Map, etc conversion, including deep casting.
68+
Future<Code> _convertField(FieldDeclaration field, Identifier jsonParam,
69+
DefinitionBuilder builder) async {
70+
var fieldType = field.type;
71+
if (fieldType is! NamedTypeAnnotation) {
72+
throw ArgumentError(
73+
'Only fields with named types are allowed on serializable classes, '
74+
'but `${field.identifier.name}` was not a named type.');
75+
}
76+
var fieldTypeDecl = await builder.declarationOf(fieldType.identifier);
77+
while (fieldTypeDecl is TypeAliasDeclaration) {
78+
var aliasedType = fieldTypeDecl.aliasedType;
79+
if (aliasedType is! NamedTypeAnnotation) {
80+
throw ArgumentError(
81+
'Only fields with named types are allowed on serializable classes, '
82+
'but `${field.identifier.name}` did not resolve to a named type.');
83+
}
84+
}
85+
if (fieldTypeDecl is! ClassDeclaration) {
86+
throw ArgumentError(
87+
'Only classes are supported in field types for serializable classes, '
88+
'but the field `${field.identifier.name}` does not have a class '
89+
'type.');
90+
}
91+
92+
var fieldConstructors = await builder.constructorsOf(fieldTypeDecl);
93+
var fieldTypeFromJson = fieldConstructors
94+
.firstWhereOrNull((c) => c.identifier.name == 'fromJson')
95+
?.identifier;
96+
if (fieldTypeFromJson != null) {
97+
return Code.fromParts([
98+
fieldTypeFromJson,
99+
'(',
100+
jsonParam,
101+
'["${field.identifier.name}"])',
102+
]);
103+
} else {
104+
return Code.fromParts([
105+
jsonParam,
106+
// TODO: support nested serializable types.
107+
'["${field.identifier.name}"] as ',
108+
field.type.code,
109+
]);
110+
}
111+
}
112+
}
113+
114+
extension _FirstWhereOrNull<T> on Iterable<T> {
115+
T? firstWhereOrNull(bool Function(T) compare) {
116+
for (var item in this) {
117+
if (compare(item)) return item;
118+
}
119+
return null;
120+
}
121+
}

0 commit comments

Comments
 (0)