mirror of
https://github.com/dart-lang/sdk
synced 2024-10-14 16:59:47 +00:00
Initial extraction of the json macro into a package, without configuration.
Change-Id: I94067a57151066736e3c703ef85bfa90a4af1c70 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/365081 Reviewed-by: Bob Nystrom <rnystrom@google.com> Reviewed-by: Marya Belanger <mbelanger@google.com> Reviewed-by: Morgan :) <davidmorgan@google.com> Commit-Queue: Jake Macdonald <jakemac@google.com>
This commit is contained in:
parent
f5fe3dddcc
commit
2f40aab365
3
pkg/json/CHANGELOG.md
Normal file
3
pkg/json/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# 0.1.0-wip
|
||||
|
||||
- Initial release, adds the JsonCodable macro.
|
27
pkg/json/LICENSE
Normal file
27
pkg/json/LICENSE
Normal file
|
@ -0,0 +1,27 @@
|
|||
Copyright 2024, the Dart project authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
93
pkg/json/README.md
Normal file
93
pkg/json/README.md
Normal file
|
@ -0,0 +1,93 @@
|
|||
This package provides an experimental macro that encodes and decodes
|
||||
user-defined Dart classes to JSON maps (maps of type `Map<String, Object?>`).
|
||||
|
||||
This relies on the experimental macros language feature, and as such may be
|
||||
unstable or have breaking changes if the feature changes.
|
||||
|
||||
## Applying the JsonCodable macro
|
||||
|
||||
To apply the JsonCodable macro to a class, add it as an annotation:
|
||||
|
||||
```dart
|
||||
import 'package:json/json.dart';
|
||||
|
||||
@JsonCodable()
|
||||
class User {
|
||||
final String name;
|
||||
|
||||
final int? age;
|
||||
}
|
||||
```
|
||||
|
||||
The macro generates two members for the `User` class: a `fromJson` constructor
|
||||
and a `toJson` method, with APIs like the following:
|
||||
|
||||
```dart
|
||||
class User {
|
||||
User.fromJson(Map<String, Object?> json);
|
||||
|
||||
Map<String, Object?> toJson();
|
||||
}
|
||||
```
|
||||
|
||||
Each non-nullable field in the annotated class must have an entry with the same name in the `json` map, but
|
||||
nullable fields are allowed to have no key at all. The `toJson` will omit null
|
||||
fields entirely from the map, and will not contain explicit null entries.
|
||||
|
||||
### Extending other classes
|
||||
|
||||
You are allowed to extend classes other than `Object`, but they must have a
|
||||
valid `toJson` method and `fromJson` constructor, so the following is allowed:
|
||||
|
||||
```dart
|
||||
@JsonCodable()
|
||||
class Manager extends User {
|
||||
final List<User> reports;
|
||||
}
|
||||
```
|
||||
|
||||
## Supported field types
|
||||
|
||||
All native JSON types are supported (`int`, `double`, `String`, `bool`, `Null`).
|
||||
|
||||
The core collection types `List`, `Set`, and `Map` are also supported, if their
|
||||
elements are supported types. For elements which require more than just a cast,
|
||||
the type must be statically provided through a generic type argument on the
|
||||
collection in order for the specialized code to be generated.
|
||||
|
||||
Additionally, custom types with a `toJson` method and `fromJson` constructor are
|
||||
allowed, including as generic type arguments to the collection types.
|
||||
|
||||
## Generics
|
||||
|
||||
Classes with generic type parameters are not supported, but may be in the
|
||||
future.
|
||||
|
||||
## Configuration
|
||||
|
||||
Macro configuration is a feature we intend to add, but it is not available at this time.
|
||||
|
||||
Because of this, field names must exactly match the keys in the maps, and
|
||||
default values are not supported.
|
||||
|
||||
## Enabling the macros experiment
|
||||
|
||||
Most tools accept the `--enable-experiment=macros` option, and appending that
|
||||
to your typical command line invocations should be all that is needed. For
|
||||
example, you can launch your flutter project like:
|
||||
`flutter run --enable-experiment=macros`.
|
||||
|
||||
For the analyzer, you will want to add some configuration to an
|
||||
`analysis_options.yaml` file at the root of your project:
|
||||
|
||||
```yaml
|
||||
analyzer:
|
||||
enable-experiment:
|
||||
- macros
|
||||
```
|
||||
|
||||
Note that `dart run` is a little bit special, in that the option must come
|
||||
_immediately_ following `dart` and before `run` - this is because it is an
|
||||
option to the Dart VM, and not the Dart script itself. For example,
|
||||
`dart --enable-experiment=macros run bin/my_script.dart`. This is also how the
|
||||
`test` package expects to be invoked, so `dart --enable-experiment=macros test`.
|
5
pkg/json/analysis_options.yaml
Normal file
5
pkg/json/analysis_options.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
include: package:lints/recommended.yaml
|
||||
|
||||
analyzer:
|
||||
enable-experiment:
|
||||
- macros
|
639
pkg/json/lib/json.dart
Normal file
639
pkg/json/lib/json.dart
Normal file
|
@ -0,0 +1,639 @@
|
|||
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:macros/macros.dart';
|
||||
|
||||
/// A macro which adds a `fromJson(Map<String, Object?> json)` constructor and
|
||||
/// a `Map<String, Object?> toJson()` method to a class.
|
||||
///
|
||||
/// To use this macro, annotate your class with `@JsonCodable()` and enable the
|
||||
/// macros experiment (see README.md for full instructions).
|
||||
///
|
||||
/// The implementations are derived from the fields defined directly on the
|
||||
/// annotated class, and field names are expected to exactly match the keys of
|
||||
/// the maps that they are being decoded from.
|
||||
///
|
||||
/// If extending any class other than [Object], then the super class is expected
|
||||
/// to also have a corresponding `toJson` method and `fromJson` constructor.
|
||||
///
|
||||
/// Annotated classes are not allowed to have a manually defined `toJson` method
|
||||
/// or `fromJson` constructor.
|
||||
macro class JsonCodable
|
||||
implements ClassDeclarationsMacro, ClassDefinitionMacro {
|
||||
const JsonCodable();
|
||||
|
||||
/// Declares the `fromJson` constructor and `toJson` method, but does not
|
||||
/// implement them.
|
||||
@override
|
||||
Future<void> buildDeclarationsForClass(
|
||||
ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
|
||||
if (clazz.typeParameters.isNotEmpty) {
|
||||
throw DiagnosticException(Diagnostic(DiagnosticMessage(
|
||||
// TODO: Target the actual type parameter, issue #55611
|
||||
'Cannot be applied to classes with generic type parameters'),
|
||||
Severity.error));
|
||||
}
|
||||
|
||||
final (map, string, object, _, _) = await (
|
||||
builder.resolveIdentifier(_dartCore, 'Map'),
|
||||
builder.resolveIdentifier(_dartCore, 'String'),
|
||||
builder.resolveIdentifier(_dartCore, 'Object'),
|
||||
// These are just for validation, and will throw if the check fails.
|
||||
_checkNoFromJson(builder, clazz),
|
||||
_checkNoToJson(builder, clazz),
|
||||
).wait;
|
||||
final mapStringObject = NamedTypeAnnotationCode(name: map, typeArguments: [
|
||||
NamedTypeAnnotationCode(name: string),
|
||||
NamedTypeAnnotationCode(name: object).asNullable
|
||||
]);
|
||||
|
||||
builder.declareInType(DeclarationCode.fromParts([
|
||||
// TODO(language#3580): Remove/replace 'external'?
|
||||
' external ',
|
||||
clazz.identifier.name,
|
||||
'.fromJson(',
|
||||
mapStringObject,
|
||||
' json);',
|
||||
]));
|
||||
|
||||
builder.declareInType(DeclarationCode.fromParts([
|
||||
// TODO(language#3580): Remove/replace 'external'?
|
||||
' external ',
|
||||
mapStringObject,
|
||||
' toJson();',
|
||||
]));
|
||||
}
|
||||
|
||||
/// Provides the actual definitions of the `fromJson` constructor and `toJson`
|
||||
/// method, which were declared in the previous phase.
|
||||
@override
|
||||
Future<void> buildDefinitionForClass(
|
||||
ClassDeclaration clazz, TypeDefinitionBuilder builder) async {
|
||||
final (introspectionData, constructors, methods) = await (
|
||||
_SharedIntrospectionData.build(builder, clazz),
|
||||
builder.constructorsOf(clazz),
|
||||
builder.methodsOf(clazz)
|
||||
).wait;
|
||||
final fromJson =
|
||||
constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson');
|
||||
final toJson =
|
||||
methods.firstWhereOrNull((c) => c.identifier.name == 'toJson');
|
||||
|
||||
// An earlier step failed, just bail out without emitting additional
|
||||
// diagnostics.
|
||||
if (fromJson == null || toJson == null) return;
|
||||
|
||||
final (fromJsonBuilder, toJsonBuilder) = await (
|
||||
builder.buildConstructor(fromJson.identifier),
|
||||
builder.buildMethod(toJson.identifier),
|
||||
).wait;
|
||||
await (
|
||||
_buildFromJson(fromJson, fromJsonBuilder, introspectionData),
|
||||
_buildToJson(toJson, toJsonBuilder, introspectionData),
|
||||
).wait;
|
||||
}
|
||||
|
||||
/// Builds the actual `fromJson` constructor.
|
||||
Future<void> _buildFromJson(
|
||||
ConstructorDeclaration constructor,
|
||||
ConstructorDefinitionBuilder builder,
|
||||
_SharedIntrospectionData introspectionData) async {
|
||||
await _checkValidFromJson(constructor, introspectionData, builder);
|
||||
|
||||
// If extending something other than `Object`, it must have a `fromJson`
|
||||
// constructor.
|
||||
var superclassHasFromJson = false;
|
||||
final superclassDeclaration = introspectionData.superclass;
|
||||
if (superclassDeclaration != null &&
|
||||
!superclassDeclaration.isExactly('Object', _dartCore)) {
|
||||
final superclassConstructors =
|
||||
await builder.constructorsOf(superclassDeclaration);
|
||||
for (final superConstructor in superclassConstructors) {
|
||||
if (superConstructor.identifier.name == 'fromJson') {
|
||||
await _checkValidFromJson(
|
||||
superConstructor, introspectionData, builder);
|
||||
superclassHasFromJson = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!superclassHasFromJson) {
|
||||
throw DiagnosticException(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Serialization of classes that extend other classes is only '
|
||||
'supported if those classes have a valid '
|
||||
'`fromJson(Map<String, Object?> json)` constructor.',
|
||||
target: introspectionData.clazz.superclass?.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
}
|
||||
}
|
||||
|
||||
final fields = introspectionData.fields;
|
||||
final jsonParam = constructor.positionalParameters.single.identifier;
|
||||
|
||||
Future<Code> initializerForField(FieldDeclaration field) async {
|
||||
return RawCode.fromParts([
|
||||
field.identifier,
|
||||
' = ',
|
||||
await _convertTypeFromJson(
|
||||
field.type,
|
||||
RawCode.fromParts([
|
||||
jsonParam,
|
||||
"['",
|
||||
field.identifier.name,
|
||||
"']",
|
||||
]),
|
||||
builder,
|
||||
introspectionData),
|
||||
]);
|
||||
}
|
||||
|
||||
final initializers = await Future.wait(fields.map(initializerForField));
|
||||
|
||||
if (superclassHasFromJson) {
|
||||
initializers.add(RawCode.fromParts([
|
||||
'super.fromJson(',
|
||||
jsonParam,
|
||||
')',
|
||||
]));
|
||||
}
|
||||
|
||||
builder.augment(initializers: initializers);
|
||||
}
|
||||
|
||||
/// Builds the actual `toJson` method.
|
||||
Future<void> _buildToJson(
|
||||
MethodDeclaration method,
|
||||
FunctionDefinitionBuilder builder,
|
||||
_SharedIntrospectionData introspectionData) async {
|
||||
if (!(await _checkValidToJson(method, introspectionData, builder))) return;
|
||||
|
||||
// If extending something other than `Object`, it must have a `toJson`
|
||||
// method.
|
||||
var superclassHasToJson = false;
|
||||
final superclassDeclaration = introspectionData.superclass;
|
||||
if (superclassDeclaration != null &&
|
||||
!superclassDeclaration.isExactly('Object', _dartCore)) {
|
||||
final superclassMethods = await builder.methodsOf(superclassDeclaration);
|
||||
for (final superMethod in superclassMethods) {
|
||||
if (superMethod.identifier.name == 'toJson') {
|
||||
if (!(await _checkValidToJson(
|
||||
superMethod, introspectionData, builder))) {
|
||||
return;
|
||||
}
|
||||
superclassHasToJson = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!superclassHasToJson) {
|
||||
builder.report(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Serialization of classes that extend other classes is only '
|
||||
'supported if those classes have a valid '
|
||||
'`Map<String, Object?> toJson()` method.',
|
||||
target: introspectionData.clazz.superclass?.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final fields = introspectionData.fields;
|
||||
final parts = <Object>[
|
||||
'{\n final json = ',
|
||||
if (superclassHasToJson)
|
||||
'super.toJson()'
|
||||
else ...[
|
||||
'<',
|
||||
introspectionData.stringCode,
|
||||
', ',
|
||||
introspectionData.objectCode.asNullable,
|
||||
'>{}',
|
||||
],
|
||||
';\n ',
|
||||
];
|
||||
|
||||
Future<Code> addEntryForField(FieldDeclaration field) async {
|
||||
final parts = <Object>[];
|
||||
final doNullCheck = field.type.isNullable;
|
||||
if (doNullCheck) {
|
||||
parts.addAll([
|
||||
'if (',
|
||||
field.identifier,
|
||||
// `null` is a reserved word, we can just use it.
|
||||
' != null) {\n ',
|
||||
]);
|
||||
}
|
||||
parts.addAll([
|
||||
"json['",
|
||||
field.identifier.name,
|
||||
"'] = ",
|
||||
await _convertTypeToJson(
|
||||
field.type,
|
||||
RawCode.fromParts([
|
||||
field.identifier,
|
||||
if (doNullCheck) '!',
|
||||
]),
|
||||
builder,
|
||||
introspectionData),
|
||||
';\n',
|
||||
]);
|
||||
if (doNullCheck) {
|
||||
parts.add(' }\n');
|
||||
}
|
||||
return RawCode.fromParts(parts);
|
||||
}
|
||||
|
||||
parts.addAll(await Future.wait(fields.map(addEntryForField)));
|
||||
|
||||
parts.add(' return json;\n }');
|
||||
|
||||
builder.augment(FunctionBodyCode.fromParts(parts));
|
||||
}
|
||||
|
||||
/// Throws a [DiagnosticException] if there is an existing `fromJson`
|
||||
/// constructor on [clazz].
|
||||
Future<void> _checkNoFromJson(
|
||||
DeclarationBuilder builder, ClassDeclaration clazz) async {
|
||||
final constructors = await builder.constructorsOf(clazz);
|
||||
final fromJson =
|
||||
constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson');
|
||||
if (fromJson != null) {
|
||||
throw DiagnosticException(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Cannot generate a fromJson constructor due to this existing '
|
||||
'one.',
|
||||
target: fromJson.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
}
|
||||
}
|
||||
|
||||
/// Throws a [DiagnosticException] if there is an existing `toJson` method on
|
||||
/// [clazz].
|
||||
Future<void> _checkNoToJson(
|
||||
DeclarationBuilder builder, ClassDeclaration clazz) async {
|
||||
final methods = await builder.methodsOf(clazz);
|
||||
final toJson =
|
||||
methods.firstWhereOrNull((m) => m.identifier.name == 'toJson');
|
||||
if (toJson != null) {
|
||||
throw DiagnosticException(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Cannot generate a toJson method due to this existing one.',
|
||||
target: toJson.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that [constructor] is a valid `fromJson` constructor, and throws a
|
||||
/// [DiagnosticException] if not.
|
||||
Future<void> _checkValidFromJson(
|
||||
ConstructorDeclaration constructor,
|
||||
_SharedIntrospectionData introspectionData,
|
||||
DefinitionBuilder builder) async {
|
||||
if (constructor.namedParameters.isNotEmpty ||
|
||||
constructor.positionalParameters.length != 1 ||
|
||||
!(await (await builder
|
||||
.resolve(constructor.positionalParameters.single.type.code))
|
||||
.isExactly(introspectionData.jsonMapType))) {
|
||||
throw DiagnosticException(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Expected exactly one parameter, with the type '
|
||||
'Map<String, Object?>',
|
||||
target: constructor.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [Code] object which is an expression that converts a JSON map
|
||||
/// (referenced by [jsonReference]) into an instance of type [type].
|
||||
Future<Code> _convertTypeFromJson(
|
||||
TypeAnnotation type,
|
||||
Code jsonReference,
|
||||
DefinitionBuilder builder,
|
||||
_SharedIntrospectionData introspectionData) async {
|
||||
if (type is! NamedTypeAnnotation) {
|
||||
builder.report(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Only fields with named types are allowed on serializable '
|
||||
'classes',
|
||||
target: type.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
return RawCode.fromString(
|
||||
"throw 'Unable to deserialize type ${type.code.debugString}'");
|
||||
}
|
||||
|
||||
// Follow type aliases until we reach an actual named type.
|
||||
var classDecl = await _classDeclarationOf(builder, type);
|
||||
if (classDecl == null) {
|
||||
return RawCode.fromString(
|
||||
"throw 'Unable to deserialize type ${type.code.debugString}'");
|
||||
}
|
||||
|
||||
var nullCheck = type.isNullable
|
||||
? RawCode.fromParts([
|
||||
jsonReference,
|
||||
// `null` is a reserved word, we can just use it.
|
||||
' == null ? null : ',
|
||||
])
|
||||
: null;
|
||||
|
||||
// Check if `typeDecl` is one of the supported collection types.
|
||||
if (classDecl.isExactly('List', _dartCore)) {
|
||||
return RawCode.fromParts([
|
||||
if (nullCheck != null) nullCheck,
|
||||
'[ for (final item in ',
|
||||
jsonReference,
|
||||
' as ',
|
||||
introspectionData.jsonListCode,
|
||||
') ',
|
||||
await _convertTypeFromJson(type.typeArguments.single,
|
||||
RawCode.fromString('item'), builder, introspectionData),
|
||||
']',
|
||||
]);
|
||||
} else if (classDecl.isExactly('Set', _dartCore)) {
|
||||
return RawCode.fromParts([
|
||||
if (nullCheck != null) nullCheck,
|
||||
'{ for (final item in ',
|
||||
jsonReference,
|
||||
' as ',
|
||||
introspectionData.jsonListCode,
|
||||
')',
|
||||
await _convertTypeFromJson(type.typeArguments.single,
|
||||
RawCode.fromString('item'), builder, introspectionData),
|
||||
'}',
|
||||
]);
|
||||
} else if (classDecl.isExactly('Map', _dartCore)) {
|
||||
return RawCode.fromParts([
|
||||
if (nullCheck != null) nullCheck,
|
||||
'{ for (final entry in ',
|
||||
jsonReference,
|
||||
' as ',
|
||||
introspectionData.jsonMapCode,
|
||||
'.entries) entry.key: ',
|
||||
await _convertTypeFromJson(type.typeArguments.single,
|
||||
RawCode.fromString('entry.value'), builder, introspectionData),
|
||||
'}',
|
||||
]);
|
||||
}
|
||||
|
||||
// Otherwise, check if `classDecl` has a `fromJson` constructor.
|
||||
final constructors = await builder.constructorsOf(classDecl);
|
||||
final fromJson = constructors
|
||||
.firstWhereOrNull((c) => c.identifier.name == 'fromJson')
|
||||
?.identifier;
|
||||
if (fromJson != null) {
|
||||
return RawCode.fromParts([
|
||||
if (nullCheck != null) nullCheck,
|
||||
fromJson,
|
||||
'(',
|
||||
jsonReference,
|
||||
' as ',
|
||||
introspectionData.jsonMapCode,
|
||||
')',
|
||||
]);
|
||||
}
|
||||
|
||||
// Finally, we just cast directly to the field type.
|
||||
// TODO: Check that it is a valid type we can cast from JSON.
|
||||
return RawCode.fromParts([
|
||||
jsonReference,
|
||||
' as ',
|
||||
type.code,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Checks that [method] is a valid `toJson` method, and throws a
|
||||
/// [DiagnosticException] if not.
|
||||
Future<bool> _checkValidToJson(
|
||||
MethodDeclaration method,
|
||||
_SharedIntrospectionData introspectionData,
|
||||
DefinitionBuilder builder) async {
|
||||
if (method.namedParameters.isNotEmpty ||
|
||||
method.positionalParameters.isNotEmpty ||
|
||||
!(await (await builder.resolve(method.returnType.code))
|
||||
.isExactly(introspectionData.jsonMapType))) {
|
||||
builder.report(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Expected no parameters, and a return type of '
|
||||
'Map<String, Object?>',
|
||||
target: method.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns a [Code] object which is an expression that converts an instance
|
||||
/// of type [type] (referenced by [valueReference]) into a JSON map.
|
||||
Future<Code> _convertTypeToJson(
|
||||
TypeAnnotation type,
|
||||
Code valueReference,
|
||||
DefinitionBuilder builder,
|
||||
_SharedIntrospectionData introspectionData) async {
|
||||
if (type is! NamedTypeAnnotation) {
|
||||
builder.report(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Only fields with named types are allowed on serializable '
|
||||
'classes',
|
||||
target: type.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
return RawCode.fromString(
|
||||
"throw 'Unable to serialize type ${type.code.debugString}'");
|
||||
}
|
||||
|
||||
// Follow type aliases until we reach an actual named type.
|
||||
var classDecl = await _classDeclarationOf(builder, type);
|
||||
if (classDecl == null) {
|
||||
return RawCode.fromString(
|
||||
"throw 'Unable to serialize type ${type.code.debugString}'");
|
||||
}
|
||||
|
||||
// Check for the supported collection types, and serialize them accordingly.
|
||||
if (classDecl.isExactly('List', _dartCore) ||
|
||||
classDecl.isExactly('Set', _dartCore)) {
|
||||
return RawCode.fromParts([
|
||||
'[ for (final item in ',
|
||||
valueReference,
|
||||
') ',
|
||||
await _convertTypeToJson(type.typeArguments.single,
|
||||
RawCode.fromString('item'), builder, introspectionData),
|
||||
']',
|
||||
]);
|
||||
} else if (classDecl.isExactly('Map', _dartCore)) {
|
||||
return RawCode.fromParts([
|
||||
'{ for (final entry in ',
|
||||
valueReference,
|
||||
'.entries) entry.key: ',
|
||||
await _convertTypeToJson(type.typeArguments.single,
|
||||
RawCode.fromString('entry.value'), builder, introspectionData),
|
||||
'}',
|
||||
]);
|
||||
}
|
||||
|
||||
// Next, check if it has a `toJson()` method and call that.
|
||||
final methods = await builder.methodsOf(classDecl);
|
||||
final toJson = methods
|
||||
.firstWhereOrNull((c) => c.identifier.name == 'toJson')
|
||||
?.identifier;
|
||||
if (toJson != null) {
|
||||
return RawCode.fromParts([
|
||||
valueReference,
|
||||
'.toJson()',
|
||||
]);
|
||||
}
|
||||
|
||||
// Finally, we just return the value as is if we can't otherwise handle it.
|
||||
// TODO: Check that it is a valid type we can serialize.
|
||||
return valueReference;
|
||||
}
|
||||
|
||||
/// Follows [type] through any type aliases, until it reaches a
|
||||
/// [ClassDeclaration], or returns null if it does not bottom out on a class.
|
||||
Future<ClassDeclaration?> _classDeclarationOf(
|
||||
DefinitionBuilder builder, NamedTypeAnnotation type) async {
|
||||
var typeDecl = await builder.typeDeclarationOf(type.identifier);
|
||||
while (typeDecl is TypeAliasDeclaration) {
|
||||
final aliasedType = typeDecl.aliasedType;
|
||||
if (aliasedType is! NamedTypeAnnotation) {
|
||||
builder.report(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Only fields with named types are allowed on serializable '
|
||||
'classes',
|
||||
target: type.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
return null;
|
||||
}
|
||||
typeDecl = await builder.typeDeclarationOf(aliasedType.identifier);
|
||||
}
|
||||
if (typeDecl is! ClassDeclaration) {
|
||||
builder.report(Diagnostic(
|
||||
DiagnosticMessage(
|
||||
'Only classes are supported as field types for serializable '
|
||||
'classes',
|
||||
target: type.asDiagnosticTarget),
|
||||
Severity.error));
|
||||
return null;
|
||||
}
|
||||
return typeDecl;
|
||||
}
|
||||
}
|
||||
|
||||
/// This data is collected asynchronously, so we only want to do it once and
|
||||
/// share that work across multiple locations.
|
||||
final class _SharedIntrospectionData {
|
||||
/// The declaration of the class we are generating for.
|
||||
final ClassDeclaration clazz;
|
||||
|
||||
/// All the fields on the [clazz].
|
||||
final List<FieldDeclaration> fields;
|
||||
|
||||
/// A [Code] representation of the type [List<Object?>].
|
||||
final NamedTypeAnnotationCode jsonListCode;
|
||||
|
||||
/// A [Code] representation of the type [Map<String, Object?>].
|
||||
final NamedTypeAnnotationCode jsonMapCode;
|
||||
|
||||
/// The resolved [StaticType] representing the [Map<String, Object?>] type.
|
||||
final StaticType jsonMapType;
|
||||
|
||||
/// A [Code] representation of the type [Object].
|
||||
final NamedTypeAnnotationCode objectCode;
|
||||
|
||||
/// A [Code] representation of the type [String].
|
||||
final NamedTypeAnnotationCode stringCode;
|
||||
|
||||
/// The declaration of the superclass of [clazz], if it is not [Object].
|
||||
final ClassDeclaration? superclass;
|
||||
|
||||
_SharedIntrospectionData({
|
||||
required this.clazz,
|
||||
required this.fields,
|
||||
required this.jsonListCode,
|
||||
required this.jsonMapCode,
|
||||
required this.jsonMapType,
|
||||
required this.objectCode,
|
||||
required this.stringCode,
|
||||
required this.superclass,
|
||||
});
|
||||
|
||||
static Future<_SharedIntrospectionData> build(
|
||||
DeclarationPhaseIntrospector builder, ClassDeclaration clazz) async {
|
||||
final (list, map, object, string) = await (
|
||||
builder.resolveIdentifier(_dartCore, 'List'),
|
||||
builder.resolveIdentifier(_dartCore, 'Map'),
|
||||
builder.resolveIdentifier(_dartCore, 'Object'),
|
||||
builder.resolveIdentifier(_dartCore, 'String'),
|
||||
).wait;
|
||||
final objectCode = NamedTypeAnnotationCode(name: object);
|
||||
final nullableObjectCode = objectCode.asNullable;
|
||||
final jsonListCode = NamedTypeAnnotationCode(name: list, typeArguments: [
|
||||
nullableObjectCode,
|
||||
]);
|
||||
final jsonMapCode = NamedTypeAnnotationCode(name: map, typeArguments: [
|
||||
NamedTypeAnnotationCode(name: string),
|
||||
nullableObjectCode,
|
||||
]);
|
||||
final stringCode = NamedTypeAnnotationCode(name: string);
|
||||
final superclass = clazz.superclass;
|
||||
final (fields, jsonMapType, superclassDecl) = await (
|
||||
builder.fieldsOf(clazz),
|
||||
builder.resolve(jsonMapCode),
|
||||
superclass == null
|
||||
? Future.value(null)
|
||||
: builder.typeDeclarationOf(superclass.identifier),
|
||||
).wait;
|
||||
|
||||
return _SharedIntrospectionData(
|
||||
clazz: clazz,
|
||||
fields: fields,
|
||||
jsonListCode: jsonListCode,
|
||||
jsonMapCode: jsonMapCode,
|
||||
jsonMapType: jsonMapType,
|
||||
objectCode: objectCode,
|
||||
stringCode: stringCode,
|
||||
superclass: superclassDecl as ClassDeclaration?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _dartCore = Uri.parse('dart:core');
|
||||
|
||||
extension _FirstWhereOrNull<T> on Iterable<T> {
|
||||
T? firstWhereOrNull(bool Function(T) compare) {
|
||||
for (final item in this) {
|
||||
if (compare(item)) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension _IsExactly on TypeDeclaration {
|
||||
/// Cheaper than checking types using a [StaticType].
|
||||
bool isExactly(String name, Uri library) =>
|
||||
identifier.name == name && this.library.uri == library;
|
||||
}
|
||||
|
||||
extension on Code {
|
||||
/// Used for error messages.
|
||||
String get debugString {
|
||||
final buffer = StringBuffer();
|
||||
_writeDebugString(buffer);
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
void _writeDebugString(StringBuffer buffer) {
|
||||
for (final part in parts) {
|
||||
switch (part) {
|
||||
case Code():
|
||||
part._writeDebugString(buffer);
|
||||
case Identifier():
|
||||
buffer.write(part.name);
|
||||
default:
|
||||
buffer.write(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
pkg/json/pubspec.yaml
Normal file
13
pkg/json/pubspec.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
name: json
|
||||
description: >
|
||||
A package which provides an experimental macro that encodes and decodes
|
||||
user-defined Dart classes to JSON maps (maps of type `Map<String, Object?>`).
|
||||
repository: https://github.com/dart-lang/sdk/tree/main/pkg/json
|
||||
version: 0.1.0-wip
|
||||
environment:
|
||||
sdk: ^3.4.0-0
|
||||
dependencies:
|
||||
macros: ^0.1.0-0
|
||||
dev_dependencies:
|
||||
lints: any
|
||||
test: any
|
57
pkg/json/test/json_codable_test.dart
Normal file
57
pkg/json/test/json_codable_test.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
//
|
||||
// SharedOptions=--enable-experiment=macros
|
||||
|
||||
import 'package:json/json.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('Can be encoded and decoded', () {
|
||||
test('without nullable fields', () {
|
||||
var userJson = {
|
||||
'name': 'John',
|
||||
'age': 35,
|
||||
};
|
||||
|
||||
var user = User.fromJson(userJson);
|
||||
expect(user.name, 'John');
|
||||
expect(user.age, 35);
|
||||
expect(user.friends, null);
|
||||
expect(userJson, equals(user.toJson()));
|
||||
});
|
||||
|
||||
test('with nullable fields', () {
|
||||
var userJson = {
|
||||
'name': 'John',
|
||||
'age': 35,
|
||||
'friends': [
|
||||
{
|
||||
'name': 'Jill',
|
||||
'age': 28,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var user = User.fromJson(userJson);
|
||||
expect(user.name, 'John');
|
||||
expect(user.age, 35);
|
||||
expect(user.friends?.length, 1);
|
||||
|
||||
var friend = user.friends!.single;
|
||||
expect(friend.name, 'Jill');
|
||||
expect(friend.age, 28);
|
||||
expect(userJson, equals(user.toJson()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@JsonCodable()
|
||||
class User {
|
||||
String name;
|
||||
|
||||
int age;
|
||||
|
||||
List<User>? friends;
|
||||
}
|
Loading…
Reference in a new issue