Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions api.md

Large diffs are not rendered by default.

71 changes: 52 additions & 19 deletions src/main/java/com/meteroid/AsyncPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,38 @@
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
* One page of a paginated list operation of the async client: its items, and the way to the next
* page.
* page. Each list operation returns its own subclass, which also has the getters of the response
* body.
*
* @param <P> the type of the pages
* @param <T> the type of the items
*/
public final class AsyncPage<T> {
public abstract class AsyncPage<P extends AsyncPage<P, T>, T> {
private final List<T> items;
private final Supplier<CompletableFuture<AsyncPage<T>>> next;

private AsyncPage(List<T> items, Supplier<CompletableFuture<AsyncPage<T>>> next) {
this.items = Collections.unmodifiableList(new ArrayList<>(items));
this.next = next;
}
private final Supplier<CompletableFuture<P>> next;

/**
* A page, built by the SDK.
*
* @param <T> the type of the items
* @param items the items of the page
* @param next fetches the next page, or null on the last page
* @return the page
*/
public static <T> AsyncPage<T> of(
List<T> items, Supplier<CompletableFuture<AsyncPage<T>>> next) {
return new AsyncPage<>(items, next);
protected AsyncPage(List<T> items, Supplier<CompletableFuture<P>> next) {
this.items = Collections.unmodifiableList(new ArrayList<>(items));
this.next = next;
}

/**
* The items of this page.
*
* @return the items, unmodifiable
*/
public List<T> items() {
public final List<T> items() {
return items;
}

Expand All @@ -50,7 +46,7 @@ public List<T> items() {
*
* @return false on the last page
*/
public boolean hasNextPage() {
public final boolean hasNextPage() {
return next != null;
}

Expand All @@ -59,16 +55,53 @@ public boolean hasNextPage() {
*
* @return the next page, failed with {@link NoSuchElementException} on the last page
*/
public CompletableFuture<AsyncPage<T>> nextPage() {
public final CompletableFuture<P> nextPage() {
if (next == null) {
return CompletableFuture.failedFuture(
new NoSuchElementException("this is the last page"));
}
return next.get();
}

@Override
public String toString() {
return "AsyncPage{items=" + items + ", hasNextPage=" + hasNextPage() + "}";
/**
* Calls {@code action} with this page and each one after it, fetching the next page once the
* action returns.
*
* @param action called with each page, in order
* @return completes after the last page, or with the first error
*/
public final CompletableFuture<Void> forEachPage(Consumer<? super P> action) {
P page = self();
action.accept(page);
if (!page.hasNextPage()) {
return CompletableFuture.completedFuture(null);
}
return page.nextPage().thenCompose(next -> next.forEachPage(action));
}

/**
* Calls {@code action} with every item from this page on, fetching the next page once the items
* of one are done.
*
* @param action called with each item, in order
* @return completes after the last item, or with the first error
*/
public final CompletableFuture<Void> forEach(Consumer<? super T> action) {
return forEachPage(page -> page.items().forEach(action));
}

/**
* Collects every item from this page on.
*
* @return the items of this page and the ones after it
*/
public final CompletableFuture<List<T>> toList() {
List<T> all = new ArrayList<>();
return forEach(all::add).thenApply(done -> all);
}

@SuppressWarnings("unchecked")
private P self() {
return (P) this;
}
}
63 changes: 0 additions & 63 deletions src/main/java/com/meteroid/AsyncPaginator.java

This file was deleted.

17 changes: 7 additions & 10 deletions src/main/java/com/meteroid/Meteroid.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@
* }</pre>
*
* <p>Without an API key, the client reads {@value MeteroidOptions#API_KEY_ENV}, and {@value
* MeteroidOptions#BASE_URL_ENV} overrides the base URL, which the API has no default for: one of
* them is required.
* MeteroidOptions#BASE_URL_ENV} overrides the base URL, {@link #DEFAULT_BASE_URL} otherwise.
*/
public final class Meteroid implements AutoCloseable {
private static final Map<String, MeteroidAuth.Scheme> SECURITY_SCHEMES =
Map.ofEntries(Map.entry("bearer_auth", new MeteroidAuth.Scheme("bearer", null, null)));

/** The base URL of the API when neither the options nor the environment set one. */
public static final String DEFAULT_BASE_URL = "https://api.meteroid.com";

private final MeteroidOptions options;
private final MeteroidHttpClient httpClient;
private final WithRawResponse withRawResponse;
Expand Down Expand Up @@ -107,17 +109,12 @@ public Meteroid(String apiKey, MeteroidOptions options) {
* A client with custom options.
*
* @param options the options; the environment supplies the API key and base URL they leave out
* @throws IllegalStateException when neither the options nor the environment set a base URL
*/
public Meteroid(MeteroidOptions options) {
this.options = options;
String baseUrl = options.baseUrl().orElseGet(() -> env(MeteroidOptions.BASE_URL_ENV, null));
if (baseUrl == null) {
throw new IllegalStateException(
"no base URL: set MeteroidOptions.builder().baseUrl(...) or the "
+ MeteroidOptions.BASE_URL_ENV
+ " environment variable");
}
String baseUrl =
options.baseUrl()
.orElseGet(() -> env(MeteroidOptions.BASE_URL_ENV, DEFAULT_BASE_URL));
HttpUrl parsedUrl = HttpUrl.parse(baseUrl);
if (parsedUrl == null) {
throw new IllegalArgumentException("Invalid base URL: " + baseUrl);
Expand Down
96 changes: 79 additions & 17 deletions src/main/java/com/meteroid/Page.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,44 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
* One page of a paginated list operation: its items, and the way to the next page.
* One page of a paginated list operation: its items, and the way to the next page. Each list
* operation returns its own subclass, which also has the getters of the response body.
*
* <p>Iterating a page yields every item from this page on, fetching the next pages as the iteration
* goes; each iteration fetches them again.
*
* @param <P> the type of the pages
* @param <T> the type of the items
*/
public final class Page<T> {
public abstract class Page<P extends Page<P, T>, T> implements Iterable<T> {
private final List<T> items;
private final Supplier<Page<T>> next;

private Page(List<T> items, Supplier<Page<T>> next) {
this.items = Collections.unmodifiableList(new ArrayList<>(items));
this.next = next;
}
private final Supplier<P> next;

/**
* A page, built by the SDK.
*
* @param <T> the type of the items
* @param items the items of the page
* @param next fetches the next page, or null on the last page
* @return the page
*/
public static <T> Page<T> of(List<T> items, Supplier<Page<T>> next) {
return new Page<>(items, next);
protected Page(List<T> items, Supplier<P> next) {
this.items = Collections.unmodifiableList(new ArrayList<>(items));
this.next = next;
}

/**
* The items of this page.
*
* @return the items, unmodifiable
*/
public List<T> items() {
public final List<T> items() {
return items;
}

Expand All @@ -47,7 +49,7 @@ public List<T> items() {
*
* @return false on the last page
*/
public boolean hasNextPage() {
public final boolean hasNextPage() {
return next != null;
}

Expand All @@ -57,15 +59,75 @@ public boolean hasNextPage() {
* @return the next page
* @throws NoSuchElementException on the last page
*/
public Page<T> nextPage() {
public final P nextPage() {
if (next == null) {
throw new NoSuchElementException("this is the last page");
}
return next.get();
}

/**
* This page and the ones after it, fetched as the iteration goes.
*
* @return the pages, from this one
*/
public final Iterable<P> pages() {
return () ->
new Iterator<>() {
private P current;

@Override
public boolean hasNext() {
return current == null || current.hasNextPage();
}

@Override
public P next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
current = current == null ? self() : current.nextPage();
return current;
}
};
}

/**
* The items of this page and the ones after it, as a sequential stream fetching pages on
* demand.
*
* @return every item from this page on
*/
public final Stream<T> stream() {
return StreamSupport.stream(spliterator(), false);
}

@Override
public String toString() {
return "Page{items=" + items + ", hasNextPage=" + hasNextPage() + "}";
public final Iterator<T> iterator() {
Iterator<P> pages = pages().iterator();
return new Iterator<>() {
private Iterator<T> items = Collections.emptyIterator();

@Override
public boolean hasNext() {
while (!items.hasNext() && pages.hasNext()) {
items = pages.next().items().iterator();
}
return items.hasNext();
}

@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return items.next();
}
};
}

@SuppressWarnings("unchecked")
private P self() {
return (P) this;
}
}
Loading
Loading