finnow/finnow-api/source/util/pagination.d

161 lines
4.3 KiB
D

/**
* Defines various components useful for paginated operations.
*/
module util.pagination;
import handy_http_primitives;
import std.conv;
enum SortDir : string {
ASC = "ASC",
DESC = "DESC"
}
struct Sort {
immutable string attribute;
immutable SortDir dir;
static Optional!Sort parse(string expr) {
import std.string;
string[] parts = expr.split(",");
if (parts.length == 1) return Optional!Sort.of(Sort(parts[0], SortDir.ASC));
if (parts.length != 2) return Optional!Sort.empty;
string attr = parts[0];
string dirExpr = parts[1];
SortDir d;
if (dirExpr == SortDir.ASC) {
d = SortDir.ASC;
} else if (dirExpr == SortDir.DESC) {
d = SortDir.DESC;
} else {
return Optional!Sort.empty;
}
return Optional!Sort.of(Sort(attr, d));
}
}
struct PageRequest {
/**
* The requested page number, starting from 1 for the first page, or zero
* for an unpaged request.
*/
immutable uint page;
/**
* The maximum number of items to include in each page of results.
*/
immutable ushort size;
/**
* A list of sorts to apply.
*/
immutable Sort[] sorts;
bool isUnpaged() const {
return page < 1;
}
static PageRequest unpaged() {
return PageRequest(0, 0, []);
}
static PageRequest parse(in ServerHttpRequest request, PageRequest defaults) {
import std.algorithm;
import std.array;
uint pg = request.getParamAs!uint("page", defaults.page);
ushort sz = request.getParamAs!ushort("size", defaults.size);
Sort[] s = request.queryParams
.filter!(p => p.key == "sort" && p.values.length > 0)
.map!(p => Sort.parse(p.values[0]))
.filter!(o => !o.isNull)
.map!(o => o.value)
.array;
if (s.length == 0 && defaults.sorts.length > 0) {
s = defaults.sorts.dup;
}
return PageRequest(pg, sz, s.idup);
}
string toSql() const {
import std.array;
auto app = appender!string;
if (sorts.length > 0) {
app ~= "ORDER BY ";
for (size_t i = 0; i < sorts.length; i++) {
app ~= sorts[i].attribute;
app ~= " ";
app ~= cast(string) sorts[i].dir;
if (i + 1 < sorts.length) app ~= ",";
}
app ~= " ";
}
if (!isUnpaged()) {
app ~= "LIMIT ";
app ~= size.to!string;
app ~= " OFFSET ";
app ~= ((page - 1) * size).to!string;
}
return app[];
}
PageRequest next() const {
if (isUnpaged) return this;
return PageRequest(page + 1, size, sorts);
}
PageRequest prev() const {
if (isUnpaged) return this;
return PageRequest(page - 1, size, sorts);
}
}
/**
* Container for a paginated response, which contains the actual page of items,
* as well as some metadata to assist any API client in navigating to other
* pages.
*/
struct Page(T) {
T[] items;
PageRequest pageRequest;
ulong totalElements;
ulong totalPages;
bool isFirst;
bool isLast;
Page!U mapTo(U)(U function(T) fn) {
import std.algorithm : map;
import std.array : array;
return Page!(U)(items.map!(fn).array, pageRequest, totalElements, totalPages, isFirst, isLast);
}
static Page of(T[] items, PageRequest pageRequest, ulong totalCount) {
ulong pageCount = getTotalPageCount(totalCount, pageRequest.size);
return Page(
items,
pageRequest,
totalCount,
pageCount,
pageRequest.page == 1,
pageRequest.page == pageCount
);
}
}
private ulong getTotalPageCount(ulong totalElements, ulong pageSize) {
return totalElements / pageSize + (totalElements % pageSize > 0 ? 1 : 0);
}
unittest {
assert(getTotalPageCount(5, 1) == 5);
assert(getTotalPageCount(5, 2) == 3);
assert(getTotalPageCount(5, 3) == 2);
assert(getTotalPageCount(5, 4) == 2);
assert(getTotalPageCount(5, 5) == 1);
assert(getTotalPageCount(5, 6) == 1);
assert(getTotalPageCount(5, 123) == 1);
assert(getTotalPageCount(250, 100) == 3);
}