Skip to content

Commit

Permalink
List.move (#1037)
Browse files Browse the repository at this point in the history
* Update realm-core to v12.12.0-7-g97ba3412b: Add realm_list_move to c-api (#6032)

* Update bindings

* Implement List.move

* Add tests of List.move

* Update CHANGELOG

* Using xor for hash is too simple.

Move(n, n).hashCode == Move(m, m).hashCode for all n, m.
Instead we use Object.hash introduced in Dart 2.14.

* Test List.move(n, n) is a no-op

* Document List.move

* Update CHANGELOG

* Simplify a bit and make it more explicit that move(n, n) is a no-op

* Use from/to for parameter names
  • Loading branch information
nielsenko authored Nov 16, 2022
1 parent 7f4331e commit 64cde9f
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

### Enhancements
* Support setting `maxNumberOfActiveVersions` when creating a `Configuration`. ([#1036](https://github.com/realm/realm-dart/pull/1036))
* Add List.move extension method that moves an element from one index to another. Delegates to ManagedRealmList.move for managed lists. This allows notifications to correctly report moves, as opposed to reporting moves as deletes + inserts. ([#1037](https://github.com/realm/realm-dart/issues/1037))

### Fixed
* Support mapping into `SyncSessionErrorCode` for "Compensating write" with error code 231. ([#1022](https://github.com/realm/realm-dart/pull/1022))
Expand Down
6 changes: 6 additions & 0 deletions lib/src/collections.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ class Move {
final int to;

const Move(this.from, this.to);

@override
bool operator ==(Object other) => other is Move && other.from == from && other.to == to;

@override
int get hashCode => Object.hash(from, to);
}

/// @nodoc
Expand Down
21 changes: 21 additions & 0 deletions lib/src/list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
//
////////////////////////////////////////////////////////////////////////////////
import 'dart:core';
import 'dart:async';
import 'dart:collection';
import 'dart:ffi';
Expand Down Expand Up @@ -144,6 +145,11 @@ class ManagedRealmList<T extends Object?> with RealmEntity, ListMixin<T> impleme
return result;
}

/// Move the element at index [from] to index [to].
void move(int from, int to) {
realmCore.listMoveElement(handle, from, to);
}

/// Removes all objects from this list; the length of the list becomes zero.
/// The objects are not deleted from the realm, but are no longer referenced from this list.
@override
Expand Down Expand Up @@ -326,3 +332,18 @@ class ListNotificationsController<T extends Object?> extends NotificationsContro
streamController.addError(error);
}
}

extension ListExtension<T> on List<T> {
/// Move the element at index [from] to index [to].
void move(int from, int to) {
RangeError.checkValidIndex(from, this, 'from', length);
RangeError.checkValidIndex(to, this, 'to', length);
if (to == from) return; // no-op
final self = this;
if (self is ManagedRealmList<T>) {
self.move(from, to);
} else {
insert(to, removeAt(from));
}
}
}
24 changes: 24 additions & 0 deletions lib/src/native/realm_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5174,6 +5174,30 @@ class RealmLibrary {
late final _realm_list_is_valid = _realm_list_is_validPtr
.asFunction<bool Function(ffi.Pointer<realm_list_t>)>();

/// Move the element at @a from_index to @a to_index.
///
/// @param from_index The index of the element to move.
/// @param to_index The index to move the element to.
/// @return True if no exception occurred.
bool realm_list_move(
ffi.Pointer<realm_list_t> arg0,
int from_index,
int to_index,
) {
return _realm_list_move(
arg0,
from_index,
to_index,
);
}

late final _realm_list_movePtr = _lookup<
ffi.NativeFunction<
ffi.Bool Function(ffi.Pointer<realm_list_t>, ffi.Size,
ffi.Size)>>('realm_list_move');
late final _realm_list_move = _realm_list_movePtr
.asFunction<bool Function(ffi.Pointer<realm_list_t>, int, int)>();

/// In a list of objects, delete all objects in the list and clear the list. In a
/// list of values, clear the list.
///
Expand Down
4 changes: 4 additions & 0 deletions lib/src/native/realm_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,10 @@ class _RealmCore {
_realmLib.invokeGetBool(() => _realmLib.realm_list_erase(handle._pointer, index));
}

void listMoveElement(RealmListHandle handle, int from, int to) {
_realmLib.invokeGetBool(() => _realmLib.realm_list_move(handle._pointer, from, to));
}

void listDeleteAll(RealmList list) {
_realmLib.invokeGetBool(() => _realmLib.realm_list_remove_all(list.handle._pointer));
}
Expand Down
7 changes: 4 additions & 3 deletions lib/src/realm_class.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export 'package:realm_common/realm_common.dart'

// always expose with `show` to explicitly control the public API surface
export 'app.dart' show AppException, App, MetadataPersistenceMode, AppConfiguration;
export 'collections.dart' show Move;
export "configuration.dart"
show
AfterResetCallback,
Expand Down Expand Up @@ -87,7 +88,7 @@ export "configuration.dart"
SyncErrorHandler,
SyncSessionError;
export 'credentials.dart' show AuthProviderType, Credentials, EmailPasswordAuthProvider;
export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges;
export 'list.dart' show RealmList, RealmListOfObject, RealmListChanges, ListExtension;
export 'migration.dart' show Migration;
export 'realm_object.dart'
show
Expand Down Expand Up @@ -522,8 +523,8 @@ class Realm implements Finalizable {
} else if (config is DisconnectedSyncConfiguration) {
compactConfig = config;
} else if (config is FlexibleSyncConfiguration) {
compactConfig = Configuration.disconnectedSync(config.schemaObjects.toList(), path: config.path,
fifoFilesFallbackPath: config.fifoFilesFallbackPath, encryptionKey: config.encryptionKey);
compactConfig = Configuration.disconnectedSync(config.schemaObjects.toList(),
path: config.path, fifoFilesFallbackPath: config.fifoFilesFallbackPath, encryptionKey: config.encryptionKey);
} else {
throw RealmError("Unsupported realm configuration type ${config.runtimeType}");
}
Expand Down
90 changes: 90 additions & 0 deletions test/list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1066,4 +1066,94 @@ Future<void> main([List<String>? args]) async {
expect(game.winnerByRound, isA<RealmList<Player>>());
expect(game.winnerByRound, isA<RealmList<Player?>>());
});

test('Move equality & hash', () {
expect(Move(1, 1), equals(Move(1, 1)));
expect(Move(1, 1), isNot(equals(Move(2, 2))));
expect(Move(1, 1).hashCode, equals(Move(1, 1).hashCode));
expect(Move(1, 1).hashCode, isNot(equals(Move(2, 2).hashCode)));
});

test('List.move', () {
final list = [0, 1, 2, 3];
list.move(1, 0);
expect(list, [1, 0, 2, 3]);
list.move(2, 3);
expect(list, [1, 0, 3, 2]);
list.move(0, 0); // no-op
expect(list, [1, 0, 3, 2]);
final length = list.length;
expect(() => list.move(-1, 0), throwsRangeError);
expect(() => list.move(0, -1), throwsRangeError);
expect(() => list.move(length, 0), throwsRangeError);
expect(() => list.move(0, length), throwsRangeError);
});

test('ManagedRealmList.move', () {
final config = Configuration.local([Team.schema, Person.schema]);
final realm = getRealm(config);

final alice = Person('Alice');
final bob = Person('Bob');
final carol = Person('Carol');
final dan = Person('Dan');
final players = [alice, bob, carol, dan];
final team = Team('Class of 92', players: players);

realm.write(() => realm.add(team));
expect(team.players.length, 4);
expect(team.players, [alice, bob, carol, dan]);

realm.write(() => team.players.move(1, 0));
expect(team.players, [bob, alice, carol, dan]);

realm.write(() => team.players.move(2, 3));
expect(team.players, [bob, alice, dan, carol]);

realm.write(() => team.players.move(0, 0)); // no-op
expect(team.players, [bob, alice, dan, carol]);

final length = team.players.length;
expect(() => realm.write(() => team.players.move(-1, 0)), throwsRangeError);
expect(() => realm.write(() => team.players.move(0, -1)), throwsRangeError);
expect(() => realm.write(() => team.players.move(length, 0)), throwsRangeError);
expect(() => realm.write(() => team.players.move(0, length)), throwsRangeError);

expect(realm.all<Person>(), unorderedEquals(players)); // nothing was added or disappeared from the realm

// .. when outside a write transaction
expect(() => team.players.move(3, 1), throws<RealmException>('Cannot modify managed objects outside of a write transaction'));
expect(() => realm.write(() => team.players.move(0, length)), throwsRangeError); // range error takes precedence
});

test('ManagedRealmList.move notifications', () async {
final config = Configuration.local([Team.schema, Person.schema]);
final realm = getRealm(config);

final alice = Person('Alice');
final bob = Person('Bob');
final carol = Person('Carol');
final dan = Person('Dan');
final players = [alice, bob, carol, dan];
final team = Team('Class of 92', players: players);

realm.write(() => realm.add(team));

expectLater(
team.players.changes,
emitsInOrder(<Matcher>[
isA<RealmListChanges<Person>>().having((ch) => ch.inserted, 'inserted', <int>[]), // always an empty event on subscription
isA<RealmListChanges<Person>>().having((ch) => ch.moved, 'moved', [Move(1, 0)]),
// no Move(0, 0)
isA<RealmListChanges<Person>>().having((ch) => ch.moved, 'moved', [Move(2, 3)]),
]));

realm.write(() => team.players.move(1, 0));
expect(team.players, [bob, alice, carol, dan]);

realm.write(() => team.players.move(0, 0)); // no-op

realm.write(() => team.players.move(2, 3));
expect(team.players, [bob, alice, dan, carol]);
});
}

0 comments on commit 64cde9f

Please sign in to comment.