mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 12:23:31 +00:00
Initial PagesManager with tests but not wired in anywhere
This commit is contained in:
parent
5c0677b923
commit
0059637551
5 changed files with 272 additions and 8 deletions
|
@ -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
|
||||
|
|
109
lib/friendica_client/pages_manager.dart
Normal file
109
lib/friendica_client/pages_manager.dart
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 =>
|
||||
|
|
|
@ -39,6 +39,7 @@ enum ErrorType {
|
|||
notFound,
|
||||
parsingError,
|
||||
serverError,
|
||||
rangeError,
|
||||
}
|
||||
|
||||
extension ExecErrorExtension<T, E> on Result<T, E> {
|
||||
|
|
147
test/pages_manager_test.dart
Normal file
147
test/pages_manager_test.dart
Normal file
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue