diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index e9d1ac5e..8220f95d 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -23,6 +23,9 @@ jobs: java-version: '17' distribution: corretto + - name: Run checkstyle + run: ./gradlew checkstyleMain + - name: Run Gradle test run: ./gradlew test -i diff --git a/.gitignore b/.gitignore index 55a578ae..f6aafae0 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,8 @@ replay_pid* #macOs .DS_STORE + +# Internal files +.fauna-project +rawstats*.csv +stats*.txt diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..8dbf6a62 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "perf-test-setup"] + path = perf-test-setup + url = git@github.com:fauna/driver-perf-utils.git diff --git a/README.md b/README.md index e7ad8a04..ab5486a0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,4 @@ -# The Official JVM Driver for [Fauna](https://fauna.com) (beta) - -> [!CAUTION] -> This driver is currently in beta and should not be used in production. +# Official JVM Driver for [Fauna v10](https://fauna.com) (current) The Fauna JVM driver is a lightweight, open-source wrapper for Fauna's [HTTP API](https://docs.fauna.com/fauna/current/reference/http/reference/). You can @@ -334,7 +331,6 @@ import java.util.concurrent.ExecutionException; import com.fauna.client.Fauna; import com.fauna.client.FaunaClient; -import com.fauna.client.FaunaConfig; import com.fauna.exception.FaunaException; import com.fauna.exception.ServiceException; import com.fauna.query.builder.Query; @@ -345,8 +341,7 @@ import com.fauna.response.QuerySuccess; public class App { public static void main(String[] args) { try { - FaunaConfig config = FaunaConfig.builder().secret("FAUNA_SECRET").build(); - FaunaClient client = Fauna.client(config); + FaunaClient client = Fauna.client(); Query query = fql("'Hello world'"); @@ -430,56 +425,369 @@ QueryOptions options = QueryOptions.builder() QuerySuccess result = client.query(query, String.class, options); ``` -## Event streaming +## Event Feeds (beta) + +The driver supports [Event Feeds](https://docs.fauna.com/fauna/current/learn/cdc/#event-feeds). + +### Request an Event Feed + +An Event Feed asynchronously polls an [event +source](https://docs.fauna.com/fauna/current/learn/cdc/#create-an-event-source) +for paginated events. + +To get an event source, append +[`eventSource()`](https://docs.fauna.com/fauna/current/reference/fql-api/schema-entities/set/eventsource/) +or +[`eventsOn()`](https://docs.fauna.com/fauna/current/reference/fql-api/schema-entities/set/eventson/) +to a [supported Set](https://docs.fauna.com/fauna/current/reference/cdc/#sets). + +To get an event feed, you can use one of the following methods: + +* `feed()`: Synchronously fetches an event feed and returns a `FeedIterator` + that you can use to iterate through the pages of events. + +* `asyncFeed()`: Asynchronously fetches an event feed and returns a + `CompletableFuture` that you can use to iterate through the + pages of events. + +* `poll()`: Asynchronously fetches a single page of events from the event feed + and returns a `CompletableFuture` that you can use to handle each + page individually. You can repeatedly call `poll()` to get successive pages. + +You can use `flatten()` on a `FeedIterator` to iterate through events rather +than pages. + +```java +import com.fauna.client.Fauna; +import com.fauna.client.FaunaClient; +import com.fauna.event.FeedIterator; +import com.fauna.event.EventSource; +import com.fauna.event.FeedOptions; +import com.fauna.event.FeedPage; +import com.fauna.event.EventSource; +import com.fauna.response.QuerySuccess; +import com.fauna.event.FaunaEvent; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; + +import static com.fauna.query.builder.Query.fql; + +// Import the Product class for event data. +import org.example.Product; + +public class EventFeedExample { + private static void printEventDetails(FaunaEvent event) { + System.out.println("Event Details:"); + System.out.println(" Type: " + event.getType()); + System.out.println(" Cursor: " + event.getCursor()); + + event.getTimestamp().ifPresent(ts -> + System.out.println(" Timestamp: " + ts) + ); + + event.getData().ifPresent(product -> + System.out.println(" Product: " + product.toString()) + ); + + if (event.getStats() != null) { + System.out.println(" Stats: " + event.getStats()); + } + + if (event.getError() != null) { + System.out.println(" Error: " + event.getError()); + } + + System.out.println("-------------------"); + } + + public static void main(String[] args) { + FaunaClient client = Fauna.client(); + + long tenMinutesAgo = System.currentTimeMillis() * 1000 - (10 * 60 * 1000 * 1000); + FeedOptions options = FeedOptions.builder() + .startTs(tenMinutesAgo) + .pageSize(10) + .build(); + + // Example 1: Using `feed()` + FeedIterator syncIterator = client.feed( + fql("Product.all().eventsOn(.price, .stock)"), + options, + Product.class + ); + + System.out.println("----------------------"); + System.out.println("`feed()` results:"); + System.out.println("----------------------"); + syncIterator.forEachRemaining(page -> { + for (FaunaEvent event : page.getEvents()) { + printEventDetails(event); + } + }); + + // Example 2: Using `asyncFeed()` + CompletableFuture> iteratorFuture = client.asyncFeed( + fql("Product.all().eventsOn(.price, .stock)"), + options, + Product.class + ); + + FeedIterator iterator = iteratorFuture.join(); + System.out.println("----------------------"); + System.out.println("`asyncFeed()` results:"); + System.out.println("----------------------"); + iterator.forEachRemaining(page -> { + for (FaunaEvent event : page.getEvents()) { + printEventDetails(event); + } + }); + + // Example 3: Using `flatten()` on a `FeedIterator` + FeedIterator flattenedIterator = client.feed( + fql("Product.all().eventSource()"), + options, + Product.class + ); + + Iterator> eventIterator = flattenedIterator.flatten(); + List> allEvents = new ArrayList<>(); + eventIterator.forEachRemaining(allEvents::add); + System.out.println("----------------------"); + System.out.println("`flatten()` results:"); + System.out.println("----------------------"); + for (FaunaEvent event : allEvents) { + printEventDetails(event); + } + + // Example 4: Using `poll()` + QuerySuccess sourceQuery = client.query( + fql("Product.all().eventSource()"), + EventSource.class + ); + EventSource source = EventSource.fromResponse(sourceQuery.getData()); + + CompletableFuture> pageFuture = client.poll( + source, + options, + Product.class + ); + + while (pageFuture != null) { + FeedPage page = pageFuture.join(); + List> events = page.getEvents(); + + System.out.println("----------------------"); + System.out.println("`poll()` results:"); + System.out.println("----------------------"); + for (FaunaEvent event : events) { + printEventDetails(event); + } + + if (page.hasNext()) { + FeedOptions nextPageOptions = options.nextPage(page); + pageFuture = client.poll(source, nextPageOptions, Product.class); + } else { + pageFuture = null; + } + } + } +} +``` + +If you pass an event source directly to `feed()` or `poll()` and changes occur +between the creation of the event source and the Event Feed request, the feed +replays and emits any related events. + +In most cases, you'll get events after a specific start time or cursor. + +### Get events after a specific start time + +When you first poll an event source using an Event Feed, you usually include a +`startTs` (start timestamp) in the `FeedOptions` passed to `feed()`, +`asyncFeed()`, or `poll()`. + +`startTs` is an integer representing a time in microseconds since the Unix +epoch. The request returns events that occurred after the specified timestamp +(exclusive). + +```java +Query query = fql("Product.all().eventsOn(.price, .stock)"); + +// Calculate the timestamp for 10 minutes ago in microseconds. +long tenMinutesAgo = System.currentTimeMillis() * 1000 - (10 * 60 * 1000 * 1000); + +FeedOptions options = FeedOptions.builder() + .startTs(tenMinutesAgo) + .pageSize(10) + .build(); + +// Example 1: Using `feed()` +FeedIterator syncIterator = client.feed( + query, + options, + Product.class +); + +// Example 2: Using `asyncFeed()` +CompletableFuture> iteratorFuture = client.asyncFeed( + query, + options, + Product.class +); + +// Example 3: Using `poll()` +QuerySuccess sourceQuery = client.query( + query, + EventSource.class +); +EventSource source = EventSource.fromResponse(sourceQuery.getData()); + +CompletableFuture> pageFuture = client.poll( + source, + options, + Product.class +); +``` + +### Get events after a specific event cursor + +After the initial request, you usually get subsequent events using the cursor +for the last page or event. To get events after a cursor (exclusive), include +the cursor in the `FeedOptions` passed to passed to `feed()`, +`asyncFeed()`, or `poll()`. + +```java +Query query = fql("Product.all().eventsOn(.price, .stock)"); + +FeedOptions options = FeedOptions.builder() + .cursor("gsGabc456") // Cursor for the last page + .pageSize(10) + .build(); + +// Example 1: Using `feed()` +FeedIterator syncIterator = client.feed( + query, + options, + Product.class +); + +// Example 2: Using `asyncFeed()` +CompletableFuture> iteratorFuture = client.asyncFeed( + query, + options, + Product.class +); + +// Example 3: Using `poll()` +QuerySuccess sourceQuery = client.query( + query, + EventSource.class +); +EventSource source = EventSource.fromResponse(sourceQuery.getData()); + +CompletableFuture> pageFuture = client.poll( + source, + options, + Product.class +); +``` + +### Error handling + +Exceptions can be raised in two different places: + +* While fetching a page +* While iterating a page's events + +This distinction lets ignore errors originating from event processing. For +example: + +```java +try { + FeedIterator syncIterator = client.feed( + fql("Product.all().map(.details.toUpperCase()).eventSource()"), + options, + Product.class + ); + + syncIterator.forEachRemaining(page -> { + try { + for (FaunaEvent event : page.getEvents()) { + // Event-specific handling. + System.out.println("Event: " + event); + } + } catch (FaunaException e) { + // Handle errors for specific events within the page. + System.err.println("Error processing event: " + e.getMessage()); + } + }); + +} catch (FaunaException e) { + // Additional handling for initialization errors. + System.err.println("Error occurred with event feed initialization: " + e.getMessage()); +} +``` + +## Event Streaming -The driver supports [event streaming](https://docs.fauna.com/fauna/current/learn/streaming). +The driver supports [Event +Streaming](https://docs.fauna.com/fauna/current/learn/cdc/#event-streaming). -To get a stream token, append -[`toStream()`](https://docs.fauna.com/fauna/current/reference/reference/schema_entities/set/tostream) +An Event Stream lets you consume events from an [event +source](https://docs.fauna.com/fauna/current/learn/cdc/#create-an-event-source) +as a real-time subscription. + +To get an event source, append +[`eventSource()`](https://docs.fauna.com/fauna/current/reference/reference/schema_entities/set/eventsource) or -[`changesOn()`](https://docs.fauna.com/fauna/current/reference/reference/schema_entities/set/changeson) -to a set from a [supported -source](https://docs.fauna.com/fauna/current/reference/streaming_reference/#supported-sources). +[`eventsOn()`](https://docs.fauna.com/fauna/current/reference/reference/schema_entities/set/eventson) +to a [supported Set](https://docs.fauna.com/fauna/current/reference/cdc/#sets). -To start and subscribe to the stream, use a stream token to create a -`StreamRequest` and pass the `StreamRequest` to `stream()` or `asyncStream()`: +To start and subscribe to the stream, pass an `EventSource` and related +`StreamOptions` to `stream()` or `asyncStream()`: ```java -// Get a stream token. -Query query = fql("Product.all().toStream() { name, stock }"); -QuerySuccess tokenResponse = client.query(query, StreamTokenResponse.class); -String streamToken = tokenResponse.getData().getToken(); - -// Create a StreamRequest. -StreamRequest request = new StreamRequest(streamToken); - -// Use stream() when you want to ensure the stream is ready before proceeding -// with other operations, or when working in a synchronous context. -FaunaStream stream = client.stream(request, Product.class); - -// Use asyncStream() when you want to start the stream operation without blocking, -// which is useful in asynchronous applications or when you need to perform other -// tasks while waiting for the stream to be established. -CompletableFuture> futureStream = client.asyncStream(request, Product.class); +// Get an event source. +Query query = fql("Product.all().eventSource() { name, stock }"); +QuerySuccess tokenResponse = client.query(query, EventSource.class); +EventSource eventSource = EventSource.fromResponse(querySuccess.getData()); + +// Calculate the timestamp for 10 minutes ago in microseconds. +long tenMinutesAgo = System.currentTimeMillis() * 1000 - (10 * 60 * 1000 * 1000); +StreamOptions streamOptions = StreamOptions.builder().startTimestamp(tenMinutesAgo).build(); + +// Example 1: Using `stream()` +FaunaStream stream = client.stream(eventSource, streamOptions, Product.class); + +// Example 2: Using `asyncStream()` +CompletableFuture> futureStream = client.asyncStream(source, streamOptions, Product.class); ``` -Alternatively, you can pass an FQL query that returns a stream token to `stream()` or +If changes occur between the creation of the event source and the stream +request, the stream replays and emits any related events. + +Alternatively, you can pass an FQL query that returns an event source to `stream()` or `asyncStream()`: ```java -Query query = fql("Product.all().toStream() { name, stock }"); -// Create and subscribe to a stream in one step. -// stream() example: +Query query = fql("Product.all().eventSource() { name, stock }"); + +// Example 1: Using `stream()` FaunaStream stream = client.stream(query, Product.class); -// asyncStream() example: + +// Example 2: Using `asyncStream()` CompletableFuture> futureStream = client.asyncStream(query, Product.class); ``` ### Create a subscriber class -The methods return a `FaunaStream` publisher that lets you handle events as they -arrive. Create a class with the `Flow.Subscriber` interface to process -events: +The methods return a +[`FaunaStream`](https://fauna.github.io/fauna-jvm/latest/com/fauna/client/FaunaStream.html) +publisher that lets you handle events as they arrive. Create a class with the +`Flow.Subscriber` interface to process events: ```java package org.example; @@ -490,21 +798,26 @@ import java.util.concurrent.atomic.AtomicInteger; import com.fauna.client.Fauna; import com.fauna.client.FaunaClient; -import com.fauna.client.FaunaStream; +import com.fauna.client.FaunaConfig; +import com.fauna.event.FaunaEvent; +import com.fauna.event.FaunaStream; import com.fauna.exception.FaunaException; + import static com.fauna.query.builder.Query.fql; -import com.fauna.response.StreamEvent; // Import the Product class for event data. import org.example.Product; -public class App { +public class EventStreamExample { public static void main(String[] args) throws InterruptedException { try { - FaunaClient client = Fauna.client(); + FaunaConfig config = FaunaConfig.builder() + .secret("FAUNA_SECRET") + .build(); + FaunaClient client = Fauna.client(config); // Create a stream of all products. Project the name and stock. - FaunaStream stream = client.stream(fql("Product.all().toStream() { name, stock }"), Product.class); + FaunaStream stream = client.stream(fql("Product.all().eventSource() { name, stock }"), Product.class); // Create a subscriber to handle stream events. ProductSubscriber subscriber = new ProductSubscriber(); @@ -520,7 +833,7 @@ public class App { } } - static class ProductSubscriber implements Flow.Subscriber> { + static class ProductSubscriber implements Flow.Subscriber> { private final AtomicInteger eventCount = new AtomicInteger(0); private Flow.Subscription subscription; private final int maxEvents; @@ -538,10 +851,11 @@ public class App { } @Override - public void onNext(StreamEvent event) { + public void onNext(FaunaEvent event) { // Handle each event... int count = eventCount.incrementAndGet(); System.out.println("Received event " + count + ":"); + System.out.println(" Type: " + event.getType()); System.out.println(" Cursor: " + event.getCursor()); System.out.println(" Timestamp: " + event.getTimestamp()); System.out.println(" Data: " + event.getData().orElse(null)); @@ -577,3 +891,28 @@ public class App { } } ``` + +## Debugging / Tracing +If you would like to see the requests and responses the client is making and receiving, you can set the environment +variable `FAUNA_DEBUG=1`. Fauna log the request and response (including headers) to `stderr`. You can also pass in your +own log handler. Setting `Level.WARNING` is equivalent to `FAUNA_DEBUG=0`, while `Level.FINE` is equivalent to +`FAUNA_DEBUG=1`. The client will log the request body at `Level.FINEST`. + +```java +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.SimpleFormatter; + +import com.fauna.client.Fauna; +import com.fauna.client.FaunaClient; + +class App { + public static void main(String[] args) { + Handler handler = new ConsoleHandler(); + handler.setLevel(Level.FINEST); + handler.setFormatter(new SimpleFormatter()); + FaunaClient client = Fauna.client(FaunaConfig.builder().logHandler(handler).build()); + } +} +``` diff --git a/concourse/RELEASING.md b/RELEASING.md similarity index 91% rename from concourse/RELEASING.md rename to RELEASING.md index 28519946..5c11c6da 100644 --- a/concourse/RELEASING.md +++ b/RELEASING.md @@ -3,7 +3,7 @@ We publish the JVM driver on maven central. Our pipeline is driven by the repository itself. The runbook below describes the release process. -1. When issuing a latest mainline release, create branch off of `main`. +1. When issuing the latest mainline release, create branch off of `main`. 2. Update [`gradle.properties`](../gradle.properties) to refer to the proper version number, and commit with a message "Release $RELEASE_VERSION". Note this commit SHA. diff --git a/build.gradle b/build.gradle index e9865f0f..37a3acbc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,9 @@ +import com.vanniktech.maven.publish.SonatypeHost + plugins { + id 'checkstyle' id 'java' + // this is a hopefully temporary plugin until maven central can add official gradle support id 'com.vanniktech.maven.publish' version '0.29.0' } @@ -16,7 +20,15 @@ java { withSourcesJar() } test { - useJUnitPlatform() + useJUnitPlatform { + excludeTags "perfTests" + } +} + +tasks.register("perfTests", Test) { + useJUnitPlatform { + includeTags "perfTests" + } } repositories { @@ -33,12 +45,13 @@ dependencies { testImplementation "org.mockito:mockito-core:${mockitoVersion}" testImplementation "org.mockito:mockito-junit-jupiter:${mockitoVersion}" testImplementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" + testImplementation 'org.apache.commons:commons-math3:3.6.1' testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" } mavenPublishing { - publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) signAllPublications() coordinates(project.group, project.name, project.version) @@ -79,18 +92,40 @@ mavenPublishing { // tasks -task printVersion { +tasks.register('printVersion') { doLast { println project.version } } -task writeProps(type: WriteProperties) { +tasks.register('writeProps', WriteProperties) { destinationFile = file("src/main/resources/version.properties") encoding = 'UTF-8' property('version', project.version) property('name', project.name) property('group', project.group) } + +tasks.register('packageJar', Zip) { + into('lib') { + from(tasks.jar) + from(configurations.runtimeClasspath) + } + archiveFileName = 'fauna-jvm-package.zip' +} + +checkstyle { + toolVersion = '10.19.0' // Latest Checkstyle version at the time of writing + ignoreFailures = false +} + +tasks.withType(Checkstyle).configureEach { + reports { + xml.required = false + html.required = true + } +} + +packageJar.dependsOn(compileJava) processResources.dependsOn(writeProps) sourcesJar.dependsOn(writeProps) diff --git a/concourse/pipeline.yml b/concourse/pipeline.yml index 1ad920c0..2c4ac4d4 100644 --- a/concourse/pipeline.yml +++ b/concourse/pipeline.yml @@ -11,6 +11,14 @@ resources: source: url: ((slack-webhook)) + - name: repo.git + type: git + icon: github + source: + uri: git@github.com:fauna/fauna-jvm.git + branch: main + private_key: ((github-ssh-key)) + - name: fauna-jvm-repository type: git icon: github @@ -26,26 +34,85 @@ resources: uri: git@github.com:fauna/fauna-jvm.git branch: gh-pages private_key: ((github-ssh-key)) + - name: testtools-repo + type: git + icon: github + source: + uri: git@github.com:fauna/testtools.git + branch: main + private_key: ((github-ssh-key)) + - name: testtools-image + type: registry-image + icon: docker + source: + repository: devex-dx-drivers-platform-tests + aws_access_key_id: ((prod-images-aws-access-key-id)) + aws_secret_access_key: ((prod-images-aws-secret-key)) + aws_region: us-east-2 + + - name: perf-notify + type: slack-notification + source: + # webhook for #notify-driver-perf channel + url: ((driver-perf-slack-url)) + + - name: dev-tests-trigger + type: time + source: + interval: 6h jobs: - name: set-self serial: true plan: - - get: fauna-jvm-repository - trigger: true - - get: fauna-jvm-repository-docs + - get: repo.git trigger: true - set_pipeline: self - file: fauna-jvm-repository/concourse/pipeline.yml + file: repo.git/concourse/pipeline.yml + + - name: tests-dev + serial: true + public: false + plan: + - get: dev-tests-trigger + trigger: true + + - get: repo.git + - get: testtools-repo + - get: testtools-image + + - in_parallel: + fail_fast: false + steps: + - task: run-perf + file: repo.git/concourse/tasks/perf.yml + params: + FAUNA_ENDPOINT: https://db.fauna-dev.com + FAUNA_SECRET: ((dev-driver-perf-test-key)) + FAUNA_ENVIRONMENT: dev + CONCOURSE_URL: http://concourse.faunadb.net/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME + on_success: + put: perf-notify + params: + text_file: slack-message/perf-stats + - task: aws-lambda-tests + image: testtools-image + file: testtools-repo/fauna-driver-platform-tests/concourse/tasks/java-aws-lambda-tests.yml + input_mapping: + driver-repo: repo.git + params: + FAUNA_JVM: repo.git + FAUNA_SECRET: ((drivers-platform-tests/fauna-secret)) + AWS_LAMBDA_ROLE_ARN: ((drivers-platform-tests/aws-lambda-role-arn)) + AWS_ACCESS_KEY_ID: ((drivers-platform-tests/aws-access-key-id)) + AWS_SECRET_ACCESS_KEY: ((drivers-platform-tests/aws-secret-key)) - name: release serial: true public: false plan: - get: fauna-jvm-repository trigger: true - passed: [set-self] - get: fauna-jvm-repository-docs - passed: [set-self] # - in_parallel: # - task: integration-tests-oracle-jdk11 diff --git a/concourse/scripts/perf.sh b/concourse/scripts/perf.sh new file mode 100755 index 00000000..c52affb4 --- /dev/null +++ b/concourse/scripts/perf.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -eou + +# For differentiating output files +export LOG_UNIQUE=$(date +%s%3N) + +# Install fauna-shell +apt update -qq +apt install -y -qq npm +npm install --silent -g fauna-shell@^2.0.0 + +cd repo.git + +# Run init.sh to setup database schema and initial data +pushd perf-test-setup +./init.sh + +# Build solution and run performance tests +popd +./gradlew clean +./gradlew perfTests -i + +# Test run should output stats.txt, cat it to the slack-message output for display in Slack +echo ":stopwatch: *Perf test results for __* ($FAUNA_ENVIRONMENT)" > ../slack-message/perf-stats +echo '_(non-query time in milliseconds)_' >> ../slack-message/perf-stats +echo '```' >> ../slack-message/perf-stats +cat ./stats_$LOG_UNIQUE.txt >> ../slack-message/perf-stats +echo '```' >> ../slack-message/perf-stats +echo "_<$CONCOURSE_URL|Concourse job>_" >> ../slack-message/perf-stats + +# Run teardown.sh to delete the test collections +pushd perf-test-setup +./teardown.sh diff --git a/concourse/tasks/perf.yml b/concourse/tasks/perf.yml new file mode 100644 index 00000000..97695d4c --- /dev/null +++ b/concourse/tasks/perf.yml @@ -0,0 +1,21 @@ +--- +platform: linux +image_resource: + type: registry-image + source: + repository: eclipse-temurin + tag: 17-noble + +params: + FAUNA_ENDPOINT: + FAUNA_SECRET: + FAUNA_ENVIRONMENT: + +inputs: + - name: repo.git + +outputs: + - name: slack-message + +run: + path: ./repo.git/concourse/scripts/perf.sh diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 00000000..8d634a65 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index c798f227..dd3c887d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=0.2.0-B1 +version=1.0 junitVersion=5.10.0 mockitoVersion=5.6.0 jacksonVersion=2.15.3 diff --git a/perf-test-setup b/perf-test-setup new file mode 160000 index 00000000..bce440cb --- /dev/null +++ b/perf-test-setup @@ -0,0 +1 @@ +Subproject commit bce440cb2ac36ea16422e861885718c6a8d96c08 diff --git a/src/main/java/com/fauna/annotation/FaunaColl.java b/src/main/java/com/fauna/annotation/FaunaColl.java index e6d155b5..38b8b076 100644 --- a/src/main/java/com/fauna/annotation/FaunaColl.java +++ b/src/main/java/com/fauna/annotation/FaunaColl.java @@ -10,4 +10,5 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -public @interface FaunaColl { } +public @interface FaunaColl { +} diff --git a/src/main/java/com/fauna/annotation/FaunaFieldImpl.java b/src/main/java/com/fauna/annotation/FaunaFieldImpl.java index c5324a02..8ca70528 100644 --- a/src/main/java/com/fauna/annotation/FaunaFieldImpl.java +++ b/src/main/java/com/fauna/annotation/FaunaFieldImpl.java @@ -1,5 +1,6 @@ package com.fauna.annotation; +@SuppressWarnings("ClassExplicitlyAnnotation") public class FaunaFieldImpl implements FaunaField { private final FaunaField annotation; @@ -10,7 +11,8 @@ public FaunaFieldImpl(FaunaField annotation) { @Override public String name() { - return (annotation != null && !annotation.name().isEmpty()) ? annotation.name() : null; + return (annotation != null && !annotation.name().isEmpty()) ? + annotation.name() : null; } @Override diff --git a/src/main/java/com/fauna/annotation/FaunaIdImpl.java b/src/main/java/com/fauna/annotation/FaunaIdImpl.java index 69927015..d4ed546b 100644 --- a/src/main/java/com/fauna/annotation/FaunaIdImpl.java +++ b/src/main/java/com/fauna/annotation/FaunaIdImpl.java @@ -1,5 +1,6 @@ package com.fauna.annotation; +@SuppressWarnings("ClassExplicitlyAnnotation") public class FaunaIdImpl implements FaunaId { private final FaunaId annotation; diff --git a/src/main/java/com/fauna/annotation/FaunaIgnore.java b/src/main/java/com/fauna/annotation/FaunaIgnore.java index eb247d9b..732f8c53 100644 --- a/src/main/java/com/fauna/annotation/FaunaIgnore.java +++ b/src/main/java/com/fauna/annotation/FaunaIgnore.java @@ -10,4 +10,5 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -public @interface FaunaIgnore { } +public @interface FaunaIgnore { +} diff --git a/src/main/java/com/fauna/annotation/FaunaObject.java b/src/main/java/com/fauna/annotation/FaunaObject.java index 6b8c25cb..83a46fef 100644 --- a/src/main/java/com/fauna/annotation/FaunaObject.java +++ b/src/main/java/com/fauna/annotation/FaunaObject.java @@ -11,4 +11,5 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Deprecated -public @interface FaunaObject { } +public @interface FaunaObject { +} diff --git a/src/main/java/com/fauna/annotation/FaunaTs.java b/src/main/java/com/fauna/annotation/FaunaTs.java index 8756ef85..0b6d4960 100644 --- a/src/main/java/com/fauna/annotation/FaunaTs.java +++ b/src/main/java/com/fauna/annotation/FaunaTs.java @@ -10,4 +10,5 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -public @interface FaunaTs { } +public @interface FaunaTs { +} diff --git a/src/main/java/com/fauna/client/BaseFaunaClient.java b/src/main/java/com/fauna/client/BaseFaunaClient.java index 172d1dad..b1d74bce 100644 --- a/src/main/java/com/fauna/client/BaseFaunaClient.java +++ b/src/main/java/com/fauna/client/BaseFaunaClient.java @@ -12,6 +12,7 @@ public final class BaseFaunaClient extends FaunaClient { private final HttpClient httpClient; private final RequestBuilder baseRequestBuilder; private final RequestBuilder streamRequestBuilder; + private final RequestBuilder feedRequestBuilder; private final RetryStrategy retryStrategy; /** @@ -23,17 +24,24 @@ public final class BaseFaunaClient extends FaunaClient { * @param httpClient A Java HTTP client instance. * @param retryStrategy An implementation of RetryStrategy. */ - public BaseFaunaClient(FaunaConfig faunaConfig, - HttpClient httpClient, RetryStrategy retryStrategy) { - super(faunaConfig.getSecret()); + public BaseFaunaClient(final FaunaConfig faunaConfig, + final HttpClient httpClient, final RetryStrategy retryStrategy) { + super(faunaConfig.getSecret(), faunaConfig.getLogHandler(), + faunaConfig.getStatsCollector()); this.httpClient = httpClient; if (Objects.isNull(faunaConfig)) { throw new IllegalArgumentException("FaunaConfig cannot be null."); } else if (Objects.isNull(httpClient)) { throw new IllegalArgumentException("HttpClient cannot be null."); } else { - this.baseRequestBuilder = RequestBuilder.queryRequestBuilder(faunaConfig); - this.streamRequestBuilder = RequestBuilder.streamRequestBuilder(faunaConfig); + this.baseRequestBuilder = + RequestBuilder.queryRequestBuilder(faunaConfig, + getLogger()); + this.streamRequestBuilder = + RequestBuilder.streamRequestBuilder(faunaConfig, + getLogger()); + this.feedRequestBuilder = + RequestBuilder.feedRequestBuilder(faunaConfig, getLogger()); } this.retryStrategy = retryStrategy; } @@ -44,8 +52,9 @@ public BaseFaunaClient(FaunaConfig faunaConfig, * * @param faunaConfig The Fauna configuration settings. */ - public BaseFaunaClient(FaunaConfig faunaConfig) { - this(faunaConfig, HttpClient.newBuilder().build(), DEFAULT_RETRY_STRATEGY); + public BaseFaunaClient(final FaunaConfig faunaConfig) { + this(faunaConfig, HttpClient.newBuilder().build(), + DEFAULT_RETRY_STRATEGY); } @@ -57,6 +66,10 @@ RequestBuilder getStreamRequestBuilder() { return this.streamRequestBuilder; } + RequestBuilder getFeedRequestBuilder() { + return this.feedRequestBuilder; + } + HttpClient getHttpClient() { return this.httpClient; } diff --git a/src/main/java/com/fauna/client/ExponentialBackoffStrategy.java b/src/main/java/com/fauna/client/ExponentialBackoffStrategy.java index 98fa9fe0..81280003 100644 --- a/src/main/java/com/fauna/client/ExponentialBackoffStrategy.java +++ b/src/main/java/com/fauna/client/ExponentialBackoffStrategy.java @@ -1,7 +1,10 @@ package com.fauna.client; - -public class ExponentialBackoffStrategy implements RetryStrategy { +/** + * Implements an exponential backoff strategy for retries. + * The backoff delay increases exponentially with each retry attempt, with optional jitter. + */ +public final class ExponentialBackoffStrategy implements RetryStrategy { private final float backoffFactor; private final int maxAttempts; private final int initialIntervalMillis; @@ -9,27 +12,23 @@ public class ExponentialBackoffStrategy implements RetryStrategy { private final float jitterFactor; /** - * Construct an Exponential backoff strategy. - * The basic formula for exponential backoff is b^(a-1) where b is the backoff factor, and a is the retry - * attempt number. So for a backoff factor of 2, you get: - * 2^0=1, 2^1=2, 2^3=4, 2^4=8 ... + * Constructs an Exponential backoff strategy. * - * @param maxAttempts The maximum amount of retry attempts. Defaults to 3 retry attempts which means - * the client will make a total of 4 requests before giving up. - * @param backoffFactor Defines how quickly the client will back off, default is 2. - * A value of 1 would not backoff (not recommended). - * @param initialIntervalMillis Defines the interval for the first wait. Default is 1000ms. - * @param maxBackoffMillis Set a cap on the delay between requests. The default is 20,000ms - * @param jitterFactor A value between 0 (0%) and 1 (100%) that controls how much to jitter the delay. - * The default is 0.5. + * @param maxAttempts The maximum number of retry attempts. Defaults to 3 retries. + * @param backoffFactor The factor by which the delay will increase. Default is 2. + * @param initialIntervalMillis The interval (in milliseconds) for the first retry attempt. Default is 1000ms. + * @param maxBackoffMillis The maximum delay (in milliseconds) between retries. Default is 20000ms. + * @param jitterFactor A value between 0 and 1 that controls the jitter factor. Default is 0.5. */ - ExponentialBackoffStrategy(int maxAttempts, float backoffFactor, int initialIntervalMillis, - int maxBackoffMillis, float jitterFactor) { + ExponentialBackoffStrategy(final int maxAttempts, final float backoffFactor, + final int initialIntervalMillis, + final int maxBackoffMillis, final float jitterFactor) { this.maxAttempts = maxAttempts; this.backoffFactor = backoffFactor; this.initialIntervalMillis = initialIntervalMillis; this.maxBackoffMillis = maxBackoffMillis; this.jitterFactor = jitterFactor; + if (jitterFactor < 0.0 || jitterFactor > 1.0) { throw new IllegalArgumentException("Jitter factor must be between 0 and 1."); } @@ -48,15 +47,16 @@ public class ExponentialBackoffStrategy implements RetryStrategy { } /** - * Get the % to jitter the backoff, will be a value between 0 and jitterFactor. - * @return + * Generates a random jitter percent between 0 and the jitterFactor. + * + * @return A random jitter percent. */ private double getJitterPercent() { return Math.random() * jitterFactor; } @Override - public boolean canRetry(int retryAttempt) { + public boolean canRetry(final int retryAttempt) { if (retryAttempt < 0) { throw new IllegalArgumentException("Retry attempt must be a natural number (not negative)."); } @@ -64,14 +64,14 @@ public boolean canRetry(int retryAttempt) { } @Override - public int getDelayMillis(int retryAttempt) { + public int getDelayMillis(final int retryAttempt) { if (retryAttempt < 0) { throw new IllegalArgumentException("Retry attempt must be a natural number (not negative)."); } else if (retryAttempt == 0) { return 0; } else { double deterministicBackoff = Math.pow(this.backoffFactor, retryAttempt - 1); - double calculatedBackoff = deterministicBackoff * (1-getJitterPercent()) * initialIntervalMillis; + double calculatedBackoff = deterministicBackoff * (1 - getJitterPercent()) * initialIntervalMillis; return (int) Math.min(calculatedBackoff, this.maxBackoffMillis); } } @@ -81,33 +81,90 @@ public int getMaxRetryAttempts() { return this.maxAttempts; } - /** - * Build a new ExponentialBackoffStrategy. This builder only supports setting maxAttempts, because that's the only - * variable that we recommend users change in production. If you need to modify other values for debugging, or other - * purposes, then you can use the constructor directly. + * Builder class for the ExponentialBackoffStrategy. + * Allows fluent configuration of the backoff strategy parameters. */ public static class Builder { - private float backoffFactor = 2.0f; // Results in delay of 1, 2, 4, 8, 16... seconds. - private int maxAttempts = 3; // Limits number of retry attempts. + private float backoffFactor = 2.0f; + private int maxAttempts = 3; private int initialIntervalMillis = 1000; private int maxBackoffMillis = 20_000; - // A jitterFactor of 0.5, combined with a backoffFactor of 2 ensures that the delay is always increasing. private float jitterFactor = 0.5f; - - public Builder setMaxAttempts(int maxAttempts) { + /** + * Sets the maximum number of retry attempts. + * + * @param maxAttempts The maximum number of retry attempts. + * @return The current Builder instance. + */ + public Builder maxAttempts(final int maxAttempts) { this.maxAttempts = maxAttempts; return this; } + /** + * Sets the backoff factor. + * + * @param backoffFactor The factor by which the backoff delay increases. + * @return The current Builder instance. + */ + public Builder backoffFactor(final float backoffFactor) { + this.backoffFactor = backoffFactor; + return this; + } + + /** + * Sets the initial interval (in milliseconds) for the first retry attempt. + * + * @param initialIntervalMillis The initial interval in milliseconds. + * @return The current Builder instance. + */ + public Builder initialIntervalMillis(final int initialIntervalMillis) { + this.initialIntervalMillis = initialIntervalMillis; + return this; + } + + /** + * Sets the maximum backoff (in milliseconds) between retries. + * + * @param maxBackoffMillis The maximum backoff in milliseconds. + * @return The current Builder instance. + */ + public Builder maxBackoffMillis(final int maxBackoffMillis) { + this.maxBackoffMillis = maxBackoffMillis; + return this; + } + + /** + * Sets the jitter factor (between 0 and 1) to control how much to jitter the backoff delay. + * + * @param jitterFactor The jitter factor. + * @return The current Builder instance. + */ + public Builder jitterFactor(final float jitterFactor) { + this.jitterFactor = jitterFactor; + return this; + } + + /** + * Builds and returns a new ExponentialBackoffStrategy instance. + * + * @return A new ExponentialBackoffStrategy. + */ public ExponentialBackoffStrategy build() { return new ExponentialBackoffStrategy( - this.maxAttempts, this.backoffFactor, this.initialIntervalMillis, + this.maxAttempts, this.backoffFactor, + this.initialIntervalMillis, this.maxBackoffMillis, this.jitterFactor); } } + /** + * Creates a new Builder instance for ExponentialBackoffStrategy. + * + * @return A new Builder instance. + */ public static Builder builder() { return new Builder(); } diff --git a/src/main/java/com/fauna/client/Fauna.java b/src/main/java/com/fauna/client/Fauna.java index c21df743..2e8de5d4 100644 --- a/src/main/java/com/fauna/client/Fauna.java +++ b/src/main/java/com/fauna/client/Fauna.java @@ -2,11 +2,15 @@ import java.net.http.HttpClient; -public class Fauna { +public final class Fauna { + + private Fauna() { + } /** * Create a default Fauna client. - * @return A FaunaClient (or subclass of it). + * + * @return A FaunaClient (or subclass of it). */ public static FaunaClient client() { return new BaseFaunaClient(FaunaConfig.builder().build()); @@ -14,10 +18,11 @@ public static FaunaClient client() { /** * Create a Fauna client with the given FaunaConfig (and default HTTP client, and RetryStrategy). - * @param config Fauna configuration object. - * @return A FaunaClient (or subclass of it). + * + * @param config Fauna configuration object. + * @return A FaunaClient (or subclass of it). */ - public static FaunaClient client(FaunaConfig config) { + public static FaunaClient client(final FaunaConfig config) { if (config == null) { throw new IllegalArgumentException("FaunaConfig cannot be null."); } @@ -26,12 +31,14 @@ public static FaunaClient client(FaunaConfig config) { /** * Create a Fauna client with the given FaunaConfig, HTTP client, and RetryStrategy. + * * @param config Fauna configuration object. - * @param httpClient A HTTP client (from java.net.http in Java 11+). + * @param httpClient An HTTP client (from java.net.http in Java 11+). * @param retryStrategy An implementation of RetryStrategy. - * @return A FaunaClient (or subclass of it). + * @return A FaunaClient (or subclass of it). */ - public static FaunaClient client(FaunaConfig config, HttpClient httpClient, RetryStrategy retryStrategy) { + public static FaunaClient client(final FaunaConfig config, final HttpClient httpClient, + final RetryStrategy retryStrategy) { if (config == null) { throw new IllegalArgumentException("FaunaConfig cannot be null."); } @@ -40,43 +47,51 @@ public static FaunaClient client(FaunaConfig config, HttpClient httpClient, Retr /** * Create a new Fauna client that wraps an existing client, but is scoped to a specific database. - * @param client Another Fauna client. - * @param database The name of the database. - * @return A FaunaClient (or subclass of it). + * + * @param client Another Fauna client. + * @param database The name of the database. + * @return A FaunaClient (or subclass of it). */ - public static FaunaClient scoped(FaunaClient client, String database) { + public static FaunaClient scoped(final FaunaClient client, final String database) { if (client == null) { throw new IllegalArgumentException("FaunaClient cannot be null."); } if (database == null || database.isEmpty()) { - throw new IllegalArgumentException("database cannot be null or empty."); + throw new IllegalArgumentException( + "database cannot be null or empty."); } - return new ScopedFaunaClient(client, FaunaScope.builder(database).build()); + return new ScopedFaunaClient(client, + FaunaScope.builder(database).build()); } /** * Create a new Fauna client that wraps an existing client, but is scoped to a specific database. - * @param client Another Fauna client. - * @param database The name of the database. - * @param role A Fauna role (either built-in or user defined). - * @return A FaunaClient (or subclass of it). + * + * @param client Another Fauna client. + * @param database The name of the database. + * @param role A Fauna role (either built-in or user defined). + * @return A FaunaClient (or subclass of it). */ - public static FaunaClient scoped(FaunaClient client, String database, FaunaRole role) { + public static FaunaClient scoped(final FaunaClient client, final String database, + final FaunaRole role) { if (client == null) { throw new IllegalArgumentException("FaunaClient cannot be null."); } if (database == null || database.isEmpty()) { - throw new IllegalArgumentException("database cannot be null or empty."); + throw new IllegalArgumentException( + "database cannot be null or empty."); } if (role == null) { throw new IllegalArgumentException("role cannot be null or empty."); } - return new ScopedFaunaClient(client, FaunaScope.builder(database).withRole(role).build()); + return new ScopedFaunaClient(client, + FaunaScope.builder(database).withRole(role).build()); } /** * Create a Fauna client for local development using the Fauna Docker container. - * @return A FaunaClient (or subclass of it). + * + * @return A FaunaClient (or subclass of it). */ public static FaunaClient local() { return new BaseFaunaClient(FaunaConfig.LOCAL); diff --git a/src/main/java/com/fauna/client/FaunaClient.java b/src/main/java/com/fauna/client/FaunaClient.java index dea1b17f..d1ef878d 100644 --- a/src/main/java/com/fauna/client/FaunaClient.java +++ b/src/main/java/com/fauna/client/FaunaClient.java @@ -4,55 +4,221 @@ import com.fauna.codec.CodecProvider; import com.fauna.codec.DefaultCodecProvider; import com.fauna.codec.DefaultCodecRegistry; +import com.fauna.codec.ParameterizedOf; +import com.fauna.event.EventSource; +import com.fauna.event.FaunaStream; +import com.fauna.event.FeedIterator; +import com.fauna.event.FeedOptions; +import com.fauna.event.FeedPage; +import com.fauna.event.StreamOptions; import com.fauna.exception.ClientException; -import com.fauna.exception.ClientRequestException; import com.fauna.exception.FaunaException; +import com.fauna.exception.ServiceException; +import com.fauna.query.AfterToken; import com.fauna.query.QueryOptions; -import com.fauna.stream.StreamRequest; -import com.fauna.query.StreamTokenResponse; import com.fauna.query.builder.Query; import com.fauna.response.QueryResponse; import com.fauna.response.QuerySuccess; -import com.fauna.codec.ParameterizedOf; +import com.fauna.types.Page; +import java.io.InputStream; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.ByteBuffer; -import java.util.List; +import java.text.MessageFormat; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; +import java.util.logging.Handler; +import java.util.logging.Logger; + +import static com.fauna.client.Logging.headersAsString; +import static com.fauna.codec.Generic.pageOf; +import static com.fauna.constants.ErrorMessages.FEED_SUBSCRIPTION; +import static com.fauna.constants.ErrorMessages.QUERY_EXECUTION; +import static com.fauna.constants.ErrorMessages.QUERY_PAGE; +import static com.fauna.constants.ErrorMessages.STREAM_SUBSCRIPTION; +/** + * A client to interact with the Fauna service, providing asynchronous and synchronous query execution, + * pagination, and streaming features. + */ public abstract class FaunaClient { - public static final RetryStrategy DEFAULT_RETRY_STRATEGY = ExponentialBackoffStrategy.builder().build(); + public static final RetryStrategy DEFAULT_RETRY_STRATEGY = + ExponentialBackoffStrategy.builder().build(); public static final RetryStrategy NO_RETRY_STRATEGY = new NoRetryStrategy(); private final String faunaSecret; - private final CodecProvider codecProvider = new DefaultCodecProvider(new DefaultCodecRegistry()); + private final CodecProvider codecProvider = + new DefaultCodecProvider(new DefaultCodecRegistry()); + private final AtomicLong lastTransactionTs = new AtomicLong(-1); + private final Logger logger; + private final StatsCollector statsCollector; abstract RetryStrategy getRetryStrategy(); + abstract HttpClient getHttpClient(); + abstract RequestBuilder getRequestBuilder(); + abstract RequestBuilder getStreamRequestBuilder(); - public FaunaClient(String secret) { + abstract RequestBuilder getFeedRequestBuilder(); + + /** + * Constructs a FaunaClient with the provided secret and logger. + * + * @param secret The Fauna secret used for authentication. + * @param logger The logger instance. + * @param statsCollector A collector for tracking statistics. + */ + public FaunaClient(final String secret, final Logger logger, + final StatsCollector statsCollector) { + this.faunaSecret = secret; + this.logger = logger; + this.statsCollector = statsCollector; + } + + /** + * Constructs a FaunaClient with the provided secret and log handler. + * + * @param secret The Fauna secret used for authentication. + * @param logHandler The handler to manage log outputs. + * @param statsCollector A collector for tracking statistics. + */ + public FaunaClient(final String secret, final Handler logHandler, + final StatsCollector statsCollector) { this.faunaSecret = secret; + this.logger = Logger.getLogger(this.getClass().getCanonicalName()); + this.logger.addHandler(logHandler); + this.logger.setLevel(logHandler.getLevel()); + this.statsCollector = statsCollector; } + /** + * Retrieves the Fauna secret used for authentication. + * + * @return The Fauna secret. + */ protected String getFaunaSecret() { return this.faunaSecret; } - private static Supplier>> makeAsyncRequest(HttpClient client, HttpRequest request, Codec codec) { - // There are other options for BodyHandlers here like ofInputStream, and ofByteArray. UTF8FaunaParser - // can be initialized with an InputStream, so we could remove the extra string conversion. - return () -> client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(body -> QueryResponse.handleResponse(body, codec)); + /** + * Retrieves the logger used for logging Fauna client activity. + * + * @return The logger instance. + */ + public Logger getLogger() { + return this.logger; + } + + /** + * Retrieves the stats collector instance. + * + * @return The stats collector instance. + */ + public StatsCollector getStatsCollector() { + return this.statsCollector; + } + + /** + * Retrieves the last known transaction timestamp. + * + * @return An Optional containing the last transaction timestamp, if available. + */ + public Optional getLastTransactionTs() { + long ts = lastTransactionTs.get(); + return ts > 0 ? Optional.of(ts) : Optional.empty(); + } + + private static Optional extractServiceException( + final Throwable throwable) { + if (throwable instanceof ServiceException) { + return Optional.of((ServiceException) throwable); + } else if (throwable.getCause() instanceof ServiceException) { + return Optional.of((ServiceException) throwable.getCause()); + } else { + return Optional.empty(); + } + } + + private void updateTs(final QueryResponse resp) { + Long newTs = resp.getLastSeenTxn(); + if (newTs != null) { + this.lastTransactionTs.updateAndGet( + oldTs -> newTs > oldTs ? newTs : oldTs); + } } + private void completeRequest(final QuerySuccess success, + final Throwable throwable) { + if (success != null) { + updateTs(success); + } else if (throwable != null) { + extractServiceException(throwable).ifPresent( + exc -> updateTs(exc.getResponse())); + } + } + + private void completeFeedRequest(final FeedPage success, + final Throwable throwable) { + // Feeds do not update the clients latest transaction timestamp. + if (throwable != null) { + extractServiceException(throwable).ifPresent( + exc -> updateTs(exc.getResponse())); + } + } + + private void logResponse(final HttpResponse response) { + logger.fine(MessageFormat.format( + "Fauna HTTP Response {0} from {1}, headers: {2}", + response.statusCode(), response.uri(), + headersAsString(response.headers()))); + } + + private Supplier>> makeAsyncRequest( + final HttpClient client, final HttpRequest request, final Codec codec) { + return () -> client.sendAsync(request, + HttpResponse.BodyHandlers.ofInputStream()).thenApply( + response -> { + logResponse(response); + return QueryResponse.parseResponse(response, codec, + statsCollector); + }).whenComplete(this::completeRequest); + } + + private Supplier>> makeAsyncFeedRequest( + final HttpClient client, final HttpRequest request, final Codec codec) { + return () -> client.sendAsync(request, + HttpResponse.BodyHandlers.ofInputStream()).thenApply( + response -> { + logResponse(response); + return FeedPage.parseResponse(response, codec, + statsCollector); + }).whenComplete(this::completeFeedRequest); + } + + private R completeAsync(final CompletableFuture future, final String executionMessage) { + try { + return future.get(); + } catch (ExecutionException | InterruptedException exc) { + if (exc.getCause() != null && exc.getCause() instanceof FaunaException) { + throw (FaunaException) exc.getCause(); + } else { + logger.warning( + "Execution|InterruptedException: " + exc.getMessage()); + throw new ClientException(executionMessage, exc); + } + } + } + + //region Asynchronous API + /** * Sends an asynchronous Fauna Query Language (FQL) query to Fauna. *

@@ -64,13 +230,17 @@ private static Supplier>> makeAsyncRequest * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. */ - public CompletableFuture> asyncQuery(Query fql) { + public CompletableFuture> asyncQuery(final Query fql) { if (Objects.isNull(fql)) { - throw new IllegalArgumentException("The provided FQL query is null."); + throw new IllegalArgumentException( + "The provided FQL query is null."); } Codec codec = codecProvider.get(Object.class, null); - return new RetryHandler>(getRetryStrategy()).execute(FaunaClient.makeAsyncRequest( - getHttpClient(), getRequestBuilder().buildRequest(fql, null, codecProvider), codec)); + return new RetryHandler>(getRetryStrategy(), + logger).execute(makeAsyncRequest( + getHttpClient(), + getRequestBuilder().buildRequest(fql, null, codecProvider, + lastTransactionTs.get()), codec)); } /** @@ -85,14 +255,22 @@ public CompletableFuture> asyncQuery(Query fql) { * @param options A (nullable) set of options to pass to the query. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * + * @param The return type of the query. */ - public CompletableFuture> asyncQuery(Query fql, Class resultClass, QueryOptions options) { + public CompletableFuture> asyncQuery(final Query fql, + final Class resultClass, + final QueryOptions options) { if (Objects.isNull(fql)) { - throw new IllegalArgumentException("The provided FQL query is null."); + throw new IllegalArgumentException( + "The provided FQL query is null."); } Codec codec = codecProvider.get(resultClass, null); - return new RetryHandler>(getRetryStrategy()).execute(FaunaClient.makeAsyncRequest( - getHttpClient(), getRequestBuilder().buildRequest(fql, options, codecProvider), codec)); + return new RetryHandler>(getRetryStrategy(), + logger).execute(makeAsyncRequest( + getHttpClient(), + getRequestBuilder().buildRequest(fql, options, codecProvider, + lastTransactionTs.get()), codec)); } /** @@ -107,14 +285,25 @@ public CompletableFuture> asyncQuery(Query fql, Class res * @param options A (nullable) set of options to pass to the query. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * + * @param The inner type for the parameterized wrapper. */ - public CompletableFuture> asyncQuery(Query fql, ParameterizedOf parameterizedType, QueryOptions options) { + public CompletableFuture> asyncQuery(final Query fql, + final ParameterizedOf parameterizedType, + final QueryOptions options) { if (Objects.isNull(fql)) { - throw new IllegalArgumentException("The provided FQL query is null."); + throw new IllegalArgumentException( + "The provided FQL query is null."); } - Codec codec = codecProvider.get((Class) parameterizedType.getRawType(), parameterizedType.getActualTypeArguments()); - return new RetryHandler>(getRetryStrategy()).execute(FaunaClient.makeAsyncRequest( - getHttpClient(), getRequestBuilder().buildRequest(fql, options, codecProvider), codec)); + @SuppressWarnings("unchecked") + Codec codec = + codecProvider.get((Class) parameterizedType.getRawType(), + parameterizedType.getActualTypeArguments()); + return new RetryHandler>(getRetryStrategy(), + logger).execute(makeAsyncRequest( + getHttpClient(), + getRequestBuilder().buildRequest(fql, options, codecProvider, + lastTransactionTs.get()), codec)); } /** @@ -126,10 +315,13 @@ public CompletableFuture> asyncQuery(Query fql, Parameterize * * @param fql The FQL query to be executed. * @param resultClass The expected class of the query result. - * @return QuerySuccess The successful query result. + * @return QuerySuccess A CompletableFuture that completes with the successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * + * @param The return type of the query. */ - public CompletableFuture> asyncQuery(Query fql, Class resultClass) { + public CompletableFuture> asyncQuery(final Query fql, + final Class resultClass) { return asyncQuery(fql, resultClass, null); } @@ -144,18 +336,28 @@ public CompletableFuture> asyncQuery(Query fql, Class res * @param parameterizedType The expected class of the query result. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The inner type for the parameterized wrapper. */ - public CompletableFuture> asyncQuery(Query fql, ParameterizedOf parameterizedType) { + public CompletableFuture> asyncQuery(final Query fql, + final ParameterizedOf parameterizedType) { if (Objects.isNull(fql)) { - throw new IllegalArgumentException("The provided FQL query is null."); + throw new IllegalArgumentException( + "The provided FQL query is null."); } - Codec codec = codecProvider.get((Class) parameterizedType.getRawType(), parameterizedType.getActualTypeArguments()); - return new RetryHandler>(getRetryStrategy()).execute(FaunaClient.makeAsyncRequest( - getHttpClient(), getRequestBuilder().buildRequest(fql, null, codecProvider), codec)); + @SuppressWarnings("unchecked") + Codec codec = + codecProvider.get((Class) parameterizedType.getRawType(), + parameterizedType.getActualTypeArguments()); + return new RetryHandler>(getRetryStrategy(), + logger).execute(makeAsyncRequest( + getHttpClient(), + getRequestBuilder().buildRequest(fql, null, codecProvider, + lastTransactionTs.get()), codec)); } //endregion //region Synchronous API + /** * Sends a Fauna Query Language (FQL) query to Fauna and returns the result. *

@@ -166,16 +368,9 @@ public CompletableFuture> asyncQuery(Query fql, Parameterize * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. */ - public QuerySuccess query(Query fql) throws FaunaException { - try { - return this.asyncQuery(fql, Object.class, null).get(); - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof FaunaException) { - throw (FaunaException) e.getCause(); - } else { - throw new ClientException("Unhandled exception.", e); - } - } + public QuerySuccess query(final Query fql) throws FaunaException { + return completeAsync(asyncQuery(fql, Object.class, null), + "Unable to execute query."); } /** @@ -188,17 +383,12 @@ public QuerySuccess query(Query fql) throws FaunaException { * @param resultClass The expected class of the query result. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The return type of the query. */ - public QuerySuccess query(Query fql, Class resultClass) throws FaunaException { - try { - return this.asyncQuery(fql, resultClass, null).get(); - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof FaunaException) { - throw (FaunaException) e.getCause(); - } else { - throw new ClientException("Unhandled exception.", e); - } - } + public QuerySuccess query(final Query fql, final Class resultClass) + throws FaunaException { + return completeAsync(asyncQuery(fql, resultClass, null), + QUERY_EXECUTION); } /** @@ -211,17 +401,13 @@ public QuerySuccess query(Query fql, Class resultClass) throws FaunaEx * @param parameterizedType The expected class of the query result. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The inner type for the parameterized wrapper. */ - public QuerySuccess query(Query fql, ParameterizedOf parameterizedType) throws FaunaException { - try { - return this.asyncQuery(fql, parameterizedType, null).get(); - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof FaunaException) { - throw (FaunaException) e.getCause(); - } else { - throw new ClientException("Unhandled exception.", e); - } - } + public QuerySuccess query(final Query fql, + final ParameterizedOf parameterizedType) + throws FaunaException { + return completeAsync(asyncQuery(fql, parameterizedType), + QUERY_EXECUTION); } /** @@ -235,17 +421,13 @@ public QuerySuccess query(Query fql, ParameterizedOf parameterizedType * @param options A (nullable) set of options to pass to the query. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The return type of the query. */ - public QuerySuccess query(Query fql, Class resultClass, QueryOptions options) throws FaunaException { - try { - return this.asyncQuery(fql, resultClass, options).get(); - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof FaunaException) { - throw (FaunaException) e.getCause(); - } else { - throw new ClientException("Unhandled exception.", e); - } - } + public QuerySuccess query(final Query fql, final Class resultClass, + final QueryOptions options) + throws FaunaException { + return completeAsync(asyncQuery(fql, resultClass, options), + QUERY_EXECUTION); } /** @@ -259,117 +441,275 @@ public QuerySuccess query(Query fql, Class resultClass, QueryOptions o * @param options A (nullable) set of options to pass to the query. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The inner type for the parameterized wrapper. */ - public QuerySuccess query(Query fql, ParameterizedOf parameterizedType, QueryOptions options) throws FaunaException { - try { - return this.asyncQuery(fql, parameterizedType, options).get(); - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof FaunaException) { - throw (FaunaException) e.getCause(); - } else { - throw new ClientException("Unhandled exception.", e); - } - } + public QuerySuccess query(final Query fql, + final ParameterizedOf parameterizedType, + final QueryOptions options) + throws FaunaException { + return completeAsync(asyncQuery(fql, parameterizedType, options), + QUERY_EXECUTION); + } + //endregion + + //region Query Page API + + /** + * Sends a query to Fauna that retrieves the Page for the given page token. + * + * @param after The page token (result of a previous paginated request). + * @param elementClass The expected class of the query result. + * @param options A (nullable) set of options to pass to the query. + * @param The type of the elements of the page. + * @return A CompletableFuture that returns a QuerySuccess with data of type Page. + * @throws FaunaException If the query does not succeed, an exception will be thrown. + */ + public CompletableFuture>> asyncQueryPage( + final AfterToken after, final Class elementClass, final QueryOptions options) { + return this.asyncQuery(PageIterator.buildPageQuery(after), + pageOf(elementClass), options); + } + + /** + * Sends a query to Fauna that retrieves the Page for the given page token. + * + * @param after The page token (result of a previous paginated request). + * @param elementClass The expected class of the query result. + * @param options A (nullable) set of options to pass to the query. + * @param The type of the elements of the page. + * @return A QuerySuccess with data of type Page. + * @throws FaunaException If the query does not succeed, an exception will be thrown. + */ + public QuerySuccess> queryPage( + final AfterToken after, final Class elementClass, final QueryOptions options) { + return completeAsync(asyncQueryPage(after, elementClass, options), + QUERY_PAGE); } //endregion + //region Paginated API + /** * Send a Fauna Query Language (FQL) query to Fauna and return a paginated result. - * @param fql The FQL query to be executed. - * @param elementClass The expected class of the query result. + * + * @param fql The FQL query to be executed. + * @param elementClass The expected class of the query result. + * @param options A (nullable) set of options to pass to the query. + * @param The type of the elements of the page. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. */ - public PageIterator paginate(Query fql, Class elementClass) { - return new PageIterator<>(this, fql, elementClass, null); + public PageIterator paginate(final Query fql, final Class elementClass, + final QueryOptions options) { + return new PageIterator<>(this, fql, elementClass, options); } /** * Send a Fauna Query Language (FQL) query to Fauna and return a paginated result. - * @param fql The FQL query to be executed. - * @param elementClass The expected class of the query result. - * @param options A (nullable) set of options to pass to the query. + * + * @param fql The FQL query to be executed. + * @return The successful query result. + * @throws FaunaException If the query does not succeed, an exception will be thrown. + */ + public PageIterator paginate(final Query fql) { + return paginate(fql, Object.class, null); + } + + /** + * Send a Fauna Query Language (FQL) query to Fauna and return a paginated result. + * + * @param fql The FQL query to be executed. + * @param options A (nullable) set of options to pass to the query. + * @return The successful query result. + * @throws FaunaException If the query does not succeed, an exception will be thrown. + */ + public PageIterator paginate(final Query fql, final QueryOptions options) { + return paginate(fql, Object.class, options); + } + + /** + * Send a Fauna Query Language (FQL) query to Fauna and return a paginated result. + * + * @param fql The FQL query to be executed. + * @param elementClass The expected class of the query result. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The type for each element in a page. */ - public PageIterator paginate(Query fql, Class elementClass, QueryOptions options) { - return new PageIterator<>(this, fql, elementClass, options); + public PageIterator paginate(final Query fql, final Class elementClass) { + return paginate(fql, elementClass, null); } + //endregion + + + //region Streaming API /** * Send a request to the Fauna stream endpoint, and return a CompletableFuture that completes with the FaunaStream * publisher. * - * @param streamRequest The request object including a stream token, and optionally a cursor, or timestamp. - * @param elementClass The expected class <E> of the stream events. + * @param eventSource The Event Source (e.g. token from `.eventSource()`). + * @param streamOptions The Stream Options (including start timestamp, retry strategy). + * @param elementClass The target type into which event data will be deserialized. * @return CompletableFuture A CompletableFuture of FaunaStream. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The type for data in an event. */ - public CompletableFuture> asyncStream(StreamRequest streamRequest, Class elementClass) { - HttpRequest streamReq = getStreamRequestBuilder().buildStreamRequest(streamRequest); - CompletableFuture> resp = getHttpClient().sendAsync(streamReq, - HttpResponse.BodyHandlers.ofPublisher()).thenCompose(response -> { - CompletableFuture> publisher = new CompletableFuture<>(); - FaunaStream fstream = new FaunaStream(elementClass); + public CompletableFuture> asyncStream( + final EventSource eventSource, + final StreamOptions streamOptions, + final Class elementClass) { + HttpRequest streamReq = + getStreamRequestBuilder().buildStreamRequest(eventSource, + streamOptions); + return getHttpClient().sendAsync(streamReq, + HttpResponse.BodyHandlers.ofPublisher()) + .thenCompose(response -> { + CompletableFuture> publisher = + new CompletableFuture<>(); + FaunaStream fstream = new FaunaStream<>(elementClass, + this.statsCollector); response.body().subscribe(fstream); publisher.complete(fstream); return publisher; }); - return resp; } /** * Send a request to the Fauna stream endpoint to start a stream, and return a FaunaStream publisher. - * @param streamRequest The request object including a stream token, and optionally a cursor, or timestamp. - * @param elementClass The expected class <E> of the stream events. - * @return FaunaStream A publisher, implementing Flow.Publisher<StreamEvent<E>> from the Java Flow API. + * + * @param eventSource The request object including a stream token, and optionally a cursor, or timestamp. + * @param streamOptions The stream options. + * @param elementClass The expected class <E> of the stream events. + * @return FaunaStream A publisher, implementing Flow.Publisher<StreamEvent<E>> from the Java Flow + * API. * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The type for data in an event. */ - public FaunaStream stream(StreamRequest streamRequest, Class elementClass) { - try { - return this.asyncStream(streamRequest, elementClass).get(); - } catch (InterruptedException | ExecutionException e) { - throw new ClientException("Unable to subscribe to stream.", e); - } - + public FaunaStream stream(final EventSource eventSource, + final StreamOptions streamOptions, + final Class elementClass) { + return completeAsync( + asyncStream(eventSource, streamOptions, elementClass), + STREAM_SUBSCRIPTION); } /** * Start a Fauna stream based on an FQL query, and return a CompletableFuture of the resulting FaunaStream * publisher. This method sends two requests, one to the query endpoint to get the stream token, and then another * to the stream endpoint. This method is equivalent to calling the query, then the stream methods on FaunaClient. + *

+ * This method does not take QueryOptions, or StreamOptions as parameters. If you need specify either + * query, or stream options; you can use the asyncQuery/asyncStream methods. * - * @param fql The FQL query to be executed. It must return a stream, e.g. ends in `.toStream()`. - * @param elementClass The expected class <E> of the stream events. - * @return FaunaStream A publisher, implementing Flow.Publisher<StreamEvent<E>> from the Java Flow API. - * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param fql The FQL query to be executed. It must return an event source, e.g. ends in `.eventSource()`. + * @param elementClass The expected class <E> of the stream events. + * @return FaunaStream A publisher, implementing Flow.Publisher<StreamEvent<E>> from the Java Flow + * API. + * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The type for data in an event. */ - public CompletableFuture> asyncStream(Query fql, Class elementClass) { - return this.asyncQuery(fql, StreamTokenResponse.class).thenApply( - queryResponse -> this.stream(StreamRequest.fromTokenResponse(queryResponse.getData()), elementClass)); + public CompletableFuture> asyncStream(final Query fql, + final Class elementClass) { + return this.asyncQuery(fql, EventSource.class) + .thenApply(queryResponse -> + this.stream(queryResponse.getData(), StreamOptions.builder().build(), elementClass)); } /** - * * Start a Fauna stream based on an FQL query. This method sends two requests, one to the query endpoint to get * the stream token, and then another request to the stream endpoint which return the FaunaStream publisher. * *

- * Query = fql("Product.all().toStream()"); - * QuerySuccess<StreamTokenResponse> tokenResp = client.query(fql, StreamTokenResponse.class); - * FaunaStream<Product> faunaStream = client.stream(new StreamRequest(tokenResp.getData.getToken(), Product.class) - * - * @param fql The FQL query to be executed. It must return a stream, e.g. ends in `.toStream()`. - * @param elementClass The expected class <E> of the stream events. - * @return FaunaStream A publisher, implementing Flow.Publisher<StreamEvent<E>> from the Java Flow API. - * @throws FaunaException If the query does not succeed, an exception will be thrown. + * Query = fql("Product.all().eventSource()"); + * QuerySuccess<EventSource> querySuccess = client.query(fql, EventSource.class); + * EventSource source = querySuccess.getData(); + * FaunaStream<Product> faunaStream = client.stream(source, StreamOptions.DEFAULT, Product.class) + * + * @param fql The FQL query to be executed. It must return a stream, e.g. ends in `.toStream()`. + * @param elementClass The expected class <E> of the stream events. + * @return FaunaStream A publisher, implementing Flow.Publisher<StreamEvent<E>> from the Java Flow + * API. + * @throws FaunaException If the query does not succeed, an exception will be thrown. + * @param The type for data in an event. */ - public FaunaStream stream(Query fql, Class elementClass) { - try { - return this.asyncStream(fql, elementClass).get(); - } catch (InterruptedException | ExecutionException e) { - throw new ClientException("Unable to subscribe to stream.", e); - } + public FaunaStream stream(final Query fql, final Class elementClass) { + return completeAsync(asyncStream(fql, elementClass), + STREAM_SUBSCRIPTION); + } + //endregion + //region Event Feeds + + /** + * Send a request to the Fauna feed endpoint, and return a CompletableFuture that completes with the feed page. + * + * @param eventSource An EventSource object (e.g. token from `.eventSource()`) + * @param feedOptions The FeedOptions object (default options will be used if null). + * @param elementClass The expected class <E> of the feed events. + * @param The type for data in an event. + * @return CompletableFuture A CompletableFuture that completes with a FeedPage<E>. + */ + public CompletableFuture> poll(final EventSource eventSource, + final FeedOptions feedOptions, + final Class elementClass) { + return new RetryHandler>(getRetryStrategy(), + logger).execute(makeAsyncFeedRequest( + getHttpClient(), + getFeedRequestBuilder().buildFeedRequest(eventSource, + feedOptions != null ? feedOptions : FeedOptions.DEFAULT), + codecProvider.get(elementClass))); + } + + /** + * Return a CompletableFuture that completes with a FeedIterator based on an FQL query. This method sends two + * requests, one to the query endpoint to get the event source token, and then another request to the feed endpoint + * to get the first page of results. + * + * @param fql The FQL query to be executed. It must return a token, e.g. ends in `.changesOn()`. + * @param feedOptions The FeedOptions object (must not be null). + * @param elementClass The expected class <E> of the feed events. + * @param The type for data in an event. + * @return FeedIterator A CompletableFuture that completes with a feed iterator that returns pages of Feed events. + */ + public CompletableFuture> asyncFeed(final Query fql, + final FeedOptions feedOptions, + final Class elementClass) { + return this.asyncQuery(fql, EventSource.class).thenApply( + success -> this.feed( + success.getData(), + feedOptions, elementClass)); } + + /** + * Return a FeedIterator based on an FQL query. This method sends two requests, one to the query endpoint to get + * the stream/feed token, and then another request to the feed endpoint to get the first page of results. + * + * @param fql The FQL query to be executed. It must return a token, e.g. ends in `.changesOn()`. + * @param feedOptions The Feed Op + * @param elementClass The expected class <E> of the feed events. + * @param The type for data in an event. + * @return FeedIterator An iterator that returns pages of Feed events. + */ + public FeedIterator feed(final Query fql, final FeedOptions feedOptions, final Class elementClass) { + return completeAsync(asyncFeed(fql, feedOptions, elementClass), + FEED_SUBSCRIPTION); + } + + /** + * Send a request to the Feed endpoint and return a FeedIterator. + * + * @param eventSource The Fauna Event Source. + * @param feedOptions The feed options. + * @param elementClass The expected class <E> of the feed events. + * @param The type for data in an event. + * @return FeedIterator An iterator that returns pages of Feed events. + */ + public FeedIterator feed(final EventSource eventSource, + final FeedOptions feedOptions, + final Class elementClass) { + return new FeedIterator<>(this, eventSource, feedOptions, elementClass); + } + + //endregion } diff --git a/src/main/java/com/fauna/client/FaunaConfig.java b/src/main/java/com/fauna/client/FaunaConfig.java index 1525017e..28b15a4a 100644 --- a/src/main/java/com/fauna/client/FaunaConfig.java +++ b/src/main/java/com/fauna/client/FaunaConfig.java @@ -1,39 +1,48 @@ package com.fauna.client; +import java.time.Duration; import java.util.Optional; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import static com.fauna.constants.Defaults.CLIENT_TIMEOUT_BUFFER; +import static com.fauna.constants.Defaults.LOCAL_FAUNA_SECRET; +import static com.fauna.constants.Defaults.MAX_CONTENTION_RETRIES; /** * FaunaConfig is a configuration class used to set up and configure a connection to Fauna. - * It encapsulates various settings such as the endpoint URL, secret key, query timeout, and others. + * It encapsulates various settings such as the endpoint URL, secret key, and more. */ -public class FaunaConfig { +public final class FaunaConfig { public static class FaunaEndpoint { - public static String DEFAULT = "https://db.fauna.com"; - public static String LOCAL = "http://localhost:8443"; + public static final String DEFAULT = "https://db.fauna.com"; + public static final String LOCAL = "http://localhost:8443"; } private final String endpoint; private final String secret; + private final int maxContentionRetries; + private final Duration clientTimeoutBuffer; + private final Handler logHandler; + private final StatsCollector statsCollector; public static final FaunaConfig DEFAULT = FaunaConfig.builder().build(); public static final FaunaConfig LOCAL = FaunaConfig.builder().endpoint( - FaunaEndpoint.LOCAL).secret("secret").build(); - - -// private Integer maxContentionRetries; -// private int maxAttempts; -// private int maxBackoff; - + FaunaEndpoint.LOCAL).secret(LOCAL_FAUNA_SECRET).build(); /** * Private constructor for FaunaConfig. * * @param builder The builder used to create the FaunaConfig instance. */ - private FaunaConfig(Builder builder) { - this.endpoint = builder.endpoint.orElseGet(() -> FaunaEnvironment.faunaEndpoint().orElse(FaunaEndpoint.DEFAULT)); - this.secret = builder.secret.orElseGet(() -> FaunaEnvironment.faunaSecret().orElse("")); + private FaunaConfig(final Builder builder) { + this.endpoint = builder.endpoint != null ? builder.endpoint : FaunaEndpoint.DEFAULT; + this.secret = builder.secret != null ? builder.secret : ""; + this.maxContentionRetries = builder.maxContentionRetries; + this.clientTimeoutBuffer = builder.clientTimeoutBuffer; + this.logHandler = builder.logHandler; + this.statsCollector = builder.statsCollector; } /** @@ -55,6 +64,42 @@ public String getSecret() { return secret; } + /** + * Gets the number of contention retries that the Fauna server will attempt. + * + * @return An integer value. + */ + public int getMaxContentionRetries() { + return maxContentionRetries; + } + + /** + * Gets the buffer that will be added to the HTTP client timeout, in addition to any query timeout. + * + * @return The timeout buffer Duration. + */ + public Duration getClientTimeoutBuffer() { + return clientTimeoutBuffer; + } + + /** + * Gets the log handler that the client will use. + * + * @return A log handler instance. + */ + public Handler getLogHandler() { + return logHandler; + } + + /** + * Gets the stats collector for the client. + * + * @return A StatsCollector instance. + */ + public StatsCollector getStatsCollector() { + return statsCollector; + } + /** * Creates a new builder for FaunaConfig. * @@ -68,8 +113,33 @@ public static Builder builder() { * Builder class for FaunaConfig. Follows the Builder Design Pattern. */ public static class Builder { - private Optional endpoint = Optional.empty(); - private Optional secret = Optional.empty(); + private String endpoint = + FaunaEnvironment.faunaEndpoint().orElse(FaunaEndpoint.DEFAULT); + private String secret = FaunaEnvironment.faunaSecret().orElse(""); + private int maxContentionRetries = MAX_CONTENTION_RETRIES; + private Duration clientTimeoutBuffer = CLIENT_TIMEOUT_BUFFER; + private Handler logHandler = defaultLogHandler(); + private StatsCollector statsCollector = new StatsCollectorImpl(); + + static Level getLogLevel(final String debug) { + if (debug == null || debug.isBlank()) { + return Level.WARNING; + } else { + try { + int debugInt = Integer.parseInt(debug); + return debugInt > 0 ? Level.FINE : Level.WARNING; + } catch (NumberFormatException e) { + return Level.FINE; + } + } + } + + private static Handler defaultLogHandler() { + Handler logHandler = new ConsoleHandler(); + logHandler.setLevel( + getLogLevel(FaunaEnvironment.faunaDebug().orElse(null))); + return logHandler; + } /** * Sets the endpoint URL. @@ -77,8 +147,8 @@ public static class Builder { * @param endpoint A String representing the endpoint URL. * @return The current Builder instance. */ - public Builder endpoint(String endpoint) { - this.endpoint = Optional.ofNullable(endpoint); + public Builder endpoint(final String endpoint) { + this.endpoint = endpoint; return this; } @@ -88,8 +158,52 @@ public Builder endpoint(String endpoint) { * @param secret A String representing the secret key. * @return The current Builder instance. */ - public Builder secret(String secret) { - this.secret = Optional.ofNullable(secret); + public Builder secret(final String secret) { + this.secret = secret; + return this; + } + + /** + * Set the Fauna max-contention-retries setting. + * + * @param maxContentionRetries A positive integer value. + * @return The current Builder instance. + */ + public Builder maxContentionRetries(final int maxContentionRetries) { + this.maxContentionRetries = maxContentionRetries; + return this; + } + + /** + * Set the client timeout buffer. + * + * @param duration The timeout buffer duration. + * @return The current Builder instance. + */ + public Builder clientTimeoutBuffer(final Duration duration) { + this.clientTimeoutBuffer = duration; + return this; + } + + /** + * Override the default log handler with the given log handler. + * + * @param handler A log handler instance. + * @return The current Builder instance. + */ + public Builder logHandler(final Handler handler) { + this.logHandler = handler; + return this; + } + + /** + * Set a StatsCollector. + * + * @param statsCollector A stats collector instance. + * @return The current Builder instance. + */ + public Builder statsCollector(final StatsCollector statsCollector) { + this.statsCollector = statsCollector; return this; } @@ -109,8 +223,9 @@ public FaunaConfig build() { public static class FaunaEnvironment { private static final String FAUNA_SECRET = "FAUNA_SECRET"; private static final String FAUNA_ENDPOINT = "FAUNA_ENDPOINT"; + private static final String FAUNA_DEBUG = "FAUNA_DEBUG"; - private static Optional environmentVariable(String name) { + private static Optional environmentVariable(final String name) { Optional var = Optional.ofNullable(System.getenv(name)); return var.isPresent() && var.get().isBlank() ? Optional.empty() : var; } @@ -128,5 +243,12 @@ public static Optional faunaSecret() { public static Optional faunaEndpoint() { return environmentVariable(FAUNA_ENDPOINT); } + + /** + * @return The (non-empty, non-blank) value of the FAUNA_DEBUG environment variable, or Optional.empty(). + */ + public static Optional faunaDebug() { + return environmentVariable(FAUNA_DEBUG); + } } } diff --git a/src/main/java/com/fauna/client/FaunaRequest.java b/src/main/java/com/fauna/client/FaunaRequest.java deleted file mode 100644 index 6e342d96..00000000 --- a/src/main/java/com/fauna/client/FaunaRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.fauna.client; - -import com.fauna.query.builder.Query; - -import java.io.Serializable; - -/** - * This class represents a Fauna POST request body that can be serialized. - */ -public class FaunaRequest implements Serializable { - Query query; - - public FaunaRequest(Query query) { - this.query = query; - } - - public Query getQuery() { - return this.query; - } -} diff --git a/src/main/java/com/fauna/client/FaunaRole.java b/src/main/java/com/fauna/client/FaunaRole.java index fa81f927..fc17bb56 100644 --- a/src/main/java/com/fauna/client/FaunaRole.java +++ b/src/main/java/com/fauna/client/FaunaRole.java @@ -7,7 +7,7 @@ * Built-in roles defined at: * docs.fauna.com. */ -public class FaunaRole { +public final class FaunaRole { private static final String ADMIN_ROLE_NAME = "admin"; private static final String SERVER_ROLE_NAME = "server"; private static final String SERVER_READ_ONLY_ROLE_NAME = "server-readonly"; @@ -16,7 +16,8 @@ public class FaunaRole { public static final FaunaRole ADMIN = new FaunaRole(ADMIN_ROLE_NAME); public static final FaunaRole SERVER = new FaunaRole(SERVER_ROLE_NAME); - public static final FaunaRole SERVER_READ_ONLY = new FaunaRole(SERVER_READ_ONLY_ROLE_NAME); + public static final FaunaRole SERVER_READ_ONLY = + new FaunaRole(SERVER_READ_ONLY_ROLE_NAME); private static final String ROLE_PREFIX = "@role/"; private static final Character UNDERSCORE = '_'; @@ -28,34 +29,54 @@ public class FaunaRole { * * @param role The role name, either @role/name or one of the built-in role names. */ - FaunaRole(String role) { + FaunaRole(final String role) { this.role = role; } + /** + * @return A {@link String} representation of the {@code FaunaRole}. + */ public String toString() { return this.role; } - public static void validateRoleName(String name) { + /** + * Validates a role name. + * + * @param name The name of the role to validate. + */ + public static void validateRoleName(final String name) { if (name == null || name.isEmpty()) { - throw new IllegalArgumentException("Role name cannot be null or empty."); + throw new IllegalArgumentException( + "Role name cannot be null or empty."); } if (BUILT_IN_ROLE_NAMES.contains(name)) { - String msg = MessageFormat.format("Role name {0} is reserved, but you can use it as a built-in role", name); + String msg = MessageFormat.format( + "Role name {0} is reserved, but you can use it as a built-in role", + name); throw new IllegalArgumentException(msg); } if (!Character.isAlphabetic(name.charAt(0))) { - throw new IllegalArgumentException("First character must be a letter."); + throw new IllegalArgumentException( + "First character must be a letter."); } for (Character c : name.toCharArray()) { - if (!Character.isAlphabetic(c) && !Character.isDigit(c) && !c.equals(UNDERSCORE)) { - throw new IllegalArgumentException("Role names can only contain letters, numbers, and underscores."); + if (!Character.isAlphabetic(c) && !Character.isDigit(c) && + !c.equals(UNDERSCORE)) { + throw new IllegalArgumentException( + "Role names can only contain letters, numbers, and underscores."); } } } - public static FaunaRole named(String name) { + /** + * Creates a {@code FaunaRole} with the desired name prepended with {@code @role/}. + * + * @param name The name of the role to use. + * @return A {@code FaunaRole} instance. + */ + public static FaunaRole named(final String name) { validateRoleName(name); return new FaunaRole(ROLE_PREFIX + name); } diff --git a/src/main/java/com/fauna/client/FaunaScope.java b/src/main/java/com/fauna/client/FaunaScope.java index 7fe75be1..bc332a6d 100644 --- a/src/main/java/com/fauna/client/FaunaScope.java +++ b/src/main/java/com/fauna/client/FaunaScope.java @@ -1,44 +1,80 @@ package com.fauna.client; -import java.util.Optional; - -public class FaunaScope { +/** + * Represents a FaunaScope, a structure that encapsulates a Fauna database and a role within that database. + * The FaunaScope is used to generate a token that is used for authorization. + */ +public final class FaunaScope { private static final String DELIMITER = ":"; private final String database; private final FaunaRole role; - public FaunaScope(String database, FaunaRole role) { + /** + * Creates a FaunaScope with the specified database and role. + * + * @param database the name of the database + * @param role the FaunaRole associated with this scope + */ + public FaunaScope(final String database, final FaunaRole role) { this.database = database; this.role = role; } - public String getToken(String secret) { + /** + * Generates a token for this scope using the provided secret. + * + * @param secret the secret used to generate the token + * @return a token string formed by concatenating secret, database, and role + */ + public String getToken(final String secret) { return String.join(DELIMITER, secret, database, role.toString()); } + /** + * A builder class for creating instances of FaunaScope. + */ public static class Builder { - public final String database; - public Optional role = Optional.empty(); + private final String database; + private FaunaRole role = null; - public Builder(String database) { + /** + * Constructs a Builder for FaunaScope. + * + * @param database the name of the database + */ + public Builder(final String database) { this.database = database; } - public Builder withRole(FaunaRole role) { - this.role = Optional.ofNullable(role); + /** + * Sets the role for the FaunaScope. + * + * @param role the FaunaRole to associate with the scope + * @return the Builder instance for method chaining + */ + public Builder withRole(final FaunaRole role) { + this.role = role; return this; } + /** + * Builds a FaunaScope instance using the current builder settings. + * + * @return a newly created FaunaScope + */ public FaunaScope build() { - return new FaunaScope(this.database, this.role.orElse(FaunaRole.SERVER)); - + return new FaunaScope(this.database, + this.role != null ? this.role : FaunaRole.SERVER); } } - public static Builder builder(String database) { + /** + * Creates a new Builder instance for a FaunaScope. + * + * @param database the name of the database + * @return a new Builder instance + */ + public static Builder builder(final String database) { return new Builder(database); } - - - } diff --git a/src/main/java/com/fauna/client/FaunaStream.java b/src/main/java/com/fauna/client/FaunaStream.java deleted file mode 100644 index 9565d785..00000000 --- a/src/main/java/com/fauna/client/FaunaStream.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.fauna.client; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fauna.codec.Codec; -import com.fauna.codec.DefaultCodecProvider; -import com.fauna.exception.ClientException; -import com.fauna.response.ErrorInfo; -import com.fauna.response.StreamEvent; -import com.fauna.response.wire.MultiByteBufferInputStream; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.text.MessageFormat; -import java.util.List; -import java.util.concurrent.Flow.Processor; -import java.util.concurrent.Flow.Subscriber; -import java.util.concurrent.Flow.Subscription; -import java.util.concurrent.SubmissionPublisher; - - -public class FaunaStream extends SubmissionPublisher> implements Processor, StreamEvent> { - ObjectMapper mapper = new ObjectMapper(); - private final Codec dataCodec; - private Subscription subscription; - private Subscriber> eventSubscriber; - private MultiByteBufferInputStream buffer = null; - - public FaunaStream(Class elementClass) { - this.dataCodec = DefaultCodecProvider.SINGLETON.get(elementClass); - } - - @Override - public void subscribe(Subscriber> subscriber) { - if (this.eventSubscriber == null) { - this.eventSubscriber = subscriber; - super.subscribe(subscriber); - this.subscription.request(1); - } else { - throw new ClientException("Only one subscriber is supported."); - } - - } - - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - } - - @Override - public void onNext(List buffers) { - try { - // Using synchronized is probably not the fastest way to do this. - synchronized (this) { - if (this.buffer == null) { - this.buffer = new MultiByteBufferInputStream(buffers); - } else { - this.buffer.add(buffers); - } - try { - JsonParser parser = mapper.getFactory().createParser(buffer); - StreamEvent event = StreamEvent.parse(parser, dataCodec); - if (event.getType() == StreamEvent.EventType.ERROR) { - ErrorInfo error = event.getError(); - this.onComplete(); - this.close(); - throw new ClientException(MessageFormat.format("Stream stopped due to error {0} {1}", error.getCode(), error.getMessage())); - } - this.submit(event); - this.buffer = null; - } catch (ClientException e) { - // Maybe we got a partial event... - this.buffer.reset(); - } catch (IOException e) { - throw new ClientException("Unable to decode stream", e); - } - } - } catch (Exception e) { - throw new ClientException("Unable to decode stream", e); - } finally { - this.subscription.request(1); - } - } - - @Override - public void onError(Throwable throwable) { - this.subscription.cancel(); - this.close(); - } - - @Override - public void onComplete() { - this.subscription.cancel(); - } -} diff --git a/src/main/java/com/fauna/client/Logging.java b/src/main/java/com/fauna/client/Logging.java new file mode 100644 index 00000000..9db130a3 --- /dev/null +++ b/src/main/java/com/fauna/client/Logging.java @@ -0,0 +1,30 @@ +package com.fauna.client; + +import java.net.http.HttpHeaders; +import java.util.stream.Collectors; + +/** + * A utility class for logging HTTP headers. + */ +public final class Logging { + + private Logging() { + } + + /** + * Converts the given HttpHeaders to a string representation. + * + * @param headers The HttpHeaders to convert. + * @return A string representation of the headers. + */ + public static String headersAsString(final HttpHeaders headers) { + String hdrs = "NONE"; + if (headers != null) { + hdrs = headers.map().entrySet().stream().map( + entry -> entry.getKey() + ": " + String.join( + ",", entry.getValue())) + .collect(Collectors.joining(";")); + } + return hdrs; + } +} diff --git a/src/main/java/com/fauna/client/NoRetryStrategy.java b/src/main/java/com/fauna/client/NoRetryStrategy.java index e32ee621..34a19a90 100644 --- a/src/main/java/com/fauna/client/NoRetryStrategy.java +++ b/src/main/java/com/fauna/client/NoRetryStrategy.java @@ -1,15 +1,17 @@ package com.fauna.client; -public class NoRetryStrategy implements RetryStrategy { - +/** + * Specifies that no retries will be made. + */ +public final class NoRetryStrategy implements RetryStrategy { @Override - public boolean canRetry(int retryAttempt) { + public boolean canRetry(final int retryAttempt) { return false; } @Override - public int getDelayMillis(int retryAttempt) { + public int getDelayMillis(final int retryAttempt) { return 0; } diff --git a/src/main/java/com/fauna/client/PageIterator.java b/src/main/java/com/fauna/client/PageIterator.java index 2f4c2671..6cb8bf94 100644 --- a/src/main/java/com/fauna/client/PageIterator.java +++ b/src/main/java/com/fauna/client/PageIterator.java @@ -1,11 +1,11 @@ package com.fauna.client; - +import com.fauna.codec.PageOf; import com.fauna.exception.FaunaException; +import com.fauna.query.AfterToken; import com.fauna.query.QueryOptions; import com.fauna.query.builder.Query; import com.fauna.response.QuerySuccess; -import com.fauna.codec.PageOf; import com.fauna.types.Page; import java.util.Iterator; @@ -18,96 +18,154 @@ /** * PageIterator iterates over paged responses from Fauna, the default page size is 16. - * @param + * + * @param The type of elements in the page. */ public class PageIterator implements Iterator> { - private static final String PAGINATE_QUERY = "Set.paginate(${after})"; + static final String TOKEN_NAME = "token"; + static final String PAGINATE_QUERY = "Set.paginate(${" + TOKEN_NAME + "})"; private final FaunaClient client; private final QueryOptions options; private final PageOf pageClass; private CompletableFuture>> queryFuture; - private Page latestResult; /** * Construct a new PageIterator. - * @param client A client that makes requests to Fauna. - * @param fql The FQL query. - * @param resultClass The class of the elements returned from Fauna (i.e. the rows). - * @param options (optionally) pass in QueryOptions. + * + * @param client A client that makes requests to Fauna. + * @param fql The FQL query. + * @param resultClass The class of the elements returned from Fauna (i.e., the rows). + * @param options (optionally) pass in QueryOptions. */ - public PageIterator(FaunaClient client, Query fql, Class resultClass, QueryOptions options) { + public PageIterator(final FaunaClient client, final Query fql, final Class resultClass, + final QueryOptions options) { this.client = client; this.pageClass = new PageOf<>(resultClass); this.options = options; // Initial query; this.queryFuture = client.asyncQuery(fql, this.pageClass, options); - this.latestResult = null; } - public PageIterator(FaunaClient client, Page page, Class resultClass, QueryOptions options) { + /** + * Construct a new PageIterator starting from a given page. + * + * @param client A client that makes requests to Fauna. + * @param firstPage The first Page of elements. + * @param resultClass The class of the elements returned from Fauna (i.e., the rows). + * @param options (optionally) pass in QueryOptions. + */ + public PageIterator(final FaunaClient client, final Page firstPage, + final Class resultClass, final QueryOptions options) { this.client = client; this.pageClass = new PageOf<>(resultClass); this.options = options; - this.queryFuture = null; - this.latestResult = page; - } - - private void completeFuture() { - if (this.queryFuture != null && this.latestResult == null) { - try { - this.latestResult = queryFuture.join().getData(); - } catch (CompletionException e) { - if (e.getCause() != null && e.getCause() instanceof FaunaException) { - throw (FaunaException) e.getCause(); - } else { - throw e; - } - } - this.queryFuture = null; - } + firstPage.getAfter() + .ifPresentOrElse(this::doPaginatedQuery, this::endPagination); } + /** + * Check if there is a next page available. + * + * @return True if there is a next page, false otherwise. + */ @Override public boolean hasNext() { - completeFuture(); - return this.latestResult != null; + return this.queryFuture != null; + } + + /** + * Build the page query with a specific AfterToken. + * + * @param afterToken The token indicating where the next page should start. + * @return A Query to fetch the next page. + */ + public static Query buildPageQuery(final AfterToken afterToken) { + return fql(PAGINATE_QUERY, Map.of(TOKEN_NAME, afterToken.getToken())); } + /** + * Performs a paginated query with the provided AfterToken. + * + * @param afterToken The token indicating where the next page should start. + */ + private void doPaginatedQuery(final AfterToken afterToken) { + this.queryFuture = + client.asyncQuery(PageIterator.buildPageQuery(afterToken), + pageClass, options); + } + /** + * Ends the pagination process when no further pages are available. + */ + private void endPagination() { + this.queryFuture = null; + } + + /** + * Returns a CompletableFuture that will complete with the next page (or throw a FaunaException). + * + * @return A CompletableFuture representing the next page of elements. + */ + public CompletableFuture> nextAsync() { + if (this.queryFuture != null) { + return this.queryFuture.thenApply(qs -> { + Page page = qs.getData(); + page.getAfter().ifPresentOrElse(this::doPaginatedQuery, + this::endPagination); + return page; + }); + } else { + throw new NoSuchElementException(); + } + } /** * Get the next Page. - * @return The next Page of elements E. + * + * @return The next Page of elements E. + * @throws FaunaException If there is an error getting the next page. */ @Override public Page next() { - completeFuture(); - if (this.latestResult != null) { - Page page = this.latestResult; - this.latestResult = null; - if (page.getAfter() != null) { - Map args = Map.of("after", page.getAfter()); - this.queryFuture = client.asyncQuery(fql(PAGINATE_QUERY, args), pageClass, options); + try { + return nextAsync().join(); + } catch (CompletionException ce) { + if (ce.getCause() != null && ce.getCause() instanceof FaunaException) { + throw (FaunaException) ce.getCause(); + } else { + throw ce; } - return page; - } else { - throw new NoSuchElementException(); } } /** * Return an iterator that iterates directly over the items that make up the page contents. - * @return An iterator of E. + * + * @return An iterator of E. */ public Iterator flatten() { return new Iterator<>() { private final PageIterator pageIterator = PageIterator.this; - private Iterator thisPage = pageIterator.hasNext() ? pageIterator.next().getData().iterator() : null; + private Iterator thisPage = pageIterator.hasNext() + ? pageIterator.next().getData().iterator() + : null; + + /** + * Check if there are more items to iterate over. + * + * @return True if there are more items, false otherwise. + */ @Override public boolean hasNext() { return thisPage != null && (thisPage.hasNext() || pageIterator.hasNext()); } + /** + * Get the next item in the iteration. + * + * @return The next item in the iteration. + * @throws NoSuchElementException if no more items are available. + */ @Override public E next() { if (thisPage == null) { @@ -117,7 +175,8 @@ public E next() { return thisPage.next(); } catch (NoSuchElementException e) { if (pageIterator.hasNext()) { - this.thisPage = pageIterator.next().getData().iterator(); + this.thisPage = + pageIterator.next().getData().iterator(); return thisPage.next(); } else { throw e; diff --git a/src/main/java/com/fauna/client/QueryStatsSummary.java b/src/main/java/com/fauna/client/QueryStatsSummary.java new file mode 100644 index 00000000..0b8a4a1a --- /dev/null +++ b/src/main/java/com/fauna/client/QueryStatsSummary.java @@ -0,0 +1,205 @@ +package com.fauna.client; + +/** + * A class for representing aggregate query stats. This should be used when collecting query stats + * across multiple requests. + *

+ * For a single request, use @link com.fauna.response.QueryStats instead. + */ +public final class QueryStatsSummary { + private final long readOps; + private final long computeOps; + private final long writeOps; + private final long queryTimeMs; + private final int contentionRetries; + private final long storageBytesRead; + private final long storageBytesWrite; + private final long processingTimeMs; + private final int queryCount; + + private final int rateLimitedReadQueryCount; + private final int rateLimitedComputeQueryCount; + private final int rateLimitedWriteQueryCount; + + /** + * @param readOps Aggregate Transactional + * Read Operations (TROs) consumed + * by the requests. + * @param computeOps Aggregate Transactional + * Compute Operations (TCOs) + * consumed by the requests. + * @param writeOps Aggregate Transactional + * Write Operations (TWOs) + * consumed by the requests. + * @param queryTimeMs Aggregate query run time for the + * requests in milliseconds. + * @param contentionRetries Aggregate number of + * retries + * for contended transactions. + * @param storageBytesRead Aggregate amount of data read from + * storage, in bytes. + * @param storageBytesWrite Aggregate amount of data written to + * storage, in bytes. + * @param processingTimeMs Aggregate event processing time in + * milliseconds. Only applies to Event + * Feed and Event Stream requests. + * @param queryCount Number of requests included in the + * summary. + * @param rateLimitedReadQueryCount Aggregate count of requests that + * exceeded + * plan + * throughput limits for + * Transactional + * Read Operations (TROs). + * @param rateLimitedComputeQueryCount Aggregate count of requests that + * exceeded + * plan + * throughput limits for + * Transactional + * Compute Operations (TCOs). + * @param rateLimitedWriteQueryCount Aggregate count of requests that + * exceeded + * plan + * throughput limits for + * Transactional + * Write Operations (TWOs). + */ + public QueryStatsSummary( + final long readOps, + final long computeOps, + final long writeOps, + final long queryTimeMs, + final int contentionRetries, + final long storageBytesRead, + final long storageBytesWrite, + final long processingTimeMs, + final int queryCount, + final int rateLimitedReadQueryCount, + final int rateLimitedComputeQueryCount, + final int rateLimitedWriteQueryCount + ) { + this.readOps = readOps; + this.computeOps = computeOps; + this.writeOps = writeOps; + this.queryTimeMs = queryTimeMs; + this.contentionRetries = contentionRetries; + this.storageBytesRead = storageBytesRead; + this.storageBytesWrite = storageBytesWrite; + this.processingTimeMs = processingTimeMs; + this.queryCount = queryCount; + this.rateLimitedReadQueryCount = rateLimitedReadQueryCount; + this.rateLimitedComputeQueryCount = rateLimitedComputeQueryCount; + this.rateLimitedWriteQueryCount = rateLimitedWriteQueryCount; + } + + /** + * Gets the aggregate Transactional Read Operations (TROs) recorded. + * + * @return A long representing the aggregate read ops + */ + public long getReadOps() { + return readOps; + } + + /** + * Gets the aggregate Transactional Compute Operations (TCOs) recorded. + * + * @return A long representing the aggregate compute ops + */ + public long getComputeOps() { + return computeOps; + } + + /** + * Gets the aggregate Transactional Write Operations (TWOs)) recorded. + * + * @return A long representing the aggregate write ops + */ + public long getWriteOps() { + return writeOps; + } + + /** + * Gets the aggregate query time in milliseconds. + * + * @return A long representing the aggregate query time in milliseconds. + */ + public long getQueryTimeMs() { + return queryTimeMs; + } + + /** + * Gets the count of retries due to contention. + * + * @return An int representing the count of retries due to contention. + */ + public int getContentionRetries() { + return contentionRetries; + } + + /** + * Gets the aggregate storage bytes read. + * + * @return A long representing the aggregate number of storage bytes read. + */ + public long getStorageBytesRead() { + return storageBytesRead; + } + + /** + * Gets the aggregate storage bytes written. + * + * @return A long representing the aggregate number of storage bytes + * written. + */ + public long getStorageBytesWrite() { + return storageBytesWrite; + } + + /** + * Gets the aggregate event processing time in milliseconds. + * Applies to Event Feeds and Event Stream requests only. + * + * @return A long representing the aggregate processing time in + * milliseconds. + */ + public long getProcessingTimeMs() { + return processingTimeMs; + } + + /** + * Gets the count of queries summarized on this instance. + * + * @return An int representing the count of queries summarized. + */ + public int getQueryCount() { + return queryCount; + } + + /** + * Gets the count of rate limited queries due to read limits. + * + * @return An int representing the count of rate limited queries. + */ + public int getRateLimitedReadQueryCount() { + return rateLimitedReadQueryCount; + } + + /** + * Gets the count of rate limited queries due to compute limits. + * + * @return An int representing the count of rate limited queries. + */ + public int getRateLimitedComputeQueryCount() { + return rateLimitedComputeQueryCount; + } + + /** + * Gets the count of rate limited queries due to write limits. + * + * @return An int representing the count of rate limited queries. + */ + public int getRateLimitedWriteQueryCount() { + return rateLimitedWriteQueryCount; + } +} diff --git a/src/main/java/com/fauna/client/RequestBuilder.java b/src/main/java/com/fauna/client/RequestBuilder.java index 269d246e..fd4ac0b7 100644 --- a/src/main/java/com/fauna/client/RequestBuilder.java +++ b/src/main/java/com/fauna/client/RequestBuilder.java @@ -1,43 +1,55 @@ package com.fauna.client; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; +import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.env.DriverEnvironment; +import com.fauna.event.EventSource; +import com.fauna.event.FeedOptions; +import com.fauna.event.FeedRequest; +import com.fauna.event.StreamOptions; +import com.fauna.event.StreamRequest; import com.fauna.exception.ClientException; -import com.fauna.exception.ClientRequestException; import com.fauna.query.QueryOptions; -import com.fauna.stream.StreamRequest; import com.fauna.query.builder.Query; -import com.fauna.codec.UTF8FaunaGenerator; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.net.http.HttpRequest; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.stream.Collectors; +import java.text.MessageFormat; +import java.time.Duration; +import java.util.logging.Logger; + +import static com.fauna.client.Logging.headersAsString; /** * The RequestBuilder class is responsible for building HTTP requests for communicating with Fauna. */ -public class RequestBuilder { +public final class RequestBuilder { private static final String BEARER = "Bearer"; private static final String QUERY_PATH = "/query/1"; private static final String STREAM_PATH = "/stream/1"; + private static final String FEED_PATH = "/feed/1"; private final HttpRequest.Builder baseRequestBuilder; + private final Duration clientTimeoutBuffer; + private final Logger logger; - static class FieldNames { + /** + * Field names for HTTP requests. + */ + public static class FieldNames { static final String QUERY = "query"; - static final String TOKEN = "token"; - static final String CURSOR = "cursor"; - static final String START_TS = "start_ts"; + public static final String TOKEN = "token"; + public static final String CURSOR = "cursor"; + public static final String START_TS = "start_ts"; + public static final String PAGE_SIZE = "page_size"; } + /** + * HTTP headers used for Fauna requests. + */ static class Headers { static final String LAST_TXN_TS = "X-Last-Txn-Ts"; static final String LINEARIZED = "X-Linearized"; @@ -54,51 +66,125 @@ static class Headers { static final String FORMAT = "X-Format"; } - public RequestBuilder(URI uri, String token) { - // DriverEnvironment is not needed outside the constructor for now. - DriverEnvironment env = new DriverEnvironment(DriverEnvironment.JvmDriver.JAVA); + /** + * Constructor for creating a RequestBuilder with the specified Fauna configuration. + * + * @param uri The URI for the Fauna endpoint. + * @param token The secret key used for authorization. + * @param maxContentionRetries The maximum retries for contention errors. + * @param clientTimeoutBuffer The buffer for the client timeout. + * @param logger The logger to log HTTP request details. + */ + public RequestBuilder(final URI uri, final String token, final int maxContentionRetries, + final Duration clientTimeoutBuffer, final Logger logger) { + DriverEnvironment env = + new DriverEnvironment(DriverEnvironment.JvmDriver.JAVA); this.baseRequestBuilder = HttpRequest.newBuilder().uri(uri).headers( RequestBuilder.Headers.FORMAT, "tagged", RequestBuilder.Headers.ACCEPT_ENCODING, "gzip", - RequestBuilder.Headers.CONTENT_TYPE, "application/json;charset=utf-8", + RequestBuilder.Headers.CONTENT_TYPE, + "application/json;charset=utf-8", RequestBuilder.Headers.DRIVER, "Java", RequestBuilder.Headers.DRIVER_ENV, env.toString(), + RequestBuilder.Headers.MAX_CONTENTION_RETRIES, + String.valueOf(maxContentionRetries), Headers.AUTHORIZATION, buildAuthHeader(token) ); + this.clientTimeoutBuffer = clientTimeoutBuffer; + this.logger = logger; } - public RequestBuilder(HttpRequest.Builder builder) { + /** + * Constructor for creating a RequestBuilder with an existing HttpRequest.Builder. + * + * @param builder The HttpRequest.Builder to use. + * @param clientTimeoutBuffer The buffer for the client timeout. + * @param logger The logger to log HTTP request details. + */ + public RequestBuilder(final HttpRequest.Builder builder, + final Duration clientTimeoutBuffer, final Logger logger) { this.baseRequestBuilder = builder; + this.clientTimeoutBuffer = clientTimeoutBuffer; + this.logger = logger; + } + + /** + * Creates a new RequestBuilder for Fauna queries. + * + * @param config The FaunaConfig containing endpoint and secret. + * @param logger The logger for logging HTTP request details. + * @return A new instance of RequestBuilder. + */ + public static RequestBuilder queryRequestBuilder(final FaunaConfig config, + final Logger logger) { + return new RequestBuilder(URI.create(config.getEndpoint() + QUERY_PATH), + config.getSecret(), config.getMaxContentionRetries(), + config.getClientTimeoutBuffer(), logger); } - public static RequestBuilder queryRequestBuilder(FaunaConfig config) { - return new RequestBuilder(URI.create(config.getEndpoint() + QUERY_PATH), config.getSecret()); + /** + * Creates a new RequestBuilder for Fauna streams. + * + * @param config The FaunaConfig containing endpoint and secret. + * @param logger The logger for logging HTTP request details. + * @return A new instance of RequestBuilder. + */ + public static RequestBuilder streamRequestBuilder(final FaunaConfig config, + final Logger logger) { + return new RequestBuilder( + URI.create(config.getEndpoint() + STREAM_PATH), + config.getSecret(), config.getMaxContentionRetries(), + config.getClientTimeoutBuffer(), logger); } - public static RequestBuilder streamRequestBuilder(FaunaConfig config) { - return new RequestBuilder(URI.create(config.getEndpoint() + STREAM_PATH), config.getSecret()); + /** + * Creates a new RequestBuilder for Fauna feed requests. + * + * @param config The FaunaConfig containing endpoint and secret. + * @param logger The logger for logging HTTP request details. + * @return A new instance of RequestBuilder. + */ + public static RequestBuilder feedRequestBuilder(final FaunaConfig config, + final Logger logger) { + return new RequestBuilder(URI.create(config.getEndpoint() + FEED_PATH), + config.getSecret(), config.getMaxContentionRetries(), + config.getClientTimeoutBuffer(), logger); } - public RequestBuilder scopedRequestBuilder(String token) { + /** + * Creates a scoped request builder with the given token. + * + * @param token The token to be used for the request's authorization header. + * @return A new instance of RequestBuilder with the scoped token. + */ + public RequestBuilder scopedRequestBuilder(final String token) { HttpRequest.Builder newBuilder = this.baseRequestBuilder.copy(); - // .setHeader(..) clears existing headers (which we want) while .header(..) would append it :) newBuilder.setHeader(Headers.AUTHORIZATION, buildAuthHeader(token)); - return new RequestBuilder(newBuilder); + return new RequestBuilder(newBuilder, clientTimeoutBuffer, logger); + } + + private void logRequest(final String body, final HttpRequest req) { + String timeout = req.timeout().map( + val -> MessageFormat.format(" (timeout: {0})", val)).orElse(""); + logger.fine(MessageFormat.format( + "Fauna HTTP {0} Request to {1}{2}, headers: {3}", + req.method(), req.uri(), timeout, + headersAsString(req.headers()))); + logger.finest("Request body: " + body); } /** * Builds and returns an HTTP request for a given Fauna query string (FQL). * - * @param fql The Fauna query string. + * @param fql The Fauna query string. + * @param options The query options. + * @param provider The codec provider to encode the query. + * @param lastTxnTs The last transaction timestamp (optional). * @return An HttpRequest object configured for the Fauna query. */ - public HttpRequest buildRequest(Query fql, QueryOptions options, CodecProvider provider) { - // TODO: I think we can avoid doing this copy if no new headers need to be set. - HttpRequest.Builder builder = baseRequestBuilder.copy(); - if (options != null) { - addOptionalHeaders(builder, options); - } - // TODO: set last-txn-ts and max-contention-retries. + public HttpRequest buildRequest(final Query fql, final QueryOptions options, + final CodecProvider provider, final Long lastTxnTs) { + HttpRequest.Builder builder = getBuilder(options, lastTxnTs); try (UTF8FaunaGenerator gen = UTF8FaunaGenerator.create()) { gen.writeStartObject(); gen.writeFieldName(FieldNames.QUERY); @@ -106,65 +192,102 @@ public HttpRequest buildRequest(Query fql, QueryOptions options, CodecProvider p codec.encode(gen, fql); gen.writeEndObject(); String body = gen.serialize(); - return builder.POST(HttpRequest.BodyPublishers.ofString(body)).build(); + HttpRequest req = + builder.POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + logRequest(body, req); + return req; } } - public String buildStreamRequestBody(StreamRequest request) throws IOException { - // Use JsonGenerator directly rather than UTF8FaunaGenerator because this is not FQL. For example, - // start_ts is a JSON numeric/integer, not a tagged '@long'. - ByteArrayOutputStream requestBytes = new ByteArrayOutputStream(); - JsonGenerator gen = new JsonFactory().createGenerator(requestBytes); - gen.writeStartObject(); - gen.writeStringField(FieldNames.TOKEN, request.getToken()); - // Only one of cursor / start_ts can be present, prefer cursor. - // Cannot use ifPresent(val -> ...) because gen.write methods can throw an IOException. - if (request.getCursor().isPresent()) { - gen.writeStringField(FieldNames.CURSOR, request.getCursor().get()); - } else if (request.getStartTs().isPresent()) { - gen.writeNumberField(FieldNames.START_TS, request.getStartTs().get()); + /** + * Builds and returns an HTTP request for a Fauna stream. + * + * @param eventSource The event source for the stream. + * @param streamOptions The stream options. + * @return An HttpRequest object configured for the Fauna stream. + */ + public HttpRequest buildStreamRequest(final EventSource eventSource, + final StreamOptions streamOptions) { + HttpRequest.Builder builder = baseRequestBuilder.copy(); + streamOptions.getTimeout().ifPresent(builder::timeout); + try { + String body = new StreamRequest(eventSource, streamOptions).serialize(); + HttpRequest req = builder.POST(HttpRequest.BodyPublishers.ofString(body)).build(); + logRequest(body, req); + return req; + } catch (IOException e) { + throw new ClientException("Unable to build Fauna Stream request.", e); } - gen.writeEndObject(); - gen.flush(); - return requestBytes.toString(StandardCharsets.UTF_8); } - public HttpRequest buildStreamRequest(StreamRequest request) { + /** + * Builds and returns an HTTP request for a Fauna feed. + * + * @param eventSource The event source for the feed. + * @param options The feed options. + * @return An HttpRequest object configured for the Fauna feed. + */ + public HttpRequest buildFeedRequest(final EventSource eventSource, + final FeedOptions options) { + FeedRequest request = new FeedRequest(eventSource, options); HttpRequest.Builder builder = baseRequestBuilder.copy(); + options.getTimeout().ifPresent(val -> { + builder.timeout(val.plus(clientTimeoutBuffer)); + builder.header(Headers.QUERY_TIMEOUT_MS, + String.valueOf(val.toMillis())); + }); try { - return builder.POST(HttpRequest.BodyPublishers.ofString(buildStreamRequestBody(request))).build(); + String body = request.serialize(); + HttpRequest req = builder.POST(HttpRequest.BodyPublishers.ofString(request.serialize())).build(); + logRequest(body, req); + return req; } catch (IOException e) { - throw new ClientException("Unable to build Fauna Stream request.", e); + throw new ClientException("Unable to build Fauna Feed request.", e); } - } - private static String buildAuthHeader(String token) { + /** + * Builds an authorization header for the given token. + * + * @param token The token to be used in the authorization header. + * @return The authorization header value. + */ + private static String buildAuthHeader(final String token) { return String.join(" ", RequestBuilder.BEARER, token); } /** - * Adds optional headers to the HttpRequest.Builder from the given QueryOptions. - * @param builder A HttpRequest.Builder that will have headers added to it. - * @param options The QueryOptions (must not be null). + * Gets the base request builder or a copy with options applied. + * + * @param options The QueryOptions (must not be null). + * @param lastTxnTs The last transaction timestamp (optional). + * @return The HttpRequest.Builder configured with options. */ - private static void addOptionalHeaders(HttpRequest.Builder builder, QueryOptions options) { - options.getTimeoutMillis().ifPresent(val -> builder.header(Headers.QUERY_TIMEOUT_MS, String.valueOf(val))); - options.getLinearized().ifPresent(val -> builder.header(Headers.LINEARIZED, String.valueOf(val))); - options.getTypeCheck().ifPresent(val -> builder.header(Headers.TYPE_CHECK, String.valueOf(val))); - options.getTraceParent().ifPresent(val -> builder.header(Headers.TRACE_PARENT, val)); - options.getQueryTags().ifPresent(val -> builder.headers(Headers.QUERY_TAGS, QueryTags.encode(val))); - } - - public static class QueryTags { - private static final String EQUALS = "="; - private static final String COMMA = ","; - - public static String encode(Map tags) { - return tags.entrySet().stream() - .map(entry -> String.join(EQUALS, entry.getKey(), entry.getValue())) - .collect(Collectors.joining(COMMA)); + private HttpRequest.Builder getBuilder(final QueryOptions options, final Long lastTxnTs) { + if (options == null && (lastTxnTs == null || lastTxnTs <= 0)) { + return baseRequestBuilder; + } + HttpRequest.Builder builder = baseRequestBuilder.copy(); + if (lastTxnTs != null) { + builder.setHeader(Headers.LAST_TXN_TS, String.valueOf(lastTxnTs)); } + if (options != null) { + options.getTimeoutMillis().ifPresent(val -> { + builder.timeout(Duration.ofMillis(val).plus(clientTimeoutBuffer)); + builder.header(Headers.QUERY_TIMEOUT_MS, String.valueOf(val)); + }); + options.getLinearized().ifPresent( + val -> builder.header(Headers.LINEARIZED, String.valueOf(val))); + options.getTypeCheck().ifPresent( + val -> builder.header(Headers.TYPE_CHECK, String.valueOf(val))); + options.getTraceParent().ifPresent( + val -> builder.header(Headers.TRACE_PARENT, val)); + options.getQueryTags().ifPresent( + val -> builder.headers(Headers.QUERY_TAGS, val.encode())); + } + return builder; } + } diff --git a/src/main/java/com/fauna/client/RetryHandler.java b/src/main/java/com/fauna/client/RetryHandler.java index a1f9f8ce..5e0679f1 100644 --- a/src/main/java/com/fauna/client/RetryHandler.java +++ b/src/main/java/com/fauna/client/RetryHandler.java @@ -3,36 +3,65 @@ import com.fauna.exception.FaunaException; import com.fauna.exception.RetryableException; -import java.util.concurrent.Callable; +import java.text.MessageFormat; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; +import java.util.logging.Logger; /** * A retry handler controls the retries for a particular request. + * + * @param The return type for a successful response. */ -public class RetryHandler { +public final class RetryHandler { private final RetryStrategy strategy; + private final Logger logger; /** - * Construct a new retry handler instance. - * @param strategy The retry strategy to use. + * Constructs a new retry handler instance. + * + * @param strategy The retry strategy to use. + * @param logger The logger used to log retry details. */ - public RetryHandler(RetryStrategy strategy) { + public RetryHandler(final RetryStrategy strategy, final Logger logger) { this.strategy = strategy; + this.logger = logger; } - public CompletableFuture delayRequest(Supplier> action, int delayMillis) { + /** + * Delays the request execution by a specified delay in milliseconds. + * + * @param action The action to be executed. + * @param delayMillis The delay in milliseconds before executing the action. + * @return A CompletableFuture representing the result of the action. + */ + public CompletableFuture delayRequest( + final Supplier> action, final int delayMillis) { return CompletableFuture.supplyAsync( - () -> action.get(), CompletableFuture.delayedExecutor(delayMillis, TimeUnit.MILLISECONDS)).join(); + action, CompletableFuture.delayedExecutor(delayMillis, + TimeUnit.MILLISECONDS)).join(); } - public static boolean isRetryable(Throwable exc) { - return exc instanceof RetryableException || exc.getCause() instanceof RetryableException; + /** + * Checks if an exception is retryable. + * + * @param exc The exception to check. + * @return True if the exception or its cause is retryable. + */ + public static boolean isRetryable(final Throwable exc) { + return exc instanceof RetryableException || + exc.getCause() instanceof RetryableException; } - public CompletableFuture rethrow(Throwable throwable) { + /** + * Rethrows a throwable as a FaunaException. + * + * @param throwable The throwable to be rethrown. + * @return A failed CompletableFuture containing the throwable. + */ + public CompletableFuture rethrow(final Throwable throwable) { if (throwable instanceof FaunaException) { throw (FaunaException) throwable; } else if (throwable.getCause() instanceof FaunaException) { @@ -41,25 +70,48 @@ public CompletableFuture rethrow(Throwable throwable) { return CompletableFuture.failedFuture(throwable); } - private CompletableFuture retry(Throwable throwable, int retryAttempt, Supplier> supplier) { + /** + * Retries the action based on the retry strategy. + * + * @param throwable The throwable that caused the failure. + * @param retryAttempt The current retry attempt number. + * @param supplier The action to retry. + * @return A CompletableFuture representing the result of the retried action. + */ + private CompletableFuture retry(final Throwable throwable, final int retryAttempt, + final Supplier> supplier) { try { - if (isRetryable(throwable) && this.strategy.canRetry(retryAttempt)) { - return delayRequest(supplier, this.strategy.getDelayMillis(retryAttempt)); + boolean retryable = isRetryable(throwable); + if (retryable && this.strategy.canRetry(retryAttempt)) { + int delay = this.strategy.getDelayMillis(retryAttempt); + logger.fine(MessageFormat.format( + "Retry attempt {0} for exception {1}", retryAttempt, + throwable.getClass())); + return delayRequest(supplier, delay); } else { + logger.fine(MessageFormat.format( + "Re-throwing {0}retryable exception: {1}", + retryable ? "" : "non-", throwable.getClass())); return rethrow(throwable); } } catch (FaunaException exc) { throw exc; } catch (Exception exc) { - throw new FaunaException("oops", exc); + throw new FaunaException("Unexpected exception.", exc); } } - public CompletableFuture execute(Supplier> action) { + /** + * Executes an action with retry logic based on the retry strategy. + * + * @param action The action to execute. + * @return A CompletableFuture representing the result of the action. + */ + public CompletableFuture execute(final Supplier> action) { CompletableFuture f = action.get(); - for(int i = 1; i <= this.strategy.getMaxRetryAttempts(); i++) { - int finalI = i; - f=f.thenApply(CompletableFuture::completedFuture) + for (int i = 1; i <= this.strategy.getMaxRetryAttempts(); i++) { + final int finalI = i; + f = f.thenApply(CompletableFuture::completedFuture) .exceptionally(t -> retry(t, finalI, action)) .thenCompose(Function.identity()); } diff --git a/src/main/java/com/fauna/client/RetryStrategy.java b/src/main/java/com/fauna/client/RetryStrategy.java index 0af09538..6d3f4e69 100644 --- a/src/main/java/com/fauna/client/RetryStrategy.java +++ b/src/main/java/com/fauna/client/RetryStrategy.java @@ -9,18 +9,25 @@ public interface RetryStrategy { /** * Returns true if the given retry attempt will be allowed by this strategy. - * @param retryAttempt The retry attempt number, starting at 1 (i.e. the second overall attempt, or first retry is attempt 1). - * @return True if this attempt can be retried, otherwise false. + * + * @param retryAttempt The retry attempt number, starting at 1 (i.e. the second overall attempt, or first retry is attempt 1). + * @return True if this attempt can be retried, otherwise false. */ boolean canRetry(int retryAttempt); /** - * Return the number of milliseconds to delay the next attempt. - * @param retryAttempt The retry attempt number, starting at 1 (i.e. the second overall attempt, or first retry is attempt 1). - * @return + * Return the number of milliseconds to delay the next retry attempt. + * + * @param retryAttempt The retry attempt number, starting at 1 (i.e. the second overall attempt/first retry is #1). + * @return The number of milliseconds to delay the next retry attempt. */ int getDelayMillis(int retryAttempt); + /** + * Return the maximum number of retry attempts for this strategy. + * + * @return The number of retry attempts that this strategy will attempt. + */ int getMaxRetryAttempts(); diff --git a/src/main/java/com/fauna/client/ScopedFaunaClient.java b/src/main/java/com/fauna/client/ScopedFaunaClient.java index 5e35f6c3..5a0bebda 100644 --- a/src/main/java/com/fauna/client/ScopedFaunaClient.java +++ b/src/main/java/com/fauna/client/ScopedFaunaClient.java @@ -2,37 +2,81 @@ import java.net.http.HttpClient; -public class ScopedFaunaClient extends FaunaClient { +/** + * ScopedFaunaClient is a subclass of FaunaClient that applies a scope to the client, + * limiting the actions and requests to the specified scope. + */ +public final class ScopedFaunaClient extends FaunaClient { private final FaunaClient client; private final RequestBuilder requestBuilder; private final RequestBuilder streamRequestBuilder; + private final RequestBuilder feedRequestBuilder; - - public ScopedFaunaClient(FaunaClient client, FaunaScope scope) { - super(client.getFaunaSecret()); + /** + * Constructs a new ScopedFaunaClient using the provided FaunaClient and FaunaScope. + * + * @param client The FaunaClient instance to base the scoped client on. + * @param scope The FaunaScope defining the scope for this client. + */ + public ScopedFaunaClient(final FaunaClient client, final FaunaScope scope) { + super(client.getFaunaSecret(), client.getLogger(), + client.getStatsCollector().createNew()); this.client = client; - this.requestBuilder = client.getRequestBuilder().scopedRequestBuilder(scope.getToken(client.getFaunaSecret())); - this.streamRequestBuilder = client.getStreamRequestBuilder().scopedRequestBuilder(scope.getToken(client.getFaunaSecret())); + this.requestBuilder = client.getRequestBuilder() + .scopedRequestBuilder(scope.getToken(client.getFaunaSecret())); + this.streamRequestBuilder = client.getStreamRequestBuilder() + .scopedRequestBuilder(scope.getToken(client.getFaunaSecret())); + this.feedRequestBuilder = client.getFeedRequestBuilder() + .scopedRequestBuilder(scope.getToken(client.getFaunaSecret())); } - + /** + * Gets the retry strategy for the scoped client. + * + * @return The retry strategy used by the client. + */ @Override - RetryStrategy getRetryStrategy() { + public RetryStrategy getRetryStrategy() { return client.getRetryStrategy(); } + /** + * Gets the HttpClient used by the scoped client. + * + * @return The HttpClient used for making HTTP requests. + */ @Override - HttpClient getHttpClient() { + public HttpClient getHttpClient() { return client.getHttpClient(); } + /** + * Gets the RequestBuilder for the scoped client. + * + * @return The RequestBuilder used for constructing HTTP requests. + */ + @Override + public RequestBuilder getRequestBuilder() { + return requestBuilder; + } + + /** + * Gets the RequestBuilder for streaming requests. + * + * @return The RequestBuilder used for constructing streaming HTTP requests. + */ @Override - RequestBuilder getRequestBuilder() { - return this.requestBuilder; + public RequestBuilder getStreamRequestBuilder() { + return streamRequestBuilder; } + /** + * Gets the RequestBuilder for feed requests. + * + * @return The RequestBuilder used for constructing feed HTTP requests. + */ @Override - RequestBuilder getStreamRequestBuilder() { - return this.streamRequestBuilder; + public RequestBuilder getFeedRequestBuilder() { + return feedRequestBuilder; } } diff --git a/src/main/java/com/fauna/client/StatsCollector.java b/src/main/java/com/fauna/client/StatsCollector.java new file mode 100644 index 00000000..7c9a24e8 --- /dev/null +++ b/src/main/java/com/fauna/client/StatsCollector.java @@ -0,0 +1,34 @@ +package com.fauna.client; + +import com.fauna.response.QueryStats; + +public interface StatsCollector { + + /** + * Add the QueryStats to the current counts. + * + * @param stats QueryStats object + */ + void add(QueryStats stats); + + /** + * Return the collected Stats. + * + * @return Stats object + */ + QueryStatsSummary read(); + + /** + * Return the collected Stats and reset counts. + * + * @return Stats object + */ + QueryStatsSummary readAndReset(); + + /** + * Creates a new instance of a {@code StatsCollector}. + * + * @return A {@code StatsCollector} instance. + */ + StatsCollector createNew(); +} diff --git a/src/main/java/com/fauna/client/StatsCollectorImpl.java b/src/main/java/com/fauna/client/StatsCollectorImpl.java new file mode 100644 index 00000000..33e6d99e --- /dev/null +++ b/src/main/java/com/fauna/client/StatsCollectorImpl.java @@ -0,0 +1,102 @@ +package com.fauna.client; + +import com.fauna.response.QueryStats; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public final class StatsCollectorImpl implements StatsCollector { + + private static final String RATE_LIMIT_READ_OPS = "read"; + private static final String RATE_LIMIT_COMPUTE_OPS = "compute"; + private static final String RATE_LIMIT_WRITE_OPS = "write"; + + private final AtomicLong readOps = new AtomicLong(); + private final AtomicLong computeOps = new AtomicLong(); + private final AtomicLong writeOps = new AtomicLong(); + private final AtomicLong queryTimeMs = new AtomicLong(); + private final AtomicInteger contentionRetries = new AtomicInteger(); + private final AtomicLong storageBytesRead = new AtomicLong(); + private final AtomicLong storageBytesWrite = new AtomicLong(); + private final AtomicLong processingTimeMs = new AtomicLong(); + private final AtomicInteger queryCount = new AtomicInteger(); + private final AtomicInteger rateLimitedReadQueryCount = new AtomicInteger(); + private final AtomicInteger rateLimitedComputeQueryCount = + new AtomicInteger(); + private final AtomicInteger rateLimitedWriteQueryCount = + new AtomicInteger(); + + @Override + public void add(final QueryStats stats) { + readOps.addAndGet(stats.getReadOps()); + computeOps.addAndGet(stats.getComputeOps()); + writeOps.addAndGet(stats.getWriteOps()); + queryTimeMs.addAndGet(stats.getQueryTimeMs()); + contentionRetries.addAndGet(stats.getContentionRetries()); + storageBytesRead.addAndGet(stats.getStorageBytesRead()); + storageBytesWrite.addAndGet(stats.getStorageBytesWrite()); + processingTimeMs.addAndGet(stats.getProcessingTimeMs()); + + List rateLimitsHit = stats.getRateLimitsHit(); + rateLimitsHit.forEach(limitHit -> { + switch (limitHit) { + case RATE_LIMIT_READ_OPS: + rateLimitedReadQueryCount.incrementAndGet(); + break; + case RATE_LIMIT_COMPUTE_OPS: + rateLimitedComputeQueryCount.incrementAndGet(); + break; + case RATE_LIMIT_WRITE_OPS: + rateLimitedWriteQueryCount.incrementAndGet(); + break; + default: + break; + } + }); + + queryCount.incrementAndGet(); + } + + @Override + public QueryStatsSummary read() { + return new QueryStatsSummary( + readOps.get(), + computeOps.get(), + writeOps.get(), + queryTimeMs.get(), + contentionRetries.get(), + storageBytesRead.get(), + storageBytesWrite.get(), + processingTimeMs.get(), + queryCount.get(), + rateLimitedReadQueryCount.get(), + rateLimitedComputeQueryCount.get(), + rateLimitedWriteQueryCount.get() + ); + } + + @Override + public QueryStatsSummary readAndReset() { + return new QueryStatsSummary( + readOps.getAndSet(0), + computeOps.getAndSet(0), + writeOps.getAndSet(0), + queryTimeMs.getAndSet(0), + contentionRetries.getAndSet(0), + storageBytesRead.getAndSet(0), + storageBytesWrite.getAndSet(0), + processingTimeMs.getAndSet(0), + queryCount.getAndSet(0), + rateLimitedReadQueryCount.getAndSet(0), + rateLimitedComputeQueryCount.getAndSet(0), + rateLimitedWriteQueryCount.getAndSet(0) + ); + } + + @Override + public StatsCollector createNew() { + return new StatsCollectorImpl(); + } +} + diff --git a/src/main/java/com/fauna/client/package-info.java b/src/main/java/com/fauna/client/package-info.java new file mode 100644 index 00000000..17531599 --- /dev/null +++ b/src/main/java/com/fauna/client/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes related initializing, configuring, and using a client to interact + * with Fauna. + */ +package com.fauna.client; diff --git a/src/main/java/com/fauna/codec/Codec.java b/src/main/java/com/fauna/codec/Codec.java index efcb2d22..c25ab6ae 100644 --- a/src/main/java/com/fauna/codec/Codec.java +++ b/src/main/java/com/fauna/codec/Codec.java @@ -2,12 +2,44 @@ import com.fauna.exception.CodecException; - +/** + * Interface for codecs, which handle the serialization and deserialization of specific types. + *

+ * Each codec is associated with a particular class and supports a set of Fauna data types. + * + * @param The type of object that this codec can encode and decode. + */ public interface Codec { + /** + * Decodes an object from the provided {@link UTF8FaunaParser}. + * + * @param parser The parser to use for reading and decoding the data. + * @return The decoded object of type {@code T}. + * @throws CodecException If an error occurs during decoding. + */ T decode(UTF8FaunaParser parser) throws CodecException; + + /** + * Encodes the specified object using the provided {@link UTF8FaunaGenerator}. + * + * @param gen The generator to use for writing and encoding the data. + * @param obj The object of type {@code T} to encode. + * @throws CodecException If an error occurs during encoding. + */ void encode(UTF8FaunaGenerator gen, T obj) throws CodecException; + + /** + * Gets the class associated with this codec. + * + * @return The {@link Class} type that this codec handles. + */ Class getCodecClass(); + /** + * Gets the set of supported Fauna data types for this codec. + * + * @return An array of {@link FaunaType} values representing the supported types. + */ FaunaType[] getSupportedTypes(); } diff --git a/src/main/java/com/fauna/codec/CodecProvider.java b/src/main/java/com/fauna/codec/CodecProvider.java index 4900720a..0cd2e508 100644 --- a/src/main/java/com/fauna/codec/CodecProvider.java +++ b/src/main/java/com/fauna/codec/CodecProvider.java @@ -2,7 +2,27 @@ import java.lang.reflect.Type; +/** + * Interface for providing codecs. Responsible for obtaining codecs for specific classes and types. + */ public interface CodecProvider { + + /** + * Retrieves a codec for the specified class type. + * + * @param clazz The class type for which to obtain a codec. + * @param The type of the class. + * @return A codec capable of serializing and deserializing instances of the specified class. + */ Codec get(Class clazz); + + /** + * Retrieves a codec for the specified class type with additional type arguments for generic classes. + * + * @param clazz The class type for which to obtain a codec. + * @param typeArgs The generic type arguments, if applicable. + * @param The type of the class. + * @return A codec capable of serializing and deserializing instances of the specified class with the provided type arguments. + */ Codec get(Class clazz, Type[] typeArgs); } diff --git a/src/main/java/com/fauna/codec/CodecRegistry.java b/src/main/java/com/fauna/codec/CodecRegistry.java index 74c2634b..bfdc0f93 100644 --- a/src/main/java/com/fauna/codec/CodecRegistry.java +++ b/src/main/java/com/fauna/codec/CodecRegistry.java @@ -1,8 +1,35 @@ package com.fauna.codec; +/** + * Interface defining a registry for codecs, which manage the serialization and deserialization of objects. + *

+ * Provides methods for storing, retrieving, and checking for codecs by their unique keys. + */ public interface CodecRegistry { + /** + * Retrieves the codec associated with the specified key. + * + * @param key The unique key representing the codec. + * @param The type of the object handled by the codec. + * @return The codec associated with the specified key, or {@code null} if not found. + */ Codec get(CodecRegistryKey key); + + /** + * Registers a codec with the specified key in the registry. + * + * @param key The unique key representing the codec. + * @param codec The codec to register. + * @param The type of the object handled by the codec. + */ void put(CodecRegistryKey key, Codec codec); + + /** + * Checks if a codec is registered under the specified key. + * + * @param key The unique key representing the codec. + * @return {@code true} if a codec exists for the specified key; {@code false} otherwise. + */ boolean contains(CodecRegistryKey key); } diff --git a/src/main/java/com/fauna/codec/CodecRegistryKey.java b/src/main/java/com/fauna/codec/CodecRegistryKey.java index 848fc8f6..808603f2 100644 --- a/src/main/java/com/fauna/codec/CodecRegistryKey.java +++ b/src/main/java/com/fauna/codec/CodecRegistryKey.java @@ -4,34 +4,73 @@ import java.util.Arrays; import java.util.Objects; +/** + * Represents a unique key in the codec registry. + */ public class CodecRegistryKey { private final Class base; private final Type[] typeArgs; - public CodecRegistryKey(Class clazz, Type[] typeArgs) { - base = clazz; + + /** + * Constructs a new {@code CodecRegistryKey} for the specified class and type arguments. + * + * @param clazz The base class of the codec. + * @param typeArgs The type arguments for generic types, if applicable. + * @param The type of the base class. + */ + public CodecRegistryKey(final Class clazz, final Type[] typeArgs) { + this.base = clazz; this.typeArgs = typeArgs; } - public static CodecRegistryKey from(Class clazz) { + /** + * Creates a {@code CodecRegistryKey} for the specified class without any type arguments. + * + * @param clazz The base class of the codec. + * @param The type of the base class. + * @return A new {@code CodecRegistryKey} instance. + */ + public static CodecRegistryKey from(final Class clazz) { return new CodecRegistryKey(clazz, null); } - public static CodecRegistryKey from(Class clazz, Type[] typeArgs) { + /** + * Creates a {@code CodecRegistryKey} for the specified class and type arguments. + * + * @param clazz The base class of the codec. + * @param typeArgs The type arguments for generic types. + * @param The type of the base class. + * @return A new {@code CodecRegistryKey} instance. + */ + public static CodecRegistryKey from(final Class clazz, final Type[] typeArgs) { return new CodecRegistryKey(clazz, typeArgs); } + /** + * Compares this key with another object for equality based on the base class and type arguments. + * + * @param other The object to compare with this key. + * @return {@code true} if the other object is a {@code CodecRegistryKey} with the same base class and type arguments; + * {@code false} otherwise. + */ @Override - public boolean equals(Object other) { + public boolean equals(final Object other) { if (other == this) { return true; } else if (other instanceof CodecRegistryKey) { CodecRegistryKey otherCRK = (CodecRegistryKey) other; - return Objects.equals(base, otherCRK.base) && Arrays.equals(typeArgs, otherCRK.typeArgs); + return Objects.equals(base, otherCRK.base) && + Arrays.equals(typeArgs, otherCRK.typeArgs); } else { return false; } } + /** + * Returns a hash code for this key, based on the base class and type arguments. + * + * @return The hash code for this {@code CodecRegistryKey}. + */ @Override public final int hashCode() { return Objects.hash(base, Arrays.hashCode(typeArgs)); diff --git a/src/main/java/com/fauna/codec/DefaultCodecProvider.java b/src/main/java/com/fauna/codec/DefaultCodecProvider.java index d7126bb8..0202ba43 100644 --- a/src/main/java/com/fauna/codec/DefaultCodecProvider.java +++ b/src/main/java/com/fauna/codec/DefaultCodecProvider.java @@ -4,6 +4,7 @@ import com.fauna.codec.codecs.ClassCodec; import com.fauna.codec.codecs.DynamicCodec; import com.fauna.codec.codecs.EnumCodec; +import com.fauna.codec.codecs.EventSourceCodec; import com.fauna.codec.codecs.ListCodec; import com.fauna.codec.codecs.MapCodec; import com.fauna.codec.codecs.NullableDocumentCodec; @@ -13,9 +14,8 @@ import com.fauna.codec.codecs.QueryCodec; import com.fauna.codec.codecs.QueryLiteralCodec; import com.fauna.codec.codecs.QueryObjCodec; -import com.fauna.codec.codecs.StreamTokenResponseCodec; -import com.fauna.query.StreamTokenResponse; import com.fauna.codec.codecs.QueryValCodec; +import com.fauna.event.EventSource; import com.fauna.query.builder.Query; import com.fauna.query.builder.QueryArr; import com.fauna.query.builder.QueryLiteral; @@ -32,13 +32,28 @@ import java.util.Map; import java.util.Optional; -public class DefaultCodecProvider implements CodecProvider { +/** + * Provides codecs for serialization and deserialization of various data types in Fauna. + *

+ * This provider supports codecs for primitive types, collections, optional values, documents, enums, and more. + *

+ */ +public final class DefaultCodecProvider implements CodecProvider { private final CodecRegistry registry; - public static final CodecProvider SINGLETON = new DefaultCodecProvider(DefaultCodecRegistry.SINGLETON); - - public DefaultCodecProvider(CodecRegistry registry) { + /** + * Singleton instance of the {@code DefaultCodecProvider} for global access. + */ + public static final CodecProvider SINGLETON = + new DefaultCodecProvider(DefaultCodecRegistry.SINGLETON); + + /** + * Initializes a new instance of {@code DefaultCodecProvider} with a specified registry. + * + * @param registry The codec registry to store generated codecs. + */ + public DefaultCodecProvider(final CodecRegistry registry) { registry.put(CodecRegistryKey.from(Object.class), new DynamicCodec(this)); registry.put(CodecRegistryKey.from(Query.class), new QueryCodec(this)); @@ -47,8 +62,7 @@ public DefaultCodecProvider(CodecRegistry registry) { registry.put(CodecRegistryKey.from(QueryVal.class), new QueryValCodec(this)); registry.put(CodecRegistryKey.from(QueryLiteral.class), new QueryLiteralCodec()); - registry.put(CodecRegistryKey.from(StreamTokenResponse.class), new StreamTokenResponseCodec()); - + registry.put(CodecRegistryKey.from(EventSource.class), new EventSourceCodec()); var bdc = new BaseDocumentCodec(this); registry.put(CodecRegistryKey.from(BaseDocument.class), bdc); @@ -58,12 +72,27 @@ public DefaultCodecProvider(CodecRegistry registry) { this.registry = registry; } - public Codec get(Class clazz) { + /** + * Retrieves the codec for the specified class type. + * + * @param clazz The class for which a codec is requested. + * @param The data type to be encoded or decoded. + * @return The {@link Codec} associated with the class. + */ + public Codec get(final Class clazz) { return get(clazz, null); } + /** + * Retrieves the codec for the specified class type and type arguments. + * + * @param clazz The class for which a codec is requested. + * @param typeArgs The type arguments for generic classes. + * @param The data type to be encoded or decoded. + * @return The {@link Codec} associated with the class and type arguments. + */ @Override - public Codec get(Class clazz, Type[] typeArgs) { + public Codec get(final Class clazz, final Type[] typeArgs) { CodecRegistryKey key = CodecRegistryKey.from(clazz, typeArgs); if (!registry.contains(key)) { @@ -74,31 +103,39 @@ public Codec get(Class clazz, Type[] typeArgs) { return registry.get(key); } + /** + * Generates a codec for the specified class type and type arguments if not already available. + * + * @param clazz The class for which a codec needs to be generated. + * @param typeArgs The type arguments for generic classes. + * @param The data type to be encoded or decoded. + * @param The element type for collection codecs. + * @return The generated {@link Codec} for the class and type arguments. + */ @SuppressWarnings({"unchecked"}) - private Codec generate(Class clazz, Type[] typeArgs) { + private Codec generate(final Class clazz, final Type[] typeArgs) { if (Map.class.isAssignableFrom(clazz)) { var ta = typeArgs == null || typeArgs.length <= 1 ? Object.class : typeArgs[1]; Codec valueCodec = this.get((Class) ta, null); - return (Codec) new MapCodec>((Codec) valueCodec); + return (Codec) new MapCodec>((Codec) valueCodec); } var ta = typeArgs == null || typeArgs.length == 0 ? Object.class : typeArgs[0]; if (List.class.isAssignableFrom(clazz)) { Codec elemCodec = this.get((Class) ta, null); - - return (Codec) new ListCodec>((Codec) elemCodec); + return (Codec) new ListCodec>((Codec) elemCodec); } if (clazz == Optional.class) { Codec valueCodec = this.get((Class) ta, null); - return (Codec) new OptionalCodec>((Codec) valueCodec); + return (Codec) new OptionalCodec>((Codec) valueCodec); } if (clazz == Page.class) { Codec valueCodec = this.get((Class) ta, null); - return (Codec) new PageCodec>((Codec) valueCodec); + return (Codec) new PageCodec>((Codec) valueCodec); } if (clazz == NullableDocument.class) { diff --git a/src/main/java/com/fauna/codec/DefaultCodecRegistry.java b/src/main/java/com/fauna/codec/DefaultCodecRegistry.java index 71b47fdc..bdd27c5b 100644 --- a/src/main/java/com/fauna/codec/DefaultCodecRegistry.java +++ b/src/main/java/com/fauna/codec/DefaultCodecRegistry.java @@ -1,48 +1,77 @@ package com.fauna.codec; -import com.fauna.codec.codecs.*; -import com.fauna.query.builder.QueryVal; -import com.fauna.types.*; +import com.fauna.codec.codecs.BaseRefCodec; +import com.fauna.codec.codecs.BoolCodec; +import com.fauna.codec.codecs.ByteArrayCodec; +import com.fauna.codec.codecs.ByteCodec; +import com.fauna.codec.codecs.CharCodec; +import com.fauna.codec.codecs.DoubleCodec; +import com.fauna.codec.codecs.FloatCodec; +import com.fauna.codec.codecs.InstantCodec; +import com.fauna.codec.codecs.IntCodec; +import com.fauna.codec.codecs.LocalDateCodec; +import com.fauna.codec.codecs.LongCodec; +import com.fauna.codec.codecs.ModuleCodec; +import com.fauna.codec.codecs.ShortCodec; +import com.fauna.codec.codecs.StringCodec; +import com.fauna.types.BaseRef; +import com.fauna.types.DocumentRef; import com.fauna.types.Module; +import com.fauna.types.NamedDocumentRef; import java.time.Instant; import java.time.LocalDate; import java.util.concurrent.ConcurrentHashMap; -public class DefaultCodecRegistry implements CodecRegistry { - +/** + * The default codec registry for Fauna serialization and deserialization. + *

+ * This registry provides pre-defined codecs for common data types, enabling + * serialization to and deserialization from FQL. + *

+ */ +public final class DefaultCodecRegistry implements CodecRegistry { + + /** + * Singleton instance of the {@code DefaultCodecRegistry} for global access. + */ public static final CodecRegistry SINGLETON = new DefaultCodecRegistry(); + private final ConcurrentHashMap> codecs; + /** + * Initializes a new instance of {@code DefaultCodecRegistry} with predefined codecs + * for commonly used data types. + */ public DefaultCodecRegistry() { codecs = new ConcurrentHashMap<>(); - codecs.put(CodecRegistryKey.from(String.class), StringCodec.singleton); + codecs.put(CodecRegistryKey.from(String.class), StringCodec.SINGLETON); - codecs.put(CodecRegistryKey.from(byte[].class), ByteArrayCodec.singleton); + codecs.put(CodecRegistryKey.from(byte[].class), ByteArrayCodec.SINGLETON); - codecs.put(CodecRegistryKey.from(boolean.class), BoolCodec.singleton); - codecs.put(CodecRegistryKey.from(Boolean.class), BoolCodec.singleton); + codecs.put(CodecRegistryKey.from(boolean.class), BoolCodec.SINGLETON); + codecs.put(CodecRegistryKey.from(Boolean.class), BoolCodec.SINGLETON); - codecs.put(CodecRegistryKey.from(char.class), CharCodec.singleton); - codecs.put(CodecRegistryKey.from(Character.class), CharCodec.singleton); + codecs.put(CodecRegistryKey.from(char.class), CharCodec.SINGLETON); + codecs.put(CodecRegistryKey.from(Character.class), CharCodec.SINGLETON); - codecs.put(CodecRegistryKey.from(byte.class), ByteCodec.singleton); - codecs.put(CodecRegistryKey.from(Byte.class), ByteCodec.singleton); + codecs.put(CodecRegistryKey.from(byte.class), ByteCodec.SINGLETON); + codecs.put(CodecRegistryKey.from(Byte.class), ByteCodec.SINGLETON); - codecs.put(CodecRegistryKey.from(short.class), ShortCodec.singleton); - codecs.put(CodecRegistryKey.from(Short.class), ShortCodec.singleton); + codecs.put(CodecRegistryKey.from(short.class), ShortCodec.SINGLETON); + codecs.put(CodecRegistryKey.from(Short.class), ShortCodec.SINGLETON); - codecs.put(CodecRegistryKey.from(Integer.class), IntCodec.singleton); - codecs.put(CodecRegistryKey.from(int.class), IntCodec.singleton); + codecs.put(CodecRegistryKey.from(Integer.class), IntCodec.SINGLETON); + codecs.put(CodecRegistryKey.from(int.class), IntCodec.SINGLETON); - codecs.put(CodecRegistryKey.from(Long.class), LongCodec.singleton); - codecs.put(CodecRegistryKey.from(long.class), LongCodec.singleton); + codecs.put(CodecRegistryKey.from(Long.class), LongCodec.SINGLETON); + codecs.put(CodecRegistryKey.from(long.class), LongCodec.SINGLETON); - codecs.put(CodecRegistryKey.from(Float.class), FloatCodec.singleton); - codecs.put(CodecRegistryKey.from(float.class), FloatCodec.singleton); + codecs.put(CodecRegistryKey.from(Float.class), FloatCodec.SINGLETON); + codecs.put(CodecRegistryKey.from(float.class), FloatCodec.SINGLETON); - codecs.put(CodecRegistryKey.from(Double.class), DoubleCodec.singleton); - codecs.put(CodecRegistryKey.from(double.class), DoubleCodec.singleton); + codecs.put(CodecRegistryKey.from(Double.class), DoubleCodec.SINGLETON); + codecs.put(CodecRegistryKey.from(double.class), DoubleCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Instant.class), InstantCodec.SINGLETON); codecs.put(CodecRegistryKey.from(LocalDate.class), LocalDateCodec.SINGLETON); @@ -54,20 +83,40 @@ public DefaultCodecRegistry() { codecs.put(CodecRegistryKey.from(NamedDocumentRef.class), BaseRefCodec.SINGLETON); } + /** + * Retrieves the codec associated with the specified key, if it exists. + * + * @param key The {@link CodecRegistryKey} representing the data type. + * @param The data type to be encoded or decoded. + * @return The {@link Codec} associated with the key, or {@code null} if not found. + */ @Override - public Codec get(CodecRegistryKey key) { + public Codec get(final CodecRegistryKey key) { @SuppressWarnings("unchecked") var codec = (Codec) codecs.get(key); return codec; } + /** + * Registers a new codec for the specified key in the registry. + * + * @param key The {@link CodecRegistryKey} representing the data type. + * @param codec The {@link Codec} to associate with the key. + * @param The data type to be encoded or decoded. + */ @Override - public void put(CodecRegistryKey key, Codec codec) { + public void put(final CodecRegistryKey key, final Codec codec) { codecs.put(key, codec); } + /** + * Checks if the registry contains a codec for the specified key. + * + * @param key The {@link CodecRegistryKey} representing the data type. + * @return {@code true} if a codec for the key is registered; otherwise, {@code false}. + */ @Override - public boolean contains(CodecRegistryKey key) { + public boolean contains(final CodecRegistryKey key) { return codecs.containsKey(key); } } diff --git a/src/main/java/com/fauna/codec/FaunaTokenType.java b/src/main/java/com/fauna/codec/FaunaTokenType.java index 20094ad0..b7ea68eb 100644 --- a/src/main/java/com/fauna/codec/FaunaTokenType.java +++ b/src/main/java/com/fauna/codec/FaunaTokenType.java @@ -1,60 +1,181 @@ package com.fauna.codec; -import java.io.IOException; +import com.fauna.exception.ClientResponseException; /** * Enumeration representing token types for Fauna serialization. + *

+ * The {@code FaunaTokenType} enum defines various tokens that are used to + * identify different elements and data structures within FQL serialization + * and deserialization processes. + *

*/ public enum FaunaTokenType { NONE, + /** + * A structural token that starts an object. + * Wire representation: { + */ START_OBJECT, + + /** + * A structural token that ends an object. + * Wire representation: } + */ END_OBJECT, + /** + * A structural token that starts an array. + * Wire representation: {@code [} + */ START_ARRAY, + + /** + * A structural token that ends an array. + * Wire representation: {@code ]} + */ END_ARRAY, + /** + * A structural token that starts a page. + * Wire representation: { "@page": + */ START_PAGE, + + /** + * A structural token that ends a page. + * Wire representation: } + */ END_PAGE, + /** + * A structural token that starts a ref. + * Wire representation: { "@ref": + */ START_REF, + + /** + * A structural token that ends a ref. + * Wire representation: } + */ END_REF, + /** + * A structural token that starts a document. + * Wire representation: { "@doc": + */ START_DOCUMENT, + + /** + * A structural token that ends a document. + * Wire representation: } + */ END_DOCUMENT, + /** + * A value token that represents a field of an Fauna object, document, or other structure. + */ FIELD_NAME, + /** + * A value token that represents a Fauna string. + */ STRING, + + /** + * A value token that represents a Fauna base64-encoded byte sequence. + */ BYTES, + /** + * A value token that represents a Fauna integer. + */ INT, + + /** + * A value token that represents a Fauna long. + */ LONG, + + /** + * A value token that represents a Fauna double. + */ DOUBLE, + /** + * A value token that represents a Fauna date. + */ DATE, + + /** + * A value token that represents a Fauna time. + */ TIME, + /** + * A value token that represents the Fauna boolean {@code true}. + */ TRUE, + + /** + * A value token that represents the Fauna boolean {@code false}. + */ FALSE, + /** + * A value token that represents null. + */ NULL, + /** + * A value token that represents a Fauna Event Source. + */ STREAM, + /** + * A value token that represents a Fauna symbolic object, such as a user collection. + */ MODULE; - public FaunaTokenType getEndToken() throws IOException { + /** + * Returns the corresponding end token for the current start token. + *

+ * For tokens representing the beginning of a structure (e.g., {@code START_OBJECT}), + * this method returns the matching token for the end of that structure + * (e.g., {@code END_OBJECT}). + *

+ * + * @return The end token associated with the current start token. + * @throws ClientResponseException If the current token has no corresponding end token. + */ + public FaunaTokenType getEndToken() { switch (this) { - case START_DOCUMENT: return END_DOCUMENT; - case START_OBJECT: return END_OBJECT; - case START_ARRAY: return END_ARRAY; - case START_PAGE: return END_PAGE; - case START_REF: return END_REF; - default: throw new IllegalStateException("No end token for " + this.name()); + case START_DOCUMENT: + return END_DOCUMENT; + case START_OBJECT: + return END_OBJECT; + case START_ARRAY: + return END_ARRAY; + case START_PAGE: + return END_PAGE; + case START_REF: + return END_REF; + default: + throw new ClientResponseException("No end token for " + this.name()); } } + /** + * Returns the {@link FaunaType} that corresponds to the current {@code FaunaTokenType}. + *

+ * This method maps each token type in {@code FaunaTokenType} to a specific {@code FaunaType}, + * which represents the underlying data type in Fauna's type system. + *

+ * + * @return The {@link FaunaType} associated with the current token type. + * @throws IllegalStateException If the token type does not have an associated {@code FaunaType}. + */ public FaunaType getFaunaType() { switch (this) { case START_OBJECT: diff --git a/src/main/java/com/fauna/codec/FaunaType.java b/src/main/java/com/fauna/codec/FaunaType.java index 7f370d95..555e2106 100644 --- a/src/main/java/com/fauna/codec/FaunaType.java +++ b/src/main/java/com/fauna/codec/FaunaType.java @@ -1,20 +1,95 @@ package com.fauna.codec; +/** + * Enum representing various FQL data types used by Fauna for data storage and retrieval. + * These types provide structured representations for + * encoding and decoding data in FQL queries and responses. + */ public enum FaunaType { + + /** + * Represents an integer value in FQL. + */ Int, + + /** + * Represents a long integer value in FQL. + */ Long, + + /** + * Represents a double-precision floating-point number in FQL. + */ Double, + + /** + * Represents a UTF-8 encoded string in FQL. + */ String, + + /** + * Represents a date without time in FQL. + * Dates are in ISO 8601 format (YYYY-MM-DD). + */ Date, + + /** + * Represents an exact timestamp or time value in FQL. + * Timestamps are in ISO 8601 format. + */ Time, + + /** + * Represents a boolean value in FQL. + */ Boolean, + + /** + * Represents an object in FQL. + */ Object, + + /** + * Represents a reference to a document. + */ Ref, + + /** + * Represents a complete document in FQL. + */ Document, + + /** + * Represents an array (or list) of values in FQL. + * Arrays are ordered collections of elements and can contain multiple data types. + */ Array, + + /** + * Represents binary data encoded in Base64 within FQL. + * Used for storing raw bytes of data. + */ Bytes, + + /** + * Represents a null value in FQL, denoting the absence of a value. + */ Null, + + /** + * Represents an event source in FQL. + * Event sources are used to track events as Event Feeds or Event Streams. + */ Stream, + + /** + * Represents a module in FQL, which serves as a symbolic object + * with associated methods + */ Module, + + /** + * Represents a pageable Set in FQL. + */ Set } diff --git a/src/main/java/com/fauna/codec/Generic.java b/src/main/java/com/fauna/codec/Generic.java index a10787fb..83f39d02 100644 --- a/src/main/java/com/fauna/codec/Generic.java +++ b/src/main/java/com/fauna/codec/Generic.java @@ -1,27 +1,70 @@ package com.fauna.codec; /** - * A helper class for static access to parameterized generics for deserialization. + * A helper class for providing static access to parameterized generic types, aiding in + * deserialization by circumventing type erasure. */ -public class Generic { +public final class Generic { - public static ListOf listOf(Class elementClass) { + /** + * Private constructor to prevent instantiation. + */ + private Generic() { + } + + /** + * Creates a {@link ListOf} instance for the specified element type. + * + * @param elementClass The class of the elements contained in the list. + * @param The type of elements in the list. + * @return A {@link ListOf} instance with the specified element type. + */ + public static ListOf listOf(final Class elementClass) { return new ListOf<>(elementClass); } - public static MapOf mapOf(Class valueClass) { + /** + * Creates a {@link MapOf} instance for a map with {@link String} keys and the specified value type. + * + * @param valueClass The class of the map's values. + * @param The type of keys in the map (constrained to {@link String}). + * @param The type of values in the map. + * @return A {@link MapOf} instance with {@link String} keys and the specified value type. + */ + public static MapOf mapOf(final Class valueClass) { return new MapOf<>(valueClass); } - public static PageOf pageOf(Class valueClass) { + /** + * Creates a {@link PageOf} instance for the specified element type. + * + * @param valueClass The class of the elements contained in the page. + * @param The type of elements in the page. + * @return A {@link PageOf} instance with the specified element type. + */ + public static PageOf pageOf(final Class valueClass) { return new PageOf<>(valueClass); } - public static OptionalOf optionalOf(Class valueClass) { + /** + * Creates an {@link OptionalOf} instance for the specified element type. + * + * @param valueClass The class of the elements contained in the optional. + * @param The type of the element in the optional. + * @return An {@link OptionalOf} instance with the specified element type. + */ + public static OptionalOf optionalOf(final Class valueClass) { return new OptionalOf<>(valueClass); } - public static NullableDocumentOf nullableDocumentOf(Class valueClass) { + /** + * Creates a {@link NullableDocumentOf} instance for the specified element type. + * + * @param valueClass The class of the elements contained in the nullable document. + * @param The type of the element in the nullable document. + * @return A {@link NullableDocumentOf} instance with the specified element type. + */ + public static NullableDocumentOf nullableDocumentOf(final Class valueClass) { return new NullableDocumentOf<>(valueClass); } } diff --git a/src/main/java/com/fauna/codec/ListOf.java b/src/main/java/com/fauna/codec/ListOf.java index 2af95f4e..da398241 100644 --- a/src/main/java/com/fauna/codec/ListOf.java +++ b/src/main/java/com/fauna/codec/ListOf.java @@ -3,14 +3,20 @@ import java.lang.reflect.Type; import java.util.List; - /** - * ListOf stores the generic parameter class to evade type erasure during deserialization. - * @param The element class of the list. + * Represents a {@link List} with a specified element type, allowing for retention of the + * generic type {@code E} during deserialization by circumventing type erasure. + * + * @param The type of elements in the list. */ -public class ListOf extends ParameterizedOf> { +public final class ListOf extends ParameterizedOf> { - public ListOf(Class elementClass) { + /** + * Constructs a {@code ListOf} instance for the specified element type. + * + * @param elementClass The class of the elements contained in the list. + */ + public ListOf(final Class elementClass) { super(List.class, new Type[]{elementClass}); } } diff --git a/src/main/java/com/fauna/codec/MapOf.java b/src/main/java/com/fauna/codec/MapOf.java index fc4fb317..f8eb91f0 100644 --- a/src/main/java/com/fauna/codec/MapOf.java +++ b/src/main/java/com/fauna/codec/MapOf.java @@ -4,12 +4,20 @@ import java.util.Map; /** - * MapOf stores the generic parameter class to evade type erasure during decoding. - * @param The value class of the Map. + * Represents a {@link Map} with {@link String} keys and a specified value type, allowing for + * retention of the generic type {@code V} during deserialization by circumventing type erasure. + * + * @param The type of keys maintained by the map (constrained to {@link String}). + * @param The type of mapped values. */ -public class MapOf extends ParameterizedOf> { +public final class MapOf extends ParameterizedOf> { - public MapOf(Class valueClass) { + /** + * Constructs a {@code MapOf} instance for a map with {@link String} keys and the specified value type. + * + * @param valueClass The class of the map's values. + */ + public MapOf(final Class valueClass) { super(Map.class, new Type[]{String.class, valueClass}); } } diff --git a/src/main/java/com/fauna/codec/NullableDocumentOf.java b/src/main/java/com/fauna/codec/NullableDocumentOf.java index b1eef646..270b6467 100644 --- a/src/main/java/com/fauna/codec/NullableDocumentOf.java +++ b/src/main/java/com/fauna/codec/NullableDocumentOf.java @@ -4,13 +4,20 @@ import java.lang.reflect.Type; - /** - * NullableDocumentOf stores the generic parameter class to evade type erasure during deserialization. - * @param The value class of the list. + * Represents a {@link NullableDocument} with a specified value type, allowing for retention + * of the generic type {@code E} during deserialization by circumventing type erasure. + * + * @param The type of the value contained in the {@code NullableDocument}. */ -public class NullableDocumentOf extends ParameterizedOf> { - public NullableDocumentOf(Class valueClass) { +public final class NullableDocumentOf extends ParameterizedOf> { + + /** + * Constructs a {@code NullableDocumentOf} instance for the specified value type. + * + * @param valueClass The class of the value contained in the {@code NullableDocument}. + */ + public NullableDocumentOf(final Class valueClass) { super(NullableDocument.class, new Type[]{valueClass}); } -} \ No newline at end of file +} diff --git a/src/main/java/com/fauna/codec/OptionalOf.java b/src/main/java/com/fauna/codec/OptionalOf.java index e1a68e95..2e2e0fb6 100644 --- a/src/main/java/com/fauna/codec/OptionalOf.java +++ b/src/main/java/com/fauna/codec/OptionalOf.java @@ -3,14 +3,20 @@ import java.lang.reflect.Type; import java.util.Optional; - /** - * OptionalOf stores the generic parameter class to evade type erasure during deserialization. - * @param The element class of the list. + * Represents an {@link Optional} with a specified element type, allowing for retention of the + * generic type {@code V} during deserialization by circumventing type erasure. + * + * @param The element type within the optional. */ -public class OptionalOf extends ParameterizedOf> { +public final class OptionalOf extends ParameterizedOf> { - public OptionalOf(Class valueClass) { + /** + * Constructs an {@code OptionalOf} instance for the specified element type. + * + * @param valueClass The class of the elements contained in the optional. + */ + public OptionalOf(final Class valueClass) { super(Optional.class, new Type[]{valueClass}); } } diff --git a/src/main/java/com/fauna/codec/PageOf.java b/src/main/java/com/fauna/codec/PageOf.java index fa024382..e20eb439 100644 --- a/src/main/java/com/fauna/codec/PageOf.java +++ b/src/main/java/com/fauna/codec/PageOf.java @@ -1,15 +1,23 @@ package com.fauna.codec; import com.fauna.types.Page; + import java.lang.reflect.Type; /** - * PageOf stores the generic parameter class to evade type erasure during deserialization. - * @param The element class of the page. + * Represents a {@link Page} with a specified element type, allowing for retention of the + * generic type {@code V} during deserialization by circumventing type erasure. + * + * @param The element type within the page. */ -public class PageOf extends ParameterizedOf> { +public final class PageOf extends ParameterizedOf> { - public PageOf(Class valueClass) { + /** + * Constructs a {@code PageOf} instance for the specified element type. + * + * @param valueClass The class of the elements contained in the page. + */ + public PageOf(final Class valueClass) { super(Page.class, new Type[]{valueClass}); } -} \ No newline at end of file +} diff --git a/src/main/java/com/fauna/codec/ParameterizedOf.java b/src/main/java/com/fauna/codec/ParameterizedOf.java index 34356a11..f9910416 100644 --- a/src/main/java/com/fauna/codec/ParameterizedOf.java +++ b/src/main/java/com/fauna/codec/ParameterizedOf.java @@ -3,26 +3,53 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -public class ParameterizedOf implements ParameterizedType { +/** + * A utility class that implements {@link ParameterizedType} to represent a type with specified + * type arguments at runtime. + * + * @param The type parameter of the parameterized type. + */ +public class ParameterizedOf implements ParameterizedType { + private final Type rawType; private final Type[] typeArguments; - public ParameterizedOf(Type rawType, Type[] typeArguments) { - + /** + * Constructs a new {@code ParameterizedOf} instance. + * + * @param rawType The raw type (e.g., {@code List.class} for {@code List}). + * @param typeArguments The type arguments (e.g., {@code String.class} for {@code List}). + */ + public ParameterizedOf(final Type rawType, final Type[] typeArguments) { this.rawType = rawType; this.typeArguments = typeArguments; } + /** + * Returns the type arguments for this parameterized type. + * + * @return An array of {@link Type} objects representing the actual type arguments. + */ @Override public Type[] getActualTypeArguments() { return typeArguments; } + /** + * Returns the raw type of this parameterized type. + * + * @return The raw {@link Type} representing the parameterized type. + */ @Override public Type getRawType() { return rawType; } + /** + * Returns the owner type of this parameterized type. + * + * @return {@code null} as this implementation does not support owner types. + */ @Override public Type getOwnerType() { return null; diff --git a/src/main/java/com/fauna/codec/UTF8FaunaGenerator.java b/src/main/java/com/fauna/codec/UTF8FaunaGenerator.java index 174df565..e77b4abe 100644 --- a/src/main/java/com/fauna/codec/UTF8FaunaGenerator.java +++ b/src/main/java/com/fauna/codec/UTF8FaunaGenerator.java @@ -1,6 +1,5 @@ package com.fauna.codec; - import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fauna.exception.CodecException; @@ -16,22 +15,31 @@ import static java.nio.charset.StandardCharsets.UTF_8; -public class UTF8FaunaGenerator implements AutoCloseable { +/** + * A generator for encoding JSON with Fauna-specific tagged values and other data types. + */ +public final class UTF8FaunaGenerator implements AutoCloseable { private final JsonGenerator jsonGenerator; private final ByteArrayOutputStream output; /** - * Initializes a new instance of the FaunaGenerator class with a specified stream. + * Initializes a new instance of the {@code UTF8FaunaGenerator} class. * + * @throws IOException If an error occurs during creation of the JSON generator. */ public UTF8FaunaGenerator() throws IOException { - JsonFactory factory = new JsonFactory(); this.output = new ByteArrayOutputStream(); this.jsonGenerator = factory.createGenerator(this.output); } + /** + * Creates a new {@code UTF8FaunaGenerator} instance. + * + * @return A new instance of the {@code UTF8FaunaGenerator}. + * @throws CodecException If an I/O error occurs. + */ public static UTF8FaunaGenerator create() throws CodecException { try { return new UTF8FaunaGenerator(); @@ -53,14 +61,17 @@ public void flush() throws CodecException { } } + /** + * Serializes the current state of the generator's buffer as a UTF-8 encoded string. + * + * @return A string representation of the serialized output. + * @throws CodecException If an I/O error occurs. + */ public String serialize() throws CodecException { this.flush(); return this.output.toString(UTF_8); - } - - /** * Writes the beginning of an object. * @@ -70,7 +81,7 @@ public void writeStartObject() throws CodecException { try { jsonGenerator.writeStartObject(); } catch (IOException exc) { - CodecException.encodingIOException(exc); + throw CodecException.encodingIOException(exc); } } @@ -83,7 +94,7 @@ public void writeEndObject() throws CodecException { try { jsonGenerator.writeEndObject(); } catch (IOException exc) { - CodecException.encodingIOException(exc); + throw CodecException.encodingIOException(exc); } } @@ -117,7 +128,7 @@ public void writeStartArray() throws CodecException { try { jsonGenerator.writeStartArray(); } catch (IOException e) { - throw new RuntimeException(e); + throw CodecException.encodingIOException(e); } } @@ -130,7 +141,7 @@ public void writeEndArray() throws CodecException { try { jsonGenerator.writeEndArray(); } catch (IOException e) { - throw new RuntimeException(e); + throw CodecException.encodingIOException(e); } } @@ -162,7 +173,8 @@ public void writeEndRef() throws CodecException { * @param value The double value to write. * @throws CodecException If an I/O error occurs. */ - public void writeDouble(String fieldName, double value) throws CodecException { + public void writeDouble(final String fieldName, final double value) + throws CodecException { writeFieldName(fieldName); writeDoubleValue(value); } @@ -174,7 +186,7 @@ public void writeDouble(String fieldName, double value) throws CodecException { * @param value The integer value to write. * @throws CodecException If an I/O error occurs. */ - public void writeInt(String fieldName, int value) throws CodecException { + public void writeInt(final String fieldName, final int value) throws CodecException { writeFieldName(fieldName); writeIntValue(value); } @@ -186,7 +198,7 @@ public void writeInt(String fieldName, int value) throws CodecException { * @param value The long integer value to write. * @throws CodecException If an I/O error occurs. */ - public void writeLong(String fieldName, long value) throws CodecException { + public void writeLong(final String fieldName, final long value) throws CodecException { writeFieldName(fieldName); writeLongValue(value); } @@ -198,7 +210,8 @@ public void writeLong(String fieldName, long value) throws CodecException { * @param value The string value to write. * @throws CodecException If an I/O error occurs. */ - public void writeString(String fieldName, String value) throws CodecException { + public void writeString(final String fieldName, final String value) + throws CodecException { writeFieldName(fieldName); writeStringValue(value); } @@ -207,10 +220,11 @@ public void writeString(String fieldName, String value) throws CodecException { * Writes a date value with a specific field name. * * @param fieldName The name of the field. - * @param value The LocalDateTime value to write. + * @param value The date value to write. * @throws CodecException If an I/O error occurs. */ - public void writeDate(String fieldName, LocalDate value) throws CodecException { + public void writeDate(final String fieldName, final LocalDate value) + throws CodecException { writeFieldName(fieldName); writeDateValue(value); } @@ -219,10 +233,11 @@ public void writeDate(String fieldName, LocalDate value) throws CodecException { * Writes a time value with a specific field name. * * @param fieldName The name of the field. - * @param value The LocalDateTime value to write. + * @param value The time value to write. * @throws CodecException If an I/O error occurs. */ - public void writeTime(String fieldName, Instant value) throws CodecException { + public void writeTime(final String fieldName, final Instant value) + throws CodecException { writeFieldName(fieldName); writeTimeValue(value); } @@ -234,7 +249,8 @@ public void writeTime(String fieldName, Instant value) throws CodecException { * @param value The boolean value to write. * @throws CodecException If an I/O error occurs. */ - public void writeBoolean(String fieldName, boolean value) throws CodecException { + public void writeBoolean(final String fieldName, final boolean value) + throws CodecException { writeFieldName(fieldName); writeBooleanValue(value); } @@ -245,7 +261,7 @@ public void writeBoolean(String fieldName, boolean value) throws CodecException * @param fieldName The name of the field. * @throws CodecException If an I/O error occurs. */ - public void writeNull(String fieldName) throws CodecException { + public void writeNull(final String fieldName) throws CodecException { writeFieldName(fieldName); writeNullValue(); } @@ -257,7 +273,8 @@ public void writeNull(String fieldName) throws CodecException { * @param value The Module value to write. * @throws CodecException If an I/O error occurs. */ - public void writeModule(String fieldName, Module value) throws CodecException { + public void writeModule(final String fieldName, final Module value) + throws CodecException { writeFieldName(fieldName); writeModuleValue(value); } @@ -268,11 +285,11 @@ public void writeModule(String fieldName, Module value) throws CodecException { * @param value The name of the field. * @throws CodecException If an I/O error occurs. */ - public void writeFieldName(String value) throws CodecException { + public void writeFieldName(final String value) throws CodecException { try { jsonGenerator.writeFieldName(value); } catch (IOException e) { - throw new RuntimeException(e); + throw CodecException.encodingIOException(e); } } @@ -283,22 +300,20 @@ public void writeFieldName(String value) throws CodecException { * @param value The value associated with the tag. * @throws CodecException If an I/O error occurs. */ - public void writeTaggedValue(String tag, String value) throws CodecException { + public void writeTaggedValue(final String tag, final String value) + throws CodecException { writeStartObject(); writeString(tag, value); writeEndObject(); } - public void writeByteArray(byte[] bytes) throws CodecException { - writeTaggedValue("@bytes", Base64.getEncoder().encodeToString(bytes)); - } /** * Writes a double value as a tagged element. * * @param value The double value to write. * @throws CodecException If an I/O error occurs. */ - public void writeDoubleValue(double value) throws CodecException { + public void writeDoubleValue(final double value) throws CodecException { writeTaggedValue("@double", Double.toString(value)); } @@ -308,18 +323,17 @@ public void writeDoubleValue(double value) throws CodecException { * @param value The float value to write as a double. * @throws CodecException If an I/O error occurs. */ - public void writeDoubleValue(float value) throws CodecException { + public void writeDoubleValue(final float value) throws CodecException { writeTaggedValue("@double", Float.toString(value)); } - /** * Writes an integer value as a tagged element. * * @param value The integer value to write. * @throws CodecException If an I/O error occurs. */ - public void writeIntValue(int value) throws CodecException { + public void writeIntValue(final int value) throws CodecException { writeTaggedValue("@int", Integer.toString(value)); } @@ -329,7 +343,7 @@ public void writeIntValue(int value) throws CodecException { * @param value The long integer value to write. * @throws CodecException If an I/O error occurs. */ - public void writeLongValue(long value) throws CodecException { + public void writeLongValue(final long value) throws CodecException { writeTaggedValue("@long", Long.toString(value)); } @@ -339,7 +353,7 @@ public void writeLongValue(long value) throws CodecException { * @param value The string value to write. * @throws CodecException If an I/O error occurs. */ - public void writeStringValue(String value) throws CodecException { + public void writeStringValue(final String value) throws CodecException { try { jsonGenerator.writeString(value); } catch (IOException exc) { @@ -353,7 +367,7 @@ public void writeStringValue(String value) throws CodecException { * @param value The date value to write. * @throws CodecException If an I/O error occurs. */ - public void writeDateValue(LocalDate value) throws CodecException { + public void writeDateValue(final LocalDate value) throws CodecException { String str = value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); writeTaggedValue("@date", str); } @@ -364,7 +378,7 @@ public void writeDateValue(LocalDate value) throws CodecException { * @param value The time value to write. * @throws CodecException If an I/O error occurs. */ - public void writeTimeValue(Instant value) throws CodecException { + public void writeTimeValue(final Instant value) throws CodecException { Instant instant = value.atZone(ZoneOffset.UTC).toInstant(); String formattedTime = instant.toString(); writeTaggedValue("@time", formattedTime); @@ -376,7 +390,7 @@ public void writeTimeValue(Instant value) throws CodecException { * @param value The boolean value to write. * @throws CodecException If an I/O error occurs. */ - public void writeBooleanValue(boolean value) throws CodecException { + public void writeBooleanValue(final boolean value) throws CodecException { try { jsonGenerator.writeBoolean(value); } catch (IOException exc) { @@ -384,7 +398,13 @@ public void writeBooleanValue(boolean value) throws CodecException { } } - public void writeCharValue(Character value) throws CodecException { + /** + * Writes a character value as an integer. + * + * @param value The character value to write. + * @throws CodecException If an I/O error occurs. + */ + public void writeCharValue(final Character value) throws CodecException { writeIntValue(value); } @@ -407,7 +427,7 @@ public void writeNullValue() throws CodecException { * @param value The module value to write. * @throws CodecException If an I/O error occurs. */ - public void writeModuleValue(Module value) throws CodecException { + public void writeModuleValue(final Module value) throws CodecException { writeTaggedValue("@mod", value.getName()); } @@ -417,10 +437,15 @@ public void writeModuleValue(Module value) throws CodecException { * @param value The byte array to write. * @throws CodecException If an I/O error occurs. */ - public void writeBytesValue(byte[] value) throws CodecException { + public void writeBytesValue(final byte[] value) throws CodecException { writeTaggedValue("@bytes", Base64.getEncoder().encodeToString(value)); } + /** + * Closes the generator and the underlying resources. + * + * @throws CodecException If an I/O error occurs during closing. + */ @Override public void close() throws CodecException { try { @@ -431,6 +456,7 @@ public void close() throws CodecException { try { output.close(); } catch (IOException e) { + //noinspection ThrowFromFinallyBlock throw CodecException.encodingIOException(e); } } diff --git a/src/main/java/com/fauna/codec/UTF8FaunaParser.java b/src/main/java/com/fauna/codec/UTF8FaunaParser.java index cae4486c..e3d9a858 100644 --- a/src/main/java/com/fauna/codec/UTF8FaunaParser.java +++ b/src/main/java/com/fauna/codec/UTF8FaunaParser.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonToken; import com.fauna.exception.CodecException; import com.fauna.types.Module; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -19,12 +20,10 @@ import java.util.Set; import java.util.Stack; -import static com.fauna.codec.FaunaTokenType.*; - /** * Represents a reader that provides fast, non-cached, forward-only access to serialized data. */ -public class UTF8FaunaParser { +public final class UTF8FaunaParser { private static final String INT_TAG = "@int"; private static final String LONG_TAG = "@long"; @@ -41,20 +40,42 @@ public class UTF8FaunaParser { private final JsonParser jsonParser; private final Stack tokenStack = new Stack<>(); - private FaunaTokenType currentFaunaTokenType = NONE; + private final Set closers = new HashSet<>(Arrays.asList( + FaunaTokenType.END_OBJECT, + FaunaTokenType.END_PAGE, + FaunaTokenType.END_DOCUMENT, + FaunaTokenType.END_REF, + FaunaTokenType.END_ARRAY + )); + + private FaunaTokenType currentFaunaTokenType = FaunaTokenType.NONE; private FaunaTokenType bufferedFaunaTokenType; + private Object bufferedTokenValue; private String taggedTokenValue; - private enum InternalTokenType { - START_ESCAPED_OBJECT + START_ESCAPED_OBJECT, + START_PAGE_UNMATERIALIZED } - public UTF8FaunaParser(JsonParser jsonParser) { + /** + * Constructs a {@code UTF8FaunaParser} instance with the given JSON parser. + * + * @param jsonParser The {@link JsonParser} used to read the JSON data. + */ + public UTF8FaunaParser(final JsonParser jsonParser) { this.jsonParser = jsonParser; } - public static UTF8FaunaParser fromInputStream(InputStream body) throws CodecException { + /** + * Creates a {@code UTF8FaunaParser} from an {@link InputStream}. + * + * @param body The input stream of JSON data. + * @return A {@code UTF8FaunaParser} instance. + * @throws CodecException if an {@link IOException} occurs while creating the parser. + */ + public static UTF8FaunaParser fromInputStream(final InputStream body) + throws CodecException { JsonFactory factory = new JsonFactory(); try { JsonParser jsonParser = factory.createParser(body); @@ -68,23 +89,30 @@ public static UTF8FaunaParser fromInputStream(InputStream body) throws CodecExce } } + /** + * Creates a {@code UTF8FaunaParser} from a JSON string + * + * @param str The JSON string. + * @return A {@code UTF8FaunaParser} instance. + */ public static UTF8FaunaParser fromString(String str) { - return UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8))); + return UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8))); } + /** + * Retrieves the current Fauna token type. + * + * @return The {@link FaunaTokenType} currently being processed. + */ public FaunaTokenType getCurrentTokenType() { return currentFaunaTokenType; } - private final Set closers = new HashSet<>(Arrays.asList( - END_OBJECT, - END_PAGE, - END_DOCUMENT, - END_REF, - END_ARRAY - )); - - public void skip() throws IOException { + /** + * Skips the current object or array in the JSON data. + */ + public void skip() { switch (getCurrentTokenType()) { case START_OBJECT: case START_ARRAY: @@ -96,7 +124,7 @@ public void skip() throws IOException { } } - private void skipInternal() throws IOException { + private void skipInternal() { int startCount = tokenStack.size(); while (read()) { if (tokenStack.size() < startCount) { @@ -105,6 +133,12 @@ private void skipInternal() throws IOException { } } + /** + * Reads the next token from the JSON parser. + * + * @return {@code true} if there is another token to read, {@code false} if there are no more tokens. + * @throws CodecException if there is an error reading the token. + */ public boolean read() throws CodecException { taggedTokenValue = null; @@ -117,6 +151,8 @@ public boolean read() throws CodecException { return true; } + bufferedTokenValue = null; + if (!advance()) { return false; } @@ -155,7 +191,7 @@ public boolean read() throws CodecException { break; default: throw new CodecException( - "Unhandled JSON token type " + currentToken + "."); + "Unhandled JSON token type " + currentToken + "."); } } else { return false; @@ -166,7 +202,6 @@ public boolean read() throws CodecException { private void handleStartObject() throws CodecException { advanceTrue(); - switch (jsonParser.currentToken()) { case FIELD_NAME: switch (getText()) { @@ -207,7 +242,20 @@ private void handleStartObject() throws CodecException { case SET_TAG: advanceTrue(); currentFaunaTokenType = FaunaTokenType.START_PAGE; - tokenStack.push(FaunaTokenType.START_PAGE); + if (jsonParser.currentToken() == JsonToken.VALUE_STRING) { + bufferedFaunaTokenType = FaunaTokenType.STRING; + + try { + bufferedTokenValue = jsonParser.getValueAsString(); + } catch (IOException e) { + throw new CodecException(e.getMessage(), e); + } + + tokenStack.push( + InternalTokenType.START_PAGE_UNMATERIALIZED); + } else { + tokenStack.push(FaunaTokenType.START_PAGE); + } break; case REF_TAG: advanceTrue(); @@ -228,33 +276,34 @@ private void handleStartObject() throws CodecException { break; default: throw new CodecException( - "Unexpected token following StartObject: " + jsonParser.currentToken()); + "Unexpected token following StartObject: " + jsonParser.currentToken()); } } private void handleEndObject() { Object startToken = tokenStack.pop(); if (startToken.equals(FaunaTokenType.START_DOCUMENT)) { - currentFaunaTokenType = END_DOCUMENT; + currentFaunaTokenType = FaunaTokenType.END_DOCUMENT; advanceTrue(); + } else if (startToken.equals(InternalTokenType.START_PAGE_UNMATERIALIZED)) { + currentFaunaTokenType = FaunaTokenType.END_PAGE; } else if (startToken.equals(FaunaTokenType.START_PAGE)) { - currentFaunaTokenType = END_PAGE; + currentFaunaTokenType = FaunaTokenType.END_PAGE; advanceTrue(); } else if (startToken.equals(FaunaTokenType.START_REF)) { - currentFaunaTokenType = END_REF; + currentFaunaTokenType = FaunaTokenType.END_REF; advanceTrue(); } else if (startToken.equals(InternalTokenType.START_ESCAPED_OBJECT)) { - currentFaunaTokenType = END_OBJECT; + currentFaunaTokenType = FaunaTokenType.END_OBJECT; advanceTrue(); } else if (startToken.equals(FaunaTokenType.START_OBJECT)) { - currentFaunaTokenType = END_OBJECT; + currentFaunaTokenType = FaunaTokenType.END_OBJECT; } else { - throw new CodecException( - "Unexpected token " + startToken + ". This might be a bug."); + throw new CodecException("Unexpected token " + startToken + ". This might be a bug."); } } - private void handleTaggedString(FaunaTokenType token) throws CodecException { + private void handleTaggedString(final FaunaTokenType token) throws CodecException { try { advanceTrue(); currentFaunaTokenType = token; @@ -275,7 +324,8 @@ private String getText() throws CodecException { private void advanceTrue() { if (!advance()) { - throw new CodecException("Unexpected end of underlying JSON reader."); + throw new CodecException( + "Unexpected end of underlying JSON reader."); } } @@ -283,68 +333,108 @@ private boolean advance() { try { return Objects.nonNull(jsonParser.nextToken()); } catch (IOException e) { - throw new CodecException("Failed to advance underlying JSON reader.", e); + throw new CodecException( + "Failed to advance underlying JSON reader.", e); } } - private void validateTaggedType(FaunaTokenType type) { + private void validateTaggedType(final FaunaTokenType type) { if (currentFaunaTokenType != type || taggedTokenValue == null) { throw new IllegalStateException( - "CurrentTokenType is a " + currentFaunaTokenType.toString() + - ", not a " + type.toString() + "."); + "CurrentTokenType is a " + currentFaunaTokenType.toString() + ", not a " + type.toString() + "."); } } - private void validateTaggedTypes(FaunaTokenType... types) { - if (!Arrays.asList(types).contains(currentFaunaTokenType)) + private void validateTaggedTypes(final FaunaTokenType... types) { + if (!Arrays.asList(types).contains(currentFaunaTokenType)) { throw new IllegalStateException( - "CurrentTokenType is a " + currentFaunaTokenType.toString() + - ", not in " + Arrays.toString(types) + "."); + "CurrentTokenType is a " + currentFaunaTokenType.toString() + ", not in " + Arrays.toString(types) + "."); + } } + // Getters for various token types with appropriate validation + + /** + * Retrieves the value as a {@code Character} if the current token type is {@link FaunaTokenType#INT}. + * + * @return The current value as a {@link Character}. + */ public Character getValueAsCharacter() { - validateTaggedType(INT); + validateTaggedType(FaunaTokenType.INT); return (char) Integer.parseInt(taggedTokenValue); } + /** + * Retrieves the current value as a {@link String}. + * + * @return The current value as a {@link String}. + */ public String getValueAsString() { try { + if (bufferedTokenValue != null) { + return bufferedTokenValue.toString(); + } return jsonParser.getValueAsString(); } catch (IOException e) { - throw new CodecException("Error getting the current token as String", e); + throw new CodecException( + "Error getting the current token as String", e); } } + /** + * Retrieves the tagged value as a {@link String}. + * + * @return The tagged value as a {@link String}. + */ public String getTaggedValueAsString() { return taggedTokenValue; } + /** + * Retrieves the value as a byte array if the current token type is {@link FaunaTokenType#BYTES}. + * + * @return The current value as a byte array. + */ public byte[] getValueAsByteArray() { - validateTaggedTypes(BYTES); + validateTaggedTypes(FaunaTokenType.BYTES); return Base64.getDecoder().decode(taggedTokenValue.getBytes()); } + /** + * Retrieves the value as a {@code Byte} if the current token type is {@link FaunaTokenType#INT}. + * + * @return The current value as a {@code Byte}. + */ public Byte getValueAsByte() { - validateTaggedType(INT); + validateTaggedType(FaunaTokenType.INT); try { return Byte.parseByte(taggedTokenValue); } catch (NumberFormatException e) { throw new CodecException("Error getting the current token as Byte", e); } - } + + /** + * Retrieves the value as a {@code Short} if the current token type is {@link FaunaTokenType#INT}. + * + * @return The current value as a {@code Short}. + */ public Short getValueAsShort() { - validateTaggedType(INT); + validateTaggedType(FaunaTokenType.INT); try { return Short.parseShort(taggedTokenValue); } catch (NumberFormatException e) { throw new CodecException("Error getting the current token as Short", e); } - } + /** + * Retrieves the value as an {@code Integer} if the current token type is {@link FaunaTokenType#INT} or {@link FaunaTokenType#LONG}. + * + * @return The current value as an {@code Integer}. + */ public Integer getValueAsInt() { - validateTaggedTypes(INT, FaunaTokenType.LONG); + validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG); try { return Integer.parseInt(taggedTokenValue); } catch (NumberFormatException e) { @@ -352,6 +442,11 @@ public Integer getValueAsInt() { } } + /** + * Retrieves the current value as a {@code Boolean}. + * + * @return The current value as a {@code Boolean}. + */ public Boolean getValueAsBoolean() { try { return jsonParser.getValueAsBoolean(); @@ -360,8 +455,13 @@ public Boolean getValueAsBoolean() { } } + /** + * Retrieves the current value as a {@link LocalDate} if the current token type is {@link FaunaTokenType#DATE}. + * + * @return The current value as a {@link LocalDate}. + */ public LocalDate getValueAsLocalDate() { - validateTaggedType(DATE); + validateTaggedType(FaunaTokenType.DATE); try { return LocalDate.parse(taggedTokenValue); } catch (DateTimeParseException e) { @@ -369,8 +469,13 @@ public LocalDate getValueAsLocalDate() { } } + /** + * Retrieves the current value as an {@link Instant} if the current token type is {@link FaunaTokenType#TIME}. + * + * @return The current value as an {@link Instant}. + */ public Instant getValueAsTime() { - validateTaggedType(TIME); + validateTaggedType(FaunaTokenType.TIME); try { return Instant.parse(taggedTokenValue); } catch (DateTimeParseException e) { @@ -378,8 +483,14 @@ public Instant getValueAsTime() { } } + /** + * Retrieves the value as a {@code Float} if the current token type is + * {@link FaunaTokenType#INT}, {@link FaunaTokenType#LONG}, or {@link FaunaTokenType#DOUBLE}. + * + * @return The current value as a {@code Float}. + */ public Float getValueAsFloat() { - validateTaggedTypes(INT, LONG, DOUBLE); + validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG, FaunaTokenType.DOUBLE); try { return Float.parseFloat(taggedTokenValue); } catch (NumberFormatException e) { @@ -387,8 +498,14 @@ public Float getValueAsFloat() { } } + /** + * Retrieves the value as a {@code Double} if the current token type is {@link FaunaTokenType#INT}, + * {@link FaunaTokenType#LONG}, or {@link FaunaTokenType#DOUBLE}. + * + * @return The current value as a {@code Double}. + */ public Double getValueAsDouble() { - validateTaggedTypes(INT, LONG, DOUBLE); + validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG, FaunaTokenType.DOUBLE); try { return Double.parseDouble(taggedTokenValue); } catch (NumberFormatException e) { @@ -396,8 +513,14 @@ public Double getValueAsDouble() { } } + /** + * Retrieves the value as a {@code Long} if the current token type is + * {@link FaunaTokenType#INT} or {@link FaunaTokenType#LONG}. + * + * @return The current value as a {@code Long}. + */ public Long getValueAsLong() { - validateTaggedTypes(INT, LONG); + validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG); try { return Long.parseLong(taggedTokenValue); } catch (NumberFormatException e) { @@ -405,6 +528,11 @@ public Long getValueAsLong() { } } + /** + * Retrieves the value as a {@link Module} if the current token type is {@link FaunaTokenType#MODULE}. + * + * @return The current value as a {@link Module}. + */ public Module getValueAsModule() { try { return new Module(taggedTokenValue); @@ -412,4 +540,4 @@ public Module getValueAsModule() { throw new CodecException("Error getting the current token as Module", e); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/fauna/codec/codecs/BaseCodec.java b/src/main/java/com/fauna/codec/codecs/BaseCodec.java index a9bfbdb2..d17f3a4f 100644 --- a/src/main/java/com/fauna/codec/codecs/BaseCodec.java +++ b/src/main/java/com/fauna/codec/codecs/BaseCodec.java @@ -10,26 +10,69 @@ import java.util.HashSet; import java.util.Set; +/** + * Abstract base class for implementing codecs to handle encoding and decoding operations for specific types. + * + * @param the type this codec can encode or decode. + */ public abstract class BaseCodec implements Codec { - public static Set TAGS = new HashSet<>(Arrays.asList( - "@int", "@long", "@double", "@date", "@time", "@mod", "@ref", "@doc", "@set", "@object", "@bytes" + /** Set of known tag identifiers for Fauna's tagged data format. */ + public static final Set TAGS = new HashSet<>(Arrays.asList( + "@int", "@long", "@double", "@date", "@time", "@mod", "@ref", + "@doc", "@set", "@object", "@bytes" )); - protected String unexpectedTokenExceptionMessage(FaunaTokenType token) { - return MessageFormat.format("Unexpected token `{0}` decoding with `{1}<{2}>`", token, this.getClass().getSimpleName(), this.getCodecClass().getSimpleName()); + /** + * Returns a formatted message indicating an unexpected token encountered during decoding. + * + * @param token the unexpected token type encountered. + * @return a formatted message string. + */ + protected String unexpectedTokenExceptionMessage(final FaunaTokenType token) { + return MessageFormat.format( + "Unexpected token `{0}` decoding with `{1}<{2}>`", token, + this.getClass().getSimpleName(), + this.getCodecClass().getSimpleName()); } - protected String unsupportedTypeDecodingMessage(FaunaType type, FaunaType[] supportedTypes) { + /** + * Returns a formatted message indicating an unsupported Fauna type encountered during decoding. + * + * @param type the Fauna type encountered. + * @param supportedTypes an array of supported Fauna types for this codec. + * @return a formatted message string. + */ + protected String unsupportedTypeDecodingMessage(final FaunaType type, + final FaunaType[] supportedTypes) { var supportedString = Arrays.toString(supportedTypes); - return MessageFormat.format("Unable to decode `{0}` with `{1}<{2}>`. Supported types for codec are {3}.", type, this.getClass().getSimpleName(), this.getCodecClass().getSimpleName(), supportedString); + return MessageFormat.format( + "Unable to decode `{0}` with `{1}<{2}>`. Supported types for codec are {3}.", + type, this.getClass().getSimpleName(), + this.getCodecClass().getSimpleName(), supportedString); } - protected String unexpectedTypeWhileDecoding(Type type) { - return MessageFormat.format("Unexpected type `{0}` decoding with `{1}<{2}>`", type, this.getClass().getSimpleName(), this.getCodecClass().getSimpleName()); + /** + * Returns a formatted message indicating an unexpected Java type encountered during decoding. + * + * @param type the unexpected Java type encountered. + * @return a formatted message string. + */ + protected String unexpectedTypeWhileDecoding(final Type type) { + return MessageFormat.format( + "Unexpected type `{0}` decoding with `{1}<{2}>`", type, + this.getClass().getSimpleName(), + this.getCodecClass().getSimpleName()); } - protected String unsupportedTypeMessage(Type type){ - return MessageFormat.format("Cannot encode `{0}` with `{1}<{2}>`", type, this.getClass(), this.getCodecClass()); + /** + * Returns a formatted message indicating an unsupported Java type encountered during encoding. + * + * @param type the unsupported Java type encountered. + * @return a formatted message string. + */ + protected String unsupportedTypeMessage(final Type type) { + return MessageFormat.format("Cannot encode `{0}` with `{1}<{2}>`", type, + this.getClass(), this.getCodecClass()); } } diff --git a/src/main/java/com/fauna/codec/codecs/BaseDocumentCodec.java b/src/main/java/com/fauna/codec/codecs/BaseDocumentCodec.java index f878b942..af436a5c 100644 --- a/src/main/java/com/fauna/codec/codecs/BaseDocumentCodec.java +++ b/src/main/java/com/fauna/codec/codecs/BaseDocumentCodec.java @@ -10,33 +10,40 @@ import com.fauna.types.Document; import com.fauna.types.NamedDocument; -import java.io.IOException; - -public class BaseDocumentCodec extends BaseCodec { +/** + * Codec for encoding and decoding FQL {@link BaseDocument} instances. + */ +public final class BaseDocumentCodec extends BaseCodec { private final CodecProvider provider; - public BaseDocumentCodec(CodecProvider provider) { + /** + * Constructs a {@code BaseDocumentCodec} with the specified codec provider. + * + * @param provider the codec provider + */ + public BaseDocumentCodec(final CodecProvider provider) { this.provider = provider; } @Override - public BaseDocument decode(UTF8FaunaParser parser) throws CodecException { + public BaseDocument decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_REF: var o = BaseRefCodec.SINGLETON.decode(parser); - // if we didn't throw a null ref, we can't deal with it throw new CodecException(unexpectedTypeWhileDecoding(o.getClass())); case START_DOCUMENT: return (BaseDocument) decodeInternal(parser); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } - private Object decodeInternal(UTF8FaunaParser parser) throws CodecException { + private Object decodeInternal(final UTF8FaunaParser parser) throws CodecException { var builder = new InternalDocument.Builder(); var valueCodec = provider.get(Object.class); @@ -67,7 +74,7 @@ private Object decodeInternal(UTF8FaunaParser parser) throws CodecException { } @Override - public void encode(UTF8FaunaGenerator gen, BaseDocument obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final BaseDocument obj) throws CodecException { gen.writeStartRef(); if (obj instanceof Document) { diff --git a/src/main/java/com/fauna/codec/codecs/BaseRefCodec.java b/src/main/java/com/fauna/codec/codecs/BaseRefCodec.java index b868b240..ca86eafb 100644 --- a/src/main/java/com/fauna/codec/codecs/BaseRefCodec.java +++ b/src/main/java/com/fauna/codec/codecs/BaseRefCodec.java @@ -5,27 +5,32 @@ import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; -import com.fauna.types.*; +import com.fauna.types.BaseRef; +import com.fauna.types.DocumentRef; +import com.fauna.types.NamedDocumentRef; -import java.io.IOException; - -public class BaseRefCodec extends BaseCodec { +/** + * Codec for encoding and decoding FQL {@link BaseRef} instances. + */ +public final class BaseRefCodec extends BaseCodec { public static final BaseRefCodec SINGLETON = new BaseRefCodec(); @Override - public BaseRef decode(UTF8FaunaParser parser) throws CodecException { + public BaseRef decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_REF: return (BaseRef) decodeInternal(parser); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } - private Object decodeInternal(UTF8FaunaParser parser) throws CodecException { + private Object decodeInternal(final UTF8FaunaParser parser) throws CodecException { var builder = new InternalDocument.Builder(); while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_REF) { @@ -43,6 +48,8 @@ private Object decodeInternal(UTF8FaunaParser parser) throws CodecException { case "cause": builder = builder.withRefField(fieldName, parser); break; + default: + break; } } @@ -50,7 +57,7 @@ private Object decodeInternal(UTF8FaunaParser parser) throws CodecException { } @Override - public void encode(UTF8FaunaGenerator gen, BaseRef obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final BaseRef obj) throws CodecException { gen.writeStartRef(); if (obj instanceof DocumentRef) { diff --git a/src/main/java/com/fauna/codec/codecs/BoolCodec.java b/src/main/java/com/fauna/codec/codecs/BoolCodec.java index 4be548f9..c95843a2 100644 --- a/src/main/java/com/fauna/codec/codecs/BoolCodec.java +++ b/src/main/java/com/fauna/codec/codecs/BoolCodec.java @@ -1,16 +1,19 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class BoolCodec extends BaseCodec { +/** + * Codec for encoding and decoding FQL boolean values. + */ +public final class BoolCodec extends BaseCodec { - public static final BoolCodec singleton = new BoolCodec(); + public static final BoolCodec SINGLETON = new BoolCodec(); @Override - public Boolean decode(UTF8FaunaParser parser) throws CodecException { + public Boolean decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; @@ -18,12 +21,15 @@ public Boolean decode(UTF8FaunaParser parser) throws CodecException { case FALSE: return parser.getValueAsBoolean(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, Boolean obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Boolean obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -38,6 +44,6 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Boolean, FaunaType.Null}; + return new FaunaType[] {FaunaType.Boolean, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/ByteArrayCodec.java b/src/main/java/com/fauna/codec/codecs/ByteArrayCodec.java index 2a600c11..78fa0ce1 100644 --- a/src/main/java/com/fauna/codec/codecs/ByteArrayCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ByteArrayCodec.java @@ -1,28 +1,48 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class ByteArrayCodec extends BaseCodec { +/** + * Codec for encoding and decoding FQL byte arrays. + */ +public final class ByteArrayCodec extends BaseCodec { - public static final ByteArrayCodec singleton = new ByteArrayCodec(); + public static final ByteArrayCodec SINGLETON = new ByteArrayCodec(); + /** + * Decodes a byte array from the parser. + * + * @param parser the parser to read from + * @return the decoded byte array, or null if the token represents a null value + * @throws CodecException if decoding fails due to an unexpected type + */ @Override - public byte[] decode(UTF8FaunaParser parser) throws CodecException { + public byte[] decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case BYTES: return parser.getValueAsByteArray(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } + /** + * Encodes a byte array to the generator. + * + * @param gen the generator to write to + * @param obj the byte array to encode + * @throws CodecException if encoding fails + */ @Override - public void encode(UTF8FaunaGenerator gen, byte[] obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final byte[] obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); return; @@ -31,13 +51,23 @@ public void encode(UTF8FaunaGenerator gen, byte[] obj) throws CodecException { gen.writeBytesValue(obj); } + /** + * Returns the class type this codec supports. + * + * @return byte array class + */ @Override public Class getCodecClass() { return byte[].class; } + /** + * Returns the Fauna types this codec supports. + * + * @return supported Fauna types + */ @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Bytes, FaunaType.Null}; + return new FaunaType[] {FaunaType.Bytes, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/ByteCodec.java b/src/main/java/com/fauna/codec/codecs/ByteCodec.java index 9497053e..b7bcfdb9 100644 --- a/src/main/java/com/fauna/codec/codecs/ByteCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ByteCodec.java @@ -1,28 +1,47 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class ByteCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@code Byte} values in Fauna's tagged data format. + */ +public final class ByteCodec extends BaseCodec { - public static final ByteCodec singleton = new ByteCodec(); + public static final ByteCodec SINGLETON = new ByteCodec(); + /** + * Decodes a {@code Byte} from the parser. + * + * @param parser the parser to read from + * @return the decoded {@code Byte} value, or null if the token represents a null value + * @throws CodecException if decoding fails due to an unexpected type + */ @Override - public Byte decode(UTF8FaunaParser parser) throws CodecException { + public Byte decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: return parser.getValueAsByte(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } + /** + * Encodes a {@code Byte} value to the generator. + * + * @param gen the generator to write to + * @param obj the {@code Byte} value to encode + * @throws CodecException if encoding fails + */ @Override - public void encode(UTF8FaunaGenerator gen, Byte obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Byte obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -30,13 +49,23 @@ public void encode(UTF8FaunaGenerator gen, Byte obj) throws CodecException { } } + /** + * Returns the class type this codec supports. + * + * @return {@code Byte} class + */ @Override public Class getCodecClass() { return Byte.class; } + /** + * Returns the Fauna types this codec supports. + * + * @return supported Fauna types + */ @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Int, FaunaType.Null}; + return new FaunaType[] {FaunaType.Int, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/CharCodec.java b/src/main/java/com/fauna/codec/codecs/CharCodec.java index 41f34e14..5da778a2 100644 --- a/src/main/java/com/fauna/codec/codecs/CharCodec.java +++ b/src/main/java/com/fauna/codec/codecs/CharCodec.java @@ -1,28 +1,48 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class CharCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@code Character} values in Fauna's tagged data format. + */ +public final class CharCodec extends BaseCodec { - public static final CharCodec singleton = new CharCodec(); + public static final CharCodec SINGLETON = new CharCodec(); + /** + * Decodes a {@code Character} from the parser. + * + * @param parser the parser to read from + * @return the decoded {@code Character} value, or null if the token represents a null value + * @throws CodecException if decoding fails due to an unexpected type + */ @Override - public Character decode(UTF8FaunaParser parser) throws CodecException { + public Character decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: return parser.getValueAsCharacter(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } + /** + * Encodes a {@code Character} value to the generator. + * + * @param gen the generator to write to + * @param obj the {@code Character} value to encode + * @throws CodecException if encoding fails + */ @Override - public void encode(UTF8FaunaGenerator gen, Character obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Character obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -30,13 +50,23 @@ public void encode(UTF8FaunaGenerator gen, Character obj) throws CodecException } } + /** + * Returns the class type this codec supports. + * + * @return {@code Character} class + */ @Override public Class getCodecClass() { return Character.class; } + /** + * Returns the Fauna types this codec supports. + * + * @return supported Fauna types + */ @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Int, FaunaType.Null}; + return new FaunaType[] {FaunaType.Int, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/ClassCodec.java b/src/main/java/com/fauna/codec/codecs/ClassCodec.java index 437c5cad..89847c65 100644 --- a/src/main/java/com/fauna/codec/codecs/ClassCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ClassCodec.java @@ -11,14 +11,13 @@ import com.fauna.codec.CodecProvider; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; -import com.fauna.mapping.FieldInfo; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; +import com.fauna.mapping.FieldInfo; import com.fauna.mapping.FieldName; import com.fauna.mapping.FieldType; -import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -30,7 +29,12 @@ import java.util.List; import java.util.Map; -public class ClassCodec extends BaseCodec { +/** + * A codec for encoding and decoding Java classes, handling Fauna-specific annotations and types. + * + * @param The type of the class to encode/decode. + */ +public final class ClassCodec extends BaseCodec { private static final String ID_FIELD = "id"; private static final String NAME_FIELD = "name"; private final Class type; @@ -38,7 +42,13 @@ public class ClassCodec extends BaseCodec { private final Map fieldsByName; private final boolean shouldEscapeObject; - public ClassCodec(Class ty, CodecProvider provider) { + /** + * Constructs a {@code ClassCodec} for a given type, initializing field mappings based on Fauna annotations. + * + * @param ty The class type. + * @param provider The codec provider for resolving codecs for field types. + */ + public ClassCodec(final Class ty, final CodecProvider provider) { this.type = ty; List fieldsList = new ArrayList<>(); @@ -56,7 +66,8 @@ public ClassCodec(Class ty, CodecProvider provider) { FieldType fieldType = getFieldType(field); - var attr = new FaunaFieldImpl(field.getAnnotation(FaunaField.class)); + var attr = + new FaunaFieldImpl(field.getAnnotation(FaunaField.class)); var name = attr.name() != null ? attr.name() : FieldName.canonical(field.getName()); if (byNameMap.containsKey(name)) { @@ -69,22 +80,25 @@ public ClassCodec(Class ty, CodecProvider provider) { // Don't init the codec here because of potential circular references; instead use a provider. if (type instanceof ParameterizedType) { - ParameterizedType pType = (ParameterizedType)type; - info = new FieldInfo(field, name, (Class) pType.getRawType(), pType.getActualTypeArguments(), provider, fieldType); + ParameterizedType pType = (ParameterizedType) type; + info = new FieldInfo(field, name, (Class) pType.getRawType(), + pType.getActualTypeArguments(), provider, fieldType); } else { - info = new FieldInfo(field, name, field.getType(), null, provider, fieldType); + info = new FieldInfo(field, name, field.getType(), null, + provider, fieldType); } fieldsList.add(info); byNameMap.put(info.getName(), info); } - this.shouldEscapeObject = TAGS.stream().anyMatch(byNameMap.keySet()::contains); + this.shouldEscapeObject = + TAGS.stream().anyMatch(byNameMap.keySet()::contains); this.fields = List.copyOf(fieldsList); this.fieldsByName = Map.copyOf(byNameMap); } - private FieldType getFieldType(Field field) { + private FieldType getFieldType(final Field field) { if (field.getAnnotation(FaunaId.class) != null) { var impl = new FaunaIdImpl(field.getAnnotation(FaunaId.class)); if (impl.isClientGenerate()) { @@ -94,13 +108,17 @@ private FieldType getFieldType(Field field) { } } - if (field.getAnnotation(FaunaTs.class) != null) return FieldType.Ts; - if (field.getAnnotation(FaunaColl.class) != null) return FieldType.Coll; + if (field.getAnnotation(FaunaTs.class) != null) { + return FieldType.Ts; + } + if (field.getAnnotation(FaunaColl.class) != null) { + return FieldType.Coll; + } return FieldType.Field; } @Override - public T decode(UTF8FaunaParser parser) throws CodecException { + public T decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; @@ -108,23 +126,29 @@ public T decode(UTF8FaunaParser parser) throws CodecException { case START_DOCUMENT: case START_OBJECT: try { - FaunaTokenType endToken = parser.getCurrentTokenType().getEndToken(); + FaunaTokenType endToken = + parser.getCurrentTokenType().getEndToken(); Object instance = createInstance(); setFields(instance, parser, endToken); @SuppressWarnings("unchecked") T typed = (T) instance; return typed; - } catch (IllegalAccessException | ClassNotFoundException | InvocationTargetException | InstantiationException | - NoSuchMethodException | IOException e) { + } catch (IllegalAccessException + | ClassNotFoundException + | InvocationTargetException + | InstantiationException + | NoSuchMethodException e) { throw new RuntimeException(e); } default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, T obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final T obj) throws CodecException { if (shouldEscapeObject) { gen.writeStartEscapedObject(); } else { @@ -133,7 +157,9 @@ public void encode(UTF8FaunaGenerator gen, T obj) throws CodecException { for (FieldInfo fi : fields) { if (!fi.getName().startsWith("this$")) { var fieldType = fi.getFieldType(); - if (fieldType == FieldType.Coll || fieldType == FieldType.Ts || fieldType == FieldType.ServerGeneratedId) { + if (fieldType == FieldType.Coll + || fieldType == FieldType.Ts + || fieldType == FieldType.ServerGeneratedId) { // never encode coll and ts and server generated IDs continue; } @@ -155,7 +181,8 @@ public void encode(UTF8FaunaGenerator gen, T obj) throws CodecException { Codec codec = fi.getCodec(); codec.encode(gen, value); } catch (IllegalAccessException e) { - throw new CodecException("Error accessing field: " + fi.getName(), + throw new CodecException( + "Error accessing field: " + fi.getName(), e); } } @@ -174,23 +201,29 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Document, FaunaType.Null, FaunaType.Object, FaunaType.Ref}; + return new FaunaType[] {FaunaType.Document, FaunaType.Null, + FaunaType.Object, FaunaType.Ref}; } - private Object createInstance() throws InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException { + private Object createInstance() + throws InvocationTargetException, InstantiationException, + IllegalAccessException, ClassNotFoundException, + NoSuchMethodException { Class clazz = Class.forName(type.getTypeName()); Constructor constructor = clazz.getConstructor(); return constructor.newInstance(); } - private void setFields(Object instance, UTF8FaunaParser parser, - FaunaTokenType endToken) throws IOException, IllegalAccessException { + private void setFields(final Object instance, final UTF8FaunaParser parser, + final FaunaTokenType endToken) + throws IllegalAccessException { InternalDocument.Builder builder = new InternalDocument.Builder(); while (parser.read() && parser.getCurrentTokenType() != endToken) { if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { - throw new CodecException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + throw new CodecException(unexpectedTokenExceptionMessage( + parser.getCurrentTokenType())); } String fieldName = parser.getValueAsString(); @@ -213,8 +246,12 @@ private void setFields(Object instance, UTF8FaunaParser parser, builder.build(); } - private void trySetId(String fieldName, Object instance, UTF8FaunaParser parser) throws IllegalAccessException { - if (parser.getCurrentTokenType() != FaunaTokenType.STRING) return; + private void trySetId(final String fieldName, final Object instance, + final UTF8FaunaParser parser) + throws IllegalAccessException { + if (parser.getCurrentTokenType() != FaunaTokenType.STRING) { + return; + } FieldInfo field = fieldsByName.get(fieldName); if (field != null) { @@ -230,8 +267,12 @@ private void trySetId(String fieldName, Object instance, UTF8FaunaParser parser) } } - private void trySetName(String fieldName,Object instance, UTF8FaunaParser parser) throws IllegalAccessException { - if (parser.getCurrentTokenType() != FaunaTokenType.STRING) return; + private void trySetName(final String fieldName, final Object instance, + final UTF8FaunaParser parser) + throws IllegalAccessException { + if (parser.getCurrentTokenType() != FaunaTokenType.STRING) { + return; + } FieldInfo field = fieldsByName.get(fieldName); if (field != null) { @@ -243,7 +284,9 @@ private void trySetName(String fieldName,Object instance, UTF8FaunaParser parser } } - private void trySetField(String fieldName, Object instance, UTF8FaunaParser parser) throws IOException, IllegalAccessException { + private void trySetField(final String fieldName, final Object instance, + final UTF8FaunaParser parser) + throws IllegalAccessException { FieldInfo field = fieldsByName.get(fieldName); if (field == null) { parser.skip(); diff --git a/src/main/java/com/fauna/codec/codecs/DoubleCodec.java b/src/main/java/com/fauna/codec/codecs/DoubleCodec.java index b8b68168..fd4a8be3 100644 --- a/src/main/java/com/fauna/codec/codecs/DoubleCodec.java +++ b/src/main/java/com/fauna/codec/codecs/DoubleCodec.java @@ -1,16 +1,26 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class DoubleCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link Double} values in Fauna's tagged data format. + */ +public final class DoubleCodec extends BaseCodec { - public static final DoubleCodec singleton = new DoubleCodec(); + public static final DoubleCodec SINGLETON = new DoubleCodec(); + /** + * Decodes a {@code Double} value from the Fauna tagged data format. + * + * @param parser The parser instance for reading Fauna tagged format data. + * @return The decoded {@code Double} value or {@code null} if the token is {@code NULL}. + * @throws CodecException If the token type is unsupported for decoding a {@code Double}. + */ @Override - public Double decode(UTF8FaunaParser parser) throws CodecException { + public Double decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; @@ -19,12 +29,22 @@ public Double decode(UTF8FaunaParser parser) throws CodecException { case DOUBLE: return parser.getValueAsDouble(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } + /** + * Encodes a {@code Double} value to Fauna's tagged data format. + * + * @param gen The generator used to write Fauna tagged format data. + * @param obj The {@code Double} value to encode, or {@code null} to write a {@code NULL} value. + * @throws CodecException If encoding fails. + */ @Override - public void encode(UTF8FaunaGenerator gen, Double obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Double obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -32,13 +52,23 @@ public void encode(UTF8FaunaGenerator gen, Double obj) throws CodecException { } } + /** + * Returns the class of the codec, which is {@code Double}. + * + * @return {@code Double.class}. + */ @Override public Class getCodecClass() { return Double.class; } + /** + * Returns the Fauna types supported by this codec. + * + * @return An array of {@link FaunaType} supported by this codec. + */ @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Double, FaunaType.Int, FaunaType.Long, FaunaType.Null}; + return new FaunaType[] {FaunaType.Double, FaunaType.Int, FaunaType.Long, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/DynamicCodec.java b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java index 0733c51d..90c6c3b2 100644 --- a/src/main/java/com/fauna/codec/codecs/DynamicCodec.java +++ b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java @@ -5,26 +5,37 @@ import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.event.EventSource; import com.fauna.exception.CodecException; -import com.fauna.query.StreamTokenResponse; -import com.fauna.types.*; +import com.fauna.types.Document; +import com.fauna.types.DocumentRef; +import com.fauna.types.Page; import java.util.List; import java.util.Map; -public class DynamicCodec extends BaseCodec { +/** + * Codec for dynamically encoding and decoding various FQL types. + *

+ * This codec adapts to different FQL types by delegating to other codecs as needed. + */ +public final class DynamicCodec extends BaseCodec { private final ListCodec> list = new ListCodec<>(this); private final PageCodec> page = new PageCodec<>(this); private final MapCodec> map = new MapCodec<>(this); private final CodecProvider provider; - public DynamicCodec(CodecProvider provider) { - + /** + * Constructs a {@code DynamicCodec} with the specified {@code CodecProvider}. + * + * @param provider The codec provider used to retrieve codecs. + */ + public DynamicCodec(final CodecProvider provider) { this.provider = provider; } @Override - public Object decode(UTF8FaunaParser parser) throws CodecException { + public Object decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; @@ -41,7 +52,7 @@ public Object decode(UTF8FaunaParser parser) throws CodecException { case START_DOCUMENT: return provider.get(Document.class).decode(parser); case STREAM: - return provider.get(StreamTokenResponse.class).decode(parser); + return provider.get(EventSource.class).decode(parser); case MODULE: return parser.getValueAsModule(); case INT: @@ -59,16 +70,20 @@ public Object decode(UTF8FaunaParser parser) throws CodecException { case TRUE: case FALSE: return parser.getValueAsBoolean(); + default: + break; } - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } @Override @SuppressWarnings("unchecked") - public void encode(UTF8FaunaGenerator gen, Object obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Object obj) + throws CodecException { - // TODO: deal with Object.class loop @SuppressWarnings("rawtypes") Codec codec = provider.get(obj.getClass()); codec.encode(gen, obj); @@ -81,6 +96,11 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Array, FaunaType.Boolean, FaunaType.Bytes, FaunaType.Date, FaunaType.Double, FaunaType.Document, FaunaType.Int, FaunaType.Long, FaunaType.Module, FaunaType.Null, FaunaType.Object, FaunaType.Ref, FaunaType.Set, FaunaType.Stream, FaunaType.String, FaunaType.Time}; + return new FaunaType[] {FaunaType.Array, FaunaType.Boolean, + FaunaType.Bytes, FaunaType.Date, FaunaType.Double, + FaunaType.Document, FaunaType.Int, FaunaType.Long, + FaunaType.Module, FaunaType.Null, FaunaType.Object, + FaunaType.Ref, FaunaType.Set, FaunaType.Stream, + FaunaType.String, FaunaType.Time}; } } diff --git a/src/main/java/com/fauna/codec/codecs/EnumCodec.java b/src/main/java/com/fauna/codec/codecs/EnumCodec.java index 7bf826f1..248318cf 100644 --- a/src/main/java/com/fauna/codec/codecs/EnumCodec.java +++ b/src/main/java/com/fauna/codec/codecs/EnumCodec.java @@ -5,32 +5,44 @@ import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; -public class EnumCodec extends BaseCodec { +/** + * Codec for encoding and decoding Java Enum types in the Fauna tagged data format. + * + * @param The type of the enum. + */ +public final class EnumCodec extends BaseCodec { private final Class enumType; - public EnumCodec(Class enumType) { + /** + * Constructs an {@code EnumCodec} for the specified enum type. + * + * @param enumType The enum class to be encoded and decoded. + */ + public EnumCodec(final Class enumType) { this.enumType = enumType; } @Override - public T decode(UTF8FaunaParser parser) throws CodecException { + public T decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case STRING: - //noinspection unchecked + //noinspection unchecked,rawtypes return (T) Enum.valueOf((Class) enumType, parser.getValueAsString()); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, T obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final T obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { - gen.writeStringValue(((Enum) obj).name()); + gen.writeStringValue(((Enum) obj).name()); } } @@ -41,6 +53,6 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Null, FaunaType.String}; + return new FaunaType[] {FaunaType.Null, FaunaType.String}; } } diff --git a/src/main/java/com/fauna/codec/codecs/EventSourceCodec.java b/src/main/java/com/fauna/codec/codecs/EventSourceCodec.java new file mode 100644 index 00000000..a5fd7d55 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/EventSourceCodec.java @@ -0,0 +1,42 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.FaunaTokenType; +import com.fauna.codec.FaunaType; +import com.fauna.codec.UTF8FaunaGenerator; +import com.fauna.codec.UTF8FaunaParser; +import com.fauna.event.EventSource; +import com.fauna.exception.CodecException; + +/** + * Codec for encoding and decoding {@link EventSource} instances in the Fauna tagged format. + */ +public final class EventSourceCodec extends BaseCodec { + + @Override + public EventSource decode(final UTF8FaunaParser parser) + throws CodecException { + if (parser.getCurrentTokenType() == FaunaTokenType.STREAM) { + return new EventSource(parser.getTaggedValueAsString()); + } else { + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); + } + } + + @Override + public void encode(final UTF8FaunaGenerator gen, final EventSource obj) + throws CodecException { + throw new CodecException("Cannot encode EventSource"); + } + + @Override + public Class getCodecClass() { + return EventSource.class; + } + + @Override + public FaunaType[] getSupportedTypes() { + return new FaunaType[] {FaunaType.Stream}; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/FloatCodec.java b/src/main/java/com/fauna/codec/codecs/FloatCodec.java index ac910619..a0b364d9 100644 --- a/src/main/java/com/fauna/codec/codecs/FloatCodec.java +++ b/src/main/java/com/fauna/codec/codecs/FloatCodec.java @@ -1,16 +1,19 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class FloatCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link Float} values in the Fauna tagged data format. + */ +public final class FloatCodec extends BaseCodec { - public static final FloatCodec singleton = new FloatCodec(); + public static final FloatCodec SINGLETON = new FloatCodec(); @Override - public Float decode(UTF8FaunaParser parser) throws CodecException { + public Float decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; @@ -19,12 +22,15 @@ public Float decode(UTF8FaunaParser parser) throws CodecException { case DOUBLE: return parser.getValueAsFloat(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, Float obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Float obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -39,6 +45,7 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Double, FaunaType.Int, FaunaType.Long, FaunaType.Null}; + return new FaunaType[] {FaunaType.Double, FaunaType.Int, FaunaType.Long, + FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/InstantCodec.java b/src/main/java/com/fauna/codec/codecs/InstantCodec.java index e9c24a3e..28552d7f 100644 --- a/src/main/java/com/fauna/codec/codecs/InstantCodec.java +++ b/src/main/java/com/fauna/codec/codecs/InstantCodec.java @@ -1,30 +1,35 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; import java.time.Instant; -public class InstantCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link Instant} values in Fauna's tagged data format. + */ +public final class InstantCodec extends BaseCodec { public static final InstantCodec SINGLETON = new InstantCodec(); @Override - public Instant decode(UTF8FaunaParser parser) throws CodecException { + public Instant decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case TIME: return parser.getValueAsTime(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, Instant obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Instant obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -39,6 +44,6 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Null, FaunaType.Time}; + return new FaunaType[] {FaunaType.Null, FaunaType.Time}; } } diff --git a/src/main/java/com/fauna/codec/codecs/IntCodec.java b/src/main/java/com/fauna/codec/codecs/IntCodec.java index 4e8a3368..2be10770 100644 --- a/src/main/java/com/fauna/codec/codecs/IntCodec.java +++ b/src/main/java/com/fauna/codec/codecs/IntCodec.java @@ -1,16 +1,19 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class IntCodec extends BaseCodec { +/** + * Codec for encoding and decoding Integer values. + */ +public final class IntCodec extends BaseCodec { - public static final IntCodec singleton = new IntCodec(); + public static final IntCodec SINGLETON = new IntCodec(); @Override - public Integer decode(UTF8FaunaParser parser) throws CodecException { + public Integer decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; @@ -18,12 +21,15 @@ public Integer decode(UTF8FaunaParser parser) throws CodecException { case LONG: return parser.getValueAsInt(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, Integer obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Integer obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -38,6 +44,6 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Int, FaunaType.Long, FaunaType.Null}; + return new FaunaType[] {FaunaType.Int, FaunaType.Long, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/InternalDocument.java b/src/main/java/com/fauna/codec/codecs/InternalDocument.java index a6eef95c..66fd8e1e 100644 --- a/src/main/java/com/fauna/codec/codecs/InternalDocument.java +++ b/src/main/java/com/fauna/codec/codecs/InternalDocument.java @@ -1,10 +1,13 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaTokenType; -import com.fauna.exception.NullDocumentException; import com.fauna.codec.UTF8FaunaParser; -import com.fauna.types.*; +import com.fauna.exception.NullDocumentException; +import com.fauna.types.Document; +import com.fauna.types.DocumentRef; import com.fauna.types.Module; +import com.fauna.types.NamedDocument; +import com.fauna.types.NamedDocumentRef; import java.time.Instant; import java.util.HashMap; @@ -12,6 +15,9 @@ class InternalDocument { + /** + * Builder class for constructing internal document representations. + */ static class Builder { private String id = null; @@ -21,13 +27,26 @@ static class Builder { private String cause = null; private Instant ts = null; private final Map data = new HashMap<>(); - private boolean throwIfNotExists = true; + /** + * Adds a data field to the document. + * + * @param key The field name. + * @param value The field value. + * @return This builder. + */ InternalDocument.Builder withDataField(String key, Object value) { data.put(key, value); return this; } + /** + * Adds document-specific fields such as id, name, collection, and timestamp. + * + * @param fieldName The field name. + * @param parser The parser used to read values. + * @return This builder. + */ InternalDocument.Builder withDocField(String fieldName, UTF8FaunaParser parser) { switch (fieldName) { case "id": @@ -53,6 +72,13 @@ InternalDocument.Builder withDocField(String fieldName, UTF8FaunaParser parser) return this; } + /** + * Adds reference-specific fields like id, name, collection, exists, and cause. + * + * @param fieldName The field name. + * @param parser The parser used to read values. + * @return This builder. + */ InternalDocument.Builder withRefField(String fieldName, UTF8FaunaParser parser) { switch (fieldName) { case "id": @@ -84,8 +110,14 @@ InternalDocument.Builder withRefField(String fieldName, UTF8FaunaParser parser) return this; } + /** + * Builds and returns the constructed document or reference object. + * + * @return The constructed document or reference object. + * @throws NullDocumentException If the document is marked as "exists: false" but lacks an id or name. + */ Object build() { - if (exists != null && !exists && throwIfNotExists) { + if (exists != null && !exists) { throw new NullDocumentException(id != null ? id : name, coll, cause); } @@ -108,7 +140,6 @@ Object build() { return new NamedDocumentRef(name, coll); } - // We got something we don't know how to handle, so just return it. if (id != null) { data.put("id", id); } diff --git a/src/main/java/com/fauna/codec/codecs/ListCodec.java b/src/main/java/com/fauna/codec/codecs/ListCodec.java index 719ba476..65e10a8a 100644 --- a/src/main/java/com/fauna/codec/codecs/ListCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ListCodec.java @@ -3,30 +3,42 @@ import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; import java.util.ArrayList; import java.util.List; -public class ListCodec> extends BaseCodec { +/** + * A codec for encoding and decoding lists of elements in Fauna's tagged data format. + * + * @param The type of elements in the list. + * @param The type of the list that will hold the elements. + */ +public final class ListCodec> extends BaseCodec { private final Codec elementCodec; - public ListCodec(Codec elementCodec) { + /** + * Creates a codec for encoding and decoding lists of elements. + * + * @param elementCodec The codec used to encode/decode elements of the list. + */ + public ListCodec(final Codec elementCodec) { this.elementCodec = elementCodec; } @Override - public L decode(UTF8FaunaParser parser) throws CodecException { + public L decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_ARRAY: List list = new ArrayList<>(); - while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_ARRAY) { + while (parser.read() && parser.getCurrentTokenType() != + FaunaTokenType.END_ARRAY) { E value = elementCodec.decode(parser); list.add(value); } @@ -34,12 +46,14 @@ public L decode(UTF8FaunaParser parser) throws CodecException { var typed = (L) list; return typed; default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, L obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj == null) { gen.writeNullValue(); return; @@ -60,6 +74,6 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Array, FaunaType.Null}; + return new FaunaType[] {FaunaType.Array, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/LocalDateCodec.java b/src/main/java/com/fauna/codec/codecs/LocalDateCodec.java index e500107a..224f662b 100644 --- a/src/main/java/com/fauna/codec/codecs/LocalDateCodec.java +++ b/src/main/java/com/fauna/codec/codecs/LocalDateCodec.java @@ -1,30 +1,36 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; import java.time.LocalDate; -public class LocalDateCodec extends BaseCodec { +/** + * A codec for encoding and decoding {@link LocalDate} in Fauna's tagged data format. + */ +public final class LocalDateCodec extends BaseCodec { public static final LocalDateCodec SINGLETON = new LocalDateCodec(); @Override - public LocalDate decode(UTF8FaunaParser parser) throws CodecException { + public LocalDate decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case DATE: return parser.getValueAsLocalDate(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, LocalDate obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final LocalDate obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -39,6 +45,6 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Date, FaunaType.Null}; + return new FaunaType[] {FaunaType.Date, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/LongCodec.java b/src/main/java/com/fauna/codec/codecs/LongCodec.java index 57d347b1..c4f7293a 100644 --- a/src/main/java/com/fauna/codec/codecs/LongCodec.java +++ b/src/main/java/com/fauna/codec/codecs/LongCodec.java @@ -1,16 +1,19 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class LongCodec extends BaseCodec { +/** + * A codec for encoding and decoding {@link Long} values in Fauna's tagged data format. + */ +public final class LongCodec extends BaseCodec { - public static final LongCodec singleton = new LongCodec(); + public static final LongCodec SINGLETON = new LongCodec(); @Override - public Long decode(UTF8FaunaParser parser) throws CodecException { + public Long decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; @@ -18,12 +21,14 @@ public Long decode(UTF8FaunaParser parser) throws CodecException { case LONG: return parser.getValueAsLong(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, Long obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Long obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -38,6 +43,6 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Int, FaunaType.Long, FaunaType.Null}; + return new FaunaType[] {FaunaType.Int, FaunaType.Long, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/MapCodec.java b/src/main/java/com/fauna/codec/codecs/MapCodec.java index 4cf913a3..98cb2bc3 100644 --- a/src/main/java/com/fauna/codec/codecs/MapCodec.java +++ b/src/main/java/com/fauna/codec/codecs/MapCodec.java @@ -3,32 +3,51 @@ import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; import java.util.HashMap; import java.util.Map; -public class MapCodec> extends BaseCodec { +/** + * A codec for encoding and decoding {@link Map} values in Fauna's tagged data format. + *

+ * This class handles encoding and decoding of maps, where the keys are strings and the values are of a generic + * type {@code V}. + *

+ * + * @param The type of the values in the map. + * @param The type of the map. + */ +public final class MapCodec> extends BaseCodec { private final Codec valueCodec; - public MapCodec(Codec valueCodec) { + /** + * Constructs a {@code MapCodec} with the specified {@code Codec}. + * + * @param valueCodec The codec to use for the value. + */ + public MapCodec(final Codec valueCodec) { this.valueCodec = valueCodec; } @Override - public L decode(UTF8FaunaParser parser) throws CodecException { + public L decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_OBJECT: Map map = new HashMap<>(); - while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_OBJECT) { - if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { - throw new CodecException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + while (parser.read() && parser.getCurrentTokenType() != + FaunaTokenType.END_OBJECT) { + if (parser.getCurrentTokenType() != + FaunaTokenType.FIELD_NAME) { + throw new CodecException( + unexpectedTokenExceptionMessage( + parser.getCurrentTokenType())); } String fieldName = parser.getValueAsString(); @@ -41,12 +60,14 @@ public L decode(UTF8FaunaParser parser) throws CodecException { L typed = (L) map; return typed; default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, L obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj == null) { gen.writeNullValue(); return; @@ -79,6 +100,6 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Null, FaunaType.Object}; + return new FaunaType[] {FaunaType.Null, FaunaType.Object}; } } diff --git a/src/main/java/com/fauna/codec/codecs/ModuleCodec.java b/src/main/java/com/fauna/codec/codecs/ModuleCodec.java index 86262728..50f78cfa 100644 --- a/src/main/java/com/fauna/codec/codecs/ModuleCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ModuleCodec.java @@ -1,29 +1,35 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; import com.fauna.types.Module; -public class ModuleCodec extends BaseCodec { +/** + * A codec for encoding and decoding {@link Module} in Fauna's tagged data format. + */ +public final class ModuleCodec extends BaseCodec { public static final ModuleCodec SINGLETON = new ModuleCodec(); @Override - public Module decode(UTF8FaunaParser parser) throws CodecException { + public Module decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case MODULE: return parser.getValueAsModule(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, Module obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Module obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -38,6 +44,6 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Module, FaunaType.Null}; + return new FaunaType[] {FaunaType.Module, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/NullableDocumentCodec.java b/src/main/java/com/fauna/codec/codecs/NullableDocumentCodec.java index a1651cf9..e6da9921 100644 --- a/src/main/java/com/fauna/codec/codecs/NullableDocumentCodec.java +++ b/src/main/java/com/fauna/codec/codecs/NullableDocumentCodec.java @@ -3,26 +3,37 @@ import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; -import com.fauna.exception.NullDocumentException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; +import com.fauna.exception.NullDocumentException; import com.fauna.types.NonNullDocument; import com.fauna.types.NullDocument; import com.fauna.types.NullableDocument; - -public class NullableDocumentCodec> extends BaseCodec { +/** + * Codec for encoding and decoding NullableDocument types. + * + * @param The type of the value in the document. + * @param The type of the document (NullableDocument). + */ +public final class NullableDocumentCodec> + extends BaseCodec { private final Codec valueCodec; - public NullableDocumentCodec(Codec valueCodec) { + /** + * Constructs a {@code NullableDocumentCodec} with the specified {@code Codec}. + * + * @param valueCodec The codec to use for the value. + */ + public NullableDocumentCodec(final Codec valueCodec) { this.valueCodec = valueCodec; } @Override @SuppressWarnings("unchecked") - public L decode(UTF8FaunaParser parser) throws CodecException { + public L decode(final UTF8FaunaParser parser) throws CodecException { if (parser.getCurrentTokenType() == FaunaTokenType.NULL) { return null; } @@ -30,16 +41,19 @@ public L decode(UTF8FaunaParser parser) throws CodecException { try { E decoded = valueCodec.decode(parser); - if (decoded instanceof NullDocument) return (L) decoded; + if (decoded instanceof NullDocument) { + return (L) decoded; + } return (L) new NonNullDocument<>(decoded); } catch (NullDocumentException e) { - return (L) new NullDocument<>(e.getId(), e.getCollection(), e.getNullCause()); + return (L) new NullDocument<>(e.getId(), e.getCollection(), + e.getNullCause()); } } @Override - public void encode(UTF8FaunaGenerator gen, L obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj instanceof NonNullDocument) { @SuppressWarnings("unchecked") NonNullDocument nn = (NonNullDocument) obj; diff --git a/src/main/java/com/fauna/codec/codecs/OptionalCodec.java b/src/main/java/com/fauna/codec/codecs/OptionalCodec.java index 4a2c9b3e..8b7e952e 100644 --- a/src/main/java/com/fauna/codec/codecs/OptionalCodec.java +++ b/src/main/java/com/fauna/codec/codecs/OptionalCodec.java @@ -9,16 +9,27 @@ import java.util.Optional; -public class OptionalCodec> extends BaseCodec { +/** + * Codec for encoding and decoding Optional types. + * + * @param The type of the value inside the Optional. + * @param The type of the Optional (Optional). + */ +public final class OptionalCodec> extends BaseCodec { private final Codec valueCodec; - public OptionalCodec(Codec valueCodec) { + /** + * Constructs a {@code OptionalCodec} with the specified {@code Codec}. + * + * @param valueCodec The codec to use for the value. + */ + public OptionalCodec(final Codec valueCodec) { this.valueCodec = valueCodec; } @Override - public L decode(UTF8FaunaParser parser) throws CodecException { + public L decode(final UTF8FaunaParser parser) throws CodecException { if (parser.getCurrentTokenType() == FaunaTokenType.NULL) { @SuppressWarnings("unchecked") L res = (L) Optional.empty(); @@ -31,7 +42,7 @@ public L decode(UTF8FaunaParser parser) throws CodecException { } @Override - public void encode(UTF8FaunaGenerator gen, L obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj == null || obj.isEmpty()) { gen.writeNullValue(); return; diff --git a/src/main/java/com/fauna/codec/codecs/PageCodec.java b/src/main/java/com/fauna/codec/codecs/PageCodec.java index 818e3f83..9d2c39e9 100644 --- a/src/main/java/com/fauna/codec/codecs/PageCodec.java +++ b/src/main/java/com/fauna/codec/codecs/PageCodec.java @@ -3,26 +3,37 @@ import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; import com.fauna.types.Page; -import java.io.IOException; +import java.util.ArrayList; import java.util.List; -public class PageCodec> extends BaseCodec { +/** + * Codec for encoding and decoding Fauna's paginated results. + * + * @param The type of elements in the page. + * @param The type of the Page (Page). + */ +public final class PageCodec> extends BaseCodec { private final Codec elementCodec; private final Codec> listCodec; - public PageCodec(Codec elementCodec) { + /** + * Constructs a {@code PageCodec} with the specified {@code Codec}. + * + * @param elementCodec The codec to use for elements of the page. + */ + public PageCodec(final Codec elementCodec) { this.elementCodec = elementCodec; this.listCodec = new ListCodec<>(elementCodec); } @Override - public L decode(UTF8FaunaParser parser) throws CodecException { + public L decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; @@ -51,16 +62,19 @@ public L decode(UTF8FaunaParser parser) throws CodecException { // In the event the user requests a Page but the query just returns T return wrapInPage(parser); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, L obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { - throw new CodecException(this.unsupportedTypeMessage(obj.getClass())); + throw new CodecException( + this.unsupportedTypeMessage(obj.getClass())); } } @@ -69,12 +83,21 @@ public Class getCodecClass() { return elementCodec.getCodecClass(); } + private L decodePage(final UTF8FaunaParser parser, final FaunaTokenType endToken) + throws CodecException { + + parser.read(); + if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { + return handleUnmaterialized(parser, endToken); + } else { + return handleMaterialized(parser, endToken); + } + } - private L decodePage(UTF8FaunaParser parser, FaunaTokenType endToken) throws CodecException { + private L handleMaterialized(final UTF8FaunaParser parser, final FaunaTokenType endToken) { List data = null; String after = null; - - while (parser.read() && parser.getCurrentTokenType() != endToken) { + do { String fieldName = parser.getValueAsString(); parser.read(); @@ -85,19 +108,29 @@ private L decodePage(UTF8FaunaParser parser, FaunaTokenType endToken) throws Cod case "after": after = parser.getValueAsString(); break; + default: + break; } - } + } while (parser.read() && parser.getCurrentTokenType() != endToken); + + //noinspection unchecked + return (L) new Page<>(data, after); + } + + private L handleUnmaterialized(final UTF8FaunaParser parser, final FaunaTokenType endToken) { + var after = parser.getValueAsString(); + parser.read(); - if (data == null) { - throw new CodecException("No page data found while deserializing into Page<>"); + if (parser.getCurrentTokenType() != endToken) { + throw new CodecException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); } - @SuppressWarnings("unchecked") - L res = (L) new Page<>(data, after); - return res; + //noinspection unchecked + return (L) new Page<>(new ArrayList<>(), after); + } - private L wrapInPage(UTF8FaunaParser parser) throws CodecException { + private L wrapInPage(final UTF8FaunaParser parser) throws CodecException { E elem = this.elementCodec.decode(parser); @SuppressWarnings("unchecked") L res = (L) new Page<>(List.of(elem), null); @@ -106,6 +139,10 @@ private L wrapInPage(UTF8FaunaParser parser) throws CodecException { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Array, FaunaType.Boolean, FaunaType.Bytes, FaunaType.Date, FaunaType.Double, FaunaType.Document, FaunaType.Int, FaunaType.Long, FaunaType.Module, FaunaType.Null, FaunaType.Object, FaunaType.Ref, FaunaType.Set, FaunaType.String, FaunaType.Time}; + return new FaunaType[] {FaunaType.Array, FaunaType.Boolean, + FaunaType.Bytes, FaunaType.Date, FaunaType.Double, + FaunaType.Document, FaunaType.Int, FaunaType.Long, + FaunaType.Module, FaunaType.Null, FaunaType.Object, + FaunaType.Ref, FaunaType.Set, FaunaType.String, FaunaType.Time}; } } diff --git a/src/main/java/com/fauna/codec/codecs/QueryArrCodec.java b/src/main/java/com/fauna/codec/codecs/QueryArrCodec.java index 4e3280f4..145e570f 100644 --- a/src/main/java/com/fauna/codec/codecs/QueryArrCodec.java +++ b/src/main/java/com/fauna/codec/codecs/QueryArrCodec.java @@ -8,22 +8,32 @@ import com.fauna.exception.CodecException; import com.fauna.query.builder.QueryArr; -public class QueryArrCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link QueryArr} objects. + */ +@SuppressWarnings("rawtypes") +public final class QueryArrCodec extends BaseCodec { private final CodecProvider provider; - public QueryArrCodec(CodecProvider provider) { - + /** + * Creates a new instance of the {@link QueryArrCodec}. + * + * @param provider The codec provider used to retrieve codecs for object types. + */ + public QueryArrCodec(final CodecProvider provider) { this.provider = provider; } @Override - public QueryArr decode(UTF8FaunaParser parser) throws CodecException { - throw new CodecException("Decoding into a QueryFragment is not supported"); + public QueryArr decode(final UTF8FaunaParser parser) throws CodecException { + throw new CodecException( + "Decoding into a QueryFragment is not supported"); } @Override - public void encode(UTF8FaunaGenerator gen, QueryArr obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final QueryArr obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -31,6 +41,7 @@ public void encode(UTF8FaunaGenerator gen, QueryArr obj) throws CodecException { gen.writeFieldName("array"); Object unwrapped = obj.get(); Codec codec = provider.get(unwrapped.getClass()); + //noinspection unchecked codec.encode(gen, unwrapped); gen.writeEndObject(); } diff --git a/src/main/java/com/fauna/codec/codecs/QueryCodec.java b/src/main/java/com/fauna/codec/codecs/QueryCodec.java index d9bd43c4..e820c407 100644 --- a/src/main/java/com/fauna/codec/codecs/QueryCodec.java +++ b/src/main/java/com/fauna/codec/codecs/QueryCodec.java @@ -9,25 +9,38 @@ import com.fauna.query.builder.Query; import com.fauna.query.builder.QueryFragment; -public class QueryCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link Query} objects. + */ +public final class QueryCodec extends BaseCodec { private final CodecProvider provider; - public QueryCodec(CodecProvider provider) { + + /** + * Creates a new instance of the {@link QueryCodec}. + * + * @param provider The codec provider used to retrieve codecs for object types. + */ + public QueryCodec(final CodecProvider provider) { this.provider = provider; } @Override - public Query decode(UTF8FaunaParser parser) throws CodecException { - throw new CodecException("Decoding into a QueryFragment is not supported"); + public Query decode(final UTF8FaunaParser parser) throws CodecException { + throw new CodecException( + "Decoding into a QueryFragment is not supported"); } + @SuppressWarnings("rawtypes") @Override - public void encode(UTF8FaunaGenerator gen, Query obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Query obj) + throws CodecException { gen.writeStartObject(); gen.writeFieldName("fql"); gen.writeStartArray(); for (QueryFragment f : obj.get()) { Codec codec = provider.get(f.getClass()); + //noinspection unchecked codec.encode(gen, f); } gen.writeEndArray(); diff --git a/src/main/java/com/fauna/codec/codecs/QueryLiteralCodec.java b/src/main/java/com/fauna/codec/codecs/QueryLiteralCodec.java index 0ffe67b7..a7afb40a 100644 --- a/src/main/java/com/fauna/codec/codecs/QueryLiteralCodec.java +++ b/src/main/java/com/fauna/codec/codecs/QueryLiteralCodec.java @@ -6,15 +6,27 @@ import com.fauna.exception.CodecException; import com.fauna.query.builder.QueryLiteral; -public class QueryLiteralCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link QueryLiteral} objects. + */ +public final class QueryLiteralCodec extends BaseCodec { + + /** + * Creates a new instance of the {@link QueryLiteralCodec}. + */ + public QueryLiteralCodec() { + // No additional setup required for the QueryLiteralCodec. + } @Override - public QueryLiteral decode(UTF8FaunaParser parser) throws CodecException { - throw new CodecException("Decoding into a QueryFragment is not supported"); + public QueryLiteral decode(final UTF8FaunaParser parser) throws CodecException { + throw new CodecException( + "Decoding into a QueryFragment is not supported"); } @Override - public void encode(UTF8FaunaGenerator gen, QueryLiteral obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final QueryLiteral obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { diff --git a/src/main/java/com/fauna/codec/codecs/QueryObjCodec.java b/src/main/java/com/fauna/codec/codecs/QueryObjCodec.java index 89e9f9da..cbcab0e3 100644 --- a/src/main/java/com/fauna/codec/codecs/QueryObjCodec.java +++ b/src/main/java/com/fauna/codec/codecs/QueryObjCodec.java @@ -8,22 +8,32 @@ import com.fauna.exception.CodecException; import com.fauna.query.builder.QueryObj; -public class QueryObjCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link QueryObj} objects. + */ +@SuppressWarnings("rawtypes") +public final class QueryObjCodec extends BaseCodec { private final CodecProvider provider; - public QueryObjCodec(CodecProvider provider) { - + /** + * Creates a new instance of the {@link QueryObjCodec}. + * + * @param provider The codec provider to retrieve codecs for the underlying object types. + */ + public QueryObjCodec(final CodecProvider provider) { this.provider = provider; } @Override - public QueryObj decode(UTF8FaunaParser parser) throws CodecException { - throw new CodecException("Decoding into a QueryFragment is not supported"); + public QueryObj decode(final UTF8FaunaParser parser) throws CodecException { + throw new CodecException( + "Decoding into a QueryFragment is not supported"); } @Override - public void encode(UTF8FaunaGenerator gen, QueryObj obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final QueryObj obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -31,6 +41,7 @@ public void encode(UTF8FaunaGenerator gen, QueryObj obj) throws CodecException { gen.writeFieldName("object"); Object unwrapped = obj.get(); Codec codec = provider.get(unwrapped.getClass()); + //noinspection unchecked codec.encode(gen, unwrapped); gen.writeEndObject(); } diff --git a/src/main/java/com/fauna/codec/codecs/QueryValCodec.java b/src/main/java/com/fauna/codec/codecs/QueryValCodec.java index 9ae241c7..05915b34 100644 --- a/src/main/java/com/fauna/codec/codecs/QueryValCodec.java +++ b/src/main/java/com/fauna/codec/codecs/QueryValCodec.java @@ -8,22 +8,32 @@ import com.fauna.exception.CodecException; import com.fauna.query.builder.QueryVal; -public class QueryValCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link QueryVal} objects. + */ +@SuppressWarnings("rawtypes") +public final class QueryValCodec extends BaseCodec { private final CodecProvider provider; - public QueryValCodec(CodecProvider provider) { - + /** + * Creates a new instance of the {@link QueryValCodec}. + * + * @param provider The codec provider to retrieve codecs for the underlying object types. + */ + public QueryValCodec(final CodecProvider provider) { this.provider = provider; } @Override - public QueryVal decode(UTF8FaunaParser parser) throws CodecException { - throw new CodecException("Decoding into a QueryFragment is not supported"); + public QueryVal decode(final UTF8FaunaParser parser) throws CodecException { + throw new CodecException( + "Decoding into a QueryFragment is not supported"); } @Override - public void encode(UTF8FaunaGenerator gen, QueryVal obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final QueryVal obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -31,6 +41,7 @@ public void encode(UTF8FaunaGenerator gen, QueryVal obj) throws CodecException { gen.writeFieldName("value"); Object unwrapped = obj.get(); Codec codec = provider.get(unwrapped.getClass()); + //noinspection unchecked codec.encode(gen, unwrapped); gen.writeEndObject(); } diff --git a/src/main/java/com/fauna/codec/codecs/ShortCodec.java b/src/main/java/com/fauna/codec/codecs/ShortCodec.java index f9052b18..390d7252 100644 --- a/src/main/java/com/fauna/codec/codecs/ShortCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ShortCodec.java @@ -1,28 +1,34 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class ShortCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link Short} values. + */ +public final class ShortCodec extends BaseCodec { - public static final ShortCodec singleton = new ShortCodec(); + public static final ShortCodec SINGLETON = new ShortCodec(); @Override - public Short decode(UTF8FaunaParser parser) throws CodecException { + public Short decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: return parser.getValueAsShort(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, Short obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final Short obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -35,9 +41,8 @@ public Class getCodecClass() { return Short.class; } - @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Int, FaunaType.Null}; + return new FaunaType[] {FaunaType.Int, FaunaType.Null}; } } diff --git a/src/main/java/com/fauna/codec/codecs/StreamTokenResponseCodec.java b/src/main/java/com/fauna/codec/codecs/StreamTokenResponseCodec.java deleted file mode 100644 index 5ac0425f..00000000 --- a/src/main/java/com/fauna/codec/codecs/StreamTokenResponseCodec.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.fauna.codec.codecs; - -import com.fauna.codec.FaunaType; -import com.fauna.codec.UTF8FaunaGenerator; -import com.fauna.codec.UTF8FaunaParser; -import com.fauna.codec.FaunaTokenType; -import com.fauna.exception.CodecException; -import com.fauna.query.StreamTokenResponse; - -public class StreamTokenResponseCodec extends BaseCodec { - - @Override - public StreamTokenResponse decode(UTF8FaunaParser parser) throws CodecException { - if (parser.getCurrentTokenType() == FaunaTokenType.STREAM) { - return new StreamTokenResponse(parser.getTaggedValueAsString()); - } else { - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); - } - } - - @Override - public void encode(UTF8FaunaGenerator gen, StreamTokenResponse obj) throws CodecException { - throw new CodecException("Cannot encode StreamTokenResponse"); - - } - - @Override - public Class getCodecClass() { - return StreamTokenResponse.class; - } - - @Override - public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Stream}; - } -} diff --git a/src/main/java/com/fauna/codec/codecs/StringCodec.java b/src/main/java/com/fauna/codec/codecs/StringCodec.java index 91704bf7..e5db37da 100644 --- a/src/main/java/com/fauna/codec/codecs/StringCodec.java +++ b/src/main/java/com/fauna/codec/codecs/StringCodec.java @@ -1,16 +1,19 @@ package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; -import com.fauna.exception.CodecException; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.CodecException; -public class StringCodec extends BaseCodec { +/** + * Codec for encoding and decoding {@link String} values. + */ +public final class StringCodec extends BaseCodec { - public static final StringCodec singleton = new StringCodec(); + public static final StringCodec SINGLETON = new StringCodec(); @Override - public String decode(UTF8FaunaParser parser) throws CodecException { + public String decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; @@ -19,12 +22,15 @@ public String decode(UTF8FaunaParser parser) throws CodecException { case BYTES: return parser.getTaggedValueAsString(); default: - throw new CodecException(this.unsupportedTypeDecodingMessage(parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); + throw new CodecException(this.unsupportedTypeDecodingMessage( + parser.getCurrentTokenType().getFaunaType(), + getSupportedTypes())); } } @Override - public void encode(UTF8FaunaGenerator gen, String obj) throws CodecException { + public void encode(final UTF8FaunaGenerator gen, final String obj) + throws CodecException { if (obj == null) { gen.writeNullValue(); } else { @@ -39,6 +45,7 @@ public Class getCodecClass() { @Override public FaunaType[] getSupportedTypes() { - return new FaunaType[]{FaunaType.Bytes, FaunaType.Null, FaunaType.String}; + return new FaunaType[] {FaunaType.Bytes, FaunaType.Null, + FaunaType.String}; } } diff --git a/src/main/java/com/fauna/codec/codecs/package-info.java b/src/main/java/com/fauna/codec/codecs/package-info.java new file mode 100644 index 00000000..dfae9274 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/package-info.java @@ -0,0 +1,4 @@ +/** + * The {@code com.fauna.codec.codecs} package provides codec implementations. + */ +package com.fauna.codec.codecs; diff --git a/src/main/java/com/fauna/codec/json/PassThroughDeserializer.java b/src/main/java/com/fauna/codec/json/PassThroughDeserializer.java deleted file mode 100644 index 265c572f..00000000 --- a/src/main/java/com/fauna/codec/json/PassThroughDeserializer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.fauna.codec.json; - -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; - -import java.io.IOException; - -public class PassThroughDeserializer extends JsonDeserializer { - @Override - public String deserialize(JsonParser jp, DeserializationContext deserializationContext) throws IOException, JacksonException { - // We can improve performance by building this from tokens instead. - return jp.readValueAsTree().toString(); - } -} diff --git a/src/main/java/com/fauna/codec/json/QueryTagsDeserializer.java b/src/main/java/com/fauna/codec/json/QueryTagsDeserializer.java deleted file mode 100644 index 789d988c..00000000 --- a/src/main/java/com/fauna/codec/json/QueryTagsDeserializer.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.fauna.codec.json; - -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; - -import java.io.IOException; -import java.text.MessageFormat; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -public class QueryTagsDeserializer extends JsonDeserializer> { - @Override - public Map deserialize(JsonParser jp, DeserializationContext deserializationContext) throws IOException, JacksonException { - // We can improve performance by building this from tokens instead. - switch (jp.currentToken()) { - case VALUE_NULL: - return null; - case VALUE_STRING: - var rawString = jp.getValueAsString(); - - if (rawString.isEmpty()) { - return Map.of(); - } - - return Arrays.stream(rawString.split(",")) - .map(queryTag -> queryTag.split("=")) - .filter(parts -> parts.length >= 2) - .collect(Collectors.toMap(t -> t[0], t -> t[1])); - default: - throw new JsonParseException(jp, MessageFormat.format("Unexpected token `{0}` deserializing query tags", jp.currentToken())); - } - } -} diff --git a/src/main/java/com/fauna/codec/package-info.java b/src/main/java/com/fauna/codec/package-info.java new file mode 100644 index 00000000..58905463 Binary files /dev/null and b/src/main/java/com/fauna/codec/package-info.java differ diff --git a/src/main/java/com/fauna/constants/Defaults.java b/src/main/java/com/fauna/constants/Defaults.java new file mode 100644 index 00000000..0e923d56 --- /dev/null +++ b/src/main/java/com/fauna/constants/Defaults.java @@ -0,0 +1,35 @@ +package com.fauna.constants; + +import java.time.Duration; + +/** + * Defines default constants used throughout the Fauna client. + * + *

The {@code Defaults} class includes constants for configuration settings, such as timeouts, + * retry limits, and default secrets, that provide sensible defaults for common client operations.

+ */ +public final class Defaults { + + private Defaults() { + } + + /** + * The buffer duration added to the client timeout to ensure safe execution time. + */ + public static final Duration CLIENT_TIMEOUT_BUFFER = Duration.ofSeconds(5); + + /** + * The default timeout duration for client requests. + */ + public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); + + /** + * The default secret for local Fauna deployments created using the Fauna Docker image. + */ + public static final String LOCAL_FAUNA_SECRET = "secret"; + + /** + * The maximum number of retries allowed for handling transaction contention. + */ + public static final int MAX_CONTENTION_RETRIES = 3; +} diff --git a/src/main/java/com/fauna/constants/ErrorMessages.java b/src/main/java/com/fauna/constants/ErrorMessages.java new file mode 100644 index 00000000..722ecdc3 --- /dev/null +++ b/src/main/java/com/fauna/constants/ErrorMessages.java @@ -0,0 +1,33 @@ +package com.fauna.constants; + +/** + * Defines standard error messages used throughout the Fauna client. + * + *

The {@code ErrorMessages} class centralizes error message constants for common + * operations, allowing for consistent messaging across the client and simplifying maintenance.

+ */ +public final class ErrorMessages { + + private ErrorMessages() { + } + + /** + * Error message indicating a query execution failure. + */ + public static final String QUERY_EXECUTION = "Unable to execute query."; + + /** + * Error message indicating a failure to subscribe to an Event Stream. + */ + public static final String STREAM_SUBSCRIPTION = "Unable to subscribe to stream."; + + /** + * Error message indicating a failure to subscribe to an Event Feed. + */ + public static final String FEED_SUBSCRIPTION = "Unable to subscribe to feed."; + + /** + * Error message indicating a failure to query a page of data. + */ + public static final String QUERY_PAGE = "Unable to query page."; +} diff --git a/src/main/java/com/fauna/constants/ResponseFields.java b/src/main/java/com/fauna/constants/ResponseFields.java index ad4c9feb..f0ee78e6 100644 --- a/src/main/java/com/fauna/constants/ResponseFields.java +++ b/src/main/java/com/fauna/constants/ResponseFields.java @@ -1,35 +1,152 @@ package com.fauna.constants; +/** + * Defines constants for field names in responses returned by the Fauna Core HTTP API. + * + *

The {@code ResponseFields} class centralizes commonly used JSON field names in Fauna responses, + * making it easier to reference them consistently and preventing hard-coded strings throughout the codebase.

+ */ public final class ResponseFields { + private ResponseFields() { + } + // Top-level fields + /** + * Field name for data returned in a response. + */ public static final String DATA_FIELD_NAME = "data"; + + /** + * Field name for the last seen transaction timestamp. + */ public static final String LAST_SEEN_TXN_FIELD_NAME = "txn_ts"; + + /** + * Field name for the static type of the response data. + */ public static final String STATIC_TYPE_FIELD_NAME = "static_type"; + + /** + * Field name for query and event stats in the response. + */ public static final String STATS_FIELD_NAME = "stats"; + + /** + * Field name for the database schema version in the response. + */ public static final String SCHEMA_VERSION_FIELD_NAME = "schema_version"; + + /** + * Field name for the summary information in the response. + */ public static final String SUMMARY_FIELD_NAME = "summary"; + + /** + * Field name for query tags included in the response. + */ public static final String QUERY_TAGS_FIELD_NAME = "query_tags"; + + /** + * Field name for error information in the response. + */ public static final String ERROR_FIELD_NAME = "error"; // "stats" block + /** + * Field name for compute operation statistics. + */ public static final String STATS_COMPUTE_OPS_FIELD_NAME = "compute_ops"; + + /** + * Field name for read operation statistics. + */ public static final String STATS_READ_OPS = "read_ops"; + + /** + * Field name for write operation statistics. + */ public static final String STATS_WRITE_OPS = "write_ops"; + + /** + * Field name for the query runtime in milliseconds. + */ public static final String STATS_QUERY_TIME_MS = "query_time_ms"; + + /** + * Field name for event processing time in milliseconds. + */ public static final String STATS_PROCESSING_TIME_MS = "processing_time_ms"; + + /** + * Field name for transaction contention retries count. + */ public static final String STATS_CONTENTION_RETRIES = "contention_retries"; + + /** + * Field name for data read from storage, in bytes. + */ public static final String STATS_STORAGE_BYTES_READ = "storage_bytes_read"; + + /** + * Field name for data written to storage, in bytes. + */ public static final String STATS_STORAGE_BYTES_WRITE = "storage_bytes_write"; + + /** + * Field name for rate limit hits. + */ public static final String STATS_RATE_LIMITS_HIT = "rate_limits_hit"; // "error" block + /** + * Field name for the error code in the response. + */ public static final String ERROR_CODE_FIELD_NAME = "code"; + + /** + * Field name for the error message in the response. + */ public static final String ERROR_MESSAGE_FIELD_NAME = "message"; + + /** + * Field name for constraint failures in error information. + */ public static final String ERROR_CONSTRAINT_FAILURES_FIELD_NAME = "constraint_failures"; + + /** + * Field name for abort error information in the error response. + */ public static final String ERROR_ABORT_FIELD_NAME = "abort"; - // Stream-related fields + /** + * Field name for the error name in the response. + */ + public static final String ERROR_NAME_FIELD_NAME = "name"; + + /** + * Field name for paths involved in the error. + */ + public static final String ERROR_PATHS_FIELD_NAME = "paths"; + + // Event-related fields + /** + * Field name for the cursor in stream and feed responses. + */ + public static final String CURSOR_FIELD_NAME = "cursor"; + /** + * Field name for the event type in Event Feed and Event Stream responses. + */ public static final String STREAM_TYPE_FIELD_NAME = "type"; - public static final String STREAM_CURSOR_FIELD_NAME = "cursor"; -} \ No newline at end of file + + // Feed-related fields + /** + * Field name for events in Event Feed responses. + */ + public static final String EVENTS_FIELD_NAME = "events"; + + /** + * Field name indicating whether there are more pages in Event Feed responses. + */ + public static final String FEED_HAS_NEXT_FIELD_NAME = "has_next"; +} diff --git a/src/main/java/com/fauna/env/DriverEnvironment.java b/src/main/java/com/fauna/env/DriverEnvironment.java index 8a400c12..f78aa501 100644 --- a/src/main/java/com/fauna/env/DriverEnvironment.java +++ b/src/main/java/com/fauna/env/DriverEnvironment.java @@ -4,6 +4,12 @@ import java.io.InputStream; import java.util.Properties; +/** + * Provides information about the runtime environment of the Fauna driver. + * + *

The {@code DriverEnvironment} class detects the environment, operating system, runtime version, + * and driver version, which can be helpful for diagnostics and compatibility checks.

+ */ public class DriverEnvironment { private final String driverVersion; @@ -11,11 +17,19 @@ public class DriverEnvironment { private final String os; private String runtime; + /** + * Enum representing the supported JVM drivers. + */ public enum JvmDriver { JAVA, SCALA } - public DriverEnvironment(JvmDriver jvmDriver) { + /** + * Constructs a {@code DriverEnvironment} instance with the specified JVM driver type. + * + * @param jvmDriver The {@link JvmDriver} type to identify the runtime (e.g., Java or Scala). + */ + public DriverEnvironment(final JvmDriver jvmDriver) { this.env = getRuntimeEnvironment(); this.os = System.getProperty("os.name") + "-" + System.getProperty("os.version"); this.runtime = String.format("java-%s", System.getProperty("java.version")); @@ -28,7 +42,7 @@ public DriverEnvironment(JvmDriver jvmDriver) { /** * Retrieves the software version from the "version.properties" file. * - * @return A String representing the software version. + * @return A {@code String} representing the software version. * @throws RuntimeException If the "version.properties" file is not found or if there is an issue reading the file. */ public static String getVersion() { @@ -44,8 +58,12 @@ public static String getVersion() { } } + /** + * Determines the runtime environment by checking for the presence of certain environment variables. + * + * @return A {@code String} representing the detected environment (e.g., "Heroku", "AWS Lambda"). + */ private String getRuntimeEnvironment() { - // Checks for various cloud environments based on environment variables if (System.getenv("PATH") != null && System.getenv("PATH").contains(".heroku")) { return "Heroku"; } else if (System.getenv("AWS_LAMBDA_FUNCTION_VERSION") != null) { @@ -56,18 +74,24 @@ private String getRuntimeEnvironment() { return "GCP Compute Instances"; } else if (System.getenv("WEBSITE_FUNCTIONS_AZUREMONITOR_CATEGORIES") != null) { return "Azure Cloud Functions"; - } else if (System.getenv("ORYX_ENV_TYPE") != null && System.getenv("WEBSITE_INSTANCE_ID") != null && - System.getenv("ORYX_ENV_TYPE").equals("AppService")) { + } else if (System.getenv("ORYX_ENV_TYPE") != null + && System.getenv("WEBSITE_INSTANCE_ID") != null + && System.getenv("ORYX_ENV_TYPE").equals("AppService")) { return "Azure Compute"; } else { return "Unknown"; } } + /** + * Returns a string representation of the driver environment, including the driver version, + * runtime, environment, and operating system. + * + * @return A {@code String} representation of this {@code DriverEnvironment}. + */ @Override public String toString() { return String.format("driver=%s; runtime=java-%s; env=%s; os=%s", driverVersion, runtime, env, os).toLowerCase(); } - } diff --git a/src/main/java/com/fauna/env/package-info.java b/src/main/java/com/fauna/env/package-info.java new file mode 100644 index 00000000..8940f6eb --- /dev/null +++ b/src/main/java/com/fauna/env/package-info.java @@ -0,0 +1,18 @@ +/** + * Provides utilities for managing the environment in which the Fauna driver operates. + * + *

This package includes classes that detect and encapsulate runtime information about the + * environment, operating system, JVM version, and driver version. These details can be used for + * diagnostics, compatibility checks, and logging purposes.

+ * + *
    + *
  • {@link com.fauna.env.DriverEnvironment} - Detects and captures information about the runtime + * environment, such as operating system details, Java version, and cloud environment (e.g., + * AWS Lambda, GCP Cloud Functions).
  • + *
+ * + *

The {@code com.fauna.env} package supports gathering environment-related details to help + * understand the conditions under which the driver is operating, which can be useful for + * troubleshooting and optimizing performance across various deployment platforms.

+ */ +package com.fauna.env; diff --git a/src/main/java/com/fauna/event/EventSource.java b/src/main/java/com/fauna/event/EventSource.java new file mode 100644 index 00000000..27e3b1c3 --- /dev/null +++ b/src/main/java/com/fauna/event/EventSource.java @@ -0,0 +1,80 @@ +package com.fauna.event; + +import com.fauna.query.builder.Query; + +import java.util.Objects; + +/** + * Represents an event source. You can consume event sources +as Event Feeds by + * calling {@link com.fauna.client.FaunaClient#feed(EventSource, FeedOptions, Class) FaunaClient.feed()} + * or as Event Streams by calling + * {@link com.fauna.client.FaunaClient#stream(EventSource, StreamOptions, Class) FaunaClient.stream()}. + *

+ * The {@code EventSource} class provides methods for constructing instances from event source tokens and responses + */ +public class EventSource { + private final String token; + + /** + * Constructs a new {@code EventSource} with the specified token. + * + * @param token A {@code String} representing the event source. + */ + public EventSource(final String token) { + this.token = token; + } + + /** + * Retrieves the token for the event source. + * + * @return A {@code String} representing the token. + */ + public String getToken() { + return token; + } + + /** + * Creates an {@code EventSource} from the specified token. + * + * @param token A {@code String} representing the token for the event source. + * @return A new {@code EventSource} instance. + */ + public static EventSource fromToken(final String token) { + return new EventSource(token); + } + /** + * Compares this {@code EventSource} with another object for equality. + * + * @param o The object to compare with. + * @return {@code true} if the specified object is equal to this {@code EventSource}; {@code false} otherwise. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (o == null) { + return false; + } + + if (getClass() != o.getClass()) { + return false; + } + + EventSource c = (EventSource) o; + + return Objects.equals(token, c.token); + } + + /** + * Returns the hash code for this {@code EventSource}. + * + * @return An {@code int} representing the hash code of this object. + */ + @Override + public int hashCode() { + return Objects.hash(token); + } +} diff --git a/src/main/java/com/fauna/event/FaunaEvent.java b/src/main/java/com/fauna/event/FaunaEvent.java new file mode 100644 index 00000000..b55fe245 --- /dev/null +++ b/src/main/java/com/fauna/event/FaunaEvent.java @@ -0,0 +1,304 @@ +package com.fauna.event; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fauna.codec.Codec; +import com.fauna.codec.FaunaTokenType; +import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.ClientResponseException; +import com.fauna.response.ErrorInfo; +import com.fauna.response.QueryStats; + +import java.io.IOException; +import java.util.Optional; + +import static com.fauna.constants.ResponseFields.CURSOR_FIELD_NAME; +import static com.fauna.constants.ResponseFields.DATA_FIELD_NAME; +import static com.fauna.constants.ResponseFields.ERROR_FIELD_NAME; +import static com.fauna.constants.ResponseFields.LAST_SEEN_TXN_FIELD_NAME; +import static com.fauna.constants.ResponseFields.STATS_FIELD_NAME; +import static com.fauna.constants.ResponseFields.STREAM_TYPE_FIELD_NAME; + +/** + * Represents an event emitted in an Event Feed or Event Stream. + * @param The type of data contained in the event. + */ +public final class FaunaEvent { + + /** + * Enum representing possible event types from a Fauna event source. + */ + public enum EventType { + STATUS, ADD, UPDATE, REMOVE, ERROR + } + + private final EventType type; + private final String cursor; + private final Long txnTs; + private final E data; + private final QueryStats stats; + private final ErrorInfo error; + + /** + * Constructs a new {@code FaunaEvent} with the specified properties. + * + * @param type The type of the event. + * @param cursor The cursor for the event. + * @param txnTs The transaction timestamp for the document change that triggered the event. + * @param data The data for the document that triggered the event. + * @param stats The event stats. + * @param error The error information for the event, if any. + */ + public FaunaEvent(final EventType type, final String cursor, final Long txnTs, + final E data, final QueryStats stats, final ErrorInfo error) { + this.type = type; + this.cursor = cursor; + this.txnTs = txnTs; + this.data = data; + this.stats = stats; + this.error = error; + } + + /** + * Retrieves the type of this event. + * + * @return The {@link EventType} of this event. + */ + public EventType getType() { + return type; + } + + /** + * Retrieves the Fauna document data associated with this event. + * + * @return An {@link Optional} containing the event data, or empty if no data is available. + */ + public Optional getData() { + return Optional.ofNullable(data); + } + + /** + * Retrieves the transaction timestamp for the document change that triggered the event. + * + * @return An {@link Optional} containing the transaction timestamp, or empty if not present. + */ + public Optional getTimestamp() { + return Optional.ofNullable(txnTs); + } + + /** + * Retrieves the cursor for this event. + * + * @return A {@code String} representing the cursor. + */ + public String getCursor() { + return cursor; + } + + /** + * Retrieves stats associated with this event. + * + * @return A {@link QueryStats} object representing the statistics. + */ + public QueryStats getStats() { + return stats; + } + + /** + * Retrieves the error information for this event, if any. + * + * @return An {@link ErrorInfo} object containing error details, or {@code null} if no error is present. + */ + public ErrorInfo getError() { + return this.error; + } + + /** + * Builder class for constructing a {@code FaunaEvent} instance. + * + * @param The type of data contained in the event. + */ + public static final class Builder { + private final Codec dataCodec; + private String cursor = null; + private FaunaEvent.EventType eventType = null; + private QueryStats stats = null; + private E data = null; + private Long txnTs = null; + private ErrorInfo errorInfo = null; + + /** + * Constructs a {@code Builder} for building a {@code FaunaEvent}. + * + * @param dataCodec The {@link Codec} used to decode event data. + */ + public Builder(final Codec dataCodec) { + this.dataCodec = dataCodec; + } + + /** + * Sets the cursor for the event. + * + * @param cursor The cursor to set. + * @return This {@code Builder} instance. + */ + public Builder cursor(final String cursor) { + this.cursor = cursor; + return this; + } + + /** + * Sets the event type. + * + * @param eventType The {@link EventType} of the event. + * @return This {@code Builder} instance. + */ + public Builder eventType(final FaunaEvent.EventType eventType) { + this.eventType = eventType; + return this; + } + + /** + * Sets the query statistics for the event. + * + * @param stats The {@link QueryStats} to set. + * @return This {@code Builder} instance. + */ + public Builder stats(final QueryStats stats) { + this.stats = stats; + return this; + } + + /** + * Parses and sets the event data from the given JSON parser. + * + * @param parser The {@link JsonParser} to decode the data from. + * @return This {@code Builder} instance. + */ + public Builder parseData(final JsonParser parser) { + UTF8FaunaParser faunaParser = new UTF8FaunaParser(parser); + if (faunaParser.getCurrentTokenType() == FaunaTokenType.NONE) { + faunaParser.read(); + } + this.data = dataCodec.decode(faunaParser); + return this; + } + + /** + * Sets the transaction timestamp for the event. + * + * @param txnTs The transaction timestamp to set. + * @return This {@code Builder} instance. + */ + public Builder txnTs(final Long txnTs) { + this.txnTs = txnTs; + return this; + } + + /** + * Sets the error information for the event. + * + * @param error The {@link ErrorInfo} containing error details. + * @return This {@code Builder} instance. + */ + public Builder error(final ErrorInfo error) { + this.errorInfo = error; + this.eventType = EventType.ERROR; + return this; + } + + /** + * Builds and returns a {@code FaunaEvent} instance. + * + * @return A new {@code FaunaEvent} instance. + */ + public FaunaEvent build() { + return new FaunaEvent<>(eventType, cursor, txnTs, data, stats, errorInfo); + } + } + + /** + * Creates a new {@code Builder} for constructing a {@code FaunaEvent}. + * + * @param dataCodec The {@link Codec} used to decode event data. + * @param The type of data contained in the event. + * @return A new {@code Builder} instance. + */ + public static Builder builder(final Codec dataCodec) { + return new Builder<>(dataCodec); + } + + /** + * Parses and sets the appropriate field in the builder based on the JSON parser's current field. + * + * @param builder The {@code Builder} being populated. + * @param parser The {@link JsonParser} for reading the field value. + * @param The type of data contained in the event. + * @return The updated {@code Builder} instance. + * @throws IOException If an error occurs while parsing. + */ + private static Builder parseField(final Builder builder, final JsonParser parser) + throws IOException { + String fieldName = parser.getValueAsString(); + switch (fieldName) { + case CURSOR_FIELD_NAME: + return builder.cursor(parser.nextTextValue()); + case DATA_FIELD_NAME: + return builder.parseData(parser); + case STREAM_TYPE_FIELD_NAME: + return builder.eventType(parseEventType(parser)); + case STATS_FIELD_NAME: + return builder.stats(QueryStats.parseStats(parser)); + case LAST_SEEN_TXN_FIELD_NAME: + return builder.txnTs(parser.nextLongValue(0L)); + case ERROR_FIELD_NAME: + return builder.error(ErrorInfo.parse(parser)); + default: + throw new ClientResponseException("Unknown FaunaEvent field: " + fieldName); + } + } + + /** + * Parses the event type from the JSON parser. + * + * @param parser The {@link JsonParser} positioned at the event type field. + * @return The parsed {@link EventType}. + * @throws IOException If an error occurs while parsing. + */ + private static FaunaEvent.EventType parseEventType(final JsonParser parser) + throws IOException { + if (parser.nextToken() == JsonToken.VALUE_STRING) { + String typeString = parser.getText().toUpperCase(); + try { + return FaunaEvent.EventType.valueOf(typeString); + } catch (IllegalArgumentException e) { + throw new ClientResponseException("Invalid event type: " + typeString, e); + } + } else { + throw new ClientResponseException("Event type should be a string, but got a " + + parser.currentToken().asString()); + } + } + + /** + * Parses a {@code FaunaEvent} from the JSON parser using the specified codec. + * + * @param parser The {@link JsonParser} positioned at the start of the event. + * @param dataCodec The {@link Codec} used to decode event data. + * @param The type of data contained in the event. + * @return The parsed {@code FaunaEvent}. + * @throws IOException If an error occurs while parsing. + */ + public static FaunaEvent parse(final JsonParser parser, final Codec dataCodec) + throws IOException { + if (parser.currentToken() == JsonToken.START_OBJECT || parser.nextToken() == JsonToken.START_OBJECT) { + Builder builder = FaunaEvent.builder(dataCodec); + while (parser.nextToken() == JsonToken.FIELD_NAME) { + builder = parseField(builder, parser); + } + return builder.build(); + } else { + throw new ClientResponseException("Invalid event starting with: " + parser.currentToken()); + } + } +} diff --git a/src/main/java/com/fauna/event/FaunaStream.java b/src/main/java/com/fauna/event/FaunaStream.java new file mode 100644 index 00000000..9b9456d7 --- /dev/null +++ b/src/main/java/com/fauna/event/FaunaStream.java @@ -0,0 +1,141 @@ +package com.fauna.event; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fauna.client.StatsCollector; +import com.fauna.codec.Codec; +import com.fauna.codec.DefaultCodecProvider; +import com.fauna.exception.ClientException; +import com.fauna.response.ErrorInfo; +import com.fauna.response.MultiByteBufferInputStream; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.text.MessageFormat; +import java.util.List; +import java.util.concurrent.Flow.Processor; +import java.util.concurrent.Flow.Subscriber; +import java.util.concurrent.Flow.Subscription; +import java.util.concurrent.SubmissionPublisher; + +/** + * A processor for handling and decoding Fauna Event Streams. + *

+ * The {@code FaunaStream} class extends {@link SubmissionPublisher} to process + * incoming ByteBuffers, decode them into {@link FaunaEvent} objects, and forward + * them to subscribers. + * + * @param The type of document data contained in the Fauna events. + */ +public class FaunaStream extends SubmissionPublisher> + implements Processor, FaunaEvent> { + + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + private final Codec dataCodec; + private Subscription subscription; + private Subscriber> eventSubscriber; + private MultiByteBufferInputStream buffer = null; + private final StatsCollector statsCollector; + + /** + * Constructs a {@code FaunaStream} instance with the specified event data type and stats collector. + * + * @param elementClass The class of the event data type. + * @param statsCollector The {@link StatsCollector} to track statistics for events. + */ + public FaunaStream(final Class elementClass, final StatsCollector statsCollector) { + this.statsCollector = statsCollector; + this.dataCodec = DefaultCodecProvider.SINGLETON.get(elementClass); + } + + /** + * Subscribes a single subscriber to this stream. + * + * @param subscriber The {@link Subscriber} to subscribe to this stream. + * @throws ClientException if more than one subscriber attempts to subscribe. + */ + @Override + public void subscribe(final Subscriber> subscriber) { + if (this.eventSubscriber == null) { + this.eventSubscriber = subscriber; + super.subscribe(subscriber); + this.subscription.request(1); + } else { + throw new ClientException("Only one subscriber is supported."); + } + } + + /** + * Handles subscription by setting the subscription and requesting data. + * + * @param subscription The subscription to this stream. + */ + @Override + public void onSubscribe(final Subscription subscription) { + this.subscription = subscription; + } + + /** + * Processes incoming ByteBuffers, decodes them into Fauna events, and submits the events to subscribers. + * + * @param buffers The list of {@link ByteBuffer}s containing encoded event data. + * @throws ClientException if there is an error decoding the stream or processing events. + */ + @Override + public void onNext(final List buffers) { + try { + synchronized (this) { + if (this.buffer == null) { + this.buffer = new MultiByteBufferInputStream(buffers); + } else { + this.buffer.add(buffers); + } + + try { + JsonParser parser = JSON_FACTORY.createParser(buffer); + FaunaEvent event = FaunaEvent.parse(parser, dataCodec); + + statsCollector.add(event.getStats()); + + if (event.getType() == FaunaEvent.EventType.ERROR) { + ErrorInfo error = event.getError(); + this.onComplete(); + this.close(); + throw new ClientException(MessageFormat.format( + "Stream stopped due to error {0} {1}", + error.getCode(), error.getMessage())); + } + this.submit(event); + this.buffer = null; + } catch (ClientException e) { + this.buffer.reset(); // Handles partial event decoding + } catch (IOException e) { + throw new ClientException("Unable to decode stream", e); + } + } + } catch (Exception e) { + throw new ClientException("Unable to decode stream", e); + } finally { + this.subscription.request(1); + } + } + + /** + * Handles errors by canceling the subscription and closing the stream. + * + * @param throwable The {@link Throwable} encountered during stream processing. + */ + @Override + public void onError(final Throwable throwable) { + this.subscription.cancel(); + this.close(); + } + + /** + * Completes the stream by canceling the subscription. + */ + @Override + public void onComplete() { + this.subscription.cancel(); + } +} diff --git a/src/main/java/com/fauna/event/FeedIterator.java b/src/main/java/com/fauna/event/FeedIterator.java new file mode 100644 index 00000000..5b2ba69d --- /dev/null +++ b/src/main/java/com/fauna/event/FeedIterator.java @@ -0,0 +1,125 @@ +package com.fauna.event; + + +import com.fauna.client.FaunaClient; +import com.fauna.exception.FaunaException; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * FeedIterator iterates over Event Feed pages from Fauna. + * + * @param + */ +public final class FeedIterator implements Iterator> { + private final FaunaClient client; + private final Class resultClass; + private final EventSource eventSource; + private FeedOptions latestOptions; + private CompletableFuture> feedFuture; + + /** + * Construct a new PageIterator. + * + * @param client A client that makes requests to Fauna. + * @param eventSource The Fauna Event Source. + * @param feedOptions The FeedOptions object. + * @param resultClass The class of the elements returned from Fauna (i.e. the rows). + */ + public FeedIterator(final FaunaClient client, final EventSource eventSource, + final FeedOptions feedOptions, final Class resultClass) { + this.client = client; + this.resultClass = resultClass; + this.eventSource = eventSource; + this.latestOptions = feedOptions; + this.feedFuture = client.poll(eventSource, feedOptions, resultClass); + } + + @Override + public boolean hasNext() { + return this.feedFuture != null; + } + + /** + * Returns a CompletableFuture that will complete with the next page (or throw a FaunaException). + * When the future completes, the next page will be fetched in the background. + * + * @return A CompletableFuture that completes with a new FeedPage instance. + */ + public CompletableFuture> nextAsync() { + if (this.feedFuture != null) { + return this.feedFuture.thenApply(fs -> { + if (fs.hasNext()) { + FeedOptions options = this.latestOptions.nextPage(fs); + this.latestOptions = options; + this.feedFuture = + client.poll(this.eventSource, options, resultClass); + } else { + this.feedFuture = null; + } + return fs; + }); + } else { + throw new NoSuchElementException(); + } + } + + + /** + * Get the next Page (synchronously). + * + * @return FeedPage The next Page of elements E. + * @throws FaunaException If there is an error getting the next page. + */ + @Override + public FeedPage next() { + try { + return nextAsync().join(); + } catch (CompletionException ce) { + if (ce.getCause() != null && ce.getCause() instanceof FaunaException) { + throw (FaunaException) ce.getCause(); + } else { + throw ce; + } + } + } + + /** + * Return an iterator that iterates directly over the items that make up the page contents. + * + * @return An iterator of E. + */ + public Iterator> flatten() { + return new Iterator<>() { + private final FeedIterator feedIterator = FeedIterator.this; + private Iterator> thisPage = feedIterator.hasNext() + ? feedIterator.next().getEvents().iterator() + : null; + + @Override + public boolean hasNext() { + return thisPage != null && (thisPage.hasNext() || feedIterator.hasNext()); + } + + @Override + public FaunaEvent next() { + if (thisPage == null) { + throw new NoSuchElementException(); + } + try { + return thisPage.next(); + } catch (NoSuchElementException e) { + if (feedIterator.hasNext()) { + this.thisPage = feedIterator.next().getEvents().iterator(); + return thisPage.next(); + } else { + throw e; + } + } + } + }; + } +} diff --git a/src/main/java/com/fauna/event/FeedOptions.java b/src/main/java/com/fauna/event/FeedOptions.java new file mode 100644 index 00000000..487efc89 --- /dev/null +++ b/src/main/java/com/fauna/event/FeedOptions.java @@ -0,0 +1,184 @@ +package com.fauna.event; + +import java.time.Duration; +import java.util.Optional; + +import static com.fauna.constants.Defaults.DEFAULT_TIMEOUT; + +/** + * Represents the options for configuring an Event Feed request in Fauna. + *

+ * The {@code FeedOptions} class provides configuration parameters such as cursor, + * start timestamp, page size, and timeout for retrieving feeds from Fauna. + */ +public class FeedOptions { + + private final String cursor; + private final Long startTs; + private final Integer pageSize; + private final Duration timeout; + + /** + * The default {@code FeedOptions} instance with default settings. + */ + public static final FeedOptions DEFAULT = FeedOptions.builder().build(); + + /** + * Constructs a new {@code FeedOptions} with the specified parameters. + * + * @param cursor A {@code String} representing the cursor in the feed. Cannot be provided with a + * {@code startTs}. + * @param startTs A {@code Long} representing the start timestamp for the feed. Represents a time in microseconds since the Unix epoch. Cannot be provided with a + * {@code cursor}. + * @param pageSize An {@code Integer} specifying the number of items per feed page. + * @param timeout A {@code Duration} specifying the timeout for the feed request. + * @throws IllegalArgumentException if both {@code cursor} and {@code startTs} are set. + */ + public FeedOptions(final String cursor, final Long startTs, final Integer pageSize, + final Duration timeout) { + this.cursor = cursor; + this.startTs = startTs; + this.pageSize = pageSize; + this.timeout = timeout; + if (cursor != null && startTs != null) { + throw new IllegalArgumentException( + "Only one of cursor and startTs can be set."); + } + } + + /** + * Retrieves the cursor. + * + * @return An {@link Optional} containing the cursor, or empty if not set. + */ + public Optional getCursor() { + return Optional.ofNullable(cursor); + } + + /** + * Retrieves the start timestamp. + * + * @return An {@link Optional} containing the start timestamp, or empty if not set. + */ + public Optional getStartTs() { + return Optional.ofNullable(startTs); + } + + /** + * Retrieves the page size. + * + * @return An {@link Optional} containing the page size, or empty if not set. + */ + public Optional getPageSize() { + return Optional.ofNullable(pageSize); + } + + /** + * Retrieves the timeout duration. + * + * @return An {@link Optional} containing the timeout duration, or empty if not set. + */ + public Optional getTimeout() { + return Optional.ofNullable(timeout); + } + + /** + * Builder class for constructing {@code FeedOptions} instances. + */ + public static class Builder { + private String cursor = null; + private Long startTs = null; + private Integer pageSize = null; + private Duration timeout = DEFAULT_TIMEOUT; + + /** + * Sets the cursor. + * + * @param cursor A {@code String} representing the cursor. + * @return This {@code Builder} instance. + * @throws IllegalArgumentException if {@code startTs} is already set. + */ + public Builder cursor(final String cursor) { + if (startTs != null) { + throw new IllegalArgumentException( + "Only one of cursor and startTs can be set."); + } + this.cursor = cursor; + return this; + } + + /** + * Sets the start timestamp. + * + * @param startTs A {@code Long} representing the start timestamp. + * @return This {@code Builder} instance. + * @throws IllegalArgumentException if {@code cursor} is already set. + */ + public Builder startTs(final Long startTs) { + if (cursor != null) { + throw new IllegalArgumentException( + "Only one of cursor and startTs can be set."); + } + this.startTs = startTs; + return this; + } + + /** + * Sets the page size. + * + * @param pageSize An {@code Integer} specifying the number of items per page. + * @return This {@code Builder} instance. + */ + public Builder pageSize(final Integer pageSize) { + this.pageSize = pageSize; + return this; + } + + /** + * Sets the timeout duration. + * + * @param timeout A {@code Duration} specifying the timeout for the feed request. + * @return This {@code Builder} instance. + */ + public Builder timeout(final Duration timeout) { + this.timeout = timeout; + return this; + } + + /** + * Builds a new {@code FeedOptions} instance with the configured parameters. + * + * @return A new {@code FeedOptions} instance. + * @throws IllegalArgumentException if both {@code cursor} and {@code startTs} are set. + */ + public FeedOptions build() { + return new FeedOptions(cursor, startTs, pageSize, timeout); + } + } + + /** + * Creates a new {@code Builder} for constructing {@code FeedOptions}. + * + * @return A new {@code Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the {@code FeedOptions} for the next page, based on the cursor of the given page. + *

+ * This method copies options like page size and timeout, but does not set or copy {@code startTs}, + * because it uses the cursor. + * + * @param page The current or latest {@code FeedPage}. + * @return A new {@code FeedOptions} instance configured for the next page. + */ + public FeedOptions nextPage(final FeedPage page) { + FeedOptions.Builder builder = FeedOptions.builder().cursor(page.getCursor()); + // Do not set or copy startTs, because we are using cursor. + getPageSize().ifPresent(builder::pageSize); + getTimeout().ifPresent(builder::timeout); + return builder.build(); + } +} diff --git a/src/main/java/com/fauna/event/FeedPage.java b/src/main/java/com/fauna/event/FeedPage.java new file mode 100644 index 00000000..fd9c7f27 --- /dev/null +++ b/src/main/java/com/fauna/event/FeedPage.java @@ -0,0 +1,263 @@ +package com.fauna.event; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fauna.client.StatsCollector; +import com.fauna.codec.Codec; +import com.fauna.exception.ClientResponseException; +import com.fauna.response.QueryResponse; +import com.fauna.response.QueryStats; + +import java.io.IOException; +import java.io.InputStream; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; + +import static com.fasterxml.jackson.core.JsonToken.END_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.FIELD_NAME; +import static com.fasterxml.jackson.core.JsonToken.START_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.START_OBJECT; +import static com.fauna.constants.ResponseFields.CURSOR_FIELD_NAME; +import static com.fauna.constants.ResponseFields.EVENTS_FIELD_NAME; +import static com.fauna.constants.ResponseFields.FEED_HAS_NEXT_FIELD_NAME; +import static com.fauna.constants.ResponseFields.STATS_FIELD_NAME; + +/** + * Represents a page of events from an Event Feed. + * + * @param The type of data contained in each event. + */ +public class FeedPage { + private final List> events; + private final String cursor; + private final boolean hasNext; + private final QueryStats stats; + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + + /** + * Constructs a {@code FeedPage} with the specified events, cursor, pagination flag, and statistics. + * + * @param events A list of {@link FaunaEvent} objects representing the events in this page. + * @param cursor A {@code String} representing the cursor for pagination. + * @param hasNext A {@code boolean} indicating if there are more pages available. + * @param stats A {@link QueryStats} object containing statistics for the page. + * @throws IllegalArgumentException if {@code events} is null or {@code cursor} is blank. + */ + public FeedPage(final List> events, final String cursor, + final boolean hasNext, final QueryStats stats) { + if (events == null) { + throw new IllegalArgumentException("events cannot be null"); + } + if (cursor == null || cursor.isBlank()) { + throw new IllegalArgumentException("cursor cannot be blank"); + } + this.events = events; + this.cursor = cursor; + this.hasNext = hasNext; + this.stats = stats; + } + + /** + * Retrieves the list of events in this feed page. + * + * @return A {@code List} of {@link FaunaEvent} objects. + */ + public List> getEvents() { + return events; + } + + /** + * Retrieves the cursor for pagination. + * + * @return A {@code String} representing the cursor. + */ + public String getCursor() { + return cursor; + } + + /** + * Checks if there are more pages available. + * + * @return {@code true} if there are more pages, {@code false} otherwise. + */ + public boolean hasNext() { + return hasNext; + } + + /** + * Retrieves the statistics for this feed page. + * + * @return A {@link QueryStats} object. + */ + public QueryStats getStats() { + return stats; + } + + /** + * Builder class for constructing {@code FeedPage} instances. + * + * @param The type of data contained in each event. + */ + public static class Builder { + private final Codec elementCodec; + private final StatsCollector statsCollector; + private List> events; + private String cursor = ""; + private Boolean hasNext = false; + private QueryStats stats = null; + + /** + * Constructs a {@code Builder} with the specified codec and stats collector. + * + * @param elementCodec The {@link Codec} used to decode events. + * @param statsCollector The {@link StatsCollector} to gather statistics for the feed. + */ + public Builder(final Codec elementCodec, final StatsCollector statsCollector) { + this.elementCodec = elementCodec; + this.statsCollector = statsCollector; + } + + /** + * Sets the list of events for the feed page. + * + * @param events A list of {@link FaunaEvent} objects representing the events in this page. + * @return This {@code Builder} instance. + */ + public Builder events(final List> events) { + this.events = events; + return this; + } + + /** + * Sets the cursor for pagination. + * + * @param cursor A {@code String} representing the cursor. + * @return This {@code Builder} instance. + */ + public Builder cursor(final String cursor) { + this.cursor = cursor; + return this; + } + + /** + * Sets the flag indicating if there are more pages available. + * + * @param hasNext A {@code Boolean} indicating if there are more pages. + * @return This {@code Builder} instance. + */ + public Builder hasNext(final Boolean hasNext) { + this.hasNext = hasNext; + return this; + } + + /** + * Sets the statistics for the feed page. + * + * @param stats A {@link QueryStats} object containing statistics for the page. + * @return This {@code Builder} instance. + */ + public Builder stats(final QueryStats stats) { + this.stats = stats; + return this; + } + + /** + * Parses and sets the list of events from the provided JSON parser. + * + * @param parser The {@link JsonParser} to decode the events from. + * @return This {@code Builder} instance. + * @throws IOException if an error occurs during parsing. + */ + public Builder parseEvents(final JsonParser parser) throws IOException { + if (parser.nextToken() == START_ARRAY) { + List> events = new ArrayList<>(); + while (parser.nextToken() != END_ARRAY) { + events.add(FaunaEvent.parse(parser, elementCodec)); + } + this.events = events; + } else { + throw new IOException("Invalid event starting with: " + parser.currentToken()); + } + return this; + } + + /** + * Builds a new {@code FeedPage} instance with the configured parameters. + * + * @return A new {@code FeedPage} instance. + * @throws IllegalArgumentException if {@code events} is null or {@code cursor} is blank. + */ + public FeedPage build() { + return new FeedPage<>(events, cursor, hasNext, stats); + } + + /** + * Parses and sets the appropriate field in the builder based on the JSON parser's current field. + * + * @param parser The {@link JsonParser} for reading the field value. + * @return The updated {@code Builder} instance. + * @throws IOException if an error occurs during parsing. + */ + public Builder parseField(final JsonParser parser) throws IOException { + String fieldName = parser.getValueAsString(); + switch (fieldName) { + case CURSOR_FIELD_NAME: + return cursor(parser.nextTextValue()); + case EVENTS_FIELD_NAME: + return parseEvents(parser); + case STATS_FIELD_NAME: + QueryStats stats = QueryStats.parseStats(parser); + statsCollector.add(stats); + return stats(stats); + case FEED_HAS_NEXT_FIELD_NAME: + return hasNext(parser.nextBooleanValue()); + default: + throw new ClientResponseException("Unknown FeedPage field: " + fieldName); + } + } + } + + /** + * Creates a new {@code Builder} for constructing a {@code FeedPage}. + * + * @param elementCodec The {@link Codec} used to decode events. + * @param statsCollector The {@link StatsCollector} to gather statistics. + * @param The type of data contained in each event. + * @return A new {@code Builder} instance. + */ + public static Builder builder(final Codec elementCodec, final StatsCollector statsCollector) { + return new Builder<>(elementCodec, statsCollector); + } + + /** + * Parses an HTTP response and constructs a {@code FeedPage} instance. + * + * @param response The {@link HttpResponse} containing the feed data. + * @param elementCodec The {@link Codec} used to decode events. + * @param statsCollector The {@link StatsCollector} to gather statistics. + * @param The type of data contained in each event. + * @return The parsed {@code FeedPage}. + * @throws ClientResponseException if an error occurs while parsing the feed response. + */ + public static FeedPage parseResponse(final HttpResponse response, + final Codec elementCodec, + final StatsCollector statsCollector) { + try { + if (response.statusCode() >= 400) { + QueryResponse.parseResponse(response, elementCodec, statsCollector); + } + JsonParser parser = JSON_FACTORY.createParser(response.body()); + if (parser.nextToken() != START_OBJECT) { + throw new ClientResponseException("Invalid event starting with: " + parser.currentToken()); + } + Builder builder = FeedPage.builder(elementCodec, statsCollector); + while (parser.nextToken() == FIELD_NAME) { + builder = builder.parseField(parser); + } + return builder.build(); + } catch (IOException e) { + throw new ClientResponseException("Error parsing Feed response.", e); + } + } +} diff --git a/src/main/java/com/fauna/event/FeedRequest.java b/src/main/java/com/fauna/event/FeedRequest.java new file mode 100644 index 00000000..44af9054 --- /dev/null +++ b/src/main/java/com/fauna/event/FeedRequest.java @@ -0,0 +1,77 @@ +package com.fauna.event; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fauna.client.RequestBuilder; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * Represents an Event Feed request from Fauna. + *

+ * The {@code FeedRequest} class contains an {@link EventSource} and {@link FeedOptions} to + * specify the details of the feed request, such as the cursor, start timestamp, and page size. + */ +public class FeedRequest { + private final EventSource source; + private final FeedOptions options; + + /** + * Constructs a {@code FeedRequest} with the specified event source and options. + * + * @param source The {@link EventSource} containing the event source token. + * @param options The {@link FeedOptions} specifying additional feed request options. + * @throws IllegalArgumentException if {@code source} or {@code options} is null. + */ + public FeedRequest(final EventSource source, final FeedOptions options) { + if (source == null) { + throw new IllegalArgumentException("EventSource cannot be null."); + } + if (options == null) { + throw new IllegalArgumentException("FeedOptions cannot be null."); + } + this.source = source; + this.options = options; + } + + /** + * Serializes this {@code FeedRequest} to a JSON string. + * + * @return A {@code String} representation of this feed request in JSON format. + * @throws IOException if an error occurs during serialization. + */ + public String serialize() throws IOException { + ByteArrayOutputStream requestBytes = new ByteArrayOutputStream(); + JsonGenerator gen = new JsonFactory().createGenerator(requestBytes); + + gen.writeStartObject(); + gen.writeStringField(RequestBuilder.FieldNames.TOKEN, source.getToken()); + + if (options.getCursor().isPresent()) { + gen.writeStringField(RequestBuilder.FieldNames.CURSOR, options.getCursor().get()); + } + if (options.getStartTs().isPresent()) { + gen.writeNumberField(RequestBuilder.FieldNames.START_TS, options.getStartTs().get()); + } + if (options.getPageSize().isPresent()) { + gen.writeNumberField(RequestBuilder.FieldNames.PAGE_SIZE, options.getPageSize().get()); + } + + gen.writeEndObject(); + gen.flush(); + return requestBytes.toString(StandardCharsets.UTF_8); + } + + /** + * Creates a new {@code FeedRequest} from an {@link EventSource}. + * + * @param resp The {@link EventSource} containing the event source token. + * @param options The {@link FeedOptions} specifying additional feed request options. + * @return A new {@code FeedRequest} instance based on the response and options. + */ + public static FeedRequest fromResponse(final EventSource resp, final FeedOptions options) { + return new FeedRequest(resp, options); + } +} diff --git a/src/main/java/com/fauna/event/StreamOptions.java b/src/main/java/com/fauna/event/StreamOptions.java new file mode 100644 index 00000000..6813e937 --- /dev/null +++ b/src/main/java/com/fauna/event/StreamOptions.java @@ -0,0 +1,168 @@ +package com.fauna.event; + +import com.fauna.client.RetryStrategy; + +import java.time.Duration; +import java.util.Optional; + +/** + * Represents configuration options for a Fauna Event Stream. + *

+ * The {@code StreamOptions} class allows customization of the stream request, including cursor, + * retry strategy, start timestamp, status events, and timeout. + */ +public class StreamOptions { + + private final String cursor; + private final RetryStrategy retryStrategy; + private final Long startTimestamp; + private final Boolean statusEvents; + private final Duration timeout; + + /** + * Default {@code StreamOptions} instance with defaults. + */ + public static final StreamOptions DEFAULT = StreamOptions.builder().build(); + + /** + * Constructs a {@code StreamOptions} instance with the specified builder. + * + * @param builder The {@link Builder} instance containing the configuration options. + */ + public StreamOptions(final Builder builder) { + this.cursor = builder.cursor; + this.retryStrategy = builder.retryStrategy; + this.startTimestamp = builder.startTimestamp; + this.statusEvents = builder.statusEvents; + this.timeout = builder.timeout; + } + + /** + * Retrieves the event cursor. Used to restart the stream. + * + * @return An {@link Optional} containing the cursor, or empty if not set. + */ + public Optional getCursor() { + return Optional.ofNullable(cursor); + } + + /** + * Retrieves the retry strategy for the stream. + * + * @return An {@link Optional} containing the retry strategy, or empty if not set. + */ + public Optional getRetryStrategy() { + return Optional.ofNullable(retryStrategy); + } + + /** + * Retrieves the start timestamp for the stream. + * + * @return An {@link Optional} containing the start timestamp, or empty if not set. + */ + public Optional getStartTimestamp() { + return Optional.ofNullable(startTimestamp); + } + + /** + * Checks if status events are enabled for the stream. + * + * @return An {@link Optional} containing a boolean for status events, or empty if not set. + */ + public Optional getStatusEvents() { + return Optional.ofNullable(statusEvents); + } + + /** + * Retrieves the timeout duration for the stream. + * + * @return An {@link Optional} containing the timeout duration, or empty if not set. + */ + public Optional getTimeout() { + return Optional.ofNullable(timeout); + } + + /** + * Builder class for constructing {@code StreamOptions} instances. + */ + public static class Builder { + private String cursor = null; + private RetryStrategy retryStrategy = null; + private Long startTimestamp = null; + private Boolean statusEvents = null; + private Duration timeout = null; + + /** + * Sets the cursor for the stream. + * + * @param cursor A {@code String} representing the cursor position. + * @return This {@code Builder} instance. + */ + public Builder cursor(final String cursor) { + this.cursor = cursor; + return this; + } + + /** + * Sets the retry strategy for the stream. + * + * @param retryStrategy The {@link RetryStrategy} for managing retries. + * @return This {@code Builder} instance. + */ + public Builder retryStrategy(final RetryStrategy retryStrategy) { + this.retryStrategy = retryStrategy; + return this; + } + + /** + * Sets the start timestamp for the stream. + * + * @param startTimestamp A {@code long} representing the start timestamp. + * @return This {@code Builder} instance. + */ + public Builder startTimestamp(final long startTimestamp) { + this.startTimestamp = startTimestamp; + return this; + } + + /** + * Enables or disables status events for the stream. + * + * @param statusEvents A {@code Boolean} indicating if status events are enabled. + * @return This {@code Builder} instance. + */ + public Builder statusEvents(final Boolean statusEvents) { + this.statusEvents = statusEvents; + return this; + } + + /** + * Sets the timeout duration for the stream. + * + * @param timeout A {@link Duration} representing the timeout. + * @return This {@code Builder} instance. + */ + public Builder timeout(final Duration timeout) { + this.timeout = timeout; + return this; + } + + /** + * Builds a new {@code StreamOptions} instance with the configured parameters. + * + * @return A new {@code StreamOptions} instance. + */ + public StreamOptions build() { + return new StreamOptions(this); + } + } + + /** + * Creates a new {@code Builder} for constructing {@code StreamOptions}. + * + * @return A new {@code Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } +} diff --git a/src/main/java/com/fauna/event/StreamRequest.java b/src/main/java/com/fauna/event/StreamRequest.java new file mode 100644 index 00000000..22fea880 --- /dev/null +++ b/src/main/java/com/fauna/event/StreamRequest.java @@ -0,0 +1,68 @@ +package com.fauna.event; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fauna.client.RequestBuilder; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * Defines the request body for interacting with the Fauna /stream endpoint. + *

+ * The {@code StreamRequest} class constructs a JSON request body that includes + * an {@link EventSource} and {@link StreamOptions} to configure the request parameters. + */ +public class StreamRequest { + + private final EventSource source; + private final StreamOptions options; + + /** + * Constructs a {@code StreamRequest} with the specified event source and options. + * + * @param eventSource The {@link EventSource} providing the event source token. + * @param streamOptions The {@link StreamOptions} specifying additional request options. + * @throws IllegalArgumentException if {@code eventSource} or {@code streamOptions} is null. + */ + public StreamRequest(final EventSource eventSource, final StreamOptions streamOptions) { + if (eventSource == null) { + throw new IllegalArgumentException("Event source cannot be null."); + } + if (streamOptions == null) { + throw new IllegalArgumentException("Stream options cannot be null."); + } + this.source = eventSource; + this.options = streamOptions; + } + + /** + * Serializes this {@code StreamRequest} to a JSON string for the Fauna /stream endpoint. + * + *

The JSON includes fields based on the {@link EventSource} and {@link StreamOptions} + * configurations. Either the cursor or start timestamp is included, with cursor taking precedence. + * + * @return A JSON-formatted {@code String} representing this stream request. + * @throws IOException if an error occurs during serialization. + */ + public String serialize() throws IOException { + ByteArrayOutputStream requestBytes = new ByteArrayOutputStream(); + JsonGenerator gen = new JsonFactory().createGenerator(requestBytes); + + gen.writeStartObject(); + gen.writeStringField(RequestBuilder.FieldNames.TOKEN, source.getToken()); + + // Prefer cursor if present, otherwise use start timestamp. + if (options.getCursor().isPresent()) { + gen.writeStringField(RequestBuilder.FieldNames.CURSOR, options.getCursor().get()); + } else if (options.getStartTimestamp().isPresent()) { + gen.writeNumberField(RequestBuilder.FieldNames.START_TS, options.getStartTimestamp().get()); + } + + gen.writeEndObject(); + gen.flush(); + + return requestBytes.toString(StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/com/fauna/event/package-info.java b/src/main/java/com/fauna/event/package-info.java new file mode 100644 index 00000000..425825dc --- /dev/null +++ b/src/main/java/com/fauna/event/package-info.java @@ -0,0 +1,21 @@ +/** + * Provides classes for managing and interacting with Event Feeds and Event Streams. + * + *

This package includes core components and utilities for handling event streaming, + * such as options, requests, and response handling mechanisms. + * + *

    + *
  • {@link com.fauna.event.EventSource} - Represents an event source.
  • + *
  • {@link com.fauna.event.FaunaEvent} - Defines an event.
  • + *
  • {@link com.fauna.event.FaunaStream} - Processes events from an Event Stream, decoding them into {@code FaunaEvent} instances.
  • + *
  • {@link com.fauna.event.FeedIterator} - Enables iteration through pages of events in an Event Feed.
  • + *
  • {@link com.fauna.event.FeedOptions} - Specifies configuration options for managing Event Feed pagination and timeout.
  • + *
  • {@link com.fauna.event.FeedPage} - Represents a paginated events in an Event Feed, including metadata like cursor and statistics.
  • + *
  • {@link com.fauna.event.FeedRequest} - Constructs a request for Fauna's Event Feed HTTP API endpoint.
  • + *
  • {@link com.fauna.event.StreamOptions} - Specified configuration options for an Event Stream, such as cursor, retry strategy, and timeout settings.
  • + *
  • {@link com.fauna.event.StreamRequest} - Constructs a request for Fauna's Event Stream HTTP API endpoint.
  • + *
+ * + *

The classes in this package are designed to support Fauna Event Feeds and Event Streams. + */ +package com.fauna.event; diff --git a/src/main/java/com/fauna/exception/AbortException.java b/src/main/java/com/fauna/exception/AbortException.java index 11a82f57..c2eac2e8 100644 --- a/src/main/java/com/fauna/exception/AbortException.java +++ b/src/main/java/com/fauna/exception/AbortException.java @@ -1,37 +1,52 @@ package com.fauna.exception; -import com.fauna.codec.CodecProvider; -import com.fauna.codec.DefaultCodecProvider; -import com.fauna.codec.UTF8FaunaParser; import com.fauna.response.QueryFailure; -import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +/** + * An exception that represents an abort error in Fauna. + * This exception extends {@link ServiceException} and includes methods to retrieve + * the abort data. + */ public class AbortException extends ServiceException { - - private Object abort = null; - private final CodecProvider provider = DefaultCodecProvider.SINGLETON; - - public AbortException(QueryFailure response) { + @SuppressWarnings("rawtypes") + private final Map decoded = new HashMap<>(); + + /** + * Constructs a new {@code AbortException} with the specified {@link QueryFailure} response. + * + * @param response The {@code QueryFailure} object containing details about the aborted query. + */ + public AbortException(final QueryFailure response) { super(response); } - public Object getAbort() throws IOException { + /** + * Returns the abort data as a top-level {@code Object}. This is primarily useful for debugging + * or situations where the type of abort data may be unknown. + * + * @return An {@code Object} containing the abort data, or {@code null} if no data is present. + */ + public Object getAbort() { return getAbort(Object.class); } - @SuppressWarnings("unchecked") - public T getAbort(Class clazz) throws IOException { - if (abort != null) return (T) abort; - - var abStr = this.getResponse().getAbortString(); - if (abStr.isPresent()) { - var codec = provider.get(clazz); - var parser = UTF8FaunaParser.fromString(abStr.get()); - abort = codec.decode(parser); - return (T) abort; - } else { - return null; + /** + * Returns the abort data decoded into the specified class, or {@code null} if there is no abort data. + * The abort data is cached upon retrieval to avoid redundant decoding. + * + * @param clazz The {@code Class} to decode the abort data into. + * @param The type of the abort data. + * @return The decoded abort data of type {@code T}, or {@code null} if no data is available. + */ + public T getAbort(final Class clazz) { + if (!decoded.containsKey(clazz)) { + Object abortData = getResponse().getAbort(clazz).orElse(null); + decoded.put(clazz, abortData); } + //noinspection unchecked + return (T) decoded.get(clazz); } } diff --git a/src/main/java/com/fauna/exception/AuthenticationException.java b/src/main/java/com/fauna/exception/AuthenticationException.java index 738bd6a2..dce49ff8 100644 --- a/src/main/java/com/fauna/exception/AuthenticationException.java +++ b/src/main/java/com/fauna/exception/AuthenticationException.java @@ -2,8 +2,21 @@ import com.fauna.response.QueryFailure; +/** + * Exception thrown when an authentication error occurs in Fauna. + * This typically indicates an issue with the authentication secret used for Fauna requests. + *

+ * Extends {@link ServiceException} and provides access to detailed failure information through the + * {@link QueryFailure} response. + */ public class AuthenticationException extends ServiceException { - public AuthenticationException(QueryFailure response) { + + /** + * Constructs a new {@code AuthenticationException} with the specified {@code QueryFailure} response. + * + * @param response The {@code QueryFailure} object containing details about the authentication failure. + */ + public AuthenticationException(final QueryFailure response) { super(response); } } diff --git a/src/main/java/com/fauna/exception/AuthorizationException.java b/src/main/java/com/fauna/exception/AuthorizationException.java index 99440ed3..095f93cf 100644 --- a/src/main/java/com/fauna/exception/AuthorizationException.java +++ b/src/main/java/com/fauna/exception/AuthorizationException.java @@ -2,9 +2,22 @@ import com.fauna.response.QueryFailure; +/** + * Exception thrown when an authorization error occurs in Fauna. + * This typically indicates that the Fauna authentication secret does not have permissions + * required to perform the requested operation. + *

+ * Extends {@link ServiceException} and provides access to detailed failure information through + * the {@link QueryFailure} response. + */ public class AuthorizationException extends ServiceException { - public static final String ERROR_CODE = "forbidden"; - public AuthorizationException(QueryFailure response) { + + /** + * Constructs a new {@code AuthorizationException} with the specified {@code QueryFailure} response. + * + * @param response The {@code QueryFailure} object containing details about the authorization failure. + */ + public AuthorizationException(final QueryFailure response) { super(response); } } diff --git a/src/main/java/com/fauna/exception/ClientException.java b/src/main/java/com/fauna/exception/ClientException.java index dd1919f0..9fa36038 100644 --- a/src/main/java/com/fauna/exception/ClientException.java +++ b/src/main/java/com/fauna/exception/ClientException.java @@ -1,12 +1,30 @@ package com.fauna.exception; +/** + * Exception representing client-side errors in Fauna. + *

+ * This exception is typically thrown when there is an issue with client configuration, + * request formation, or any other client-specific error that does not originate from Fauna. + * Extends {@link FaunaException} to provide detailed information about the error. + */ public class ClientException extends FaunaException { - public ClientException(String message) { + /** + * Constructs a new {@code ClientException} with the specified detail message. + * + * @param message A {@code String} describing the reason for the client error. + */ + public ClientException(final String message) { super(message); } - public ClientException(String message, Throwable cause) { + /** + * Constructs a new {@code ClientException} with the specified detail message and cause. + * + * @param message A {@code String} describing the reason for the client error. + * @param cause The underlying {@code Throwable} cause of the error. + */ + public ClientException(final String message, final Throwable cause) { super(message, cause); } -} \ No newline at end of file +} diff --git a/src/main/java/com/fauna/exception/ClientRequestException.java b/src/main/java/com/fauna/exception/ClientRequestException.java index 226adfe9..2bc43628 100644 --- a/src/main/java/com/fauna/exception/ClientRequestException.java +++ b/src/main/java/com/fauna/exception/ClientRequestException.java @@ -1,12 +1,30 @@ package com.fauna.exception; +/** + * Exception representing errors related to client requests in Fauna. + *

+ * This exception is thrown when there is an issue with the structure or content of a request + * sent from the client, such as invalid parameters or improperly formatted data. + * Extends {@link ClientException} to provide information specific to request-related errors. + */ public class ClientRequestException extends ClientException { - public ClientRequestException(String message) { + /** + * Constructs a new {@code ClientRequestException} with the specified detail message. + * + * @param message A {@code String} describing the reason for the client request error. + */ + public ClientRequestException(final String message) { super(message); } - public ClientRequestException(String message, Throwable cause) { + /** + * Constructs a new {@code ClientRequestException} with the specified detail message and cause. + * + * @param message A {@code String} describing the reason for the client request error. + * @param cause The underlying {@code Throwable} cause of the error. + */ + public ClientRequestException(final String message, final Throwable cause) { super(message, cause); } -} \ No newline at end of file +} diff --git a/src/main/java/com/fauna/exception/ClientResponseException.java b/src/main/java/com/fauna/exception/ClientResponseException.java index 6768dad4..79a95b52 100644 --- a/src/main/java/com/fauna/exception/ClientResponseException.java +++ b/src/main/java/com/fauna/exception/ClientResponseException.java @@ -2,21 +2,56 @@ import java.text.MessageFormat; +/** + * Exception representing errors in the client's response handling. + *

+ * This exception is typically thrown when there is an issue with the response received from + * Fauna, including unexpected status codes or other response-related errors. + * Extends {@link ClientException} to provide information specific to response handling errors. + */ public class ClientResponseException extends ClientException { - public ClientResponseException(String message) { + /** + * Constructs a new {@code ClientResponseException} with the specified detail message. + * + * @param message A {@code String} describing the reason for the client response error. + */ + public ClientResponseException(final String message) { super(message); } - private static String buildMessage(String message, int statusCode) { - return MessageFormat.format("ClientResponseException HTTP {0}: {1}", statusCode, message); + /** + * Constructs a new {@code ClientResponseException} with a formatted message + * based on the provided status code and message. + * + * @param message A {@code String} describing the reason for the client response error. + * @param statusCode An {@code int} representing the HTTP status code received. + * @return A formatted message string. + */ + private static String buildMessage(final String message, final int statusCode) { + return MessageFormat.format("ClientResponseException HTTP {0}: {1}", + statusCode, message); } - public ClientResponseException(String message, Throwable exc, int statusCode) { + /** + * Constructs a new {@code ClientResponseException} with the specified detail message, cause, and status code. + * + * @param message A {@code String} describing the reason for the client response error. + * @param exc The underlying {@code Throwable} cause of the error. + * @param statusCode An {@code int} representing the HTTP status code received. + */ + public ClientResponseException(final String message, final Throwable exc, + final int statusCode) { super(buildMessage(message, statusCode), exc); } - public ClientResponseException(String message, Throwable cause) { + /** + * Constructs a new {@code ClientResponseException} with the specified detail message and cause. + * + * @param message A {@code String} describing the reason for the client response error. + * @param cause The underlying {@code Throwable} cause of the error. + */ + public ClientResponseException(final String message, final Throwable cause) { super(message, cause); } -} \ No newline at end of file +} diff --git a/src/main/java/com/fauna/exception/CodecException.java b/src/main/java/com/fauna/exception/CodecException.java index d17616c3..9062de72 100644 --- a/src/main/java/com/fauna/exception/CodecException.java +++ b/src/main/java/com/fauna/exception/CodecException.java @@ -1,23 +1,52 @@ package com.fauna.exception; import java.io.IOException; -import java.util.concurrent.Callable; +/** + * Exception representing errors encountered during encoding or decoding operations. + *

+ * This exception is typically thrown when an encoding or decoding error occurs within the Fauna + * client, such as an {@link IOException} while reading or writing data. + * Extends {@link FaunaException} to provide detailed information about codec-related errors. + */ public class CodecException extends FaunaException { - public CodecException(String message) { + /** + * Constructs a new {@code CodecException} with the specified detail message. + * + * @param message A {@code String} describing the reason for the codec error. + */ + public CodecException(final String message) { super(message); } - public CodecException(String message, Throwable cause) { + /** + * Constructs a new {@code CodecException} with the specified detail message and cause. + * + * @param message A {@code String} describing the reason for the codec error. + * @param cause The underlying {@code Throwable} cause of the error. + */ + public CodecException(final String message, final Throwable cause) { super(message, cause); } - public static CodecException decodingIOException(IOException exc) { + /** + * Creates a new {@code CodecException} specifically for decoding {@link IOException}s. + * + * @param exc The {@code IOException} encountered during decoding. + * @return A {@code CodecException} describing the decoding error. + */ + public static CodecException decodingIOException(final IOException exc) { return new CodecException("IOException while decoding.", exc); } - public static CodecException encodingIOException(IOException exc) { + /** + * Creates a new {@code CodecException} specifically for encoding {@link IOException}s. + * + * @param exc The {@code IOException} encountered during encoding. + * @return A {@code CodecException} describing the encoding error. + */ + public static CodecException encodingIOException(final IOException exc) { return new CodecException("IOException while encoding.", exc); } -} \ No newline at end of file +} diff --git a/src/main/java/com/fauna/exception/ConstraintFailureException.java b/src/main/java/com/fauna/exception/ConstraintFailureException.java index 815544ce..b12692cb 100644 --- a/src/main/java/com/fauna/exception/ConstraintFailureException.java +++ b/src/main/java/com/fauna/exception/ConstraintFailureException.java @@ -3,15 +3,30 @@ import com.fauna.response.ConstraintFailure; import com.fauna.response.QueryFailure; -import java.util.List; - +/** + * Exception representing a constraint failure in a Fauna query. + *

+ * This exception is typically thrown when a query violates a collection's check + * or unique constraints. + * Extends {@link ServiceException} and provides access to details about the constraint failures. + */ public class ConstraintFailureException extends ServiceException { - public ConstraintFailureException(QueryFailure failure) { + + /** + * Constructs a new {@code ConstraintFailureException} with the specified {@code QueryFailure}. + * + * @param failure The {@code QueryFailure} object containing details about the constraint failure. + */ + public ConstraintFailureException(final QueryFailure failure) { super(failure); } - public List getConstraintFailures() { - var cf = this.getResponse().getConstraintFailures(); - return cf.orElseGet(List::of); + /** + * Retrieves an array of {@link ConstraintFailure} objects representing the individual constraint failures. + * + * @return An array of {@code ConstraintFailure} objects, or {@code null} if no constraint failures are present. + */ + public ConstraintFailure[] getConstraintFailures() { + return getResponse().getConstraintFailures().orElse(null); } } diff --git a/src/main/java/com/fauna/exception/ContendedTransactionException.java b/src/main/java/com/fauna/exception/ContendedTransactionException.java index 9620971b..56a362c9 100644 --- a/src/main/java/com/fauna/exception/ContendedTransactionException.java +++ b/src/main/java/com/fauna/exception/ContendedTransactionException.java @@ -2,9 +2,25 @@ import com.fauna.response.QueryFailure; +/** + * An exception indicating that too much transaction contention occurred while executing a query. + *

+ * This exception is thrown when a transaction cannot proceed due to conflicts or contention + * with other concurrent transactions. + *

+ * Extends {@link ServiceException} to provide detailed information about the failed query. + * + * @see ServiceException + * @see QueryFailure + */ public class ContendedTransactionException extends ServiceException { - public static final String ERROR_CODE = "contended_transaction"; - public ContendedTransactionException(QueryFailure response) { + + /** + * Constructs a new {@code ContendedTransactionException} with the specified {@code QueryFailure} response. + * + * @param response The {@code QueryFailure} object containing details about the failed query. + */ + public ContendedTransactionException(final QueryFailure response) { super(response); } } diff --git a/src/main/java/com/fauna/exception/ErrorHandler.java b/src/main/java/com/fauna/exception/ErrorHandler.java index 6bb59f2d..2b29b457 100644 --- a/src/main/java/com/fauna/exception/ErrorHandler.java +++ b/src/main/java/com/fauna/exception/ErrorHandler.java @@ -1,11 +1,7 @@ package com.fauna.exception; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fauna.response.wire.QueryResponseWire; import com.fauna.response.QueryFailure; -import java.io.IOException; - import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_CONFLICT; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; @@ -13,7 +9,13 @@ import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; -public class ErrorHandler { +/** + * Provides error handling based on error codes and HTTP status codes returned by Fauna. + *

+ * The {@code ErrorHandler} class contains a static method to manage various error scenarios + * by analyzing the HTTP status code and specific error codes, mapping them to relevant exceptions. + */ +public final class ErrorHandler { private static final String INVALID_QUERY = "invalid_query"; private static final String LIMIT_EXCEEDED = "limit_exceeded"; private static final String INVALID_REQUEST = "invalid_request"; @@ -24,64 +26,47 @@ public class ErrorHandler { private static final String CONTENDED_TRANSACTION = "contended_transaction"; private static final String TIME_OUT = "time_out"; private static final String INTERNAL_ERROR = "internal_error"; + private static final int HTTP_LIMIT_EXCEEDED = 429; + private static final int HTTP_TIME_OUT = 440; - - /** - * Handles errors based on the HTTP status code and response body. - * - * @param statusCode The HTTP status code. - * @param response The decoded response. - * @throws AbortException - * @throws AuthenticationException - * @throws AuthorizationException - * @throws ConstraintFailureException - * @throws ContendedTransactionException - * @throws InvalidRequestException - * @throws ProtocolException - * @throws QueryCheckException - * @throws QueryRuntimeException - * @throws QueryTimeoutException - * @throws ThrottlingException - * - */ - public static void handleErrorResponse(int statusCode, QueryResponseWire response, String body) { - QueryFailure failure = new QueryFailure(statusCode, response); - handleQueryFailure(statusCode, failure); - throw new ProtocolException(statusCode, body); + private ErrorHandler() { } /** - * Handles errors based on the HTTP status code and error code. - * - * @param statusCode The HTTP status code. - * @param failure The decoded QueryFailure body. - * @throws AbortException - * @throws AuthenticationException - * @throws AuthorizationException - * @throws ConstraintFailureException - * @throws ContendedTransactionException - * @throws InvalidRequestException - * @throws QueryCheckException - * @throws QueryRuntimeException - * @throws QueryTimeoutException - * @throws ThrottlingException + * Handles errors based on the HTTP status code and error code returned by Fauna. * + * @param statusCode The HTTP status code received from Fauna. + * @param failure The {@link QueryFailure} object containing details about the failure. + * @throws AbortException Thrown if the transaction was aborted. + * @throws AuthenticationException Thrown if authentication credentials are invalid or missing. + * @throws AuthorizationException Thrown if authorization credentials are invalid or insufficient. + * @throws ConstraintFailureException Thrown if the transaction failed a database constraint check. + * @throws ContendedTransactionException Thrown if too much contention occurred during a transaction. + * @throws InvalidRequestException Thrown if the request body does not conform to API specifications. + * @throws QueryCheckException Thrown if the query failed validation checks. + * @throws QueryRuntimeException Thrown if the query encountered a runtime error. + * @throws QueryTimeoutException Thrown if the query exceeded the specified timeout. + * @throws ServiceInternalException Thrown if an unexpected server error occurred. + * @throws ThrottlingException Thrown if the query exceeded capacity limits. */ - public static void handleQueryFailure(int statusCode, QueryFailure failure) { + public static void handleQueryFailure( + final int statusCode, + final QueryFailure failure) { switch (statusCode) { case HTTP_BAD_REQUEST: switch (failure.getErrorCode()) { - case INVALID_QUERY: throw new QueryCheckException(failure); - case LIMIT_EXCEEDED: throw new ThrottlingException(failure); - case INVALID_REQUEST: throw new InvalidRequestException(failure); - case ABORT: throw new AbortException(failure); - case CONSTRAINT_FAILURE: throw new ConstraintFailureException(failure); - // There are ~30 more error codes that map to a QueryRuntimeException. - // By using a default here, one of them is not strictly required. But we - // _do_ require a valid JSON body that can be decoded to a - // QueryFailure. Defaulting here also slightly future-proofs this client - // because Fauna can throw 400s with new error codes. - default: throw new QueryRuntimeException(failure); + case INVALID_QUERY: + throw new QueryCheckException(failure); + case LIMIT_EXCEEDED: + throw new ThrottlingException(failure); + case INVALID_REQUEST: + throw new InvalidRequestException(failure); + case ABORT: + throw new AbortException(failure); + case CONSTRAINT_FAILURE: + throw new ConstraintFailureException(failure); + default: + throw new QueryRuntimeException(failure); } case HTTP_UNAUTHORIZED: if (UNAUTHORIZED.equals(failure.getErrorCode())) { @@ -95,14 +80,12 @@ public static void handleQueryFailure(int statusCode, QueryFailure failure) { if (CONTENDED_TRANSACTION.equals(failure.getErrorCode())) { throw new ContendedTransactionException(failure); } - case 429: - // 400 (above), or 429 with "limit_exceeded" -> ThrottlingException. + case HTTP_LIMIT_EXCEEDED: if (LIMIT_EXCEEDED.equals(failure.getErrorCode())) { throw new ThrottlingException(failure); } - case 440: + case HTTP_TIME_OUT: case HTTP_UNAVAILABLE: - // 400 or 503 with "time_out" -> QueryTimeoutException. if (TIME_OUT.equals(failure.getErrorCode())) { throw new QueryTimeoutException(failure); } diff --git a/src/main/java/com/fauna/exception/FaunaException.java b/src/main/java/com/fauna/exception/FaunaException.java index c2be1e60..1f6b555e 100644 --- a/src/main/java/com/fauna/exception/FaunaException.java +++ b/src/main/java/com/fauna/exception/FaunaException.java @@ -1,12 +1,27 @@ package com.fauna.exception; +/** + * Represents a general exception for errors encountered within the Fauna client. + * This exception serves as the base class for other specific exceptions in the drive. + */ public class FaunaException extends RuntimeException { - public FaunaException(String message) { + /** + * Constructs a new {@code FaunaException} with the specified message. + * + * @param message A {@code String} describing the reason for the exception. + */ + public FaunaException(final String message) { super(message); } - public FaunaException(String message, Throwable err) { + /** + * Constructs a new {@code FaunaException} with the specified detail message and cause. + * + * @param message A {@code String} describing the reason for the exception. + * @param err The underlying {@code Throwable} cause of the exception. + */ + public FaunaException(final String message, final Throwable err) { super(message, err); } } diff --git a/src/main/java/com/fauna/exception/InvalidRequestException.java b/src/main/java/com/fauna/exception/InvalidRequestException.java index 0f3273eb..e08642a8 100644 --- a/src/main/java/com/fauna/exception/InvalidRequestException.java +++ b/src/main/java/com/fauna/exception/InvalidRequestException.java @@ -2,8 +2,21 @@ import com.fauna.response.QueryFailure; +/** + * Exception representing an invalid query request. + *

+ * This exception is thrown when a request sent to Fauna does not conform to the API specifications, + * typically due to malformed data, incorrect parameters, or other request-related issues. + * Extends {@link ServiceException} to provide specific details about the invalid request. + */ public class InvalidRequestException extends ServiceException { - public InvalidRequestException(QueryFailure response) { + + /** + * Constructs a new {@code InvalidRequestException} with the specified {@code QueryFailure} response. + * + * @param response The {@code QueryFailure} object containing details about the invalid request. + */ + public InvalidRequestException(final QueryFailure response) { super(response); } } diff --git a/src/main/java/com/fauna/exception/NullDocumentException.java b/src/main/java/com/fauna/exception/NullDocumentException.java index d37716b4..2c06b40b 100644 --- a/src/main/java/com/fauna/exception/NullDocumentException.java +++ b/src/main/java/com/fauna/exception/NullDocumentException.java @@ -2,28 +2,58 @@ import com.fauna.types.Module; +/** + * Exception representing a error in Fauna. + *

+ * This exception is thrown when a document is null in Fauna, providing details about + * the document ID, its collection, and the reason it is null. + * Extends {@link FaunaException} to provide information specific to null document scenarios. + */ public class NullDocumentException extends FaunaException { - private String id; - private Module coll; - private String nullCause; + private final String id; + private final Module coll; + private final String nullCause; + /** + * Constructs a new {@code NullDocumentException} with the specified document ID, collection, and cause. + * + * @param id The ID of the null document. + * @param coll The {@link Module} representing the collection of the document. + * @param nullCause A {@code String} describing the reason the document is null. + */ + public NullDocumentException(final String id, final Module coll, final String nullCause) { + super(String.format("Document %s in collection %s is null: %s", id, + coll != null ? coll.getName() : "unknown", nullCause)); + this.id = id; + this.coll = coll; + this.nullCause = nullCause; + } + + /** + * Retrieves the ID of the null document. + * + * @return A {@code String} representing the document ID. + */ public String getId() { return id; } + /** + * Retrieves the collection associated with the null document. + * + * @return A {@link Module} representing the document's collection, or {@code null} if unknown. + */ public Module getCollection() { return coll; } + /** + * Retrieves the cause for the document being null. + * + * @return A {@code String} describing why the document is null. + */ public String getNullCause() { return nullCause; } - - public NullDocumentException(String id, Module coll, String nullCause) { - super(String.format("Document %s in collection %s is null: %s", id, coll != null ? coll.getName() : "unknown", nullCause)); - this.id = id; - this.coll = coll; - this.nullCause = nullCause; - } } diff --git a/src/main/java/com/fauna/exception/ProtocolException.java b/src/main/java/com/fauna/exception/ProtocolException.java index 620519c1..82eda84f 100644 --- a/src/main/java/com/fauna/exception/ProtocolException.java +++ b/src/main/java/com/fauna/exception/ProtocolException.java @@ -1,32 +1,83 @@ package com.fauna.exception; +import com.fauna.response.QueryFailure; + import java.text.MessageFormat; +import java.util.Optional; +/** + * Exception representing protocol-level errors in communication with Fauna. + *

+ * This exception is typically thrown when there is an unexpected shape is received on response.. + * Extends {@link FaunaException} to provide details specific to protocol errors. + */ public class ProtocolException extends FaunaException { + private final int statusCode; + private final QueryFailure queryFailure; private final String body; - private static String buildMessage(int statusCode) { - return MessageFormat.format("ProtocolException HTTP {0}", statusCode); - } - - public ProtocolException(Throwable exc, int statusCode, String body) { - super(buildMessage(statusCode), exc); + /** + * Constructs a {@code ProtocolException} with the specified HTTP status code and {@code QueryFailure} details. + * + * @param statusCode The HTTP status code received. + * @param failure The {@link QueryFailure} object containing details about the protocol failure. + */ + public ProtocolException(final int statusCode, final QueryFailure failure) { + super(MessageFormat.format("ProtocolException HTTP {0}", statusCode)); this.statusCode = statusCode; - this.body = body; + this.queryFailure = failure; + this.body = null; } - public ProtocolException(int statusCode, String body) { + /** + * Constructs a {@code ProtocolException} with the specified HTTP status code and response body. + * + * @param statusCode The HTTP status code received. + * @param body A {@code String} containing the response body associated with the failure. + */ + public ProtocolException(final int statusCode, final String body) { super(buildMessage(statusCode)); this.statusCode = statusCode; this.body = body; + this.queryFailure = null; } + /** + * Builds a formatted error message based on the HTTP status code. + * + * @param statusCode The HTTP status code received. + * @return A formatted {@code String} message for the protocol error. + */ + private static String buildMessage(final int statusCode) { + return MessageFormat.format("ProtocolException HTTP {0}", statusCode); + } + + /** + * Retrieves the HTTP status code associated with this protocol error. + * + * @return An {@code int} representing the HTTP status code. + */ public int getStatusCode() { return this.statusCode; } + /** + * Retrieves the response body associated with this protocol error, if available. + * + * @return A {@code String} containing the response body, or {@code null} if the body is unavailable. + */ public String getBody() { return this.body; } + + /** + * Retrieves the {@link QueryFailure} details associated with this protocol error, if available. + * + * @return An {@code Optional} containing the failure details, or {@code Optional.empty()} if not + * present. + */ + public Optional getQueryFailure() { + return Optional.ofNullable(this.queryFailure); + } } diff --git a/src/main/java/com/fauna/exception/QueryCheckException.java b/src/main/java/com/fauna/exception/QueryCheckException.java index 53388a5c..2ccb7c8c 100644 --- a/src/main/java/com/fauna/exception/QueryCheckException.java +++ b/src/main/java/com/fauna/exception/QueryCheckException.java @@ -2,8 +2,21 @@ import com.fauna.response.QueryFailure; +/** + * Exception representing a query validation error in Fauna. + *

+ * This exception is thrown when a query fails one or more validation checks in Fauna, + * indicating issues with the query's syntax, or other query validation prior to execution. + * Extends {@link ServiceException} to provide information specific to query validation errors. + */ public class QueryCheckException extends ServiceException { - public QueryCheckException(QueryFailure failure) { + + /** + * Constructs a new {@code QueryCheckException} with the specified {@code QueryFailure} details. + * + * @param failure The {@link QueryFailure} object containing details about the validation failure. + */ + public QueryCheckException(final QueryFailure failure) { super(failure); } } diff --git a/src/main/java/com/fauna/exception/QueryRuntimeException.java b/src/main/java/com/fauna/exception/QueryRuntimeException.java index 3c5cd766..9c5381db 100644 --- a/src/main/java/com/fauna/exception/QueryRuntimeException.java +++ b/src/main/java/com/fauna/exception/QueryRuntimeException.java @@ -2,8 +2,20 @@ import com.fauna.response.QueryFailure; +/** + * Exception representing a runtime error encountered during query execution in Fauna. + *

+ * This exception is thrown when a query fails due to a runtime error. + * Extends {@link ServiceException} to provide details specific to runtime query errors. + */ public class QueryRuntimeException extends ServiceException { - public QueryRuntimeException(QueryFailure response) { + + /** + * Constructs a new {@code QueryRuntimeException} with the specified {@code QueryFailure} details. + * + * @param response The {@link QueryFailure} object containing details about the runtime error. + */ + public QueryRuntimeException(final QueryFailure response) { super(response); } } diff --git a/src/main/java/com/fauna/exception/QueryTimeoutException.java b/src/main/java/com/fauna/exception/QueryTimeoutException.java index 64dc7f29..44a89c28 100644 --- a/src/main/java/com/fauna/exception/QueryTimeoutException.java +++ b/src/main/java/com/fauna/exception/QueryTimeoutException.java @@ -2,8 +2,21 @@ import com.fauna.response.QueryFailure; +/** + * Exception representing a timeout error encountered during query execution in Fauna. + *

+ * This exception is thrown when a query fails to complete within the specified time limit, + * indicating that the query timeout was exceeded. + * Extends {@link ServiceException} to provide details specific to query timeout errors. + */ public class QueryTimeoutException extends ServiceException { - public QueryTimeoutException(QueryFailure response) { + + /** + * Constructs a new {@code QueryTimeoutException} with the specified {@code QueryFailure} details. + * + * @param response The {@link QueryFailure} object containing details about the timeout error. + */ + public QueryTimeoutException(final QueryFailure response) { super(response); } } diff --git a/src/main/java/com/fauna/exception/RetryableException.java b/src/main/java/com/fauna/exception/RetryableException.java index 12ca6cfd..545fe84d 100644 --- a/src/main/java/com/fauna/exception/RetryableException.java +++ b/src/main/java/com/fauna/exception/RetryableException.java @@ -1,4 +1,11 @@ package com.fauna.exception; +/** + * Marker interface for exceptions that indicate a retryable operation. + * Exceptions implementing this interface suggest that the operation may be retried, + * as the error might be transient or recoverable. + * + *

This interface allows for easy identification of retryable exceptions in Fauna.

+ */ public interface RetryableException { } diff --git a/src/main/java/com/fauna/exception/ServiceException.java b/src/main/java/com/fauna/exception/ServiceException.java index a0631bd9..ad0b6fba 100644 --- a/src/main/java/com/fauna/exception/ServiceException.java +++ b/src/main/java/com/fauna/exception/ServiceException.java @@ -1,34 +1,35 @@ package com.fauna.exception; -import java.util.Map; - import com.fauna.response.QueryFailure; import com.fauna.response.QueryStats; +import java.util.Map; +import java.util.Optional; + /** * An exception representing a query failure returned by Fauna. * - *

The exception extends {@link FaunaException} and contains details about - * the failed query, including HTTP status codes, error codes, and other - * metadata.

+ *

This exception extends {@link FaunaException} and provides detailed information + * about the failed query, including HTTP status codes, error codes, statistics, + * and other metadata.

*/ public class ServiceException extends FaunaException { private final QueryFailure response; /** - * Constructs a new ServiceException with the specified QueryFailure response. + * Constructs a new {@code ServiceException} with the specified {@code QueryFailure} response. * - * @param response the QueryFailure object containing details about the failed query + * @param response The {@code QueryFailure} object containing details about the failed query. */ - public ServiceException(QueryFailure response) { + public ServiceException(final QueryFailure response) { super(response.getFullMessage()); this.response = response; } /** - * Returns the QueryFailure response associated with the exception. + * Returns the {@link QueryFailure} response associated with this exception. * - * @return the QueryFailure object + * @return The {@code QueryFailure} object containing details of the query failure. */ public QueryFailure getResponse() { return this.response; @@ -37,66 +38,65 @@ public QueryFailure getResponse() { /** * Returns the HTTP status code of the response returned by the query request. * - * @return the HTTP status code as an integer + * @return The HTTP status code as an integer. */ public int getStatusCode() { return this.response.getStatusCode(); } /** - * Returns the - *
Fauna error code - * associated with the failure. + * Returns the + * Fauna error code associated with the failure. * - *

Codes indicate the cause of the error. It is safe to write - * programmatic logic against the code. They are part of the API contract.

+ *

Fauna error codes indicate the specific cause of the error and are part of the API contract, + * allowing for programmatic logic based on the error type.

* - * @return the error code as a String + * @return The error code as a {@code String}. */ public String getErrorCode() { return this.response.getErrorCode(); } /** - * Returns a summary of the error. + * Returns a brief summary of the error. * - * @return a String containing the error summary + * @return A {@code String} containing the error summary. */ public String getSummary() { return this.response.getSummary(); } /** - * Returns the statistics for the failed query. + * Returns the statistics associated with the failed query. * - * @return a QueryStats object containing statistical information + * @return A {@link QueryStats} object containing statistical information for the failed query. */ public QueryStats getStats() { return this.response.getStats(); } /** - * Returns the faled query's last transaction timestamp. + * Returns the last transaction timestamp seen for the failed query, if available. * - * @return the transaction timestamp as a long value + * @return An {@code Optional} representing the last transaction timestamp, or {@code Optional.empty()} if not available. */ - public long getTxnTs() { - return this.response.getLastSeenTxn(); + public Optional getTxnTs() { + return Optional.ofNullable(this.response.getLastSeenTxn()); } /** - * The schema version that was used for query execution. + * Returns the schema version used during query execution. * - * @return the schema version as a long value + * @return The schema version as a {@code Long} value. */ - public long getSchemaVersion() { + public Long getSchemaVersion() { return this.response.getSchemaVersion(); } /** - * Returns a map of query tags for the failed query. + * Returns a map of query tags for the failed query, containing key-value pairs of tags. * - * @return a Map containing query tags as key-value pairs + * @return A {@code Map} with query tags. */ public Map getQueryTags() { return this.response.getQueryTags(); diff --git a/src/main/java/com/fauna/exception/ServiceInternalException.java b/src/main/java/com/fauna/exception/ServiceInternalException.java index f6bd1c70..aaea75c6 100644 --- a/src/main/java/com/fauna/exception/ServiceInternalException.java +++ b/src/main/java/com/fauna/exception/ServiceInternalException.java @@ -2,8 +2,21 @@ import com.fauna.response.QueryFailure; +/** + * Exception representing an unexpected internal server error in Fauna. + *

+ * This exception is thrown when Fauna encounters an unexpected internal error that prevents + * it from completing a request, typically indicating a server-side issue. + * Extends {@link ServiceException} to provide details specific to internal server errors. + */ public class ServiceInternalException extends ServiceException { - public ServiceInternalException(QueryFailure response) { + + /** + * Constructs a new {@code ServiceInternalException} with the specified {@code QueryFailure} details. + * + * @param response The {@link QueryFailure} object containing details about the internal server error. + */ + public ServiceInternalException(final QueryFailure response) { super(response); } } diff --git a/src/main/java/com/fauna/exception/ThrottlingException.java b/src/main/java/com/fauna/exception/ThrottlingException.java index 39e23c9a..de137a88 100644 --- a/src/main/java/com/fauna/exception/ThrottlingException.java +++ b/src/main/java/com/fauna/exception/ThrottlingException.java @@ -2,8 +2,21 @@ import com.fauna.response.QueryFailure; +/** + * Exception representing a throttling error in Fauna, indicating that a query exceeded + * plan throughput limits. + *

+ * It implements {@link RetryableException} + * Extends {@link ServiceException} to provide details specific to throttling errors. + */ public class ThrottlingException extends ServiceException implements RetryableException { - public ThrottlingException(QueryFailure response) { + + /** + * Constructs a new {@code ThrottlingException} with the specified {@code QueryFailure} details. + * + * @param response The {@link QueryFailure} object containing details about the throttling error. + */ + public ThrottlingException(final QueryFailure response) { super(response); } } diff --git a/src/main/java/com/fauna/mapping/FieldInfo.java b/src/main/java/com/fauna/mapping/FieldInfo.java index 0567b5c1..87564027 100644 --- a/src/main/java/com/fauna/mapping/FieldInfo.java +++ b/src/main/java/com/fauna/mapping/FieldInfo.java @@ -6,6 +6,10 @@ import java.lang.reflect.Field; import java.lang.reflect.Type; +/** + * Represents metadata for a Fauna document field in a class, including its name, type, associated codec, + * and other properties used for serialization and deserialization. + */ public final class FieldInfo { private final String name; @@ -16,7 +20,23 @@ public final class FieldInfo { private final Field field; private Codec codec; - public FieldInfo(Field field, String name, Class clazz, Type[] genericTypeArgs, CodecProvider provider, FieldType fieldType) { + /** + * Constructs a {@code FieldInfo} object with the specified field metadata. + * + * @param field The field represented by this {@code FieldInfo} instance. + * @param name The name of the field. + * @param clazz The class type of the field. + * @param genericTypeArgs An array of generic type arguments for the field, if any. + * @param provider The {@link CodecProvider} used to obtain a codec for this field. + * @param fieldType The {@link FieldType} of the field. + */ + public FieldInfo( + final Field field, + final String name, + final Class clazz, + final Type[] genericTypeArgs, + final CodecProvider provider, + final FieldType fieldType) { this.field = field; this.name = name; this.clazz = clazz; @@ -25,30 +45,61 @@ public FieldInfo(Field field, String name, Class clazz, Type[] genericTypeArg this.fieldType = fieldType; } + /** + * Retrieves the name of the field. + * + * @return A {@code String} representing the field name. + */ public String getName() { return name; } + /** + * Retrieves the class type of the field. + * + * @return A {@code Class} representing the field's class type. + */ public Class getType() { return clazz; } + /** + * Retrieves the {@link FieldType} of this field. + * + * @return The {@code FieldType} associated with this field. + */ public FieldType getFieldType() { return fieldType; } + /** + * Retrieves the codec used to serialize and deserialize the field. If the codec is not already set, + * it will be retrieved from the {@link CodecProvider} and cached. + * + * @return A {@code Codec} instance associated with the field type. + */ + @SuppressWarnings("rawtypes") public Codec getCodec() { - if (codec != null) return codec; + if (codec != null) { + return codec; + } synchronized (this) { - // check again in case it was set by another thread - if (codec != null) return codec; + // Double-checked locking to ensure thread-safe lazy initialization + if (codec != null) { + return codec; + } codec = provider.get(clazz, genericTypeArgs); } return codec; } + /** + * Retrieves the {@code Field} object representing this field in the class. + * + * @return The {@code Field} associated with this {@code FieldInfo}. + */ public Field getField() { return field; } diff --git a/src/main/java/com/fauna/mapping/FieldName.java b/src/main/java/com/fauna/mapping/FieldName.java index d1d83cab..dea87ff3 100644 --- a/src/main/java/com/fauna/mapping/FieldName.java +++ b/src/main/java/com/fauna/mapping/FieldName.java @@ -1,9 +1,24 @@ package com.fauna.mapping; -public class FieldName { +/** + * Utility class for handling field names. + */ +public final class FieldName { - public static String canonical(String name) { - if (name == null || name.isEmpty() || Character.isLowerCase(name.charAt(0))) { + private FieldName() { + } + + /** + * Converts the given field name to a canonical format where the first character is lowercase. + * If the name is null, empty, or already starts with a lowercase letter, it is unchanged. + * + * @param name The field name to be converted. + * @return The canonicalized field name, or the original name if it is null, empty, or already starts with a + * lowercase letter. + */ + public static String canonical(final String name) { + if (name == null || name.isEmpty() + || Character.isLowerCase(name.charAt(0))) { return name; } else { return Character.toLowerCase(name.charAt(0)) + name.substring(1); diff --git a/src/main/java/com/fauna/mapping/FieldType.java b/src/main/java/com/fauna/mapping/FieldType.java index 1fc6159e..783aea15 100644 --- a/src/main/java/com/fauna/mapping/FieldType.java +++ b/src/main/java/com/fauna/mapping/FieldType.java @@ -1,10 +1,37 @@ package com.fauna.mapping; +/** + * Enum representing the different types of fields that can be used in Fauna document field mappings. + * Each field type specifies a distinct role or characteristic of a field in the mapping process. + */ public enum FieldType { + /** + * Represents a client-generated document ID. + * Typically used when the ID is provided by the client and not generated by Fauna. + */ ClientGeneratedId, + + /** + * Represents a Fauna-generated document ID. + * Typically used for document IDs auto-generated by Fauna. + */ ServerGeneratedId, + + /** + * Represents the document's `coll` (collection) metadata field. + * Used to denote the collection to which the document belongs. + */ Coll, + + /** + * Represents the document's `ts` (timestamp) metadata field. + * Used to track the last write to the document. + */ Ts, + + /** + * Represents a user-defined document field. + */ Field, } diff --git a/src/main/java/com/fauna/mapping/package-info.java b/src/main/java/com/fauna/mapping/package-info.java new file mode 100644 index 00000000..0b569110 --- /dev/null +++ b/src/main/java/com/fauna/mapping/package-info.java @@ -0,0 +1,17 @@ +/** + * The {@code com.fauna.mapping} package provides classes and utilities used to + * map Fauna document fields for serialization and deserialization in the client. + * + *

The classes in this package include: + *

    + *
  • {@link com.fauna.mapping.FieldInfo}: Holds metadata for individual fields, such as name, + * type, and codec, used to map and handle fields within a Fauna data model.
  • + * + *
  • {@link com.fauna.mapping.FieldName}: Provides utility methods for handling field names, + * including a method to convert names to a canonical format.
  • + * + *
  • {@link com.fauna.mapping.FieldType}: Defines various field types that can exist within + * Fauna mappings, such as identifiers, timestamps, and general-purpose fields.
  • + *
+ */ +package com.fauna.mapping; diff --git a/src/main/java/com/fauna/query/AfterToken.java b/src/main/java/com/fauna/query/AfterToken.java new file mode 100644 index 00000000..c8b39be3 --- /dev/null +++ b/src/main/java/com/fauna/query/AfterToken.java @@ -0,0 +1,45 @@ +package com.fauna.query; + +import java.util.Optional; + +/** + * Represents an `after` token used for Set + * pagination. + */ +public class AfterToken { + + private final String token; + + /** + * Constructs an {@code AfterToken} with the specified token. + * + * @param token the token to be stored in this {@code AfterToken} instance. + */ + public AfterToken(final String token) { + this.token = token; + } + + /** + * Returns the token stored in this {@code AfterToken} instance. + * + * @return the token as a {@code String}. + */ + public String getToken() { + return token; + } + + /** + * Creates an {@code AfterToken} instance from the specified token string. + * If the provided token is {@code null}, an empty {@code Optional} is + * returned. + * + * @param token the token string to convert into an {@code AfterToken}. + * @return an {@code Optional} containing an {@code AfterToken} if the + * token is non-null, or an empty {@code Optional} if it is null. + */ + public static Optional fromString(final String token) { + return Optional.ofNullable( + token != null ? new AfterToken(token) : null); + } +} diff --git a/src/main/java/com/fauna/query/QueryOptions.java b/src/main/java/com/fauna/query/QueryOptions.java index ba105d4b..285bff8b 100644 --- a/src/main/java/com/fauna/query/QueryOptions.java +++ b/src/main/java/com/fauna/query/QueryOptions.java @@ -1,20 +1,28 @@ package com.fauna.query; import java.time.Duration; -import java.util.Map; import java.util.Optional; +import static com.fauna.constants.Defaults.DEFAULT_TIMEOUT; + +/** + * Encapsulates options for configuring Fauna queries, such as timeout, + * linearized reads, typechecking, query tags, and trace parent for + * distributed tracing. + */ public class QueryOptions { private final Boolean linearized; private final Boolean typeCheck; private final Duration timeout; - private final Map queryTags; + private final QueryTags queryTags; private final String traceParent; - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); - public static QueryOptions DEFAULT = QueryOptions.builder().build(); - - public QueryOptions(Builder builder) { + /** + * Creates an instance of QueryOptions using the specified builder. + * + * @param builder the builder with values for query options. + */ + public QueryOptions(final Builder builder) { this.linearized = builder.linearized; this.typeCheck = builder.typeCheck; this.timeout = builder.timeout; @@ -22,66 +30,174 @@ public QueryOptions(Builder builder) { this.traceParent = builder.traceParent; } + /** + * Default QueryOptions instance with default configurations. + * + * @return a new QueryOptions instance with defaults. + */ + public static QueryOptions getDefault() { + return QueryOptions.builder().build(); + } + + /** + * Returns an Optional indicating if linearized reads are enabled. + * + * @return an Optional containing the linearized setting, or empty if not + * specified. + */ public Optional getLinearized() { return Optional.ofNullable(this.linearized); } + /** + * Returns an Optional indicating if type checking is enabled. + * + * @return an Optional containing the typeCheck setting, or empty if not + * specified. + */ public Optional getTypeCheck() { return Optional.ofNullable(this.typeCheck); } + /** + * Returns an Optional of the query timeout duration in milliseconds. + * + * @return an Optional containing the query timeout duration in milliseconds, or + * empty if not specified. + */ public Optional getTimeoutMillis() { return Optional.ofNullable(this.timeout).map(Duration::toMillis); } - public Optional> getQueryTags() { + /** + * Returns an Optional of the query tags. + * + * @return an Optional containing the QueryTags, or empty if not specified. + */ + public Optional getQueryTags() { return Optional.ofNullable(this.queryTags); } + /** + * Returns an Optional of the trace parent for distributed tracing. + * + * @return an Optional containing the traceParent, or empty if not + * specified. + */ public Optional getTraceParent() { return Optional.ofNullable(this.traceParent); } + /** + * Builder class for constructing instances of QueryOptions. + */ public static class Builder { - public Boolean linearized = null; - public Boolean typeCheck = null; - public Duration timeout = DEFAULT_TIMEOUT; - public Map queryTags = null; - public String traceParent = null; - - public Builder linearized(boolean linearized) { + private Boolean linearized = null; + private Boolean typeCheck = null; + private Duration timeout = DEFAULT_TIMEOUT; + private QueryTags queryTags = null; + private String traceParent = null; + + /** + * If true, read-only transactions that don't read indexes are strictly + * serialized. + * + * @param linearized true to enable linearized reads, false otherwise. + * @return this Builder instance for chaining. + */ + public Builder linearized(final boolean linearized) { this.linearized = linearized; return this; } - public Builder typeCheck(boolean typeCheck) { + /** + * If true, typechecking + * is enabled for queries. You can only enable typechecking for databases that + * have typechecking enabled. + * + * @param typeCheck true to enable type checking, false otherwise. + * @return this Builder instance for chaining. + */ + public Builder typeCheck(final boolean typeCheck) { this.typeCheck = typeCheck; return this; } - public Builder timeout(Duration timeout) { + /** + * Sets the timeout duration for the query. + * + * @param timeout the timeout Duration for the query. + * @return this Builder instance for chaining. + */ + public Builder timeout(final Duration timeout) { this.timeout = timeout; return this; } - public Builder queryTags(Map tags) { - this.queryTags = tags; + /** + * Sets query tags used to + * instrument the query. You typically use query tags to monitor and debug query requests in + * Fauna Logs. + * + * @param queryTags the QueryTags to associate with the query. + * @return this Builder instance for chaining. + */ + public Builder queryTags(final QueryTags queryTags) { + if (this.queryTags != null) { + this.queryTags.putAll(queryTags); + } else { + this.queryTags = queryTags; + } return this; } - public Builder traceParent(String traceParent) { + /** + * Adds a single query tag to the existing tags. + * + * @param key the key of the query tag. + * @param value the value of the query tag. + * @return this Builder instance for chaining. + */ + public Builder queryTag(final String key, final String value) { + if (this.queryTags == null) { + this.queryTags = new QueryTags(); + } + this.queryTags.put(key, value); + return this; + } + + /** + * Traceparent identifier used for distributed tracing. Passed by the drive in the `traceparent` header of Query + * HTTP endpoint requests. If you don’t include a traceparent identifier or use an invalid identifier, + * Fauna generates a valid identifier. + * + * @param traceParent the trace parent ID. + * @return this Builder instance for chaining. + */ + public Builder traceParent(final String traceParent) { this.traceParent = traceParent; return this; } + /** + * Builds and returns a new instance of QueryOptions. + * + * @return a new QueryOptions instance with the configured settings. + */ public QueryOptions build() { return new QueryOptions(this); } } + /** + * Creates and returns a new Builder instance for constructing QueryOptions. + * + * @return a new Builder instance. + */ public static Builder builder() { return new Builder(); } - } diff --git a/src/main/java/com/fauna/query/QueryTags.java b/src/main/java/com/fauna/query/QueryTags.java new file mode 100644 index 00000000..437f301f --- /dev/null +++ b/src/main/java/com/fauna/query/QueryTags.java @@ -0,0 +1,83 @@ +package com.fauna.query; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fauna.exception.ClientResponseException; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A utility class representing a collection of query tags as a map of key-value pairs. + * This class extends {@link HashMap} and provides methods to build, encode, and parse query tags. + */ +public class QueryTags extends HashMap { + private static final String EQUALS = "="; + private static final String COMMA = ","; + + /** + * Creates a new {@code QueryTags} instance from an array of tag strings. + * Each tag string must be in the form "key=value". + * + * @param tags an array of strings representing tags in the format "k=v", + * where {@code k} is the tag key and {@code v} is the tag value. + * @return a new {@code QueryTags} instance containing the parsed tags. + * @throws ClientResponseException if a tag string is not in the expected "k=v" format. + */ + public static QueryTags of(final String... tags) { + QueryTags queryTags = new QueryTags(); + for (String tagString : tags) { + String[] tag = tagString.split(EQUALS); + if (tag.length == 2) { + queryTags.put(tag[0].strip(), tag[1].strip()); + } else { + throw new ClientResponseException( + "Invalid tag encoding: " + tagString); + } + } + return queryTags; + } + + /** + * Encodes the {@code QueryTags} instance as a single string. + * The tags are sorted by their keys and concatenated in the format "key=value,key=value,...". + * + * @return a {@code String} representing the encoded query tags. + */ + public String encode() { + return this.entrySet().stream().sorted(Map.Entry.comparingByKey()) + .map(entry -> String.join(EQUALS, entry.getKey(), + entry.getValue())) + .collect(Collectors.joining(COMMA)); + } + + /** + * Parses a JSON parser to construct a {@code QueryTags} instance. + * This method expects the JSON to contain either a null value or a string representation of tags in + * "key=value,key=value,..." format. + * + * @param parser a {@code JsonParser} positioned at the JSON data to parse. + * @return a {@code QueryTags} instance representing the parsed tags, or {@code null} if the JSON value is null. + * @throws IOException if an error occurs during parsing. + * @throws ClientResponseException if the JSON token is not a string or null. + */ + public static QueryTags parse(final JsonParser parser) throws IOException { + if (parser.nextToken() == JsonToken.VALUE_NULL) { + return null; + } else if (parser.getCurrentToken() == JsonToken.VALUE_STRING) { + String tagString = parser.getText(); + if (!tagString.isEmpty()) { + return QueryTags.of(tagString.split(COMMA)); + } else { + return new QueryTags(); + } + } else { + throw new ClientResponseException( + "Unexpected token for QueryTags: " + + parser.getCurrentToken()); + } + } +} diff --git a/src/main/java/com/fauna/query/StreamOptions.java b/src/main/java/com/fauna/query/StreamOptions.java deleted file mode 100644 index 53c10cc9..00000000 --- a/src/main/java/com/fauna/query/StreamOptions.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.fauna.query; - -import com.fauna.client.RetryStrategy; - -import java.util.Optional; - -public class StreamOptions { - - private final RetryStrategy retryStrategy; - private final Long startTimestamp; - private final Boolean statusEvents; - - public StreamOptions(Builder builder) { - this.retryStrategy = builder.retryStrategy; - this.startTimestamp = builder.startTimestamp; - this.statusEvents = builder.statusEvents; - } - - public Optional getRetryStrategy() { - return Optional.ofNullable(retryStrategy); - } - - public Optional getStartTimestamp() { - return Optional.ofNullable(startTimestamp); - - } - - public Optional getStatusEvents() { - return Optional.ofNullable(statusEvents); - } - - - public static class Builder { - public RetryStrategy retryStrategy = null; - public Long startTimestamp = null; - public Boolean statusEvents = null; - - public Builder withRetryStrategy(RetryStrategy retryStrategy) { - this.retryStrategy = retryStrategy; - return this; - } - - public Builder withStartTimestamp(long startTimestamp) { - this.startTimestamp = startTimestamp; - return this; - } - - public Builder withStatusEvents(Boolean statusEvents) { - this.statusEvents = statusEvents; - return this; - } - - public StreamOptions build() { - return new StreamOptions(this); - } - } - - public static Builder builder() { - return new Builder(); - } -} diff --git a/src/main/java/com/fauna/query/StreamTokenResponse.java b/src/main/java/com/fauna/query/StreamTokenResponse.java deleted file mode 100644 index cecf30ae..00000000 --- a/src/main/java/com/fauna/query/StreamTokenResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.fauna.query; - - -import java.util.Objects; - -public class StreamTokenResponse { - private String token; - - public StreamTokenResponse(String token) { - this.token = token; - } - - public StreamTokenResponse() {} - - public String getToken() { - return this.token; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - - if (o == null) return false; - - if (getClass() != o.getClass()) return false; - - StreamTokenResponse c = (StreamTokenResponse) o; - - return Objects.equals(token, c.token); - } - - @Override - public int hashCode() { - return Objects.hash(token); - } -} diff --git a/src/main/java/com/fauna/query/builder/Query.java b/src/main/java/com/fauna/query/builder/Query.java index decdaff6..282290d0 100644 --- a/src/main/java/com/fauna/query/builder/Query.java +++ b/src/main/java/com/fauna/query/builder/Query.java @@ -9,58 +9,74 @@ /** - * Represents a Fauna query that is constructed from fragments. - * This class allows the building of queries from literal and variable parts. + * Represents a Fauna query that is constructed from multiple query fragments. This class enables the creation of + * queries from both literal strings and variable placeholders. */ +@SuppressWarnings("rawtypes") public class Query extends QueryFragment { private final QueryFragment[] fql; /** - * Construct a Query from the given template String and args. - * @param query A Fauna Query Language (FQL) v10 template string. - * @param args A map of variable names -> values. + * Constructs a Query instance based on the given template string and variable arguments. + * + * @param query A Fauna Query Language (FQL) v10 template string containing both literals and variable placeholders. + * Placeholders should follow the syntax defined by {@link FaunaTemplate}. + * @param args A map of variable names to their corresponding values. This map provides the values that are + * substituted for placeholders in the template string. + * @throws IllegalArgumentException if any placeholder in the template string lacks a matching entry in + * {@code args}. */ - public Query(String query, Map args) throws IllegalArgumentException { - Spliterator iter = new FaunaTemplate(query).spliterator(); + public Query(final String query, final Map args) + throws IllegalArgumentException { + Spliterator iter = + new FaunaTemplate(query).spliterator(); this.fql = StreamSupport.stream(iter, true).map( part -> { - Map foo = Objects.requireNonNullElse(args, Map.of()); + Map foo = + Objects.requireNonNullElse(args, Map.of()); return part.toFragment(foo); }).toArray(QueryFragment[]::new); } - /** - * Creates a Query instance from a String and arguments. - * The template strings can contain literals and variable placeholders. + * Creates a Query instance based on a template string and a set of arguments. The template string can contain both + * literals and variable placeholders, allowing for dynamic query construction. * - * @param query A Fauna Query Language (FQL) v10 template string. - * @param args A map of variable names -> values. - * @return a Query instance representing the complete query. - * @throws IllegalArgumentException if a template variable does not have a corresponding entry in the provided args. + * @param query A Fauna Query Language (FQL) v10 template string. It may contain variables designated by + * placeholders. + * @param args A map associating variable names with their values for substitution within the query. If + * {@code null}, no variables will be substituted. + * @return a Query instance representing the constructed query. + * @throws IllegalArgumentException if any placeholder in the template string lacks a corresponding entry in + * {@code args}. */ - public static Query fql(String query, - Map args) throws IllegalArgumentException { + public static Query fql( + final String query, + final Map args) + throws IllegalArgumentException { return new Query(query, args); } /** - * Creates a Query instance from a String. Without any args, the template string cannot contain variables. + * Creates a Query instance based solely on a template string without any arguments. The template string should + * contain only literals since no variable values are provided. * - * @param query the string template of the query. - * @return a Query instance representing the complete query. - * @throws IllegalArgumentException if a template variable does not have a corresponding entry in the provided args. + * @param query A Fauna Query Language (FQL) v10 template string. This version of the template should contain no + * placeholders, as there are no arguments for substitution. + * @return a Query instance representing the constructed query. + * @throws IllegalArgumentException if the template contains placeholders without a matching entry in the provided + * arguments. */ - public static Query fql(String query) throws IllegalArgumentException { + public static Query fql(final String query) + throws IllegalArgumentException { return fql(query, null); } /** - * Retrieves the list of fragments that make up this query. + * Retrieves the list of fragments that compose this query, where each fragment is either a literal or a variable. * - * @return an array of Fragments. - * @throws IllegalArgumentException if a template variable does not have a corresponding entry in the provided args. + * @return an array of QueryFragment instances representing the parts of the query. */ @Override public QueryFragment[] get() { diff --git a/src/main/java/com/fauna/query/builder/QueryArr.java b/src/main/java/com/fauna/query/builder/QueryArr.java index bf666d64..cc5f7915 100644 --- a/src/main/java/com/fauna/query/builder/QueryArr.java +++ b/src/main/java/com/fauna/query/builder/QueryArr.java @@ -4,51 +4,102 @@ import java.util.Objects; /** - * Represents a value fragment of a Fauna query. - * This class encapsulates a value that can be a variable in the query. + * Represents a special type that allows Fauna to evaluate an array of individual + * queries, each of which will be processed, and its result will be an element + * in the returned array. + *

+ * Example usage: + *

+ *   var listOfQueries = List.of(fql("1 + 1"), fql("2 + 2"), fql("3 + 3"));
+ * 
+ * Directly providing this list to a query will fail because it would be treated + * as a {@code QueryVal}. By wrapping it in a {@code QueryArr}, each query within the + * list will be evaluated separately: + *
+ *   client.query(fql("${queries}", Map.of("queries", listOfQueries)));
+ *   // Error: the list is treated as a single value.
+ *
+ *   client.query(fql("${queries}", Map.of("queries", QueryArr.of(listOfQueries)));
+ *   // Returns: [2, 4, 6]
+ * 
+ * + * @param The type of elements in the QueryArr, which must be a subtype of + * {@link QueryFragment}. */ -public class QueryArr extends QueryFragment> { - public static QueryArr of(List val) { - return new QueryArr(val); - } +@SuppressWarnings("rawtypes") +public final class QueryArr + extends QueryFragment> { private final List value; /** - * Constructs a ValueFragment with the specified value. + * Static factory method to create a new {@code QueryArr} instance. + * + * @param val the list of {@link QueryFragment} elements to wrap. + * @param the type of elements in the list, which must extend {@link QueryFragment}. + * @return a new instance of {@code QueryArr} encapsulating the provided list. + */ + public static QueryArr of(final List val) { + return new QueryArr<>(val); + } + + /** + * Constructs a {@code QueryArr} with the specified list of query fragments. * - * @param value the value to encapsulate. + * @param value the list of query fragments to encapsulate. */ - public QueryArr(List value) { + public QueryArr(final List value) { this.value = value; } /** - * Retrieves the encapsulated value of this fragment. + * Retrieves the encapsulated list of query fragments in this {@code QueryArr}. * - * @return the encapsulated object. + * @return the list of query fragments. */ @Override public List get() { return value; } + /** + * Checks if this {@code QueryArr} is equal to another object. + * Two {@code QueryArr} objects are equal if they contain the same list of query fragments. + * + * @param o the object to compare with. + * @return {@code true} if the specified object is equal to this {@code QueryArr}; + * {@code false} otherwise. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } - QueryArr that = (QueryArr) o; + QueryArr that = (QueryArr) o; return Objects.equals(value, that.value); } + /** + * Returns the hash code of this {@code QueryArr}, based on its encapsulated list of query fragments. + * + * @return the hash code of this object. + */ @Override public int hashCode() { return value != null ? value.hashCode() : 0; } - public Object getValue() { + /** + * Retrieves the encapsulated list directly. + * + * @return the encapsulated list of query fragments. + */ + public List getValue() { return this.value; } } diff --git a/src/main/java/com/fauna/query/builder/QueryFragment.java b/src/main/java/com/fauna/query/builder/QueryFragment.java index 6d7f2c1f..900fe6b9 100644 --- a/src/main/java/com/fauna/query/builder/QueryFragment.java +++ b/src/main/java/com/fauna/query/builder/QueryFragment.java @@ -1,12 +1,9 @@ package com.fauna.query.builder; -import com.fauna.codec.CodecProvider; -import com.fauna.codec.UTF8FaunaGenerator; - -import java.io.IOException; /** * An abstract class serving as a base for different types of query fragments. + * @param The type of QueryFragment. */ public abstract class QueryFragment { diff --git a/src/main/java/com/fauna/query/builder/QueryLiteral.java b/src/main/java/com/fauna/query/builder/QueryLiteral.java index 5d8a85e9..940f22aa 100644 --- a/src/main/java/com/fauna/query/builder/QueryLiteral.java +++ b/src/main/java/com/fauna/query/builder/QueryLiteral.java @@ -1,37 +1,37 @@ package com.fauna.query.builder; import com.fasterxml.jackson.annotation.JsonValue; -import com.fauna.codec.CodecProvider; -import com.fauna.codec.UTF8FaunaGenerator; -import java.io.IOException; import java.util.Objects; /** * Represents a literal fragment of a Fauna query. * This class encapsulates a fixed string that does not contain any variables. + * A {@code QueryLiteral} is used to represent literal values in a query. */ -public class QueryLiteral extends QueryFragment { +@SuppressWarnings("rawtypes") +public final class QueryLiteral extends QueryFragment { private final String value; /** - * Constructs a new {@code LiteralFragment} with the given literal value. + * Constructs a new {@code QueryLiteral} with the given literal value. * * @param value the string value of this fragment; must not be null. * @throws IllegalArgumentException if {@code value} is null. */ - public QueryLiteral(String value) { + public QueryLiteral(final String value) { if (value == null) { - throw new IllegalArgumentException("A literal value must not be null"); + throw new IllegalArgumentException( + "A literal value must not be null"); } this.value = value; } /** - * Retrieves the string value of this fragment. + * Retrieves the string value of this literal fragment. * - * @return the string value. + * @return the string value of this fragment. */ @Override public String get() { @@ -39,9 +39,13 @@ public String get() { } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } QueryLiteral that = (QueryLiteral) o; @@ -50,12 +54,16 @@ public boolean equals(Object o) { @Override public int hashCode() { - return value != null ? value.hashCode() : 0; + return value.hashCode(); } + /** + * Gets the wrapped literal value. + * + * @return The literal value as a string. + */ @JsonValue public String getValue() { return this.value; } - } diff --git a/src/main/java/com/fauna/query/builder/QueryObj.java b/src/main/java/com/fauna/query/builder/QueryObj.java index 11de7c2b..2663e789 100644 --- a/src/main/java/com/fauna/query/builder/QueryObj.java +++ b/src/main/java/com/fauna/query/builder/QueryObj.java @@ -4,40 +4,74 @@ import java.util.Objects; /** - * Represents an object fragment of a Fauna query. Object fragments allow for the evaluation of FQL statements - * stored on the object. This class encapsulates an object that can be a variable in the query. + * This class represents a special type of query fragment that allows users + * to provide Fauna with an object whose values are individual queries. + * Each of these queries will be evaluated, and the result of each query + * will be a value in the returned object. + * + *

+ * Example usage: + * Given a map of queries: + *

+ *   var o = Map.of("key1", fql("1 + 1"));
+ * 
+ * If this map is passed directly to a query, it will fail because the entire + * object will be treated as a value. However, if you wrap the map in a + * {@code QueryObj}, Fauna will evaluate each query. + *
+ *   client.query(fql("${obj}", Map.of("obj", o));
+ *   // Error: the map is treated as a value.
+ *
+ *   client.query(fql("${obj}", Map.of("obj", QueryObj.of(o)))
+ *   // Result: { "key1": 2 }
+ * 
+ * + * @param The type of {@code QueryObj}. Must be a subtype of {@code QueryFragment}. */ -public class QueryObj extends QueryFragment> { +@SuppressWarnings("rawtypes") +public final class QueryObj extends QueryFragment> { - public static QueryObj of(Map val) { + /** + * Creates a new {@code QueryObj} instance with the specified map of query fragments. + * + * @param val the map of query fragments to wrap. + * @param The map value type, which must extend {@code QueryFragment}. + * @return a new {@code QueryObj} instance wrapping the provided map. + */ + public static QueryObj of(final Map val) { + //noinspection unchecked return new QueryObj(val); } private final Map value; /** - * Constructs a QueryObj with the specified value. + * Constructs a new {@code QueryObj} with the given map of query fragments. * - * @param value the value to encapsulate. + * @param value the map to encapsulate. */ - public QueryObj(Map value) { + public QueryObj(final Map value) { this.value = value; } /** - * Retrieves the encapsulated value of this fragment. + * Retrieves the encapsulated map of query fragments that make up this query object. * - * @return the encapsulated object. + * @return the map of query fragments. */ @Override - public Map get() { + public Map get() { return value; } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } QueryObj that = (QueryObj) o; @@ -49,7 +83,12 @@ public int hashCode() { return value != null ? value.hashCode() : 0; } - public Object getValue() { + /** + * Retrieves the wrapped map value. + * + * @return the encapsulated map. + */ + public Map getValue() { return this.value; } } diff --git a/src/main/java/com/fauna/query/builder/QueryVal.java b/src/main/java/com/fauna/query/builder/QueryVal.java index 2949cbdb..7a8d39e5 100644 --- a/src/main/java/com/fauna/query/builder/QueryVal.java +++ b/src/main/java/com/fauna/query/builder/QueryVal.java @@ -1,54 +1,76 @@ package com.fauna.query.builder; -import com.fauna.codec.Codec; -import com.fauna.codec.CodecProvider; -import com.fauna.codec.UTF8FaunaGenerator; - -import java.io.IOException; import java.util.Objects; /** * Represents a value fragment of a Fauna query. - * This class encapsulates a value that can be a variable in the query. + * This class encapsulates a value that can be a variable in the query, such as a literal value or a reference. + * The value can be any object type and will be substituted into the query at runtime. + * + * @param The type of the value in the fragment, which can be any object. */ -public class QueryVal extends QueryFragment { +public final class QueryVal extends QueryFragment { private final T value; /** - * Constructs a ValueFragment with the specified value. + * Constructs a QueryVal with the specified value. * * @param value the value to encapsulate, which can be any object. + * It can represent a literal value or a reference to be used in the query. */ - public QueryVal(T value) { + public QueryVal(final T value) { this.value = value; } /** * Retrieves the encapsulated value of this fragment. * - * @return the encapsulated object. + * @return the value contained within this fragment. */ @Override public T get() { return value; } + /** + * Compares this QueryVal to another object for equality. + * Two QueryVal objects are considered equal if their encapsulated values are equal. + * + * @param o the object to compare to. + * @return {@code true} if this QueryVal is equal to the other object, otherwise {@code false}. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + @SuppressWarnings("unchecked") QueryVal that = (QueryVal) o; return Objects.equals(value, that.value); } + /** + * Returns the hash code for this QueryVal. + * The hash code is computed based on the encapsulated value. + * + * @return the hash code of this QueryVal. + */ @Override public int hashCode() { return value != null ? value.hashCode() : 0; } + /** + * Retrieves the value wrapped inside this fragment. + * + * @return the value contained in the QueryVal fragment. + */ public Object getValue() { return this.value; } diff --git a/src/main/java/com/fauna/query/builder/package-info.java b/src/main/java/com/fauna/query/builder/package-info.java new file mode 100644 index 00000000..fae87667 --- /dev/null +++ b/src/main/java/com/fauna/query/builder/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for building queries with Fauna. + */ +package com.fauna.query.builder; diff --git a/src/main/java/com/fauna/query/package-info.java b/src/main/java/com/fauna/query/package-info.java new file mode 100644 index 00000000..6794b97e --- /dev/null +++ b/src/main/java/com/fauna/query/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for defining queries with Fauna. + */ +package com.fauna.query; diff --git a/src/main/java/com/fauna/query/template/FaunaTemplate.java b/src/main/java/com/fauna/query/template/FaunaTemplate.java index fd536ff2..3023e24a 100644 --- a/src/main/java/com/fauna/query/template/FaunaTemplate.java +++ b/src/main/java/com/fauna/query/template/FaunaTemplate.java @@ -1,6 +1,8 @@ package com.fauna.query.template; -import com.fauna.query.builder.*; +import com.fauna.query.builder.QueryFragment; +import com.fauna.query.builder.QueryLiteral; +import com.fauna.query.builder.QueryVal; import java.util.Iterator; import java.util.Map; @@ -10,9 +12,10 @@ /** * Represents a template for constructing Fauna queries with placeholders - * for variable interpolation. + * for variable interpolation. This template uses a dollar-sign ($) syntax for + * identifying variable placeholders. */ -public class FaunaTemplate implements Iterable { +public final class FaunaTemplate implements Iterable { private static final char DELIMITER = '$'; private static final String ID_PATTERN = "[\\p{L}_][\\p{L}\\p{N}_]*"; @@ -30,19 +33,22 @@ public class FaunaTemplate implements Iterable { private final String template; /** - * Constructs a new FaunaTemplate with the given string template. + * Constructs a new {@code FaunaTemplate} with the specified template + * string. The template may contain literals and variable placeholders + * identified by a dollar sign and an optional set of braces + * (e.g., ${variable}). * * @param template the string template containing literals and placeholders. */ - public FaunaTemplate(String template) { + public FaunaTemplate(final String template) { this.template = template; } /** * Creates an iterator over the parts of the template, distinguishing - * between literals and variable placeholders. + * between literal text and variable placeholders. * - * @return an Iterator that traverses the template parts. + * @return an Iterator that iterates over the template parts. */ @Override public Iterator iterator() { @@ -80,25 +86,35 @@ public TemplatePart next() { TemplatePart part; if (escapedPart != null) { - String literalPart = template.substring(curPos, spanStartPos) + DELIMITER; - part = new TemplatePart(literalPart, TemplatePartType.LITERAL); + String literalPart = + template.substring(curPos, spanStartPos) + + DELIMITER; + part = new TemplatePart(literalPart, + TemplatePartType.LITERAL); curPos = spanEndPos; } else if (variablePart != null) { if (curPos < spanStartPos) { - part = new TemplatePart(template.substring(curPos, spanStartPos), TemplatePartType.LITERAL); + part = new TemplatePart( + template.substring(curPos, spanStartPos), + TemplatePartType.LITERAL); curPos = spanStartPos; } else { - part = new TemplatePart(variablePart, TemplatePartType.VARIABLE); + part = new TemplatePart(variablePart, + TemplatePartType.VARIABLE); curPos = spanEndPos; } } else { - part = new TemplatePart(template.substring(curPos, spanStartPos), TemplatePartType.LITERAL); + part = new TemplatePart( + template.substring(curPos, spanStartPos), + TemplatePartType.LITERAL); curPos = spanEndPos; } foundMatch = false; // Reset after processing a match return part; } else { - TemplatePart part = new TemplatePart(template.substring(curPos), TemplatePartType.LITERAL); + TemplatePart part = + new TemplatePart(template.substring(curPos), + TemplatePartType.LITERAL); curPos = template.length(); return part; } @@ -112,37 +128,49 @@ public TemplatePart next() { * @param position the starting position of the invalid placeholder. * @throws IllegalArgumentException if the placeholder syntax is invalid. */ - private void handleInvalid(int position) { + private void handleInvalid(final int position) { String substringUpToPosition = template.substring(0, position); String[] lines = substringUpToPosition.split("\r?\n"); - int colno, lineno; + + int colno; + int lineno; + if (lines.length == 0) { colno = 1; lineno = 1; } else { String lastLine = lines[lines.length - 1]; // Adjust the column number for invalid placeholder - colno = position - (substringUpToPosition.length() - lastLine.length()) - 1; // -1 to exclude the dollar sign + colno = position + - (substringUpToPosition.length() + - lastLine.length()) + - 1; // -1 to exclude the dollar sign lineno = lines.length; } - throw new IllegalArgumentException(String.format("Invalid placeholder in template: line %d, col %d", lineno, colno)); + throw new IllegalArgumentException(String.format( + "Invalid placeholder in template: line %d, col %d", lineno, + colno)); } /** * Represents a part of the template, which can either be a literal string * or a variable placeholder. */ - public static class TemplatePart { + public static final class TemplatePart { private final String part; private final TemplatePartType type; /** - * Constructs a new TemplatePart with the given text and type. + * Constructs a new {@code TemplatePart} with the specified text and + * type. * - * @param part the text of this part of the template. - * @param type the type of this part of the template. + * @param part the text for this part of the template, either literal + * text or a variable. + * @param type the type of this part of the template, + * either {@link TemplatePartType#LITERAL} + * or {@link TemplatePartType#VARIABLE}. */ - public TemplatePart(String part, TemplatePartType type) { + public TemplatePart(final String part, final TemplatePartType type) { this.part = part; this.type = type; } @@ -150,7 +178,7 @@ public TemplatePart(String part, TemplatePartType type) { /** * Retrieves the text of this part of the template. * - * @return the text of this template part. + * @return the text for this template part. */ public String getPart() { return part; @@ -159,20 +187,33 @@ public String getPart() { /** * Retrieves the type of this part of the template. * - * @return the type of this template part. + * @return the type of this template part, either literal or variable. */ public TemplatePartType getType() { return type; } - public QueryFragment toFragment(Map args) { + /** + * Converts this template part to a {@code QueryFragment} using the + * given arguments. If this part is a variable, the argument map is + * checked for a corresponding key, returning an appropriate + * {@code QueryFragment}. If no matching argument is found, an exception + * is thrown. + * + * @param args the map of arguments for template substitution. + * @return a {@code QueryFragment} representing this template part. + * @throws IllegalArgumentException if required arguments are missing. + */ + @SuppressWarnings("rawtypes") + public QueryFragment toFragment(final Map args) { if (this.getType().equals(TemplatePartType.VARIABLE)) { if (Objects.isNull(args)) { throw new IllegalArgumentException( - String.format("No args provided for Template variable %s.", this.getPart())); + String.format( + "No args provided for Template variable %s.", + this.getPart())); } if (args.containsKey(this.getPart())) { - // null values are valid, so can't use computeIfPresent / computeIfAbsent here. var arg = args.get(this.getPart()); if (arg instanceof QueryFragment) { return (QueryFragment) arg; @@ -181,12 +222,13 @@ public QueryFragment toFragment(Map args) { } } else { throw new IllegalArgumentException( - String.format("Template variable %s not found in provided args.", this.getPart())); + String.format( + "Template variable %s not found in provided args.", + this.getPart())); } } else { return new QueryLiteral(this.getPart()); } } } - } diff --git a/src/main/java/com/fauna/query/template/package-info.java b/src/main/java/com/fauna/query/template/package-info.java new file mode 100644 index 00000000..d1b892fa --- /dev/null +++ b/src/main/java/com/fauna/query/template/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for parsing query templates. + */ +package com.fauna.query.template; diff --git a/src/main/java/com/fauna/response/ConstraintFailure.java b/src/main/java/com/fauna/response/ConstraintFailure.java index 54ba966f..11421c46 100644 --- a/src/main/java/com/fauna/response/ConstraintFailure.java +++ b/src/main/java/com/fauna/response/ConstraintFailure.java @@ -1,31 +1,327 @@ package com.fauna.response; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fauna.constants.ResponseFields; +import com.fauna.exception.ClientResponseException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; -public class ConstraintFailure { +public final class ConstraintFailure { private final String message; private final String name; - private final List> paths; + private final PathElement[][] paths; - public ConstraintFailure(String message, String name, List> paths) { + /** + * Initialize a new ConstraintFailure instance. Queries that fail a check + * or unique + * constraint return a constraint failure. + * + * @param message Human-readable description of the constraint failure. + * @param name Name of the failed constraint. + * @param paths A list of paths where the constraint failure occurred. + */ + public ConstraintFailure( + final String message, + final String name, + final PathElement[][] paths) { this.message = message; this.name = name; this.paths = paths; } + /** + * Constructs a PathElement[] from the provided objects. Supported types + * are String and Integer. + * + * @param elements The String objects or Integer objects to use. + * @return A array of PathElement instances. + */ + public static PathElement[] createPath(final Object... elements) { + List path = new ArrayList<>(); + for (Object element : elements) { + if (element instanceof String) { + path.add(new PathElement((String) element)); + } else if (element instanceof Integer) { + path.add(new PathElement((Integer) element)); + } else { + throw new IllegalArgumentException( + "Only strings and integers supported"); + } + } + return path.toArray(new PathElement[0]); + } + + /** + * Initializes a new empty Builder. + * + * @return A new Builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builds a ConstraintFailure instance from the provided JsonParser. + * + * @param parser The JsonParser to consume. + * @return A new ConstraintFailure instance. + * @throws IOException Thrown if an error is encountered while reading the + * parser. + */ + public static ConstraintFailure parse(final JsonParser parser) + throws IOException { + if (parser.currentToken() != JsonToken.START_OBJECT + && parser.nextToken() != JsonToken.START_OBJECT) { + throw new ClientResponseException( + "Constraint failure should be a JSON object."); + } + Builder builder = ConstraintFailure.builder(); + while (parser.nextToken() == JsonToken.FIELD_NAME) { + String fieldName = parser.getValueAsString(); + switch (fieldName) { + case ResponseFields.ERROR_MESSAGE_FIELD_NAME: + builder.message(parser.nextTextValue()); + break; + case ResponseFields.ERROR_NAME_FIELD_NAME: + builder.name(parser.nextTextValue()); + break; + case ResponseFields.ERROR_PATHS_FIELD_NAME: + List paths = new ArrayList<>(); + JsonToken firstPathToken = parser.nextToken(); + if (firstPathToken == JsonToken.START_ARRAY) { + while (parser.nextToken() == JsonToken.START_ARRAY) { + List path = new ArrayList<>(); + while (parser.nextToken() != JsonToken.END_ARRAY) { + path.add(PathElement.parse(parser)); + } + paths.add(path.toArray(new PathElement[0])); + } + } else if (firstPathToken != JsonToken.VALUE_NULL) { + throw new ClientResponseException( + "Constraint failure path should be array or null, got: " + + firstPathToken.toString()); + } + paths.forEach(builder::path); + break; + default: + } + } + return builder.build(); + + } + + /** + * Gets the constraint failure message. + * + * @return A string representation of the message. + */ public String getMessage() { return this.message; } + /** + * Gets the constraint failure name. + * + * @return A string representation of the name. + */ public Optional getName() { return Optional.ofNullable(this.name); } - public List> getPaths() { - return paths != null ? paths : List.of(); + /** + * Gets an optional path elements related to the constraint failure. + * + * @return An array of arrays of PathElements. + */ + public Optional getPaths() { + return Optional.ofNullable(paths); + } + + /** + * Gets a list of string representations of the constraint failure paths. + * + * @return A list of string representations of constraint failure paths. + */ + public Optional> getPathStrings() { + if (paths == null) { + return Optional.empty(); + } else { + return Optional.of(Arrays.stream(paths).map( + pathElements -> Arrays.stream(pathElements) + .map(PathElement::toString).collect( + Collectors.joining("."))) + .collect(Collectors.toList())); + } + } + + /** + * Tests path equality with another ConstraintFailure. + * + * @param otherFailure The other ConstraintFailure. + * @return True if the paths are equal. + */ + public boolean pathsAreEqual(final ConstraintFailure otherFailure) { + PathElement[][] thisArray = + this.getPaths().orElse(new PathElement[0][]); + PathElement[][] otherArray = + otherFailure.getPaths().orElse(new PathElement[0][]); + return Arrays.deepEquals(thisArray, otherArray); + } + + @Override + public boolean equals(final Object other) { + if (other instanceof ConstraintFailure) { + ConstraintFailure otherFailure = (ConstraintFailure) other; + return this.getMessage().equals(otherFailure.getMessage()) + && this.getName().equals(otherFailure.getName()) + && pathsAreEqual(otherFailure); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash( + this.name, + this.message, + Arrays.deepHashCode(this.paths)); + } + + public static final class PathElement { + private String sVal = null; + private Integer iVal = null; + + /** + * Initializes a PathElement with a string value. + * + * @param sVal The string value. + */ + public PathElement(final String sVal) { + this.sVal = sVal; + } + + /** + * Initializes a PathElement with an integer value. + * + * @param iVal The integer value. + */ + public PathElement(final Integer iVal) { + this.iVal = iVal; + } + + /** + * Note that this parse method does not advance the parser. + * + * @param parser A JsonParser instance. + * @return A new PathElement. + * @throws IOException Can be thrown if e.g. stream ends. + */ + public static PathElement parse(final JsonParser parser) + throws IOException { + if (parser.currentToken().isNumeric()) { + return new PathElement(parser.getValueAsInt()); + } else { + return new PathElement(parser.getText()); + } + } + + /** + * Tests whether the PathElement stores a string or an integer. + * + * @return If it's a string, true. Otherwise, false. + */ + public boolean isString() { + return sVal != null; + } + + @Override + public boolean equals(final Object o) { + if (o instanceof PathElement) { + PathElement other = (PathElement) o; + return other.isString() == this.isString() + && other.toString().equals(this.toString()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + /** + * Converts the PathElement to a string. + * + * @return A string representation of the PathElement. + */ + public String toString() { + return sVal == null ? String.valueOf(iVal) : sVal; + } + } + + public static class Builder { + private final List paths = new ArrayList<>(); + private String message = null; + private String name = null; + + /** + * Sets a message on the builder. + * + * @param message The message to set. + * @return this. + */ + public Builder message(final String message) { + this.message = message; + return this; + } + + /** + * Sets a name on the builder. + * + * @param name The name to set. + * @return this. + */ + public Builder name(final String name) { + this.name = name; + return this; + } + + /** + * Sets a path on the builder. + * + * @param path The path to set. + * @return this. + */ + public Builder path(final PathElement[] path) { + this.paths.add(path); + return this; + } + + /** + * Builds a ConstraintFailure instance from the current builder. + * + * @return A ConstraintFailure instance. + */ + public ConstraintFailure build() { + PathElement[][] paths = + this.paths.toArray(new PathElement[this.paths.size()][]); + return new ConstraintFailure(this.message, this.name, + this.paths.isEmpty() ? null : paths); + } + } } diff --git a/src/main/java/com/fauna/response/ErrorInfo.java b/src/main/java/com/fauna/response/ErrorInfo.java index 050f4e65..4a7fcbc1 100644 --- a/src/main/java/com/fauna/response/ErrorInfo.java +++ b/src/main/java/com/fauna/response/ErrorInfo.java @@ -1,5 +1,24 @@ package com.fauna.response; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fauna.codec.Codec; +import com.fauna.codec.DefaultCodecProvider; +import com.fauna.codec.UTF8FaunaParser; +import com.fauna.exception.ClientResponseException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.fauna.constants.ResponseFields.ERROR_ABORT_FIELD_NAME; +import static com.fauna.constants.ResponseFields.ERROR_CODE_FIELD_NAME; +import static com.fauna.constants.ResponseFields.ERROR_CONSTRAINT_FAILURES_FIELD_NAME; +import static com.fauna.constants.ResponseFields.ERROR_MESSAGE_FIELD_NAME; + /** * This class will encapsulate all the information Fauna returns about errors including constraint failures, and * abort data, for now it just has the code and message. @@ -7,17 +26,212 @@ public class ErrorInfo { private final String code; private final String message; + private final ConstraintFailure[] constraintFailures; + private final TreeNode abort; - public ErrorInfo(String code, String message) { + /** + * Initializes a new ErrorInfo. + * + * @param code The + * Fauna error code. + * @param message A short, human-readable description of the error. + * @param constraintFailures The constraint failures for the error, if any. + * Only present if the error code is + * `constraint_failure`. + * @param abort A user-defined error message passed using an + * FQL `abort()` method call. Only present if the error + * code is `abort`. + */ + public ErrorInfo( + final String code, + final String message, + final ConstraintFailure[] constraintFailures, + final TreeNode abort) { this.code = code; this.message = message; + this.constraintFailures = constraintFailures; + this.abort = abort; + } + + /** + * A utility method to instantiate an empty builder. + * + * @return A new builder + */ + public static Builder builder() { + return new Builder(); + } + + private static Builder handleField(final Builder builder, + final JsonParser parser) + throws IOException { + String fieldName = parser.getCurrentName(); + switch (fieldName) { + case ERROR_CODE_FIELD_NAME: + return builder.code(parser.nextTextValue()); + case ERROR_MESSAGE_FIELD_NAME: + return builder.message(parser.nextTextValue()); + case ERROR_ABORT_FIELD_NAME: + parser.nextToken(); + return builder.abort(new ObjectMapper().readTree(parser)); + case ERROR_CONSTRAINT_FAILURES_FIELD_NAME: + List failures = new ArrayList<>(); + JsonToken token = parser.nextToken(); + if (token == JsonToken.VALUE_NULL) { + return builder; + } else if (token == JsonToken.START_ARRAY) { + JsonToken nextToken = parser.nextToken(); + while (nextToken == JsonToken.START_OBJECT) { + failures.add(ConstraintFailure.parse(parser)); + nextToken = parser.nextToken(); + } + return builder.constraintFailures(failures); + } else { + throw new ClientResponseException( + "Unexpected token in constraint failures: " + + token); + } + default: + throw new ClientResponseException( + "Unexpected token in error info: " + + parser.currentToken()); + } + } + + /** + * Builds a new ErrorInfo from a JsonParser. + * + * @param parser The JsonParser to read. + * @return A new ErrorInfo instance. + * @throws IOException Thrown on errors reading from the parser. + */ + public static ErrorInfo parse(final JsonParser parser) throws IOException { + if (parser.nextToken() != JsonToken.START_OBJECT) { + throw new ClientResponseException( + "Error parsing error info, got token" + + parser.currentToken()); + } + Builder builder = ErrorInfo.builder(); + + while (parser.nextToken() == JsonToken.FIELD_NAME) { + builder = handleField(builder, parser); + } + return builder.build(); } + /** + * Gets the Fauna error code. + * + * @return A string representing the Fauna error code. + */ public String getCode() { return code; } + /** + * Gets the error message. + * + * @return A string representing the error message. + */ public String getMessage() { return message; } + + /** + * Gets the constraint failures. + * + * @return An optional containing the constraint failures. + */ + public Optional getConstraintFailures() { + return Optional.ofNullable(this.constraintFailures); + } + + /** + * Gets the user-defined abort error message as a JSON node. + * + * @return An optional TreeNode with the abort data. + */ + public Optional getAbortJson() { + return Optional.ofNullable(this.abort); + } + + /** + * Parses the abort data into the provided class. + * + * @param abortDataClass The class to decode into. + * @param The type to decode into. + * @return An instance of the provided type. + */ + public Optional getAbort(final Class abortDataClass) { + return this.getAbortJson().map(tree -> { + UTF8FaunaParser parser = new UTF8FaunaParser(tree.traverse()); + Codec codec = DefaultCodecProvider.SINGLETON.get(abortDataClass); + parser.read(); + return codec.decode(parser); + }); + } + + public static class Builder { + private String code = null; + private String message = null; + private ConstraintFailure[] constraintFailures = null; + private TreeNode abort = null; + + /** + * Sets the error code on the builder. + * + * @param code The error code. + * @return this + */ + public Builder code(final String code) { + this.code = code; + return this; + } + + /** + * Sets the message on the builder. + * + * @param message The message. + * @return this + */ + public Builder message(final String message) { + this.message = message; + return this; + } + + /** + * Sets the abort data on the builder. + * + * @param abort The abort JSON node. + * @return this + */ + public Builder abort(final TreeNode abort) { + this.abort = abort; + return this; + } + + /** + * Sets the constraint failures on the builder. + * + * @param constraintFailures The constraint failures. + * @return this + */ + public Builder constraintFailures( + final List constraintFailures) { + this.constraintFailures = + constraintFailures.toArray(new ConstraintFailure[0]); + return this; + } + + /** + * Returns a new ErrorInfo instance based on the current builder. + * + * @return An ErrorInfo instance + */ + public ErrorInfo build() { + return new ErrorInfo(this.code, this.message, + this.constraintFailures, this.abort); + } + } } diff --git a/src/main/java/com/fauna/response/wire/MultiByteBufferInputStream.java b/src/main/java/com/fauna/response/MultiByteBufferInputStream.java similarity index 60% rename from src/main/java/com/fauna/response/wire/MultiByteBufferInputStream.java rename to src/main/java/com/fauna/response/MultiByteBufferInputStream.java index 49f3dc19..4051e063 100644 --- a/src/main/java/com/fauna/response/wire/MultiByteBufferInputStream.java +++ b/src/main/java/com/fauna/response/MultiByteBufferInputStream.java @@ -1,4 +1,4 @@ -package com.fauna.response.wire; +package com.fauna.response; import java.io.EOFException; import java.io.IOException; @@ -8,31 +8,48 @@ /** * Joins a list of byte buffers to make them appear as a single input stream. - * + *

* The reset method is supported, and always resets to restart the stream at the beginning of the first buffer, * although markSupported() returns false for this class. */ public class MultiByteBufferInputStream extends InputStream { - + private final int ff = 0xFF; private final List buffers; private int index = 0; private ByteBuffer currentBuffer; - public MultiByteBufferInputStream(List initialBuffers) { + /** + * Initializes a MultiByteBufferInputStream using the provided byte buffers. + * + * @param initialBuffers A list of ByteBuffers to use. + */ + public MultiByteBufferInputStream(final List initialBuffers) { this.buffers = initialBuffers; this.currentBuffer = buffers.get(index); } - public synchronized void add(List additionalBuffers) { + /** + * Adds additional byte buffers to this instance in a thread-safe manner. + * + * @param additionalBuffers The additional ByteBuffers. + */ + public synchronized void add(final List additionalBuffers) { buffers.addAll(additionalBuffers); } + /** + * Reads the next byte from the buffer. + * + * @return The next byte. + * @throws IOException Thrown when the byte buffers are exhausted. + */ + @SuppressWarnings("checkstyle:MagicNumber") @Override public synchronized int read() throws IOException { if (currentBuffer.hasRemaining()) { return currentBuffer.get() & 0xFF; - } else if (buffers.size() > index+1) { + } else if (buffers.size() > index + 1) { index++; currentBuffer = buffers.get(index); return currentBuffer.get() & 0xFF; @@ -41,6 +58,9 @@ public synchronized int read() throws IOException { } } + /** + * Resets the byte buffer. + */ @Override public synchronized void reset() { for (ByteBuffer buffer : buffers.subList(0, index)) { @@ -51,5 +71,4 @@ public synchronized void reset() { } - } diff --git a/src/main/java/com/fauna/response/QueryFailure.java b/src/main/java/com/fauna/response/QueryFailure.java index c9209bfa..c1c164fa 100644 --- a/src/main/java/com/fauna/response/QueryFailure.java +++ b/src/main/java/com/fauna/response/QueryFailure.java @@ -1,74 +1,16 @@ package com.fauna.response; -import com.fauna.codec.DefaultCodecProvider; -import com.fauna.codec.UTF8FaunaParser; -import com.fauna.exception.ClientException; -import com.fauna.exception.ClientResponseException; -import com.fauna.exception.CodecException; -import com.fauna.response.wire.ConstraintFailureWire; -import com.fauna.response.wire.QueryResponseWire; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; public final class QueryFailure extends QueryResponse { private final int statusCode; - private String errorCode = ""; - private String message = ""; - - private List constraintFailures; - private String abortString; - - private final String fullMessage; - - /** - * Initializes a new instance of the {@link QueryFailure} class, parsing the provided raw - * response to extract error information. - * - * @param statusCode The HTTP status code. - * @param response The parsed response. - */ - public QueryFailure(int statusCode, QueryResponseWire response) { - super(response); - - this.statusCode = statusCode; - - var err = response.getError(); - if (err != null) { - errorCode = err.getCode(); - message = err.getMessage(); - - if (err.getConstraintFailures().isPresent()) { - var cf = new ArrayList(); - var codec = DefaultCodecProvider.SINGLETON.get(Object.class); - for (ConstraintFailureWire cfw : err.getConstraintFailures().get()) { - try { - var parser = UTF8FaunaParser.fromString(cfw.getPaths()); - var paths = codec.decode(parser); - cf.add(new ConstraintFailure(cfw.getMessage(), cfw.getName(), (List>) paths)); - } catch (CodecException exc) { - throw new ClientResponseException("Failed to parse constraint failure.", exc); - } - } - constraintFailures = cf; - } - - if (err.getAbort().isPresent()) { - abortString = err.getAbort().get(); - } - } - - var maybeSummary = this.getSummary() != null ? "\n---\n" + this.getSummary() : ""; - fullMessage = String.format( - "%d (%s): %s%s", - this.getStatusCode(), - this.getErrorCode(), - this.getMessage(), - maybeSummary); + private final ErrorInfo errorInfo; + public QueryFailure(int httpStatus, Builder builder) { + super(builder); + this.statusCode = httpStatus; + this.errorInfo = builder.getError(); } public int getStatusCode() { @@ -76,22 +18,27 @@ public int getStatusCode() { } public String getErrorCode() { - return errorCode; + return errorInfo.getCode(); } public String getMessage() { - return message; + return errorInfo.getMessage(); } - public String getFullMessage() { - return fullMessage; + public Optional getAbort(Class clazz) { + return errorInfo.getAbort(clazz); } - public Optional> getConstraintFailures() { - return Optional.ofNullable(constraintFailures); + public String getFullMessage() { + String summarySuffix = + this.getSummary() != null ? "\n---\n" + this.getSummary() : ""; + return String.format("%d (%s): %s%s", + this.getStatusCode(), this.getErrorCode(), this.getMessage(), + summarySuffix); + } - public Optional getAbortString() { - return Optional.ofNullable(this.abortString); + public Optional getConstraintFailures() { + return this.errorInfo.getConstraintFailures(); } } diff --git a/src/main/java/com/fauna/response/QueryResponse.java b/src/main/java/com/fauna/response/QueryResponse.java index 4aa52988..4b88dac7 100644 --- a/src/main/java/com/fauna/response/QueryResponse.java +++ b/src/main/java/com/fauna/response/QueryResponse.java @@ -1,21 +1,35 @@ package com.fauna.response; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fauna.client.StatsCollector; import com.fauna.codec.Codec; +import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.ClientResponseException; import com.fauna.exception.ErrorHandler; import com.fauna.exception.FaunaException; import com.fauna.exception.ProtocolException; -import com.fauna.response.wire.QueryResponseWire; +import com.fauna.query.QueryTags; import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; import java.net.http.HttpResponse; import java.util.Map; +import static com.fauna.constants.ResponseFields.DATA_FIELD_NAME; +import static com.fauna.constants.ResponseFields.ERROR_FIELD_NAME; +import static com.fauna.constants.ResponseFields.LAST_SEEN_TXN_FIELD_NAME; +import static com.fauna.constants.ResponseFields.QUERY_TAGS_FIELD_NAME; +import static com.fauna.constants.ResponseFields.SCHEMA_VERSION_FIELD_NAME; +import static com.fauna.constants.ResponseFields.STATIC_TYPE_FIELD_NAME; +import static com.fauna.constants.ResponseFields.STATS_FIELD_NAME; +import static com.fauna.constants.ResponseFields.SUMMARY_FIELD_NAME; + public abstract class QueryResponse { - private static final ObjectMapper mapper = new ObjectMapper(); + private static final JsonFactory JSON_FACTORY = new JsonFactory(); private final Long lastSeenTxn; private final Long schemaVersion; @@ -23,52 +37,291 @@ public abstract class QueryResponse { private final Map queryTags; private final QueryStats stats; - QueryResponse(QueryResponseWire response) { + @SuppressWarnings("rawtypes") + QueryResponse(final Builder builder) { + this.lastSeenTxn = builder.lastSeenTxn; + this.summary = builder.summary; + this.schemaVersion = builder.schemaVersion; + this.stats = builder.stats; + this.queryTags = builder.queryTags; + } - lastSeenTxn = response.getTxnTs(); - schemaVersion = response.getSchemaVersion(); - summary = response.getSummary(); - stats = response.getStats(); - queryTags = response.getQueryTags(); + /** + * A helper method to instantiate a new builder. + * + * @param codec The codec to use when parsing data. + * @param The return type of the data. + * @return A new Builder instance. + */ + public static Builder builder(final Codec codec) { + return new Builder<>(codec); + } + + private static Builder handleField(final Builder builder, + final JsonParser parser) + throws IOException { + String fieldName = parser.getCurrentName(); + switch (fieldName) { + case ERROR_FIELD_NAME: + return builder.error(ErrorInfo.parse(parser)); + case DATA_FIELD_NAME: + return builder.data(parser); + case STATS_FIELD_NAME: + return builder.stats(QueryStats.parseStats(parser)); + case QUERY_TAGS_FIELD_NAME: + return builder.queryTags(QueryTags.parse(parser)); + case LAST_SEEN_TXN_FIELD_NAME: + return builder.lastSeenTxn(parser.nextLongValue(0)); + case SCHEMA_VERSION_FIELD_NAME: + return builder.schemaVersion(parser.nextLongValue(0)); + case STATIC_TYPE_FIELD_NAME: + return builder.staticType(parser.nextTextValue()); + case SUMMARY_FIELD_NAME: + return builder.summary(parser.nextTextValue()); + default: + throw new ClientResponseException( + "Unexpected field '" + fieldName + "'."); + } } /** - * Handle a HTTPResponse and return a QuerySuccess, or throw a FaunaException. - * @param response The HTTPResponse object. - * @return A successful response from Fauna. - * @throws FaunaException + * A helper method to adapt an HTTP response into a QuerySuccess or throw + * the appropriate FaunaException. + * + * @param response The HTTP response to adapt. + * @param codec The codec to use when reading the HTTP response body. + * @param statsCollector The stats collector to accumulate stats against. + * @param The response type on success. + * @return A QuerySuccess instance. + * @throws FaunaException Thrown on non-200 responses. */ - public static QuerySuccess handleResponse(HttpResponse response, Codec codec) throws FaunaException { - String body = response.body(); + public static QuerySuccess parseResponse( + final HttpResponse response, final Codec codec, + final StatsCollector statsCollector) throws FaunaException { try { - var responseInternal = mapper.readValue(body, QueryResponseWire.class); - if (response.statusCode() >= 400) { - ErrorHandler.handleErrorResponse(response.statusCode(), responseInternal, body); + JsonParser parser = JSON_FACTORY.createParser(response.body()); + + JsonToken firstToken = parser.nextToken(); + Builder builder = QueryResponse.builder(codec); + if (firstToken != JsonToken.START_OBJECT) { + throw new ClientResponseException( + "Response must be JSON object."); + } + while (parser.nextToken() == JsonToken.FIELD_NAME) { + builder = handleField(builder, parser); + } + + if (builder.stats != null) { + statsCollector.add(builder.stats); + } + + int httpStatus = response.statusCode(); + if (httpStatus >= HttpURLConnection.HTTP_BAD_REQUEST) { + QueryFailure failure = new QueryFailure(httpStatus, builder); + ErrorHandler.handleQueryFailure(response.statusCode(), failure); + // Fall back on ProtocolException. + throw new ProtocolException(response.statusCode(), failure); } - return new QuerySuccess<>(codec, responseInternal); - } catch (JsonProcessingException exc) { // Jackson JsonProcessingException subclasses IOException - throw new ClientResponseException("Failed to handle error response.", exc, response.statusCode()); + return builder.buildSuccess(); + } catch (IOException exc) { + throw new ClientResponseException( + "Failed to handle error response.", exc, + response.statusCode()); } + } + /** + * Gets the last seen transaction timestamp. + * + * @return A long representing the last seen transaction timestamp. + */ public Long getLastSeenTxn() { return lastSeenTxn; } + /** + * Gets the schema version. + * + * @return A long representing the schema version. + */ public Long getSchemaVersion() { return schemaVersion; } + /** + * Gets the summary associated with the response. + * + * @return A string representing the summary. + */ public String getSummary() { return summary; } + /** + * Gets the query tags associated with the response. + * + * @return A Map containing the query tags. + */ public Map getQueryTags() { return queryTags; } + /** + * Gets the query stats associated with the response. + * + * @return A QueryStats instance. + */ public QueryStats getStats() { return stats; } + + public static final class Builder { + private final Codec codec; + private Long lastSeenTxn; + private String summary; + private Long schemaVersion; + private QueryStats stats; + private QueryTags queryTags; + private String staticType; + private ErrorInfo error; + private T data; + + /** + * Initializes a QueryResponse.Builder. + * + * @param codec The codec to use when building data. + */ + public Builder(final Codec codec) { + this.codec = codec; + } + + /** + * Set the last seen transaction timestamp on the builder. + * + * @param lastSeenTxn The last seen transaction timestamp. + * @return This + */ + public Builder lastSeenTxn(final Long lastSeenTxn) { + this.lastSeenTxn = lastSeenTxn; + return this; + } + + /** + * Set the schema version on the builder. + * + * @param schemaVersion The schema version. + * @return This + */ + public Builder schemaVersion(final Long schemaVersion) { + this.schemaVersion = schemaVersion; + return this; + } + + /** + * Set the data on the builder by consuming the provided JsonParser with + * the configured codec. + * + * @param parser The JsonParser to consume. + * @return This + */ + public Builder data(final JsonParser parser) { + UTF8FaunaParser faunaParser = new UTF8FaunaParser(parser); + faunaParser.read(); + this.data = this.codec.decode(faunaParser); + return this; + } + + /** + * Set the query tags on the builder. + * + * @param tags The query tags to set. + * @return This + */ + public Builder queryTags(final QueryTags tags) { + this.queryTags = tags; + return this; + } + + /** + * Sets the error info on the builder. + * + * @param info The error info to set. + * @return This + */ + public Builder error(final ErrorInfo info) { + this.error = info; + return this; + } + + /** + * Sets the static type on the builder. + * + * @param staticType The static type to set. + * @return This + */ + public Builder staticType(final String staticType) { + this.staticType = staticType; + return this; + } + + /** + * Sets the summary on the builder. + * + * @param summary The summary to set. + * @return This + */ + public Builder summary(final String summary) { + this.summary = summary; + return this; + } + + /** + * Sets the query stats on the builder. + * + * @param stats The query stats to set. + * @return This + */ + public Builder stats(final QueryStats stats) { + this.stats = stats; + return this; + } + + /** + * Builds a QuerySuccess. + * + * @return A QuerySuccess from the current builder. + */ + public QuerySuccess buildSuccess() { + return new QuerySuccess<>(this); + } + + /** + * Gets a string representing the static type. + * + * @return A string representing the static type. + */ + public String getStaticType() { + return staticType; + } + + /** + * Gets an ErrorInfo instance representing an error on the response. + * + * @return An ErrorInfo instance. + */ + public ErrorInfo getError() { + return error; + } + + /** + * Gets the parsed data from the response. + * + * @return The parsed data. + */ + public T getData() { + return data; + } + } } diff --git a/src/main/java/com/fauna/response/QueryStats.java b/src/main/java/com/fauna/response/QueryStats.java index d3e9acd6..67cdcea6 100644 --- a/src/main/java/com/fauna/response/QueryStats.java +++ b/src/main/java/com/fauna/response/QueryStats.java @@ -1,10 +1,8 @@ package com.fauna.response; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonParser; import com.fauna.constants.ResponseFields; -import com.fauna.exception.ClientException; +import com.fauna.exception.ClientResponseException; import java.io.IOException; import java.util.ArrayList; @@ -18,107 +16,280 @@ public final class QueryStats { - public final int computeOps; + private final int computeOps; + private final int readOps; + private final int writeOps; + private final int queryTimeMs; + private final int processingTimeMs; + private final int contentionRetries; + private final int storageBytesRead; + private final int storageBytesWrite; + private final List rateLimitsHit; - public final int readOps; + private String stringValue = null; - public final int writeOps; - - public final int queryTimeMs; - - public final int processingTimeMs; - - public final int contentionRetries; - - public final int storageBytesRead; - - public final int storageBytesWrite; - - public final List rateLimitsHit; - - @JsonCreator - public QueryStats( - @JsonProperty(ResponseFields.STATS_COMPUTE_OPS_FIELD_NAME) int computeOps, - @JsonProperty(ResponseFields.STATS_READ_OPS) int readOps, - @JsonProperty(ResponseFields.STATS_WRITE_OPS) int writeOps, - @JsonProperty(ResponseFields.STATS_QUERY_TIME_MS) int queryTimeMs, - @JsonProperty(ResponseFields.STATS_CONTENTION_RETRIES) int contentionRetries, - @JsonProperty(ResponseFields.STATS_STORAGE_BYTES_READ) int storageBytesRead, - @JsonProperty(ResponseFields.STATS_STORAGE_BYTES_WRITE) int storageBytesWrite, - @JsonProperty(ResponseFields.STATS_PROCESSING_TIME_MS) int processingTimeMs, - @JsonProperty(ResponseFields.STATS_RATE_LIMITS_HIT) List rateLimitsHit) { + /** + * @param computeOps Transactional + * Compute Operations (TCOs) consumed by the request. + * @param readOps Transactional + * Read Operations (TROs) consumed by the request. + * @param writeOps Transactional + * Write Operations (TROs) consumed by the request. + * @param queryTimeMs Query run time for the request in milliseconds. + * @param contentionRetries Number of retries + * for contended transactions + * @param storageBytesRead Amount of data read from storage, in bytes. + * @param storageBytesWrite Amount of data written to storage, in bytes. + * @param processingTimeMs Aggregate event processing time in milliseconds. + * Only applies to Event Feed and Event Stream + * requests. + * @param rateLimitsHit Operation types that exceeded + * plan + * throughput limits. + */ + public QueryStats(final int computeOps, final int readOps, + final int writeOps, + final int queryTimeMs, final int contentionRetries, + final int storageBytesRead, final int storageBytesWrite, + final int processingTimeMs, + final List rateLimitsHit) { this.computeOps = computeOps; this.readOps = readOps; this.writeOps = writeOps; this.queryTimeMs = queryTimeMs; - this.processingTimeMs = queryTimeMs; this.contentionRetries = contentionRetries; this.storageBytesRead = storageBytesRead; this.storageBytesWrite = storageBytesWrite; + this.processingTimeMs = processingTimeMs; this.rateLimitsHit = rateLimitsHit != null ? rateLimitsHit : List.of(); } + static Builder builder() { + return new Builder(); + } - public static QueryStats parseStats(JsonParser parser) throws IOException { + static Builder parseField(final Builder builder, final JsonParser parser) + throws IOException { + String fieldName = parser.getValueAsString(); + switch (fieldName) { + case ResponseFields.STATS_COMPUTE_OPS_FIELD_NAME: + return builder.computeOps(parser.nextIntValue(0)); + case ResponseFields.STATS_READ_OPS: + return builder.readOps(parser.nextIntValue(0)); + case ResponseFields.STATS_WRITE_OPS: + return builder.writeOps(parser.nextIntValue(0)); + case ResponseFields.STATS_QUERY_TIME_MS: + return builder.queryTimeMs(parser.nextIntValue(0)); + case ResponseFields.STATS_PROCESSING_TIME_MS: + return builder.processingTimeMs(parser.nextIntValue(0)); + case ResponseFields.STATS_CONTENTION_RETRIES: + return builder.contentionRetries(parser.nextIntValue(0)); + case ResponseFields.STATS_STORAGE_BYTES_READ: + return builder.storageBytesRead(parser.nextIntValue(0)); + case ResponseFields.STATS_STORAGE_BYTES_WRITE: + return builder.storageBytesWrite(parser.nextIntValue(0)); + case ResponseFields.STATS_RATE_LIMITS_HIT: + List limits = new ArrayList<>(); + if (parser.nextToken() == START_ARRAY) { + while (parser.nextToken() == VALUE_STRING) { + limits.add(parser.getValueAsString()); + } + } + return builder.rateLimitsHit(limits); + default: + throw new ClientResponseException("Unknown field " + fieldName); + } + } + + /** + * Parse QueryStats from a JsonParser. + * + * @param parser the JsonParser to consume + * @return a QueryStats object containing the parsed stats + * @throws IOException thrown from the JsonParser + */ + public static QueryStats parseStats(final JsonParser parser) + throws IOException { if (parser.nextToken() == START_OBJECT) { - int computeOps = 0; - int readOps = 0; - int writeOps = 0; - int queryTimeMs = 0; - int processingTimeMs = 0; - int contentionRetries = 0; - int storageBytesRead = 0; - int storageBytesWrite = 0; - List rateLimitsHit = null; + Builder builder = builder(); while (parser.nextToken() == FIELD_NAME) { - String fieldName = parser.getValueAsString(); - switch (fieldName) { - case ResponseFields.STATS_COMPUTE_OPS_FIELD_NAME: - parser.nextToken(); - parser.getValueAsInt(); - case ResponseFields.STATS_READ_OPS: - parser.nextToken(); - parser.getValueAsInt(); - case ResponseFields.STATS_WRITE_OPS: - parser.nextToken(); - parser.getValueAsInt(); - case ResponseFields.STATS_QUERY_TIME_MS: - parser.nextToken(); - parser.getValueAsInt(); - case ResponseFields.STATS_PROCESSING_TIME_MS: - parser.nextToken(); - parser.getValueAsInt(); - case ResponseFields.STATS_CONTENTION_RETRIES: - parser.nextToken(); - parser.getValueAsString(); - case ResponseFields.STATS_STORAGE_BYTES_READ: - parser.nextToken(); - parser.getValueAsInt(); - case ResponseFields.STATS_STORAGE_BYTES_WRITE: - parser.nextToken(); - parser.getValueAsInt(); - case ResponseFields.STATS_RATE_LIMITS_HIT: - List limits = new ArrayList<>(); - if (parser.nextToken() == START_ARRAY) { - while (parser.nextToken() == VALUE_STRING) { - limits.add(parser.getValueAsString()); - } - } - } + builder = parseField(builder, parser); } - return new QueryStats(computeOps, readOps, writeOps, queryTimeMs, processingTimeMs, contentionRetries, storageBytesRead, storageBytesWrite, rateLimitsHit); + return builder.build(); } else if (parser.nextToken() == VALUE_NULL) { return null; } else { - throw new ClientException("Query stats should be an object or null"); + throw new ClientResponseException( + "Query stats should be an object or null, not " + + parser.getCurrentToken()); } } + private static String statString(final String name, final Object value) { + return String.join(": ", name, String.valueOf(value)); + } + + /** + * Gets the Transactional Compute Operations (TCOs) recorded. + * + * @return An int representing the compute ops. + */ + public int getComputeOps() { + return computeOps; + } + + /** + * Gets the Transactional Read Operations (TROs) recorded. + * + * @return An int representing the read ops. + */ + public int getReadOps() { + return readOps; + } + + /** + * Gets the Transactional Write Operations (TWOs) recorded. + * + * @return An int representing the write ops. + */ + public int getWriteOps() { + return writeOps; + } + + /** + * Gets the query time in milliseconds. + * + * @return An int representing the query time in milliseconds. + */ + public int getQueryTimeMs() { + return queryTimeMs; + } + + /** + * Gets the event processing time in milliseconds. + * Applies to Event Feeds and Event Stream requests only. + * + * @return An int representing the processing time in milliseconds. + */ + public int getProcessingTimeMs() { + return processingTimeMs; + } + + /** + * Gets the number of retries + * for transaction contention. + * + * @return An int representing the number of transaction contention retries. + */ + public int getContentionRetries() { + return contentionRetries; + } + + /** + * Gets the amount of data read from storage in bytes. + * + * @return An int representing the number of bytes read. + */ + public int getStorageBytesRead() { + return storageBytesRead; + } + + /** + * Gets the amount of data written to storage in bytes. + * + * @return An int representing the number of bytes written. + */ + public int getStorageBytesWrite() { + return storageBytesWrite; + } + + /** + * Gets a list of operation types that exceeded their plan + * throughput limits. + * + * @return A list of operation types that exceeded their throughput limit. + */ + public List getRateLimitsHit() { + return rateLimitsHit; + } + @Override public String toString() { - return "compute: " + computeOps + ", read: " + readOps + ", write: " + writeOps + ", " + - "queryTime: " + queryTimeMs + ", retries: " + contentionRetries + ", " + - "storageRead: " + storageBytesRead + ", storageWrite: " + storageBytesWrite + ", " + - "limits: " + rateLimitsHit.toString(); + if (this.stringValue == null) { + this.stringValue = String.join(", ", + statString("compute", computeOps), + statString("read", readOps), + statString("write", writeOps), + statString("queryTime", queryTimeMs), + statString("retries", contentionRetries), + statString("storageRead", storageBytesRead), + statString("storageWrite", storageBytesWrite), + statString("limits", rateLimitsHit) + ); + } + return this.stringValue; + } + + static class Builder { + private int computeOps; + private int readOps; + private int writeOps; + private int queryTimeMs; + private int contentionRetries; + private int storageBytesRead; + private int storageBytesWrite; + private int processingTimeMs; + private List rateLimitsHit; + + Builder computeOps(final int value) { + this.computeOps = value; + return this; + } + + Builder readOps(final int value) { + this.readOps = value; + return this; + } + + Builder writeOps(final int value) { + this.writeOps = value; + return this; + } + + Builder queryTimeMs(final int value) { + this.queryTimeMs = value; + return this; + } + + Builder contentionRetries(final int value) { + this.contentionRetries = value; + return this; + } + + Builder storageBytesRead(final int value) { + this.storageBytesRead = value; + return this; + } + + Builder storageBytesWrite(final int value) { + this.storageBytesWrite = value; + return this; + } + + Builder processingTimeMs(final int value) { + this.processingTimeMs = value; + return this; + } + + Builder rateLimitsHit(final List value) { + this.rateLimitsHit = value; + return this; + } + + QueryStats build() { + return new QueryStats(computeOps, readOps, writeOps, queryTimeMs, + contentionRetries, storageBytesRead, storageBytesWrite, + processingTimeMs, rateLimitsHit); + } } } diff --git a/src/main/java/com/fauna/response/QuerySuccess.java b/src/main/java/com/fauna/response/QuerySuccess.java index 831c80c6..6f76449a 100644 --- a/src/main/java/com/fauna/response/QuerySuccess.java +++ b/src/main/java/com/fauna/response/QuerySuccess.java @@ -1,12 +1,5 @@ package com.fauna.response; -import com.fauna.codec.Codec; -import com.fauna.codec.UTF8FaunaParser; -import com.fauna.exception.ClientResponseException; -import com.fauna.exception.CodecException; -import com.fauna.response.wire.QueryResponseWire; - -import java.io.IOException; import java.util.Optional; public final class QuerySuccess extends QueryResponse { @@ -14,23 +7,10 @@ public final class QuerySuccess extends QueryResponse { private final T data; private final String staticType; - /** - * Initializes a new instance of the {@link QuerySuccess} class, decoding the query - * response into the specified type. - * - * @param codec A codec for the response data type. - * @param response The parsed response. - */ - public QuerySuccess(Codec codec, QueryResponseWire response) { - super(response); - - try { - UTF8FaunaParser reader = UTF8FaunaParser.fromString(response.getData()); - this.data = codec.decode(reader); - } catch (CodecException exc) { - throw new ClientResponseException("Failed to decode response.", exc); - } - this.staticType = response.getStaticType(); + public QuerySuccess(final Builder builder) { + super(builder); + this.data = builder.getData(); + this.staticType = builder.getStaticType(); } public T getData() { diff --git a/src/main/java/com/fauna/response/StreamEvent.java b/src/main/java/com/fauna/response/StreamEvent.java deleted file mode 100644 index 4e09fa92..00000000 --- a/src/main/java/com/fauna/response/StreamEvent.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.fauna.response; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fauna.codec.Codec; -import com.fauna.codec.DefaultCodecProvider; -import com.fauna.codec.FaunaTokenType; -import com.fauna.codec.UTF8FaunaParser; -import com.fauna.exception.ClientException; -import com.fauna.response.wire.ErrorInfoWire; - -import java.io.IOException; -import java.util.Optional; - -import static com.fasterxml.jackson.core.JsonToken.FIELD_NAME; -import static com.fasterxml.jackson.core.JsonToken.START_OBJECT; -import static com.fasterxml.jackson.core.JsonToken.VALUE_STRING; -import static com.fauna.constants.ResponseFields.DATA_FIELD_NAME; -import static com.fauna.constants.ResponseFields.ERROR_FIELD_NAME; -import static com.fauna.constants.ResponseFields.LAST_SEEN_TXN_FIELD_NAME; -import static com.fauna.constants.ResponseFields.STATS_FIELD_NAME; -import static com.fauna.constants.ResponseFields.STREAM_CURSOR_FIELD_NAME; -import static com.fauna.constants.ResponseFields.STREAM_TYPE_FIELD_NAME; - -public class StreamEvent { - private static final Codec statsCodec = DefaultCodecProvider.SINGLETON.get(QueryStats.class); - public enum EventType { - STATUS, ADD, UPDATE, REMOVE, ERROR - } - - private final EventType type; - private final String cursor; - private final Long txn_ts; - private final E data; - private final QueryStats stats; - private final ErrorInfoWire error; - - - public StreamEvent(EventType type, String cursor, Long txn_ts, E data, QueryStats stats, ErrorInfoWire error) { - this.type = type; - this.cursor = cursor; - this.txn_ts = txn_ts; - this.data = data; - this.stats = stats; - this.error = error; - } - - private static StreamEvent.EventType parseEventType(JsonParser parser) throws IOException { - if (parser.nextToken() == VALUE_STRING) { - String typeString = parser.getText().toUpperCase(); - try { - return StreamEvent.EventType.valueOf(typeString); - } catch (IllegalArgumentException e) { - throw new ClientException("Invalid event type: " + typeString, e); - } - } else { - throw new ClientException("Event type should be a string, but got a " + parser.currentToken().asString()); - } - } - - public static StreamEvent parse(JsonParser parser, Codec dataCodec) throws IOException { - if (parser.nextToken() == START_OBJECT) { - String cursor = null; - StreamEvent.EventType eventType = null; - QueryStats stats = null; - E data = null; - Long txn_ts = null; - ErrorInfoWire errorInfo = null; - while (parser.nextToken() == FIELD_NAME) { - String fieldName = parser.getValueAsString(); - switch (fieldName) { - case STREAM_CURSOR_FIELD_NAME: - parser.nextToken(); - cursor = parser.getText(); - break; - case DATA_FIELD_NAME: - UTF8FaunaParser faunaParser = new UTF8FaunaParser(parser); - if (faunaParser.getCurrentTokenType() == FaunaTokenType.NONE) { - faunaParser.read(); - } - data = dataCodec.decode(faunaParser); - break; - case STREAM_TYPE_FIELD_NAME: eventType = parseEventType(parser); - break; - case STATS_FIELD_NAME: - stats = QueryStats.parseStats(parser); - break; - case LAST_SEEN_TXN_FIELD_NAME: - parser.nextToken(); - txn_ts = parser.getValueAsLong(); - break; - case ERROR_FIELD_NAME: - ObjectMapper mapper = new ObjectMapper(); - errorInfo = mapper.readValue(parser, ErrorInfoWire.class); - break; - } - } - return new StreamEvent(eventType, cursor, txn_ts, data, stats, errorInfo); - } else { - throw new ClientException("Invalid event starting with: " + parser.currentToken()); - } - } - - public StreamEvent.EventType getType() { - return type; - } - - public Optional getData() { - return Optional.ofNullable(data); - } - - public Optional getTimestamp() { - return Optional.ofNullable(txn_ts); - } - - public String getCursor() { - return cursor; - } - - public QueryStats getStats() { - return stats; - } - - public ErrorInfo getError() { - return new ErrorInfo(this.error.getCode(), this.error.getMessage()); - } - - -} diff --git a/src/main/java/com/fauna/response/package-info.java b/src/main/java/com/fauna/response/package-info.java new file mode 100644 index 00000000..ef472810 --- /dev/null +++ b/src/main/java/com/fauna/response/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for modeling and handling query responses from Fauna. + */ +package com.fauna.response; diff --git a/src/main/java/com/fauna/response/wire/ConstraintFailureWire.java b/src/main/java/com/fauna/response/wire/ConstraintFailureWire.java deleted file mode 100644 index b16e22dd..00000000 --- a/src/main/java/com/fauna/response/wire/ConstraintFailureWire.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.fauna.response.wire; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fauna.codec.json.PassThroughDeserializer; - -import java.util.Optional; - -public class ConstraintFailureWire { - @JsonProperty("message") - private String message; - - @JsonProperty("name") - private String name; - - @JsonProperty("paths") - @JsonDeserialize(using = PassThroughDeserializer.class) - private String paths; - - - public String getMessage() { - return this.message; - } - - public String getName() { - return this.name; - } - - public String getPaths() { - return paths; - } -} diff --git a/src/main/java/com/fauna/response/wire/ErrorInfoWire.java b/src/main/java/com/fauna/response/wire/ErrorInfoWire.java deleted file mode 100644 index 8f419f03..00000000 --- a/src/main/java/com/fauna/response/wire/ErrorInfoWire.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.fauna.response.wire; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fauna.codec.json.PassThroughDeserializer; - -import java.util.Optional; - -/** - * This class will be removed and replaced by the ErrorInfo class. - */ -@Deprecated -public class ErrorInfoWire { - - @JsonProperty("code") - private String code; - - @JsonProperty("message") - private String message; - - @JsonProperty("constraint_failures") - private ConstraintFailureWire[] constraintFailures; - - @JsonProperty("abort") - @JsonDeserialize(using = PassThroughDeserializer.class) - private String abort; - - - public String getCode() { - return code; - } - - public String getMessage() { - return message; - } - - public Optional getConstraintFailures() { - return Optional.ofNullable(constraintFailures); - } - - public Optional getAbort() { - return Optional.ofNullable(this.abort); - } -} diff --git a/src/main/java/com/fauna/response/wire/QueryResponseWire.java b/src/main/java/com/fauna/response/wire/QueryResponseWire.java deleted file mode 100644 index a3ba3b8a..00000000 --- a/src/main/java/com/fauna/response/wire/QueryResponseWire.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.fauna.response.wire; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fauna.codec.json.PassThroughDeserializer; -import com.fauna.codec.json.QueryTagsDeserializer; -import com.fauna.response.QueryStats; -import com.fauna.constants.ResponseFields; - -import java.util.Map; - -public class QueryResponseWire { - - @JsonProperty(ResponseFields.DATA_FIELD_NAME) - @JsonDeserialize(using = PassThroughDeserializer.class) - private String data; - - @JsonProperty(ResponseFields.ERROR_FIELD_NAME) - private ErrorInfoWire error; - - @JsonProperty(ResponseFields.QUERY_TAGS_FIELD_NAME) - @JsonDeserialize(using = QueryTagsDeserializer.class) - private Map queryTags; - - @JsonProperty(ResponseFields.SCHEMA_VERSION_FIELD_NAME) - private Long schemaVersion; - - @JsonProperty(ResponseFields.STATS_FIELD_NAME) - private QueryStats stats; - - @JsonProperty(ResponseFields.STATIC_TYPE_FIELD_NAME) - private String staticType; - - @JsonProperty(ResponseFields.SUMMARY_FIELD_NAME) - private String summary; - - @JsonProperty(ResponseFields.LAST_SEEN_TXN_FIELD_NAME) - private Long txnTs; - - public String getData() { - return data != null ? data : "null"; - } - - public ErrorInfoWire getError() { - return error; - } - - public Map getQueryTags() { - return queryTags; - } - - public Long getSchemaVersion() { - return schemaVersion; - } - - public QueryStats getStats() { - return stats; - } - - public String getStaticType() { - return staticType; - } - - public String getSummary() { - return summary; - } - - public Long getTxnTs() { - return txnTs; - } -} - diff --git a/src/main/java/com/fauna/stream/StreamRequest.java b/src/main/java/com/fauna/stream/StreamRequest.java deleted file mode 100644 index 0890f67f..00000000 --- a/src/main/java/com/fauna/stream/StreamRequest.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.fauna.stream; - -import com.fauna.query.StreamTokenResponse; - -import java.util.Optional; - -/** - * This class defines the request body expected by the fauna /stream endpoint. - */ -public class StreamRequest { - private final String token; - private final String cursor; - private final Long start_ts; - - public StreamRequest(String token) { - this.token = token; - this.cursor = null; - this.start_ts = null; - if (token == null || token.isEmpty()) { - throw new IllegalArgumentException("token cannot be null or empty"); - } - } - - public StreamRequest(String token, String cursor) { - this.token = token; - this.cursor = cursor; - this.start_ts = null; - if (token == null || token.isEmpty()) { - throw new IllegalArgumentException("token cannot be null or empty"); - } - } - - public StreamRequest(String token, Long start_ts) { - this.token = token; - this.cursor = null; - this.start_ts = start_ts; - if (token == null || token.isEmpty()) { - throw new IllegalArgumentException("token cannot be null or empty"); - } - } - - public static StreamRequest fromTokenResponse(StreamTokenResponse tokenResponse) { - return new StreamRequest(tokenResponse.getToken()); - } - - public String getToken() { - return token; - } - - public Optional getCursor() { - return Optional.ofNullable(cursor); - } - - public Optional getStartTs() { - return Optional.ofNullable(start_ts); - } -} diff --git a/src/main/java/com/fauna/types/BaseDocument.java b/src/main/java/com/fauna/types/BaseDocument.java index e70addb7..1e3f71fa 100644 --- a/src/main/java/com/fauna/types/BaseDocument.java +++ b/src/main/java/com/fauna/types/BaseDocument.java @@ -7,11 +7,12 @@ import java.util.Map; /** - * Represents the base structure of a document. + * Represents the base structure of a document with key-value pairs, a timestamp, and an associated collection. + * This class provides functionality to store, retrieve, and iterate over document data. */ -public abstract class BaseDocument implements Iterable { +public abstract class BaseDocument implements Iterable { - protected final Hashtable data = new Hashtable<>(); + private final Hashtable data = new Hashtable<>(); private final Instant ts; private final Module collection; @@ -20,9 +21,9 @@ public abstract class BaseDocument implements Iterable { * and timestamp. * * @param coll The collection to which the document belongs. - * @param ts The timestamp of the document. + * @param ts The timestamp indicating when the document was created or last modified. */ - public BaseDocument(Module coll, Instant ts) { + public BaseDocument(final Module coll, final Instant ts) { this.collection = coll; this.ts = ts; } @@ -33,23 +34,25 @@ public BaseDocument(Module coll, Instant ts) { * * @param coll The collection to which the document belongs. * @param ts The timestamp of the document. - * @param data Initial data for the document in key-value pairs. + * @param data Initial data for the document represented as key-value pairs. */ - public BaseDocument(Module coll, Instant ts, Map data) { + public BaseDocument( + final Module coll, + final Instant ts, + final Map data) { this(coll, ts); - for (Map.Entry entry : data.entrySet()) { - this.data.put(entry.getKey(), entry.getValue()); - } + this.data.putAll(data); } /** - * Returns an iterator over the elements in this document. + * Returns an iterator over the entries in this document. + * Each entry represents a key-value pair in the document's data. * - * @return an iterator over the elements in this document. + * @return an {@code Iterator} over the elements in this document. */ @Override public Iterator iterator() { - return new Iterator() { + return new Iterator<>() { private final Enumeration keys = data.keys(); @Override @@ -73,51 +76,67 @@ public static class Entry { private final String key; private final Object value; - public Entry(String key, Object value) { + /** + * Initializes an entry with a specified key and value. + * + * @param key The key for the entry. + * @param value The value associated with the key. + */ + public Entry(final String key, final Object value) { this.key = key; this.value = value; } + /** + * Gets the key of this entry. + * + * @return The key as a {@code String}. + */ public String getKey() { return key; } + /** + * Gets the value associated with this entry's key. + * + * @return The value as an {@code Object}. + */ public Object getValue() { return value; } } /** - * Gets the timestamp of the document. + * Gets the timestamp of the document, indicating its creation or last modification time. * - * @return The Instant of the document. + * @return An {@code Instant} representing the document's timestamp. */ public Instant getTs() { return ts; } /** - * Gets the collection to which the document belongs. + * Gets the collection to which this document belongs. * - * @return The collection to which the document belongs. + * @return The {@code Module} representing the document's collection. */ public Module getCollection() { return collection; } /** - * Gets a copy of the underlying data as a Map. + * Retrieves a copy of the document's data as a {@code Map}. * - * @return The data. + * @return A {@code Map} containing the document's key-value pairs. */ - public Map getData() { - return Map.copyOf(data); + public Map getData() { + return data; } /** - * Gets the count of key-value pairs contained in the document. + * Returns the number of key-value pairs contained in the document. * - * @return The number of key-value pairs. + * @return The total number of key-value pairs in the document. */ public int size() { return data.size(); @@ -126,21 +145,21 @@ public int size() { /** * Determines whether the document contains the specified key. * - * @param key The key to locate in the document. - * @return {@code true} if the document contains an element with the specified key; otherwise, - * {@code false}. + * @param key The key to search for in the document. + * @return {@code true} if the document contains an element with the specified key; + * otherwise, {@code false}. */ - public boolean containsKey(String key) { + public boolean containsKey(final String key) { return data.containsKey(key); } /** - * Gets the value associated with the specified key. + * Retrieves the value associated with the specified key. * - * @param key The key of the value to get. - * @return The value associated with the specified key, or {@code null} if the key is not found. + * @param key The key of the value to retrieve. + * @return The value associated with the specified key, or {@code null} if the key is not present. */ - public Object get(String key) { + public Object get(final String key) { return data.get(key); } } diff --git a/src/main/java/com/fauna/types/BaseRef.java b/src/main/java/com/fauna/types/BaseRef.java index f1cf053b..b52ff735 100644 --- a/src/main/java/com/fauna/types/BaseRef.java +++ b/src/main/java/com/fauna/types/BaseRef.java @@ -1,46 +1,49 @@ package com.fauna.types; - -import java.util.Objects; - - +/** + * Represents a reference to a document within a collection. + * This abstract class serves as a base for specific types of document references, + * encapsulating the collection to which the reference belongs. + */ public abstract class BaseRef { private final Module collection; /** - * Constructs a new Ref object with the specified id and collection. + * Constructs a new {@code BaseRef} object with the specified collection. * - * @param coll The module to which the document ref belongs. + * @param coll The module to which the document reference belongs. */ - public BaseRef(Module coll) { + public BaseRef(final Module coll) { this.collection = coll; } /** - * Gets the collection to which the ref belongs. + * Gets the collection to which this reference belongs. * - * @return The collection to which the ref belongs. + * @return The {@code Module} representing the collection associated with this reference. */ public Module getCollection() { return collection; } + /** + * Indicates whether some other object is "equal to" this reference. + * This method should be overridden in subclasses to provide specific equality logic. + * + * @param o the reference object with which to compare. + * @return {@code true} if this reference is the same as the object argument; + * {@code false} otherwise. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; - - if (o == null) return false; - - if (getClass() != o.getClass()) return false; - - BaseRef c = (BaseRef) o; - - return getCollection().equals(c.getCollection()); - } + public abstract boolean equals(Object o); + /** + * Returns a hash code value for the object. + * This method should be overridden in subclasses to provide specific hash code logic. + * + * @return a hash code value for this reference. + */ @Override - public int hashCode() { - return Objects.hash(getCollection()); - } + public abstract int hashCode(); } diff --git a/src/main/java/com/fauna/types/Document.java b/src/main/java/com/fauna/types/Document.java index df5facdf..f75d39b5 100644 --- a/src/main/java/com/fauna/types/Document.java +++ b/src/main/java/com/fauna/types/Document.java @@ -5,65 +5,89 @@ import java.util.Objects; /** - * Represents a document. + * Represents an immutable document with an ID, associated collection, timestamp, and optional key-value data. + * This class extends {@link BaseDocument} to provide additional document-specific functionality, + * such as unique identification and data equality checks. */ public final class Document extends BaseDocument { private final String id; /** - * Initializes a new instance of the Document class with the specified id, coll, and ts. + * Initializes a new instance of the {@code Document} class with the specified ID, collection, and timestamp. * - * @param id The string value of the document id. - * @param coll The module to which the document belongs. - * @param ts The timestamp of the document. + * @param id The unique string identifier of the document. + * @param coll The module (collection) to which the document belongs. + * @param ts The timestamp indicating the document's creation or last modification. */ - public Document(String id, Module coll, Instant ts) { + public Document(final String id, final Module coll, final Instant ts) { super(coll, ts); this.id = id; } /** - * Initializes a new instance of the Document class with the specified id, coll, ts, and - * additional data stored as key/value pairs on the instance. + * Initializes a new instance of the {@code Document} class with the specified ID, collection, + * timestamp, and additional key-value data. * - * @param id The string value of the document id. - * @param coll The module to which the document belongs. - * @param ts The timestamp of the document. - * @param data Additional data on the document. + * @param id The unique string identifier of the document. + * @param coll The module (collection) to which the document belongs. + * @param ts The timestamp indicating the document's creation or last modification. + * @param data Additional data for the document, represented as a map of key-value pairs. */ - public Document(String id, Module coll, Instant ts, Map data) { + public Document( + final String id, + final Module coll, + final Instant ts, + final Map data) { super(coll, ts, data); this.id = id; } /** - * Gets the string value of the document id. + * Gets the unique identifier for this document. * - * @return The string value of the document id. + * @return A {@code String} representing the document's unique ID. */ public String getId() { return id; } + /** + * Checks if this document is equal to another object. Two documents are considered equal + * if they have the same ID, timestamp, collection, and data content. + * + * @param o The object to compare with this document for equality. + * @return {@code true} if the specified object is equal to this document; otherwise, {@code false}. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; + public boolean equals(final Object o) { + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } Document c = (Document) o; return id.equals(c.id) && getTs().equals(c.getTs()) && getCollection().equals(c.getCollection()) - && data.equals(c.data); + && getData().equals(c.getData()); } + /** + * Returns a hash code value for this document based on its ID, timestamp, collection, and data. + * + * @return An integer hash code for this document. + */ @Override public int hashCode() { - return Objects.hash(id, getTs(), getCollection(), data); + return Objects.hash(id, getTs(), getCollection(), getData()); } } diff --git a/src/main/java/com/fauna/types/DocumentRef.java b/src/main/java/com/fauna/types/DocumentRef.java index a77f5fd1..04d60027 100644 --- a/src/main/java/com/fauna/types/DocumentRef.java +++ b/src/main/java/com/fauna/types/DocumentRef.java @@ -1,42 +1,56 @@ package com.fauna.types; - import java.util.Objects; /** - * Represents a document ref. + * Represents a reference to a specific document within a collection. + * This class provides a unique identifier for the document and references + * the collection to which it belongs. */ -public class DocumentRef extends BaseRef { +public final class DocumentRef extends BaseRef { private final String id; /** - * Constructs a new Ref object with the specified id and collection. + * Constructs a new {@code DocumentRef} object with the specified ID and collection. * - * @param id The string value of the document ref id. - * @param coll The module to which the document ref belongs. + * @param id The unique string identifier of the document reference. + * @param coll The module (collection) to which the document reference belongs. */ - public DocumentRef(String id, Module coll) { + public DocumentRef(final String id, final Module coll) { super(coll); this.id = id; } /** - * Gets the string value of the ref id. + * Gets the unique identifier of the document reference. * - * @return The string value of the ref id. + * @return A {@code String} representing the document reference ID. */ public String getId() { return id; } + /** + * Checks if this document reference is equal to another object. Two document references + * are considered equal if they have the same ID and belong to the same collection. + * + * @param o The object to compare with this document reference for equality. + * @return {@code true} if the specified object is equal to this document reference; otherwise, {@code false}. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; + public boolean equals(final Object o) { + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } DocumentRef c = (DocumentRef) o; @@ -44,6 +58,11 @@ public boolean equals(Object o) { && getCollection().equals(c.getCollection()); } + /** + * Returns a hash code value for this document reference based on its ID and collection. + * + * @return An integer hash code for this document reference. + */ @Override public int hashCode() { return Objects.hash(id, getCollection()); diff --git a/src/main/java/com/fauna/types/Module.java b/src/main/java/com/fauna/types/Module.java index 3faa008e..4ee8d499 100644 --- a/src/main/java/com/fauna/types/Module.java +++ b/src/main/java/com/fauna/types/Module.java @@ -2,20 +2,53 @@ import java.util.Objects; +/** + * Represents a module in Fauna Query Language (FQL), which serves as a symbolic object + * with associated methods. Modules can represent built-in FQL objects, such as "Collection" or "Math", + * as well as custom entities like user-defined collections. + *

+ * For example, a specific collection named "MyCollection" can be represented as a {@code Module} in Java + * to enable document creation via the Fauna Java driver: + *

+ *     var q = fql(
+ *         "${coll}.create({foo:'bar'})",
+ *         Map.of("coll", new Module("MyCollection"))
+ *     );
+ *
+ *     client.query(q);
+ * 
+ */ public final class Module { private final String name; - public Module(String name) { + /** + * Constructs a new {@code Module} object with the specified name. + * + * @param name The name of the module, representing either a built-in FQL object or a user-defined collection. + */ + public Module(final String name) { this.name = name; } + /** + * Gets the name of this module as a string representation. + * + * @return A {@code String} representing the module's name. + */ public String getName() { return name; } + /** + * Determines if this module is equal to another object. Two modules are considered equal + * if they have the same name. + * + * @param obj The object to compare with this module for equality. + * @return {@code true} if the specified object is equal to this module; otherwise, {@code false}. + */ @Override - public boolean equals(Object obj) { + public boolean equals(final Object obj) { if (this == obj) { return true; } @@ -26,6 +59,11 @@ public boolean equals(Object obj) { return Objects.equals(name, module.name); } + /** + * Returns a hash code value for this module based on its name. + * + * @return An integer hash code for this module. + */ @Override public int hashCode() { return Objects.hash(name); diff --git a/src/main/java/com/fauna/types/NamedDocument.java b/src/main/java/com/fauna/types/NamedDocument.java index 8397cba8..f54cf5f1 100644 --- a/src/main/java/com/fauna/types/NamedDocument.java +++ b/src/main/java/com/fauna/types/NamedDocument.java @@ -5,69 +5,96 @@ import java.util.Objects; /** - * Represents a document that has a "name" instead of an "id". For example, a Role document is - * represented as a NamedDocument. + * Represents a document identified by a "name" rather than an "id". + * This class is commonly used for documents in system collections where a unique name + * (e.g., for a Role) is more relevant than a numeric or auto-generated ID. */ public final class NamedDocument extends BaseDocument { /** - * The string value of the document name. + * The unique name identifier for this document. */ private final String name; /** - * Initializes a new instance of the NamedDocument class with the specified name, coll, and ts. + * Initializes a new instance of the {@code NamedDocument} class with the specified + * name, collection, and timestamp. * - * @param name The string value of the document name. - * @param coll The module to which the document belongs. - * @param ts The timestamp of the document. + * @param name The unique string name of the document. + * @param coll The module (collection) to which the document belongs. + * @param ts The timestamp indicating the document's creation or last modification. */ - public NamedDocument(String name, Module coll, Instant ts) { + public NamedDocument( + final String name, + final Module coll, + final Instant ts) { super(coll, ts); this.name = name; } /** - * Initializes a new instance of the NamedDocument class with the specified name, coll, ts, and - * additional data stored as key/value pairs on the instance. + * Initializes a new instance of the {@code NamedDocument} class with the specified + * name, collection, timestamp, and additional data. * - * @param name The string value of the document name. - * @param coll The module to which the document belongs. - * @param ts The timestamp of the document. - * @param data Additional data on the document. + * @param name The unique string name of the document. + * @param coll The module (collection) to which the document belongs. + * @param ts The timestamp indicating the document's creation or last modification. + * @param data Additional key-value data to store in the document. */ - public NamedDocument(String name, Module coll, Instant ts, Map data) { + public NamedDocument( + final String name, + final Module coll, + final Instant ts, + final Map data) { super(coll, ts, data); this.name = name; } /** - * Gets the string value of the document name. + * Gets the unique name of the document. * - * @return The name of the document. + * @return A {@code String} representing the document's name. */ public String getName() { return name; } + /** + * Checks if this document is equal to another object. Two documents are considered equal + * if they have the same name, timestamp, collection, and data. + * + * @param o The object to compare with this document for equality. + * @return {@code true} if the specified object is equal to this document; otherwise, {@code false}. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; + public boolean equals(final Object o) { + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } NamedDocument c = (NamedDocument) o; return name.equals(c.name) && getTs().equals(c.getTs()) && getCollection().equals(c.getCollection()) - && data.equals(c.data); + && getData().equals(c.getData()); } + /** + * Returns a hash code value for this document based on its name, timestamp, collection, and data. + * + * @return An integer hash code for this document. + */ @Override public int hashCode() { - return Objects.hash(name, getTs(), getCollection(), data); + return Objects.hash(name, getTs(), getCollection(), getData()); } -} \ No newline at end of file +} diff --git a/src/main/java/com/fauna/types/NamedDocumentRef.java b/src/main/java/com/fauna/types/NamedDocumentRef.java index 29a8cbde..4aa94c7a 100644 --- a/src/main/java/com/fauna/types/NamedDocumentRef.java +++ b/src/main/java/com/fauna/types/NamedDocumentRef.java @@ -3,40 +3,56 @@ import java.util.Objects; /** - * Represents a document ref that has a "name" instead of an "id". For example, a Role document - * reference is represented as a NamedRef. + * Represents a reference to a document identified by a "name" instead of an "id". + * This class is used for references to system collection documents where a unique name + * (e.g., for a Role) is used instead of a numeric or auto-generated ID. */ -public class NamedDocumentRef extends BaseRef { +public final class NamedDocumentRef extends BaseRef { private final String name; /** - * Constructs a new NamedRef object with the specified name and collection. + * Constructs a new {@code NamedDocumentRef} object with the specified name and collection. * - * @param name The string value of the named document ref name. - * @param coll The module to which the named document ref belongs. + * @param name The unique string name identifying the document reference. + * @param coll The module (collection) to which the named document reference belongs. */ - public NamedDocumentRef(String name, Module coll) { + public NamedDocumentRef( + final String name, + final Module coll) { super(coll); this.name = name; } /** - * Gets the string value of the ref name. + * Gets the unique name of the document reference. * - * @return The string value of the ref name. + * @return A {@code String} representing the name of the document reference. */ public String getName() { return name; } + /** + * Checks if this document reference is equal to another object. Two document references + * are considered equal if they have the same name and belong to the same collection. + * + * @param o The object to compare with this document reference for equality. + * @return {@code true} if the specified object is equal to this document reference; otherwise, {@code false}. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; + public boolean equals(final Object o) { + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } NamedDocumentRef c = (NamedDocumentRef) o; @@ -44,6 +60,11 @@ public boolean equals(Object o) { && getCollection().equals(c.getCollection()); } + /** + * Returns a hash code value for this document reference based on its name and collection. + * + * @return An integer hash code for this document reference. + */ @Override public int hashCode() { return Objects.hash(name, getCollection()); diff --git a/src/main/java/com/fauna/types/NonNullDocument.java b/src/main/java/com/fauna/types/NonNullDocument.java index 9023f754..f888dee8 100644 --- a/src/main/java/com/fauna/types/NonNullDocument.java +++ b/src/main/java/com/fauna/types/NonNullDocument.java @@ -2,47 +2,80 @@ import java.util.Objects; +/** + * Represents a document that is guaranteed to have a non-null value. + * This class extends {@link NullableDocument} and enforces non-null data + * for the document by disallowing null values in its constructor. + * + * @param The type of the document content. + */ public final class NonNullDocument extends NullableDocument { - public NonNullDocument(T val) { + /** + * Constructs a {@code NonNullDocument} with the specified non-null value. + * + * @param val The document's content of type {@code T}. Must not be null. + * @throws NullPointerException if {@code val} is null. + */ + public NonNullDocument(final T val) { super(val); } /** - * Get the wrapped value. + * Retrieves the non-null wrapped value of the document. * - * @return The wrapped value + * @return The non-null wrapped value of type {@code T}. */ @Override public T get() { - return val; + return getUnderlyingValue(); } /** - * Get the wrapped value. + * Retrieves the non-null wrapped value of the document. + * This method provides compatibility for default serialization. * - * @return The wrapped value + * @return The non-null wrapped value of type {@code T}. */ public T getValue() { - // Allows for default serialization without attributes. return get(); } + /** + * Checks if this document is equal to another object. + * Two {@code NonNullDocument} objects are considered equal if they hold values of the same type and content. + * + * @param o The object to compare with this document for equality. + * @return {@code true} if the specified object is equal to this document; otherwise, {@code false}. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; + public boolean equals(final Object o) { + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } - if (val.getClass() != ((NonNullDocument) o).get().getClass()) return false; + if (get().getClass() != ((NonNullDocument) o).get().getClass()) { + return false; + } - return val.equals(((NonNullDocument) o).get()); + return get().equals(((NonNullDocument) o).get()); } + /** + * Returns a hash code value for this document based on its non-null value. + * + * @return An integer hash code for this document. + */ @Override public int hashCode() { - return Objects.hash(val); + return Objects.hash(get()); } } diff --git a/src/main/java/com/fauna/types/NullDocument.java b/src/main/java/com/fauna/types/NullDocument.java index 85547e0a..45cb1778 100644 --- a/src/main/java/com/fauna/types/NullDocument.java +++ b/src/main/java/com/fauna/types/NullDocument.java @@ -4,56 +4,90 @@ import java.util.Objects; +/** + * Represents a document that is explicitly null, providing information about the cause of its null state. + * This class extends {@link NullableDocument} and throws a {@link NullDocumentException} when accessed. + * + * @param The type of the document content, although it will not contain an actual value. + */ public final class NullDocument extends NullableDocument { private final String id; private final Module coll; private final String cause; - public NullDocument(String id, Module coll, String cause) { - super(null); + /** + * Constructs a {@code NullDocument} with the specified ID, collection, and cause of nullity. + * + * @param id The unique identifier of the document. + * @param coll The module (collection) to which this null document belongs. + * @param cause A description of the reason why the document is null. + */ + public NullDocument(final String id, final Module coll, final String cause) { + super(); this.id = id; this.coll = coll; this.cause = cause; } /** - * Get the cause of the null document. + * Retrieves the cause of the document's null state. * - * @return A string describing the cause of the null document. + * @return A {@code String} describing the cause of the null document. */ public String getCause() { return cause; } + /** + * Throws a {@link NullDocumentException} when called, as this document is explicitly null. + * + * @return Never returns a value, as it always throws an exception. + * @throws NullDocumentException Always thrown to indicate that this is a null document. + */ @Override public T get() { throw new NullDocumentException(id, coll, cause); } /** - * Get the ID of the null document. - * @return The document ID. + * Retrieves the ID of the null document. + * + * @return A {@code String} representing the document's ID. */ public String getId() { return id; } /** - * Get the Collection associated with the null document. - * @return A Module representing the collection. + * Retrieves the collection associated with the null document. + * + * @return A {@code Module} representing the collection to which this null document belongs. */ public Module getCollection() { return coll; } + /** + * Checks if this null document is equal to another object. + * Two {@code NullDocument} objects are considered equal if they have the same ID, collection, and cause. + * + * @param o The object to compare with this null document for equality. + * @return {@code true} if the specified object is equal to this null document; otherwise, {@code false}. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; + public boolean equals(final Object o) { + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } var c = (NullDocument) o; return id.equals(c.getId()) @@ -61,6 +95,11 @@ public boolean equals(Object o) { && cause.equals(c.getCause()); } + /** + * Returns a hash code value for this null document based on its ID, collection, and cause. + * + * @return An integer hash code for this null document. + */ @Override public int hashCode() { return Objects.hash(id, coll, cause); diff --git a/src/main/java/com/fauna/types/NullableDocument.java b/src/main/java/com/fauna/types/NullableDocument.java index 7e007b2a..7385568d 100644 --- a/src/main/java/com/fauna/types/NullableDocument.java +++ b/src/main/java/com/fauna/types/NullableDocument.java @@ -1,11 +1,45 @@ package com.fauna.types; +/** + * Represents a generic document wrapper that may hold a value representing a document. + * This abstract class provides a base for documents that can optionally hold a value. + * + * @param The type of the document's content. + */ public abstract class NullableDocument { - final T val; + private final T value; - public NullableDocument(T val) { - this.val = val; + /** + * Constructs a {@code NullableDocument} without a value, initializing it to {@code null}. + */ + public NullableDocument() { + this.value = null; } + /** + * Constructs a {@code NullableDocument} with the specified value. + * + * @param val The value to wrap, which may be null. + */ + public NullableDocument(final T val) { + this.value = val; + } + + /** + * Retrieves the document's value. This method must be implemented by subclasses + * to specify how the value should be accessed. + * + * @return The document's content of type {@code T}. + */ public abstract T get(); + + /** + * Provides protected access to the underlying value, allowing subclasses + * to directly access the stored value without additional logic. + * + * @return The underlying value of type {@code T}, which may be {@code null}. + */ + protected T getUnderlyingValue() { + return value; + } } diff --git a/src/main/java/com/fauna/types/Page.java b/src/main/java/com/fauna/types/Page.java index db0cfee5..981390a4 100644 --- a/src/main/java/com/fauna/types/Page.java +++ b/src/main/java/com/fauna/types/Page.java @@ -1,46 +1,87 @@ package com.fauna.types; +import com.fauna.query.AfterToken; + import java.util.List; import java.util.Objects; +import java.util.Optional; /** - * Represents a page in a dataset for pagination. + * Represents a page of data in a Fauna Set. Supports pagination with an optional `after` + * token for retrieving additional pages. * - * @param The type of data contained in the page. + * @param The type of data contained within the page. */ -public class Page{ +public final class Page { private final List data; private final String after; - public Page(List data, String after) { + /** + * Constructs a {@code Page} with the specified data and an optional after token. + * + * @param data The list of data items belonging to this page. + * @param after The after token for pagination, which may be null if there are no more pages. + */ + public Page(final List data, final String after) { this.data = data; this.after = after; } + /** + * Retrieves the data items contained in this page. + * + * @return A {@code List} of data items belonging to this page. + */ public List getData() { return data; } - public String getAfter() { - return after; + /** + * Retrieves the optional after token for pagination. If present, this token can be used to + * request the next page of results from Fauna. + * + * @return An {@code Optional} representing the after token, or an empty {@code Optional} if no token + * is present. + */ + public Optional getAfter() { + return AfterToken.fromString(after); } + /** + * Checks if this page is equal to another object. Two pages are considered equal + * if they have the same data and after token. + * + * @param o The object to compare with this page for equality. + * @return {@code true} if the specified object is equal to this page; otherwise, {@code false}. + */ @Override - public boolean equals(Object o) { - if (this == o) return true; - - if (o == null) return false; - - if (getClass() != o.getClass()) return false; + public boolean equals(final Object o) { + if (this == o) { + return true; + } - Page c = (Page) o; + if (o == null) { + return false; + } - return Objects.equals(after,c.after) - && data.equals(c.data); + if (o instanceof Page) { + @SuppressWarnings("rawtypes") + Page c = (Page) o; + return Objects.equals(after, c.after) + && data.equals(c.data); + } else { + return false; + } } + /** + * Returns a hash code value for this page based on its data and after token. + * + * @return An integer hash code for this page. + */ @Override public int hashCode() { return Objects.hash(after, data); } -} \ No newline at end of file +} diff --git a/src/main/java/com/fauna/types/package-info.java b/src/main/java/com/fauna/types/package-info.java new file mode 100644 index 00000000..56fff2be --- /dev/null +++ b/src/main/java/com/fauna/types/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes representing standard return types from Fauna. + */ +package com.fauna.types; diff --git a/src/test/java/com/fauna/beans/ClassWithAttributes.java b/src/test/java/com/fauna/beans/ClassWithAttributes.java index ff7d6231..8ff0b56b 100644 --- a/src/test/java/com/fauna/beans/ClassWithAttributes.java +++ b/src/test/java/com/fauna/beans/ClassWithAttributes.java @@ -39,11 +39,17 @@ public Integer getAge() { @Override public boolean equals(Object o) { - if (this == o) return true; + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } ClassWithAttributes c = (ClassWithAttributes) o; diff --git a/src/test/java/com/fauna/beans/ClassWithClientGeneratedIdCollTsAnnotations.java b/src/test/java/com/fauna/beans/ClassWithClientGeneratedIdCollTsAnnotations.java index cdac60e4..68c07e83 100644 --- a/src/test/java/com/fauna/beans/ClassWithClientGeneratedIdCollTsAnnotations.java +++ b/src/test/java/com/fauna/beans/ClassWithClientGeneratedIdCollTsAnnotations.java @@ -10,7 +10,7 @@ public class ClassWithClientGeneratedIdCollTsAnnotations { - @FaunaId( isClientGenerate = true ) + @FaunaId(isClientGenerate = true) private String id; @FaunaColl private Module coll; @@ -21,7 +21,10 @@ public class ClassWithClientGeneratedIdCollTsAnnotations { private String lastName; - public ClassWithClientGeneratedIdCollTsAnnotations(String id, Module coll, Instant ts, String firstName, String lastName) { + public ClassWithClientGeneratedIdCollTsAnnotations(String id, Module coll, + Instant ts, + String firstName, + String lastName) { this.id = id; this.coll = coll; this.ts = ts; @@ -44,13 +47,20 @@ public String getLastName() { @Override public boolean equals(Object o) { - if (this == o) return true; + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } - ClassWithClientGeneratedIdCollTsAnnotations c = (ClassWithClientGeneratedIdCollTsAnnotations) o; + ClassWithClientGeneratedIdCollTsAnnotations c = + (ClassWithClientGeneratedIdCollTsAnnotations) o; return Objects.equals(id, c.id) && Objects.equals(coll, c.coll) diff --git a/src/test/java/com/fauna/beans/ClassWithFaunaIgnore.java b/src/test/java/com/fauna/beans/ClassWithFaunaIgnore.java index c38f233c..deaeec2f 100644 --- a/src/test/java/com/fauna/beans/ClassWithFaunaIgnore.java +++ b/src/test/java/com/fauna/beans/ClassWithFaunaIgnore.java @@ -15,7 +15,8 @@ public class ClassWithFaunaIgnore { @FaunaIgnore private Integer age; - public ClassWithFaunaIgnore(String firstName, String lastName, Integer age) { + public ClassWithFaunaIgnore(String firstName, String lastName, + Integer age) { this.firstName = firstName; this.lastName = lastName; this.age = age; @@ -39,11 +40,17 @@ public Integer getAge() { @Override public boolean equals(Object o) { - if (this == o) return true; + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } ClassWithFaunaIgnore c = (ClassWithFaunaIgnore) o; diff --git a/src/test/java/com/fauna/beans/ClassWithIdCollTsAnnotations.java b/src/test/java/com/fauna/beans/ClassWithIdCollTsAnnotations.java index ec973385..2e2bb61a 100644 --- a/src/test/java/com/fauna/beans/ClassWithIdCollTsAnnotations.java +++ b/src/test/java/com/fauna/beans/ClassWithIdCollTsAnnotations.java @@ -21,7 +21,8 @@ public class ClassWithIdCollTsAnnotations { private String lastName; - public ClassWithIdCollTsAnnotations(String id, Module coll, Instant ts, String firstName, String lastName) { + public ClassWithIdCollTsAnnotations(String id, Module coll, Instant ts, + String firstName, String lastName) { this.id = id; this.coll = coll; this.ts = ts; @@ -44,11 +45,17 @@ public String getLastName() { @Override public boolean equals(Object o) { - if (this == o) return true; + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } ClassWithIdCollTsAnnotations c = (ClassWithIdCollTsAnnotations) o; diff --git a/src/test/java/com/fauna/beans/ClassWithInheritance.java b/src/test/java/com/fauna/beans/ClassWithInheritance.java index 19d197fd..c6b48bb1 100644 --- a/src/test/java/com/fauna/beans/ClassWithInheritance.java +++ b/src/test/java/com/fauna/beans/ClassWithInheritance.java @@ -14,11 +14,17 @@ public ClassWithInheritance() { @Override public boolean equals(Object o) { - if (this == o) return true; + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } ClassWithInheritance c = (ClassWithInheritance) o; diff --git a/src/test/java/com/fauna/beans/ClassWithInheritanceL2.java b/src/test/java/com/fauna/beans/ClassWithInheritanceL2.java index e92f40ba..3d0e819f 100644 --- a/src/test/java/com/fauna/beans/ClassWithInheritanceL2.java +++ b/src/test/java/com/fauna/beans/ClassWithInheritanceL2.java @@ -14,11 +14,17 @@ public ClassWithInheritanceL2() { @Override public boolean equals(Object o) { - if (this == o) return true; + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } ClassWithInheritanceL2 c = (ClassWithInheritanceL2) o; diff --git a/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java b/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java index 78687665..1e773080 100644 --- a/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java +++ b/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java @@ -12,7 +12,10 @@ public class ClassWithParameterizedFields { public ClassWithParameterizedFields() { } - public ClassWithParameterizedFields(String firstName, List list, Map map, Optional optional) { + public ClassWithParameterizedFields(String firstName, + List list, + Map map, + Optional optional) { this.firstName = firstName; this.list = list; this.map = map; @@ -26,18 +29,24 @@ public ClassWithParameterizedFields(String firstName, List public List list; @FaunaField(name = "a_map") - public Map map; + public Map map; @FaunaField(name = "an_optional") public Optional optional; @Override public boolean equals(Object o) { - if (this == o) return true; + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } ClassWithParameterizedFields c = (ClassWithParameterizedFields) o; diff --git a/src/test/java/com/fauna/beans/ClassWithRefTagCollision.java b/src/test/java/com/fauna/beans/ClassWithRefTagCollision.java index bca7402c..4d68181c 100644 --- a/src/test/java/com/fauna/beans/ClassWithRefTagCollision.java +++ b/src/test/java/com/fauna/beans/ClassWithRefTagCollision.java @@ -8,7 +8,9 @@ public class ClassWithRefTagCollision { - public ClassWithRefTagCollision(){} + public ClassWithRefTagCollision() { + } + public ClassWithRefTagCollision(String field) { this.field = field; } @@ -18,11 +20,17 @@ public ClassWithRefTagCollision(String field) { @Override public boolean equals(Object o) { - if (this == o) return true; + if (this == o) { + return true; + } - if (o == null) return false; + if (o == null) { + return false; + } - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) { + return false; + } ClassWithRefTagCollision c = (ClassWithRefTagCollision) o; diff --git a/src/test/java/com/fauna/beans/InventorySource.java b/src/test/java/com/fauna/beans/InventorySource.java new file mode 100644 index 00000000..00f17296 --- /dev/null +++ b/src/test/java/com/fauna/beans/InventorySource.java @@ -0,0 +1,10 @@ +package com.fauna.beans; + +import com.fauna.e2e.beans.Product; +import com.fauna.event.EventSource; +import com.fauna.types.Page; + +public class InventorySource { + public Page firstPage; + public EventSource eventSource; +} diff --git a/src/test/java/com/fauna/beans/Person.java b/src/test/java/com/fauna/beans/Person.java index 8d146d3a..61dd7ba8 100644 --- a/src/test/java/com/fauna/beans/Person.java +++ b/src/test/java/com/fauna/beans/Person.java @@ -11,7 +11,8 @@ public class Person { private int age; - public Person(String firstName, String lastName, char middleInitial, int age) { + public Person(String firstName, String lastName, char middleInitial, + int age) { this.firstName = firstName; this.lastName = lastName; this.middleInitial = middleInitial; diff --git a/src/test/java/com/fauna/client/DefaultsTest.java b/src/test/java/com/fauna/client/DefaultsTest.java new file mode 100644 index 00000000..f614acd1 --- /dev/null +++ b/src/test/java/com/fauna/client/DefaultsTest.java @@ -0,0 +1,129 @@ +package com.fauna.client; + +import com.fauna.codec.DefaultCodecProvider; +import com.fauna.event.EventSource; +import com.fauna.event.FeedOptions; +import com.fauna.event.StreamOptions; +import com.fauna.query.QueryOptions; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.time.Duration; +import java.util.logging.Level; + +import static com.fauna.query.builder.Query.fql; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DefaultsTest { + public static FaunaClient client = Fauna.client(); + public static FaunaClient local = Fauna.local(); + public static QueryOptions options = QueryOptions.builder().build(); + public static RequestBuilder queryRequestBuilder = + client.getRequestBuilder(); + public static RequestBuilder streamRequestBuilder = + client.getStreamRequestBuilder(); + public static RequestBuilder feedRequestBuilder = + client.getFeedRequestBuilder(); + private static final EventSource source = EventSource.fromToken("token"); + + @Test + public void testClientDefaults() { + assertTrue(client.getHttpClient().connectTimeout().isEmpty()); + assertTrue(client.getFaunaSecret().isEmpty()); + assertTrue(client.getLastTransactionTs().isEmpty()); + assertEquals(FaunaClient.DEFAULT_RETRY_STRATEGY, + client.getRetryStrategy()); + assertEquals(Level.WARNING, client.getLogger().getLevel()); + } + + @Test + public void testLocalClientDefaults() { + assertTrue(local.getHttpClient().connectTimeout().isEmpty()); + assertEquals("secret", local.getFaunaSecret()); + assertTrue(local.getLastTransactionTs().isEmpty()); + assertEquals(FaunaClient.DEFAULT_RETRY_STRATEGY, + local.getRetryStrategy()); + assertEquals(Level.WARNING, local.getLogger().getLevel()); + } + + @Test + public void testTimeoutDefaults() { + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(URI.create("https://hello.world.com")).build(); + // Java does not set the HTTP request timeout by default. + assertTrue(httpRequest.timeout().isEmpty()); + + // Default Query timeout is 5 seconds. + assertEquals(5000, options.getTimeoutMillis().orElseThrow()); + + HttpRequest queryRequest = feedRequestBuilder.buildRequest(fql(""), + QueryOptions.builder().build(), DefaultCodecProvider.SINGLETON, + 0L); + // Default HTTP timeout (for queries) is 5+5=10s. + assertEquals(Duration.ofSeconds(10), + queryRequest.timeout().orElseThrow()); + assertEquals("5000", queryRequest.headers() + .firstValue(RequestBuilder.Headers.QUERY_TIMEOUT_MS) + .orElseThrow()); + + // Feeds follow the same defaults as queries. + HttpRequest feedRequest = feedRequestBuilder.buildFeedRequest(source, + FeedOptions.DEFAULT); + assertEquals("5000", feedRequest.headers() + .firstValue(RequestBuilder.Headers.QUERY_TIMEOUT_MS) + .orElseThrow()); + assertEquals(Duration.ofSeconds(10), + feedRequest.timeout().orElseThrow()); + + // We do not want a timeout set for stream requests, because the client may hold the stream open indefinitely. + HttpRequest streamRequest = + streamRequestBuilder.buildStreamRequest(source, + StreamOptions.builder().build()); + assertTrue(streamRequest.headers() + .firstValue(RequestBuilder.Headers.QUERY_TIMEOUT_MS).isEmpty()); + assertTrue(streamRequest.timeout().isEmpty()); + + } + + @Test + public void testNullQueryTimeouts() { + // Show that it's possible to prevent the HTTP client timeout from being set + QueryOptions override = QueryOptions.builder().timeout(null).build(); + HttpRequest queryRequest = + queryRequestBuilder.buildRequest(fql(""), override, + DefaultCodecProvider.SINGLETON, 0L); + assertTrue(queryRequest.timeout().isEmpty()); + assertTrue(queryRequest.headers() + .firstValue(RequestBuilder.Headers.QUERY_TIMEOUT_MS).isEmpty()); + + HttpRequest feedRequest = feedRequestBuilder.buildFeedRequest(source, + FeedOptions.builder().timeout(null).build()); + assertTrue(feedRequest.timeout().isEmpty()); + assertTrue(feedRequest.headers() + .firstValue(RequestBuilder.Headers.QUERY_TIMEOUT_MS).isEmpty()); + } + + @Test + public void testOverridingTimeouts() { + HttpRequest streamRequest = + streamRequestBuilder.buildStreamRequest(source, + StreamOptions.builder().timeout(Duration.ofMinutes(10)) + .build()); + assertEquals(Duration.ofMinutes(10), + streamRequest.timeout().orElseThrow()); + } + + @Test + public void testFeedDefaults() { + RequestBuilder builder = Fauna.client().getFeedRequestBuilder(); + HttpRequest req = builder.buildFeedRequest(source, FeedOptions.DEFAULT); + assertEquals(Duration.ofSeconds(10), req.timeout() + .orElseThrow()); // Unlike query, the default timeout for feeds is not set. + assertEquals("POST", req.method()); + assertEquals("https://db.fauna.com/feed/1", req.uri().toString()); + } + + +} diff --git a/src/test/java/com/fauna/client/FaunaClientTest.java b/src/test/java/com/fauna/client/FaunaClientTest.java index 8ef62af6..8fc220b1 100644 --- a/src/test/java/com/fauna/client/FaunaClientTest.java +++ b/src/test/java/com/fauna/client/FaunaClientTest.java @@ -1,6 +1,7 @@ package com.fauna.client; import com.fauna.beans.Person; +import com.fauna.codec.DefaultCodecProvider; import com.fauna.e2e.beans.Product; import com.fauna.exception.QueryCheckException; import com.fauna.exception.ThrottlingException; @@ -17,11 +18,16 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Map; import java.util.Optional; @@ -29,24 +35,23 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; -import org.mockito.junit.jupiter.MockitoExtension; - import static com.fauna.query.builder.Query.fql; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class FaunaClientTest { @@ -55,17 +60,40 @@ class FaunaClientTest { @Mock public HttpClient mockHttpClient; + String productBase = + "{\"@doc\":{\"id\":\"406412545672348160\",\"coll\":{\"@mod\":\"Product\"}," + + "\"ts\":{\"@time\":\"2024-08-16T21:34:16.700Z\"},\"name\":\"%s\",\"quantity\":{\"@int\":\"0\"}}}"; + String bodyBase = + "{\"data\":{\"@set\":{\"data\":[%s],\"after\":%s}},\"summary\":\"\"," + + "\"txn_ts\":1723844145837000,\"stats\":{},\"schema_version\":1723844028490000}"; + + String feedResponse = "{\"events\": " + + "[{\"type\": \"update\",\"data\": %s,\"txn_ts\": 424242424242," + + "\"cursor\": \"eventCursor\",\"stats\": {}}]," + + "\"cursor\": \"pageCursor\",\"has_next\": false,\"stats\": {}}"; @BeforeEach void setUp() { - client = Fauna.client(FaunaConfig.LOCAL, mockHttpClient, FaunaClient.DEFAULT_RETRY_STRATEGY); + client = Fauna.client(FaunaConfig.LOCAL, mockHttpClient, + FaunaClient.DEFAULT_RETRY_STRATEGY); + } + + static HttpResponse mockResponse(String body) { + HttpResponse resp = mock(HttpResponse.class); + doAnswer(invocationOnMock -> new ByteArrayInputStream( + body.getBytes(StandardCharsets.UTF_8))).when(resp).body(); + return resp; } @Test - void defaultConfigBuilder() { - FaunaConfig config = FaunaConfig.builder().build(); - assertEquals("https://db.fauna.com", config.getEndpoint()); - assertEquals("", config.getSecret()); + void defaultClient() { + FaunaClient client = Fauna.client(); + assertTrue(client.getHttpClient().connectTimeout().isEmpty()); + assertNotNull(client.getStatsCollector()); + assertEquals(URI.create("https://db.fauna.com/query/1"), + client.getRequestBuilder().buildRequest( + fql("hello"), QueryOptions.builder().build(), + DefaultCodecProvider.SINGLETON, 1L).uri()); } @Test @@ -83,26 +111,38 @@ void customConfigBuilder() { @Test void customConfigConstructor() { - FaunaClient client = Fauna.client(FaunaConfig.builder().secret("foo").build()); - assertTrue(client.toString().startsWith("com.fauna.client.BaseFaunaClient")); + FaunaConfig cfg = FaunaConfig.builder() + .secret("foo") + .build(); + FaunaClient client = Fauna.client(cfg); + assertTrue(client.toString() + .startsWith("com.fauna.client.BaseFaunaClient")); + assertNotNull(client.getStatsCollector()); } @Test void customConfigAndClientConstructor() { FaunaConfig config = FaunaConfig.builder().build(); - HttpClient multiThreadedClient = HttpClient.newBuilder().executor(Executors.newFixedThreadPool(20)) + HttpClient multiThreadedClient = HttpClient.newBuilder() + .executor(Executors.newFixedThreadPool(20)) .connectTimeout(Duration.ofSeconds(15)).build(); - FaunaClient client = Fauna.client(config, multiThreadedClient, FaunaClient.DEFAULT_RETRY_STRATEGY); - assertTrue(client.toString().startsWith("com.fauna.client.BaseFaunaClient")); + FaunaClient client = Fauna.client(config, multiThreadedClient, + FaunaClient.DEFAULT_RETRY_STRATEGY); + assertTrue(client.toString() + .startsWith("com.fauna.client.BaseFaunaClient")); } @Test() void environmentVarConfigConstructor() { // Note that the secret passed in through the builder is overridden by the FAUNA_* environment variables. - try (MockedStatic env = Mockito.mockStatic(FaunaConfig.FaunaEnvironment.class)) { - env.when(FaunaConfig.FaunaEnvironment::faunaSecret).thenReturn(Optional.of("secret")); - env.when(FaunaConfig.FaunaEnvironment::faunaEndpoint).thenReturn(Optional.of("endpoint")); - FaunaConfig faunaConfig = FaunaConfig.builder().secret("overridden").endpoint("overridden").build(); + try (MockedStatic env = Mockito.mockStatic( + FaunaConfig.FaunaEnvironment.class)) { + env.when(FaunaConfig.FaunaEnvironment::faunaSecret) + .thenReturn(Optional.of("secret")); + env.when(FaunaConfig.FaunaEnvironment::faunaEndpoint) + .thenReturn(Optional.of("endpoint")); + FaunaConfig faunaConfig = FaunaConfig.builder().secret("overridden") + .endpoint("overridden").build(); assertEquals("overridden", faunaConfig.getSecret()); assertEquals("overridden", faunaConfig.getEndpoint()); } @@ -110,10 +150,14 @@ void environmentVarConfigConstructor() { @Test void emptyEnvironmentVarConfigConstructor() { - try (MockedStatic env = Mockito.mockStatic(FaunaConfig.FaunaEnvironment.class)) { - env.when(FaunaConfig.FaunaEnvironment::faunaSecret).thenReturn(Optional.empty()); - env.when(FaunaConfig.FaunaEnvironment::faunaEndpoint).thenReturn(Optional.empty()); - FaunaConfig.Builder builder = FaunaConfig.builder().secret("secret").endpoint("endpoint"); + try (MockedStatic env = Mockito.mockStatic( + FaunaConfig.FaunaEnvironment.class)) { + env.when(FaunaConfig.FaunaEnvironment::faunaSecret) + .thenReturn(Optional.empty()); + env.when(FaunaConfig.FaunaEnvironment::faunaEndpoint) + .thenReturn(Optional.empty()); + FaunaConfig.Builder builder = + FaunaConfig.builder().secret("secret").endpoint("endpoint"); FaunaConfig faunaConf = builder.build(); assertEquals("secret", faunaConf.getSecret()); assertEquals("endpoint", faunaConf.getEndpoint()); @@ -127,7 +171,7 @@ void nullConfigClientConstructor() { () -> Fauna.client(null), "null FaunaConfig should throw" ); - assertEquals("FaunaConfig cannot be null.", thrown.getMessage() ); + assertEquals("FaunaConfig cannot be null.", thrown.getMessage()); } @@ -138,16 +182,22 @@ void query_WhenFqlIsNull_ShouldThrowIllegalArgumentException() { () -> client.query(null, Document.class), "Expected query() to throw, but it didn't" ); - assertTrue(thrown.getMessage().contains("The provided FQL query is null.")); + assertTrue(thrown.getMessage() + .contains("The provided FQL query is null.")); } @Test - void query_WithValidFQL_ShouldCall() throws IOException, InterruptedException { - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn("{\"summary\":\"success\",\"stats\":{}}"); - ArgumentMatcher matcher = new HttpRequestMatcher(Map.of("Authorization", "Bearer secret")); - when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); - QuerySuccess response = client.query(fql("Collection.create({ name: 'Dogs' })"), Document.class); + void query_WithValidFQL_ShouldCall() + throws IOException, InterruptedException { + HttpResponse resp = + mockResponse("{\"summary\":\"success\",\"stats\":{}}"); + ArgumentMatcher matcher = new HttpRequestMatcher( + Map.of("Authorization", "Bearer secret")); + when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); + QuerySuccess response = + client.query(fql("Collection.create({ name: 'Dogs' })"), + Document.class); assertEquals("success", response.getSummary()); assertNull(response.getLastSeenTxn()); verify(resp, atLeastOnce()).statusCode(); @@ -155,17 +205,17 @@ void query_WithValidFQL_ShouldCall() throws IOException, InterruptedException { @Test void query_WithTypedResponse() throws IOException, InterruptedException { - HttpResponse resp = mock(HttpResponse.class); String baz = "{" + "\"firstName\": \"Baz2\"," + "\"lastName\": \"Luhrmann2\"," + "\"middleInitial\": {\"@int\":\"65\"}," + "\"age\": { \"@int\": \"612\" }" + "}"; - ; - String body = "{\"summary\":\"success\",\"stats\":{},\"data\":" + baz + "}"; - when(resp.body()).thenReturn(body); - when(mockHttpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); + String body = + "{\"summary\":\"success\",\"stats\":{},\"data\":" + baz + "}"; + HttpResponse resp = mockResponse(body); + when(mockHttpClient.sendAsync(any(), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); Query fql = fql("Collection.create({ name: 'Dogs' })"); QuerySuccess response = client.query(fql, Person.class); assertEquals("success", response.getSummary()); @@ -174,22 +224,24 @@ void query_WithTypedResponse() throws IOException, InterruptedException { } @Test - void asyncQuery_WithTypedResponse() throws IOException, InterruptedException, ExecutionException { + void asyncQuery_WithTypedResponse() + throws IOException, InterruptedException, ExecutionException { // Given - HttpResponse resp = mock(HttpResponse.class); String baz = "{" + "\"firstName\": \"Baz\"," + "\"lastName\": \"Luhrmann2\"," + "\"middleInitial\": {\"@int\":\"65\"}," + "\"age\": { \"@int\": \"612\" }" + "}"; - ; - String body = "{\"summary\":\"success\",\"stats\":{},\"data\":" + baz + "}"; - when(resp.body()).thenReturn(body); - when(mockHttpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); + String body = + "{\"summary\":\"success\",\"stats\":{},\"data\":" + baz + "}"; + HttpResponse resp = mockResponse(body); + when(mockHttpClient.sendAsync(any(), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); Query fql = fql("Collection.create({ name: 'Dogs' })"); // When - CompletableFuture> future = client.asyncQuery(fql, Person.class); + CompletableFuture> future = + client.asyncQuery(fql, Person.class); QuerySuccess response = future.get(); Person data = response.getData(); // Then @@ -200,11 +252,15 @@ void asyncQuery_WithTypedResponse() throws IOException, InterruptedException, Ex } @Test - void asyncQuery_WithValidFQL_ShouldCall() throws ExecutionException, InterruptedException { - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn("{\"summary\":\"success\",\"stats\":{}}"); - when(mockHttpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); - CompletableFuture> future = client.asyncQuery(fql("Collection.create({ name: 'Dogs' })"), Document.class); + void asyncQuery_WithValidFQL_ShouldCall() + throws ExecutionException, InterruptedException { + HttpResponse resp = + mockResponse("{\"summary\":\"success\",\"stats\":{}}"); + when(mockHttpClient.sendAsync(any(), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); + CompletableFuture> future = + client.asyncQuery(fql("Collection.create({ name: 'Dogs' })"), + Document.class); QueryResponse response = future.get(); assertEquals("success", response.getSummary()); assertNull(response.getLastSeenTxn()); @@ -213,12 +269,14 @@ void asyncQuery_WithValidFQL_ShouldCall() throws ExecutionException, Interrupted @Test void query_withFailure_ShouldThrow() { - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn("{\"stats\":{},\"error\":{\"code\":\"invalid_query\"}}"); + HttpResponse resp = mockResponse( + "{\"stats\":{},\"error\":{\"code\":\"invalid_query\"}}"); when(resp.statusCode()).thenReturn(400); - when(mockHttpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); + when(mockHttpClient.sendAsync(any(), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); QueryCheckException exc = assertThrows(QueryCheckException.class, - () -> client.query(fql("Collection.create({ name: 'Dogs' })"), Document.class)); + () -> client.query(fql("Collection.create({ name: 'Dogs' })"), + Document.class)); assertEquals("invalid_query", exc.getResponse().getErrorCode()); assertEquals(400, exc.getResponse().getStatusCode()); @@ -226,12 +284,16 @@ void query_withFailure_ShouldThrow() { @Test void asyncQuery_withFailure_ShouldThrow() { - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn("{\"stats\":{},\"error\":{\"code\":\"invalid_query\"}}"); + HttpResponse resp = mockResponse( + "{\"stats\":{},\"error\":{\"code\":\"invalid_query\"}}"); when(resp.statusCode()).thenReturn(400); - when(mockHttpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); - CompletableFuture> future = client.asyncQuery(fql("Collection.create({ name: 'Dogs' })"), Document.class); - ExecutionException exc = assertThrows(ExecutionException.class, () -> future.get()); + when(mockHttpClient.sendAsync(any(), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); + CompletableFuture> future = + client.asyncQuery(fql("Collection.create({ name: 'Dogs' })"), + Document.class); + ExecutionException exc = + assertThrows(ExecutionException.class, () -> future.get()); QueryCheckException cause = (QueryCheckException) exc.getCause(); assertEquals("invalid_query", cause.getResponse().getErrorCode()); assertEquals(400, cause.getResponse().getStatusCode()); @@ -240,14 +302,20 @@ void asyncQuery_withFailure_ShouldThrow() { @Test void asyncQuery_withNoRetries_ShouldNotRetry() { - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn("{\"stats\":{},\"error\":{\"code\":\"limit_exceeded\"}}"); + HttpResponse resp = mockResponse( + "{\"stats\":{},\"error\":{\"code\":\"limit_exceeded\"}}"); when(resp.statusCode()).thenReturn(429); - when(mockHttpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); - FaunaClient noRetryClient = Fauna.client(FaunaConfig.DEFAULT, mockHttpClient, FaunaClient.NO_RETRY_STRATEGY); - CompletableFuture> future = noRetryClient.asyncQuery( - fql("Collection.create({ name: 'Dogs' })"), Document.class); - ExecutionException exc = assertThrows(ExecutionException.class, () -> future.get()); + when(mockHttpClient.sendAsync(any(), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); + FaunaClient noRetryClient = + Fauna.client(FaunaConfig.DEFAULT, mockHttpClient, + FaunaClient.NO_RETRY_STRATEGY); + CompletableFuture> future = + noRetryClient.asyncQuery( + fql("Collection.create({ name: 'Dogs' })"), + Document.class); + ExecutionException exc = + assertThrows(ExecutionException.class, future::get); ThrottlingException cause = (ThrottlingException) exc.getCause(); assertEquals("limit_exceeded", cause.getResponse().getErrorCode()); assertEquals(429, cause.getResponse().getStatusCode()); @@ -257,37 +325,46 @@ void asyncQuery_withNoRetries_ShouldNotRetry() { @Test void asyncQuery_withRetryableException_ShouldRetry() { // GIVEN - HttpResponse resp = mock(HttpResponse.class); + HttpResponse resp = mockResponse( + "{\"stats\":{},\"error\":{\"code\":\"limit_exceeded\"}}"); int retryAttempts = 10; - when(resp.body()).thenReturn("{\"stats\":{},\"error\":{\"code\":\"limit_exceeded\"}}"); when(resp.statusCode()).thenReturn(429); - when(mockHttpClient.sendAsync(any(), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); + when(mockHttpClient.sendAsync(any(), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); // WHEN - BaseFaunaClient fastClient = new BaseFaunaClient(FaunaConfig.builder().build(), mockHttpClient, - new ExponentialBackoffStrategy(retryAttempts, 1f, 10, 10, 0.1f)); - CompletableFuture> future = fastClient.asyncQuery( - fql("Collection.create({ name: 'Dogs' })"), Document.class); + BaseFaunaClient fastClient = + new BaseFaunaClient(FaunaConfig.builder().build(), + mockHttpClient, + new ExponentialBackoffStrategy(retryAttempts, 1f, 10, + 10, 0.1f)); + CompletableFuture> future = + fastClient.asyncQuery( + fql("Collection.create({ name: 'Dogs' })"), + Document.class); // THEN - ExecutionException exc = assertThrows(ExecutionException.class, () -> future.get()); + ExecutionException exc = + assertThrows(ExecutionException.class, future::get); ThrottlingException cause = (ThrottlingException) exc.getCause(); assertEquals("limit_exceeded", cause.getResponse().getErrorCode()); assertEquals(429, cause.getResponse().getStatusCode()); - verify(mockHttpClient, times(retryAttempts + 1)).sendAsync(any(), any()); + verify(mockHttpClient, times(retryAttempts + 1)).sendAsync(any(), + any()); } @Test - void asyncQuery_shouldSucceedOnRetry() throws ExecutionException, InterruptedException { + void asyncQuery_shouldSucceedOnRetry() + throws ExecutionException, InterruptedException { // GIVEN - HttpResponse retryableResp = mock(HttpResponse.class); - when(retryableResp.body()).thenReturn("{\"stats\":{},\"error\":{\"code\":\"limit_exceeded\"}}"); + HttpResponse retryableResp = mockResponse( + "{\"stats\":{},\"error\":{\"code\":\"limit_exceeded\"}}"); when(retryableResp.statusCode()).thenReturn(429); - HttpResponse successResp = mock(HttpResponse.class); - when(successResp.body()).thenReturn("{\"stats\": {}}"); + HttpResponse successResp = mockResponse("{\"stats\": {}}"); when(successResp.statusCode()).thenReturn(200); when(mockHttpClient.sendAsync(any(), any())).thenReturn( - CompletableFuture.supplyAsync(() -> retryableResp), CompletableFuture.supplyAsync(() -> successResp)); + CompletableFuture.supplyAsync(() -> retryableResp), + CompletableFuture.supplyAsync(() -> successResp)); // WHEN CompletableFuture> future = client.asyncQuery( @@ -301,25 +378,26 @@ void asyncQuery_shouldSucceedOnRetry() throws ExecutionException, InterruptedExc @Test void paginateWithQueryOptions() throws IOException { - String productBase = "{\"@doc\":{\"id\":\"406412545672348160\",\"coll\":{\"@mod\":\"Product\"}," + - "\"ts\":{\"@time\":\"2024-08-16T21:34:16.700Z\"},\"name\":\"%s\",\"quantity\":{\"@int\":\"0\"}}}"; - String bodyBase = "{\"data\":{\"@set\":{\"data\":[%s],\"after\":%s}},\"summary\":\"\"," + - "\"txn_ts\":1723844145837000,\"stats\":{},\"schema_version\":1723844028490000}"; - QueryOptions options = QueryOptions.builder().timeout(Duration.ofMillis(42)).build(); - - HttpResponse firstPageResp = mock(HttpResponse.class); - when(firstPageResp.body()).thenReturn(String.format(bodyBase, String.format(productBase, "product-0"), "\"after_token\"")); + QueryOptions options = + QueryOptions.builder().timeout(Duration.ofMillis(42)).build(); + + HttpResponse firstPageResp = mockResponse( + String.format(bodyBase, String.format(productBase, "product-0"), + "\"after_token\"")); when(firstPageResp.statusCode()).thenReturn(200); - HttpResponse secondPageResp = mock(HttpResponse.class); - when(secondPageResp.body()).thenReturn(String.format(bodyBase, String.format(productBase, "product-1"), "null")); + HttpResponse secondPageResp = mockResponse( + String.format(bodyBase, String.format(productBase, "product-1"), + "null")); when(secondPageResp.statusCode()).thenReturn(200); - ArgumentMatcher matcher = new HttpRequestMatcher(Map.of("X-Query-Timeout-Ms", "42")); + ArgumentMatcher matcher = + new HttpRequestMatcher(Map.of("X-Query-Timeout-Ms", "42")); when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn( CompletableFuture.supplyAsync(() -> firstPageResp), CompletableFuture.supplyAsync(() -> secondPageResp)); - PageIterator iter = client.paginate(fql("Document.all()"), Product.class, options); + PageIterator iter = + client.paginate(fql("Document.all()"), Product.class, options); assertTrue(iter.hasNext()); Page firstPage = iter.next(); assertEquals("product-0", firstPage.getData().get(0).getName()); @@ -327,7 +405,38 @@ void paginateWithQueryOptions() throws IOException { Page secondPage = iter.next(); assertEquals("product-1", secondPage.getData().get(0).getName()); assertFalse(iter.hasNext()); - } + @Test + void paginateWithQueryOptionsAndNoElementType() { + QueryOptions options = + QueryOptions.builder().timeout(Duration.ofMillis(42)).build(); + + HttpResponse firstPageResp = mockResponse( + String.format(bodyBase, String.format(productBase, "product-0"), + "\"after_token\"")); + when(firstPageResp.statusCode()).thenReturn(200); + HttpResponse secondPageResp = mockResponse( + String.format(bodyBase, String.format(productBase, "product-1"), + "null")); + when(secondPageResp.statusCode()).thenReturn(200); + + ArgumentMatcher matcher = + new HttpRequestMatcher(Map.of("X-Query-Timeout-Ms", "42")); + when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn( + CompletableFuture.supplyAsync(() -> firstPageResp), + CompletableFuture.supplyAsync(() -> secondPageResp)); + + PageIterator iter = + client.paginate(fql("Document.all()"), options); + assertTrue(iter.hasNext()); + Page firstPage = iter.next(); + assertEquals("product-0", + ((Document) firstPage.getData().get(0)).get("name")); + assertTrue(iter.hasNext()); + Page secondPage = iter.next(); + assertEquals("product-1", + ((Document) secondPage.getData().get(0)).get("name")); + assertFalse(iter.hasNext()); + } } \ No newline at end of file diff --git a/src/test/java/com/fauna/client/FaunaConfigTest.java b/src/test/java/com/fauna/client/FaunaConfigTest.java new file mode 100644 index 00000000..0e0dea05 --- /dev/null +++ b/src/test/java/com/fauna/client/FaunaConfigTest.java @@ -0,0 +1,59 @@ +package com.fauna.client; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.Duration; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class FaunaConfigTest { + + @Test + public void testDefaultFaunaConfig() { + // Running this test with FAUNA_ENDPOINT, FAUNA_SECRET, and FAUNA_DEBUG environment variables in an + // IDE can be used to check that we set things correctly. + FaunaConfig config = FaunaConfig.builder().build(); + assertEquals("https://db.fauna.com", config.getEndpoint()); + assertEquals(Level.WARNING, config.getLogHandler().getLevel()); + assertEquals("", config.getSecret()); + assertEquals(3, config.getMaxContentionRetries()); + assertNotNull(config.getStatsCollector()); + } + + @Test + public void testOverridingDefaultFaunaConfig() { + // Running this test with FAUNA_ENDPOINT, FAUNA_SECRET, and FAUNA_DEBUG environment variables in an + // IDE can be used to check that we set things correctly. + ConsoleHandler handler = new ConsoleHandler(); + handler.setLevel(Level.ALL); + FaunaConfig config = FaunaConfig.builder() + .secret("foo") + .endpoint("endpoint") + .logHandler(handler) + .maxContentionRetries(1) + .clientTimeoutBuffer(Duration.ofSeconds(1)) + .build(); + assertEquals("endpoint", config.getEndpoint()); + assertEquals(Level.ALL, config.getLogHandler().getLevel()); + assertEquals("foo", config.getSecret()); + assertEquals(1, config.getMaxContentionRetries()); + assertNotNull(config.getStatsCollector()); + } + + @ParameterizedTest + @ValueSource(strings = {"1", "DEBUG", "2", "foo", "0.0", " 1", " 1000 "}) + public void testDebugLogVals(String val) { + assertEquals(Level.FINE, FaunaConfig.Builder.getLogLevel(val)); + } + + @ParameterizedTest + @ValueSource(strings = {"0", " ", "-1", "", "\n", " \r \n \t"}) + public void testWarningLogVals(String val) { + assertEquals(Level.WARNING, FaunaConfig.Builder.getLogLevel(val)); + } +} diff --git a/src/test/java/com/fauna/client/FaunaRoleTest.java b/src/test/java/com/fauna/client/FaunaRoleTest.java index e290c830..f50c540e 100644 --- a/src/test/java/com/fauna/client/FaunaRoleTest.java +++ b/src/test/java/com/fauna/client/FaunaRoleTest.java @@ -1,6 +1,7 @@ package com.fauna.client; import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -16,15 +17,20 @@ public void testBuiltInRoles() { @Test public void testValidUserDefinedRoles() { assertEquals("@role/foo", FaunaRole.named("foo").toString()); - assertEquals("@role/slartibartfast", FaunaRole.named("slartibartfast").toString()); + assertEquals("@role/slartibartfast", + FaunaRole.named("slartibartfast").toString()); } @Test public void testInvalidUserDefinedRoles() { - assertThrows(IllegalArgumentException.class, () -> FaunaRole.named("server").toString()); - assertThrows(IllegalArgumentException.class, () -> FaunaRole.named("1foo").toString()); - assertThrows(IllegalArgumentException.class, () -> FaunaRole.named("foo$").toString()); - assertThrows(IllegalArgumentException.class, () -> FaunaRole.named("foo bar").toString()); + assertThrows(IllegalArgumentException.class, + () -> FaunaRole.named("server").toString()); + assertThrows(IllegalArgumentException.class, + () -> FaunaRole.named("1foo").toString()); + assertThrows(IllegalArgumentException.class, + () -> FaunaRole.named("foo$").toString()); + assertThrows(IllegalArgumentException.class, + () -> FaunaRole.named("foo bar").toString()); } } diff --git a/src/test/java/com/fauna/client/FeedIteratorTest.java b/src/test/java/com/fauna/client/FeedIteratorTest.java new file mode 100644 index 00000000..48669669 --- /dev/null +++ b/src/test/java/com/fauna/client/FeedIteratorTest.java @@ -0,0 +1,192 @@ +package com.fauna.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fauna.codec.Codec; +import com.fauna.codec.DefaultCodecProvider; +import com.fauna.event.EventSource; +import com.fauna.event.FaunaEvent; +import com.fauna.event.FeedIterator; +import com.fauna.event.FeedOptions; +import com.fauna.event.FeedPage; +import com.fauna.exception.InvalidRequestException; +import com.fauna.response.ErrorInfo; +import com.fauna.response.QueryFailure; +import com.fauna.response.QueryResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + + +@ExtendWith(MockitoExtension.class) +public class FeedIteratorTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final EventSource source = EventSource.fromToken("token"); + private static final String CURSOR_0 = "cursor0"; + + @Mock + private FaunaClient client; + + private CompletableFuture> successFuture(boolean after, + int num) + throws IOException { + List> events = new ArrayList<>(); + Codec codec = DefaultCodecProvider.SINGLETON.get(String.class); + events.add(new FaunaEvent<>(FaunaEvent.EventType.ADD, + "cursor0", System.currentTimeMillis() - 10, + num + "-a", null, null)); + events.add(new FaunaEvent<>(FaunaEvent.EventType.ADD, + "cursor0", System.currentTimeMillis() - 5, + num + "-b", null, null)); + + return CompletableFuture.supplyAsync( + () -> FeedPage.builder(codec, new StatsCollectorImpl()) + .events(events).cursor("cursor0").hasNext(after) + .build()); + } + + private CompletableFuture> failureFuture() + throws IOException { + ObjectNode root = MAPPER.createObjectNode(); + ObjectNode error = root.putObject("error"); + error.put("code", "invalid_query"); + + QueryFailure failure = new QueryFailure(400, QueryResponse.builder(null) + .error(ErrorInfo.builder().code("invalid_query").build())); + return CompletableFuture.failedFuture( + new InvalidRequestException(failure)); + } + + + @Test + public void test_single_page() throws IOException { + FeedOptions options = FeedOptions.builder().pageSize(8).build(); + when(client.poll(source, options, String.class)).thenReturn( + successFuture(false, 0)); + FeedIterator feedIterator = + new FeedIterator<>(client, source, options, String.class); + assertTrue(feedIterator.hasNext()); + assertEquals(List.of("0-a", "0-b"), + feedIterator.next().getEvents().stream() + .map(e -> e.getData().get()) + .collect(Collectors.toList())); + assertFalse(feedIterator.hasNext()); + assertThrows(NoSuchElementException.class, feedIterator::next); + } + + @Test + public void test_single_page_without_calling_hasNext() throws IOException { + when(client.poll(source, FeedOptions.DEFAULT, String.class)).thenReturn( + successFuture(false, 0)); + FeedIterator feedIterator = + new FeedIterator<>(client, source, FeedOptions.DEFAULT, + String.class); + // No call to hasNext here. + assertEquals(List.of("0-a", "0-b"), + feedIterator.next().getEvents().stream() + .map(e -> e.getData().get()) + .collect(Collectors.toList())); + assertFalse(feedIterator.hasNext()); + assertThrows(NoSuchElementException.class, feedIterator::next); + } + + @Test + public void test_multiple_pages() throws IOException { + when(client.poll(argThat(source::equals), any(), + any(Class.class))).thenReturn(successFuture(true, 0), + successFuture(false, 1)); + FeedIterator feedIterator = + new FeedIterator<>(client, source, FeedOptions.DEFAULT, + String.class); + + assertTrue(feedIterator.hasNext()); + assertEquals(List.of("0-a", "0-b"), + feedIterator.next().getEvents().stream() + .map(e -> e.getData().get()) + .collect(Collectors.toList())); + assertTrue(feedIterator.hasNext()); + + assertEquals(List.of("1-a", "1-b"), + feedIterator.next().getEvents().stream() + .map(e -> e.getData().get()) + .collect(Collectors.toList())); + assertFalse(feedIterator.hasNext()); + assertThrows(NoSuchElementException.class, feedIterator::next); + } + + @Test + public void test_multiple_pages_async() + throws IOException, ExecutionException, InterruptedException { + when(client.poll(argThat(source::equals), any(), + any(Class.class))).thenReturn(successFuture(true, 0), + successFuture(false, 1)); + FeedIterator feedIterator = + new FeedIterator<>(client, source, FeedOptions.DEFAULT, + String.class); + + boolean hasNext = feedIterator.hasNext(); + List products = new ArrayList<>(); + while (hasNext) { + hasNext = feedIterator.nextAsync().thenApply(page -> { + products.addAll( + page.getEvents().stream().map(e -> e.getData().get()) + .collect(Collectors.toList())); + return feedIterator.hasNext(); + }).get(); + } + assertEquals(4, products.size()); + } + + @Test + public void test_flatten() throws IOException { + when(client.poll(argThat(source::equals), + argThat(FeedOptions.DEFAULT::equals), + any(Class.class))).thenReturn(successFuture(true, 0)); + when(client.poll(argThat(source::equals), + argThat(opts -> opts.getCursor().orElse("").equals(CURSOR_0)), + any(Class.class))).thenReturn(successFuture(false, 1)); + FeedIterator feedIterator = + new FeedIterator<>(client, source, FeedOptions.DEFAULT, + String.class); + Iterator> iter = feedIterator.flatten(); + List products = new ArrayList<>(); + iter.forEachRemaining( + event -> products.add(event.getData().orElseThrow())); + assertEquals(4, products.size()); + + } + + @Test + public void test_error_thrown() throws IOException { + when(client.poll(source, FeedOptions.DEFAULT, String.class)).thenReturn( + failureFuture()); + FeedIterator feedIterator = + new FeedIterator<>(client, source, FeedOptions.DEFAULT, + String.class); + + // We could return the wrapped completion exception here. + assertTrue(feedIterator.hasNext()); + InvalidRequestException exc = + assertThrows(InvalidRequestException.class, + () -> feedIterator.next()); + assertEquals("invalid_query", exc.getResponse().getErrorCode()); + } +} diff --git a/src/test/java/com/fauna/client/HttpRequestMatcher.java b/src/test/java/com/fauna/client/HttpRequestMatcher.java index 9913d477..6e5d83ed 100644 --- a/src/test/java/com/fauna/client/HttpRequestMatcher.java +++ b/src/test/java/com/fauna/client/HttpRequestMatcher.java @@ -19,7 +19,8 @@ public boolean matches(HttpRequest httpRequest) { Map> headers = httpRequest.headers().map(); for (Map.Entry header : expectedHeaders.entrySet()) { // It's possible to have multiple headers returned, but assert that we only get one. - if (!headers.getOrDefault(header.getKey(), List.of()).equals(List.of(header.getValue()))) { + if (!headers.getOrDefault(header.getKey(), List.of()) + .equals(List.of(header.getValue()))) { return false; } } diff --git a/src/test/java/com/fauna/client/PageIteratorTest.java b/src/test/java/com/fauna/client/PageIteratorTest.java index 2ef48628..da7d012d 100644 --- a/src/test/java/com/fauna/client/PageIteratorTest.java +++ b/src/test/java/com/fauna/client/PageIteratorTest.java @@ -4,13 +4,13 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fauna.codec.DefaultCodecProvider; +import com.fauna.codec.PageOf; +import com.fauna.codec.ParameterizedOf; import com.fauna.exception.InvalidRequestException; +import com.fauna.response.ErrorInfo; import com.fauna.response.QueryFailure; -import com.fauna.response.wire.QueryResponseWire; +import com.fauna.response.QueryResponse; import com.fauna.response.QuerySuccess; -import com.fauna.codec.PageOf; -import com.fauna.codec.ParameterizedOf; -import com.fauna.types.Document; import com.fauna.types.Page; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,6 +19,7 @@ import java.io.IOException; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.CompletableFuture; @@ -39,9 +40,9 @@ public class PageIteratorTest { @Mock private FaunaClient client; - private CompletableFuture>> successFuture(boolean after, int num) throws IOException { - ObjectNode root = MAPPER.createObjectNode(); - ObjectNode page = root.putObject("data"); + private CompletableFuture>> successFuture( + boolean after, int num) throws IOException { + ObjectNode page = MAPPER.createObjectNode(); if (after) { page.put("after", "afterToken"); } @@ -49,24 +50,32 @@ private CompletableFuture>> successFuture(boolean af arr.add(num + "-a"); arr.add(num + "-b"); - var res = MAPPER.readValue(root.toString(), QueryResponseWire.class); - QuerySuccess> success = new QuerySuccess(DefaultCodecProvider.SINGLETON.get(Page.class, new Type[]{String.class}), res); - return CompletableFuture.supplyAsync(() -> success); + QueryResponse.Builder builder = QueryResponse.builder( + DefaultCodecProvider.SINGLETON.get(Page.class, + new Type[] {String.class})) + .data(MAPPER.createParser(page.toString())); + return CompletableFuture.supplyAsync(() -> new QuerySuccess<>(builder)); } - private CompletableFuture> failureFuture() throws IOException { + private CompletableFuture> failureFuture() + throws IOException { ObjectNode root = MAPPER.createObjectNode(); ObjectNode error = root.putObject("error"); error.put("code", "invalid_query"); - var res = MAPPER.readValue(root.toString(), QueryResponseWire.class); - return CompletableFuture.failedFuture(new InvalidRequestException(new QueryFailure(400, res))); + + QueryFailure failure = new QueryFailure(400, QueryResponse.builder(null) + .error(ErrorInfo.builder().code("invalid_query").build())); + return CompletableFuture.failedFuture( + new InvalidRequestException(failure)); } @Test public void test_single_page() throws Exception { - when(client.asyncQuery(any(), any(ParameterizedOf.class), any())).thenReturn(successFuture(false, 0)); - PageIterator pageIterator = new PageIterator<>(client, fql("hello"), String.class, null); + when(client.asyncQuery(any(), any(ParameterizedOf.class), + any())).thenReturn(successFuture(false, 0)); + PageIterator pageIterator = + new PageIterator<>(client, fql("hello"), String.class, null); assertTrue(pageIterator.hasNext()); assertEquals(pageIterator.next().getData(), List.of("0-a", "0-b")); assertFalse(pageIterator.hasNext()); @@ -75,8 +84,10 @@ public void test_single_page() throws Exception { @Test public void test_single_page_without_calling_hasNext() throws Exception { - when(client.asyncQuery(any(), any(ParameterizedOf.class), any())).thenReturn(successFuture(false, 0)); - PageIterator pageIterator = new PageIterator<>(client, fql("hello"), String.class, null); + when(client.asyncQuery(any(), any(ParameterizedOf.class), + any())).thenReturn(successFuture(false, 0)); + PageIterator pageIterator = + new PageIterator<>(client, fql("hello"), String.class, null); // No call to hasNext here. assertEquals(pageIterator.next().getData(), List.of("0-a", "0-b")); assertFalse(pageIterator.hasNext()); @@ -85,9 +96,11 @@ public void test_single_page_without_calling_hasNext() throws Exception { @Test public void test_multiple_pages() throws Exception { - when(client.asyncQuery(any(), any(ParameterizedOf.class), any())).thenReturn( + when(client.asyncQuery(any(), any(ParameterizedOf.class), + any())).thenReturn( successFuture(true, 0), successFuture(false, 1)); - PageIterator pageIterator = new PageIterator<>(client, fql("hello"), String.class, null); + PageIterator pageIterator = + new PageIterator<>(client, fql("hello"), String.class, null); assertTrue(pageIterator.hasNext()); assertEquals(List.of("0-a", "0-b"), pageIterator.next().getData()); @@ -98,40 +111,35 @@ public void test_multiple_pages() throws Exception { } @Test - public void test_error_thrown() throws IOException { - when(client.asyncQuery(any(), any(ParameterizedOf.class), any())).thenReturn(failureFuture()); - PageIterator pageIterator = new PageIterator<>(client, fql("hello"), String.class, null); - // We could return the wrapped completion exception here. - InvalidRequestException exc = assertThrows(InvalidRequestException.class, () -> pageIterator.hasNext()); - assertEquals("invalid_query", exc.getResponse().getErrorCode()); - } - - @Test - public void test_PageIterator_from_single_Page() { - Page page = new Page<>(List.of("hello"), null); - PageIterator pageIterator = new PageIterator<>(client, page, String.class, null); - assertTrue(pageIterator.hasNext()); - assertEquals(page, pageIterator.next()); - assertFalse(pageIterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> pageIterator.next()); - + public void test_multiple_pages_async() throws Exception { + when(client.asyncQuery(any(), any(ParameterizedOf.class), + any())).thenReturn( + successFuture(true, 0), successFuture(false, 1)); + PageIterator pageIterator = + new PageIterator<>(client, fql("hello"), String.class, null); + + boolean hasNext = pageIterator.hasNext(); + List products = new ArrayList<>(); + while (hasNext) { + hasNext = pageIterator.nextAsync().thenApply(page -> { + products.addAll(page.getData()); + return pageIterator.hasNext(); + }).get(); + } + assertEquals(4, products.size()); } @Test - public void test_PageIterator_from_Page() throws IOException { - Page page = new Page<>(List.of("hello"), "foo"); - PageIterator pageIterator = new PageIterator<>(client, page, String.class, null); - - assertTrue(pageIterator.hasNext()); - when(client.asyncQuery(any(), any(ParameterizedOf.class), any())).thenReturn( - successFuture(false, 0)); - assertEquals(page, pageIterator.next()); - + public void test_error_thrown() throws IOException { + when(client.asyncQuery(any(), any(ParameterizedOf.class), + any())).thenReturn(failureFuture()); + PageIterator pageIterator = + new PageIterator<>(client, fql("hello"), String.class, null); + // We could return the wrapped completion exception here. assertTrue(pageIterator.hasNext()); - assertEquals(List.of("0-a", "0-b"), pageIterator.next().getData()); - - assertFalse(pageIterator.hasNext()); - assertThrows(NoSuchElementException.class, () -> pageIterator.next()); - + InvalidRequestException exc = + assertThrows(InvalidRequestException.class, + () -> pageIterator.next()); + assertEquals("invalid_query", exc.getResponse().getErrorCode()); } } diff --git a/src/test/java/com/fauna/client/QueryTagsTest.java b/src/test/java/com/fauna/client/QueryTagsTest.java index c3cbe6fe..0050682f 100644 --- a/src/test/java/com/fauna/client/QueryTagsTest.java +++ b/src/test/java/com/fauna/client/QueryTagsTest.java @@ -1,10 +1,8 @@ package com.fauna.client; +import com.fauna.query.QueryTags; import org.junit.jupiter.api.Test; -import java.util.HashMap; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,45 +10,43 @@ class QueryTagsTest { @Test void encode_shouldConvertMapToString() { - Map tags = new HashMap<>(); + QueryTags tags = new QueryTags(); tags.put("key1", "value1"); tags.put("key2", "value2"); tags.put("key3", "value3"); - String result = RequestBuilder.QueryTags.encode(tags); + String result = tags.encode(); - assertTrue(result.contains("key1=value1")); - assertTrue(result.contains("key2=value2")); - assertTrue(result.contains("key3=value3")); - assertTrue(result.matches("^(key1=value1,key2=value2,key3=value3|key1=value1,key3=value3,key2=value2|key2=value2,key1=value1,key3=value3|key2=value2,key3=value3,key1=value1|key3=value3,key1=value1,key2=value2|key3=value3,key2=value2,key1=value1)$")); + assertEquals("key1=value1,key2=value2,key3=value3", result); } @Test void encode_shouldHandleEmptyMap() { - Map tags = new HashMap<>(); + QueryTags tags = new QueryTags(); - String result = RequestBuilder.QueryTags.encode(tags); + String result = tags.encode(); assertEquals("", result); } @Test void encode_shouldHandleSingleEntry() { - Map tags = new HashMap<>(); + QueryTags tags = new QueryTags(); tags.put("key1", "value1"); - String result = RequestBuilder.QueryTags.encode(tags); + + String result = tags.encode(); assertEquals("key1=value1", result); } @Test void encode_shouldHandleSpecialCharacters() { - Map tags = new HashMap<>(); + QueryTags tags = new QueryTags(); tags.put("key1", "value1,value2"); tags.put("key2", "value3,value4"); - String result = RequestBuilder.QueryTags.encode(tags); + String result = tags.encode(); assertTrue(result.contains("key1=value1,value2")); assertTrue(result.contains("key2=value3,value4")); diff --git a/src/test/java/com/fauna/client/RequestBuilderTest.java b/src/test/java/com/fauna/client/RequestBuilderTest.java index fb71ea1d..bdb3c2a9 100644 --- a/src/test/java/com/fauna/client/RequestBuilderTest.java +++ b/src/test/java/com/fauna/client/RequestBuilderTest.java @@ -1,8 +1,13 @@ package com.fauna.client; -import com.fauna.codec.*; +import com.fauna.codec.CodecProvider; +import com.fauna.codec.CodecRegistry; +import com.fauna.codec.DefaultCodecProvider; +import com.fauna.codec.DefaultCodecRegistry; +import com.fauna.event.EventSource; +import com.fauna.event.StreamOptions; +import com.fauna.event.StreamRequest; import com.fauna.query.QueryOptions; -import com.fauna.stream.StreamRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -10,15 +15,20 @@ import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.time.Duration; -import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import java.util.stream.Collectors; import java.util.stream.IntStream; import static com.fauna.client.RequestBuilder.Headers.AUTHORIZATION; import static com.fauna.client.RequestBuilder.Headers.DRIVER_ENV; +import static com.fauna.client.RequestBuilder.Headers.LAST_TXN_TS; import static com.fauna.client.RequestBuilder.Headers.LINEARIZED; +import static com.fauna.client.RequestBuilder.Headers.MAX_CONTENTION_RETRIES; import static com.fauna.client.RequestBuilder.Headers.QUERY_TAGS; +import static com.fauna.client.RequestBuilder.Headers.QUERY_TIMEOUT_MS; import static com.fauna.client.RequestBuilder.Headers.TRACE_PARENT; import static com.fauna.client.RequestBuilder.Headers.TYPE_CHECK; import static com.fauna.query.builder.Query.fql; @@ -30,48 +40,92 @@ class RequestBuilderTest { private final FaunaConfig faunaConfig = FaunaConfig.builder() .endpoint(FaunaConfig.FaunaEndpoint.LOCAL) - .secret("secret").build();; + .secret("secret").build(); - private final RequestBuilder requestBuilder = RequestBuilder.queryRequestBuilder(faunaConfig); + private static final EventSource SOURCE = EventSource.fromToken("tkn"); + + private final RequestBuilder requestBuilder = + RequestBuilder.queryRequestBuilder(faunaConfig, Logger.getGlobal()); private final CodecRegistry codecRegistry = new DefaultCodecRegistry(); - private final CodecProvider codecProvider = new DefaultCodecProvider(codecRegistry); + private final CodecProvider codecProvider = + new DefaultCodecProvider(codecRegistry); @Test void buildRequest_shouldConstructCorrectHttpRequest() { - HttpRequest httpRequest = requestBuilder.buildRequest(fql("Sample fql query"), null, codecProvider); + HttpRequest httpRequest = + requestBuilder.buildRequest(fql("Sample fql query"), null, + codecProvider, -1L); - assertEquals("http://localhost:8443/query/1", httpRequest.uri().toString()); + assertEquals("http://localhost:8443/query/1", + httpRequest.uri().toString()); assertEquals("POST", httpRequest.method()); - assertTrue(httpRequest.bodyPublisher().orElseThrow().contentLength() > 0); + assertTrue( + httpRequest.bodyPublisher().orElseThrow().contentLength() > 0); HttpHeaders headers = httpRequest.headers(); - assertTrue(headers.firstValue(DRIVER_ENV).orElse("").contains("runtime=java")); - assertTrue(headers.firstValue(DRIVER_ENV).orElse("").contains("driver=")); + assertTrue(httpRequest.timeout().isEmpty()); + assertTrue(headers.firstValue(DRIVER_ENV).orElse("") + .contains("runtime=java")); + assertTrue( + headers.firstValue(DRIVER_ENV).orElse("").contains("driver=")); assertNotNull(headers.firstValue(AUTHORIZATION)); - assertEquals("Bearer secret", headers.firstValue(AUTHORIZATION).orElseThrow()); + assertEquals("Bearer secret", + headers.firstValue(AUTHORIZATION).orElseThrow()); + assertEquals("3", + headers.firstValue(MAX_CONTENTION_RETRIES).orElseThrow()); + List.of(LINEARIZED, TYPE_CHECK, QUERY_TAGS, LAST_TXN_TS, + QUERY_TIMEOUT_MS).forEach( + hdr -> assertTrue(headers.firstValue(hdr).isEmpty())); } @Test void buildRequest_shouldIncludeOptionalHeadersWhenPresent() { - QueryOptions options = QueryOptions.builder().timeout(Duration.ofSeconds(15)) - .linearized(true).typeCheck(true).traceParent("traceParent").build(); - - HttpRequest httpRequest = requestBuilder.buildRequest(fql("Sample FQL Query"), options, codecProvider); + QueryOptions options = + QueryOptions.builder().timeout(Duration.ofSeconds(15)) + .linearized(true).typeCheck(true) + .traceParent("traceParent").build(); + + HttpRequest httpRequest = + requestBuilder.buildRequest(fql("Sample FQL Query"), options, + codecProvider, 1L); HttpHeaders headers = httpRequest.headers(); assertEquals("true", headers.firstValue(LINEARIZED).orElseThrow()); assertEquals("true", headers.firstValue(TYPE_CHECK).orElseThrow()); assertNotNull(headers.firstValue(QUERY_TAGS)); - assertEquals("traceParent", headers.firstValue(TRACE_PARENT).orElseThrow()); + assertEquals("traceParent", + headers.firstValue(TRACE_PARENT).orElseThrow()); + assertEquals("1", headers.firstValue(LAST_TXN_TS).orElseThrow()); + // Query timeout + 5 seconds (default). + assertEquals(Duration.ofSeconds(20), + httpRequest.timeout().orElseThrow()); + } + + @Test + void buildRequest_withCustomTimeoutBuffer() { + QueryOptions defaultOpts = QueryOptions.builder().build(); + QueryOptions timeoutOpts = + QueryOptions.builder().timeout(Duration.ofSeconds(15)).build(); + + RequestBuilder requestBuilder = RequestBuilder.queryRequestBuilder( + FaunaConfig.builder().clientTimeoutBuffer(Duration.ofSeconds(1)) + .build(), Logger.getGlobal()); + assertEquals(Duration.ofSeconds(6), + requestBuilder.buildRequest(fql("42"), defaultOpts, + codecProvider, 1L).timeout().orElseThrow()); + assertEquals(Duration.ofSeconds(16), + requestBuilder.buildRequest(fql("42"), timeoutOpts, + codecProvider, 1L).timeout().orElseThrow()); } @Test void buildStreamRequestBody_shouldOnlyIncludeToken() throws IOException { // Given - StreamRequest request = new StreamRequest("tkn"); + StreamRequest request = + new StreamRequest(SOURCE, StreamOptions.DEFAULT); // When - String body = requestBuilder.buildStreamRequestBody(request); + String body = request.serialize(); // Then assertEquals("{\"token\":\"tkn\"}", body); } @@ -79,30 +133,57 @@ void buildStreamRequestBody_shouldOnlyIncludeToken() throws IOException { @Test void buildStreamRequestBody_shouldIncludeCursor() throws IOException { // Given - StreamRequest request = new StreamRequest("tkn", "cur"); + HttpRequest req = requestBuilder.buildStreamRequest(SOURCE, + StreamOptions.builder().cursor("cur").build()); // When - String body = requestBuilder.buildStreamRequestBody(request); + long contentLength = req.bodyPublisher().orElseThrow().contentLength(); + // Then - assertEquals("{\"token\":\"tkn\",\"cursor\":\"cur\"}", body); + assertEquals("{\"token\":\"tkn\",\"cursor\":\"cur\"}".length(), + contentLength); } @Test void buildStreamRequestBody_shouldIncludeTimestamp() throws IOException { // Given - Long timestamp = Long.MAX_VALUE / 2; - StreamRequest request = new StreamRequest("tkn", Long.MAX_VALUE / 2); + HttpRequest request = requestBuilder.buildStreamRequest(SOURCE, + StreamOptions.builder().startTimestamp(Long.MAX_VALUE).build()); // When - String body = requestBuilder.buildStreamRequestBody(request); + long contentLength = + request.bodyPublisher().orElseThrow().contentLength(); // Then - assertEquals("{\"token\":\"tkn\",\"start_ts\":4611686018427387903}", body); + assertEquals( + "{\"token\":\"tkn\",\"start_ts\":4611686018427387903}".length(), + contentLength); } @Test - @Timeout(value=1000, unit = TimeUnit.MILLISECONDS) + @Timeout(value = 1_000, unit = TimeUnit.MILLISECONDS) void buildRequest_shouldBeFast() { // This was faster, but now I think it's taking time to do things like create the FaunaRequest object. // Being able to build 10k requests per second still seems like reasonable performance. - IntStream.range(0, 10000).forEach(i -> requestBuilder.buildRequest( - fql("Sample FQL Query ${i}", Map.of("i", i)), null, codecProvider)); + IntStream.range(0, 10_000).forEach(i -> requestBuilder.buildRequest( + fql("Sample FQL Query ${i}", Map.of("i", i)), null, + codecProvider, 1L)); + } + + @Test + @Timeout(value = 1000, unit = TimeUnit.MILLISECONDS) + void buildRequest_withQueryOptions_shouldBeFast() { + // I tried implementing a cache in RequestBuilder.java that re-uses the HttpRequest.Builder if the QueryOptions + // are identical to a previous request. It wasn't any faster, even for up to 1MM buildRequest iterations. I'm + // leaving this test here in case we ever want to prove that requestBuilder is "fast enough". + // It takes 90ms on my Mac to build 1k requests. That seems "good enough" for now! + List opts = + IntStream.range(0, 100).mapToObj(i -> QueryOptions.builder() + .queryTag("key" + i, String.valueOf(i)) + .timeout(Duration.ofSeconds(i % 10)) + .traceParent("trace" + i) + .typeCheck(i % 2 == 0) + .linearized((i + 1) % 2 == 0) + .build()).collect(Collectors.toList()); + IntStream.range(0, 1_000).forEach(i -> requestBuilder.buildRequest( + fql("Sample FQL Query ${i}", Map.of("i", i)), opts.get(i % 100), + codecProvider, 1L)); } } \ No newline at end of file diff --git a/src/test/java/com/fauna/client/ScopedFaunaClientTest.java b/src/test/java/com/fauna/client/ScopedFaunaClientTest.java index 861ab596..80799322 100644 --- a/src/test/java/com/fauna/client/ScopedFaunaClientTest.java +++ b/src/test/java/com/fauna/client/ScopedFaunaClientTest.java @@ -11,10 +11,13 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -40,31 +43,50 @@ public class ScopedFaunaClientTest { @BeforeEach void setUp() { - FaunaClient baseClient = new BaseFaunaClient(FaunaConfig.LOCAL, mockHttpClient, FaunaClient.NO_RETRY_STRATEGY); + FaunaClient baseClient = + new BaseFaunaClient(FaunaConfig.LOCAL, mockHttpClient, + FaunaClient.NO_RETRY_STRATEGY); scopedClient = Fauna.scoped(baseClient, "myDB", SERVER_READ_ONLY); } + static HttpResponse mockResponse(String body) { + HttpResponse resp = mock(HttpResponse.class); + when(resp.body()).thenReturn(new ByteArrayInputStream( + body.getBytes(StandardCharsets.UTF_8))); + return resp; + } @Test - void query_shouldHaveScopedAuthHeader() throws IOException, InterruptedException { - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn("{\"summary\":\"success\",\"stats\":{}}"); - ArgumentMatcher matcher = new HttpRequestMatcher(Map.of("Authorization", "Bearer secret:myDB:server-readonly")); + void query_shouldHaveScopedAuthHeader() + throws IOException, InterruptedException { + HttpResponse resp = + mockResponse("{\"summary\":\"success\",\"stats\":{}}"); + ArgumentMatcher matcher = new HttpRequestMatcher( + Map.of("Authorization", "Bearer secret:myDB:server-readonly")); - when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); - QuerySuccess response = scopedClient.query(Query.fql("Collection.create({ name: 'Dogs' })"), Document.class); + when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); + QuerySuccess response = scopedClient.query( + Query.fql("Collection.create({ name: 'Dogs' })"), + Document.class); assertEquals("success", response.getSummary()); assertNull(response.getLastSeenTxn()); verify(resp, atLeastOnce()).statusCode(); } @Test - void asyncQuery_shouldHaveScopedAuthHeader() throws InterruptedException, ExecutionException { - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn("{\"summary\":\"success\",\"stats\":{}}"); - ArgumentMatcher matcher = new HttpRequestMatcher(Map.of("Authorization", "Bearer secret:myDB:server-readonly")); - when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); - CompletableFuture> future = scopedClient.asyncQuery(Query.fql("Collection.create({ name: 'Dogs' })"), Document.class); + void asyncQuery_shouldHaveScopedAuthHeader() + throws InterruptedException, ExecutionException { + HttpResponse resp = + mockResponse("{\"summary\":\"success\",\"stats\":{}}"); + ArgumentMatcher matcher = new HttpRequestMatcher( + Map.of("Authorization", "Bearer secret:myDB:server-readonly")); + when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); + CompletableFuture> future = + scopedClient.asyncQuery( + Query.fql("Collection.create({ name: 'Dogs' })"), + Document.class); QueryResponse response = future.get(); assertEquals("success", response.getSummary()); assertNull(response.getLastSeenTxn()); @@ -75,11 +97,14 @@ void asyncQuery_shouldHaveScopedAuthHeader() throws InterruptedException, Execut void recursiveScopedClient_shouldhaveCorrectHeader() { // Default role is "server" FaunaClient recursive = Fauna.scoped(scopedClient, "myOtherDB"); - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn("{\"summary\":\"success\",\"stats\":{}}"); - ArgumentMatcher matcher = new HttpRequestMatcher(Map.of("Authorization", "Bearer secret:myOtherDB:server")); + HttpResponse resp = + mockResponse("{\"summary\":\"success\",\"stats\":{}}"); + ArgumentMatcher matcher = new HttpRequestMatcher( + Map.of("Authorization", "Bearer secret:myOtherDB:server")); - when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn(CompletableFuture.supplyAsync(() -> resp)); - recursive.query(Query.fql("Collection.create({ name: 'Dogs' })"), Document.class); + when(mockHttpClient.sendAsync(argThat(matcher), any())).thenReturn( + CompletableFuture.supplyAsync(() -> resp)); + recursive.query(Query.fql("Collection.create({ name: 'Dogs' })"), + Document.class); } } diff --git a/src/test/java/com/fauna/client/TestExponentialBackoffStrategy.java b/src/test/java/com/fauna/client/TestExponentialBackoffStrategy.java index 2e3d1e8f..34b88527 100644 --- a/src/test/java/com/fauna/client/TestExponentialBackoffStrategy.java +++ b/src/test/java/com/fauna/client/TestExponentialBackoffStrategy.java @@ -17,7 +17,8 @@ public void testNoRetriesBehavior() { @Test public void testBackoffBehaviour() { // Set jitter to 0 just to make testing easier. - RetryStrategy strategy = new ExponentialBackoffStrategy(3, 2, 1000, 20_000, 0.0f); + RetryStrategy strategy = + new ExponentialBackoffStrategy(3, 2, 1000, 20_000, 0.0f); assertTrue(strategy.canRetry(1)); assertEquals(1000, strategy.getDelayMillis(1)); assertTrue(strategy.canRetry(1)); @@ -28,6 +29,7 @@ public void testBackoffBehaviour() { assertFalse(strategy.canRetry(4)); } + @Test public void testDefaultBehaviour() { RetryStrategy strategy = FaunaClient.DEFAULT_RETRY_STRATEGY; @@ -56,7 +58,8 @@ public void testDefaultBehaviour() { @Test public void testMaxBackoffBehaviour() { - ExponentialBackoffStrategy strategy = ExponentialBackoffStrategy.builder().setMaxAttempts(7).build(); + ExponentialBackoffStrategy strategy = + ExponentialBackoffStrategy.builder().maxAttempts(7).build(); assertTrue(strategy.canRetry(0)); assertEquals(0, strategy.getDelayMillis(0), 0); @@ -78,7 +81,13 @@ public void testMaxBackoffBehaviour() { @Test public void testCustomStrategy() { - RetryStrategy strategy = new ExponentialBackoffStrategy(4, 4, 100, 2000, 0.1f); + RetryStrategy strategy = ExponentialBackoffStrategy.builder() + .backoffFactor(4) + .maxAttempts(4) + .initialIntervalMillis(100) + .maxBackoffMillis(2000) + .jitterFactor(0.1f) + .build(); assertTrue(strategy.canRetry(1)); assertTrue(strategy.getDelayMillis(1) >= 90); diff --git a/src/test/java/com/fauna/client/TestRetryHandler.java b/src/test/java/com/fauna/client/TestRetryHandler.java index 451b168b..b0d40cef 100644 --- a/src/test/java/com/fauna/client/TestRetryHandler.java +++ b/src/test/java/com/fauna/client/TestRetryHandler.java @@ -1,17 +1,17 @@ package com.fauna.client; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fauna.exception.ThrottlingException; +import com.fauna.response.ErrorInfo; import com.fauna.response.QueryFailure; -import com.fauna.response.wire.QueryResponseWire; +import com.fauna.response.QueryResponse; import org.junit.jupiter.api.Test; -import java.io.IOException; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Supplier; +import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -30,23 +30,31 @@ public static class FakeResponder { /** * Responds with n throttling exceptions before responding with a success. + * * @param failCount */ public FakeResponder(int failCount) { this.failCount = failCount; } - public CompletableFuture getResponse() throws IOException { + public CompletableFuture getResponse() { responseCount += 1; if (responseCount > failCount) { - return CompletableFuture.supplyAsync(TestRetryHandler::timestamp); + return CompletableFuture.supplyAsync( + TestRetryHandler::timestamp); } else { - return CompletableFuture.failedFuture(new ThrottlingException(new QueryFailure(500, new QueryResponseWire()))); + return CompletableFuture.failedFuture( + new ThrottlingException( + new QueryFailure(500, + QueryResponse.builder(null) + .error(ErrorInfo.builder() + .build())))); } } } - public static Supplier> respond(FakeResponder responder) { + public static Supplier> respond( + FakeResponder responder) { return () -> { try { return responder.getResponse(); @@ -58,34 +66,45 @@ public static Supplier> respond(FakeResponder responde @Test public void testExecute() { - RetryHandler handler = new RetryHandler<>(ExponentialBackoffStrategy.builder().build()); - handler.execute(() -> CompletableFuture.supplyAsync(TestRetryHandler::timestamp)); + RetryHandler handler = + new RetryHandler<>(ExponentialBackoffStrategy.builder().build(), + Logger.getGlobal()); + handler.execute(() -> CompletableFuture.supplyAsync( + TestRetryHandler::timestamp)); } @Test public void testFailWithNoRetries() { - RetryHandler handler = new RetryHandler<>(FaunaClient.NO_RETRY_STRATEGY); + RetryHandler handler = + new RetryHandler<>(FaunaClient.NO_RETRY_STRATEGY, + Logger.getGlobal()); FakeResponder responder = new FakeResponder(1); CompletableFuture future = handler.execute(respond(responder)); - ExecutionException exc = assertThrows(ExecutionException.class, future::get); + ExecutionException exc = + assertThrows(ExecutionException.class, future::get); assertInstanceOf(ThrottlingException.class, exc.getCause()); } @Test public void testFailWithRetries() { - RetryHandler handler = new RetryHandler<>(new ExponentialBackoffStrategy( - 3, 2f, 10, 20_000, 0.5f)); + RetryHandler handler = + new RetryHandler<>(new ExponentialBackoffStrategy( + 3, 2f, 10, 20_000, 0.5f), Logger.getGlobal()); FakeResponder responder = new FakeResponder(4); CompletableFuture future = handler.execute(respond(responder)); - ExecutionException exc = assertThrows(ExecutionException.class, future::get); + ExecutionException exc = + assertThrows(ExecutionException.class, future::get); assertInstanceOf(ThrottlingException.class, exc.getCause()); } @Test - public void testSucceedWithRetries() throws ExecutionException, InterruptedException { - RetryHandler handler = new RetryHandler<>(new ExponentialBackoffStrategy( - 3, 2f, 10, 20_000, 0.5f)); + public void testSucceedWithRetries() + throws ExecutionException, InterruptedException { + RetryHandler handler = + new RetryHandler<>(new ExponentialBackoffStrategy( + 3, 2f, 10, 20_000, 0.5f), + Logger.getGlobal()); FakeResponder responder = new FakeResponder(1); String output = handler.execute(respond(responder)).get(); assertTrue(output.length() > 10); diff --git a/src/test/java/com/fauna/client/TestStatsCollectorImpl.java b/src/test/java/com/fauna/client/TestStatsCollectorImpl.java new file mode 100644 index 00000000..9cc2b62b --- /dev/null +++ b/src/test/java/com/fauna/client/TestStatsCollectorImpl.java @@ -0,0 +1,143 @@ +package com.fauna.client; + +import com.fauna.response.QueryStats; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestStatsCollectorImpl { + + private StatsCollectorImpl statsCollector; + + @BeforeEach + public void setUp() { + statsCollector = new StatsCollectorImpl(); + } + + @Test + public void testAdd_singleQueryStats_updatesCorrectly() { + // Arrange + QueryStats stats = new QueryStats( + 10, + 20, + 5, + 100, + 1, + 500, + 300, + 50, + Arrays.asList("read", "compute") + ); + + // Act + statsCollector.add(stats); + + // Assert + QueryStatsSummary result = statsCollector.read(); + assertEquals(10, result.getComputeOps()); + assertEquals(20, result.getReadOps()); + assertEquals(5, result.getWriteOps()); + assertEquals(100, result.getQueryTimeMs()); + assertEquals(1, result.getContentionRetries()); + assertEquals(500, result.getStorageBytesRead()); + assertEquals(300, result.getStorageBytesWrite()); + assertEquals(50, result.getProcessingTimeMs()); + assertEquals(1, result.getQueryCount()); + assertEquals(1, result.getRateLimitedReadQueryCount()); + assertEquals(1, result.getRateLimitedComputeQueryCount()); + assertEquals(0, result.getRateLimitedWriteQueryCount()); + } + + @Test + public void testAdd_multipleQueryStats_accumulatesValuesCorrectly() { + // Arrange + QueryStats stats1 = new QueryStats(10, 20, 5, 100, 1, 500, 300, 30, + Collections.singletonList("read")); + QueryStats stats2 = new QueryStats(15, 25, 10, 200, 2, 600, 400, 40, + Collections.singletonList("write")); + + // Act + statsCollector.add(stats1); + statsCollector.add(stats2); + + // Assert + QueryStatsSummary result = statsCollector.read(); + assertEquals(25, result.getComputeOps()); + assertEquals(45, result.getReadOps()); + assertEquals(15, result.getWriteOps()); + assertEquals(300, result.getQueryTimeMs()); + assertEquals(3, result.getContentionRetries()); + assertEquals(1100, result.getStorageBytesRead()); + assertEquals(700, result.getStorageBytesWrite()); + assertEquals(70, result.getProcessingTimeMs()); + assertEquals(2, result.getQueryCount()); + assertEquals(1, result.getRateLimitedReadQueryCount()); + assertEquals(0, result.getRateLimitedComputeQueryCount()); + assertEquals(1, result.getRateLimitedWriteQueryCount()); + } + + @Test + public void testRead_initialStats_returnsZeroStats() { + // Act + QueryStatsSummary result = statsCollector.read(); + + // Assert + assertEquals(0, result.getComputeOps()); + assertEquals(0, result.getReadOps()); + assertEquals(0, result.getWriteOps()); + assertEquals(0, result.getQueryTimeMs()); + assertEquals(0, result.getContentionRetries()); + assertEquals(0, result.getStorageBytesRead()); + assertEquals(0, result.getStorageBytesWrite()); + assertEquals(0, result.getProcessingTimeMs()); + assertEquals(0, result.getQueryCount()); + assertEquals(0, result.getRateLimitedReadQueryCount()); + assertEquals(0, result.getRateLimitedComputeQueryCount()); + assertEquals(0, result.getRateLimitedWriteQueryCount()); + } + + @Test + public void testReadAndReset_returnsAndResetsStats() { + // Arrange + QueryStats stats = new QueryStats( + 10, 20, 5, 100, 1, 500, 300, 75, Arrays.asList("read", "write") + ); + statsCollector.add(stats); + + // Act + QueryStatsSummary beforeReset = statsCollector.readAndReset(); + QueryStatsSummary afterReset = statsCollector.read(); + + // Assert the stats before reset + assertEquals(10, beforeReset.getComputeOps()); + assertEquals(20, beforeReset.getReadOps()); + assertEquals(5, beforeReset.getWriteOps()); + assertEquals(100, beforeReset.getQueryTimeMs()); + assertEquals(1, beforeReset.getContentionRetries()); + assertEquals(500, beforeReset.getStorageBytesRead()); + assertEquals(300, beforeReset.getStorageBytesWrite()); + assertEquals(75, beforeReset.getProcessingTimeMs()); + assertEquals(1, beforeReset.getQueryCount()); + assertEquals(1, beforeReset.getRateLimitedReadQueryCount()); + assertEquals(0, beforeReset.getRateLimitedComputeQueryCount()); + assertEquals(1, beforeReset.getRateLimitedWriteQueryCount()); + + // Assert the stats after reset + assertEquals(0, afterReset.getReadOps()); + assertEquals(0, afterReset.getComputeOps()); + assertEquals(0, afterReset.getWriteOps()); + assertEquals(0, afterReset.getQueryTimeMs()); + assertEquals(0, afterReset.getContentionRetries()); + assertEquals(0, afterReset.getStorageBytesRead()); + assertEquals(0, afterReset.getStorageBytesWrite()); + assertEquals(0, afterReset.getProcessingTimeMs()); + assertEquals(0, afterReset.getQueryCount()); + assertEquals(0, afterReset.getRateLimitedReadQueryCount()); + assertEquals(0, afterReset.getRateLimitedComputeQueryCount()); + assertEquals(0, afterReset.getRateLimitedWriteQueryCount()); + } +} \ No newline at end of file diff --git a/src/test/java/com/fauna/codec/Assertions.java b/src/test/java/com/fauna/codec/Assertions.java index afcb9a1f..74843230 100644 --- a/src/test/java/com/fauna/codec/Assertions.java +++ b/src/test/java/com/fauna/codec/Assertions.java @@ -15,6 +15,7 @@ public class Assertions { * assertStringEquivalence("abc", "cba") -> good! * assertStringEquivalence("abc", "abd") -> fail! * assertStringEquivalence("abc", "ab") -> fail! + * * @param expected * @param actual */ @@ -25,9 +26,10 @@ public static void assertStringEquivalence(String expected, String actual) { for (Character c : expected.toCharArray()) { expectedChars.put(c, expectedChars.getOrDefault(c, 0) + 1); } - for (Character c: actual.toCharArray()) { + for (Character c : actual.toCharArray()) { actualChars.put(c, actualChars.getOrDefault(c, 0) + 1); } - expectedChars.forEach((c, i) -> assertEquals(i, actualChars.getOrDefault(c, -1))); + expectedChars.forEach( + (c, i) -> assertEquals(i, actualChars.getOrDefault(c, -1))); } } diff --git a/src/test/java/com/fauna/codec/CodecRegistryKeyTest.java b/src/test/java/com/fauna/codec/CodecRegistryKeyTest.java index 48eb02f2..c329ba0f 100644 --- a/src/test/java/com/fauna/codec/CodecRegistryKeyTest.java +++ b/src/test/java/com/fauna/codec/CodecRegistryKeyTest.java @@ -12,8 +12,10 @@ public class CodecRegistryKeyTest { @Test public void equals_classAndSubClassNotNullAreEqual() { - CodecRegistryKey key1 = CodecRegistryKey.from(String.class, new Type[]{Integer.class}); - CodecRegistryKey key2 = CodecRegistryKey.from(String.class, new Type[]{Integer.class}); + CodecRegistryKey key1 = + CodecRegistryKey.from(String.class, new Type[] {Integer.class}); + CodecRegistryKey key2 = + CodecRegistryKey.from(String.class, new Type[] {Integer.class}); assertEquals(key1, key2); assertEquals(key1.hashCode(), key2.hashCode()); } @@ -28,16 +30,20 @@ public void equals_classAndSubclassNullAreEqual() { @Test public void equals_differentClassesNotEqual() { - CodecRegistryKey key1 = CodecRegistryKey.from(String.class, new Type[]{Integer.class}); - CodecRegistryKey key2 = CodecRegistryKey.from(Object.class, new Type[]{Integer.class}); + CodecRegistryKey key1 = + CodecRegistryKey.from(String.class, new Type[] {Integer.class}); + CodecRegistryKey key2 = + CodecRegistryKey.from(Object.class, new Type[] {Integer.class}); assertNotEquals(key1, key2); assertNotEquals(key1.hashCode(), key2.hashCode()); } @Test public void equals_differentSubClassesNotEqual() { - CodecRegistryKey key1 = CodecRegistryKey.from(String.class, new Type[]{Integer.class}); - CodecRegistryKey key2 = CodecRegistryKey.from(String.class, new Type[]{Object.class}); + CodecRegistryKey key1 = + CodecRegistryKey.from(String.class, new Type[] {Integer.class}); + CodecRegistryKey key2 = + CodecRegistryKey.from(String.class, new Type[] {Object.class}); assertNotEquals(key1, key2); assertNotEquals(key1.hashCode(), key2.hashCode()); } diff --git a/src/test/java/com/fauna/codec/CodecRegistryTest.java b/src/test/java/com/fauna/codec/CodecRegistryTest.java index 6f1714ed..d6715a7a 100644 --- a/src/test/java/com/fauna/codec/CodecRegistryTest.java +++ b/src/test/java/com/fauna/codec/CodecRegistryTest.java @@ -6,7 +6,6 @@ import java.lang.reflect.Type; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; public class CodecRegistryTest { @@ -14,7 +13,8 @@ public class CodecRegistryTest { @Test public void put_addsCodecWithKey() { - CodecRegistryKey key = CodecRegistryKey.from(String.class, new Type[]{Integer.class}); + CodecRegistryKey key = + CodecRegistryKey.from(String.class, new Type[] {Integer.class}); Codec codec = new IntCodec(); reg.put(key, codec); Codec result = reg.get(key); diff --git a/src/test/java/com/fauna/codec/DefaultCodecProviderTest.java b/src/test/java/com/fauna/codec/DefaultCodecProviderTest.java index 11dcad43..1e17c1b8 100644 --- a/src/test/java/com/fauna/codec/DefaultCodecProviderTest.java +++ b/src/test/java/com/fauna/codec/DefaultCodecProviderTest.java @@ -3,14 +3,14 @@ import com.fauna.beans.Circular; import com.fauna.codec.codecs.ListCodec; import com.fauna.codec.codecs.MapCodec; -import com.fauna.types.Document; import org.junit.jupiter.api.Test; import java.lang.reflect.Type; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class DefaultCodecProviderTest { @@ -21,13 +21,15 @@ public class DefaultCodecProviderTest { public void get_returnsRegisteredCodec() { Codec codec = cp.get(Integer.class, null); assertNotNull(codec); - assertEquals(Integer.class ,codec.getCodecClass()); + assertEquals(Integer.class, codec.getCodecClass()); } @Test @SuppressWarnings({"rawtypes", "unchecked"}) public void get_generatesListCodec() { - Codec> codec = (Codec>) (Codec) cp.get(List.class, new Type[]{Integer.class}); + Codec> codec = + (Codec>) (Codec) cp.get(List.class, + new Type[] {Integer.class}); assertNotNull(codec); assertEquals(ListCodec.class, codec.getClass()); assertEquals(Integer.class, codec.getCodecClass()); @@ -37,7 +39,9 @@ public void get_generatesListCodec() { @SuppressWarnings({"rawtypes", "unchecked"}) public void get_generatesMapCodecForImmutableMap() { var map = Map.of(); - Codec> codec = (Codec>) (Codec) cp.get(map.getClass(), new Type[]{String.class, Integer.class}); + Codec> codec = + (Codec>) cp.get(map.getClass(), + new Type[] {String.class, Integer.class}); assertNotNull(codec); assertEquals(MapCodec.class, codec.getClass()); assertEquals(Integer.class, codec.getCodecClass()); @@ -47,7 +51,9 @@ public void get_generatesMapCodecForImmutableMap() { @SuppressWarnings({"rawtypes", "unchecked"}) public void get_generatesListCodecForImmutableList() { var list = List.of(); - Codec> codec = (Codec>) (Codec) cp.get(list.getClass(), new Type[]{Integer.class}); + Codec> codec = + (Codec>) cp.get(list.getClass(), + new Type[] {Integer.class}); assertNotNull(codec); assertEquals(ListCodec.class, codec.getClass()); assertEquals(Integer.class, codec.getCodecClass()); diff --git a/src/test/java/com/fauna/codec/Helpers.java b/src/test/java/com/fauna/codec/Helpers.java index bfd37832..7103a266 100644 --- a/src/test/java/com/fauna/codec/Helpers.java +++ b/src/test/java/com/fauna/codec/Helpers.java @@ -4,7 +4,7 @@ public class Helpers { - public static T decode(Codec codec, String val) throws IOException { + public static T decode(Codec codec, String val) { var parser = UTF8FaunaParser.fromString(val); return codec.decode(parser); } diff --git a/src/test/java/com/fauna/codec/UTF8FaunaGeneratorTest.java b/src/test/java/com/fauna/codec/UTF8FaunaGeneratorTest.java index 8d464097..3699f6cb 100644 --- a/src/test/java/com/fauna/codec/UTF8FaunaGeneratorTest.java +++ b/src/test/java/com/fauna/codec/UTF8FaunaGeneratorTest.java @@ -1,15 +1,15 @@ package com.fauna.codec; -import static org.junit.jupiter.api.Assertions.assertEquals; - import com.fauna.types.Module; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.time.Instant; import java.time.LocalDate; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; public class UTF8FaunaGeneratorTest { @@ -159,7 +159,8 @@ public void writeRef() throws IOException { writer.writeModule("coll", new Module("Authors")); writer.writeEndRef(); - assertWriter("{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Authors\"}}}"); + assertWriter( + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Authors\"}}}"); } public void writeTimeWithSixDecimalPrecision() throws IOException { diff --git a/src/test/java/com/fauna/codec/UTF8FaunaParserTest.java b/src/test/java/com/fauna/codec/UTF8FaunaParserTest.java index bf9291a5..2e8a598e 100644 --- a/src/test/java/com/fauna/codec/UTF8FaunaParserTest.java +++ b/src/test/java/com/fauna/codec/UTF8FaunaParserTest.java @@ -1,11 +1,9 @@ package com.fauna.codec; -import static org.junit.Assert.*; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - import com.fauna.exception.CodecException; import com.fauna.types.Module; +import org.junit.jupiter.api.Test; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -15,14 +13,20 @@ import java.util.List; import java.util.Map; -import org.junit.jupiter.api.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class UTF8FaunaParserTest { @Test - public void testGetValueAsString() { - InputStream inputStream = new ByteArrayInputStream("\"hello\"".getBytes()); + public void testGetValueAsString() { + InputStream inputStream = + new ByteArrayInputStream("\"hello\"".getBytes()); UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(inputStream); List> expectedTokens = List.of( @@ -61,8 +65,10 @@ public void testGetValueAsInt() throws IOException { @Test public void testGetValueAsIntFail() throws IOException { String invalidJson = "{\"@int\": \"abc\"}"; - InputStream invalidInputStream = new ByteArrayInputStream(invalidJson.getBytes()); - UTF8FaunaParser invalidReader = UTF8FaunaParser.fromInputStream(invalidInputStream); + InputStream invalidInputStream = + new ByteArrayInputStream(invalidJson.getBytes()); + UTF8FaunaParser invalidReader = + UTF8FaunaParser.fromInputStream(invalidInputStream); List> expectedTokens = List.of( Map.entry(FaunaTokenType.INT, "abc") @@ -71,7 +77,8 @@ public void testGetValueAsIntFail() throws IOException { Exception ex = assertThrows(CodecException.class, () -> assertReader(invalidReader, expectedTokens)); - assertEquals("Error getting the current token as Integer", ex.getMessage()); + assertEquals("Error getting the current token as Integer", + ex.getMessage()); } @Test @@ -82,7 +89,8 @@ public void testUnexpectedEndDuringAdvance() throws IOException { Exception ex = assertThrows(CodecException.class, () -> UTF8FaunaParser.fromInputStream(inputStream)); - assertEquals("Failed to advance underlying JSON reader.", ex.getMessage()); + assertEquals("Failed to advance underlying JSON reader.", + ex.getMessage()); } @Test @@ -125,8 +133,10 @@ public void testGetValueAsLocalDate() throws IOException { @Test public void testGetValueAsLocalDateFail() throws IOException { String invalidJson = "{\"@date\": \"abc\"}"; - InputStream invalidInputStream = new ByteArrayInputStream(invalidJson.getBytes()); - UTF8FaunaParser invalidReader = UTF8FaunaParser.fromInputStream(invalidInputStream); + InputStream invalidInputStream = + new ByteArrayInputStream(invalidJson.getBytes()); + UTF8FaunaParser invalidReader = + UTF8FaunaParser.fromInputStream(invalidInputStream); List> expectedTokens = List.of( Map.entry(FaunaTokenType.DATE, "abc") @@ -135,7 +145,8 @@ public void testGetValueAsLocalDateFail() throws IOException { Exception ex = assertThrows(CodecException.class, () -> assertReader(invalidReader, expectedTokens)); - assertEquals("Error getting the current token as LocalDate", ex.getMessage()); + assertEquals("Error getting the current token as LocalDate", + ex.getMessage()); } @Test @@ -170,8 +181,10 @@ public void testGetValueAsStreamToken() throws IOException { @Test public void testGetValueAsTimeFail() throws IOException { String invalidJson = "{\"@time\": \"abc\"}"; - InputStream invalidInputStream = new ByteArrayInputStream(invalidJson.getBytes()); - UTF8FaunaParser invalidReader = UTF8FaunaParser.fromInputStream(invalidInputStream); + InputStream invalidInputStream = + new ByteArrayInputStream(invalidJson.getBytes()); + UTF8FaunaParser invalidReader = + UTF8FaunaParser.fromInputStream(invalidInputStream); List> expectedTokens = List.of( Map.entry(FaunaTokenType.TIME, "abc") @@ -180,7 +193,8 @@ public void testGetValueAsTimeFail() throws IOException { Exception ex = assertThrows(CodecException.class, () -> assertReader(invalidReader, expectedTokens)); - assertEquals("Error getting the current token as LocalDateTime", ex.getMessage()); + assertEquals("Error getting the current token as LocalDateTime", + ex.getMessage()); } @Test @@ -215,8 +229,10 @@ public void testGetValueAsDouble() throws IOException { @Test public void testGetValueAsDoubleFail() throws IOException { String invalidJson = "{\"@double\": \"abc\"}"; - InputStream invalidInputStream = new ByteArrayInputStream(invalidJson.getBytes()); - UTF8FaunaParser invalidReader = UTF8FaunaParser.fromInputStream(invalidInputStream); + InputStream invalidInputStream = + new ByteArrayInputStream(invalidJson.getBytes()); + UTF8FaunaParser invalidReader = + UTF8FaunaParser.fromInputStream(invalidInputStream); List> expectedTokens = List.of( Map.entry(FaunaTokenType.DOUBLE, "abc") @@ -225,7 +241,8 @@ public void testGetValueAsDoubleFail() throws IOException { Exception ex = assertThrows(CodecException.class, () -> assertReader(invalidReader, expectedTokens)); - assertEquals("Error getting the current token as Double", ex.getMessage()); + assertEquals("Error getting the current token as Double", + ex.getMessage()); } @Test @@ -244,8 +261,10 @@ public void testGetValueAsLong() throws IOException { @Test public void testGetValueAsLongFail() throws IOException { String invalidJson = "{\"@long\": \"abc\"}"; - InputStream invalidInputStream = new ByteArrayInputStream(invalidJson.getBytes()); - UTF8FaunaParser invalidReader = UTF8FaunaParser.fromInputStream(invalidInputStream); + InputStream invalidInputStream = + new ByteArrayInputStream(invalidJson.getBytes()); + UTF8FaunaParser invalidReader = + UTF8FaunaParser.fromInputStream(invalidInputStream); List> expectedTokens = List.of( Map.entry(FaunaTokenType.LONG, "abc") @@ -254,7 +273,8 @@ public void testGetValueAsLongFail() throws IOException { Exception ex = assertThrows(CodecException.class, () -> assertReader(invalidReader, expectedTokens)); - assertEquals("Error getting the current token as Long", ex.getMessage()); + assertEquals("Error getting the current token as Long", + ex.getMessage()); } @Test @@ -278,7 +298,8 @@ public void readArrayWithEmptyObject() throws IOException { List> expectedTokens = List.of( new AbstractMap.SimpleEntry<>(FaunaTokenType.START_ARRAY, null), - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), new AbstractMap.SimpleEntry<>(FaunaTokenType.END_OBJECT, null), new AbstractMap.SimpleEntry<>(FaunaTokenType.END_ARRAY, null) ); @@ -296,10 +317,12 @@ public void testReadEscapedObject() throws IOException { " \"anEscapedObject\": { \"@object\": { \"@long\": \"notalong\" } }\n" + " }\n" + "}"; - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(s.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(s.getBytes())); List> expectedTokens = List.of( - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), Map.entry(FaunaTokenType.FIELD_NAME, "@int"), Map.entry(FaunaTokenType.STRING, "notanint"), Map.entry(FaunaTokenType.FIELD_NAME, "anInt"), @@ -307,7 +330,8 @@ public void testReadEscapedObject() throws IOException { Map.entry(FaunaTokenType.FIELD_NAME, "@object"), Map.entry(FaunaTokenType.STRING, "notanobject"), Map.entry(FaunaTokenType.FIELD_NAME, "anEscapedObject"), - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), Map.entry(FaunaTokenType.FIELD_NAME, "@long"), Map.entry(FaunaTokenType.STRING, "notalong"), new AbstractMap.SimpleEntry<>(FaunaTokenType.END_OBJECT, null), @@ -327,18 +351,22 @@ public void testReadDocumentTokens() throws IOException { " \"data\": { \"foo\": \"bar\" }\n" + " }\n" + "}"; - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(s.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(s.getBytes())); List> expectedTokens = List.of( - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_DOCUMENT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_DOCUMENT, + null), Map.entry(FaunaTokenType.FIELD_NAME, "id"), Map.entry(FaunaTokenType.STRING, "123"), Map.entry(FaunaTokenType.FIELD_NAME, "coll"), Map.entry(FaunaTokenType.MODULE, new Module("Coll")), Map.entry(FaunaTokenType.FIELD_NAME, "ts"), - Map.entry(FaunaTokenType.TIME, Instant.parse("2023-12-03T16:07:23.111012Z")), + Map.entry(FaunaTokenType.TIME, + Instant.parse("2023-12-03T16:07:23.111012Z")), Map.entry(FaunaTokenType.FIELD_NAME, "data"), - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), Map.entry(FaunaTokenType.FIELD_NAME, "foo"), Map.entry(FaunaTokenType.STRING, "bar"), new AbstractMap.SimpleEntry<>(FaunaTokenType.END_OBJECT, null), @@ -356,7 +384,8 @@ public void testReadSet() throws IOException { " \"after\": \"afterme\"\n" + " }\n" + "}"; - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(s.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(s.getBytes())); List> expectedTokens = List.of( new AbstractMap.SimpleEntry<>(FaunaTokenType.START_PAGE, null), @@ -372,11 +401,35 @@ public void testReadSet() throws IOException { assertReader(reader, expectedTokens); } + + @Test + public void testReadUnmaterializedSet() { + String s = "{\"products\":{\"@set\":\"sometoken\"},\"name\":\"foo\"}"; + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(s.getBytes())); + + List> expectedTokens = List.of( + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), + Map.entry(FaunaTokenType.FIELD_NAME, "products"), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_PAGE, null), + Map.entry(FaunaTokenType.STRING, "sometoken"), + new AbstractMap.SimpleEntry<>(FaunaTokenType.END_PAGE, null), + Map.entry(FaunaTokenType.FIELD_NAME, "name"), + Map.entry(FaunaTokenType.STRING, "foo"), + new AbstractMap.SimpleEntry<>(FaunaTokenType.END_OBJECT, null) + ); + + assertReader(reader, expectedTokens); + } + @Test public void testReadRef() throws IOException { - String s = "{\"@ref\": {\"id\": \"123\", \"coll\": {\"@mod\": \"Col\"}}}"; + String s = + "{\"@ref\": {\"id\": \"123\", \"coll\": {\"@mod\": \"Col\"}}}"; - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(s.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(s.getBytes())); List> expectedTokens = List.of( new AbstractMap.SimpleEntry<>(FaunaTokenType.START_REF, null), @@ -407,16 +460,19 @@ public void testReadObjectTokens() throws IOException { " \"false\": false,\n" + " \"null\": null\n" + "}"; - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(s.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(s.getBytes())); List> expectedTokens = List.of( - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), Map.entry(FaunaTokenType.FIELD_NAME, "aString"), Map.entry(FaunaTokenType.STRING, "foo"), Map.entry(FaunaTokenType.FIELD_NAME, "anObject"), - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), Map.entry(FaunaTokenType.FIELD_NAME, "baz"), Map.entry(FaunaTokenType.STRING, "luhrmann"), new AbstractMap.SimpleEntry<>(FaunaTokenType.END_OBJECT, null), @@ -437,10 +493,12 @@ public void testReadObjectTokens() throws IOException { Map.entry(FaunaTokenType.DATE, LocalDate.of(2023, 12, 3)), Map.entry(FaunaTokenType.FIELD_NAME, "aTime"), - Map.entry(FaunaTokenType.TIME, Instant.parse("2023-12-03T14:52:10.001001Z")), + Map.entry(FaunaTokenType.TIME, + Instant.parse("2023-12-03T14:52:10.001001Z")), Map.entry(FaunaTokenType.FIELD_NAME, "anEscapedObject"), - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), Map.entry(FaunaTokenType.FIELD_NAME, "@int"), Map.entry(FaunaTokenType.STRING, "escaped"), new AbstractMap.SimpleEntry<>(FaunaTokenType.END_OBJECT, null), @@ -481,14 +539,16 @@ public void testReadArray() throws IOException { " false,\n" + " null\n" + "]"; - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(s.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(s.getBytes())); List> expectedTokens = List.of( new AbstractMap.SimpleEntry<>(FaunaTokenType.START_ARRAY, null), Map.entry(FaunaTokenType.STRING, "foo"), - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), Map.entry(FaunaTokenType.FIELD_NAME, "baz"), Map.entry(FaunaTokenType.STRING, "luhrmann"), new AbstractMap.SimpleEntry<>(FaunaTokenType.END_OBJECT, null), @@ -503,9 +563,11 @@ public void testReadArray() throws IOException { Map.entry(FaunaTokenType.DATE, LocalDate.of(2023, 12, 3)), - Map.entry(FaunaTokenType.TIME, Instant.parse("2023-12-03T14:52:10.001001Z")), + Map.entry(FaunaTokenType.TIME, + Instant.parse("2023-12-03T14:52:10.001001Z")), - new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, null), + new AbstractMap.SimpleEntry<>(FaunaTokenType.START_OBJECT, + null), Map.entry(FaunaTokenType.FIELD_NAME, "@int"), Map.entry(FaunaTokenType.STRING, "escaped"), new AbstractMap.SimpleEntry<>(FaunaTokenType.END_OBJECT, null), @@ -529,7 +591,8 @@ public void testReadArray() throws IOException { public void throwsOnMalformedJson() { String s = "{"; assertThrows(CodecException.class, () -> { - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(s.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(s.getBytes())); reader.read(); reader.read(); }, "Failed to advance underlying JSON reader."); @@ -547,7 +610,8 @@ public void skipValues() throws IOException { ); for (String test : tests) { - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(test.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(test.getBytes())); reader.skip(); assertFalse(reader.read()); } @@ -555,8 +619,10 @@ public void skipValues() throws IOException { @Test public void skipNestedEscapedObject() throws IOException { - String test = "{\"@object\": {\"inner\": {\"@object\": {\"foo\": \"bar\"}}, \"k2\": {}}}"; - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(test.getBytes())); + String test = + "{\"@object\": {\"inner\": {\"@object\": {\"foo\": \"bar\"}}, \"k2\": {}}}"; + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(test.getBytes())); assertEquals(FaunaTokenType.START_OBJECT, reader.getCurrentTokenType()); reader.read(); // inner assertEquals(FaunaTokenType.FIELD_NAME, reader.getCurrentTokenType()); @@ -572,7 +638,8 @@ public void skipNestedEscapedObject() throws IOException { @Test public void skipNestedObject() throws IOException { String test = "{\"k\":{\"inner\":{}},\"k2\":{}}"; - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(test.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(test.getBytes())); assertEquals(FaunaTokenType.START_OBJECT, reader.getCurrentTokenType()); reader.read(); // k assertEquals(FaunaTokenType.FIELD_NAME, reader.getCurrentTokenType()); @@ -588,7 +655,8 @@ public void skipNestedObject() throws IOException { @Test public void skipNestedArrays() throws IOException { String test = "{\"k\":[\"1\",\"2\"],\"k2\":{}}"; - UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream(new ByteArrayInputStream(test.getBytes())); + UTF8FaunaParser reader = UTF8FaunaParser.fromInputStream( + new ByteArrayInputStream(test.getBytes())); assertEquals(FaunaTokenType.START_OBJECT, reader.getCurrentTokenType()); reader.read(); // k assertEquals(FaunaTokenType.FIELD_NAME, reader.getCurrentTokenType()); @@ -614,7 +682,7 @@ private static void assertReader(UTF8FaunaParser reader, assertEquals(entry.getValue(), reader.getValueAsString()); break; case BYTES: - var ar1 = (byte[])entry.getValue(); + var ar1 = (byte[]) entry.getValue(); var ar2 = reader.getValueAsByteArray(); assertArrayEquals(ar1, ar2); break; @@ -626,7 +694,8 @@ private static void assertReader(UTF8FaunaParser reader, assertEquals(entry.getValue(), reader.getValueAsBoolean()); break; case DATE: - assertEquals(entry.getValue(), reader.getValueAsLocalDate()); + assertEquals(entry.getValue(), + reader.getValueAsLocalDate()); break; case TIME: assertEquals(entry.getValue(), reader.getValueAsTime()); @@ -641,7 +710,8 @@ private static void assertReader(UTF8FaunaParser reader, assertEquals(entry.getValue(), reader.getValueAsModule()); break; case STREAM: - assertEquals(entry.getValue(), reader.getTaggedValueAsString()); + assertEquals(entry.getValue(), + reader.getTaggedValueAsString()); break; default: assertNull(entry.getValue()); diff --git a/src/test/java/com/fauna/codec/codecs/BaseDocumentCodecTest.java b/src/test/java/com/fauna/codec/codecs/BaseDocumentCodecTest.java index e620a8b7..eed9c2e0 100644 --- a/src/test/java/com/fauna/codec/codecs/BaseDocumentCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/BaseDocumentCodecTest.java @@ -22,50 +22,70 @@ public class BaseDocumentCodecTest extends TestBase { - public static final Codec BASE_DOCUMENT_CODEC = DefaultCodecProvider.SINGLETON.get(BaseDocument.class); + public static final Codec BASE_DOCUMENT_CODEC = + DefaultCodecProvider.SINGLETON.get(BaseDocument.class); // Docs - public static final String DOCUMENT_WIRE = "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; + public static final String DOCUMENT_WIRE = + "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; public static final Document DOCUMENT = new Document( "123", new Module("Foo"), Instant.parse("2023-12-15T01:01:01.0010010Z"), - Map.of("first_name","foo", "last_name", "bar","age",42) + Map.of("first_name", "foo", "last_name", "bar", "age", 42) ); - public static final String NULL_DOC_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; - public static final NullDocumentException NULL_DOC_EXCEPTION = new NullDocumentException("123", new Module("Foo"), "not found"); - - + public static final String NULL_DOC_WIRE = + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; + public static final NullDocumentException NULL_DOC_EXCEPTION = + new NullDocumentException("123", new Module("Foo"), "not found"); + + // Named docs - public static final String NAMED_DOCUMENT_WIRE = "{\"@doc\":{\"name\":\"Boogles\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; + public static final String NAMED_DOCUMENT_WIRE = + "{\"@doc\":{\"name\":\"Boogles\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; public static final NamedDocument NAMED_DOCUMENT = new NamedDocument( "Boogles", new Module("Foo"), Instant.parse("2023-12-15T01:01:01.0010010Z"), - Map.of("first_name","foo", "last_name", "bar") + Map.of("first_name", "foo", "last_name", "bar") ); - + // Refs - public static final String DOCUMENT_REF_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final String DOCUMENT_REF_WIRE = + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; + + public static final String NAMED_DOCUMENT_REF_WIRE = + "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final ClassWithAttributes PERSON_WITH_ATTRIBUTES = + new ClassWithAttributes("foo", "bar", 42); - public static final String NAMED_DOCUMENT_REF_WIRE = "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; - public static final ClassWithAttributes PERSON_WITH_ATTRIBUTES = new ClassWithAttributes("foo","bar",42); - public static Stream testCases() { return Stream.of( - Arguments.of(TestType.Decode, BASE_DOCUMENT_CODEC, DOCUMENT_WIRE, DOCUMENT, null), - Arguments.of(TestType.Decode, BASE_DOCUMENT_CODEC, NAMED_DOCUMENT_WIRE, NAMED_DOCUMENT, null), - Arguments.of(TestType.Decode, BASE_DOCUMENT_CODEC, NULL_DOC_WIRE, null, NULL_DOC_EXCEPTION), - Arguments.of(TestType.Decode, BASE_DOCUMENT_CODEC, DOCUMENT_REF_WIRE, null, new CodecException("Unexpected type `class com.fauna.types.DocumentRef` decoding with `BaseDocumentCodec`")), - Arguments.of(TestType.Encode, BASE_DOCUMENT_CODEC, DOCUMENT_REF_WIRE, DOCUMENT, null), - Arguments.of(TestType.Encode, BASE_DOCUMENT_CODEC, NAMED_DOCUMENT_REF_WIRE, NAMED_DOCUMENT, null) + Arguments.of(TestType.Decode, BASE_DOCUMENT_CODEC, + DOCUMENT_WIRE, DOCUMENT, null), + Arguments.of(TestType.Decode, BASE_DOCUMENT_CODEC, + NAMED_DOCUMENT_WIRE, NAMED_DOCUMENT, null), + Arguments.of(TestType.Decode, BASE_DOCUMENT_CODEC, + NULL_DOC_WIRE, null, NULL_DOC_EXCEPTION), + Arguments.of(TestType.Decode, BASE_DOCUMENT_CODEC, + DOCUMENT_REF_WIRE, null, new CodecException( + "Unexpected type `class com.fauna.types.DocumentRef` decoding with `BaseDocumentCodec`")), + Arguments.of(TestType.Encode, BASE_DOCUMENT_CODEC, + DOCUMENT_REF_WIRE, DOCUMENT, null), + Arguments.of(TestType.Encode, BASE_DOCUMENT_CODEC, + NAMED_DOCUMENT_REF_WIRE, NAMED_DOCUMENT, null) ); } @ParameterizedTest(name = "BaseDocumentCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void baseDoc_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void baseDoc_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -75,8 +95,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "BaseDocCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void baseDoc_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `BaseDocumentCodec`. Supported types for codec are [Document, Null, Ref].", type); - runCase(TestType.Decode, BASE_DOCUMENT_CODEC, wire, null, new CodecException(exMsg)); + public void baseDoc_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `BaseDocumentCodec`. Supported types for codec are [Document, Null, Ref].", + type); + runCase(TestType.Decode, BASE_DOCUMENT_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/BaseRefCodecTest.java b/src/test/java/com/fauna/codec/codecs/BaseRefCodecTest.java index 7ba3d049..f5d9b79a 100644 --- a/src/test/java/com/fauna/codec/codecs/BaseRefCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/BaseRefCodecTest.java @@ -19,32 +19,47 @@ public class BaseRefCodecTest extends TestBase { - public static final Codec BASE_REF_CODEC = DefaultCodecProvider.SINGLETON.get(BaseRef.class); - + public static final Codec BASE_REF_CODEC = + DefaultCodecProvider.SINGLETON.get(BaseRef.class); + // Doc ref - public static final String DOCUMENT_REF_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; - public static final DocumentRef DOCUMENT_REF = new DocumentRef("123", new Module("Foo")); + public static final String DOCUMENT_REF_WIRE = + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final DocumentRef DOCUMENT_REF = + new DocumentRef("123", new Module("Foo")); // Named ref - public static final String NAMED_DOCUMENT_REF_WIRE = "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; - public static final NamedDocumentRef NAMED_DOCUMENT_REF = new NamedDocumentRef("Boogles", new Module("Foo")); + public static final String NAMED_DOCUMENT_REF_WIRE = + "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final NamedDocumentRef NAMED_DOCUMENT_REF = + new NamedDocumentRef("Boogles", new Module("Foo")); // Null doc - public static final String NULL_DOC_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; - public static final NullDocumentException NULL_DOC_EXCEPTION = new NullDocumentException("123", new Module("Foo"), "not found"); + public static final String NULL_DOC_WIRE = + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; + public static final NullDocumentException NULL_DOC_EXCEPTION = + new NullDocumentException("123", new Module("Foo"), "not found"); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, BASE_REF_CODEC, DOCUMENT_REF_WIRE, DOCUMENT_REF, null), - Arguments.of(TestType.RoundTrip, BASE_REF_CODEC, NAMED_DOCUMENT_REF_WIRE, NAMED_DOCUMENT_REF, null), - Arguments.of(TestType.Decode, BASE_REF_CODEC, NULL_DOC_WIRE, null, NULL_DOC_EXCEPTION) + Arguments.of(TestType.RoundTrip, BASE_REF_CODEC, + DOCUMENT_REF_WIRE, DOCUMENT_REF, null), + Arguments.of(TestType.RoundTrip, BASE_REF_CODEC, + NAMED_DOCUMENT_REF_WIRE, NAMED_DOCUMENT_REF, null), + Arguments.of(TestType.Decode, BASE_REF_CODEC, NULL_DOC_WIRE, + null, NULL_DOC_EXCEPTION) ); } @ParameterizedTest(name = "BaseRefCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void baseRef_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void baseRef_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -54,8 +69,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "BaseRefCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void baseRef_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `BaseRefCodec`. Supported types for codec are [Null, Ref].", type); - runCase(TestType.Decode, BASE_REF_CODEC, wire, null, new CodecException(exMsg)); + public void baseRef_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `BaseRefCodec`. Supported types for codec are [Null, Ref].", + type); + runCase(TestType.Decode, BASE_REF_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/BoolCodecTest.java b/src/test/java/com/fauna/codec/codecs/BoolCodecTest.java index 11ef220f..9da04a82 100644 --- a/src/test/java/com/fauna/codec/codecs/BoolCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/BoolCodecTest.java @@ -14,11 +14,15 @@ public class BoolCodecTest extends TestBase { - public static final Codec BOOL_CODEC = DefaultCodecProvider.SINGLETON.get(Boolean.class); + public static final Codec BOOL_CODEC = + DefaultCodecProvider.SINGLETON.get(Boolean.class); + public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, BOOL_CODEC, "true", true, null), - Arguments.of(TestType.RoundTrip, BOOL_CODEC, "false", false, null), + Arguments.of(TestType.RoundTrip, BOOL_CODEC, "true", true, + null), + Arguments.of(TestType.RoundTrip, BOOL_CODEC, "false", false, + null), Arguments.of(TestType.RoundTrip, BOOL_CODEC, "null", null, null) ); @@ -26,7 +30,12 @@ public static Stream testCases() { @ParameterizedTest(name = "BoolCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void bool_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void bool_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -36,8 +45,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "BoolCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void bool_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `BoolCodec`. Supported types for codec are [Boolean, Null].", type); - runCase(TestType.Decode, BOOL_CODEC, wire, null, new CodecException(exMsg)); + public void bool_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `BoolCodec`. Supported types for codec are [Boolean, Null].", + type); + runCase(TestType.Decode, BOOL_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/ByteArrayCodecTest.java b/src/test/java/com/fauna/codec/codecs/ByteArrayCodecTest.java index 8a45b340..9b2f772a 100644 --- a/src/test/java/com/fauna/codec/codecs/ByteArrayCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/ByteArrayCodecTest.java @@ -13,18 +13,24 @@ import java.util.stream.Stream; public class ByteArrayCodecTest extends TestBase { - public static final Codec BYTE_ARRAY_CODEC = DefaultCodecProvider.SINGLETON.get(byte[].class); + public static final Codec BYTE_ARRAY_CODEC = + DefaultCodecProvider.SINGLETON.get(byte[].class); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, BYTE_ARRAY_CODEC, "{\"@bytes\":\"RmF1bmE=\"}", new byte[]{70, 97, 117, 110, 97}, null), - Arguments.of(TestType.RoundTrip, BYTE_ARRAY_CODEC, "null", null, null) - ); + Arguments.of(TestType.RoundTrip, BYTE_ARRAY_CODEC, + "{\"@bytes\":\"RmF1bmE=\"}", + new byte[] {70, 97, 117, 110, 97}, null), + Arguments.of(TestType.RoundTrip, BYTE_ARRAY_CODEC, "null", null, + null) + ); } @ParameterizedTest(name = "ByteArrayCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void byteArray_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void byteArray_runTestCases( + TestType testType, Codec codec, String wire, Object obj, + E exception) throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -34,8 +40,13 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "ByteArrayCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void byteArray_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `ByteArrayCodec`. Supported types for codec are [Bytes, Null].", type); - runCase(TestType.Decode, BYTE_ARRAY_CODEC, wire, null, new CodecException(exMsg)); + public void byteArray_runUnsupportedTypeTestCases(String wire, + FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `ByteArrayCodec`. Supported types for codec are [Bytes, Null].", + type); + runCase(TestType.Decode, BYTE_ARRAY_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/ByteCodecTest.java b/src/test/java/com/fauna/codec/codecs/ByteCodecTest.java index 133be5a2..e13c025c 100644 --- a/src/test/java/com/fauna/codec/codecs/ByteCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/ByteCodecTest.java @@ -3,7 +3,6 @@ import com.fauna.codec.Codec; import com.fauna.codec.DefaultCodecProvider; import com.fauna.codec.FaunaType; -import com.fauna.codec.Helpers; import com.fauna.exception.CodecException; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -11,26 +10,33 @@ import java.io.IOException; import java.text.MessageFormat; -import java.util.Arrays; import java.util.stream.Stream; import static com.fauna.codec.codecs.Fixtures.INT_WIRE; public class ByteCodecTest extends TestBase { - public static final Codec BYTE_CODEC = DefaultCodecProvider.SINGLETON.get(Byte.class); + public static final Codec BYTE_CODEC = + DefaultCodecProvider.SINGLETON.get(Byte.class); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, BYTE_CODEC, INT_WIRE((int) Byte.MAX_VALUE), Byte.MAX_VALUE, null), - Arguments.of(TestType.RoundTrip, BYTE_CODEC, INT_WIRE((int) Byte.MIN_VALUE), Byte.MIN_VALUE, null), + Arguments.of(TestType.RoundTrip, BYTE_CODEC, + INT_WIRE((int) Byte.MAX_VALUE), Byte.MAX_VALUE, null), + Arguments.of(TestType.RoundTrip, BYTE_CODEC, + INT_WIRE((int) Byte.MIN_VALUE), Byte.MIN_VALUE, null), Arguments.of(TestType.RoundTrip, BYTE_CODEC, "null", null, null) ); } @ParameterizedTest(name = "ByteCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void byte_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void byte_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -40,8 +46,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "ByteCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void byte_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `ByteCodec`. Supported types for codec are [Int, Null].", type); - runCase(TestType.Decode, BYTE_CODEC, wire, null, new CodecException(exMsg)); + public void byte_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `ByteCodec`. Supported types for codec are [Int, Null].", + type); + runCase(TestType.Decode, BYTE_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/CharCodecTest.java b/src/test/java/com/fauna/codec/codecs/CharCodecTest.java index ef033939..ed85b71e 100644 --- a/src/test/java/com/fauna/codec/codecs/CharCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/CharCodecTest.java @@ -16,17 +16,25 @@ public class CharCodecTest extends TestBase { - public static final Codec CHAR_CODEC = DefaultCodecProvider.SINGLETON.get(Character.class); + public static final Codec CHAR_CODEC = + DefaultCodecProvider.SINGLETON.get(Character.class); + public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, CHAR_CODEC, INT_WIRE(84), 'T', null), + Arguments.of(TestType.RoundTrip, CHAR_CODEC, INT_WIRE(84), 'T', + null), Arguments.of(TestType.RoundTrip, CHAR_CODEC, "null", null, null) ); } @ParameterizedTest(name = "CharCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void char_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void char_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -37,8 +45,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "CharCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void char_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `CharCodec`. Supported types for codec are [Int, Null].", type); - runCase(TestType.Decode, CHAR_CODEC, wire, null, new CodecException(exMsg)); + public void char_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `CharCodec`. Supported types for codec are [Int, Null].", + type); + runCase(TestType.Decode, CHAR_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java b/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java index b774baf0..0e0ee16e 100644 --- a/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java @@ -1,12 +1,12 @@ package com.fauna.codec.codecs; +import com.fauna.beans.ClassWithAttributes; import com.fauna.beans.ClassWithClientGeneratedIdCollTsAnnotations; -import com.fauna.beans.ClassWithInheritanceL2; import com.fauna.beans.ClassWithFaunaIgnore; import com.fauna.beans.ClassWithIdCollTsAnnotations; +import com.fauna.beans.ClassWithInheritanceL2; import com.fauna.beans.ClassWithParameterizedFields; import com.fauna.beans.ClassWithRefTagCollision; -import com.fauna.beans.ClassWithAttributes; import com.fauna.codec.Codec; import com.fauna.codec.DefaultCodecProvider; import com.fauna.codec.FaunaType; @@ -32,67 +32,141 @@ public class ClassCodecTest extends TestBase { // Class with FaunaField attributes - public static final Codec CLASS_WITH_ATTRIBUTES_CODEC = DefaultCodecProvider.SINGLETON.get(ClassWithAttributes.class); - public static final String DOCUMENT_WIRE = "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; - public static final ClassWithAttributes CLASS_WITH_ATTRIBUTES = new ClassWithAttributes("foo","bar",42); - public static final String NULL_DOC_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; - public static final NullDocumentException NULL_DOC_EXCEPTION = new NullDocumentException("123", new Module("Foo"), "not found"); + public static final Codec CLASS_WITH_ATTRIBUTES_CODEC = + DefaultCodecProvider.SINGLETON.get(ClassWithAttributes.class); + public static final String DOCUMENT_WIRE = + "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; + public static final ClassWithAttributes CLASS_WITH_ATTRIBUTES = + new ClassWithAttributes("foo", "bar", 42); + public static final String NULL_DOC_WIRE = + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; + public static final NullDocumentException NULL_DOC_EXCEPTION = + new NullDocumentException("123", new Module("Foo"), "not found"); // Class with tag collision - public static final Codec CLASS_WITH_REF_TAG_COLLISION_CODEC = DefaultCodecProvider.SINGLETON.get(ClassWithRefTagCollision.class); - public static final String REF_TAG_COLLISION_WIRE = ESCAPED_OBJECT_WIRE_WITH("@ref"); - public static final ClassWithRefTagCollision CLASS_WITH_REF_TAG_COLLISION = new ClassWithRefTagCollision("not"); + public static final Codec + CLASS_WITH_REF_TAG_COLLISION_CODEC = + DefaultCodecProvider.SINGLETON.get(ClassWithRefTagCollision.class); + public static final String REF_TAG_COLLISION_WIRE = + ESCAPED_OBJECT_WIRE_WITH("@ref"); + public static final ClassWithRefTagCollision CLASS_WITH_REF_TAG_COLLISION = + new ClassWithRefTagCollision("not"); // Class with parameterized Fields - public static final Codec CLASS_WITH_PARAMETERIZED_FIELDS_CODEC = DefaultCodecProvider.SINGLETON.get(ClassWithParameterizedFields.class); - public static final String CLASS_WITH_PARAMETERIZED_FIELDS_WIRE = "{\"first_name\":\"foo\",\"a_list\":[{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}],\"a_map\":{\"key1\":{\"@int\":\"42\"}},\"an_optional\":\"Fauna\"}"; - public static final ClassWithParameterizedFields CLASS_WITH_PARAMETERIZED_FIELDS = new ClassWithParameterizedFields("foo", List.of(CLASS_WITH_ATTRIBUTES), Map.of("key1", 42), Optional.of("Fauna")); + public static final Codec + CLASS_WITH_PARAMETERIZED_FIELDS_CODEC = + DefaultCodecProvider.SINGLETON.get( + ClassWithParameterizedFields.class); + public static final String CLASS_WITH_PARAMETERIZED_FIELDS_WIRE = + "{\"first_name\":\"foo\",\"a_list\":[{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}],\"a_map\":{\"key1\":{\"@int\":\"42\"}},\"an_optional\":\"Fauna\"}"; + public static final ClassWithParameterizedFields + CLASS_WITH_PARAMETERIZED_FIELDS = + new ClassWithParameterizedFields("foo", + List.of(CLASS_WITH_ATTRIBUTES), Map.of("key1", 42), + Optional.of("Fauna")); // Class with FaunaIgnore attributes - public static final Codec CLASS_WITH_FAUNA_IGNORE_CODEC = DefaultCodecProvider.SINGLETON.get(ClassWithFaunaIgnore.class); - public static String CLASS_WITH_FAUNA_IGNORE_WITH_AGE_WIRE = "{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}"; - public static final ClassWithFaunaIgnore CLASS_WITH_FAUNA_IGNORE_WITH_AGE = new ClassWithFaunaIgnore("foo", "bar", 42); - public static String CLASS_WITH_FAUNA_IGNORE_WIRE = "{\"first_name\":\"foo\",\"last_name\":\"bar\"}"; - public static final ClassWithFaunaIgnore CLASS_WITH_FAUNA_IGNORE = new ClassWithFaunaIgnore("foo", "bar", null); + public static final Codec + CLASS_WITH_FAUNA_IGNORE_CODEC = + DefaultCodecProvider.SINGLETON.get(ClassWithFaunaIgnore.class); + public static final String CLASS_WITH_FAUNA_IGNORE_WITH_AGE_WIRE = + "{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}"; + public static final ClassWithFaunaIgnore CLASS_WITH_FAUNA_IGNORE_WITH_AGE = + new ClassWithFaunaIgnore("foo", "bar", 42); + public static final String CLASS_WITH_FAUNA_IGNORE_WIRE = + "{\"first_name\":\"foo\",\"last_name\":\"bar\"}"; + public static final ClassWithFaunaIgnore CLASS_WITH_FAUNA_IGNORE = + new ClassWithFaunaIgnore("foo", "bar", null); // Class with Id, Coll, Ts annotations - private static final Object CLASS_WITH_ID_COLL_TS_ANNOTATIONS_CODEC = DefaultCodecProvider.SINGLETON.get(ClassWithIdCollTsAnnotations.class); - private static final String CLASS_WITH_ID_COLL_TS_ANNOTATIONS_WIRE = "{\"firstName\":\"foo\",\"lastName\":\"bar\"}"; - private static final ClassWithIdCollTsAnnotations CLASS_WITH_ID_COLL_TS_ANNOTATIONS = new ClassWithIdCollTsAnnotations("123", new Module("mod"), Instant.parse("2024-01-23T13:33:10.300Z"), "foo", "bar"); + private static final Object CLASS_WITH_ID_COLL_TS_ANNOTATIONS_CODEC = + DefaultCodecProvider.SINGLETON.get( + ClassWithIdCollTsAnnotations.class); + private static final String CLASS_WITH_ID_COLL_TS_ANNOTATIONS_WIRE = + "{\"firstName\":\"foo\",\"lastName\":\"bar\"}"; + private static final ClassWithIdCollTsAnnotations + CLASS_WITH_ID_COLL_TS_ANNOTATIONS = + new ClassWithIdCollTsAnnotations("123", new Module("mod"), + Instant.parse("2024-01-23T13:33:10.300Z"), "foo", "bar"); // Class with Client Generated Id, Coll, Ts annotations - private static final Object CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_CODEC = DefaultCodecProvider.SINGLETON.get(ClassWithClientGeneratedIdCollTsAnnotations.class); - private static final String CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_WIRE = "{\"id\":\"123\",\"firstName\":\"foo\",\"lastName\":\"bar\"}"; - private static final ClassWithClientGeneratedIdCollTsAnnotations CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS = new ClassWithClientGeneratedIdCollTsAnnotations("123", new Module("mod"), Instant.parse("2024-01-23T13:33:10.300Z"), "foo", "bar"); - private static final ClassWithClientGeneratedIdCollTsAnnotations CLASS_WITH_CLIENT_GENERATED_ID_NULL_ANNOTATIONS = new ClassWithClientGeneratedIdCollTsAnnotations(null, new Module("mod"), Instant.parse("2024-01-23T13:33:10.300Z"), "foo", "bar"); + private static final Object + CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_CODEC = + DefaultCodecProvider.SINGLETON.get( + ClassWithClientGeneratedIdCollTsAnnotations.class); + private static final String + CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_WIRE = + "{\"id\":\"123\",\"firstName\":\"foo\",\"lastName\":\"bar\"}"; + private static final ClassWithClientGeneratedIdCollTsAnnotations + CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS = + new ClassWithClientGeneratedIdCollTsAnnotations("123", + new Module("mod"), + Instant.parse("2024-01-23T13:33:10.300Z"), "foo", "bar"); + private static final ClassWithClientGeneratedIdCollTsAnnotations + CLASS_WITH_CLIENT_GENERATED_ID_NULL_ANNOTATIONS = + new ClassWithClientGeneratedIdCollTsAnnotations(null, + new Module("mod"), + Instant.parse("2024-01-23T13:33:10.300Z"), "foo", "bar"); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, CLASS_WITH_PARAMETERIZED_FIELDS_CODEC, CLASS_WITH_PARAMETERIZED_FIELDS_WIRE, CLASS_WITH_PARAMETERIZED_FIELDS, null), - Arguments.of(TestType.RoundTrip, CLASS_WITH_REF_TAG_COLLISION_CODEC, REF_TAG_COLLISION_WIRE, CLASS_WITH_REF_TAG_COLLISION, null), - Arguments.of(TestType.RoundTrip, CLASS_WITH_FAUNA_IGNORE_CODEC, CLASS_WITH_FAUNA_IGNORE_WIRE, CLASS_WITH_FAUNA_IGNORE, null), - Arguments.of(TestType.Encode, CLASS_WITH_FAUNA_IGNORE_CODEC, CLASS_WITH_FAUNA_IGNORE_WIRE, CLASS_WITH_FAUNA_IGNORE_WITH_AGE, null), - Arguments.of(TestType.Decode, CLASS_WITH_FAUNA_IGNORE_CODEC, CLASS_WITH_FAUNA_IGNORE_WITH_AGE_WIRE, CLASS_WITH_FAUNA_IGNORE, null), - Arguments.of(TestType.Decode, CLASS_WITH_ATTRIBUTES_CODEC, DOCUMENT_WIRE, CLASS_WITH_ATTRIBUTES, null), - Arguments.of(TestType.Decode, CLASS_WITH_ATTRIBUTES_CODEC, NULL_DOC_WIRE, null, NULL_DOC_EXCEPTION), - Arguments.of(TestType.Encode, CLASS_WITH_ID_COLL_TS_ANNOTATIONS_CODEC, CLASS_WITH_ID_COLL_TS_ANNOTATIONS_WIRE, CLASS_WITH_ID_COLL_TS_ANNOTATIONS, null), - Arguments.of(TestType.Encode, CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_CODEC, CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_WIRE, CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS, null), - Arguments.of(TestType.Encode, CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_CODEC, CLASS_WITH_ID_COLL_TS_ANNOTATIONS_WIRE, CLASS_WITH_CLIENT_GENERATED_ID_NULL_ANNOTATIONS, null) + Arguments.of(TestType.RoundTrip, + CLASS_WITH_PARAMETERIZED_FIELDS_CODEC, + CLASS_WITH_PARAMETERIZED_FIELDS_WIRE, + CLASS_WITH_PARAMETERIZED_FIELDS, null), + Arguments.of(TestType.RoundTrip, + CLASS_WITH_REF_TAG_COLLISION_CODEC, + REF_TAG_COLLISION_WIRE, CLASS_WITH_REF_TAG_COLLISION, + null), + Arguments.of(TestType.RoundTrip, CLASS_WITH_FAUNA_IGNORE_CODEC, + CLASS_WITH_FAUNA_IGNORE_WIRE, CLASS_WITH_FAUNA_IGNORE, + null), + Arguments.of(TestType.Encode, CLASS_WITH_FAUNA_IGNORE_CODEC, + CLASS_WITH_FAUNA_IGNORE_WIRE, + CLASS_WITH_FAUNA_IGNORE_WITH_AGE, null), + Arguments.of(TestType.Decode, CLASS_WITH_FAUNA_IGNORE_CODEC, + CLASS_WITH_FAUNA_IGNORE_WITH_AGE_WIRE, + CLASS_WITH_FAUNA_IGNORE, null), + Arguments.of(TestType.Decode, CLASS_WITH_ATTRIBUTES_CODEC, + DOCUMENT_WIRE, CLASS_WITH_ATTRIBUTES, null), + Arguments.of(TestType.Decode, CLASS_WITH_ATTRIBUTES_CODEC, + NULL_DOC_WIRE, null, NULL_DOC_EXCEPTION), + Arguments.of(TestType.Encode, + CLASS_WITH_ID_COLL_TS_ANNOTATIONS_CODEC, + CLASS_WITH_ID_COLL_TS_ANNOTATIONS_WIRE, + CLASS_WITH_ID_COLL_TS_ANNOTATIONS, null), + Arguments.of(TestType.Encode, + CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_CODEC, + CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_WIRE, + CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS, + null), + Arguments.of(TestType.Encode, + CLASS_WITH_CLIENT_GENERATED_ID_COLL_TS_ANNOTATIONS_CODEC, + CLASS_WITH_ID_COLL_TS_ANNOTATIONS_WIRE, + CLASS_WITH_CLIENT_GENERATED_ID_NULL_ANNOTATIONS, null) ); } @ParameterizedTest(name = "ClassCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void class_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void class_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } + @Test public void class_roundTripWithInheritance() throws IOException { - var codec = DefaultCodecProvider.SINGLETON.get(ClassWithInheritanceL2.class); - var wire = "{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}"; - var obj = new ClassWithInheritanceL2("foo","bar",42); + var codec = DefaultCodecProvider.SINGLETON.get( + ClassWithInheritanceL2.class); + var wire = + "{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}"; + var obj = new ClassWithInheritanceL2("foo", "bar", 42); runCase(TestType.RoundTrip, codec, wire, obj, null); } @@ -102,8 +176,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "ClassCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void class_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `ClassCodec`. Supported types for codec are [Document, Null, Object, Ref].", type); - runCase(TestType.Decode, CLASS_WITH_ATTRIBUTES_CODEC, wire, null, new CodecException(exMsg)); + public void class_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `ClassCodec`. Supported types for codec are [Document, Null, Object, Ref].", + type); + runCase(TestType.Decode, CLASS_WITH_ATTRIBUTES_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/DoubleCodecTest.java b/src/test/java/com/fauna/codec/codecs/DoubleCodecTest.java index 6bebb3aa..77831d5d 100644 --- a/src/test/java/com/fauna/codec/codecs/DoubleCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/DoubleCodecTest.java @@ -16,18 +16,28 @@ public class DoubleCodecTest extends TestBase { - public static final Codec DOUBLE_CODEC = DefaultCodecProvider.SINGLETON.get(Double.class); + public static final Codec DOUBLE_CODEC = + DefaultCodecProvider.SINGLETON.get(Double.class); + public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, DOUBLE_CODEC, DOUBLE_WIRE(Double.MAX_VALUE), Double.MAX_VALUE, null), - Arguments.of(TestType.RoundTrip, DOUBLE_CODEC, DOUBLE_WIRE(Double.MIN_VALUE), Double.MIN_VALUE, null), - Arguments.of(TestType.RoundTrip, DOUBLE_CODEC, "null", null, null) + Arguments.of(TestType.RoundTrip, DOUBLE_CODEC, + DOUBLE_WIRE(Double.MAX_VALUE), Double.MAX_VALUE, null), + Arguments.of(TestType.RoundTrip, DOUBLE_CODEC, + DOUBLE_WIRE(Double.MIN_VALUE), Double.MIN_VALUE, null), + Arguments.of(TestType.RoundTrip, DOUBLE_CODEC, "null", null, + null) ); } @ParameterizedTest(name = "DoubleCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void double_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void double_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -37,8 +47,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "DoubleCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void double_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `DoubleCodec`. Supported types for codec are [Double, Int, Long, Null].", type); - runCase(TestType.Decode, DOUBLE_CODEC, wire, null, new CodecException(exMsg)); + public void double_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `DoubleCodec`. Supported types for codec are [Double, Int, Long, Null].", + type); + runCase(TestType.Decode, DOUBLE_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java b/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java index f5c7e3ad..4ea7796c 100644 --- a/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java @@ -4,84 +4,109 @@ import com.fauna.codec.Codec; import com.fauna.codec.DefaultCodecProvider; import com.fauna.codec.FaunaType; -import com.fauna.codec.Helpers; -import com.fauna.exception.ClientException; +import com.fauna.event.EventSource; import com.fauna.exception.NullDocumentException; -import com.fauna.query.StreamTokenResponse; +import com.fauna.types.Document; +import com.fauna.types.DocumentRef; import com.fauna.types.Module; -import com.fauna.types.*; +import com.fauna.types.NamedDocument; +import com.fauna.types.NamedDocumentRef; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.io.IOException; -import java.text.MessageFormat; import java.time.Instant; import java.util.Arrays; import java.util.Map; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; public class DynamicCodecTest extends TestBase { - public static final Codec DYNAMIC_CODEC = DefaultCodecProvider.SINGLETON.get(Object.class); - + public static final Codec DYNAMIC_CODEC = + DefaultCodecProvider.SINGLETON.get(Object.class); + // Class with attributes - public static final String CLASS_WITH_ATTRIBUTES_WIRE = "{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}"; - public static final ClassWithAttributes CLASS_WITH_ATTRIBUTES = new ClassWithAttributes("foo","bar",42); + public static final String CLASS_WITH_ATTRIBUTES_WIRE = + "{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}"; + public static final ClassWithAttributes CLASS_WITH_ATTRIBUTES = + new ClassWithAttributes("foo", "bar", 42); // Doc - public static final String DOCUMENT_WIRE = "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; + public static final String DOCUMENT_WIRE = + "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; public static final Document DOCUMENT = new Document( "123", new Module("Foo"), Instant.parse("2023-12-15T01:01:01.0010010Z"), - Map.of("first_name","foo", "last_name", "bar","age",42) + Map.of("first_name", "foo", "last_name", "bar", "age", 42) ); - public static final DocumentRef DOCUMENT_REF = new DocumentRef("123", new Module("Foo")); - public static final String DOCUMENT_REF_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final DocumentRef DOCUMENT_REF = + new DocumentRef("123", new Module("Foo")); + public static final String DOCUMENT_REF_WIRE = + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; // Named doc - public static final String NAMED_DOCUMENT_WIRE = "{\"@doc\":{\"name\":\"Boogles\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; + public static final String NAMED_DOCUMENT_WIRE = + "{\"@doc\":{\"name\":\"Boogles\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; public static final NamedDocument NAMED_DOCUMENT = new NamedDocument( "Boogles", new Module("Foo"), Instant.parse("2023-12-15T01:01:01.0010010Z"), - Map.of("first_name","foo", "last_name", "bar") + Map.of("first_name", "foo", "last_name", "bar") ); - public static final String NAMED_DOCUMENT_REF_WIRE = "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; - public static final NamedDocumentRef NAMED_DOCUMENT_REF = new NamedDocumentRef("Boogles", new Module("Foo")); + public static final String NAMED_DOCUMENT_REF_WIRE = + "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final NamedDocumentRef NAMED_DOCUMENT_REF = + new NamedDocumentRef("Boogles", new Module("Foo")); // Null doc - public static final String NULL_DOC_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; - public static final NullDocumentException NULL_DOC_EXCEPTION = new NullDocumentException("123", new Module("Foo"), "not found"); + public static final String NULL_DOC_WIRE = + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; + public static final NullDocumentException NULL_DOC_EXCEPTION = + new NullDocumentException("123", new Module("Foo"), "not found"); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.Decode, DYNAMIC_CODEC, DOCUMENT_WIRE, DOCUMENT, null), - Arguments.of(TestType.Decode, DYNAMIC_CODEC, DOCUMENT_REF_WIRE, DOCUMENT_REF, null), - Arguments.of(TestType.Decode, DYNAMIC_CODEC, NAMED_DOCUMENT_WIRE, NAMED_DOCUMENT, null), - Arguments.of(TestType.Decode, DYNAMIC_CODEC, NAMED_DOCUMENT_REF_WIRE, NAMED_DOCUMENT_REF, null), - Arguments.of(TestType.Decode, DYNAMIC_CODEC, NULL_DOC_WIRE, null, NULL_DOC_EXCEPTION), - Arguments.of(TestType.Decode, DYNAMIC_CODEC, "{\"@stream\":\"token\"}", new StreamTokenResponse("token"), null), - Arguments.of(TestType.Decode, DYNAMIC_CODEC, "{\"@bytes\": \"RmF1bmE=\"}", new byte[]{70, 97, 117, 110, 97}, null) + Arguments.of(TestType.Decode, DYNAMIC_CODEC, DOCUMENT_WIRE, + DOCUMENT, null), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, DOCUMENT_REF_WIRE, + DOCUMENT_REF, null), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, + NAMED_DOCUMENT_WIRE, NAMED_DOCUMENT, null), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, + NAMED_DOCUMENT_REF_WIRE, NAMED_DOCUMENT_REF, null), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, NULL_DOC_WIRE, + null, NULL_DOC_EXCEPTION), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, + "{\"@stream\":\"token\"}", + new EventSource("token"), null), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, + "{\"@bytes\": \"RmF1bmE=\"}", + new byte[] {70, 97, 117, 110, 97}, null) ); } @ParameterizedTest(name = "DynamicCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void dynamic_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void dynamic_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @Test - public void dynamic_shouldSupportAllTypes() { + public void dynamic_shouldSupportAllTypes() { var arr = Arrays.stream(FaunaType.values()) - .filter(f -> Arrays.stream(DYNAMIC_CODEC.getSupportedTypes()).noneMatch(f::equals)).toArray(); + .filter(f -> Arrays.stream(DYNAMIC_CODEC.getSupportedTypes()) + .noneMatch(f::equals)).toArray(); assertEquals("[]", Arrays.toString(arr)); } } diff --git a/src/test/java/com/fauna/codec/codecs/EnumCodecTest.java b/src/test/java/com/fauna/codec/codecs/EnumCodecTest.java index e903390a..534d3603 100644 --- a/src/test/java/com/fauna/codec/codecs/EnumCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/EnumCodecTest.java @@ -29,13 +29,19 @@ public void class_encodeEnum() throws IOException { public static Stream unsupportedTypeCases() { - return unsupportedTypeCases(DefaultCodecProvider.SINGLETON.get(TestEnum.class)); + return unsupportedTypeCases( + DefaultCodecProvider.SINGLETON.get(TestEnum.class)); } @ParameterizedTest(name = "EnumCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void enum_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `EnumCodec`. Supported types for codec are [Null, String].", type); - runCase(TestType.Decode, DefaultCodecProvider.SINGLETON.get(TestEnum.class), wire, null, new CodecException(exMsg)); + public void enum_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `EnumCodec`. Supported types for codec are [Null, String].", + type); + runCase(TestType.Decode, + DefaultCodecProvider.SINGLETON.get(TestEnum.class), wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/Fixtures.java b/src/test/java/com/fauna/codec/codecs/Fixtures.java index 6335440a..54364553 100644 --- a/src/test/java/com/fauna/codec/codecs/Fixtures.java +++ b/src/test/java/com/fauna/codec/codecs/Fixtures.java @@ -11,7 +11,7 @@ public static String LONG_WIRE(Long l) { return String.format("{\"@long\":\"%s\"}", l); } - public static String DOUBLE_WIRE(Double d){ + public static String DOUBLE_WIRE(Double d) { return String.format("{\"@double\":\"%s\"}", d); } @@ -22,6 +22,7 @@ public static String DOUBLE_WIRE(Float f) { public static String TIME_WIRE(String s) { return String.format("{\"@time\":\"%s\"}", s); } + public static String ESCAPED_OBJECT_WIRE_WITH(String tag) { return String.format("{\"@object\":{\"%s\":\"not\"}}", tag); } diff --git a/src/test/java/com/fauna/codec/codecs/FloatCodecTest.java b/src/test/java/com/fauna/codec/codecs/FloatCodecTest.java index 25f6c288..dcad7ce7 100644 --- a/src/test/java/com/fauna/codec/codecs/FloatCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/FloatCodecTest.java @@ -16,18 +16,28 @@ public class FloatCodecTest extends TestBase { - public static final Codec FLOAT_CODEC = DefaultCodecProvider.SINGLETON.get(Float.class); + public static final Codec FLOAT_CODEC = + DefaultCodecProvider.SINGLETON.get(Float.class); + public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, FLOAT_CODEC, DOUBLE_WIRE(Float.MAX_VALUE), Float.MAX_VALUE, null), - Arguments.of(TestType.RoundTrip, FLOAT_CODEC, DOUBLE_WIRE(Float.MIN_VALUE), Float.MIN_VALUE, null), - Arguments.of(TestType.RoundTrip, FLOAT_CODEC, "null", null, null) + Arguments.of(TestType.RoundTrip, FLOAT_CODEC, + DOUBLE_WIRE(Float.MAX_VALUE), Float.MAX_VALUE, null), + Arguments.of(TestType.RoundTrip, FLOAT_CODEC, + DOUBLE_WIRE(Float.MIN_VALUE), Float.MIN_VALUE, null), + Arguments.of(TestType.RoundTrip, FLOAT_CODEC, "null", null, + null) ); } @ParameterizedTest(name = "FloatCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void float_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void float_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -37,8 +47,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "FloatCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void float_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `FloatCodec`. Supported types for codec are [Double, Int, Long, Null].", type); - runCase(TestType.Decode, FLOAT_CODEC, wire, null, new CodecException(exMsg)); + public void float_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `FloatCodec`. Supported types for codec are [Double, Int, Long, Null].", + type); + runCase(TestType.Decode, FLOAT_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/InstantCodecTest.java b/src/test/java/com/fauna/codec/codecs/InstantCodecTest.java index 9c5246df..3b58ce4f 100644 --- a/src/test/java/com/fauna/codec/codecs/InstantCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/InstantCodecTest.java @@ -17,23 +17,33 @@ public class InstantCodecTest extends TestBase { - public static final Codec INSTANT_CODEC = DefaultCodecProvider.SINGLETON.get(Instant.class); - public static String TIME_STRING_PACIFIC = "2023-12-03T05:52:10.000001-09:00"; + public static final Codec INSTANT_CODEC = + DefaultCodecProvider.SINGLETON.get(Instant.class); + public static String TIME_STRING_PACIFIC = + "2023-12-03T05:52:10.000001-09:00"; public static String TIME_STRING_UTC = "2023-12-03T14:52:10.000001Z"; public static Instant INSTANT = Instant.parse(TIME_STRING_PACIFIC); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, INSTANT_CODEC, TIME_WIRE(TIME_STRING_UTC), INSTANT, null), - Arguments.of(TestType.RoundTrip, INSTANT_CODEC, "null", null, null), - Arguments.of(TestType.Decode, INSTANT_CODEC, TIME_WIRE(TIME_STRING_PACIFIC), INSTANT, null) + Arguments.of(TestType.RoundTrip, INSTANT_CODEC, + TIME_WIRE(TIME_STRING_UTC), INSTANT, null), + Arguments.of(TestType.RoundTrip, INSTANT_CODEC, "null", null, + null), + Arguments.of(TestType.Decode, INSTANT_CODEC, + TIME_WIRE(TIME_STRING_PACIFIC), INSTANT, null) ); } @ParameterizedTest(name = "InstantCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void instant_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void instant_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -43,8 +53,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "InstantCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void instant_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `InstantCodec`. Supported types for codec are [Null, Time].", type); - runCase(TestType.Decode, INSTANT_CODEC, wire, null, new CodecException(exMsg)); + public void instant_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `InstantCodec`. Supported types for codec are [Null, Time].", + type); + runCase(TestType.Decode, INSTANT_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/IntCodecTest.java b/src/test/java/com/fauna/codec/codecs/IntCodecTest.java index 611e18d1..1954773d 100644 --- a/src/test/java/com/fauna/codec/codecs/IntCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/IntCodecTest.java @@ -15,18 +15,27 @@ import static com.fauna.codec.codecs.Fixtures.INT_WIRE; public class IntCodecTest extends TestBase { - public static final Codec INT_CODEC = DefaultCodecProvider.SINGLETON.get(Integer.class); + public static final Codec INT_CODEC = + DefaultCodecProvider.SINGLETON.get(Integer.class); + public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, INT_CODEC, INT_WIRE(Integer.MAX_VALUE), Integer.MAX_VALUE, null), - Arguments.of(TestType.RoundTrip, INT_CODEC, INT_WIRE(Integer.MIN_VALUE), Integer.MIN_VALUE, null), + Arguments.of(TestType.RoundTrip, INT_CODEC, + INT_WIRE(Integer.MAX_VALUE), Integer.MAX_VALUE, null), + Arguments.of(TestType.RoundTrip, INT_CODEC, + INT_WIRE(Integer.MIN_VALUE), Integer.MIN_VALUE, null), Arguments.of(TestType.RoundTrip, INT_CODEC, "null", null, null) ); } @ParameterizedTest(name = "IntCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void int_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void int_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -36,8 +45,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "IntCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void int_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `IntCodec`. Supported types for codec are [Int, Long, Null].", type); - runCase(TestType.Decode, INT_CODEC, wire, null, new CodecException(exMsg)); + public void int_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `IntCodec`. Supported types for codec are [Int, Long, Null].", + type); + runCase(TestType.Decode, INT_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/ListCodecTest.java b/src/test/java/com/fauna/codec/codecs/ListCodecTest.java index acce9aac..df8ebdca 100644 --- a/src/test/java/com/fauna/codec/codecs/ListCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/ListCodecTest.java @@ -4,7 +4,6 @@ import com.fauna.codec.DefaultCodecProvider; import com.fauna.codec.FaunaType; import com.fauna.exception.CodecException; -import com.fauna.types.Document; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -18,18 +17,27 @@ public class ListCodecTest extends TestBase { - public static final Codec> LIST_INT_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(List.class, new Type[]{int.class}); + public static final Codec> LIST_INT_CODEC = + (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get( + List.class, new Type[] {int.class}); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, LIST_INT_CODEC, "[{\"@int\":\"42\"}]", List.of(42), null), - Arguments.of(TestType.RoundTrip, LIST_INT_CODEC, "null", null, null) + Arguments.of(TestType.RoundTrip, LIST_INT_CODEC, + "[{\"@int\":\"42\"}]", List.of(42), null), + Arguments.of(TestType.RoundTrip, LIST_INT_CODEC, "null", null, + null) ); } @ParameterizedTest(name = "ListCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void list_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void list_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -39,8 +47,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "ListCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void list_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `ListCodec`. Supported types for codec are [Array, Null].", type); - runCase(TestType.Decode, LIST_INT_CODEC, wire, null, new CodecException(exMsg)); + public void list_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `ListCodec`. Supported types for codec are [Array, Null].", + type); + runCase(TestType.Decode, LIST_INT_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/LocalDateCodecTest.java b/src/test/java/com/fauna/codec/codecs/LocalDateCodecTest.java index 2143784d..7c2b3d86 100644 --- a/src/test/java/com/fauna/codec/codecs/LocalDateCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/LocalDateCodecTest.java @@ -14,18 +14,24 @@ import java.util.stream.Stream; public class LocalDateCodecTest extends TestBase { - public static final Codec LOCAL_DATE_CODEC = DefaultCodecProvider.SINGLETON.get(LocalDate.class); + public static final Codec LOCAL_DATE_CODEC = + DefaultCodecProvider.SINGLETON.get(LocalDate.class); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, LOCAL_DATE_CODEC, "{\"@date\":\"2023-12-03\"}", LocalDate.parse("2023-12-03"), null), - Arguments.of(TestType.RoundTrip, LOCAL_DATE_CODEC, "null", null, null) + Arguments.of(TestType.RoundTrip, LOCAL_DATE_CODEC, + "{\"@date\":\"2023-12-03\"}", + LocalDate.parse("2023-12-03"), null), + Arguments.of(TestType.RoundTrip, LOCAL_DATE_CODEC, "null", null, + null) ); } @ParameterizedTest(name = "LocalDateCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void localDate_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void localDate_runTestCases( + TestType testType, Codec codec, String wire, Object obj, + E exception) throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -35,8 +41,13 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "LocalDateCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void localDate_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `LocalDateCodec`. Supported types for codec are [Date, Null].", type); - runCase(TestType.Decode, LOCAL_DATE_CODEC, wire, null, new CodecException(exMsg)); + public void localDate_runUnsupportedTypeTestCases(String wire, + FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `LocalDateCodec`. Supported types for codec are [Date, Null].", + type); + runCase(TestType.Decode, LOCAL_DATE_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/LongCodecTest.java b/src/test/java/com/fauna/codec/codecs/LongCodecTest.java index c1933cc5..b7e936b1 100644 --- a/src/test/java/com/fauna/codec/codecs/LongCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/LongCodecTest.java @@ -16,18 +16,27 @@ public class LongCodecTest extends TestBase { - public static final Codec LONG_CODEC = DefaultCodecProvider.SINGLETON.get(Long.class); + public static final Codec LONG_CODEC = + DefaultCodecProvider.SINGLETON.get(Long.class); + public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, LONG_CODEC, LONG_WIRE(Long.MAX_VALUE), Long.MAX_VALUE, null), - Arguments.of(TestType.RoundTrip, LONG_CODEC, LONG_WIRE(Long.MIN_VALUE), Long.MIN_VALUE, null), + Arguments.of(TestType.RoundTrip, LONG_CODEC, + LONG_WIRE(Long.MAX_VALUE), Long.MAX_VALUE, null), + Arguments.of(TestType.RoundTrip, LONG_CODEC, + LONG_WIRE(Long.MIN_VALUE), Long.MIN_VALUE, null), Arguments.of(TestType.RoundTrip, LONG_CODEC, "null", null, null) ); } @ParameterizedTest(name = "LongCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void long_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void long_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -37,8 +46,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "LongCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void long_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `LongCodec`. Supported types for codec are [Int, Long, Null].", type); - runCase(TestType.Decode, LONG_CODEC, wire, null, new CodecException(exMsg)); + public void long_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `LongCodec`. Supported types for codec are [Int, Long, Null].", + type); + runCase(TestType.Decode, LONG_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/MapCodecTest.java b/src/test/java/com/fauna/codec/codecs/MapCodecTest.java index cb03cad6..83dff871 100644 --- a/src/test/java/com/fauna/codec/codecs/MapCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/MapCodecTest.java @@ -19,17 +19,27 @@ public class MapCodecTest extends TestBase { - public static final Codec> MAP_INT_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(Map.class, new Type[]{String.class, Integer.class}); - public static final Codec> MAP_STRING_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(Map.class, new Type[]{String.class, String.class}); + public static final Codec> MAP_INT_CODEC = + (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get( + Map.class, new Type[] {String.class, Integer.class}); + public static final Codec> MAP_STRING_CODEC = + (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get( + Map.class, new Type[] {String.class, String.class}); public static Stream testCases() { - return Stream.of(Arguments.of(TestType.RoundTrip, MAP_INT_CODEC, "{\"key1\":{\"@int\":\"42\"}}", Map.of("key1", 42), null)); + return Stream.of(Arguments.of(TestType.RoundTrip, MAP_INT_CODEC, + "{\"key1\":{\"@int\":\"42\"}}", Map.of("key1", 42), null)); } @ParameterizedTest(name = "MapCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void map_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void map_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -37,7 +47,8 @@ public void map_runTestCases(TestType testType, Codec @ParameterizedTest @MethodSource("tags") public void map_escapeOnReservedKey(String tag) throws IOException { - runCase(TestType.RoundTrip, MAP_STRING_CODEC, ESCAPED_OBJECT_WIRE_WITH(tag), Map.of(tag, "not"), null); + runCase(TestType.RoundTrip, MAP_STRING_CODEC, + ESCAPED_OBJECT_WIRE_WITH(tag), Map.of(tag, "not"), null); } public static Stream unsupportedTypeCases() { @@ -46,8 +57,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "MapCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void map_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `MapCodec`. Supported types for codec are [Null, Object].", type); - runCase(TestType.Decode, MAP_STRING_CODEC, wire, null, new CodecException(exMsg)); + public void map_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `MapCodec`. Supported types for codec are [Null, Object].", + type); + runCase(TestType.Decode, MAP_STRING_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/ModuleCodecTest.java b/src/test/java/com/fauna/codec/codecs/ModuleCodecTest.java index 5b2e1ab6..5618fcfe 100644 --- a/src/test/java/com/fauna/codec/codecs/ModuleCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/ModuleCodecTest.java @@ -14,18 +14,26 @@ import java.util.stream.Stream; public class ModuleCodecTest extends TestBase { - public static final Codec MODULE_CODEC = DefaultCodecProvider.SINGLETON.get(Module.class); + public static final Codec MODULE_CODEC = + DefaultCodecProvider.SINGLETON.get(Module.class); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, MODULE_CODEC, "{\"@mod\":\"Foo\"}", new Module("Foo"), null), - Arguments.of(TestType.RoundTrip, MODULE_CODEC, "null", null, null) + Arguments.of(TestType.RoundTrip, MODULE_CODEC, + "{\"@mod\":\"Foo\"}", new Module("Foo"), null), + Arguments.of(TestType.RoundTrip, MODULE_CODEC, "null", null, + null) ); } @ParameterizedTest(name = "ModuleCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void module_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void module_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -35,8 +43,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "ModuleCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void module_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `ModuleCodec`. Supported types for codec are [Module, Null].", type); - runCase(TestType.Decode, MODULE_CODEC, wire, null, new CodecException(exMsg)); + public void module_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `ModuleCodec`. Supported types for codec are [Module, Null].", + type); + runCase(TestType.Decode, MODULE_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/NullableDocumentCodecTest.java b/src/test/java/com/fauna/codec/codecs/NullableDocumentCodecTest.java index 7cfaa655..431d9ea9 100644 --- a/src/test/java/com/fauna/codec/codecs/NullableDocumentCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/NullableDocumentCodecTest.java @@ -5,8 +5,12 @@ import com.fauna.codec.DefaultCodecProvider; import com.fauna.codec.FaunaType; import com.fauna.exception.CodecException; -import com.fauna.types.*; +import com.fauna.types.Document; import com.fauna.types.Module; +import com.fauna.types.NamedDocument; +import com.fauna.types.NonNullDocument; +import com.fauna.types.NullDocument; +import com.fauna.types.NullableDocument; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -19,53 +23,81 @@ import java.util.stream.Stream; public class NullableDocumentCodecTest extends TestBase { - public static final Codec> NULLABLE_DOC_CODEC = (Codec)DefaultCodecProvider.SINGLETON.get(NullableDocument.class, new Type[]{Document.class}); - public static final Codec> NULLABLE_ClASS_CODEC = (Codec)DefaultCodecProvider.SINGLETON.get(NullableDocument.class, new Type[]{ClassWithAttributes.class}); - + public static final Codec> NULLABLE_DOC_CODEC = + (Codec) DefaultCodecProvider.SINGLETON.get(NullableDocument.class, + new Type[] {Document.class}); + public static final Codec> + NULLABLE_ClASS_CODEC = + (Codec) DefaultCodecProvider.SINGLETON.get(NullableDocument.class, + new Type[] {ClassWithAttributes.class}); + // Class with attributes - public static final String CLASS_WITH_ATTRIBUTES_WIRE = "{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}"; - public static final ClassWithAttributes CLASS_WITH_ATTRIBUTES = new ClassWithAttributes("foo","bar",42); + public static final String CLASS_WITH_ATTRIBUTES_WIRE = + "{\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}"; + public static final ClassWithAttributes CLASS_WITH_ATTRIBUTES = + new ClassWithAttributes("foo", "bar", 42); // Doc - public static final String DOCUMENT_WIRE = "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; - public static final String DOCUMENT_REF_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final String DOCUMENT_WIRE = + "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; + public static final String DOCUMENT_REF_WIRE = + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; public static final Document DOCUMENT = new Document( "123", new Module("Foo"), Instant.parse("2023-12-15T01:01:01.0010010Z"), - Map.of("first_name","foo", "last_name", "bar","age",42) + Map.of("first_name", "foo", "last_name", "bar", "age", 42) ); - + // Named doc - public static final String NAMED_DOCUMENT_WIRE = "{\"@doc\":{\"name\":\"Boogles\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; - public static final String NAMED_DOCUMENT_REF_WIRE = "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final String NAMED_DOCUMENT_WIRE = + "{\"@doc\":{\"name\":\"Boogles\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; + public static final String NAMED_DOCUMENT_REF_WIRE = + "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; public static final NamedDocument NAMED_DOCUMENT = new NamedDocument( "Boogles", new Module("Foo"), Instant.parse("2023-12-15T01:01:01.0010010Z"), - Map.of("first_name","foo", "last_name", "bar") + Map.of("first_name", "foo", "last_name", "bar") ); - + // Null doc - public static final String NULL_DOC_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; - public static final NullDocument NULL_DOCUMENT = new NullDocument<>("123", new Module("Foo"), "not found"); + public static final String NULL_DOC_WIRE = + "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; + public static final NullDocument NULL_DOCUMENT = + new NullDocument<>("123", new Module("Foo"), "not found"); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.Decode, NULLABLE_DOC_CODEC, DOCUMENT_WIRE, new NonNullDocument<>(DOCUMENT), null), - Arguments.of(TestType.Encode, NULLABLE_DOC_CODEC, DOCUMENT_REF_WIRE, new NonNullDocument<>(DOCUMENT), null), - Arguments.of(TestType.Decode, NULLABLE_DOC_CODEC, NAMED_DOCUMENT_WIRE, new NonNullDocument<>(NAMED_DOCUMENT), null), - Arguments.of(TestType.Encode, NULLABLE_DOC_CODEC, NAMED_DOCUMENT_REF_WIRE, new NonNullDocument<>(NAMED_DOCUMENT), null), - Arguments.of(TestType.Decode, NULLABLE_DOC_CODEC, NULL_DOC_WIRE, NULL_DOCUMENT, null), - Arguments.of(TestType.Decode, NULLABLE_ClASS_CODEC, DOCUMENT_WIRE, new NonNullDocument<>(CLASS_WITH_ATTRIBUTES), null), - Arguments.of(TestType.Encode, NULLABLE_ClASS_CODEC, CLASS_WITH_ATTRIBUTES_WIRE, new NonNullDocument<>(CLASS_WITH_ATTRIBUTES), null), - Arguments.of(TestType.Decode, NULLABLE_ClASS_CODEC, NULL_DOC_WIRE, NULL_DOCUMENT, null) + Arguments.of(TestType.Decode, NULLABLE_DOC_CODEC, DOCUMENT_WIRE, + new NonNullDocument<>(DOCUMENT), null), + Arguments.of(TestType.Encode, NULLABLE_DOC_CODEC, + DOCUMENT_REF_WIRE, new NonNullDocument<>(DOCUMENT), + null), + Arguments.of(TestType.Decode, NULLABLE_DOC_CODEC, + NAMED_DOCUMENT_WIRE, + new NonNullDocument<>(NAMED_DOCUMENT), null), + Arguments.of(TestType.Encode, NULLABLE_DOC_CODEC, + NAMED_DOCUMENT_REF_WIRE, + new NonNullDocument<>(NAMED_DOCUMENT), null), + Arguments.of(TestType.Decode, NULLABLE_DOC_CODEC, NULL_DOC_WIRE, + NULL_DOCUMENT, null), + Arguments.of(TestType.Decode, NULLABLE_ClASS_CODEC, + DOCUMENT_WIRE, + new NonNullDocument<>(CLASS_WITH_ATTRIBUTES), null), + Arguments.of(TestType.Encode, NULLABLE_ClASS_CODEC, + CLASS_WITH_ATTRIBUTES_WIRE, + new NonNullDocument<>(CLASS_WITH_ATTRIBUTES), null), + Arguments.of(TestType.Decode, NULLABLE_ClASS_CODEC, + NULL_DOC_WIRE, NULL_DOCUMENT, null) ); } @ParameterizedTest(name = "NullableCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void nullable_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void nullable_runTestCases( + TestType testType, Codec codec, String wire, Object obj, + E exception) throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -75,9 +107,14 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "NullableCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void nullable_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { + public void nullable_runUnsupportedTypeTestCases(String wire, + FaunaType type) + throws IOException { // This codec will pass through the supported types of the underlying codec - var exMsg = MessageFormat.format("Unable to decode `{0}` with `BaseDocumentCodec`. Supported types for codec are [Document, Null, Ref].", type); - runCase(TestType.Decode, NULLABLE_DOC_CODEC, wire, null, new CodecException(exMsg)); + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `BaseDocumentCodec`. Supported types for codec are [Document, Null, Ref].", + type); + runCase(TestType.Decode, NULLABLE_DOC_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/OptionalCodecTest.java b/src/test/java/com/fauna/codec/codecs/OptionalCodecTest.java index 64a6c7db..ab1d5a77 100644 --- a/src/test/java/com/fauna/codec/codecs/OptionalCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/OptionalCodecTest.java @@ -16,18 +16,24 @@ public class OptionalCodecTest extends TestBase { - public static final Codec> OPTIONAL_STRING_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(Optional.class, new Type[]{String.class}); + public static final Codec> OPTIONAL_STRING_CODEC = + (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get( + Optional.class, new Type[] {String.class}); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, OPTIONAL_STRING_CODEC, "null", Optional.empty(), null), - Arguments.of(TestType.RoundTrip, OPTIONAL_STRING_CODEC, "\"Fauna\"", Optional.of("Fauna"), null) + Arguments.of(TestType.RoundTrip, OPTIONAL_STRING_CODEC, "null", + Optional.empty(), null), + Arguments.of(TestType.RoundTrip, OPTIONAL_STRING_CODEC, + "\"Fauna\"", Optional.of("Fauna"), null) ); } @ParameterizedTest(name = "OptionalCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void optional_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void optional_runTestCases( + TestType testType, Codec codec, String wire, Object obj, + E exception) throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -37,9 +43,14 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "OptionalCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void optional_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { + public void optional_runUnsupportedTypeTestCases(String wire, + FaunaType type) + throws IOException { // This codec will pass through the supported types of the underlying codec - var exMsg = MessageFormat.format("Unable to decode `{0}` with `StringCodec`. Supported types for codec are [Bytes, Null, String].", type); - runCase(TestType.Decode, OPTIONAL_STRING_CODEC, wire, null, new CodecException(exMsg)); + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `StringCodec`. Supported types for codec are [Bytes, Null, String].", + type); + runCase(TestType.Decode, OPTIONAL_STRING_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/PageCodecTest.java b/src/test/java/com/fauna/codec/codecs/PageCodecTest.java index 3f179e3e..2750ec76 100644 --- a/src/test/java/com/fauna/codec/codecs/PageCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/PageCodecTest.java @@ -4,9 +4,10 @@ import com.fauna.codec.Codec; import com.fauna.codec.DefaultCodecProvider; import com.fauna.codec.FaunaType; +import com.fauna.codec.Helpers; import com.fauna.exception.CodecException; -import com.fauna.types.Document; import com.fauna.types.Page; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -17,33 +18,51 @@ import java.util.List; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class PageCodecTest extends TestBase { - public static final String PAGE_WIRE = "{\"@set\":{\"data\":[{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}],\"after\": null}}"; - public static final String DOCUMENT_WIRE = "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; - public static final String ARRAY_WIRE = "[{\"@int\":\"1\"},{\"@int\":\"2\"}]"; + public static final String PAGE_WIRE = + "{\"@set\":{\"data\":[{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}],\"after\": null}}"; + public static final String DOCUMENT_WIRE = + "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; + public static final String ARRAY_WIRE = + "[{\"@int\":\"1\"},{\"@int\":\"2\"}]"; - public static final ClassWithAttributes PERSON_WITH_ATTRIBUTES = new ClassWithAttributes("foo","bar",42); - public static final Page PAGE = new Page<>(List.of(PERSON_WITH_ATTRIBUTES),null); + public static final ClassWithAttributes PERSON_WITH_ATTRIBUTES = + new ClassWithAttributes("foo", "bar", 42); + public static final Page PAGE = + new Page<>(List.of(PERSON_WITH_ATTRIBUTES), null); public static Codec> pageCodecOf(Class clazz) { //noinspection unchecked,rawtypes - return (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(Page.class, new Type[]{clazz}); + return (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get( + Page.class, new Type[] {clazz}); } public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, ClassWithAttributes.class, "null", null, null), - Arguments.of(TestType.Decode, ClassWithAttributes.class, PAGE_WIRE, PAGE, null), - Arguments.of(TestType.Decode, ClassWithAttributes.class, DOCUMENT_WIRE, PAGE, null), - Arguments.of(TestType.Decode, Integer.class, ARRAY_WIRE, new Page<>(List.of(1, 2), null), null), - Arguments.of(TestType.Decode, Integer.class, "{\"@int\":\"1\"}", new Page<>(List.of(1), null), null) + Arguments.of(TestType.RoundTrip, ClassWithAttributes.class, + "null", null, null), + Arguments.of(TestType.Decode, ClassWithAttributes.class, + PAGE_WIRE, PAGE, null), + Arguments.of(TestType.Decode, ClassWithAttributes.class, + DOCUMENT_WIRE, PAGE, null), + Arguments.of(TestType.Decode, Integer.class, ARRAY_WIRE, + new Page<>(List.of(1, 2), null), null), + Arguments.of(TestType.Decode, Integer.class, "{\"@int\":\"1\"}", + new Page<>(List.of(1), null), null) ); } @ParameterizedTest(name = "PageCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void page_runTestCases(TestType testType, Class pageElemClass, String wire, Object obj, E exception) throws IOException { + public void page_runTestCases(TestType testType, + Class pageElemClass, + String wire, + Object obj, + E exception) + throws IOException { var codec = pageCodecOf(pageElemClass); runCase(testType, codec, wire, obj, exception); } @@ -54,8 +73,23 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "PageCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void page_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `PageCodec`. Supported types for codec are [Array, Boolean, Bytes, Date, Double, Document, Int, Long, Module, Null, Object, Ref, Set, String, Time].", type); - runCase(TestType.Decode, pageCodecOf(Object.class), wire, null, new CodecException(exMsg)); + public void page_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `PageCodec`. Supported types for codec are [Array, Boolean, Bytes, Date, Double, Document, Int, Long, Module, Null, Object, Ref, Set, String, Time].", + type); + runCase(TestType.Decode, pageCodecOf(Object.class), wire, null, + new CodecException(exMsg)); + } + + @Test + public void page_decodeUnmaterializedSet() + { + var token = "aftertoken"; + var wire = "{\"@set\":\"" + token + "\"}"; + var codec = pageCodecOf(Object.class); + var decoded = Helpers.decode(codec, wire); + assertEquals(token, decoded.getAfter().get().getToken()); + assertEquals(0, decoded.getData().size()); } } diff --git a/src/test/java/com/fauna/codec/codecs/QueryArrCodecTest.java b/src/test/java/com/fauna/codec/codecs/QueryArrCodecTest.java index 18796e4b..26ee88a8 100644 --- a/src/test/java/com/fauna/codec/codecs/QueryArrCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/QueryArrCodecTest.java @@ -15,24 +15,33 @@ import static com.fauna.query.builder.Query.fql; public class QueryArrCodecTest extends TestBase { - public static final Codec QUERY_ARR_CODEC = DefaultCodecProvider.SINGLETON.get(QueryArr.class); + public static final Codec QUERY_ARR_CODEC = + DefaultCodecProvider.SINGLETON.get(QueryArr.class); - public static final String QUERY_ARR_BASIC_WIRE = "{\"array\":[{\"fql\":[{\"value\":{\"@int\":\"42\"}}]}]}"; - public static final QueryArr QUERY_ARR_BASIC = QueryArr.of(List.of(fql("${n}", Map.of("n", 42)))); + public static final String QUERY_ARR_BASIC_WIRE = + "{\"array\":[{\"fql\":[{\"value\":{\"@int\":\"42\"}}]}]}"; + public static final QueryArr QUERY_ARR_BASIC = + QueryArr.of(List.of(fql("${n}", Map.of("n", 42)))); - public static final String QUERY_ARR_NESTED_WIRE = "{\"array\":[{\"array\":[{\"fql\":[{\"value\":{\"@int\":\"42\"}}]}]}]}"; - public static final QueryArr QUERY_ARR_NESTED = QueryArr.of(List.of(QueryArr.of(List.of(fql("${n}", Map.of("n", 42)))))); + public static final String QUERY_ARR_NESTED_WIRE = + "{\"array\":[{\"array\":[{\"fql\":[{\"value\":{\"@int\":\"42\"}}]}]}]}"; + public static final QueryArr QUERY_ARR_NESTED = QueryArr.of( + List.of(QueryArr.of(List.of(fql("${n}", Map.of("n", 42)))))); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.Encode, QUERY_ARR_CODEC, QUERY_ARR_BASIC_WIRE, QUERY_ARR_BASIC, null), - Arguments.of(TestType.Encode, QUERY_ARR_CODEC, QUERY_ARR_NESTED_WIRE, QUERY_ARR_NESTED, null) + Arguments.of(TestType.Encode, QUERY_ARR_CODEC, + QUERY_ARR_BASIC_WIRE, QUERY_ARR_BASIC, null), + Arguments.of(TestType.Encode, QUERY_ARR_CODEC, + QUERY_ARR_NESTED_WIRE, QUERY_ARR_NESTED, null) ); } @ParameterizedTest(name = "QueryArrCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void queryArr_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void queryArr_runTestCases( + TestType testType, Codec codec, String wire, Object obj, + E exception) throws IOException { runCase(testType, codec, wire, obj, exception); } } diff --git a/src/test/java/com/fauna/codec/codecs/QueryCodecTest.java b/src/test/java/com/fauna/codec/codecs/QueryCodecTest.java index f8d7236a..45ad950f 100644 --- a/src/test/java/com/fauna/codec/codecs/QueryCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/QueryCodecTest.java @@ -14,17 +14,26 @@ import static com.fauna.query.builder.Query.fql; public class QueryCodecTest extends TestBase { - public static final Codec QUERY_CODEC = DefaultCodecProvider.SINGLETON.get(Query.class); + public static final Codec QUERY_CODEC = + DefaultCodecProvider.SINGLETON.get(Query.class); public static final Query QUERY = fql("let age = ${n}", Map.of("n", 42)); + public static Stream testCases() { return Stream.of( - Arguments.of(TestType.Encode, QUERY_CODEC, "{\"fql\":[\"let age = \",{\"value\":{\"@int\":\"42\"}}]}", QUERY, null) + Arguments.of(TestType.Encode, QUERY_CODEC, + "{\"fql\":[\"let age = \",{\"value\":{\"@int\":\"42\"}}]}", + QUERY, null) ); } @ParameterizedTest(name = "QueryCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void query_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void query_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } } diff --git a/src/test/java/com/fauna/codec/codecs/QueryObjCodecTest.java b/src/test/java/com/fauna/codec/codecs/QueryObjCodecTest.java index bee20501..f925c8e1 100644 --- a/src/test/java/com/fauna/codec/codecs/QueryObjCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/QueryObjCodecTest.java @@ -14,24 +14,36 @@ import static com.fauna.query.builder.Query.fql; public class QueryObjCodecTest extends TestBase { - public static final Codec QUERY_OBJ_CODEC = DefaultCodecProvider.SINGLETON.get(QueryObj.class); + public static final Codec QUERY_OBJ_CODEC = + DefaultCodecProvider.SINGLETON.get(QueryObj.class); - public static final String QUERY_OBJ_BASIC_WIRE = "{\"object\":{\"calc\":{\"fql\":[{\"value\":{\"@int\":\"42\"}}]}}}"; - public static final QueryObj QUERY_OBJ_BASIC = QueryObj.of(Map.of("calc", fql("${n}", Map.of("n", 42)))); + public static final String QUERY_OBJ_BASIC_WIRE = + "{\"object\":{\"calc\":{\"fql\":[{\"value\":{\"@int\":\"42\"}}]}}}"; + public static final QueryObj QUERY_OBJ_BASIC = + QueryObj.of(Map.of("calc", fql("${n}", Map.of("n", 42)))); - public static final String QUERY_OBJ_NESTED_WIRE = "{\"object\":{\"outer\":{\"object\":{\"inner\":{\"fql\":[{\"value\":{\"@int\":\"42\"}}]}}}}}"; - public static final QueryObj QUERY_OBJ_NESTED = QueryObj.of(Map.of("outer", QueryObj.of(Map.of("inner",fql("${n}", Map.of("n", 42)))))); + public static final String QUERY_OBJ_NESTED_WIRE = + "{\"object\":{\"outer\":{\"object\":{\"inner\":{\"fql\":[{\"value\":{\"@int\":\"42\"}}]}}}}}"; + public static final QueryObj QUERY_OBJ_NESTED = QueryObj.of(Map.of("outer", + QueryObj.of(Map.of("inner", fql("${n}", Map.of("n", 42)))))); public static Stream testCases() { return Stream.of( - Arguments.of(TestType.Encode, QUERY_OBJ_CODEC, QUERY_OBJ_BASIC_WIRE, QUERY_OBJ_BASIC, null), - Arguments.of(TestType.Encode, QUERY_OBJ_CODEC, QUERY_OBJ_NESTED_WIRE, QUERY_OBJ_NESTED, null) + Arguments.of(TestType.Encode, QUERY_OBJ_CODEC, + QUERY_OBJ_BASIC_WIRE, QUERY_OBJ_BASIC, null), + Arguments.of(TestType.Encode, QUERY_OBJ_CODEC, + QUERY_OBJ_NESTED_WIRE, QUERY_OBJ_NESTED, null) ); } @ParameterizedTest(name = "QueryObjCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void query_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void query_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } } diff --git a/src/test/java/com/fauna/codec/codecs/ShortCodecTest.java b/src/test/java/com/fauna/codec/codecs/ShortCodecTest.java index cb924b1c..af6b8f73 100644 --- a/src/test/java/com/fauna/codec/codecs/ShortCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/ShortCodecTest.java @@ -16,18 +16,28 @@ public class ShortCodecTest extends TestBase { - public static final Codec SHORT_CODEC = DefaultCodecProvider.SINGLETON.get(Short.class); + public static final Codec SHORT_CODEC = + DefaultCodecProvider.SINGLETON.get(Short.class); + public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, SHORT_CODEC, INT_WIRE((int) Short.MAX_VALUE), Short.MAX_VALUE, null), - Arguments.of(TestType.RoundTrip, SHORT_CODEC, INT_WIRE((int) Short.MIN_VALUE), Short.MIN_VALUE, null), - Arguments.of(TestType.RoundTrip, SHORT_CODEC, "null", null, null) + Arguments.of(TestType.RoundTrip, SHORT_CODEC, + INT_WIRE((int) Short.MAX_VALUE), Short.MAX_VALUE, null), + Arguments.of(TestType.RoundTrip, SHORT_CODEC, + INT_WIRE((int) Short.MIN_VALUE), Short.MIN_VALUE, null), + Arguments.of(TestType.RoundTrip, SHORT_CODEC, "null", null, + null) ); } @ParameterizedTest(name = "ShortCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void short_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void short_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -37,8 +47,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "ShortCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void short_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `ShortCodec`. Supported types for codec are [Int, Null].", type); - runCase(TestType.Decode, SHORT_CODEC, wire, null, new CodecException(exMsg)); + public void short_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `ShortCodec`. Supported types for codec are [Int, Null].", + type); + runCase(TestType.Decode, SHORT_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/StringCodecTest.java b/src/test/java/com/fauna/codec/codecs/StringCodecTest.java index 92e7f5dd..6c9ae77d 100644 --- a/src/test/java/com/fauna/codec/codecs/StringCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/StringCodecTest.java @@ -14,18 +14,28 @@ public class StringCodecTest extends TestBase { - public static final Codec STRING_CODEC = DefaultCodecProvider.SINGLETON.get(String.class); + public static final Codec STRING_CODEC = + DefaultCodecProvider.SINGLETON.get(String.class); + public static Stream testCases() { return Stream.of( - Arguments.of(TestType.RoundTrip, STRING_CODEC, "\"Fauna\"", "Fauna", null), - Arguments.of(TestType.RoundTrip, STRING_CODEC, "null", null, null), - Arguments.of(TestType.Decode, STRING_CODEC, "{\"@bytes\":\"RmF1bmE=\"}", "RmF1bmE=", null) + Arguments.of(TestType.RoundTrip, STRING_CODEC, "\"Fauna\"", + "Fauna", null), + Arguments.of(TestType.RoundTrip, STRING_CODEC, "null", null, + null), + Arguments.of(TestType.Decode, STRING_CODEC, + "{\"@bytes\":\"RmF1bmE=\"}", "RmF1bmE=", null) ); } @ParameterizedTest(name = "StringCodec({index}) -> {0}:{1}:{2}:{3}:{4}") @MethodSource("testCases") - public void string_runTestCases(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void string_runTestCases(TestType testType, + Codec codec, + String wire, + Object obj, + E exception) + throws IOException { runCase(testType, codec, wire, obj, exception); } @@ -35,8 +45,12 @@ public static Stream unsupportedTypeCases() { @ParameterizedTest(name = "StringCodecUnsupportedTypes({index}) -> {0}:{1}") @MethodSource("unsupportedTypeCases") - public void string_runUnsupportedTypeTestCases(String wire, FaunaType type) throws IOException { - var exMsg = MessageFormat.format("Unable to decode `{0}` with `StringCodec`. Supported types for codec are [Bytes, Null, String].", type); - runCase(TestType.Decode, STRING_CODEC, wire, null, new CodecException(exMsg)); + public void string_runUnsupportedTypeTestCases(String wire, FaunaType type) + throws IOException { + var exMsg = MessageFormat.format( + "Unable to decode `{0}` with `StringCodec`. Supported types for codec are [Bytes, Null, String].", + type); + runCase(TestType.Decode, STRING_CODEC, wire, null, + new CodecException(exMsg)); } } diff --git a/src/test/java/com/fauna/codec/codecs/TestBase.java b/src/test/java/com/fauna/codec/codecs/TestBase.java index 78820c63..d4dc48f8 100644 --- a/src/test/java/com/fauna/codec/codecs/TestBase.java +++ b/src/test/java/com/fauna/codec/codecs/TestBase.java @@ -3,10 +3,7 @@ import com.fauna.codec.Codec; import com.fauna.codec.FaunaType; import com.fauna.codec.Helpers; -import com.fauna.exception.ClientException; -import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import java.io.IOException; import java.util.Arrays; @@ -32,11 +29,15 @@ public static Set tags() { public static Stream unsupportedTypeCases(Codec codec) { return Arrays.stream(FaunaType.values()) - .filter(f -> Arrays.stream(codec.getSupportedTypes()).noneMatch(f::equals)) + .filter(f -> Arrays.stream(codec.getSupportedTypes()) + .noneMatch(f::equals)) .map(f -> Arguments.of(Helpers.getWire(f), f)); } - public void runCase(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + public void runCase(TestType testType, + Codec codec, String wire, + Object obj, E exception) + throws IOException { switch (testType) { case RoundTrip: var decodeRoundTrip = Helpers.decode(codec, wire); diff --git a/src/test/java/com/fauna/codec/json/PassThroughDeserializerTest.java b/src/test/java/com/fauna/codec/json/PassThroughDeserializerTest.java deleted file mode 100644 index 3fb76d15..00000000 --- a/src/test/java/com/fauna/codec/json/PassThroughDeserializerTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.fauna.codec.json; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fauna.response.wire.QueryResponseWire; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class PassThroughDeserializerTest { - - @Test - public void deserializeObjectAsRawString() throws JsonProcessingException { - var mapper = new ObjectMapper(); - var json = "{\"error\":{\"code\":\"err\",\"abort\":{\"@int\":42}}}"; - var res = mapper.readValue(json, QueryResponseWire.class); - Assertions.assertEquals("{\"@int\":42}", res.getError().getAbort().get()); - } - - @Test - public void deserializeStringAsRawString() throws JsonProcessingException { - var mapper = new ObjectMapper(); - var json = "{\"error\":{\"code\":\"err\",\"abort\":\"stringy\"}}"; - var res = mapper.readValue(json, QueryResponseWire.class); - Assertions.assertEquals("\"stringy\"", res.getError().getAbort().get()); - } - - @Test - public void deserializeNull() throws JsonProcessingException { - var mapper = new ObjectMapper(); - var json = "{\"error\":{\"code\":\"err\",\"abort\":null}}"; - var res = mapper.readValue(json, QueryResponseWire.class); - Assertions.assertTrue(res.getError().getAbort().isEmpty()); - } -} diff --git a/src/test/java/com/fauna/codec/json/QueryTagsDeserializerTest.java b/src/test/java/com/fauna/codec/json/QueryTagsDeserializerTest.java deleted file mode 100644 index 80c92e0e..00000000 --- a/src/test/java/com/fauna/codec/json/QueryTagsDeserializerTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.fauna.codec.json; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fauna.response.wire.QueryResponseWire; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -public class QueryTagsDeserializerTest { - - @Test - public void deserializeNull() throws JsonProcessingException { - var mapper = new ObjectMapper(); - var json = "{\"query_tags\":null}"; - var res = mapper.readValue(json, QueryResponseWire.class); - Assertions.assertNull(res.getQueryTags()); - } - - @Test - public void deserializeEmptyString() throws JsonProcessingException { - var mapper = new ObjectMapper(); - var json = "{\"query_tags\":\"\"}"; - var res = mapper.readValue(json, QueryResponseWire.class); - Assertions.assertTrue(res.getQueryTags().isEmpty()); - } - - @Test - public void deserializeQueryTags() throws JsonProcessingException { - var mapper = new ObjectMapper(); - var json = "{\"query_tags\":\"foo=bar,baz=foo\"}"; - var res = mapper.readValue(json, QueryResponseWire.class); - Assertions.assertNotNull(res.getQueryTags()); - Assertions.assertEquals(Map.of("foo", "bar", "baz", "foo"), res.getQueryTags()); - } -} diff --git a/src/test/java/com/fauna/codec/json/QueryTagsParsingTest.java b/src/test/java/com/fauna/codec/json/QueryTagsParsingTest.java new file mode 100644 index 00000000..fd53f287 --- /dev/null +++ b/src/test/java/com/fauna/codec/json/QueryTagsParsingTest.java @@ -0,0 +1,41 @@ +package com.fauna.codec.json; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fauna.query.QueryTags; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QueryTagsParsingTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + + @Test + public void deserializeNull() throws IOException { + JsonParser parser = MAPPER.createParser("null"); + QueryTags tags = QueryTags.parse(parser); + assertNull(tags); + } + + @Test + public void deserializeEmptyString() throws IOException { + JsonParser parser = MAPPER.createParser("\"\""); + QueryTags tags = QueryTags.parse(parser); + assertTrue(tags.isEmpty()); + } + + @Test + public void deserializeQueryTags() throws IOException { + JsonParser parser = MAPPER.createParser("\"foo=bar, baz=foo\""); + QueryTags tags = QueryTags.parse(parser); + assertTrue(Set.of("foo", "baz").containsAll(tags.keySet())); + assertEquals("bar", tags.get("foo")); + assertEquals("foo", tags.get("baz")); + } +} diff --git a/src/test/java/com/fauna/configuration/VersionTest.java b/src/test/java/com/fauna/configuration/VersionTest.java index f9c1b073..f01293dc 100644 --- a/src/test/java/com/fauna/configuration/VersionTest.java +++ b/src/test/java/com/fauna/configuration/VersionTest.java @@ -11,7 +11,7 @@ class VersionTest { @Test void getVersion_shouldReturnCorrectVersion() { String version = DriverEnvironment.getVersion(); - assertTrue(version.contains("0.")); + assertTrue(version.contains("1.")); } } diff --git a/src/test/java/com/fauna/e2e/E2EConstraintTest.java b/src/test/java/com/fauna/e2e/E2EConstraintTest.java deleted file mode 100644 index 30b50b0b..00000000 --- a/src/test/java/com/fauna/e2e/E2EConstraintTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.fauna.e2e; - -import com.fauna.client.Fauna; -import com.fauna.client.FaunaClient; -import com.fauna.exception.ConstraintFailureException; -import com.fauna.response.ConstraintFailure; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import static com.fauna.query.builder.Query.fql; -import static java.time.LocalTime.now; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class E2EConstraintTest { - public static final FaunaClient client = Fauna.local(); - - @BeforeAll - public static void setup() { - Fixtures.ProductCollection(client); - } - - @Test - public void checkConstraintFailure() throws IOException { - ConstraintFailureException exc = assertThrows(ConstraintFailureException.class, - () -> client.query(fql("Product.create({name: ${name}, quantity: -1})", Map.of("name", now().toString())))); - - ConstraintFailure actual = exc.getConstraintFailures().get(0); - assertEquals("Document failed check constraint `posQuantity`", actual.getMessage()); - assertTrue(actual.getName().isEmpty()); - assertEquals(0, actual.getPaths().size()); - } - - @Test - public void uniqueConstraintFailure() throws IOException { - client.query(fql("Product.create({name: 'cheese', quantity: 1})")); - - ConstraintFailureException exc = assertThrows(ConstraintFailureException.class, - () -> client.query(fql("Product.create({name: 'cheese', quantity: 2})"))); - - ConstraintFailure actual = exc.getConstraintFailures().get(0); - assertEquals("Failed unique constraint", actual.getMessage()); - assertTrue(actual.getName().isEmpty()); - - var paths = actual.getPaths(); - assertEquals(1, paths.size()); - assertEquals(List.of("name"), paths.get(0)); - } -} diff --git a/src/test/java/com/fauna/e2e/E2EErrorHandlingTest.java b/src/test/java/com/fauna/e2e/E2EErrorHandlingTest.java new file mode 100644 index 00000000..df0fc101 --- /dev/null +++ b/src/test/java/com/fauna/e2e/E2EErrorHandlingTest.java @@ -0,0 +1,103 @@ +package com.fauna.e2e; + +import com.fauna.client.Fauna; +import com.fauna.client.FaunaClient; +import com.fauna.client.FaunaConfig; +import com.fauna.exception.AbortException; +import com.fauna.exception.ConstraintFailureException; +import com.fauna.response.ConstraintFailure; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.SimpleFormatter; + +import static com.fauna.query.builder.Query.fql; +import static java.time.LocalTime.now; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class E2EErrorHandlingTest { + public static FaunaClient client; + + @BeforeAll + public static void setup() throws IOException { + Handler handler = new ConsoleHandler(); + handler.setLevel(Level.FINEST); + handler.setFormatter(new SimpleFormatter()); + client = Fauna.client(FaunaConfig.builder().logHandler(handler) + .endpoint(FaunaConfig.FaunaEndpoint.LOCAL).secret("secret") + .build()); + + Fixtures.ProductCollection(client); + } + + @Test + public void checkConstraintFailure() throws IOException { + ConstraintFailureException exc = + assertThrows(ConstraintFailureException.class, + () -> client.query( + fql("Product.create({name: ${name}, quantity: -1})", + Map.of("name", now().toString())))); + + ConstraintFailure actual = exc.getConstraintFailures()[0]; + assertEquals("Document failed check constraint `posQuantity`", + actual.getMessage()); + assertTrue(actual.getName().isEmpty()); + assertTrue(actual.getPaths().isEmpty()); + } + + @Test + public void uniqueConstraintFailure() throws IOException { + client.query(fql("Product.create({name: 'cheese', quantity: 1})")); + + ConstraintFailureException exc = + assertThrows(ConstraintFailureException.class, + () -> client.query( + fql("Product.create({name: 'cheese', quantity: 2})"), + String.class)); + + ConstraintFailure actual = exc.getConstraintFailures()[0]; + assertEquals("Failed unique constraint", actual.getMessage()); + assertTrue(actual.getName().isEmpty()); + + ConstraintFailure.PathElement[][] paths = actual.getPaths().get(); + assertEquals(1, paths.length); + assertEquals(List.of("name"), actual.getPathStrings().orElseThrow()); + } + + @Test + public void constraintFailureWithInteger() { + ConstraintFailureException exc = + assertThrows(ConstraintFailureException.class, + () -> client.query( + fql("Collection.create({name: \"Foo\", constraints: [{unique: [\"$$$\"] }]})"))); + + ConstraintFailure actual = exc.getConstraintFailures()[0]; + ConstraintFailure expected = ConstraintFailure.builder() + .message("Value `$` is not a valid FQL path expression.") + .path(ConstraintFailure.createPath("constraints", 0, "unique")) + .build(); + assertEquals(expected.getMessage(), actual.getMessage()); + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected, actual); + } + + @Test + public void testAbortAPI() throws IOException { + Instant bigBang = Instant.parse("2019-12-31T23:59:59.999Z"); + AbortException exc = assertThrows(AbortException.class, + () -> client.query( + fql("abort(${bigBang})", Map.of("bigBang", bigBang)))); + assertEquals(999000000, exc.getAbort(Instant.class).getNano()); + assertEquals(Instant.class, exc.getAbort().getClass()); + assertEquals(999000000, ((Instant) exc.getAbort()).getNano()); + } +} diff --git a/src/test/java/com/fauna/e2e/E2EFeedsTest.java b/src/test/java/com/fauna/e2e/E2EFeedsTest.java new file mode 100644 index 00000000..f30c9704 --- /dev/null +++ b/src/test/java/com/fauna/e2e/E2EFeedsTest.java @@ -0,0 +1,171 @@ +package com.fauna.e2e; + +import com.fauna.client.Fauna; +import com.fauna.client.FaunaClient; +import com.fauna.client.FaunaConfig; +import com.fauna.e2e.beans.Product; +import com.fauna.event.EventSource; +import com.fauna.event.FaunaEvent; +import com.fauna.event.FeedIterator; +import com.fauna.event.FeedOptions; +import com.fauna.event.FeedPage; +import com.fauna.exception.InvalidRequestException; +import com.fauna.response.QueryFailure; +import com.fauna.response.QuerySuccess; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.stream.Collectors; + +import static com.fauna.client.FaunaConfig.FaunaEndpoint.LOCAL; +import static com.fauna.event.FaunaEvent.EventType.ERROR; +import static com.fauna.query.builder.Query.fql; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class E2EFeedsTest { + + public static FaunaClient client; + public static Long productCollectionTs; + + @BeforeAll + public static void setup() { + Handler handler = new ConsoleHandler(); + handler.setLevel(Level.FINEST); + FaunaConfig config = + FaunaConfig.builder().endpoint(LOCAL).secret("secret").build(); + client = Fauna.client(config); + productCollectionTs = Fixtures.ProductCollection(client); + } + + @Test + public void feedOfAll() { + FeedOptions options = + FeedOptions.builder().startTs(productCollectionTs).build(); + FeedIterator iter = + client.feed(fql("Product.all().eventSource()"), options, + Product.class); + List>> pages = new ArrayList<>(); + iter.forEachRemaining(page -> pages.add(page.getEvents())); + assertEquals(4, pages.size()); + List> products = + pages.stream().flatMap(p -> p.stream()) + .collect(Collectors.toList()); + assertEquals(50, products.size()); + } + + @Test + public void manualFeed() { + // Use the feeds API with complete (i.e. manual) control of the calls made to Fauna. + QuerySuccess sourceQuery = + client.query(fql("Product.all().eventSource()"), + EventSource.class); + EventSource source = sourceQuery.getData(); + List> productUpdates = new ArrayList<>(); + FeedOptions initialOptions = + FeedOptions.builder().startTs(productCollectionTs).pageSize(2) + .build(); + CompletableFuture> pageFuture = + client.poll(source, initialOptions, Product.class); + int pageCount = 0; + String lastPageCursor = null; + + while (pageFuture != null) { + // Handle page + FeedPage latestPage = pageFuture.join(); + lastPageCursor = latestPage.getCursor(); + productUpdates.addAll(latestPage.getEvents()); + pageCount++; + + // Get next page (if it's not null) + FeedOptions nextPageOptions = initialOptions.nextPage(latestPage); + // You can also inspect next + if (latestPage.hasNext()) { + pageFuture = + client.poll(source, nextPageOptions, Product.class); + } else { + pageFuture = null; + } + } + assertEquals(50, productUpdates.size()); + assertEquals(25, pageCount); + // Because there is no filtering, these cursors are the same. + // If we filtered events, then the page cursor could be different from the cursor of the last element. + assertEquals(lastPageCursor, + productUpdates.get(productUpdates.size() - 1).getCursor()); + + } + + @Test + public void feedError() { + // Fauna can throw a HTTP error in some cases. In this case it's bad request to the feed API. Presumably + // some of the others like ThrottlingException, and AuthenticationException can also be thrown. + CompletableFuture> future = + client.poll(EventSource.fromToken("badToken"), + FeedOptions.DEFAULT, Product.class); + CompletionException ce = + assertThrows(CompletionException.class, () -> future.join()); + InvalidRequestException ire = (InvalidRequestException) ce.getCause(); + + assertEquals("invalid_request", ire.getErrorCode()); + assertEquals(400, ire.getStatusCode()); + assertEquals( + "400 (invalid_request): Invalid request body: invalid event source provided.", + ire.getMessage()); + assertTrue(ire.getTxnTs().isEmpty()); + assertNull(ire.getStats()); + assertNull(ire.getSchemaVersion()); + assertNull(ire.getSummary()); + assertNull(ire.getCause()); + assertNull(ire.getQueryTags()); + + QueryFailure failure = ire.getResponse(); + assertTrue(failure.getConstraintFailures().isEmpty()); + assertTrue(failure.getAbort(null).isEmpty()); + } + + @Test + public void feedEventError() { + // Fauna can also return a valid feed page, with HTTP 200, but an "error" event type. + FeedOptions options = FeedOptions.builder().startTs(0L).build(); + QuerySuccess sourceQuery = + client.query(fql("Product.all().eventSource()"), EventSource.class); + FeedIterator iter = client.feed(sourceQuery.getData(), options, Product.class); + FeedPage pageOne = iter.next(); + assertFalse(pageOne.hasNext()); + assertEquals(1, pageOne.getEvents().size()); + FaunaEvent errorEvent = pageOne.getEvents().get(0); + assertEquals(ERROR, errorEvent.getType()); + assertEquals("invalid_stream_start_time", + errorEvent.getError().getCode()); + assertTrue(errorEvent.getError().getMessage() + .contains("is too far in the past")); + } + + @Test + public void feedFlattened() { + FeedOptions options = + FeedOptions.builder().startTs(productCollectionTs).build(); + FeedIterator iter = + client.feed(fql("Product.all().eventSource()"), options, + Product.class); + Iterator> productIter = iter.flatten(); + List> products = new ArrayList<>(); + // Java iterators not being iterable (or useable in a for-each loop) is annoying. + for (FaunaEvent p : (Iterable>) () -> productIter) { + products.add(p); + } + assertEquals(50, products.size()); + } +} diff --git a/src/test/java/com/fauna/e2e/E2EPaginationTest.java b/src/test/java/com/fauna/e2e/E2EPaginationTest.java index c03e79b4..c1da340b 100644 --- a/src/test/java/com/fauna/e2e/E2EPaginationTest.java +++ b/src/test/java/com/fauna/e2e/E2EPaginationTest.java @@ -2,26 +2,25 @@ import com.fauna.client.Fauna; import com.fauna.client.FaunaClient; +import com.fauna.client.FaunaConfig; import com.fauna.client.PageIterator; +import com.fauna.codec.PageOf; import com.fauna.e2e.beans.Product; import com.fauna.response.QuerySuccess; -import com.fauna.codec.PageOf; +import com.fauna.types.Document; import com.fauna.types.Page; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; - import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import java.util.stream.Collectors; import static com.fauna.query.builder.Query.fql; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -36,39 +35,63 @@ public static void setup() { @Test public void query_single_item_gets_wrapped_in_page() { - PageIterator iter = client.paginate(fql("Product.firstWhere(.name == 'product-1')"), Product.class); + PageIterator iter = + client.paginate(fql("Product.firstWhere(.name == 'product-1')"), + Product.class); assertTrue(iter.hasNext()); Page page = iter.next(); assertEquals(1, page.getData().size()); - assertNull(page.getAfter()); + assertTrue(page.getAfter().isEmpty()); + assertFalse(iter.hasNext()); + assertThrows(NoSuchElementException.class, iter::next); + } + + @Test + public void query_single_object_gets_wrapped_in_page() { + PageIterator iter = client.paginate( + fql("Product.firstWhere(.name == 'product-1')")); + assertTrue(iter.hasNext()); + // We didn't pass in a type, so the client returns Page + Page page = iter.next(); + assertEquals(1, page.getData().size()); + // In this case the "Object" is actually a Document, so we can cast it. + Document document = (Document) page.getData().get(0); + assertEquals("product-1", document.get("name")); + assertTrue(page.getAfter().isEmpty()); assertFalse(iter.hasNext()); assertThrows(NoSuchElementException.class, iter::next); } @Test public void query_single_page_gets_wrapped_in_page() { - PageIterator iter = client.paginate(fql("Product.where(.quantity < 8)"), Product.class); + PageIterator iter = + client.paginate(fql("Product.where(.quantity < 8)"), + Product.class); assertTrue(iter.hasNext()); Page page = iter.next(); assertEquals(8, page.getData().size()); - assertNull(page.getAfter()); + assertTrue(page.getAfter().isEmpty()); assertFalse(iter.hasNext()); assertThrows(NoSuchElementException.class, iter::next); } @Test public void query_all_with_manual_pagination() { - // Demonstrate how a user could paginate without the paginate API. + // Demonstrate how a user could paginate without PageIterator. PageOf pageOf = new PageOf<>(Product.class); - QuerySuccess> first = client.query(fql("Product.all()"), pageOf); + QuerySuccess> first = + client.query(fql("Product.all()"), pageOf); Page latest = first.getData(); List> pages = new ArrayList<>(); pages.add(latest.getData()); - while (latest.getAfter() != null) { - QuerySuccess> paged = client.query(fql("Set.paginate(${after})", Map.of("after", latest.getAfter())), pageOf); - latest = paged.getData(); - pages.add(latest.getData()); + while (latest != null) { + latest = latest.getAfter().map(after -> { + Page page = + client.queryPage(after, Product.class, null).getData(); + pages.add(page.getData()); + return page; + }).orElse(null); } assertEquals(4, pages.size()); assertEquals(2, pages.get(3).size()); @@ -76,17 +99,21 @@ public void query_all_with_manual_pagination() { @Test public void query_all_with_pagination() { - PageIterator iter = client.paginate(fql("Product.all()"), Product.class); + PageIterator iter = + client.paginate(fql("Product.all()"), Product.class); List> pages = new ArrayList<>(); iter.forEachRemaining(pages::add); assertEquals(4, pages.size()); - List products = pages.stream().flatMap(p -> p.getData().stream()).collect(Collectors.toList()); + List products = + pages.stream().flatMap(p -> p.getData().stream()) + .collect(Collectors.toList()); assertEquals(50, products.size()); } @Test public void query_all_flattened() { - PageIterator iter = client.paginate(fql("Product.all()"), Product.class); + PageIterator iter = + client.paginate(fql("Product.all()"), Product.class); Iterator productIter = iter.flatten(); List products = new ArrayList<>(); // Java iterators not being iterable (or useable in a for-each loop) is annoying. @@ -95,4 +122,39 @@ public void query_all_flattened() { } assertEquals(50, products.size()); } + + @Test + public void query_statsAreTrackedForExplicitPagination() { + var cfg = FaunaConfig.builder() + .secret("secret") + .endpoint("http://localhost:8443") + .build(); + var client = Fauna.client(cfg); + PageIterator iter = + client.paginate(fql("Product.all()"), Product.class); + iter.forEachRemaining(page -> { + }); + + var stats = client.getStatsCollector().read(); + assertEquals(82, stats.getReadOps()); + assertEquals(4, stats.getComputeOps()); + } + + @Test + public void query_statsAreTrackedForFlattenedPagination() { + var cfg = FaunaConfig.builder() + .secret("secret") + .endpoint("http://localhost:8443") + .build(); + var client = Fauna.client(cfg); + PageIterator iter = + client.paginate(fql("Product.all()"), Product.class); + Iterator productIter = iter.flatten(); + for (Product p : (Iterable) () -> productIter) { + } + + var stats = client.getStatsCollector().read(); + assertEquals(82, stats.getReadOps()); + assertEquals(4, stats.getComputeOps()); + } } diff --git a/src/test/java/com/fauna/e2e/E2EQueryArrTest.java b/src/test/java/com/fauna/e2e/E2EQueryArrTest.java index 0020c06b..9faa1da4 100644 --- a/src/test/java/com/fauna/e2e/E2EQueryArrTest.java +++ b/src/test/java/com/fauna/e2e/E2EQueryArrTest.java @@ -24,7 +24,7 @@ public void query_listWithEmbeddedQueries() { Query q = fql("${obj}", Map.of("obj", obj)); QuerySuccess> res = c.query(q, listOf(Integer.class)); - assertEquals(List.of(4,6,8), res.getData()); + assertEquals(List.of(4, 6, 8), res.getData()); } @Test @@ -34,6 +34,6 @@ public void query_listWithNestedEmbeddedQueries() { Query q = fql("${obj}", Map.of("obj", obj)); var res = c.query(q); - assertEquals(List.of(List.of(4,6,8)), res.getData()); + assertEquals(List.of(List.of(4, 6, 8)), res.getData()); } } diff --git a/src/test/java/com/fauna/e2e/E2EQueryObjTest.java b/src/test/java/com/fauna/e2e/E2EQueryObjTest.java index 30e20cac..2477b2bf 100644 --- a/src/test/java/com/fauna/e2e/E2EQueryObjTest.java +++ b/src/test/java/com/fauna/e2e/E2EQueryObjTest.java @@ -24,7 +24,8 @@ public void query_mapWithEmbeddedQuery() { Map obj = Map.of("k", subq); Query q = fql("${obj}", Map.of("obj", QueryObj.of(obj))); - QuerySuccess> res = c.query(q, mapOf(Integer.class)); + QuerySuccess> res = + c.query(q, mapOf(Integer.class)); assertEquals(Map.of("k", 6), res.getData()); } @@ -32,10 +33,12 @@ public void query_mapWithEmbeddedQuery() { @Test public void query_mapMixedWithEmbeddedQuery() { Query subq = fql("4 + 2"); - Map obj = Map.of("k", subq, "k2", new QueryVal<>(42)); + Map obj = + Map.of("k", subq, "k2", new QueryVal<>(42)); Query q = fql("${obj}", Map.of("obj", QueryObj.of(obj))); - QuerySuccess> res = c.query(q, mapOf(Integer.class)); + QuerySuccess> res = + c.query(q, mapOf(Integer.class)); assertEquals(Map.of("k", 6, "k2", 42), res.getData()); } diff --git a/src/test/java/com/fauna/e2e/E2EQueryTest.java b/src/test/java/com/fauna/e2e/E2EQueryTest.java index f832ead8..96c08f7b 100644 --- a/src/test/java/com/fauna/e2e/E2EQueryTest.java +++ b/src/test/java/com/fauna/e2e/E2EQueryTest.java @@ -2,8 +2,11 @@ import com.fauna.client.Fauna; import com.fauna.client.FaunaClient; +import com.fauna.client.FaunaConfig; import com.fauna.e2e.beans.Author; import com.fauna.exception.AbortException; +import com.fauna.exception.QueryRuntimeException; +import com.fauna.exception.QueryTimeoutException; import com.fauna.query.QueryOptions; import com.fauna.query.builder.Query; import com.fauna.response.QuerySuccess; @@ -14,18 +17,20 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.time.Duration; +import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; -import static com.fauna.codec.Generic.nullableDocumentOf; -import static com.fauna.query.builder.Query.fql; import static com.fauna.codec.Generic.listOf; import static com.fauna.codec.Generic.mapOf; -import static com.fauna.codec.Generic.pageOf; +import static com.fauna.codec.Generic.nullableDocumentOf; import static com.fauna.codec.Generic.optionalOf; +import static com.fauna.codec.Generic.pageOf; +import static com.fauna.query.builder.Query.fql; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; @@ -48,6 +53,36 @@ public void query_sync() { assertEquals(exp, res.getData()); } + @Test + public void clientTransactionTsOnSuccess() { + FaunaClient client = Fauna.local(); + assertTrue(client.getLastTransactionTs().isEmpty()); + client.query(fql("42")); + long y2k = Instant.parse("1999-12-31T23:59:59.99Z").getEpochSecond() * + 1_000_000; + assertTrue(client.getLastTransactionTs().orElseThrow() > y2k); + } + + @Test + public void clientTransactionTsOnFailure() { + FaunaClient client = Fauna.local(); + assertTrue(client.getLastTransactionTs().isEmpty()); + assertThrows(QueryRuntimeException.class, + () -> client.query(fql("NonExistantCollection.all()"))); + long y2k = Instant.parse("1999-12-31T23:59:59.99Z").getEpochSecond() * + 1_000_000; + assertTrue(client.getLastTransactionTs().orElseThrow() > y2k); + } + + @Test + public void queryTimeout() { + QueryOptions opts = + QueryOptions.builder().timeout(Duration.ofMillis(1)).build(); + QueryTimeoutException exc = assertThrows(QueryTimeoutException.class, + () -> c.query(fql("Author.all()"), listOf(Author.class), opts)); + assertTrue(exc.getMessage().contains("Client set aggressive deadline")); + } + @Test public void query_syncWithClass() { var q = fql("42"); @@ -68,7 +103,7 @@ public void query_syncWithParameterized() { @Test public void query_syncWithClassAndOptions() { var q = fql("42"); - var res = c.query(q, int.class, QueryOptions.builder().build()); + var res = c.query(q, int.class, QueryOptions.getDefault()); var exp = 42; assertEquals(exp, res.getData()); } @@ -76,7 +111,7 @@ public void query_syncWithClassAndOptions() { @Test public void query_syncWithParameterizedAndOptions() { var q = fql("[42]"); - var res = c.query(q, listOf(int.class), QueryOptions.builder().build()); + var res = c.query(q, listOf(int.class), QueryOptions.getDefault()); var exp = List.of(42); assertEquals(exp, res.getData()); } @@ -90,7 +125,8 @@ public void query_async() throws ExecutionException, InterruptedException { } @Test - public void query_asyncWithClass() throws ExecutionException, InterruptedException { + public void query_asyncWithClass() + throws ExecutionException, InterruptedException { var q = fql("42"); var res = c.asyncQuery(q, int.class).get(); var exp = 42; @@ -98,7 +134,8 @@ public void query_asyncWithClass() throws ExecutionException, InterruptedExcepti } @Test - public void query_asyncWithParameterized() throws ExecutionException, InterruptedException { + public void query_asyncWithParameterized() + throws ExecutionException, InterruptedException { var q = fql("[42]"); var res = c.asyncQuery(q, listOf(int.class)).get(); var exp = List.of(42); @@ -106,17 +143,20 @@ public void query_asyncWithParameterized() throws ExecutionException, Interrupte } @Test - public void query_asyncWithClassAndOptions() throws ExecutionException, InterruptedException { + public void query_asyncWithClassAndOptions() + throws ExecutionException, InterruptedException { var q = fql("42"); - var res = c.asyncQuery(q, int.class, QueryOptions.builder().build()).get(); + var res = c.asyncQuery(q, int.class, QueryOptions.getDefault()).get(); var exp = 42; assertEquals(exp, res.getData()); } @Test - public void query_asyncWithParameterizedAndOptions() throws ExecutionException, InterruptedException { + public void query_asyncWithParameterizedAndOptions() + throws ExecutionException, InterruptedException { var q = fql("[42]"); - var res = c.asyncQuery(q, listOf(int.class), QueryOptions.builder().build()).get(); + var res = + c.asyncQuery(q, listOf(int.class), QueryOptions.getDefault()).get(); var exp = List.of(42); assertEquals(exp, res.getData()); } @@ -135,11 +175,13 @@ public void query_arrayOfPersonIncoming() { @Test public void query_arrayOfPersonOutgoing() { - var q = fql("${var}", Map.of("var", List.of(new Author("alice","smith","w", 42)))); + var q = fql("${var}", + Map.of("var", List.of(new Author("alice", "smith", "w", 42)))); var res = c.query(q); - List> elem = (List>) res.getData(); + List> elem = + (List>) res.getData(); assertEquals("alice", elem.get(0).get("firstName")); } @@ -172,7 +214,7 @@ public void query_pageOfPerson() { @Test public void query_optionalNull() { var empty = Optional.empty(); - var q = fql("${empty}", new HashMap<>(){{ + var q = fql("${empty}", new HashMap<>() {{ put("empty", empty); }}); @@ -185,7 +227,7 @@ public void query_optionalNull() { @Test public void query_optionalNotNull() { var val = Optional.of(42); - var q = fql("${val}", new HashMap<>(){{ + var q = fql("${val}", new HashMap<>() {{ put("val", val); }}); @@ -203,7 +245,7 @@ public void query_nullableOf() { var qs = c.query(q, nullableDocumentOf(Author.class)); NullableDocument actual = qs.getData(); assertInstanceOf(NullDocument.class, actual); - assertEquals("not found", ((NullDocument)actual).getCause()); + assertEquals("not found", ((NullDocument) actual).getCause()); } @Test @@ -212,14 +254,16 @@ public void query_nullableOfNotNull() { var qs = c.query(q, nullableDocumentOf(Author.class)); NullableDocument actual = qs.getData(); assertInstanceOf(NonNullDocument.class, actual); - assertEquals("Alice", ((NonNullDocument)actual).getValue().getFirstName()); + assertEquals("Alice", + ((NonNullDocument) actual).getValue().getFirstName()); } @Test - public void query_abortEmpty() throws IOException { + public void query_abortNull() throws IOException { var q = fql("abort(null)"); var e = assertThrows(AbortException.class, () -> c.query(q)); assertNull(e.getAbort()); + assertNull(e.getAbort(Author.class)); } @Test @@ -235,4 +279,46 @@ public void query_abortClass() throws IOException { var e = assertThrows(AbortException.class, () -> c.query(q)); assertEquals("alice", e.getAbort(Author.class).getFirstName()); } + + @Test + public void query_withTags() { + QuerySuccess> success = c.query( + fql("Author.byId('9090090')"), + nullableDocumentOf(Author.class), QueryOptions.builder() + .queryTag("first", "1") + .queryTag("second", "2").build()); + assertEquals("1", success.getQueryTags().get("first")); + assertEquals("2", success.getQueryTags().get("second")); + } + + @Test + public void query_trackStatsOnSuccess() { + var cfg = FaunaConfig.builder() + .secret("secret") + .endpoint("http://localhost:8443") + .build(); + var client = Fauna.client(cfg); + + var q = fql("Author.all().toArray()"); + + client.query(q, listOf(Author.class)); + var stats = client.getStatsCollector().read(); + assertEquals(10, stats.getReadOps()); + assertEquals(1, stats.getComputeOps()); + } + + @Test + public void query_trackStatsOnFailure() throws IOException { + var cfg = FaunaConfig.builder() + .secret("secret") + .endpoint("http://localhost:8443") + .build(); + var client = Fauna.client(cfg); + + var q = fql("Author.all().toArray()\nabort(null)"); + assertThrows(AbortException.class, () -> client.query(q)); + var stats = client.getStatsCollector().read(); + assertEquals(8, stats.getReadOps()); + assertEquals(1, stats.getComputeOps()); + } } diff --git a/src/test/java/com/fauna/e2e/E2EScopedTest.java b/src/test/java/com/fauna/e2e/E2EScopedTest.java index 789482a0..a6fb9744 100644 --- a/src/test/java/com/fauna/e2e/E2EScopedTest.java +++ b/src/test/java/com/fauna/e2e/E2EScopedTest.java @@ -10,7 +10,8 @@ public class E2EScopedTest { public static final FaunaClient baseClient = Fauna.local(); - public static final FaunaClient scopedClient = Fauna.scoped(baseClient, "People"); + public static final FaunaClient scopedClient = + Fauna.scoped(baseClient, "People"); @BeforeAll public static void setup() { diff --git a/src/test/java/com/fauna/e2e/E2EStreamingTest.java b/src/test/java/com/fauna/e2e/E2EStreamingTest.java index ae95aeec..47f0018d 100644 --- a/src/test/java/com/fauna/e2e/E2EStreamingTest.java +++ b/src/test/java/com/fauna/e2e/E2EStreamingTest.java @@ -1,35 +1,54 @@ package com.fauna.e2e; +import com.fauna.beans.InventorySource; import com.fauna.client.Fauna; import com.fauna.client.FaunaClient; -import com.fauna.client.FaunaStream; +import com.fauna.client.PageIterator; +import com.fauna.client.QueryStatsSummary; import com.fauna.e2e.beans.Product; +import com.fauna.event.EventSource; +import com.fauna.event.FaunaEvent; +import com.fauna.event.FaunaStream; +import com.fauna.event.StreamOptions; +import com.fauna.exception.ClientException; +import com.fauna.exception.NullDocumentException; +import com.fauna.query.QueryOptions; import com.fauna.query.builder.Query; -import com.fauna.response.StreamEvent; +import com.fauna.response.QuerySuccess; +import com.fauna.types.NullableDocument; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.net.http.HttpTimeoutException; import java.text.MessageFormat; +import java.time.Duration; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.fauna.codec.Generic.nullableDocumentOf; import static com.fauna.query.builder.Query.fql; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class E2EStreamingTest { public static final FaunaClient client = Fauna.local(); private static final Random random = new Random(); - private static final String candidateChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + private static final String candidateChars = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; @BeforeAll public static void setup() { @@ -38,23 +57,54 @@ public static void setup() { public static String randomName(int length) { return Stream.generate( - () -> candidateChars.charAt(random.nextInt(candidateChars.length()))).map( - c -> Character.toString(c)).limit(length).collect(Collectors.joining()); + () -> candidateChars.charAt( + random.nextInt(candidateChars.length()))).map( + c -> Character.toString(c)).limit(length) + .collect(Collectors.joining()); } public static Query createProduct() { return fql("Product.create({name: ${name}, quantity: ${quantity}})", - Map.of("name", randomName(5), "quantity", random.nextInt(1, 16))); + Map.of("name", randomName(5), "quantity", + random.nextInt(1, 16))); } + public static void waitForSync(InventorySubscriber watcher) + throws InterruptedException { + long start = System.currentTimeMillis(); + int events = watcher.countEvents(); + while (System.currentTimeMillis() < start + 2_000) { + Thread.sleep(10); + int latest = watcher.countEvents(); + if (latest > events) { + events = latest; + } + } + + } - static class InventorySubscriber implements Flow.Subscriber> { + private static Long doDelete(String name) { + Query query = fql("Product.firstWhere(.name == ${name})!.delete()", + Map.of("name", name)); + QuerySuccess> success = + client.query(query, nullableDocumentOf(Product.class)); + NullableDocument nullDoc = success.getData(); + assertThrows(NullDocumentException.class, () -> nullDoc.get()); + return success.getLastSeenTxn(); + } + + static class InventorySubscriber + implements Flow.Subscriber> { private final AtomicLong timestamp = new AtomicLong(0); private String cursor = null; private final AtomicInteger events = new AtomicInteger(0); - Map inventory = new ConcurrentHashMap<>(); + private final Map inventory; Flow.Subscription subscription; + public InventorySubscriber(Map inventory) { + this.inventory = inventory; + } + @Override public void onSubscribe(Flow.Subscription subscription) { this.subscription = subscription; @@ -62,12 +112,20 @@ public void onSubscribe(Flow.Subscription subscription) { } @Override - public void onNext(StreamEvent event) { - System.out.println(MessageFormat.format("Event {0}, {1}", event.getCursor(), event.getTimestamp().orElse(-1L))); + public void onNext(FaunaEvent event) { events.addAndGet(1); - event.getData().ifPresent(product -> inventory.put(product.getName(), product.getQuantity())); + if (event.getType().equals(FaunaEvent.EventType.ADD) || + event.getType().equals(FaunaEvent.EventType.UPDATE)) { + event.getData().ifPresent( + product -> inventory.put(product.getName(), + product.getQuantity())); + } else if (event.getType().equals(FaunaEvent.EventType.REMOVE)) { + event.getData().ifPresent( + product -> inventory.remove(product.getName())); + } // Fauna delivers events to the client in order, but it's up to the user to keep those events in order. - event.getTimestamp().ifPresent(ts -> this.timestamp.updateAndGet(value -> value < ts ? value : ts)); + event.getTimestamp().ifPresent(ts -> this.timestamp.updateAndGet( + value -> value < ts ? value : ts)); this.cursor = event.getCursor(); System.out.println("Total inventory: " + this.countInventory()); this.subscription.request(1); @@ -96,42 +154,144 @@ public int countEvents() { public String status() { return MessageFormat.format( "Processed {0} events, inventory {1} at cursor/timestamp: {2}/{3}", - countEvents(), countInventory(), this.cursor, this.timestamp.get()); + countEvents(), countInventory(), this.cursor, + this.timestamp.get()); } } @Test public void query_streamOfProduct() throws InterruptedException { - FaunaStream stream = client.stream(fql("Product.all().toStream()"), Product.class); - InventorySubscriber inventory = new InventorySubscriber(); - stream.subscribe(inventory); + QueryStatsSummary initialStats = client.getStatsCollector().read(); + // Initialize stream outside try-with-resources so we can assert that it's closed at the end of this test;. + FaunaStream stream = + client.stream(fql("Product.all().eventSource()"), + Product.class); + try (stream) { + InventorySubscriber watcher = + new InventorySubscriber(new ConcurrentHashMap<>()); + stream.subscribe(watcher); + assertFalse(stream.isClosed()); + + doDelete("product-2"); + + waitForSync(watcher); + watcher.onComplete(); + assertFalse(stream.isClosed()); + } + stream.close(); + assertTrue(stream.isClosed()); + } + + @Test + public void query_trackStateWithStream() throws InterruptedException { + // This test demonstrates querying the current state of a collection, and then tracking the changes from + // that moment on, which guarantees that no updates will be missed. + QueryStatsSummary initialStats = client.getStatsCollector().read(); + QuerySuccess success = client.query( + fql("let products = Product.all()\n{ firstPage: products.pageSize(4), eventSource: products.eventSource()}"), + InventorySource.class); + InventorySource inventorySource = success.getData(); + + // First, we process all the products that existed when the query was made. + PageIterator pageIterator = + new PageIterator<>(client, inventorySource.firstPage, + Product.class, QueryOptions.getDefault()); + Map inventory = new HashMap<>(); List products = new ArrayList<>(); - products.add(client.query(fql("Product.create({name: 'cheese', quantity: 1})"), Product.class).getData()); - products.add(client.query(fql("Product.create({name: 'bread', quantity: 2})"), Product.class).getData()); - products.add(client.query(fql("Product.create({name: 'wine', quantity: 3})"), Product.class).getData()); + pageIterator.flatten().forEachRemaining(product -> { + products.add(product); + inventory.put(product.getName(), product.getQuantity()); + }); + + // Now start a stream based on same query, and it's transaction timestamp. + EventSource eventSource = inventorySource.eventSource; + StreamOptions streamOptions = + StreamOptions.builder().startTimestamp(success.getLastSeenTxn()) + .build(); + FaunaStream stream = + client.stream(eventSource, streamOptions, Product.class); + try (stream) { + InventorySubscriber watcher = new InventorySubscriber(inventory); + stream.subscribe(watcher); + assertFalse(stream.isClosed()); + products.add(client.query( + fql("Product.create({name: 'bread', quantity: 2})"), + Product.class).getData()); + products.add(client.query( + fql("Product.firstWhere(.name == \"product-0\")!.update({quantity: 30})"), + Product.class).getData()); + waitForSync(watcher); + int before = watcher.countInventory(); + + client.query(fql("Product.create({name: 'cheese', quantity: 17})"), + Product.class).getData(); + waitForSync(watcher); + assertEquals(before + 17, watcher.countInventory()); + + doDelete("cheese"); + waitForSync(watcher); + assertEquals(before, watcher.countInventory()); + + watcher.onComplete(); + Integer total = products.stream().map(Product::getQuantity) + .reduce(0, Integer::sum); + assertEquals(total, watcher.countInventory()); + assertFalse(stream.isClosed()); + } + stream.close(); + assertTrue(stream.isClosed()); + + QueryStatsSummary finalStats = client.getStatsCollector().read(); + assertTrue(finalStats.getReadOps() > initialStats.getReadOps()); + assertTrue(finalStats.getComputeOps() > initialStats.getComputeOps()); + } + + @Test + public void handleStreamError() throws InterruptedException { + // It would be nice to have another test that generates a stream with normal events, and then an error + // event, but this at least tests some of the functionality. + QuerySuccess queryResp = + client.query(fql("Product.all().eventSource()"), + EventSource.class); + EventSource source = queryResp.getData(); + StreamOptions options = + StreamOptions.builder().cursor("invalid_cursor").build(); + FaunaStream stream = + client.stream(source, options, Product.class); + InventorySubscriber inventory = + new InventorySubscriber(new ConcurrentHashMap<>()); + stream.subscribe(inventory); long start = System.currentTimeMillis(); - int events = inventory.countEvents(); - System.out.println("Events: " + events); - while (System.currentTimeMillis() < start + 2_000) { - Thread.sleep(10); - int latest = inventory.countEvents(); - if (latest > events) { - events = latest; - } + while (!stream.isClosed() && + System.currentTimeMillis() < (start + 5_000)) { + Thread.sleep(100); } - inventory.onComplete(); - System.out.println(inventory.status()); - Integer total = products.stream().map(Product::getQuantity).reduce(0, Integer::sum); - assertEquals(total, inventory.countInventory()); + assertTrue(stream.isClosed()); + } + + @Test + public void handleStreamTimeout() { + QuerySuccess queryResp = client.query( + fql("Product.all().eventSource()"), EventSource.class); + StreamOptions options = + StreamOptions.builder().timeout(Duration.ofMillis(1)).build(); + ClientException exc = assertThrows(ClientException.class, + () -> client.stream(queryResp.getData(), options, Product.class)); + assertEquals(ExecutionException.class, exc.getCause().getClass()); + assertEquals(HttpTimeoutException.class, + exc.getCause().getCause().getClass()); } @Disabled("Will fix this for GA, I think the other drivers have this bug too.") @Test public void handleLargeEvents() throws InterruptedException { - FaunaStream stream = client.stream(fql("Product.all().toStream()"), Product.class); - InventorySubscriber inventory = new InventorySubscriber(); - stream.subscribe(inventory); + InventorySubscriber inventory; + try (FaunaStream stream = client.stream( + fql("Product.all().eventSource()"), Product.class)) { + inventory = new InventorySubscriber(new ConcurrentHashMap<>()); + stream.subscribe(inventory); + } List products = new ArrayList<>(); byte[] image = new byte[20]; @@ -139,23 +299,29 @@ public void handleLargeEvents() throws InterruptedException { // Product cheese = new Product("cheese", 1, image); StringBuilder fifteenKName = new StringBuilder(); for (int i = 0; i < 1024 * 15; i++) { - fifteenKName.append(candidateChars.charAt(random.nextInt(candidateChars.length()))); + fifteenKName.append(candidateChars.charAt( + random.nextInt(candidateChars.length()))); } assertEquals(fifteenKName.length(), 15360); // 15k string works. - products.add(client.query(fql("Product.create({name: ${name}, quantity: 1})", - Map.of("name", fifteenKName.toString())), Product.class).getData()); + products.add( + client.query(fql("Product.create({name: ${name}, quantity: 1})", + Map.of("name", fifteenKName.toString())), Product.class) + .getData()); StringBuilder sixteenKName = new StringBuilder(); for (int i = 0; i < 1024 * 16; i++) { - sixteenKName.append(candidateChars.charAt(random.nextInt(candidateChars.length()))); + sixteenKName.append(candidateChars.charAt( + random.nextInt(candidateChars.length()))); } assertEquals(sixteenKName.length(), 16384); // 16k string causes the stream to throw. // FaunaStream onError: com.fasterxml.jackson.databind.JsonMappingException: Unexpected end-of-input: was // expecting closing quote for a string value at [Source: ... - products.add(client.query(fql("Product.create({name: ${name}, quantity: 1})", - Map.of("name", sixteenKName.toString())), Product.class).getData()); + products.add( + client.query(fql("Product.create({name: ${name}, quantity: 1})", + Map.of("name", sixteenKName.toString())), Product.class) + .getData()); long start = System.currentTimeMillis(); int events = inventory.countEvents(); @@ -169,15 +335,18 @@ public void handleLargeEvents() throws InterruptedException { } inventory.onComplete(); System.out.println(inventory.status()); - Integer total = products.stream().map(Product::getQuantity).reduce(0, Integer::sum); + Integer total = products.stream().map(Product::getQuantity) + .reduce(0, Integer::sum); assertEquals(total, inventory.countInventory()); } @Disabled("This test sometimes causes Fauna to generate an error for getting too far behind.") @Test public void handleManyEvents() throws InterruptedException { - FaunaStream stream = client.stream(fql("Product.all().toStream()"), Product.class); - InventorySubscriber inventory = new InventorySubscriber(); + FaunaStream stream = client.stream(fql("Product.all().eventSource()"), + Product.class); + InventorySubscriber inventory = + new InventorySubscriber(new ConcurrentHashMap<>()); stream.subscribe(inventory); List> productFutures = new ArrayList<>(); @@ -186,10 +355,13 @@ public void handleManyEvents() throws InterruptedException { // at [Source: (String)"{"type":"error","error":{"code":"stream_overflow","message":"Too many events to process."},"stats":{"read_ops":0,"storage_bytes_read":0,"compute_ops":0,"processing_time_ms":0,"rate_limits_hit":[]}} //"; line: 1, column: 26] (through reference chain: com.fauna.response.wire.StreamEventWire["error"]) Stream.generate(E2EStreamingTest::createProduct).limit(10_000).forEach( - fql -> productFutures.add(client.asyncQuery(fql, Product.class).thenApply(success -> success.getData()))); - Thread.sleep(10_000); + fql -> productFutures.add(client.asyncQuery(fql, Product.class) + .thenApply(success -> success.getData()))); + Thread.sleep(60_000); - int totalInventory = productFutures.stream().map(p -> p.join().getQuantity()).reduce(0, Integer::sum); + int totalInventory = + productFutures.stream().map(p -> p.join().getQuantity()) + .reduce(0, Integer::sum); assertEquals(totalInventory, inventory.countInventory()); } diff --git a/src/test/java/com/fauna/e2e/Fixtures.java b/src/test/java/com/fauna/e2e/Fixtures.java index a1808ced..bfe73f54 100644 --- a/src/test/java/com/fauna/e2e/Fixtures.java +++ b/src/test/java/com/fauna/e2e/Fixtures.java @@ -10,23 +10,32 @@ public class Fixtures { public static void PeopleDatabase(FaunaClient client) { - client.asyncQuery(fql("Database.byName('People')?.delete()")).exceptionally(t -> null).join(); + client.asyncQuery(fql("Database.byName('People')?.delete()")) + .exceptionally(t -> null).join(); client.query(fql("Database.create({name: 'People'})")); } public static void PersonCollection(FaunaClient client) { - client.asyncQuery(fql("Collection.byName('Author')?.delete()")).exceptionally(t -> null).join(); + client.asyncQuery(fql("Collection.byName('Author')?.delete()")) + .exceptionally(t -> null).join(); client.query(fql("Collection.create({name: 'Author'})")); - client.query(fql("Author.create({'firstName': 'Alice', 'lastName': 'Wonderland', 'middleInitial': 'N', 'age': 65})")); - client.query(fql("Author.create({'firstName': 'Mad', 'lastName': 'Atter', 'middleInitial': 'H', 'age': 90})")); + client.query( + fql("Author.create({'firstName': 'Alice', 'lastName': 'Wonderland', 'middleInitial': 'N', 'age': 65})")); + client.query( + fql("Author.create({'firstName': 'Mad', 'lastName': 'Atter', 'middleInitial': 'H', 'age': 90})")); } - public static void ProductCollection(FaunaClient client) { - client.asyncQuery(fql("Collection.byName('Product')?.delete()")).exceptionally(t -> null).join(); - // client.query(fql("Collection.create({name: 'Product'})")); - client.query(fql("Collection.create({name: 'Product', fields: {'name': {signature: 'String'},'quantity': {signature: 'Int', default: '0'}}, constraints: [{unique: ['name']},{check:{name: 'posQuantity', body: '(doc) => doc.quantity >= 0' }}]})")); + public static long ProductCollection(FaunaClient client) { + client.asyncQuery(fql("Collection.byName('Product')?.delete()")) + .exceptionally(t -> null).join(); + client.query( + fql("Collection.create({name: 'Product', fields: {'name': {signature: 'String'},'quantity': {signature: 'Int', default: '0'}}, constraints: [{unique: ['name']},{check:{name: 'posQuantity', body: '(doc) => doc.quantity >= 0' }}]})")); + // For testing the event feed API, we need to know a timestamp that's after the collection was created, but + // before any items are added to it. + long collectionTs = client.getLastTransactionTs().orElseThrow(); IntStream.range(0, 50).forEach(i -> client.query( fql("Product.create({'name': ${name}, 'quantity': ${quantity}})", Map.of("name", "product-" + i, "quantity", i)))); + return collectionTs; } } diff --git a/src/test/java/com/fauna/e2e/beans/Author.java b/src/test/java/com/fauna/e2e/beans/Author.java index eb689b72..49d947e8 100644 --- a/src/test/java/com/fauna/e2e/beans/Author.java +++ b/src/test/java/com/fauna/e2e/beans/Author.java @@ -10,7 +10,8 @@ public class Author { private int age; - public Author(String firstName, String lastName, String middleInitial, int age) { + public Author(String firstName, String lastName, String middleInitial, + int age) { this.firstName = firstName; this.lastName = lastName; this.middleInitial = middleInitial; diff --git a/src/test/java/com/fauna/env/DriverEnvironmentTest.java b/src/test/java/com/fauna/env/DriverEnvironmentTest.java index 7488d0bd..8c507ded 100644 --- a/src/test/java/com/fauna/env/DriverEnvironmentTest.java +++ b/src/test/java/com/fauna/env/DriverEnvironmentTest.java @@ -8,7 +8,8 @@ public class DriverEnvironmentTest { @Test public void testDriverEnvironment() { - DriverEnvironment env = new DriverEnvironment(DriverEnvironment.JvmDriver.JAVA); + DriverEnvironment env = + new DriverEnvironment(DriverEnvironment.JvmDriver.JAVA); String serialized = env.toString(); // These assertions attempt to check that everything looks correct while passing on both developer diff --git a/src/test/java/com/fauna/exception/ConstraintFailureTest.java b/src/test/java/com/fauna/exception/ConstraintFailureTest.java index efa913f7..0d9b35f4 100644 --- a/src/test/java/com/fauna/exception/ConstraintFailureTest.java +++ b/src/test/java/com/fauna/exception/ConstraintFailureTest.java @@ -1,36 +1,34 @@ package com.fauna.exception; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fauna.response.QueryStats; -import com.fauna.response.wire.QueryResponseWire; +import com.fauna.response.ConstraintFailure; +import com.fauna.response.QueryResponse; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.http.HttpResponse; import java.util.List; +import java.util.Optional; -import static com.fauna.exception.ErrorHandler.handleErrorResponse; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class ConstraintFailureTest { ObjectMapper mapper = new ObjectMapper(); + private static final JsonFactory JSON_FACTORY = new JsonFactory(); - private QueryResponseWire getQueryResponseWire(List> paths) throws JsonProcessingException { - ObjectNode body = mapper.createObjectNode(); - QueryStats stats = new QueryStats(0, 0, 0, 0, 0, 0, 0, 0, List.of()); - // body.put("stats", mapper.writeValueAsString(stats)); - body.putPOJO("stats", stats); - body.put("summary", "error: failed to..."); - body.put("txn_ts", 1723490275035000L); - body.put("schema_version", 1723490246890000L); - ObjectNode error = body.putObject("error"); - error.put("code", "constraint_failure"); - error.put("message", "Failed to create... "); - - ArrayNode failures = error.putArray("constraint_failures"); - ObjectNode failure = failures.addObject(); + private ObjectNode constraintFailure(List> paths) { + ObjectNode failure = mapper.createObjectNode(); ArrayNode pathArray = failure.putArray("paths"); for (List path : paths) { ArrayNode pathNode = mapper.createArrayNode(); @@ -46,22 +44,92 @@ private QueryResponseWire getQueryResponseWire(List> paths) throws pathArray.add(pathNode); } failure.put("message", "Document failed check constraint"); - return mapper.readValue(body.toString(), QueryResponseWire.class); + return failure; + } + + private String getConstraintFailureBody(List> paths) + throws JsonProcessingException { + ObjectNode body = mapper.createObjectNode(); + ObjectNode stats = body.putObject("stats"); + stats.put("compute_ops", 100); + // body.putPOJO("stats", stats); + body.put("summary", "error: failed to..."); + body.put("txn_ts", 1723490275035000L); + body.put("schema_version", 1723490246890000L); + ObjectNode error = body.putObject("error"); + error.put("code", "constraint_failure"); + error.put("message", "Failed to create... "); + ArrayNode failures = error.putArray("constraint_failures"); + failures.add(constraintFailure(paths)); + return body.toString(); + } + + @Test + public void testPathElementEquality() { + ConstraintFailure.PathElement one = + new ConstraintFailure.PathElement("1"); + ConstraintFailure.PathElement two = + new ConstraintFailure.PathElement("1"); + ConstraintFailure.PathElement three = + new ConstraintFailure.PathElement("3"); + assertEquals(one, two); + assertNotEquals(one, three); } @Test - public void TestConstraintFailureFromBodyWithPath() throws JsonProcessingException { + public void testConstraintFailureEquality() { + ConstraintFailure one = ConstraintFailure.builder().message("hell") + .path(ConstraintFailure.createPath("one", 2)).build(); + ConstraintFailure two = ConstraintFailure.builder().message("hell") + .path(ConstraintFailure.createPath("one", 2)).build(); + assertEquals(one.getMessage(), two.getMessage()); + assertEquals(one.getName(), two.getName()); + assertArrayEquals(one.getPaths().orElseThrow(), + two.getPaths().orElseThrow()); + assertEquals(one, two); + } + + @Test + public void TestConstraintFailureFromBodyUsingParser() throws IOException { + String failureWire = + constraintFailure(List.of(List.of("pathElement"))).toString(); + ConstraintFailure failure = + ConstraintFailure.parse(JSON_FACTORY.createParser(failureWire)); + assertEquals(Optional.of(List.of("pathElement")), + failure.getPathStrings()); + } + + @Test + public void TestConstraintFailureFromBodyWithPath() + throws JsonProcessingException { List> expected = List.of(List.of("name")); - var res = getQueryResponseWire(expected); - ConstraintFailureException exc = assertThrows(ConstraintFailureException.class, () -> handleErrorResponse(400, res, "")); - assertEquals(expected, exc.getConstraintFailures().get(0).getPaths()); + + String body = getConstraintFailureBody(expected); + HttpResponse resp = mock(HttpResponse.class); + when(resp.body()).thenReturn(new ByteArrayInputStream(body.getBytes())); + when(resp.statusCode()).thenReturn(400); + ConstraintFailureException exc = + assertThrows(ConstraintFailureException.class, + () -> QueryResponse.parseResponse(resp, null, null)); + assertEquals(Optional.of(List.of("name")), + exc.getConstraintFailures()[0].getPathStrings()); } @Test - public void TestConstraintFailureFromBodyWithIntegerInPath() throws JsonProcessingException { - List> expected = List.of(List.of("name"), List.of("name2", 1, 2, "name3")); - var res = getQueryResponseWire(expected); - ConstraintFailureException exc = assertThrows(ConstraintFailureException.class, () -> handleErrorResponse(400, res, "")); - assertEquals(expected, exc.getConstraintFailures().get(0).getPaths()); + public void TestConstraintFailureFromBodyWithIntegerInPath() + throws JsonProcessingException { + List> expected = + List.of(List.of("name"), List.of("name2", 1, 2, "name3")); + + String body = getConstraintFailureBody(expected); + HttpResponse resp = mock(HttpResponse.class); + when(resp.body()).thenReturn(new ByteArrayInputStream(body.getBytes())); + when(resp.statusCode()).thenReturn(400); + ConstraintFailureException exc = + assertThrows(ConstraintFailureException.class, + () -> QueryResponse.parseResponse(resp, null, null)); + assertEquals(Optional.of(List.of("name", "name2.1.2.name3")), + exc.getConstraintFailures()[0].getPathStrings()); } + } diff --git a/src/test/java/com/fauna/exception/ErrorInfoTest.java b/src/test/java/com/fauna/exception/ErrorInfoTest.java new file mode 100644 index 00000000..1f5a8cc3 --- /dev/null +++ b/src/test/java/com/fauna/exception/ErrorInfoTest.java @@ -0,0 +1,168 @@ +package com.fauna.exception; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fauna.codec.Codec; +import com.fauna.codec.DefaultCodecProvider; +import com.fauna.codec.UTF8FaunaParser; +import com.fauna.response.ConstraintFailure; +import com.fauna.response.ErrorInfo; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.Instant; +import java.util.List; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ErrorInfoTest { + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + private static final ObjectMapper MAPPER = new ObjectMapper(JSON_FACTORY); + + public ArrayNode addPaths(ObjectNode failure, List> elements) { + ArrayNode paths = failure.putArray("paths"); + for (List pathElements : elements) { + ArrayNode path = paths.addArray(); + pathElements.forEach(e -> path.add(e.toString())); + } + return paths; + } + + public ObjectNode buildError(String code, String message) { + ObjectNode infoJson = MAPPER.createObjectNode(); + infoJson.put("code", code); + infoJson.put("message", message); + return infoJson; + } + + @Test + public void testParseSimpleError() throws IOException { + ObjectNode errorJson = + buildError("invalid_request", "Invalid Request!"); + ErrorInfo info = ErrorInfo.parse( + JSON_FACTORY.createParser(errorJson.toString())); + assertEquals("invalid_request", info.getCode()); + assertEquals("Invalid Request!", info.getMessage()); + assertTrue(info.getAbort(null).isEmpty()); + assertTrue(info.getAbortJson().isEmpty()); + assertTrue(info.getConstraintFailures().isEmpty()); + } + + @Test + public void testParseWithAbortData() throws IOException { + ObjectNode infoJson = buildError("abort", "aborted!"); + ObjectNode abortData = infoJson.putObject("abort"); + abortData.put("@time", "2023-04-06T03:33:32.226Z"); + ErrorInfo info = + ErrorInfo.parse(JSON_FACTORY.createParser(infoJson.toString())); + assertEquals("abort", info.getCode()); + TreeNode tree = info.getAbortJson().get(); + UTF8FaunaParser parser = new UTF8FaunaParser(tree.traverse()); + Codec instantCodec = + DefaultCodecProvider.SINGLETON.get(Instant.class); + parser.read(); + Instant abortTime = instantCodec.decode(parser); + assertEquals(1680752012226L, abortTime.toEpochMilli()); + } + + @Test + public void testParseAndGetAbortData() throws IOException { + ObjectNode infoJson = buildError("abort", "aborted!"); + ObjectNode abortData = infoJson.putObject("abort"); + abortData.put("@time", "2023-04-06T03:33:32.226Z"); + ErrorInfo info = + ErrorInfo.parse(JSON_FACTORY.createParser(infoJson.toString())); + assertEquals("abort", info.getCode()); + Instant abortTime = info.getAbort(Instant.class).get(); + assertEquals(1680752012226L, abortTime.toEpochMilli()); + } + + @Test + public void testParseWithMinimalConstraintFailure() throws IOException { + ObjectNode infoJson = + buildError("constraint_failure", "Constraint failed!"); + ArrayNode failures = infoJson.putArray("constraint_failures"); + ObjectNode failure1 = failures.addObject(); + failure1.put("message", "msg1"); + + ErrorInfo info = + ErrorInfo.parse(JSON_FACTORY.createParser(infoJson.toString())); + assertTrue(info.getAbortJson().isEmpty()); + assertTrue(info.getAbort(String.class).isEmpty()); + assertEquals("Constraint failed!", info.getMessage()); + assertEquals("constraint_failure", info.getCode()); + assertTrue(info.getConstraintFailures().isPresent()); + assertEquals(1, info.getConstraintFailures().orElseThrow().length); + ConstraintFailure constraintFailure = + info.getConstraintFailures().get()[0]; + assertEquals("msg1", constraintFailure.getMessage()); + assertTrue(constraintFailure.getName().isEmpty()); + assertTrue(constraintFailure.getPaths().isEmpty()); + } + + @Test + public void testParseWithMultipleConstraintFailures() throws IOException { + ObjectNode infoJson = + buildError("constraint_failure", "Constraint failed!"); + ArrayNode failures = infoJson.putArray("constraint_failures"); + ObjectNode obj1 = failures.addObject(); + obj1.put("message", "msg1"); + obj1.put("name", "name1"); + ObjectNode obj2 = failures.addObject(); + obj2.put("message", "msg2"); + + ErrorInfo info = + ErrorInfo.parse(JSON_FACTORY.createParser(infoJson.toString())); + assertEquals("Constraint failed!", info.getMessage()); + assertEquals("constraint_failure", info.getCode()); + assertTrue(info.getConstraintFailures().isPresent()); + assertEquals(2, info.getConstraintFailures().orElseThrow().length); + ConstraintFailure constraintFailure = + info.getConstraintFailures().get()[0]; + assertEquals("msg1", constraintFailure.getMessage()); + assertEquals("name1", constraintFailure.getName().orElseThrow()); + assertTrue(constraintFailure.getPaths().isEmpty()); + + ConstraintFailure failure2 = info.getConstraintFailures().get()[1]; + assertEquals("msg2", failure2.getMessage()); + assertTrue(failure2.getName().isEmpty()); + assertTrue(failure2.getPaths().isEmpty()); + } + + @Test + public void testParseWithConstraintFailuresWithPaths() throws IOException { + ObjectNode infoJson = + buildError("constraint_failure", "Constraint failed!"); + ArrayNode failures = infoJson.putArray("constraint_failures"); + ObjectNode obj1 = failures.addObject(); + obj1.put("message", "msg1"); + addPaths(obj1, List.of(List.of(1, "a"), List.of("1b"))); + + ObjectNode obj2 = failures.addObject(); + obj2.put("message", "msg2"); + addPaths(obj2, List.of(List.of(2, "a"))); + + ErrorInfo info = + ErrorInfo.parse(JSON_FACTORY.createParser(infoJson.toString())); + assertEquals("Constraint failed!", info.getMessage()); + assertEquals("constraint_failure", info.getCode()); + assertTrue(info.getConstraintFailures().isPresent()); + assertEquals(2, info.getConstraintFailures().orElseThrow().length); + ConstraintFailure failure1 = info.getConstraintFailures().get()[0]; + assertTrue(failure1.getPaths().isPresent()); + assertEquals("msg1", failure1.getMessage()); + assertTrue(failure1.getName().isEmpty()); + assertEquals(List.of("1.a", "1b"), + failure1.getPathStrings().orElseThrow()); + + ConstraintFailure failure2 = info.getConstraintFailures().get()[1]; + assertEquals("msg2", failure2.getMessage()); + assertTrue(failure2.getName().isEmpty()); + assertEquals(List.of("2.a"), failure2.getPathStrings().orElseThrow()); + } + +} diff --git a/src/test/java/com/fauna/exception/TestAbortException.java b/src/test/java/com/fauna/exception/TestAbortException.java index 56734829..c59ef448 100644 --- a/src/test/java/com/fauna/exception/TestAbortException.java +++ b/src/test/java/com/fauna/exception/TestAbortException.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fauna.response.ErrorInfo; import com.fauna.response.QueryFailure; -import com.fauna.response.wire.QueryResponseWire; +import com.fauna.response.QueryResponse; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -12,7 +14,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; public class TestAbortException { ObjectMapper mapper = new ObjectMapper(); @@ -20,21 +21,18 @@ public class TestAbortException { @Test public void testAbortDataObject() throws IOException { // Given - ObjectNode root = mapper.createObjectNode(); - ObjectNode error = root.putObject("error"); - error.put("code", "abort"); - ObjectNode abort = error.putObject("abort"); + ObjectNode abort = mapper.createObjectNode(); ObjectNode num = abort.putObject("num"); num.put("@int", "42"); - var res = mapper.readValue(root.toString(), QueryResponseWire.class); - QueryFailure failure = new QueryFailure(500, res); + QueryFailure failure = new QueryFailure(500, QueryResponse.builder(null) + .error(ErrorInfo.builder().code("abort").abort(abort).build())); // When AbortException exc = new AbortException(failure); // Then - HashMap expected = new HashMap<>(); + HashMap expected = new HashMap<>(); expected.put("num", 42); assertEquals(expected, exc.getAbort()); @@ -45,12 +43,10 @@ public void testAbortDataObject() throws IOException { @Test public void testAbortDataString() throws IOException { // Given - ObjectNode root = mapper.createObjectNode(); - ObjectNode error = root.putObject("error"); - error.put("code", "abort"); - error.put("abort", "some reason"); - var res = mapper.readValue(root.toString(), QueryResponseWire.class); - QueryFailure failure = new QueryFailure(500, res); + QueryFailure failure = new QueryFailure(500, + QueryResponse.builder(null).error( + ErrorInfo.builder().code("abort").abort( + TextNode.valueOf("some reason")).build())); // When AbortException exc = new AbortException(failure); @@ -62,11 +58,10 @@ public void testAbortDataString() throws IOException { @Test public void testAbortDataMissing() throws IOException { // Given - ObjectNode root = mapper.createObjectNode(); - ObjectNode error = root.putObject("error"); - error.put("code", "abort"); - var res = mapper.readValue(root.toString(), QueryResponseWire.class); - QueryFailure failure = new QueryFailure(500, res); + QueryFailure failure = new QueryFailure(200, + QueryResponse.builder(null).error( + ErrorInfo.builder().code("abort") + .message("some message").build())); // When AbortException exc = new AbortException(failure); diff --git a/src/test/java/com/fauna/exception/TestErrorHandler.java b/src/test/java/com/fauna/exception/TestErrorHandler.java index fa66b9c4..b6edbdd9 100644 --- a/src/test/java/com/fauna/exception/TestErrorHandler.java +++ b/src/test/java/com/fauna/exception/TestErrorHandler.java @@ -3,15 +3,20 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fauna.response.wire.QueryResponseWire; import com.fauna.response.QueryStats; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.http.HttpResponse; import java.util.List; import java.util.stream.Stream; +import static com.fauna.response.QueryResponse.parseResponse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class TestErrorHandler { @@ -23,6 +28,7 @@ public static class TestArgs { public int httpStatus; public String code; public Class exception; + public TestArgs(int httpStatus, String code, Class exception) { this.httpStatus = httpStatus; this.code = code; @@ -32,17 +38,23 @@ public TestArgs(int httpStatus, String code, Class exception) { public static Stream testArgStream() { return Stream.of( - new TestArgs(400, "unbound_variable", QueryRuntimeException.class), + new TestArgs(400, "unbound_variable", + QueryRuntimeException.class), new TestArgs(400, "invalid_query", QueryCheckException.class), new TestArgs(400, "limit_exceeded", ThrottlingException.class), - new TestArgs(400, "invalid_request", InvalidRequestException.class), + new TestArgs(400, "invalid_request", + InvalidRequestException.class), new TestArgs(400, "abort", AbortException.class), - new TestArgs(400, "constraint_failure", ConstraintFailureException.class), - new TestArgs(401, "unauthorized", AuthenticationException.class), + new TestArgs(400, "constraint_failure", + ConstraintFailureException.class), + new TestArgs(401, "unauthorized", + AuthenticationException.class), new TestArgs(403, "forbidden", AuthorizationException.class), - new TestArgs(409, "contended_transaction", ContendedTransactionException.class), + new TestArgs(409, "contended_transaction", + ContendedTransactionException.class), new TestArgs(440, "time_out", QueryTimeoutException.class), - new TestArgs(500, "internal_error", ServiceInternalException.class), + new TestArgs(500, "internal_error", + ServiceInternalException.class), new TestArgs(503, "time_out", QueryTimeoutException.class), // Unknown error code results in ProtocolException, except in case of 400. new TestArgs(400, "unknown_code", QueryRuntimeException.class), @@ -53,22 +65,16 @@ public static Stream testArgStream() { @ParameterizedTest @MethodSource("testArgStream") - public void testHandleBadRequest(TestArgs args) throws JsonProcessingException { + public void testHandleBadRequest(TestArgs args) + throws JsonProcessingException { ObjectNode root = mapper.createObjectNode(); ObjectNode error = root.putObject("error"); - ObjectNode stats = root.putObject("stats"); error.put("code", args.code); - String body = mapper.writeValueAsString(root); - var res = mapper.readValue(body, QueryResponseWire.class); - assertThrows(args.exception, () -> ErrorHandler.handleErrorResponse(args.httpStatus, res, body)); + HttpResponse resp = mock(HttpResponse.class); + when(resp.body()).thenReturn( + new ByteArrayInputStream(root.toString().getBytes())); + when(resp.statusCode()).thenReturn(args.httpStatus); + assertThrows(args.exception, () -> parseResponse(resp, null, null)); } -// public void testMissingStats() throws JsonProcessingException { -// ObjectNode root = mapper.createObjectNode(); -// ObjectNode error = root.putObject("error"); -// error.put("code", "invalid_query"); -// String body = mapper.writeValueAsString(root); -// assertThrows(ProtocolException.class, -// () -> ErrorHandler.handleErrorResponse(400, body, mapper)); -// } } diff --git a/src/test/java/com/fauna/exception/TestServiceException.java b/src/test/java/com/fauna/exception/TestServiceException.java index 86668940..40b03165 100644 --- a/src/test/java/com/fauna/exception/TestServiceException.java +++ b/src/test/java/com/fauna/exception/TestServiceException.java @@ -1,14 +1,16 @@ package com.fauna.exception; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fauna.response.ErrorInfo; import com.fauna.response.QueryFailure; -import com.fauna.response.wire.QueryResponseWire; +import com.fauna.response.QueryResponse; +import com.fauna.response.QueryStats; +import com.fauna.query.QueryTags; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.Map; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -18,27 +20,21 @@ public class TestServiceException { @Test public void testNullResponseThrowsNullPointer() { - assertThrows(NullPointerException.class, () -> new ServiceException(null)); + assertThrows(NullPointerException.class, + () -> new ServiceException(null)); } @Test public void testGetters() throws IOException { // Given - ObjectNode root = mapper.createObjectNode(); - root.put("summary", "summarized"); - root.put("schema_version", 10); - root.put("query_tags", "foo=bar"); - root.put("txn_ts", Long.MAX_VALUE / 4); // would cause int overflow - - ObjectNode error = root.putObject("error"); - error.put("code", "bad_thing"); - error.put("message", "message in a bottle"); - - ObjectNode stats = root.putObject("stats"); - stats.put("compute_ops", 100); - - var res = mapper.readValue(root.toString(), QueryResponseWire.class); - QueryFailure failure = new QueryFailure(500, res); + QueryFailure failure = new QueryFailure(500, + QueryResponse.builder(null).summary("summarized") + .schemaVersion(10L) + .stats(new QueryStats(100, 0, 0, 0, 0, 0, 0, 0, null)) + .queryTags(QueryTags.of("foo=bar")) + .lastSeenTxn(Long.MAX_VALUE / 4).error( + ErrorInfo.builder().code("bad_thing") + .message("message in a bottle").build())); // When ServiceException exc = new ServiceException(failure); @@ -46,11 +42,12 @@ public void testGetters() throws IOException { // Then assertEquals(500, exc.getStatusCode()); assertEquals("bad_thing", exc.getErrorCode()); - assertEquals("500 (bad_thing): message in a bottle\n---\nsummarized", exc.getMessage()); + assertEquals("500 (bad_thing): message in a bottle\n---\nsummarized", + exc.getMessage()); assertEquals("summarized", exc.getSummary()); - assertEquals(100, exc.getStats().computeOps); + assertEquals(100, exc.getStats().getComputeOps()); assertEquals(10, exc.getSchemaVersion()); - assertEquals(Long.MAX_VALUE / 4, exc.getTxnTs()); + assertEquals(Optional.of(Long.MAX_VALUE / 4), exc.getTxnTs()); assertEquals(Map.of("foo", "bar"), exc.getQueryTags()); } diff --git a/src/test/java/com/fauna/perf/MetricsHandler.java b/src/test/java/com/fauna/perf/MetricsHandler.java new file mode 100644 index 00000000..e03ea6c3 --- /dev/null +++ b/src/test/java/com/fauna/perf/MetricsHandler.java @@ -0,0 +1,89 @@ +package com.fauna.perf; + +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class MetricsHandler { + private static final String unique = + System.getenv("LOG_UNIQUE") != null ? System.getenv("LOG_UNIQUE") : + ""; + private static final String rawStatsFilename = + "rawstats_" + unique + ".csv"; + private static final String statsBlockFilename = "stats_" + unique + ".txt"; + private static final Map> metricsCollector = + new HashMap<>(); + + public static void recordMetrics(String queryName, int roundTripMs, + int queryTimeMs) { + int overhead = roundTripMs - queryTimeMs; + + metricsCollector.computeIfAbsent(queryName, k -> new ArrayList<>()) + .add(new TestTimings(roundTripMs, queryTimeMs, overhead)); + } + + public static void writeMetricsToFile() throws IOException { + List headers = + List.of("ts,metric,roundTrip,queryTime,diff,tags"); + Files.write(Paths.get(rawStatsFilename), headers, + StandardOpenOption.CREATE); + + List blockHeaders = List.of( + String.format("%-35s%9s%9s%9s%9s", "TEST", "P50", "P95", "P99", + "STDDEV"), + new String(new char[71]).replace("\0", "-") + ); + Files.write(Paths.get(statsBlockFilename), blockHeaders, + StandardOpenOption.CREATE); + + for (Map.Entry> entry : new TreeMap<>( + metricsCollector).entrySet()) { + DescriptiveStatistics stats = new DescriptiveStatistics(); + + List lines = entry.getValue().stream() + .map(testRun -> { + stats.addValue(testRun.getOverheadMs()); + + return String.join(",", + testRun.getCreatedAt().toString(), + entry.getKey(), + Integer.toString(testRun.getRoundTripMs()), + Integer.toString(testRun.getQueryTimeMs()), + Integer.toString(testRun.getOverheadMs()), + String.join(";", getMetricsTags()) + ); + }) + .collect(Collectors.toList()); + Files.write(Paths.get(rawStatsFilename), lines, + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + + // *100, round, /100 trick to get two decimal places + double p50 = Math.round(stats.getPercentile(50) * 100) / 100; + double p95 = Math.round(stats.getPercentile(95) * 100) / 100; + double p99 = Math.round(stats.getPercentile(99) * 100) / 100; + double stddev = + Math.round(stats.getStandardDeviation() * 100) / 100; + + var line = + String.format("%-35s%9s%9s%9s%9s", entry.getKey(), p50, p95, + p99, stddev); + Files.write(Paths.get(statsBlockFilename), List.of(line), + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } + } + + public static String[] getMetricsTags() { + String env = System.getenv("FAUNA_ENVIRONMENT") != null ? + System.getenv("FAUNA_ENVIRONMENT") : "test"; + return new String[] {"env:" + env, "driver_lang:java", "version:0.0.1"}; + } +} diff --git a/src/test/java/com/fauna/perf/PerformanceTest.java b/src/test/java/com/fauna/perf/PerformanceTest.java new file mode 100644 index 00000000..f040b497 --- /dev/null +++ b/src/test/java/com/fauna/perf/PerformanceTest.java @@ -0,0 +1,116 @@ +package com.fauna.perf; + +import com.fauna.client.Fauna; +import com.fauna.client.FaunaClient; +import com.fauna.perf.model.Product; +import com.fauna.perf.testdata.TestDataParser; +import com.fauna.query.AfterToken; +import com.fauna.query.builder.Query; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import static com.fauna.codec.Generic.pageOf; +import static com.fauna.query.builder.Query.fql; + +public class PerformanceTest { + private static FaunaClient client; + + private static Stream getTestData() throws IOException { + return TestDataParser.getQueriesFromFile(); + } + + @BeforeAll + public static void setup() { + client = Fauna.client(); + } + + @AfterAll + public static void tearDown() throws IOException { + MetricsHandler.writeMetricsToFile(); + } + + @ParameterizedTest + @MethodSource("getTestData") + @Tag("perfTests") + public void executeQueryAndCollectStats(String name, + List queryParts, + boolean typed, boolean page) + throws InterruptedException, ExecutionException { + if (queryParts.size() == 0) { + System.out.println("Skipping empty query from queries.json"); + return; + } + + for (int i = 0; i < 20; i++) { + Query query = getCompositedQueryFromParts(queryParts); + AtomicInteger queryTime = new AtomicInteger(0); + + long startTime = System.currentTimeMillis(); + + CompletableFuture future = null; + + if (typed && page) { + var result = + client.asyncQuery(query, pageOf(Product.class)).get(); + int queryCount = 1; + int queryTimeAgg = result.getStats().getQueryTimeMs(); + + while (result.getData().getAfter().isPresent()) { + AfterToken after = result.getData().getAfter().get(); + result = client.asyncQuery( + fql("Set.paginate(${after})", + Map.of("after", after.getToken())), + pageOf(Product.class)).get(); + queryCount++; + queryTimeAgg += result.getStats().getQueryTimeMs(); + } + + long endTime = System.currentTimeMillis(); + long elapsedTime = endTime - startTime; + MetricsHandler.recordMetrics( + name + " (query)", + (int) elapsedTime / queryCount, + queryTimeAgg / queryCount); + } else if (typed) { + future = client.asyncQuery(query, Product.class) + .thenAccept(result -> { + queryTime.set(result.getStats().getQueryTimeMs()); + }); + } else { + future = client.asyncQuery(query) + .thenAccept(result -> { + queryTime.set(result.getStats().getQueryTimeMs()); + }); + } + + if (!page) { + future.thenRun(() -> { + long endTime = System.currentTimeMillis(); + long elapsedTime = endTime - startTime; + MetricsHandler.recordMetrics(name, (int) elapsedTime, + queryTime.get()); + }).get(); + } + } + } + + private Query getCompositedQueryFromParts(List parts) { + if (parts.size() == 1) { + return fql(parts.get(0)); + } + + return fql(String.join("", parts)); + } +} diff --git a/src/test/java/com/fauna/perf/TestTimings.java b/src/test/java/com/fauna/perf/TestTimings.java new file mode 100644 index 00000000..daeef53f --- /dev/null +++ b/src/test/java/com/fauna/perf/TestTimings.java @@ -0,0 +1,34 @@ +package com.fauna.perf; + +import java.time.Instant; + +public class TestTimings { + private final Instant createdAt; + private final int roundTripMs; + private final int queryTimeMs; + private final int overheadMs; + + public TestTimings(int roundTripMs, int queryTimeMs, int overheadMs) { + this.createdAt = Instant.now(); + this.roundTripMs = roundTripMs; + this.queryTimeMs = queryTimeMs; + this.overheadMs = overheadMs; + } + + // Getters + public Instant getCreatedAt() { + return createdAt; + } + + public int getRoundTripMs() { + return roundTripMs; + } + + public int getQueryTimeMs() { + return queryTimeMs; + } + + public int getOverheadMs() { + return overheadMs; + } +} diff --git a/src/test/java/com/fauna/perf/model/Manufacturer.java b/src/test/java/com/fauna/perf/model/Manufacturer.java new file mode 100644 index 00000000..37f56b82 --- /dev/null +++ b/src/test/java/com/fauna/perf/model/Manufacturer.java @@ -0,0 +1,19 @@ +package com.fauna.perf.model; + +public class Manufacturer { + private String name; + private String location; + + public Manufacturer() { + // Default constructor if needed for instantiation via reflection or other methods + } + + // Getters for all properties + public String getName() { + return name; + } + + public String getLocation() { + return location; + } +} diff --git a/src/test/java/com/fauna/perf/model/Product.java b/src/test/java/com/fauna/perf/model/Product.java new file mode 100644 index 00000000..28ff049c --- /dev/null +++ b/src/test/java/com/fauna/perf/model/Product.java @@ -0,0 +1,40 @@ +package com.fauna.perf.model; + +import com.fauna.types.DocumentRef; + +public class Product { + private String name; + private String category; + private final int price = 0; + private final int quantity = 0; + private final boolean inStock = false; + private DocumentRef manufacturerRef; + + public Product() { + } + + // Getters for all properties + public String getName() { + return name; + } + + public String getCategory() { + return category; + } + + public int getPrice() { + return price; + } + + public int getQuantity() { + return quantity; + } + + public boolean isInStock() { + return inStock; + } + + public DocumentRef getManufacturerRef() { + return manufacturerRef; + } +} diff --git a/src/test/java/com/fauna/perf/testdata/TestDataParser.java b/src/test/java/com/fauna/perf/testdata/TestDataParser.java new file mode 100644 index 00000000..0ae7b1e5 --- /dev/null +++ b/src/test/java/com/fauna/perf/testdata/TestDataParser.java @@ -0,0 +1,25 @@ +package com.fauna.perf.testdata; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.params.provider.Arguments; + +import java.io.File; +import java.io.IOException; +import java.util.stream.Stream; + +public class TestDataParser { + public static Stream getQueriesFromFile() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + TestQueries testQueries = mapper.readValue( + new File("./perf-test-setup/queries.json"), TestQueries.class); + + return testQueries.getQueries().stream() + .map(q -> { + var response = q.getResponse(); + var typed = response != null && response.isTyped(); + var paginate = response != null && response.isPage(); + return Arguments.of(q.getName(), q.getParts(), typed, + paginate); + }); + } +} diff --git a/src/test/java/com/fauna/perf/testdata/TestQueries.java b/src/test/java/com/fauna/perf/testdata/TestQueries.java new file mode 100644 index 00000000..469473e3 --- /dev/null +++ b/src/test/java/com/fauna/perf/testdata/TestQueries.java @@ -0,0 +1,21 @@ +package com.fauna.perf.testdata; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +// Define TestQueries (container for TestQuery objects) +public class TestQueries { + @JsonProperty("queries") + private List queries = new ArrayList<>(); + + // Getter and setter + public List getQueries() { + return queries; + } + + public void setQueries(List queries) { + this.queries = queries; + } +} diff --git a/src/test/java/com/fauna/perf/testdata/TestQuery.java b/src/test/java/com/fauna/perf/testdata/TestQuery.java new file mode 100644 index 00000000..805b7193 --- /dev/null +++ b/src/test/java/com/fauna/perf/testdata/TestQuery.java @@ -0,0 +1,43 @@ +package com.fauna.perf.testdata; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +// Define TestQuery +public class TestQuery { + @JsonProperty("name") + private String name = ""; + + @JsonProperty("parts") + private List parts = new ArrayList<>(); + + @JsonProperty("response") + private TestResponse response; + + // Getters and setters + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getParts() { + return parts; + } + + public void setParts(List parts) { + this.parts = parts; + } + + public TestResponse getResponse() { + return response; + } + + public void setResponse(TestResponse response) { + this.response = response; + } +} diff --git a/src/test/java/com/fauna/perf/testdata/TestResponse.java b/src/test/java/com/fauna/perf/testdata/TestResponse.java new file mode 100644 index 00000000..fbfab266 --- /dev/null +++ b/src/test/java/com/fauna/perf/testdata/TestResponse.java @@ -0,0 +1,29 @@ +package com.fauna.perf.testdata; + +import com.fasterxml.jackson.annotation.JsonProperty; + +// Define TestResponse +public class TestResponse { + @JsonProperty("typed") + private boolean typed = false; + + @JsonProperty("page") + private boolean page = false; + + // Getters and setters + public boolean isTyped() { + return typed; + } + + public void setTyped(boolean typed) { + this.typed = typed; + } + + public boolean isPage() { + return page; + } + + public void setPage(boolean page) { + this.page = page; + } +} diff --git a/src/test/java/com/fauna/query/TestQueryOptions.java b/src/test/java/com/fauna/query/TestQueryOptions.java index d91f4062..594eb7a8 100644 --- a/src/test/java/com/fauna/query/TestQueryOptions.java +++ b/src/test/java/com/fauna/query/TestQueryOptions.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import java.time.Duration; -import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -26,7 +25,7 @@ public void testAllOptions() { QueryOptions options = QueryOptions.builder() .linearized(false).typeCheck(true) .traceParent("parent").timeout(Duration.ofMinutes(5)) - .queryTags(Map.of("hello", "world")) + .queryTag("hello", "world") .build(); assertEquals(false, options.getLinearized().get()); assertEquals(true, options.getTypeCheck().get()); @@ -34,4 +33,24 @@ public void testAllOptions() { assertEquals(5 * 60 * 1000, options.getTimeoutMillis().get()); assertEquals("world", options.getQueryTags().get().get("hello")); } + + @Test + public void testQueryTagsBuilderMethods() { + QueryTags initialTags = new QueryTags(); + initialTags.put("foo", "bar"); + QueryOptions opts = QueryOptions.builder().queryTags(initialTags) + .queryTag("hello", "world").build(); + assertEquals("foo=bar,hello=world", + opts.getQueryTags().orElseThrow().encode()); + } + + @Test + public void testQueryTagsBuilderMethodsAreAdditive() { + QueryTags initialTags = new QueryTags(); + initialTags.put("foo", "bar"); + QueryOptions opts = QueryOptions.builder().queryTag("hello", "world") + .queryTags(initialTags).build(); + assertEquals("foo=bar,hello=world", + opts.getQueryTags().orElseThrow().encode()); + } } diff --git a/src/test/java/com/fauna/query/builder/QueryTest.java b/src/test/java/com/fauna/query/builder/QueryTest.java index b52cd049..ff90da38 100644 --- a/src/test/java/com/fauna/query/builder/QueryTest.java +++ b/src/test/java/com/fauna/query/builder/QueryTest.java @@ -20,7 +20,6 @@ class QueryTest { - private String encode(Query q) throws IOException { var gen = new UTF8FaunaGenerator(); DefaultCodecProvider.SINGLETON.get(Query.class).encode(gen, q); @@ -30,7 +29,8 @@ private String encode(Query q) throws IOException { @Test public void testQueryBuilderStrings() { Query actual = fql("let x = 11", Collections.emptyMap()); - QueryFragment[] expected = new QueryFragment[]{new QueryLiteral("let x = 11")}; + QueryFragment[] expected = + new QueryFragment[] {new QueryLiteral("let x = 11")}; assertArrayEquals(expected, actual.get()); } @@ -40,7 +40,8 @@ public void testQueryBuilderStrings_WithNullValue() { args.put("n", null); Query actual = fql("let x = ${n}", args); - assertArrayEquals(new QueryFragment[] {new QueryLiteral("let x = "), new QueryVal(null)}, actual.get()); + assertArrayEquals(new QueryFragment[] {new QueryLiteral("let x = "), + new QueryVal(null)}, actual.get()); } @Test @@ -50,14 +51,16 @@ public void testMalformedFQL() { // Bug BT-5003, this would get into an infinite loop. Query actual = fql("let x = $n", args); - assertArrayEquals(new QueryFragment[] {new QueryLiteral("let x = "), new QueryLiteral("n")}, actual.get()); + assertArrayEquals(new QueryFragment[] {new QueryLiteral("let x = "), + new QueryLiteral("n")}, actual.get()); } @Test public void testQueryBuilderInterpolatedStrings() { Map variables = new HashMap<>(); variables.put("n1", 5); - Query actual = fql("let age = ${n1}\n\"Alice is #{age} years old.\"", variables); + Query actual = fql("let age = ${n1}\n\"Alice is #{age} years old.\"", + variables); QueryFragment[] expected = new QueryFragment[] { new QueryLiteral("let age = "), new QueryVal(5), @@ -72,7 +75,9 @@ public void testQueryBuilderValues() { "age", 0, "birthdate", LocalDate.of(2023, 2, 24)); Query actual = fql("let x = ${my_var}", Map.of("my_var", user)); - QueryFragment[] expected = new QueryFragment[]{new QueryLiteral("let x = "), new QueryVal(user)}; + QueryFragment[] expected = + new QueryFragment[] {new QueryLiteral("let x = "), + new QueryVal(user)}; assertArrayEquals(expected, actual.get()); } @@ -85,15 +90,18 @@ public void testQueryBuilderSubQueries() throws IOException { Query inner = fql("let x = ${my_var}", Map.of("my_var", user)); Query actual = fql("${inner}\nx { name }", Map.of("inner", inner)); - QueryFragment[] expected = new QueryFragment[]{inner, new QueryLiteral("\nx { name }")}; + QueryFragment[] expected = + new QueryFragment[] {inner, new QueryLiteral("\nx { name }")}; assertArrayEquals(expected, actual.get()); } @Test public void testOverloadedFqlBuildingMethods() { // Test that the four different fql(...) methods produce equivalent results. - Query explicit_vars = fql("let age = 5\n\"Alice is #{age} years old.\"", Map.of()); - Query implicit_vars = fql("let age = 5\n\"Alice is #{age} years old.\"", null); + Query explicit_vars = + fql("let age = 5\n\"Alice is #{age} years old.\"", Map.of()); + Query implicit_vars = + fql("let age = 5\n\"Alice is #{age} years old.\"", null); Query no_vars = fql("let age = 5\n\"Alice is #{age} years old.\""); assertArrayEquals(explicit_vars.get(), implicit_vars.get()); assertArrayEquals(no_vars.get(), implicit_vars.get()); @@ -101,19 +109,23 @@ public void testOverloadedFqlBuildingMethods() { @Test public void testQueryWithMissingArgs() { - IllegalArgumentException first = assertThrows(IllegalArgumentException.class, - () -> fql("let first = ${first}")); + IllegalArgumentException first = + assertThrows(IllegalArgumentException.class, + () -> fql("let first = ${first}")); // I haven't figured out why yet, but these error messages are sometimes: // "java.lang.IllegalArgumentException: message", and sometimes just "message" ?? - assertTrue(first.getMessage().contains("Template variable first not found in provided args.")); + assertTrue(first.getMessage().contains( + "Template variable first not found in provided args.")); } @Test public void testQueryUsingMessageFormat() { String email = "alice@home.com"; - Query q1 = fql(MessageFormat.format("Users.firstWhere(.email == {0})", email)); + Query q1 = fql(MessageFormat.format("Users.firstWhere(.email == {0})", + email)); Query q2 = fql(String.format("Users.firstWhere(.email == %s)", email)); - Query q3 = fql(new StringBuilder().append("Users.firstWhere(.email == ").append(email).append(")").toString()); + Query q3 = fql("Users.firstWhere(.email == " + + email + ")"); assertArrayEquals(q1.get(), q2.get()); assertArrayEquals(q1.get(), q3.get()); } diff --git a/src/test/java/com/fauna/query/template/FaunaTemplateTest.java b/src/test/java/com/fauna/query/template/FaunaTemplateTest.java index e23a030f..31a89bbe 100644 --- a/src/test/java/com/fauna/query/template/FaunaTemplateTest.java +++ b/src/test/java/com/fauna/query/template/FaunaTemplateTest.java @@ -1,15 +1,15 @@ package com.fauna.query.template; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.util.ArrayList; import java.util.List; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; class FaunaTemplateTest { @@ -30,7 +30,8 @@ void testTemplate_WithSingleVariable() { void testTemplate_WithDollarSignDoesNotInfiniteLoop(String literal) { FaunaTemplate template = new FaunaTemplate(literal); FaunaTemplate.TemplatePart[] parts = StreamSupport.stream( - template.spliterator(), true).toArray(FaunaTemplate.TemplatePart[]::new); + template.spliterator(), true) + .toArray(FaunaTemplate.TemplatePart[]::new); // The dollar sign gets swallowed, even if it's escaped? assertEquals(2, parts.length); for (FaunaTemplate.TemplatePart part : parts) { @@ -40,7 +41,8 @@ void testTemplate_WithDollarSignDoesNotInfiniteLoop(String literal) { @Test void testTemplate_WithDuplicateVariable() { - FaunaTemplate template = new FaunaTemplate("let x = ${my_var}\nlet y = ${my_var}\nx * y"); + FaunaTemplate template = new FaunaTemplate( + "let x = ${my_var}\nlet y = ${my_var}\nx * y"); List expanded = new ArrayList<>(); template.forEach(expanded::add); assertEquals(5, expanded.size()); diff --git a/src/test/java/com/fauna/response/QueryResponseTest.java b/src/test/java/com/fauna/response/QueryResponseTest.java index 109607b5..f88563f0 100644 --- a/src/test/java/com/fauna/response/QueryResponseTest.java +++ b/src/test/java/com/fauna/response/QueryResponseTest.java @@ -1,36 +1,44 @@ package com.fauna.response; -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.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.fauna.beans.ClassWithAttributes; -import com.fauna.codec.*; -import com.fauna.constants.ResponseFields; +import com.fauna.client.StatsCollectorImpl; +import com.fauna.codec.Codec; +import com.fauna.codec.CodecProvider; +import com.fauna.codec.CodecRegistry; +import com.fauna.codec.DefaultCodecProvider; +import com.fauna.codec.DefaultCodecRegistry; +import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.exception.ClientResponseException; -import com.fauna.exception.CodecException; +import org.junit.jupiter.api.Test; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.http.HttpResponse; -import java.util.Optional; +import java.nio.charset.StandardCharsets; -import com.fauna.exception.ProtocolException; -import com.fauna.codec.UTF8FaunaGenerator; -import com.fauna.response.wire.QueryResponseWire; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class QueryResponseTest { CodecRegistry codecRegistry = new DefaultCodecRegistry(); CodecProvider codecProvider = new DefaultCodecProvider(codecRegistry); + static HttpResponse mockResponse(String body) { + HttpResponse resp = mock(HttpResponse.class); + doAnswer(invocationOnMock -> new ByteArrayInputStream( + body.getBytes(StandardCharsets.UTF_8))).when(resp).body(); + return resp; + } + @SuppressWarnings("unchecked") private String encode(Codec codec, T obj) throws IOException { - try(UTF8FaunaGenerator gen = new UTF8FaunaGenerator()) { + try (UTF8FaunaGenerator gen = new UTF8FaunaGenerator()) { codec.encode(gen, obj); return gen.serialize(); } @@ -40,14 +48,18 @@ private String encode(Codec codec, T obj) throws IOException { public void getFromResponseBody_Success() throws IOException { ClassWithAttributes baz = new ClassWithAttributes("baz", "luhrman", 64); - Codec codec = codecProvider.get(ClassWithAttributes.class); + Codec codec = + codecProvider.get(ClassWithAttributes.class); String data = encode(codec, baz); - String body = "{\"stats\":{},\"static_type\":\"PersonWithAttributes\",\"data\":" + data + "}"; - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn(body); + String body = + "{\"stats\":{},\"static_type\":\"PersonWithAttributes\",\"data\":" + + data + "}"; + HttpResponse resp = mockResponse(body); when(resp.statusCode()).thenReturn(200); - QuerySuccess success = QueryResponse.handleResponse(resp, codec); + QuerySuccess success = + QueryResponse.parseResponse(resp, codec, + new StatsCollectorImpl()); assertEquals(baz.getFirstName(), success.getData().getFirstName()); assertEquals("PersonWithAttributes", success.getStaticType().get()); @@ -56,57 +68,31 @@ public void getFromResponseBody_Success() throws IOException { } @Test - public void getFromResponseBody_Failure() throws IOException { - ObjectMapper mapper = new ObjectMapper(); - - ObjectNode errorData = mapper.createObjectNode(); - errorData.put(ResponseFields.ERROR_CODE_FIELD_NAME, "ErrorCode"); - errorData.put(ResponseFields.ERROR_MESSAGE_FIELD_NAME, "ErrorMessage"); - // ObjectNode cf = errorData.putObject(ResponseFields.ERROR_CONSTRAINT_FAILURES_FIELD_NAME); - errorData.put(ResponseFields.ERROR_ABORT_FIELD_NAME, "AbortData"); - ObjectNode failureNode = mapper.createObjectNode(); - failureNode.put(ResponseFields.ERROR_FIELD_NAME, errorData); - - var res = mapper.readValue(failureNode.toString(), QueryResponseWire.class); - QueryFailure response = new QueryFailure(400, res); - - assertEquals(400, response.getStatusCode()); - assertEquals("ErrorCode", response.getErrorCode()); - assertEquals("ErrorMessage", response.getMessage()); - assertTrue(response.getConstraintFailures().isEmpty()); - assertEquals(Optional.of("\"AbortData\""), response.getAbortString()); - } - - @Test - public void handleResponseWithInvalidJsonThrowsProtocolException() { - HttpResponse resp = mock(HttpResponse.class); - String body = "{\"not valid json\""; + public void handleResponseWithInvalidJsonThrowsClientResponseException() { + HttpResponse resp = mockResponse("{\"not valid json\""); when(resp.statusCode()).thenReturn(400); - when(resp.body()).thenReturn(body); - ClientResponseException exc = assertThrows(ClientResponseException.class, () -> QueryResponse.handleResponse(resp, codecProvider.get(Object.class))); - assertEquals("ClientResponseException HTTP 400: Failed to handle error response.", exc.getMessage()); + ClientResponseException exc = + assertThrows(ClientResponseException.class, + () -> QueryResponse.parseResponse(resp, + codecProvider.get(Object.class), + new StatsCollectorImpl())); + assertEquals( + "ClientResponseException HTTP 400: Failed to handle error response.", + exc.getMessage()); } @Test - public void handleResponseWithMissingStatsThrowsProtocolException() { - HttpResponse resp = mock(HttpResponse.class); - when(resp.body()).thenReturn("{\"not valid json\""); - assertThrows(ClientResponseException.class, () -> QueryResponse.handleResponse(resp, codecProvider.get(Object.class))); + public void handleResponseWithEmptyFieldsDoesNotThrow() { + HttpResponse resp = mockResponse("{}"); + QuerySuccess response = QueryResponse.parseResponse(resp, + codecProvider.get(Object.class), new StatsCollectorImpl()); + assertEquals(QuerySuccess.class, response.getClass()); + assertNull(response.getSchemaVersion()); + assertNull(response.getSummary()); + assertNull(response.getLastSeenTxn()); + assertNull(response.getQueryTags()); + assertNull(response.getData()); + assertNull(response.getStats()); } - - @Test - void getFromResponseBody_Exception() { - String body = "Invalid JSON"; - - // TODO call FaunaClient.handleResponse here. - CodecException exception = assertThrows(CodecException.class, () -> { - throw new CodecException("Error occurred while parsing the response body"); - }); - - assertEquals("Error occurred while parsing the response body", exception.getMessage()); - // assertTrue(exception.getCause().getMessage().contains( - // "Unrecognized token 'Invalid': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')")); - } - } \ No newline at end of file diff --git a/src/test/java/com/fauna/response/QueryStatsTest.java b/src/test/java/com/fauna/response/QueryStatsTest.java new file mode 100644 index 00000000..558d6279 --- /dev/null +++ b/src/test/java/com/fauna/response/QueryStatsTest.java @@ -0,0 +1,57 @@ +package com.fauna.response; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class QueryStatsTest { + static final ObjectMapper MAPPER = new ObjectMapper(); + static final JsonFactory FACTORY = new JsonFactory(); + + @Test + public void testQueryStatsStringValue() { + QueryStats stats = + new QueryStats(1, 2, 3, 4, 5, 6, 7, 8, List.of("a", "b")); + assertEquals( + "compute: 1, read: 2, write: 3, queryTime: 4, retries: 5, storageRead: 6, storageWrite: 7, limits: [a, b]", + stats.toString()); + } + + @Test + public void testParseQueryStats() throws IOException { + ObjectNode statsNode = MAPPER.createObjectNode(); + statsNode.put("compute_ops", 1); + statsNode.put("read_ops", 2); + statsNode.put("write_ops", 3); + statsNode.put("query_time_ms", 4); + statsNode.put("contention_retries", 5); + statsNode.put("storage_bytes_read", 6); + statsNode.put("storage_bytes_write", 7); + ArrayNode limits = statsNode.putArray("rate_limits_hit"); + limits.add("a"); + limits.add("b"); + QueryStats stats = QueryStats.parseStats( + FACTORY.createParser(statsNode.toString().getBytes())); + assertEquals( + "compute: 1, read: 2, write: 3, queryTime: 4, retries: 5, storageRead: 6, storageWrite: 7, limits: [a, b]", + stats.toString()); + } + + @Test + public void testParseNullStats() throws IOException { + JsonParser parser = + FACTORY.createParser("{\"stats\": null}".getBytes()); + parser.nextToken(); + QueryStats stats = QueryStats.parseStats(parser); + assertNull(stats); + } +} diff --git a/src/test/java/com/fauna/stream/StreamRequestTest.java b/src/test/java/com/fauna/stream/StreamRequestTest.java index 6e384d3a..7ffbe692 100644 --- a/src/test/java/com/fauna/stream/StreamRequestTest.java +++ b/src/test/java/com/fauna/stream/StreamRequestTest.java @@ -1,41 +1,47 @@ package com.fauna.stream; -import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import com.fauna.codec.DefaultCodecProvider; -import com.fauna.codec.UTF8FaunaGenerator; +import com.fauna.event.EventSource; +import com.fauna.event.StreamOptions; +import com.fauna.event.StreamRequest; import org.junit.jupiter.api.Test; import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; public class StreamRequestTest { public static final CodecProvider provider = DefaultCodecProvider.SINGLETON; + private final static EventSource SOURCE = new EventSource("abc"); @Test - public void testTokenOnlyRequest() { - StreamRequest req = new StreamRequest("abc"); - assertEquals("abc", req.getToken()); - assertTrue(req.getCursor().isEmpty()); - assertTrue(req.getStartTs().isEmpty()); + public void testTokenOnlyRequest() throws IOException { + StreamRequest req = new StreamRequest(SOURCE, StreamOptions.DEFAULT); + assertEquals("{\"token\":\"abc\"}", req.serialize()); } @Test - public void testCursorRequest() { - StreamRequest req = new StreamRequest("abc", "def"); - assertEquals("abc", req.getToken()); - assertEquals("def", req.getCursor().get()); - assertTrue(req.getStartTs().isEmpty()); + public void testCursorRequest() throws IOException { + StreamRequest req = new StreamRequest(SOURCE, + StreamOptions.builder().cursor("def").build()); + assertEquals("{\"token\":\"abc\",\"cursor\":\"def\"}", req.serialize()); } @Test - public void testTsRequest() { - StreamRequest req = new StreamRequest("abc", 1234L); - assertEquals("abc", req.getToken()); - assertTrue(req.getCursor().isEmpty()); - assertEquals(1234L, req.getStartTs().get()); + public void testTsRequest() throws IOException { + StreamRequest req = new StreamRequest(SOURCE, + StreamOptions.builder().startTimestamp(1234L).build()); + assertEquals("{\"token\":\"abc\",\"start_ts\":1234}", req.serialize()); + } + + @Test + public void testMissingArgsRequest() { + assertThrows(IllegalArgumentException.class, + () -> new StreamRequest(SOURCE, null)); + assertThrows(IllegalArgumentException.class, + () -> new StreamRequest(null, StreamOptions.DEFAULT)); } } diff --git a/src/test/java/com/fauna/types/DocumentRefTest.java b/src/test/java/com/fauna/types/DocumentRefTest.java index 7dc2c683..ee3c49d8 100644 --- a/src/test/java/com/fauna/types/DocumentRefTest.java +++ b/src/test/java/com/fauna/types/DocumentRefTest.java @@ -15,6 +15,7 @@ public void docRef_playsNiceWithJackson() throws JsonProcessingException { ); var result = mapper.writeValueAsString(doc); - assertEquals("{\"collection\":{\"name\":\"MyColl\"},\"id\":\"123\"}", result); + assertEquals("{\"collection\":{\"name\":\"MyColl\"},\"id\":\"123\"}", + result); } } diff --git a/src/test/java/com/fauna/types/DocumentTest.java b/src/test/java/com/fauna/types/DocumentTest.java index 68ffe02f..aae45a00 100644 --- a/src/test/java/com/fauna/types/DocumentTest.java +++ b/src/test/java/com/fauna/types/DocumentTest.java @@ -20,6 +20,8 @@ public void document_playsNiceWithJackson() throws JsonProcessingException { ); var result = mapper.writeValueAsString(doc); - assertEquals("{\"data\":{\"some_key\":\"some_val\"},\"ts\":1706016790.300000000,\"collection\":{\"name\":\"MyColl\"},\"id\":\"123\"}", result); + assertEquals( + "{\"data\":{\"some_key\":\"some_val\"},\"ts\":1706016790.300000000,\"collection\":{\"name\":\"MyColl\"},\"id\":\"123\"}", + result); } } diff --git a/src/test/java/com/fauna/types/NamedDocumentRefTest.java b/src/test/java/com/fauna/types/NamedDocumentRefTest.java index 55b6e7f2..898cba5d 100644 --- a/src/test/java/com/fauna/types/NamedDocumentRefTest.java +++ b/src/test/java/com/fauna/types/NamedDocumentRefTest.java @@ -3,21 +3,21 @@ import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.Test; -import java.time.Instant; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.assertEquals; public class NamedDocumentRefTest extends TypeTestBase { @Test - public void namedDocRef_playsNiceWithJackson() throws JsonProcessingException { + public void namedDocRef_playsNiceWithJackson() + throws JsonProcessingException { var doc = new NamedDocumentRef( "AName", new Module("MyColl") ); var result = mapper.writeValueAsString(doc); - assertEquals("{\"collection\":{\"name\":\"MyColl\"},\"name\":\"AName\"}", result); + assertEquals( + "{\"collection\":{\"name\":\"MyColl\"},\"name\":\"AName\"}", + result); } } diff --git a/src/test/java/com/fauna/types/NamedDocumentTest.java b/src/test/java/com/fauna/types/NamedDocumentTest.java index 05cc7e70..9395918a 100644 --- a/src/test/java/com/fauna/types/NamedDocumentTest.java +++ b/src/test/java/com/fauna/types/NamedDocumentTest.java @@ -1,7 +1,6 @@ package com.fauna.types; import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -21,6 +20,8 @@ public void document_playsNiceWithJackson() throws JsonProcessingException { ); var result = mapper.writeValueAsString(doc); - assertEquals("{\"data\":{\"some_key\":\"some_val\"},\"ts\":1706016790.300000000,\"collection\":{\"name\":\"MyColl\"},\"name\":\"AName\"}", result); + assertEquals( + "{\"data\":{\"some_key\":\"some_val\"},\"ts\":1706016790.300000000,\"collection\":{\"name\":\"MyColl\"},\"name\":\"AName\"}", + result); } } diff --git a/src/test/java/com/fauna/types/NonNullDocumentTest.java b/src/test/java/com/fauna/types/NonNullDocumentTest.java index db518798..939fadd5 100644 --- a/src/test/java/com/fauna/types/NonNullDocumentTest.java +++ b/src/test/java/com/fauna/types/NonNullDocumentTest.java @@ -21,6 +21,8 @@ public void nonNull_playsNiceWithJackson() throws JsonProcessingException { var nonNull = new NonNullDocument<>(doc); var result = mapper.writeValueAsString(nonNull); - assertEquals("{\"value\":{\"data\":{\"some_key\":\"some_val\"},\"ts\":1706016790.300000000,\"collection\":{\"name\":\"MyColl\"},\"id\":\"123\"}}", result); + assertEquals( + "{\"value\":{\"data\":{\"some_key\":\"some_val\"},\"ts\":1706016790.300000000,\"collection\":{\"name\":\"MyColl\"},\"id\":\"123\"}}", + result); } } diff --git a/src/test/java/com/fauna/types/NullDocumentTest.java b/src/test/java/com/fauna/types/NullDocumentTest.java index 335c1f3b..1140070d 100644 --- a/src/test/java/com/fauna/types/NullDocumentTest.java +++ b/src/test/java/com/fauna/types/NullDocumentTest.java @@ -9,9 +9,12 @@ public class NullDocumentTest extends TypeTestBase { @Test public void nullDoc_playsNiceWithJackson() throws JsonProcessingException { - var nonNull = new NullDocument<>("123", new Module("MyColl"), "not found"); + var nonNull = + new NullDocument<>("123", new Module("MyColl"), "not found"); var result = mapper.writeValueAsString(nonNull); - assertEquals("{\"id\":\"123\",\"cause\":\"not found\",\"collection\":{\"name\":\"MyColl\"}}", result); + assertEquals( + "{\"id\":\"123\",\"cause\":\"not found\",\"collection\":{\"name\":\"MyColl\"}}", + result); } } diff --git a/src/test/java/com/fauna/types/PageTest.java b/src/test/java/com/fauna/types/PageTest.java index 40ac689a..41bb635b 100644 --- a/src/test/java/com/fauna/types/PageTest.java +++ b/src/test/java/com/fauna/types/PageTest.java @@ -6,13 +6,25 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; public class PageTest extends TypeTestBase { @Test - public void page_playsNiceWithJackson() throws JsonProcessingException { + public void page_doesNotPlayNiceWithJackson() + throws JsonProcessingException { + // Page no longer plays nice with Jackson, but we use our Codec/parser instead. var page = new Page<>(List.of(1), "next"); var result = mapper.writeValueAsString(page); - assertEquals("{\"data\":[1],\"after\":\"next\"}", result); + assertEquals( + "{\"data\":[1],\"after\":{\"empty\":false,\"present\":true}}", + result); + } + + @Test + public void page_equals_NotAPage() { + Page page = new Page<>(List.of(1), "next"); + assertNotEquals("notapage", page); } }