Skip to content

Commit 609c52f

Browse files
committed
add support for fast_immutable_collections types including ListMap, ListSet, and IMapOfSets
1 parent 07e7a48 commit 609c52f

File tree

11 files changed

+256
-24
lines changed

11 files changed

+256
-24
lines changed

builders/json_serializable_fic/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.1.4
2+
3+
- **FEAT**: Add support for `fast_immutable_collections` `ListMap`, `ListSet` and `IMapOfSets` types.
4+
15
## 1.1.3
26

37
- **CHORE**: Update `fast_immutable_collections` to ^11.1.0

builders/json_serializable_fic/lib/builder.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Builder jsonSerializable(BuilderOptions options) {
1818
FICIListTypeHelper(),
1919
FICISetTypeHelper(),
2020
FICIMapTypeHelper(),
21+
FICListSetTypeHelper(),
22+
FICListMapTypeHelper(),
23+
FICIMapOfSetsTypeHelper(),
2124
], config: config),
2225
const JsonLiteralGenerator(),
2326
], 'json_serializable');
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
export 'package:json_serializable_fic_typehelpers/json_serializable_fic_typehelpers.dart'
2-
show FICIListTypeHelper, FICIMapTypeHelper, FICISetTypeHelper;
2+
show
3+
FICIListTypeHelper,
4+
FICIMapTypeHelper,
5+
FICISetTypeHelper,
6+
FICListSetTypeHelper,
7+
FICListMapTypeHelper,
8+
FICIMapOfSetsTypeHelper;

builders/json_serializable_fic/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: json_serializable_fic
22
description: Adding support for fast immutable collections for json_serializable
3-
version: 1.1.3
3+
version: 1.1.4
44
homepage: https://github.com/knaeckeKami/immutable_json_list_serializer
55

66
environment:
@@ -21,4 +21,4 @@ dependencies:
2121
source_gen: ^4.2.0
2222
analyzer: ^10.0.1
2323
build: ^4.0.4
24-
json_serializable_fic_typehelpers: ^2.2.1
24+
json_serializable_fic_typehelpers: ^2.2.3

builders/json_serializable_fic/test/integration/model.dart

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class MyModel {
2424
required this.nullableMap,
2525
required this.nullableSet,
2626
required this.enumMap,
27+
required this.myListSet,
28+
required this.myListMap,
29+
required this.myMapOfSets,
2730
});
2831

