diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml
index cf9bf5f..2df3a68 100644
--- a/.github/workflows/auto-merge-dependabot.yml
+++ b/.github/workflows/auto-merge-dependabot.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Fetch Dependabot metadata
id: dependabot-metadata
- uses: dependabot/fetch-metadata@dbb049abf0d677abbd7f7eee0375145b417fdd34
+ uses: dependabot/fetch-metadata@dbb049abf0d677abbd7f7eee0375145b417fdd34 # v2.2.0
- name: Enable auto-merge for minor updates
if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}
env:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3e4d7a4..ef9d7ac 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,9 +19,9 @@ jobs:
timeout-minutes: 1
steps:
- name: Checkout repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup Java
- uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018
+ uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2
with:
distribution: temurin
java-version: 17
@@ -40,15 +40,15 @@ jobs:
security-events: write
steps:
- name: Checkout repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup Java
- uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018
+ uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2
with:
distribution: temurin
java-version: 17
cache: maven
- name: Initialize CodeQL
- uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a
+ uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
with:
languages: java
queries: security-and-quality
@@ -57,4 +57,4 @@ jobs:
- name: Compile project
run: ./mvnw compile
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a
\ No newline at end of file
+ uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 51a54e3..5871ab2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,9 +13,9 @@ jobs:
packages: write
steps:
- name: Checkout repository
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup Java
- uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018
+ uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2
with:
distribution: temurin
java-version: 17
diff --git a/codewars-sdk-client/pom.xml b/codewars-sdk-client/pom.xml
index 096dfbe..c40c057 100644
--- a/codewars-sdk-client/pom.xml
+++ b/codewars-sdk-client/pom.xml
@@ -50,6 +50,11 @@
junit-jupiter
test
+
+ org.mockito
+ mockito-core
+ test
+
diff --git a/codewars-sdk-client/src/main/java/dev/noid/codewars/client/CodewarsClient.java b/codewars-sdk-client/src/main/java/dev/noid/codewars/client/CodewarsClient.java
index 26e0d2c..c06fbf9 100644
--- a/codewars-sdk-client/src/main/java/dev/noid/codewars/client/CodewarsClient.java
+++ b/codewars-sdk-client/src/main/java/dev/noid/codewars/client/CodewarsClient.java
@@ -8,7 +8,7 @@
import dev.noid.codewars.client.model.CompletedChallenges;
import dev.noid.codewars.client.model.User;
import java.util.List;
-import java.util.function.IntFunction;
+import java.util.Map;
public final class CodewarsClient {
@@ -41,9 +41,11 @@ public List listAuthoredChallenges(String idOrUsername) throw
public List listCompletedChallenges(String idOrUsername) throws ApiException {
CompletedChallenges firstPage = usersApi.listCompletedChallenges(idOrUsername, 0);
- List recentChallenges = firstPage.getData();
- IntFunction> pageLoader = page -> usersApi.listCompletedChallenges(idOrUsername, page).getData();
- return new LazyList<>(pageLoader, recentChallenges, recentChallenges.size(), firstPage.getTotalItems());
+ return new PaginatedList<>(
+ page -> usersApi.listCompletedChallenges(idOrUsername, page).getData(),
+ firstPage.getTotalPages(),
+ firstPage.getTotalItems(),
+ Map.of(0, firstPage.getData()));
}
public CodeChallenge getCodeChallenge(String idOrSlag) throws ApiException {
diff --git a/codewars-sdk-client/src/main/java/dev/noid/codewars/client/LazyList.java b/codewars-sdk-client/src/main/java/dev/noid/codewars/client/LazyList.java
deleted file mode 100644
index 64cb425..0000000
--- a/codewars-sdk-client/src/main/java/dev/noid/codewars/client/LazyList.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package dev.noid.codewars.client;
-
-import java.util.AbstractList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.IntFunction;
-
-public class LazyList extends AbstractList {
-
- private final Map loaded = new LinkedHashMap<>();
- private final IntFunction> pageLoader;
- private final int totalItems;
- private final int pageSize;
-
- /**
- * Create a LazyList backed by the given query, using pageSize results per page, and expecting numResults from the
- * query.
- */
- public LazyList(IntFunction> pageLoader, List preload, int pageSize, int totalItems) {
- this.pageLoader = pageLoader;
- this.pageSize = pageSize;
- this.totalItems = totalItems;
- addItems(0, preload);
- }
-
- /**
- * Fetch an item, loading it from the query results if it hasn't already been.
- */
- @Override
- public E get(int i) {
- if (i < 0 || i >= size()) {
- throw new IllegalArgumentException(i + " is not valid index.");
- }
- if (!loaded.containsKey(i)) {
- List page = pageLoader.apply(i / pageSize);
- addItems(i, page);
- }
- return loaded.get(i);
- }
-
- @Override
- public int size() {
- return totalItems;
- }
-
- private void addItems(int offset, List items) {
- for (int i = 0; i < items.size(); i++) {
- loaded.put(offset + i, items.get(i));
- }
- }
-}
\ No newline at end of file
diff --git a/codewars-sdk-client/src/main/java/dev/noid/codewars/client/PaginatedList.java b/codewars-sdk-client/src/main/java/dev/noid/codewars/client/PaginatedList.java
new file mode 100644
index 0000000..5a84d7f
--- /dev/null
+++ b/codewars-sdk-client/src/main/java/dev/noid/codewars/client/PaginatedList.java
@@ -0,0 +1,42 @@
+package dev.noid.codewars.client;
+
+import java.util.AbstractList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.IntFunction;
+
+class PaginatedList extends AbstractList {
+
+ private final IntFunction> pageLoader;
+ private final List[] cache;
+ private final int totalItems;
+ private final int pageSize;
+
+ PaginatedList(IntFunction> pageLoader, int totalPages, int totalItems, Map> preloaded) {
+ this.pageLoader = pageLoader;
+ this.cache = new List[totalPages];
+ preloaded.forEach((pageNumber, page) -> cache[pageNumber] = page);
+ this.totalItems = totalItems;
+ this.pageSize = (int) Math.ceil((double) totalItems / totalPages);
+ }
+
+ @Override
+ public E get(int i) {
+ if (i < 0 || i >= size()) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ int pageNumber = Math.floorDiv(i, pageSize);
+ List page = cache[pageNumber];
+ if (page == null) {
+ page = pageLoader.apply(pageNumber);
+ cache[pageNumber] = page;
+ }
+ return page.get(i % pageSize);
+ }
+
+ @Override
+ public int size() {
+ return totalItems;
+ }
+}
diff --git a/codewars-sdk-client/src/test/java/dev/noid/codewars/client/LazyListTest.java b/codewars-sdk-client/src/test/java/dev/noid/codewars/client/LazyListTest.java
deleted file mode 100644
index b2a0129..0000000
--- a/codewars-sdk-client/src/test/java/dev/noid/codewars/client/LazyListTest.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package dev.noid.codewars.client;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-class LazyListTest {
-
-}
\ No newline at end of file
diff --git a/codewars-sdk-client/src/test/java/dev/noid/codewars/client/PaginatedListTest.java b/codewars-sdk-client/src/test/java/dev/noid/codewars/client/PaginatedListTest.java
new file mode 100644
index 0000000..d09a131
--- /dev/null
+++ b/codewars-sdk-client/src/test/java/dev/noid/codewars/client/PaginatedListTest.java
@@ -0,0 +1,141 @@
+package dev.noid.codewars.client;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class PaginatedListTest {
+
+ private final List> remotePages = Mockito.spy(List.of(
+ List.of("a", "b", "c", "d", "e"),
+ List.of("f", "g", "h", "i", "j"),
+ List.of("k", "l", "m", "n", "o"),
+ List.of("p", "q", "r", "s", "t"),
+ List.of("u", "v", "w", "x", "y"),
+ List.of("z")
+ ));
+
+ @Test
+ void load_all_remote_pages() {
+ assertEquals(List.of(
+ "a", "b", "c", "d", "e",
+ "f", "g", "h", "i", "j",
+ "k", "l", "m", "n", "o",
+ "p", "q", "r", "s", "t",
+ "u", "v", "w", "x", "y",
+ "z"
+ ), getList(6, 26, Map.of()));
+
+ Mockito.verify(remotePages, Mockito.times(remotePages.size())).get(anyInt());
+ for (int page = 0; page < remotePages.size(); page++) {
+ Mockito.verify(remotePages, Mockito.times(1)).get(page);
+ }
+ }
+
+ @Test
+ void preload_first_page() {
+ assertEquals(List.of(
+ "1", "2", "3", "4", "5",
+ "f", "g", "h", "i", "j",
+ "k", "l", "m", "n", "o",
+ "p", "q", "r", "s", "t",
+ "u", "v", "w", "x", "y",
+ "z"
+ ), getList(6, 26, Map.of(0, List.of("1", "2", "3", "4", "5"))));
+
+ Mockito.verify(remotePages, Mockito.times(remotePages.size() - 1)).get(anyInt());
+ for (int page = 1; page < remotePages.size(); page++) {
+ Mockito.verify(remotePages, Mockito.times(1)).get(page);
+ }
+ }
+
+ @Test
+ void preload_last_page() {
+ assertEquals(List.of(
+ "a", "b", "c", "d", "e",
+ "f", "g", "h", "i", "j",
+ "k", "l", "m", "n", "o",
+ "p", "q", "r", "s", "t",
+ "u", "v", "w", "x", "y",
+ "1"
+ ), getList(6, 26, Map.of(5, List.of("1", "2", "3", "4", "5"))));
+
+ Mockito.verify(remotePages, Mockito.times(remotePages.size() - 1)).get(anyInt());
+ for (int page = 0; page < remotePages.size() - 1; page++) {
+ Mockito.verify(remotePages, Mockito.times(1)).get(page);
+ }
+ }
+
+ @Test
+ void load_no_remote_pages() {
+ Map> preloaded = Map.of(
+ 0, List.of("1", "2", "3", "4", "5"),
+ 1, List.of("6", "7", "8", "9", "0"),
+ 2, List.of("~", "!", "@", "#", "$"),
+ 3, List.of("%", "^", "&", "*", "("),
+ 4, List.of(")", "_", "+", "`", "-"),
+ 5, List.of("=")
+ );
+
+ assertEquals(List.of(
+ "1", "2", "3", "4", "5",
+ "6", "7", "8", "9", "0",
+ "~", "!", "@", "#", "$",
+ "%", "^", "&", "*", "(",
+ ")", "_", "+", "`", "-",
+ "="
+ ), getList(6, 26, preloaded));
+
+ Mockito.verify(remotePages, Mockito.times(0)).get(anyInt());
+ }
+
+ @Test
+ void limit_pages_access() {
+ assertEquals(List.of(
+ "a", "b", "c", "d", "e",
+ "f", "g", "h", "i", "j",
+ "k", "l", "m"
+ ), getList(3, 13, Map.of()));
+
+ Mockito.verify(remotePages, Mockito.times(3)).get(anyInt());
+ Mockito.verify(remotePages, Mockito.times(1)).get(0);
+ Mockito.verify(remotePages, Mockito.times(1)).get(1);
+ Mockito.verify(remotePages, Mockito.times(1)).get(2);
+ }
+
+ @Test
+ void read_from_empty() {
+ List list = getList(0, 0, Map.of());
+ assertTrue(list.isEmpty());
+ assertEquals(List.of(), list);
+ }
+
+ @Test
+ void constant_size() {
+ List list = getList(6, 26, Map.of());
+ assertEquals(26, list.size());
+ for (String ignored : list) {
+ assertEquals(26, list.size());
+ }
+ assertEquals(26, list.size());
+ }
+
+ @Test
+ void immutability() {
+ List list = getList(1, 1, Map.of());
+ assertThrows(UnsupportedOperationException.class, () -> list.add(0, "!"));
+ assertThrows(UnsupportedOperationException.class, () -> list.set(0, "!"));
+ assertThrows(UnsupportedOperationException.class, () -> list.remove(0));
+ assertThrows(UnsupportedOperationException.class, () -> list.remove("a"));
+ }
+
+ private List getList(int totalPages, int totalItems, Map> preloaded) {
+ return new PaginatedList<>(remotePages::get, totalPages, totalItems, preloaded);
+ }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 97f43a6..77269c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,7 @@
3.0.2
1.6.14
5.10.3
+ 5.12.0
@@ -63,6 +64,15 @@
org.junit.jupiter
junit-jupiter
${junit.version}
+ pom
+ import
+
+
+ org.mockito
+ mockito-bom
+ ${mockito.version}
+ pom
+ import