//// The cursor convention shared by shelf.listEntries and catalog.listEditProposals. import at_record_server/pagination import gleam/bit_array import gleam/int import gleam/list import gleam/option.{None, Some} import gleam/string fn ids(count: Int) -> List(String) { list.repeat(Nil, count) |> list.index_map(fn(_, i) { "item-" <> zero_padded(i + 1) }) } // Zero-padded so string.compare sorts the same as numeric order, matching // how entry ids (TIDs) sort lexically. fn zero_padded(n: Int) -> String { string.pad_start(int.to_string(n), 3, "0") } fn identity(s: String) -> String { s } pub fn decode_cursor_roundtrips_an_encoded_cursor_test() { let token = pagination.encode_cursor("owned", "item-005") assert pagination.decode_cursor("owned", token) == Some("item-005") } pub fn decode_cursor_rejects_malformed_or_mismatched_tokens_test() { let mismatched_namespace = pagination.encode_cursor("owned", "item-005") let no_separator = bit_array.base64_url_encode(bit_array.from_string("no-colon-here"), False) [ #("wanted", mismatched_namespace), #("owned", "!!!not base64!!!"), #("owned", no_separator), ] |> list.each(fn(row) { let #(namespace, token) = row assert pagination.decode_cursor(namespace, token) == None }) } pub fn page_with_no_cursor_and_a_limit_covering_everything_returns_it_all_test() { let items = ids(5) [None, Some(5), Some(1000), Some(0)] |> list.each(fn(limit) { let #(page, next) = pagination.page(items, identity, "owned", None, limit) assert page == items assert next == None }) } pub fn page_past_the_end_of_the_list_is_empty_with_no_next_cursor_test() { let items = ids(3) let cursor = pagination.encode_cursor("owned", "item-003") let #(page, next) = pagination.page(items, identity, "owned", Some(cursor), Some(10)) assert page == [] assert next == None } pub fn page_walks_across_pages_by_cursor_test() { let items = ids(5) [ #(None, ["item-001", "item-002"], Some("item-002")), #(Some("item-002"), ["item-003", "item-004"], Some("item-004")), #(Some("item-004"), ["item-005"], None), ] |> list.each(fn(row) { let #(cursor_id, expected_page, next_id) = row let cursor = option.map(cursor_id, pagination.encode_cursor("owned", _)) let #(page, next) = pagination.page(items, identity, "owned", cursor, Some(2)) assert page == expected_page assert next == option.map(next_id, pagination.encode_cursor("owned", _)) }) } pub fn page_restarts_from_the_top_on_a_stale_or_mismatched_cursor_test() { let items = ids(5) let stale = pagination.encode_cursor("owned", "item-099") let wrong_namespace = pagination.encode_cursor("owned", "item-002") [#("owned", stale), #("owned", "not-a-cursor"), #("wanted", wrong_namespace)] |> list.each(fn(row) { let #(namespace, cursor) = row let #(page, _next) = pagination.page(items, identity, namespace, Some(cursor), Some(2)) assert page == ["item-001", "item-002"] }) } pub fn page_with_only_a_cursor_and_no_limit_uses_the_default_limit_test() { let items = ids(pagination.default_limit + 5) let cursor = pagination.encode_cursor("owned", "item-001") let #(page, next) = pagination.page(items, identity, "owned", Some(cursor), None) assert list.length(page) == pagination.default_limit assert next != None }