/** * 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); }