-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add LazyMergedMap for merging Model objects (#105)
Close #102
- Loading branch information
Showing
6 changed files
with
232 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// 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 != 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 | ||
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; | ||
} | ||
} | ||
} | ||
|
||
extension MergeModels on Model { | ||
/// Creates a lazy merged view of `this` with [other]. | ||
Model mergeWith(Model other) => | ||
Model.fromJson(LazyMergedMapView(node, other.node)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// 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 'package:dart_model/dart_model.dart'; | ||
import 'package:dart_model/src/lazy_merged_map.dart'; | ||
import 'package:test/test.dart' hide test; | ||
import 'package:test/test.dart' as t show test; | ||
|
||
void main() { | ||
test('Can merge models with different libraries', () async { | ||
final libA = Library(); | ||
final libB = Library(); | ||
final a = Model()..uris['package:a/a.dart'] = libA; | ||
final b = Model()..uris['package:b/b.dart'] = libB; | ||
final c = a.mergeWith(b); | ||
expect(c.uris['package:a/a.dart'], libA); | ||
expect(c.uris['package:b/b.dart'], libB); | ||
}); | ||
|
||
test('Can merge models with different scopes from the same library', | ||
() async { | ||
final interfaceA = Interface(); | ||
final interfaceB = Interface(); | ||
final a = Model() | ||
..uris['package:a/a.dart'] = (Library()..scopes['A'] = interfaceA); | ||
final b = Model() | ||
..uris['package:a/a.dart'] = (Library()..scopes['B'] = interfaceB); | ||
final c = a.mergeWith(b); | ||
expect(c.uris['package:a/a.dart'], isA<LazyMergedMapView>()); | ||
expect(c.uris['package:a/a.dart']!.scopes['A'], interfaceA); | ||
expect(c.uris['package:a/a.dart']!.scopes['B'], interfaceB); | ||
}); | ||
|
||
test('Can merge models with the same interface but different properties', | ||
() async { | ||
final interfaceA1 = Interface(properties: Properties(isClass: true)); | ||
final interfaceA2 = Interface(properties: Properties(isAbstract: true)); | ||
final a = Model() | ||
..uris['package:a/a.dart'] = (Library()..scopes['A'] = interfaceA1); | ||
final b = Model() | ||
..uris['package:a/a.dart'] = (Library()..scopes['A'] = interfaceA2); | ||
final c = a.mergeWith(b); | ||
final properties = c.uris['package:a/a.dart']!.scopes['A']!.properties; | ||
expect(properties, isA<LazyMergedMapView>()); | ||
expect(properties.isClass, true); | ||
expect(properties.isAbstract, true); | ||
// Not set | ||
expect(() => properties.isConstructor, throwsA(isA<TypeError>())); | ||
}); | ||
|
||
test('Errors if maps have same the key with different values', () async { | ||
final interfaceA1 = Interface(properties: Properties(isClass: true)); | ||
final interfaceA2 = Interface(properties: Properties(isClass: false)); | ||
final a = Model() | ||
..uris['package:a/a.dart'] = (Library()..scopes['A'] = interfaceA1); | ||
final b = Model() | ||
..uris['package:a/a.dart'] = (Library()..scopes['A'] = interfaceA2); | ||
final c = a.mergeWith(b); | ||
expect(() => c.uris['package:a/a.dart']!.scopes['A']!.properties.isClass, | ||
throwsA(isA<StateError>())); | ||
}); | ||
} | ||
|
||
void test(String description, void Function() body) => | ||
t.test(description, () => Scope.query.run(body)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters