Skip to content

Add @JsonEnum annotation #989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions json_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- Added `JsonSerializabel.constructor` field to allow specifying an alternative
constructor to invoke when creating a `fromJson` helper.
- Added `JsonEnum` for annotating `enum` types.

## 4.1.0

Expand Down
1 change: 1 addition & 0 deletions json_annotation/lib/json_annotation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ library json_annotation;
export 'src/allowed_keys_helpers.dart';
export 'src/checked_helpers.dart';
export 'src/json_converter.dart';
export 'src/json_enum.dart';
export 'src/json_key.dart';
export 'src/json_literal.dart';
export 'src/json_serializable.dart';
Expand Down
13 changes: 13 additions & 0 deletions json_annotation/lib/src/json_enum.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2021, 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.

import 'package:meta/meta_meta.dart';

/// When applied to `enum` definitions, causes the corresponding private
// `_$EnumNameEnumMap` and `_$enumDecode` helpers to be generated, even if the
// `enum` is not referenced elsewhere in generated code.
@Target({TargetKind.enumType})
class JsonEnum {
const JsonEnum();
}
3 changes: 3 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

- Added support for `JsonSerializabel.constructor` to allow specifying an
alternative constructor to invoke when creating a `fromJson` helper.
- Support the new `@JsonEnum` annotation by generating the corresponding private
`_$EnumNameEnumMap` and `_$enumDecode` helpers, even if the `enum` is not
referenced elsewhere in generated code.
- Require `json_annotation` `'>=4.2.0 <4.3.0'`.

## 5.0.2
Expand Down
1 change: 1 addition & 0 deletions json_serializable/lib/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
// 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.

export 'src/json_enum_generator.dart' show JsonEnumGenerator;
export 'src/json_literal_generator.dart' show JsonLiteralGenerator;
export 'src/json_serializable_generator.dart' show JsonSerializableGenerator;
33 changes: 33 additions & 0 deletions json_serializable/lib/src/json_enum_generator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2017, 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.

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:source_gen/source_gen.dart';

import 'type_helpers/enum_helper.dart';

class JsonEnumGenerator extends GeneratorForAnnotation<JsonEnum> {
const JsonEnumGenerator();

@override
List<String> generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
if (element is! ClassElement || !element.isEnum) {
throw InvalidGenerationSourceError(
'`@JsonEnum` can only be used on enum elements.',
element: element,
);
}

return [
enumDecodeHelper,
enumValueMapFromType(element.thisType)!,
];
}
}
63 changes: 62 additions & 1 deletion json_serializable/lib/src/json_part_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:build/build.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:source_gen/source_gen.dart';

import 'json_enum_generator.dart';
import 'json_literal_generator.dart';
import 'json_serializable_generator.dart';
import 'settings.dart';
Expand All @@ -23,10 +24,70 @@ Builder jsonPartBuilder({

return SharedPartBuilder(
[
JsonSerializableGenerator.fromSettings(settings),
_UnifiedGenerator([
JsonSerializableGenerator.fromSettings(settings),
const JsonEnumGenerator(),
]),
const JsonLiteralGenerator(),
],
'json_serializable',
formatOutput: formatOutput,
);
}

/// Allows exposing separate [GeneratorForAnnotation] instances as one
/// generator.
///
/// Output can be merged while keeping implementations separate.
class _UnifiedGenerator extends Generator {
final List<GeneratorForAnnotation> _generators;

_UnifiedGenerator(this._generators);

@override
Future<String?> generate(LibraryReader library, BuildStep buildStep) async {
final values = <String>{};

for (var generator in _generators) {
for (var annotatedElement
in library.annotatedWith(generator.typeChecker)) {
final generatedValue = generator.generateForAnnotatedElement(
annotatedElement.element, annotatedElement.annotation, buildStep);
for (var value in _normalizeGeneratorOutput(generatedValue)) {
assert(value.length == value.trim().length);
values.add(value);
}
}
}

return values.join('\n\n');
}

@override
String toString() => 'JsonSerializableGenerator';
}

// Borrowed from `package:source_gen`
Iterable<String> _normalizeGeneratorOutput(Object? value) {
if (value == null) {
return const [];
} else if (value is String) {
value = [value];
}

if (value is Iterable) {
return value.where((e) => e != null).map((e) {
if (e is String) {
return e.trim();
}

throw _argError(e as Object);
}).where((e) => e.isNotEmpty);
}
throw _argError(value);
}

// Borrowed from `package:source_gen`
ArgumentError _argError(Object value) => ArgumentError(
'Must be a String or be an Iterable containing String values. '
'Found `${Error.safeToString(value)}` (${value.runtimeType}).');
2 changes: 1 addition & 1 deletion json_serializable/lib/src/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class JsonSerializableGenerator
);
}