2932
final IList<int> myList;
@@ -60,14 +63,20 @@ class MyModel {
6063

6164
final IMap<MyEnum, String> enumMap;
6265

66+
final ListSet<String> myListSet;
67+
68+
final ListMap<String, int> myListMap;
69+
70+
final IMapOfSets<String, int> myMapOfSets;
71+
6372
factory MyModel.fromJson(Map<String, dynamic> json) =>
6473
_$MyModelFromJson(json);
6574

6675
Map<String, dynamic> toJson() => _$MyModelToJson(this);
6776

6877
@override
6978
String toString() {
70-
return 'MyModel{myList: $myList, myString: $myString, myNested: $myNested, normalList: $normalList, normalSet: $normalSet, builtMap: $builtMap, builtMapString: $builtMapString, builtMapNested: $builtMapNested, nullList: $nullList, listWithNullable: $listWithNullable, nullablelistWithNullable: $nullablelistWithNullable, nullSet: $nullSet, nullableSet: $nullableSet, nullMap: $nullMap, nullableMap: $nullableMap, dynamicMap: $dynamicMap, enumMap: $enumMap}';
79+
return 'MyModel{myList: $myList, myString: $myString, myNested: $myNested, normalList: $normalList, normalSet: $normalSet, builtMap: $builtMap, builtMapString: $builtMapString, builtMapNested: $builtMapNested, nullList: $nullList, listWithNullable: $listWithNullable, nullablelistWithNullable: $nullablelistWithNullable, nullSet: $nullSet, nullableSet: $nullableSet, nullMap: $nullMap, nullableMap: $nullableMap, dynamicMap: $dynamicMap, enumMap: $enumMap, myListSet: $myListSet, myListMap: $myListMap, myMapOfSets: $myMapOfSets}';
7180
}
7281

7382
@override
@@ -91,26 +100,35 @@ class MyModel {
91100
nullMap == other.nullMap &&
92101
nullableMap == other.nullableMap &&
93102
dynamicMap == other.dynamicMap &&
94-
enumMap == other.enumMap;
103+
enumMap == other.enumMap &&
104+
DeepCollectionEquality().equals(myListSet, other.myListSet) &&
105+
DeepCollectionEquality().equals(myListMap, other.myListMap) &&
106+
myMapOfSets == other.myMapOfSets;
95107

96108
@override
97-
int get hashCode =>
98-
myList.hashCode ^
99-
myString.hashCode ^
100-
myNested.hashCode ^
101-
normalList.hashCode ^
102-
normalSet.hashCode ^
103-
builtMap.hashCode ^
104-
builtMapString.hashCode ^
105-
builtMapNested.hashCode ^
106-
nullList.hashCode ^
107-
listWithNullable.hashCode ^
108-
nullablelistWithNullable.hashCode ^
109-
nullSet.hashCode ^
110-
nullableSet.hashCode ^
111-
nullMap.hashCode ^
112-
nullableMap.hashCode ^
113-
dynamicMap.hashCode;
109+
int get hashCode {
110+
final deepEquality = DeepCollectionEquality();
111+
return myList.hashCode ^
112+
myString.hashCode ^
113+
myNested.hashCode ^
114+
deepEquality.hash(normalList) ^
115+
deepEquality.hash(normalSet) ^
116+
builtMap.hashCode ^
117+
builtMapString.hashCode ^
118+
builtMapNested.hashCode ^
119+
nullList.hashCode ^
120+
listWithNullable.hashCode ^
121+
nullablelistWithNullable.hashCode ^
122+
nullSet.hashCode ^
123+
nullableSet.hashCode ^
124+
nullMap.hashCode ^
125+
nullableMap.hashCode ^
126+
dynamicMap.hashCode ^
127+
enumMap.hashCode ^
128+
deepEquality.hash(myListSet) ^
129+
deepEquality.hash(myListMap) ^
130+
myMapOfSets.hashCode;
131+
}
114132
}
115133

116134
@JsonSerializable()

builders/json_serializable_fic/test/integration/model.g.dart

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builders/json_serializable_fic/test/integration/model_test.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ final model = MyModel(
2323
nullableMap: {"a": null}.toIMap(),
2424
nullableSet: {"", null}.toISet(),
2525
enumMap: IMap<MyEnum, String>({MyEnum.one: "1", MyEnum.two: "2"}),
26+
myListSet: ListSet.of(["x", "y", "z"]),
27+
myListMap: ListMap.of({"key1": 10, "key2": 20}),
28+
myMapOfSets: IMapOfSets<String, int>({
29+
"set1": {1, 2, 3},
30+
"set2": {4, 5},
31+
}),
2632
);
2733

2834
const jsonMapExpected = {
@@ -47,6 +53,12 @@ const jsonMapExpected = {
4753
"nullableMap": {"a": null},
4854
"nullableSet": ["", null],
4955
"enumMap": {"one": "1", "two": "2"},
56+
"myListSet": ["x", "y", "z"],
57+
"myListMap": {"key1": 10, "key2": 20},
58+
"myMapOfSets": {
59+
"set1": [1, 2, 3],
60+
"set2": [4, 5],
61+
},
5062
};
5163

5264
void main() {

typehelpers/json_serializable_fic_typehelpers/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.2.3
2+
3+
- **FEAT**: Add support for `fast_immutable_collections` `ListMap`, `ListSet` and `IMapOfSets` types.
4+
15
## 2.2.2
26

37
- **CHORE**: Update `fast_immutable_collections` to ^11.1.0
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
export 'src/fic_type_helpers.dart'
2-
show FICIMapTypeHelper, FICIListTypeHelper, FICISetTypeHelper;
2+
show
3+
FICIMapTypeHelper,
4+
FICIListTypeHelper,
5+
FICISetTypeHelper,
6+
FICListSetTypeHelper,
7+
FICListMapTypeHelper,
8+
FICIMapOfSetsTypeHelper;

typehelpers/json_serializable_fic_typehelpers/lib/src/fic_type_helpers.dart

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
import 'package:analyzer/dart/element/type.dart';
66
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
77
import 'package:json_serializable_type_helper_utils/json_serializable_type_helper_utils.dart';
8+
import 'package:json_serializable/type_helper.dart';
9+
// ignore: implementation_imports
10+
import 'package:json_serializable/src/constants.dart';
811
import 'package:source_gen/source_gen.dart' show TypeChecker;
12+
import 'package:source_helper/source_helper.dart';
913

1014
class FICIListTypeHelper extends CustomIterableTypeHelper<IList> {
1115
@override
@@ -86,3 +90,165 @@ class FICIMapTypeHelper extends CustomMapTypeHelper<IMap> {
8690
return '$mapExpression$optionalQuestion.unlockLazy';
8791
}
8892
}
93+
94+
class FICListSetTypeHelper extends CustomIterableTypeHelper<ListSet> {
95+
@override
96+
TypeChecker get typeChecker => const TypeChecker.typeNamed(
97+
ListSet,
98+
inPackage: 'fast_immutable_collections',
99+
);
100+
101+
@override
102+
String deserializeFromIterableExpression(
103+
String expression,
104+
DartType resolvedGenericType,
105+
) {
106+
// Convert to List first to ensure consistent ordering and equality
107+
return 'ListSet.of(($expression).toList())';
108+
}
109+
110+
@override
111+
String serializeToList(
112+
String expression,
113+
DartType resolvedGenericType,
114+
bool isExpressionNullable,
115+
) {
116+
// not needed as ListSet implements Iterable
117+
throw UnimplementedError();
118+
}
119+
}
120+
121+
class FICListMapTypeHelper extends CustomMapTypeHelper<ListMap> {
122+
@override
123+
TypeChecker get typeChecker => const TypeChecker.typeNamed(
124+
ListMap,
125+
inPackage: 'fast_immutable_collections',
126+
);
127+
128+
@override
129+
String deserializeFromMapExpression(
130+
String mapExpression,
131+
DartType keyType,
132+
DartType valueType,
133+
) {
134+
return 'ListMap.of($mapExpression)';
135+
}
136+
137+
@override
138+
String serializeToMapExpression(
139+
String mapExpression,
140+
DartType keyType,
141+
DartType valueType,
142+
bool isMapExpressionNullable,
143+
) {
144+
// ListMap implements Map directly, so we can use it as-is
145+
// No conversion needed since ListMap is already a Map
146+
return mapExpression;
147+
}
148+
}
149+
150+
class FICIMapOfSetsTypeHelper extends TypeHelper<TypeHelperContextWithConfig> {
151+
@override
152+
final TypeChecker typeChecker = const TypeChecker.typeNamed(
153+
IMapOfSets,
154+
inPackage: 'fast_immutable_collections',
155+
);
156+
157+
DartType _getKeyType(DartType type) =>
158+
type.typeArgumentsOf(typeChecker)!.first;
159+
160+
DartType _getValueType(DartType type) {
161+
// IMapOfSets<K, V> is Map<K, Set<V>>
162+
// The type arguments are [K, V] where V is the element type of the sets
163+
// So we can directly return the second type argument
164+
return type.typeArgumentsOf(typeChecker)!.last;
165+
}
166+
167+
@override
168+
Object? serialize(
169+
DartType targetType,
170+
String expression,
171+
TypeHelperContext context,
172+
) {
173+
if (!typeChecker.isAssignableFromType(targetType)) {
174+
return null;
175+
}
176+
177+
final keyType = _getKeyType(targetType);
178+
final valueType = _getValueType(targetType);
179+
final targetTypeIsNullable = targetType.isNullableType;
180+
final optionalQuestion = targetTypeIsNullable ? '?' : '';
181+
182+
// Serialize each Set<V> value to List<V>
183+
final subFieldValue = context.serialize(valueType, closureArg);
184+
final subKeyValue = forType(keyType)?.serialize(keyType, keyParam, false) ??
185+
context.serialize(keyType, keyParam);
186+
187+
if (closureArg == subFieldValue && keyParam == subKeyValue) {
188+
// Simple case: just convert Set<V> to List<V>
189+
return '$expression$optionalQuestion.unlock.map(($keyParam, v) => MapEntry($subKeyValue, v.toList()))';
190+
}
191+
192+
// Complex case: need to serialize individual values
193+
return '$expression$optionalQuestion.unlock.map(($keyParam, v) => MapEntry($subKeyValue, (v as Set<${valueType.getDisplayString()}>).map((e) => $subFieldValue).toList()))';
194+
}
195+
196+
@override
197+
Object? deserialize(
198+
DartType targetType,
199+
String expression,
200+
TypeHelperContextWithConfig context,
201+
bool defaultProvided,
202+
) {
203+
if (!typeChecker.isExactlyType(targetType)) {
204+
return null;
205+
}
206+
207+
final keyType = _getKeyType(targetType);
208+
final valueType = _getValueType(targetType);
209+
final targetTypeIsNullable = targetType.isNullableType || defaultProvided;
210+
final optionalQuestion = targetTypeIsNullable ? '?' : '';
211+
212+
checkSafeKeyType(expression, keyType);
213+
214+
final itemSubValObj = context.deserialize(valueType, closureArg);
215+
final itemSubVal = itemSubValObj.toString();
216+
final mapCast = context.config.anyMap ? 'as Map' : 'as Map<String, dynamic>';
217+
218+
String keyUsage;
219+
if (keyType.isEnum) {
220+
keyUsage = context.deserialize(keyType, keyParam).toString();
221+
} else if (context.config.anyMap &&
222+
!(keyType.isDartCoreObject || keyType is DynamicType)) {
223+
keyUsage = '$keyParam as String';
224+
} else if (context.config.anyMap &&
225+
keyType.isDartCoreObject &&
226+
!keyType.isNullableType) {
227+
keyUsage = '$keyParam as Object';
228+
} else {
229+
keyUsage = keyParam;
230+
}
231+
232+
final toFromString = forType(keyType);
233+
if (toFromString != null) {
234+
keyUsage = toFromString
235+
.deserialize(keyType, keyUsage, false, true)
236+
.toString();
237+
}
238+
239+
// Convert Map<K, List<V>> to Map<K, Set<V>>, then use .lock
240+
if (closureArg == itemSubValObj) {
241+
return wrapNullableIfNecessary(
242+
expression,
243+
'(($expression $mapCast).map(($keyParam, v) => MapEntry($keyUsage, Set<${valueType.getDisplayString()}>.from(v as List)))).lock',
244+
targetTypeIsNullable,
245+
);
246+
}
247+
248+
return wrapNullableIfNecessary(
249+
expression,
250+
'(($expression $mapCast).map(($keyParam, v) => MapEntry($keyUsage, Set<${valueType.getDisplayString()}>.from((v as List).map((e) => $itemSubVal))))).lock',
251+
targetTypeIsNullable,
252+
);
253+
}
254+
}

0 commit comments

Comments
 (0)