blob: f749dd42b0d23380db3e7b5cb208415fb309977c [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, DatastoreError;
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<dynamic>> = 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<dynamic>> inserts, List<Key<dynamic>> 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<dynamic>, Model<dynamic>>? values,
Map<Type, QueryCallback<Model<dynamic>>>? onQuery,
this.onCommit,
this.commitException = false,
}) : values = values ?? <Key<dynamic>, Model<dynamic>>{},
onQuery = onQuery ?? <Type, QueryCallback<Model<dynamic>>>{};
final Map<Key<dynamic>, Model<dynamic>> values;
final Map<Type, QueryCallback<Model<dynamic>>> onQuery;
CommitCallback? onCommit;
// Flag used in tests whether the transaction commit throws exception.
bool? commitException;
/// 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<dynamic>>(QueryCallback<T> callback) {
onQuery[T] = (Iterable<Model<dynamic>> results) {
return callback(results.cast<T>()).cast<Model<dynamic>>();
};
}
@override
Future<dynamic> commit({List<Model<dynamic>>? inserts, List<Key<dynamic>>? deletes}) async {
inserts ??= <Model<dynamic>>[];
deletes ??= <Key<dynamic>>[];
if (onCommit != null) {
onCommit!(List<Model<dynamic>>.unmodifiable(inserts), List<Key<dynamic>>.unmodifiable(deletes));
}
deletes.forEach(values.remove);
for (Model<dynamic> model in inserts) {
values[model.key] = model;
}
}
@override
Datastore get datastore => throw UnimplementedError();
@override
Partition get defaultPartition => Partition(null);
@override
Key<dynamic> get emptyKey => defaultPartition.emptyKey;
@override
Future<List<T?>> lookup<T extends Model<dynamic>>(List<Key<dynamic>> keys) async {
final List<T?> found = <T?>[];
for (Key<dynamic> key in keys) {
for (Model<dynamic> model in values.values) {
if (model.key.id == key.id) {
found.add(model as T?);
}
}
if (found.isEmpty) {
throw KeyNotFoundException(key);
}
}
return found;
}
@override
Future<T> lookupValue<T extends Model<dynamic>>(Key<dynamic> key, {T Function()? orElse}) async {
final List<T?> values = await lookup(<Key<dynamic>>[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<dynamic>>({Partition? partition, Key<dynamic>? ancestorKey}) {
List<T> results = values.values.whereType<T>().toList();
if (ancestorKey != null) {
results = results.where((T entity) => entity.parentKey == ancestorKey).toList();
}
return FakeQuery<T>._(this, results);
}
@override
Future<T> withTransaction<T>(TransactionHandler<T> transactionHandler) {
final FakeTransaction transaction = FakeTransaction._(this);
return transactionHandler(transaction);
}
@override
Future<T> lookupOrNull<T extends Model<dynamic>>(Key<dynamic> key) {
throw UnimplementedError();
}
}
/// A query that will return all values of type `T` that exist in the
/// [FakeDatastoreDB.values] map.
///
/// This fake query respects [order], [limit], and [offset]. However, [filter]
/// may require local additions here to respect new filters.
class FakeQuery<T extends Model<dynamic>> 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) {
// In production, Datastore filters cannot have a space at the end.
assert(filterString.trim() == filterString);
filters.add(FakeFilterSpec._(filterString, comparisonObject));
}
@override
void limit(int limit) {
assert(limit >= 1);
count = limit;
}
@override
void offset(int offset) {
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;
for (FakeFilterSpec filter in filters) {
final String filterString = filter.filterString;
final Object? value = filter.comparisonObject;
if (filterString.contains('branch =') ||
filterString.contains('head =') ||
filterString.contains('pr =') ||
filterString.contains('repository =') ||
filterString.contains('name =')) {
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;
@override
String toString() => 'FakeFilterSpec($filterString, $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<dynamic>, Model<dynamic>> inserts = <Key<dynamic>, Model<dynamic>>{};
final Set<Key<dynamic>> deletes = <Key<dynamic>>{};
bool sealed = false;
@override
final FakeDatastoreDB db;
@override
Future<dynamic> commit() async {
if (db.commitException!) {
throw DatastoreError();
}
if (sealed) {
throw StateError('Transaction sealed');
}
if (db.onCommit != null) {
db.onCommit!(List<Model<dynamic>>.unmodifiable(inserts.values), List<Key<dynamic>>.unmodifiable(deletes));
}
for (MapEntry<Key<dynamic>, Model<dynamic>> 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<dynamic>>(List<Key<dynamic>> keys) async {
final List<T> results = <T>[];
for (Key<dynamic> 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<dynamic>>(Key<dynamic> key, {T Function()? orElse}) async {
final List<T?> values = await lookup(<Key<dynamic>>[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<dynamic>>(Key<dynamic> 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<dynamic>>? inserts, List<Key<dynamic>>? deletes}) {
if (sealed) {
throw StateError('Transaction sealed');
}
if (inserts != null) {
final math.Random random = math.Random();
for (Model<dynamic> insert in inserts) {
Key<dynamic> key = insert.key;
if (key.id == null) {
key = Key<dynamic>(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;
}
@override
Future<T> lookupOrNull<T extends Model<dynamic>>(Key<dynamic> key) {
throw UnimplementedError();
}
}