kopia lustrzana https://gitlab.com/mysocialportal/relatica
Initial PagesManager with tests but not wired in anywhere
rodzic
5c0677b923
commit
0059637551
|
@ -1,5 +1,6 @@
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../models/exec_error.dart';
|
import '../models/exec_error.dart';
|
||||||
import 'paging_data.dart';
|
import 'paging_data.dart';
|
||||||
|
@ -7,11 +8,13 @@ import 'paging_data.dart';
|
||||||
final _logger = Logger('PagedResponse');
|
final _logger = Logger('PagedResponse');
|
||||||
|
|
||||||
class PagedResponse<T> {
|
class PagedResponse<T> {
|
||||||
|
String id;
|
||||||
PagingData? previous;
|
PagingData? previous;
|
||||||
PagingData? next;
|
PagingData? next;
|
||||||
T data;
|
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;
|
bool get hasMorePages => previous != null || next != null;
|
||||||
|
|
||||||
|
@ -76,6 +79,7 @@ class PagedResponse<T> {
|
||||||
func(data),
|
func(data),
|
||||||
previous: previous,
|
previous: previous,
|
||||||
next: next,
|
next: next,
|
||||||
|
id: id,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -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;
|
return pagingData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isLimitOnly =>
|
||||||
|
minId == null && maxId == null && sinceId == null && offset == null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'PagingData{maxId: $maxId, minId: $minId, sinceId: $sinceId, offset: $offset, limit: $limit}';
|
return 'PagingData{maxId: $maxId, minId: $minId, sinceId: $sinceId, offset: $offset, limit: $limit}';
|
||||||
|
@ -60,13 +63,13 @@ class PagingData {
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is PagingData &&
|
other is PagingData &&
|
||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
minId == other.minId &&
|
minId == other.minId &&
|
||||||
maxId == other.maxId &&
|
maxId == other.maxId &&
|
||||||
sinceId == other.sinceId &&
|
sinceId == other.sinceId &&
|
||||||
offset == other.offset &&
|
offset == other.offset &&
|
||||||
limit == other.limit;
|
limit == other.limit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
|
|
|
@ -39,6 +39,7 @@ enum ErrorType {
|
||||||
notFound,
|
notFound,
|
||||||
parsingError,
|
parsingError,
|
||||||
serverError,
|
serverError,
|
||||||
|
rangeError,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExecErrorExtension<T, E> on Result<T, E> {
|
extension ExecErrorExtension<T, E> on Result<T, E> {
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
Ładowanie…
Reference in New Issue