Skip to content

Commit d1d414e

Browse files
committed
feat: add support for moving and setting values with wildcards in projections
1 parent c9fbf92 commit d1d414e

File tree

2 files changed

+122
-15
lines changed

2 files changed

+122
-15
lines changed

packages/dogs/lib/src/projections.dart

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@ final class Projection<T> {
329329
}
330330

331331
/// Moves the value from [from] and writes it to [to].
332+
///
333+
/// To merge all values from a map to another map, specify [from] as
334+
/// `from.*` and [to] as the target path.
335+
///
336+
/// To move to the root map, specify [to] as `""` or `"."`.
332337
Projection<T> move(String from, String to) {
333338
transformers.add(Projections.move(from, to));
334339
return this;
@@ -384,6 +389,15 @@ final class Projection<T> {
384389
}
385390
return engine.fromNative<T>(result, type: type, tree: tree);
386391
}
392+
393+
/// Applies the projection to the given optional [initial] map and returns the result as a map.
394+
Map<String, dynamic> performMap([Map<String, dynamic>? initial]) {
395+
var result = initial ?? <String, dynamic>{};
396+
for (final transformer in transformers) {
397+
result = transformer(result);
398+
}
399+
return result;
400+
}
387401
}
388402

389403
/// A result of traversing a map.
@@ -400,6 +414,7 @@ class Projections {
400414
final subPaths = path.split(".");
401415
dynamic value = map;
402416
for (var path in subPaths) {
417+
if (path.isEmpty) continue;
403418
if (value is! Map) return (exists: false, value: null);
404419
if (!value.containsKey(path)) return (exists: false, value: null);
405420
value = value[path];
@@ -414,6 +429,7 @@ class Projections {
414429
final subPaths = path.split(".");
415430
final result = <String, dynamic>{};
416431
var current = result;
432+
final isRoot = path.replaceAll(".", "").isEmpty;
417433
for (var i = 0; i < subPaths.length - 1; i++) {
418434
final path = subPaths[i];
419435
if (current.containsKey(path)) {
@@ -423,27 +439,65 @@ class Projections {
423439
current = current[path];
424440
}
425441
}
442+
if (isRoot) return value;
426443
current[subPaths.last] = value;
427444
return $clone(map)..addAll(result);
428445
}
429446

447+
static Map<String,dynamic> $move(Map<String, dynamic> map, String from, String to, bool delete) {
448+
final isWildcard = from.endsWith(".*");
449+
if (isWildcard) {
450+
final path = from.substring(0, from.length - 2);
451+
final value = $get(map, path);
452+
if (!value.exists) return map;
453+
if (value.value is! Map) {
454+
throw ArgumentError("Source path '$from' is not a map");
455+
}
456+
if (delete) {
457+
map = $delete(map, path);
458+
}
459+
final target = $get(map, to);
460+
if (!target.exists) {
461+
return $set(map, to, value.value);
462+
}
463+
if (target.value is! Map) {
464+
throw ArgumentError("Target path '$to' is not a map");
465+
}
466+
467+
final targetMap = $clone((target.value as Map<String, dynamic>?) ?? <String, dynamic>{});
468+
targetMap.addAll(value.value as Map<String, dynamic>);
469+
return $set(map, to, targetMap);
470+
} else {
471+
final value = $get(map, from);
472+
if (!value.exists) return map;
473+
if (delete) {
474+
map = $delete(map, from);
475+
}
476+
return $set(map, to, value.value);
477+
}
478+
}
479+
430480
/// Deletes the value at [path] in the given [map]. Returns a new map
431481
/// with the updated values and leaves the original map untouched.
432482
static Map<String, dynamic> $delete(Map<String, dynamic> map, String path) {
483+
if (path.replaceAll(".", "").isEmpty) return <String, dynamic>{};
433484
final subPaths = path.split(".");
434-
final result = <String, dynamic>{};
435-
var current = result;
436-
for (var i = 0; i < subPaths.length - 1; i++) {
437-
final path = subPaths[i];
438-
if (current.containsKey(path)) {
439-
current = current[path];
485+
486+
final Map<String, dynamic> result = $clone(map);
487+
Map<String, dynamic> current = result;
488+
489+
for (int i = 0; i < subPaths.length - 1; i++) {
490+
final key = subPaths[i];
491+
if (current[key] is Map<String, dynamic>) {
492+
current[key] = Map<String, dynamic>.from(current[key]);
493+
current = current[key];
440494
} else {
441-
current[path] = <String, dynamic>{};
442-
current = current[path];
495+
return result;
443496
}
444497
}
498+
445499
current.remove(subPaths.last);
446-
return $clone(map)..addAll(result);
500+
return result;
447501
}
448502

449503
/// Deep clones the given [map] and returns a new map with the same values.
@@ -498,12 +552,7 @@ class Projections {
498552

499553
/// Applies a transformer that moves the value at [from] to [to].
500554
static ProjectionTransformer move(String from, String to) {
501-
return (data) {
502-
final result = $get(data, from);
503-
if (!result.exists) return data;
504-
data = $delete(data, from);
505-
return $set(data, to, result.value);
506-
};
555+
return (data) => Projections.$move(data, from, to, true);
507556
}
508557
}
509558

packages/dogs/test/projection_transformers.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import "package:dogs_core/dogs_core.dart";
1818
import "package:test/test.dart";
1919

20+
import "utils/schema.dart";
21+
2022
void main() {
2123
test("Transform root field", () {
2224
final buffer = <String, dynamic>{
@@ -75,6 +77,7 @@ void main() {
7577
}
7678
};
7779
final transformed = Projections.move("a.b", "d")(buffer);
80+
print(transformed);
7881
expect(transformed["a"].containsKey("b"), false);
7982
expect(transformed["d"], 1);
8083
expect(buffer["a"]["b"], 1);
@@ -92,4 +95,59 @@ void main() {
9295
expect(transformed["a"]["d"]["e"], 1);
9396
expect(buffer["a"]["b"], 1);
9497
});
98+
99+
test("Set root using set", () {
100+
final buffer = <String, dynamic>{
101+
"a": <String, dynamic>{
102+
"b": 1,
103+
"c": 2,
104+
}
105+
};
106+
final transformed = Projections.$set(buffer, "", {"x": 42});
107+
expect(transformed, unorderedDeepEquals({"x": 42}));
108+
});
109+
110+
test("Move using trailing wildcard", () {
111+
final buffer = <String, dynamic>{
112+
"a": <String, dynamic>{
113+
"b": 1,
114+
"c": 2,
115+
},
116+
"d": <String, dynamic>{
117+
"e": 3,
118+
}
119+
};
120+
final transformed = Projections.$move(buffer, "a.*", "d", true);
121+
expect(
122+
transformed,
123+
unorderedDeepEquals({
124+
"d": <String, dynamic>{
125+
"e": 3,
126+
"b": 1,
127+
"c": 2,
128+
}
129+
}));
130+
});
131+
132+
test("Move using trailing wildcard to root", () {
133+
final buffer = <String, dynamic>{
134+
"a": <String, dynamic>{
135+
"b": 1,
136+
"c": 2,
137+
},
138+
"d": <String, dynamic>{
139+
"e": 3,
140+
}
141+
};
142+
final transformed = Projections.$move(buffer, "a.*", "", true);
143+
expect(
144+
transformed,
145+
unorderedDeepEquals({
146+
"d": <String, dynamic>{
147+
"e": 3,
148+
},
149+
"b": 1,
150+
"c": 2,
151+
}));
152+
});
95153
}

0 commit comments

Comments
 (0)