blob: 4e1815c32b638a6921c9a64c6c26c10cb1e52b6e [file] [log] [blame] [edit]
// 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.
import 'dart:collection';
import 'dart_model.dart';
/// An implementation of a lazy merged json [Map] view over two [Map]s.
///
/// The intended use case is for merging JSON payloads together into a single
/// payload, where their structure is the same.
///
/// If both maps have the same key present, the logic for the values of those
/// shared keys goes as follows:
///
/// - If both values are `Map<String, Object?>`, a nested [LazyMergedMapView]
/// is returned.
/// - Else if they are equal values, the value from [left] is returned.
/// - Else a [StateError] is thrown.
///
/// Nested [List]s are not specifically handled at this time and must be equal.
///
/// The [keys] getter will de-duplicate the keys.
class LazyMergedMapView extends MapBase<String, Object?> {
final Map<String, Object?> left;
final Map<String, Object?> right;
LazyMergedMapView(this.left, this.right);
@override
Object? operator [](Object? key) {
// TODO: Can we do better? These lookups can each be linear for buffer maps.
var leftValue = left[key];
var rightValue = right[key];
if (leftValue != null) {
if (rightValue != null) {
if (leftValue is Map<String, Object?> &&
rightValue is Map<String, Object?>) {
return LazyMergedMapView(leftValue, rightValue);
}
if (leftValue is List<Object?> && rightValue is List<Object?>) {
if (leftValue.length != rightValue.length) {
throw StateError('Cannot merge lists of different lengths, '
'got $leftValue and $rightValue');
}
// TODO: Something better for lists, it isn't clear how to merge them.
return leftValue;
} else if (leftValue != rightValue) {
throw StateError('Cannot merge maps with different values, and '
'$leftValue != $rightValue');
}
return leftValue;
}
return leftValue;
} else if (rightValue != null) {
return rightValue;
}
return null;
}
@override
void operator []=(String key, Object? value) =>
throw UnsupportedError('Merged maps are read only');
@override
bool operator ==(Object other) =>
other is LazyMergedMapView && other.left == left && other.right == right;
@override
int get hashCode => Object.hash(left, right);
@override
void clear() => throw UnsupportedError('Merged maps are read only');
@override
Iterable<String> get keys sync* {
var seen = <String>{};
for (var key in left.keys.followedBy(right.keys)) {
if (seen.add(key)) yield key;
}
}
@override
Object? remove(Object? key) =>
throw UnsupportedError('Merged maps are read only');
}
extension AllMaps on Map<String, Object?> {
/// All the maps merged into this map, recursively expanded.
Iterable<Map<String, Object?>> get expand sync* {
if (this case final LazyMergedMapView self) {
yield* self.left.expand;
yield* self.right.expand;
} else {
yield this;
}
}
/// All the maps merged into this map, recursively expanded, including the
/// merged map objects themselves.
Iterable<Map<String, Object?>> get expandWithMerged sync* {
if (this case final LazyMergedMapView self) {
yield self;
yield* self.left.expand;
yield* self.right.expand;
} else {
yield this;
}
}
}
extension MergeModels on Model {
/// Creates a lazy merged view of `this` with [other].
Model mergeWith(Model other) =>
Model.fromJson(LazyMergedMapView(node, other.node));
}