Skip to content
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

Adding case insensitive to enum based on loading JsonEnum config #1155

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
24 changes: 22 additions & 2 deletions json_annotation/lib/src/enum_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@

import 'json_key.dart';

/// Compare an enum value against a source using case-insensitive.
///
/// Exposed only for code generated by `package:json_serializable`.
/// Not meant to be used directly by user code.
bool $enumCompareCaseInsensitive<V>(V arg1, Object arg2) =>
((arg1 is String) && (arg2 is String))
? (arg1.toLowerCase() == arg2.toLowerCase())
: arg1 == arg2;

/// Compare an enum value against a source.
///
/// Exposed only for code generated by `package:json_serializable`.
/// Not meant to be used directly by user code.
bool $enumCompareStandard<V>(V arg1, Object arg2) => arg1 == arg2;

/// Returns the key associated with value [source] from [enumValues], if one
/// exists.
///
Expand All @@ -18,13 +33,16 @@ K? $enumDecodeNullable<K extends Enum, V>(
Map<K, V> enumValues,
Object? source, {
Enum? unknownValue,
bool Function(V arg1, Object arg2)? comparator,
}) {
if (source == null) {
return null;
}

comparator ??= $enumCompareStandard;

for (var entry in enumValues.entries) {
if (entry.value == source) {
if (comparator(entry.value, source)) {
return entry.key;
}
}
Expand Down Expand Up @@ -65,6 +83,7 @@ K $enumDecode<K extends Enum, V>(
Map<K, V> enumValues,
Object? source, {
K? unknownValue,
bool Function(V arg1, Object arg2)? comparator,
}) {
if (source == null) {
throw ArgumentError(
Expand All @@ -73,8 +92,9 @@ K $enumDecode<K extends Enum, V>(
);
}

comparator ??= $enumCompareStandard;
for (var entry in enumValues.entries) {
if (entry.value == source) {
if (comparator(entry.value, source)) {
return entry.key;
}
}
Expand Down
7 changes: 7 additions & 0 deletions json_annotation/lib/src/json_enum.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class JsonEnum {
const JsonEnum({
this.alwaysCreate = false,
this.fieldRename = FieldRename.none,
this.caseInsensitive = false,
this.valueField,
});

Expand All @@ -36,6 +37,12 @@ class JsonEnum {
/// for entries annotated with [JsonValue].
final FieldRename fieldRename;

/// If `true`, enum comparison will be done using case-insensitive.
///
/// The default, `false`, means enum comparison will be done using
/// case-sensitive.
final bool caseInsensitive;

/// Specifies the field within an "enhanced enum" to use as the value
/// to use for serialization.
///
Expand Down
12 changes: 10 additions & 2 deletions json_annotation/lib/src/json_key.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,26 @@ class JsonKey {
/// valid on a nullable enum field.
final Enum? unknownEnumValue;

/// If true, enum will be parsed with case-insensitive.
/// Specifically, both values will be lower-cased and compared.
///
/// Valid only on enum fields with a compatible enum value.
final bool caseInsensitive;

/// Creates a new [JsonKey] instance.
///
/// Only required when the default behavior is not desired.
const JsonKey({
@Deprecated('Has no effect') bool? nullable,
@Deprecated('Has no effect')
bool? nullable,
this.defaultValue,
this.disallowNullValue,
this.fromJson,
@Deprecated(
'Use `includeFromJson` and `includeToJson` with a value of `false` '
'instead.',
)
this.ignore,
this.ignore,
this.includeFromJson,
this.includeIfNull,
this.includeToJson,
Expand All @@ -170,6 +177,7 @@ class JsonKey {
this.required,
this.toJson,
this.unknownEnumValue,
this.caseInsensitive = false,
});

/// Sentinel value for use with [unknownEnumValue].
Expand Down
6 changes: 5 additions & 1 deletion json_serializable/lib/src/enum_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ JsonEnum _fromAnnotation(DartObject? dartObject) {
final reader = ConstantReader(dartObject);
return JsonEnum(
alwaysCreate: reader.read('alwaysCreate').literalValue as bool,
fieldRename: readEnum(reader.read('fieldRename'), FieldRename.values)!,
fieldRename: enumValueForDartObject(
reader.read('fieldRename').objectValue,
FieldRename.values,
(f) => f.toString().split('.')[1],
),
valueField: reader.read('valueField').literalValue as String?,
);
}
Expand Down
8 changes: 8 additions & 0 deletions json_serializable/lib/src/json_key_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
final ctorParamDefault = classAnnotation.ctorParamDefaults[element.name];

if (obj.isNull) {
final enumObj = jsonEnumAnnotation(element);

return _populateJsonKey(
classAnnotation,
element,
defaultValue: ctorParamDefault,
caseInsensitive: enumObj.isNull
? null
: enumObj.read('caseInsensitive').literalValue as bool?,
includeFromJson: classAnnotation.ignoreUnannotated ? false : null,
includeToJson: classAnnotation.ignoreUnannotated ? false : null,
);
Expand Down Expand Up @@ -273,6 +278,7 @@ KeyConfig _from(FieldElement element, ClassConfig classAnnotation) {
createAnnotationValue('unknownEnumValue', mustBeEnum: true),
includeToJson: includeToJson,
includeFromJson: includeFromJson,
caseInsensitive: obj.read('caseInsensitive').literalValue as bool?,
);
}

Expand All @@ -286,6 +292,7 @@ KeyConfig _populateJsonKey(
String? readValueFunctionName,
bool? required,
String? unknownEnumValue,
bool? caseInsensitive,
bool? includeToJson,
bool? includeFromJson,
}) {
Expand All @@ -307,6 +314,7 @@ KeyConfig _populateJsonKey(
readValueFunctionName: readValueFunctionName,
required: required ?? false,
unknownEnumValue: unknownEnumValue,
caseInsensitive: caseInsensitive,
includeFromJson: includeFromJson,
includeToJson: includeToJson,
);
Expand Down
3 changes: 3 additions & 0 deletions json_serializable/lib/src/type_helpers/config_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class KeyConfig {

final String? unknownEnumValue;

final bool? caseInsensitive;

final String? readValueFunctionName;

KeyConfig({
Expand All @@ -35,6 +37,7 @@ class KeyConfig {
required this.readValueFunctionName,
required this.required,
required this.unknownEnumValue,
required this.caseInsensitive,
});
}

Expand Down
2 changes: 2 additions & 0 deletions json_serializable/lib/src/type_helpers/enum_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
expression,
if (jsonKey.unknownEnumValue != null)
'unknownValue: ${jsonKey.unknownEnumValue}',
if ((jsonKey.caseInsensitive ?? false) == true)
r'comparator: $enumCompareCaseInsensitive',
];

return '$functionName(${args.join(', ')})';
Expand Down
10 changes: 10 additions & 0 deletions json_serializable/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import 'type_helpers/config_types.dart';

const _jsonKeyChecker = TypeChecker.fromRuntime(JsonKey);

const _jsonEnumChecker = TypeChecker.fromRuntime(JsonEnum);

DartObject? _jsonKeyAnnotation(FieldElement element) =>
_jsonKeyChecker.firstAnnotationOf(element) ??
(element.getter == null
Expand All @@ -22,6 +24,14 @@ DartObject? _jsonKeyAnnotation(FieldElement element) =>
ConstantReader jsonKeyAnnotation(FieldElement element) =>
ConstantReader(_jsonKeyAnnotation(element));

DartObject? _jsonEnumAnnotation(Element? element) =>
(element != null && element is EnumElement)
? _jsonEnumChecker.firstAnnotationOf(element)
: null;

ConstantReader jsonEnumAnnotation(FieldElement element) =>
ConstantReader(_jsonEnumAnnotation(element.type.element));

/// Returns `true` if [element] is annotated with [JsonKey].
bool hasJsonKeyAnnotation(FieldElement element) =>
_jsonKeyAnnotation(element) != null;
Expand Down
17 changes: 16 additions & 1 deletion json_serializable/test/integration/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'converter_examples.dart';
import 'create_per_field_to_json_example.dart';
import 'field_map_example.dart';
import 'json_enum_example.dart';
import 'json_test_common.dart' show Category, Platform, StatusCode;
import 'json_test_common.dart' show Category, Colors, Platform, StatusCode;
import 'json_test_example.dart';

Matcher _throwsArgumentError(matcher) =>
Expand Down Expand Up @@ -91,6 +91,21 @@ void main() {
roundTripOrder(order);
});

test('case insensitive map', () {
final jsonOrder = {'category': 'CHaRmED', 'color': 'bLuE'};
final order = Order.fromJson(jsonOrder);
expect(order.category, Category.charmed);
expect(order.color, Colors.blue);
});

test('case sensitive map throw', () {
expect(
() => Order.fromJson({'direction': 'dOwN'}),
_throwsArgumentError(
'`dOwN` is not one of the supported values: up, down, left, right'),
);
});

test('required, but missing enum value fails', () {
expect(
() => Person.fromJson({
Expand Down
6 changes: 5 additions & 1 deletion json_serializable/test/integration/json_test_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'dart:collection';

import 'package:json_annotation/json_annotation.dart';

@JsonEnum(fieldRename: FieldRename.kebab)
@JsonEnum(fieldRename: FieldRename.kebab, caseInsensitive: true)
enum Category {
top,
bottom,
Expand All @@ -19,6 +19,10 @@ enum Category {
notDiscoveredYet
}

enum Colors { red, green, yellow, blue }

enum Direction { up, down, left, right }

enum StatusCode {
@JsonValue(200)
success,
Expand Down
3 changes: 3 additions & 0 deletions json_serializable/test/integration/json_test_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class Order {
Duration? duration;

final Category? category;
@JsonKey(caseInsensitive: true)
Colors? color;
Direction? direction;
final UnmodifiableListView<Item>? items;
Platform? platform;
Map<String, Platform>? altPlatforms;
Expand Down
25 changes: 23 additions & 2 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.