Initial PagesManager with tests but not wired in anywhere

codemagic-setup
Hank Grabowski 2023-02-10 11:29:02 +01:00
rodzic 5c0677b923
commit 0059637551
5 zmienionych plików z 272 dodań i 8 usunięć

Wyświetl plik

@ -1,5 +1,6 @@
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import 'package:uuid/uuid.dart';
import '../models/exec_error.dart';
import 'paging_data.dart';
@ -7,11 +8,13 @@ import 'paging_data.dart';
final _logger = Logger('PagedResponse');
class PagedResponse<T> {
String id;
PagingData? previous;
PagingData? next;
T data;
PagedResponse(this.data, {this.previous, this.next});
PagedResponse(this.data, {String? id, this.previous, this.next})
: id = id ?? Uuid().v4();
bool get hasMorePages => previous != null || next != null;
@ -76,6 +79,7 @@ class PagedResponse<T> {
func(data),
previous: previous,
next: next,
id: id,
);
@override

Wyświetl plik

@ -0,0 +1,109 @@
import 'dart:collection';
import 'package:result_monad/result_monad.dart';
import '../models/exec_error.dart';
import 'paged_response.dart';
import 'paging_data.dart';
class PagesManager<TResult, TID> {
final _pages = <PagedResponse<List<TID>>>[];
final List<TID> Function(TResult) idMapper;
final FutureResult<PagedResponse<TResult>, ExecError> Function(PagingData)
onRequest;
PagesManager({
required this.idMapper,
required this.onRequest,
});
UnmodifiableListView<PagedResponse> get pages => UnmodifiableListView(_pages);
void clear() {
_pages.clear();
}
Result<PagedResponse<List<TID>>, ExecError> pageFromId(TID id) {
for (final p in _pages) {
if (p.data.contains(id)) {
return Result.ok(p);
}
}
return buildErrorResult(
type: ErrorType.notFound, message: 'ID $id not in any page');
}
FutureResult<PagedResponse<TResult>, ExecError> initialize(int limit) async {
if (_pages.isNotEmpty) {
return buildErrorResult(
type: ErrorType.rangeError,
message: 'Cannot initialize a loaded manager');
}
final result = await onRequest(PagingData(limit: limit));
if (result.isSuccess) {
final newPage = result.value.map((data) => idMapper(data));
_pages.add(newPage);
}
return result;
}
FutureResult<PagedResponse<TResult>, ExecError> nextWithPage(
PagedResponse<List<TID>> currentPage) async {
return _previousOrNext(currentPage.id, false);
}
FutureResult<PagedResponse<TResult>, ExecError> previousWithPage(
PagedResponse<List<TID>> currentPage) async {
return _previousOrNext(currentPage.id, true);
}
FutureResult<PagedResponse<TResult>, ExecError> nextWithResult(
PagedResponse<TResult> currentPage) async {
return _previousOrNext(currentPage.id, false);
}
FutureResult<PagedResponse<TResult>, ExecError> previousWithResult(
PagedResponse<TResult> currentPage) async {
return _previousOrNext(currentPage.id, true);
}
FutureResult<PagedResponse<TResult>, ExecError> nextFromEnd() async {
return _previousOrNext(_pages.last.id, false);
}
FutureResult<PagedResponse<TResult>, ExecError>
previousFromBeginning() async {
return _previousOrNext(_pages.first.id, true);
}
FutureResult<PagedResponse<TResult>, ExecError> _previousOrNext(
String id, bool asPrevious) async {
final currentIndex = _pages.indexWhere((p) => p.id == id);
if (currentIndex < 0) {
return buildErrorResult(
type: ErrorType.notFound,
message: 'Passed in page is not part of this manager',
);
}
final currentPage = _pages[currentIndex];
final newPagingData = asPrevious ? currentPage.previous : currentPage.next;
if (newPagingData == null) {
return buildErrorResult(
type: ErrorType.rangeError,
message: asPrevious ? 'No previous page' : 'No next page',
);
}
final result = await onRequest(newPagingData);
if (result.isSuccess) {
final newPage = result.value.map((data) => idMapper(data));
if (asPrevious) {
_pages.insert(currentIndex, newPage);
} else {
_pages.insert(currentIndex + 1, newPage);
}
}
return result;
}
}

Wyświetl plik

@ -52,6 +52,9 @@ class PagingData {
return pagingData;
}
bool get isLimitOnly =>
minId == null && maxId == null && sinceId == null && offset == null;
@override
String toString() {
return 'PagingData{maxId: $maxId, minId: $minId, sinceId: $sinceId, offset: $offset, limit: $limit}';
@ -60,13 +63,13 @@ class PagingData {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PagingData &&
runtimeType == other.runtimeType &&
minId == other.minId &&
maxId == other.maxId &&
sinceId == other.sinceId &&
offset == other.offset &&
limit == other.limit;
other is PagingData &&
runtimeType == other.runtimeType &&
minId == other.minId &&
maxId == other.maxId &&
sinceId == other.sinceId &&
offset == other.offset &&
limit == other.limit;
@override
int get hashCode =>

Wyświetl plik

@ -39,6 +39,7 @@ enum ErrorType {
notFound,
parsingError,
serverError,
rangeError,
}
extension ExecErrorExtension<T, E> on Result<T, E> {

Wyświetl plik

@ -0,0 +1,147 @@
import 'dart:collection';
import 'dart:math';
import 'package:flutter_test/flutter_test.dart';
import 'package:relatica/friendica_client/paged_response.dart';
import 'package:relatica/friendica_client/pages_manager.dart';
import 'package:relatica/friendica_client/paging_data.dart';
import 'package:relatica/models/exec_error.dart';
import 'package:result_monad/result_monad.dart';
//Ensure works for ascending and descending tests
void main() async {
test('Full range test', () async {
final pm = _buildPagesManager();
final numbers = <int>[];
final initial = await pm.initialize(10);
initial.value.data.forEach((e) => numbers.add(e.id));
var current = initial.value;
while (current.next != null) {
final result = await pm.nextWithResult(current);
current = result.value;
result.value.data.forEach((e) => numbers.add(e.id));
}
current = initial.value;
while (current.previous != null) {
final result = await pm.previousWithResult(current);
current = result.value;
result.value.data.forEach((e) => numbers.add(e.id));
}
numbers.sort();
final expected = elements.map((e) => e.id).toList();
expected.sort();
expect(numbers.length, equals(elements.length));
expect(numbers, equals(expected));
_checkPagesOrder(pm.pages);
});
test('End fills test', () async {
final pm = _buildPagesManager();
final numbers = <int>[];
final initial = await pm.initialize(10);
initial.value.data.reversed.forEach((e) => numbers.add(e.id));
var moreWork = true;
while (moreWork) {
final nextFromEnd = await pm.nextFromEnd();
final previousFromBeginning = await pm.previousFromBeginning();
nextFromEnd.andThenSuccess(
(r) => r.data.reversed.forEach((e) => numbers.add(e.id)));
previousFromBeginning.andThenSuccess(
(r) => r.data.forEach((e) => numbers.insert(0, e.id)));
moreWork = nextFromEnd.isSuccess || previousFromBeginning.isSuccess;
}
for (var i = 0; i < numbers.length - 1; i++) {
expect(numbers[i], greaterThan(numbers[i + 1]));
}
numbers.sort();
expect(numbers.length, equals(elements.length));
expect(numbers, equals(elements.map((e) => e.id)));
_checkPagesOrder(pm.pages);
});
test('Can find page by index', () async {
final pm = _buildPagesManager();
final initial = await pm.initialize(10);
final next = await pm.nextWithResult(initial.value);
final previous = await pm.previousWithResult(initial.value);
final initialFromQuery = pm.pageFromId(initial.value.data.first.id);
expect(initialFromQuery.value.id, equals(initial.value.id));
final nextFromQuery = pm.pageFromId(next.value.data.first.id);
expect(nextFromQuery.value.id, equals(next.value.id));
final previousFromQuery = pm.pageFromId(previous.value.data.first.id);
expect(previousFromQuery.value.id, equals(previous.value.id));
expect(pm.pageFromId(elements.last.id).isFailure, true);
});
}
void _checkPagesOrder(UnmodifiableListView<PagedResponse> pages) {
expect(pages.first.previous, equals(null));
expect(pages.last.next, equals(null));
for (var i = 1; i < pages.length - 2; i++) {
final p0 = pages[i];
final p1 = pages[i + 1];
expect(p0.previous!.minId, greaterThan(p1.previous!.minId!));
expect(p0.next!.maxId, greaterThan(p1.next!.maxId!));
}
}
class _DataElement {
final int id;
final int value;
_DataElement({required this.id, required this.value});
@override
String toString() {
return '_DataElement{id: $id}';
}
}
const count = 1000;
final elements = List.generate(
count, (index) => _DataElement(id: index, value: Random().nextInt(100)));
PagesManager<List<_DataElement>, int> _buildPagesManager() => PagesManager(
idMapper: (data) => data.map((e) => e.id).toList(),
onRequest: getDataElements);
FutureResult<PagedResponse<List<_DataElement>>, ExecError> getDataElements(
PagingData page) async {
final count = page.limit;
late final int start;
late final int stop;
if (page.isLimitOnly) {
stop = elements.length ~/ 2;
start = stop - count;
} else if (page.minId != null) {
start = page.minId!;
stop = start + count;
} else if (page.maxId != null) {
stop = page.maxId!;
start = stop - count;
} else {
return buildErrorResult(
type: ErrorType.serverError,
message: 'Unknown paging type combo (only min and max supported)',
);
}
int previous = stop;
int next = start;
final data = elements.sublist(max(0, start), min(elements.length, stop));
return Result.ok(
PagedResponse(
data,
previous: previous > elements.length - 1
? null
: PagingData(limit: count, minId: previous),
next: next < 0 ? null : PagingData(limit: count, maxId: next),
),
);
}