if (element is! ClassElement) {
if (element is! ClassElement || element.isEnum) {
throw InvalidGenerationSourceError(
'`@JsonSerializable` can only be used on classes.',
element: element,
Expand Down
10 changes: 5 additions & 5 deletions json_serializable/lib/src/type_helpers/enum_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
String expression,
TypeHelperContextWithConfig context,
) {
final memberContent = _enumValueMapFromType(targetType);
final memberContent = enumValueMapFromType(targetType);

if (memberContent == null) {
return null;
Expand All @@ -39,13 +39,13 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
TypeHelperContextWithConfig context,
bool defaultProvided,
) {
final memberContent = _enumValueMapFromType(targetType);
final memberContent = enumValueMapFromType(targetType);

if (memberContent == null) {
return null;
}

context.addMember(_enumDecodeHelper);
context.addMember(enumDecodeHelper);

String functionName;
if (targetType.isNullableType || defaultProvided) {
Expand All @@ -72,7 +72,7 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
String _constMapName(DartType targetType) =>
'_\$${targetType.element!.name}EnumMap';

String? _enumValueMapFromType(DartType targetType) {
String? enumValueMapFromType(DartType targetType) {
final enumMap = enumFieldsMap(targetType);

if (enumMap == null) {
Expand All @@ -87,7 +87,7 @@ String? _enumValueMapFromType(DartType targetType) {
return 'const ${_constMapName(targetType)} = {\n$items\n};';
}

const _enumDecodeHelper = r'''
const enumDecodeHelper = r'''
K _$enumDecode<K, V>(
Map<K, V> enumValues,
Object? source, {
Expand Down
3 changes: 3 additions & 0 deletions json_serializable/lib/src/type_helpers/json_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ InterfaceType? _instantiate(
}

ClassConfig? _annotation(ClassConfig config, InterfaceType source) {
if (source.isEnum) {
return null;
}
final annotations = const TypeChecker.fromRuntime(JsonSerializable)
.annotationsOfExact(source.element, throwOnUnresolved: false)
.toList();
Expand Down
4 changes: 4 additions & 0 deletions json_serializable/test/integration/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,8 @@ void main() {

validateRoundTrip(value, (json) => PrivateConstructor.fromJson(json));
});

test('enum helpers', () {
expect(standAloneEnumKeys, ['a', 'b', 'g', 'd']);
});
}
14 changes: 14 additions & 0 deletions json_serializable/test/integration/json_test_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,17 @@ class PrivateConstructor {
bool operator ==(Object other) =>
other is PrivateConstructor && id == other.id && value == other.value;
}

@JsonEnum()
enum StandAloneEnum {
@JsonValue('a')
alpha,
@JsonValue('b')
beta,
@JsonValue('g')
gamma,
@JsonValue('d')
delta,
}

Iterable<String> get standAloneEnumKeys => _$StandAloneEnumEnumMap.values;
7 changes: 7 additions & 0 deletions json_serializable/test/integration/json_test_example.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,17 @@ class PrivateConstructor {
bool operator ==(Object other) =>
other is PrivateConstructor && id == other.id && value == other.value;
}

@JsonEnum()
enum StandAloneEnum {
@JsonValue('a')
alpha,
@JsonValue('b')
beta,
@JsonValue('g')
gamma,
@JsonValue('d')
delta,
}

Iterable<String> get standAloneEnumKeys => _$StandAloneEnumEnumMap.values;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions json_serializable/test/json_serializable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Future<void> main() async {

const _expectedAnnotatedTests = {
'annotatedMethod',
'unsupportedEnum',
'BadFromFuncReturnType',
'BadNoArgs',
'BadOneNamed',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import 'dart:collection';

import 'package:json_annotation/json_annotation.dart';
// ignore: import_of_legacy_library_into_null_safe
import 'package:source_gen_test/annotations.dart';

part 'checked_test_input.dart';
Expand All @@ -25,6 +24,10 @@ part 'unknown_enum_value_test_input.dart';
@JsonSerializable() // ignore: invalid_annotation_target
const theAnswer = 42;

@ShouldThrow('`@JsonSerializable` can only be used on classes.')
@JsonSerializable() // ignore: invalid_annotation_target
enum unsupportedEnum { not, valid }

@ShouldThrow('`@JsonSerializable` can only be used on classes.')
@JsonSerializable() // ignore: invalid_annotation_target
Object annotatedMethod() => throw UnimplementedError();
Expand Down