blob: 09f7409bf02110edf859acf3f65142b52410b50a [file] [log] [blame]
// Copyright 2019 The Flutter Authors. 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:async';
import 'dart:math' as math;
import 'package:gcloud/datastore.dart' show Datastore, OrderDirection;
import 'package:gcloud/db.dart';
/// Signature for a callback function that will be notified whenever a
/// [FakeQuery] is run.
///
/// The `results` argument contains the provisional results of the query (after
/// [FakeQuery.limit] and [FakeQuery.offset] have been applied). Callers can
/// affect the results of the query by returning a different set of results
/// from the callback.
///
/// The callback must not return null.
typedef QueryCallback<T extends Model> = Iterable<T> Function(
Iterable<T> results);
/// Signature for a callback function that will be notified whenever `commit()`
/// is called, either via [FakeDatastoreDB.commit] or [FakeTransaction.commit].
///
/// The `inserts` and `deletes` arguments represent the prospective mutations.
/// Both arguments are immutable.
///
/// This callback will be invoked before any mutations are applied, so by
/// throwing an exception, callbacks can simulate a failed commit.
typedef CommitCallback = void Function(List<Model> inserts, List<Key> deletes);
/// A fake datastore database implementation.
///
/// This datastore's contents are stored in a single [values] map. Callers can
/// set up the map to populate the datastore in a way that works for their
/// test.
class FakeDatastoreDB implements DatastoreDB {
FakeDatastoreDB({
Map<Key, Model> values,
Map<Type, QueryCallback<dynamic>> onQuery,
this.onCommit,
}) : values = values ?? <Key, Model>{},
onQuery = onQuery ?? <Type, QueryCallback<dynamic>>{};
final Map<Key, Model> values;
final Map<Type, QueryCallback<dynamic>> onQuery;
CommitCallback onCommit;
/// Adds a [QueryCallback] to the set of callbacks that will be notified when
/// queries are run.
///
/// The [callback] argument will replace any existing callback that has been
/// specified for type `T`, as only one callback may exist per type.
void addOnQuery<T extends Model>(QueryCallback<T> callback) {
final QueryCallback<dynamic> untypedCallback = (Iterable<dynamic> results) {
return callback(results.cast<T>()).cast<dynamic>();
};
onQuery[T] = untypedCallback;
}
@override
Future<dynamic> commit({List<Model> inserts, List<Key> deletes}) async {
inserts ??= <Model>[];
deletes ??= <Key>[];
if (onCommit != null) {
onCommit(
List<Model>.unmodifiable(inserts), List<Key>.unmodifiable(deletes));
}
deletes.forEach(values.remove);
for (Model model in inserts) {
values[model.key] = model;
}
}
@override
Datastore get datastore => throw UnimplementedError();
@override
Partition get defaultPartition => Partition(null);
@override
Key get emptyKey => defaultPartition.emptyKey;
@override
Future<List<T>> lookup<T extends Model>(List<Key> keys) async {
return keys.map<T>((Key key) => values[key] as T).toList();
}
@override
Future<T> lookupValue<T extends Model>(Key key, {T orElse()}) async {
final List<T> values = await lookup(<Key>[key]);
T value = values.single;
if (value == null) {
if (orElse != null) {
value = orElse();
} else {
throw KeyNotFoundException(key);
}
}
return value;
}
@override
ModelDB get modelDB => throw UnimplementedError();
@override
Partition newPartition(String namespace) => Partition(namespace);
@override
FakeQuery<T> query<T extends Model>({Partition partition, Key ancestorKey}) {
final List<T> results = values.values.whereType<T>().toList();
return FakeQuery<T>._(this, results);
}
@override
Future<T> withTransaction<T>(TransactionHandler<T> transactionHandler) {
final FakeTransaction transaction = FakeTransaction._(this);
return transactionHandler(transaction);
}
}
/// A query that will return all values of type `T` that exist in the
/// [FakeDatastoreDB.values] map.
///
/// This fake query ignores any [filter] or [order] directives, though it does
/// respect [limit] and [offset] directives.
class FakeQuery<T extends Model> implements Query<T> {
FakeQuery._(this.db, this.results);
final FakeDatastoreDB db;
final List<FakeFilterSpec> filters = <FakeFilterSpec>[];
final List<FakeOrderSpec> orders = <FakeOrderSpec>[];
List<T> results;
int start = 0;
int count = 100;
@override
void filter(String filterString, Object comparisonObject) {
filters.add(FakeFilterSpec._(filterString, comparisonObject));
}
@override
void limit(int limit) {
assert(limit != null);
assert(limit >= 1);
count = limit;
}
@override
void offset(int offset) {
assert(offset != null);
assert(offset >= 0);
start = offset;
}
@override
void order(String orderString) {
if (orderString.startsWith('-')) {
orders.add(
FakeOrderSpec._(orderString.substring(1), OrderDirection.Decending));
} else {
orders.add(FakeOrderSpec._(orderString, OrderDirection.Ascending));
}
}
@override
Stream<T> run() {
Iterable<T> resultsView = results;
// This considers only the special case when there exists [branch] or [pr] filter.
for (FakeFilterSpec filter in filters) {
final String filterString = filter.filterString;
final Object value = filter.comparisonObject;
if (filterString.contains('branch =') ||
filterString.contains('head =')) {
resultsView = resultsView
.where((T result) => result.toString().contains(value.toString()));
}
}
resultsView = resultsView.skip(start).take(count);
if (db.onQuery.containsKey(T)) {
resultsView = db.onQuery[T](resultsView).cast<T>();
}
return Stream<T>.fromIterable(resultsView);
}
}
class FakeFilterSpec {
const FakeFilterSpec._(this.filterString, this.comparisonObject);
final String filterString;
final Object comparisonObject;
}
class FakeOrderSpec {
const FakeOrderSpec._(this.fieldName, this.direction);
final String fieldName;
final OrderDirection direction;
}
/// A fake datastore transaction.
///
/// This class keeps track of [inserts] and [deletes] and updates the parent
/// [FakeDatastoreDB] when the transaction is committed.
class FakeTransaction implements Transaction {
FakeTransaction._(this.db);
final Map<Key, Model> inserts = <Key, Model>{};
final Set<Key> deletes = <Key>{};
bool sealed = false;
@override
final FakeDatastoreDB db;
@override
Future<dynamic> commit() async {
if (sealed) {
throw StateError('Transaction sealed');
}
if (db.onCommit != null) {
db.onCommit(List<Model>.unmodifiable(inserts.values),
List<Key>.unmodifiable(deletes));
}
for (MapEntry<Key, Model> entry in inserts.entries) {
db.values[entry.key] = entry.value;
}
deletes.forEach(db.values.remove);
sealed = true;
}
@override
Future<List<T>> lookup<T extends Model>(List<Key> keys) async {
final List<T> results = <T>[];
for (Key key in keys) {
if (deletes.contains(key)) {
results.add(null);
} else if (inserts.containsKey(key)) {
results.add(inserts[key] as T);
} else if (db.values.containsKey(key)) {
results.add(db.values[key] as T);
} else {
results.add(null);
}
}
return results;
}
@override
Future<T> lookupValue<T extends Model>(Key key, {T orElse()}) async {
final List<T> values = await lookup(<Key>[key]);
T value = values.single;
if (value == null) {
if (orElse != null) {
value = orElse();
} else {
throw KeyNotFoundException(key);
}
}
return value;
}
@override
Query<T> query<T extends Model>(Key ancestorKey, {Partition partition}) {
final List<T> queryResults = <T>[
...inserts.values.whereType<T>(),
...db.values.values.whereType<T>(),
];
deletes.whereType<T>().forEach(queryResults.remove);
return FakeQuery<T>._(db, queryResults);
}
@override
void queueMutations({List<Model> inserts, List<Key> deletes}) {
if (sealed) {
throw StateError('Transaction sealed');
}
if (inserts != null) {
final math.Random random = math.Random();
for (Model insert in inserts) {
Key key = insert.key;
if (key.id == null) {
key = Key(
key.parent, key.type, random.nextInt(math.pow(2, 20).toInt()));
}
this.inserts[key] = insert;
}
}
if (deletes != null) {
this.deletes.addAll(deletes);
}
}
@override
Future<dynamic> rollback() async {
if (sealed) {
throw StateError('Transaction sealed');
}
inserts.clear();
deletes.clear();
sealed = true;
}
}