diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 71594a4c..121ace97 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,9 +17,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- - name: Validate Gradle wrapper
- uses: gradle/actions/wrapper-validation@v3
-
- name: Set up Java
uses: actions/setup-java@v4
with:
@@ -30,7 +27,7 @@ jobs:
cache: gradle
- name: Set up Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/actions/setup-gradle@v4
- name: Run lints
run: ./scripts/lint
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 51b15c88..3b4c2d4b 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "4.0.2"
+ ".": "4.1.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 119fd722..23dfdad6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,2 +1,2 @@
configured_endpoints: 40
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-97bf4795deec23d1bfd3b0b5fd77c2a93c87f10e5fa3375ab30f4a877be97f53.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-5d2b3a9cdbcfb6f10b6e621ef7ababbd2701316e5ea1117ced072ab76bdbec34.yml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04de6b78..9fe07907 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,48 @@
# Changelog
+## 4.1.0 (2025-02-25)
+
+Full Changelog: [v4.0.2...v4.1.0](https://github.com/Finch-API/finch-api-java/compare/v4.0.2...v4.1.0)
+
+### Features
+
+* **api:** api update ([#454](https://github.com/Finch-API/finch-api-java/issues/454)) ([6a36319](https://github.com/Finch-API/finch-api-java/commit/6a3631913bea77b83216b72d47bc96fc9381aaa7))
+* **client:** get rid of annoying checked exceptions ([#449](https://github.com/Finch-API/finch-api-java/issues/449)) ([e7312ce](https://github.com/Finch-API/finch-api-java/commit/e7312ce5f83cad66a8b1c71697dda825c595480b))
+* **client:** support `JsonField#asX()` for known values ([#443](https://github.com/Finch-API/finch-api-java/issues/443)) ([d2c8ddd](https://github.com/Finch-API/finch-api-java/commit/d2c8ddd34286909b0c77b36a931c7784c6031297))
+* **client:** update enum `asX` methods ([#442](https://github.com/Finch-API/finch-api-java/issues/442)) ([caffc54](https://github.com/Finch-API/finch-api-java/commit/caffc542b831856cac7e1be5079b03a112b203f7))
+
+
+### Bug Fixes
+
+* **client:** mark some request bodies as optional ([#451](https://github.com/Finch-API/finch-api-java/issues/451)) ([344ac4b](https://github.com/Finch-API/finch-api-java/commit/344ac4bd5a99c29f50c8bf62640e536251fc491d))
+* **internal:** format code ([a34264f](https://github.com/Finch-API/finch-api-java/commit/a34264f4f11d18a8322e289f6505cc119853fc68))
+
+
+### Chores
+
+* **ci:** update gradle actions to v4 ([#447](https://github.com/Finch-API/finch-api-java/issues/447)) ([97cc45e](https://github.com/Finch-API/finch-api-java/commit/97cc45e241bf75d7255ba8fc645975168a944b7d))
+* **docs:** add faq to readme ([#450](https://github.com/Finch-API/finch-api-java/issues/450)) ([fd63219](https://github.com/Finch-API/finch-api-java/commit/fd63219b1fe6bda4743f2150cfa1ee4290e271d9))
+* **docs:** reorganize readme ([#444](https://github.com/Finch-API/finch-api-java/issues/444)) ([164050e](https://github.com/Finch-API/finch-api-java/commit/164050e39888c30f3c6f38c8b94f31c356813db5))
+* **internal:** codegen related update ([#448](https://github.com/Finch-API/finch-api-java/issues/448)) ([6db2948](https://github.com/Finch-API/finch-api-java/commit/6db294858870c374b2cb30bf4fd02a52e66452b6))
+* **internal:** get rid of configuration cache ([#446](https://github.com/Finch-API/finch-api-java/issues/446)) ([35edbd8](https://github.com/Finch-API/finch-api-java/commit/35edbd85822b3a521077d9d1ac96d5e071d018a5))
+* **internal:** make body class constructors private ([545758d](https://github.com/Finch-API/finch-api-java/commit/545758da4865c820e81165c78cd9034aa75e0a9b))
+* **internal:** make body classes for multipart requests ([545758d](https://github.com/Finch-API/finch-api-java/commit/545758da4865c820e81165c78cd9034aa75e0a9b))
+* **internal:** misc formatting changes ([545758d](https://github.com/Finch-API/finch-api-java/commit/545758da4865c820e81165c78cd9034aa75e0a9b))
+* **internal:** optimize build and test perf ([54b58c8](https://github.com/Finch-API/finch-api-java/commit/54b58c894270795449f4a4ad42d0bc6ff0448658))
+* **internal:** remove unnecessary non-null asserts in tests ([344ac4b](https://github.com/Finch-API/finch-api-java/commit/344ac4bd5a99c29f50c8bf62640e536251fc491d))
+* **internal:** rename internal body classes ([545758d](https://github.com/Finch-API/finch-api-java/commit/545758da4865c820e81165c78cd9034aa75e0a9b))
+* **internal:** update formatter ([#439](https://github.com/Finch-API/finch-api-java/issues/439)) ([54b58c8](https://github.com/Finch-API/finch-api-java/commit/54b58c894270795449f4a4ad42d0bc6ff0448658))
+* **internal:** update some formatting in `Values.kt` ([d2c8ddd](https://github.com/Finch-API/finch-api-java/commit/d2c8ddd34286909b0c77b36a931c7784c6031297))
+* **internal:** use `assertNotNull` in tests for type narrowing ([344ac4b](https://github.com/Finch-API/finch-api-java/commit/344ac4bd5a99c29f50c8bf62640e536251fc491d))
+* **internal:** use better test example values ([#441](https://github.com/Finch-API/finch-api-java/issues/441)) ([545758d](https://github.com/Finch-API/finch-api-java/commit/545758da4865c820e81165c78cd9034aa75e0a9b))
+* **test:** update some test values ([#437](https://github.com/Finch-API/finch-api-java/issues/437)) ([602f2c0](https://github.com/Finch-API/finch-api-java/commit/602f2c0e0ae1d1351d141faf91e20a3c5e3164eb))
+
+
+### Documentation
+
+* add immutability explanation to readme ([#452](https://github.com/Finch-API/finch-api-java/issues/452)) ([8f256fb](https://github.com/Finch-API/finch-api-java/commit/8f256fbfc0f39a56d8f416b5ea03e42c3e422c60))
+* add source file links to readme ([#453](https://github.com/Finch-API/finch-api-java/issues/453)) ([becaa47](https://github.com/Finch-API/finch-api-java/commit/becaa47cb5fb7138518157dfa92410d268fa55fd))
+
## 4.0.2 (2025-02-12)
Full Changelog: [v4.0.1...v4.0.2](https://github.com/Finch-API/finch-api-java/compare/v4.0.1...v4.0.2)
diff --git a/README.md b/README.md
index 12b43fd0..d469a892 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-[](https://central.sonatype.com/artifact/com.tryfinch.api/finch-java/4.0.2)
+[](https://central.sonatype.com/artifact/com.tryfinch.api/finch-java/4.1.0)
@@ -23,7 +23,7 @@ The REST API documentation can be found [in the Finch Documentation Center](htt
### Gradle
```kotlin
-implementation("com.tryfinch.api:finch-java:4.0.2")
+implementation("com.tryfinch.api:finch-java:4.1.0")
```
### Maven
@@ -32,7 +32,7 @@ implementation("com.tryfinch.api:finch-java:4.0.2")
com.tryfinch.api
finch-java
- 4.0.2
+ 4.1.0
```
@@ -44,42 +44,17 @@ This library requires Java 8 or later.
## Usage
-### Configure the client
-
-Use `FinchOkHttpClient.builder()` to configure the client.
-
-Alternately, set the environment with `FINCH_CLIENT_ID`, `FINCH_CLIENT_SECRET` or `FINCH_WEBHOOK_SECRET`, and use `FinchOkHttpClient.fromEnv()` to read from the environment.
-
```java
import com.tryfinch.api.client.FinchClient;
import com.tryfinch.api.client.okhttp.FinchOkHttpClient;
+import com.tryfinch.api.models.HrisDirectoryListPage;
+import com.tryfinch.api.models.HrisDirectoryListParams;
-FinchClient client = FinchOkHttpClient.fromEnv();
-
-// Note: you can also call fromEnv() from the client builder, for example if you need to set additional properties
FinchClient client = FinchOkHttpClient.builder()
+ // Configures using the `FINCH_CLIENT_ID`, `FINCH_CLIENT_SECRET` and `FINCH_WEBHOOK_SECRET` environment variables
.fromEnv()
- // ... set properties on the builder
+ .accessToken("My Access Token")
.build();
-```
-
-| Property | Environment variable | Required | Default value |
-| ------------- | ---------------------- | -------- | ------------- |
-| clientId | `FINCH_CLIENT_ID` | false | — |
-| clientSecret | `FINCH_CLIENT_SECRET` | false | — |
-| webhookSecret | `FINCH_WEBHOOK_SECRET` | false | — |
-
-Read the documentation for more configuration options.
-
----
-
-### Example: creating a resource
-
-To create a new hris directory, first use the `HrisDirectoryListParams` builder to specify attributes, then pass that to the `list` method of the `directory` service.
-
-```java
-import com.tryfinch.api.models.HrisDirectoryListPage;
-import com.tryfinch.api.models.HrisDirectoryListParams;
HrisDirectoryListParams params = HrisDirectoryListParams.builder()
.candidateId("")
@@ -87,100 +62,136 @@ HrisDirectoryListParams params = HrisDirectoryListParams.builder()
HrisDirectoryListPage page = client.hris().directory().list(params);
```
-### Example: listing resources
+## Client configuration
-The Finch API provides a `list` method to get a paginated list of directory. You can retrieve the first page by:
+Configure the client using environment variables:
```java
-import com.tryfinch.api.models.HrisDirectoryListPage;
-import com.tryfinch.api.models.IndividualInDirectory;
+import com.tryfinch.api.client.FinchClient;
+import com.tryfinch.api.client.okhttp.FinchOkHttpClient;
-HrisDirectoryListPage page = client.hris().directory().list();
-for (IndividualInDirectory directory : page.individuals()) {
- System.out.println(directory);
-}
+FinchClient client = FinchOkHttpClient.builder()
+ // Configures using the `FINCH_CLIENT_ID`, `FINCH_CLIENT_SECRET` and `FINCH_WEBHOOK_SECRET` environment variables
+ .fromEnv()
+ .accessToken("My Access Token")
+ .build();
```
-Use the `HrisDirectoryListParams` builder to set parameters:
+Or manually:
```java
-import com.tryfinch.api.models.HrisDirectoryListPage;
-import com.tryfinch.api.models.HrisDirectoryListParams;
+import com.tryfinch.api.client.FinchClient;
+import com.tryfinch.api.client.okhttp.FinchOkHttpClient;
-HrisDirectoryListParams params = HrisDirectoryListParams.builder()
- .limit(0L)
- .offset(0L)
+FinchClient client = FinchOkHttpClient.builder()
+ .accessToken("My Access Token")
.build();
-HrisDirectoryListPage page1 = client.hris().directory().list(params);
+```
+
+Or using a combination of the two approaches:
-// Using the `from` method of the builder you can reuse previous params values:
-HrisDirectoryListPage page2 = client.hris().directory().list(HrisDirectoryListParams.builder()
- .from(params)
- .build());
+```java
+import com.tryfinch.api.client.FinchClient;
+import com.tryfinch.api.client.okhttp.FinchOkHttpClient;
-// Or easily get params for the next page by using the helper `getNextPageParams`:
-HrisDirectoryListPage page3 = client.hris().directory().list(params.getNextPageParams(page2));
+FinchClient client = FinchOkHttpClient.builder()
+ // Configures using the `FINCH_CLIENT_ID`, `FINCH_CLIENT_SECRET` and `FINCH_WEBHOOK_SECRET` environment variables
+ .fromEnv()
+ .accessToken("My Access Token")
+ .accessToken("My Access Token")
+ .build();
```
-See [Pagination](#pagination) below for more information on transparently working with lists of objects without worrying about fetching each page.
+See this table for the available options:
----
+| Setter | Environment variable | Required | Default value |
+| --------------- | ---------------------- | -------- | ------------- |
+| `clientId` | `FINCH_CLIENT_ID` | false | - |
+| `clientSecret` | `FINCH_CLIENT_SECRET` | false | - |
+| `webhookSecret` | `FINCH_WEBHOOK_SECRET` | false | - |
-## Requests
+> [!TIP]
+> Don't create more than one client in the same application. Each client has a connection pool and
+> thread pools, which are more efficient to share between requests.
-### Parameters and bodies
+## Requests and responses
-To make a request to the Finch API, you generally build an instance of the appropriate `Params` class.
+To send a request to the Finch API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
-See [Undocumented request params](#undocumented-request-params) for how to send arbitrary parameters.
+For example, `client.hris().directory().list(...)` should be called with an instance of `HrisDirectoryListParams`, and it will return an instance of `HrisDirectoryListPage`.
-## Responses
+## Immutability
-### Response validation
+Each class in the SDK has an associated [builder](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) or factory method for constructing it.
-When receiving a response, the Finch Java SDK will deserialize it into instances of the typed model classes. In rare cases, the API may return a response property that doesn't match the expected Java type. If you directly access the mistaken property, the SDK will throw an unchecked `FinchInvalidDataException` at runtime. If you would prefer to check in advance that that response is completely well-typed, call `.validate()` on the returned model.
+Each class is [immutable](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html) once constructed. If the class has an associated builder, then it has a `toBuilder()` method, which can be used to convert it back to a builder for making a modified copy.
+
+Because each class is immutable, builder modification will _never_ affect already built class instances.
+
+## Asynchronous execution
+
+The default client is synchronous. To switch to asynchronous execution, call the `async()` method:
```java
-import com.tryfinch.api.models.HrisDirectoryListPage;
+import com.tryfinch.api.client.FinchClient;
+import com.tryfinch.api.client.okhttp.FinchOkHttpClient;
+import com.tryfinch.api.models.HrisDirectoryListPageAsync;
+import com.tryfinch.api.models.HrisDirectoryListParams;
+import java.util.concurrent.CompletableFuture;
-HrisDirectoryListPage page = client.hris().directory().list().validate();
-```
+FinchClient client = FinchOkHttpClient.builder()
+ // Configures using the `FINCH_CLIENT_ID`, `FINCH_CLIENT_SECRET` and `FINCH_WEBHOOK_SECRET` environment variables
+ .fromEnv()
+ .accessToken("My Access Token")
+ .build();
-### Response properties as JSON
+HrisDirectoryListParams params = HrisDirectoryListParams.builder().build();
+CompletableFuture page = client.async().hris().directory().list(params);
+```
-In rare cases, you may want to access the underlying JSON value for a response property rather than using the typed version provided by this SDK. Each model property has a corresponding JSON version, with an underscore before the method name, which returns a `JsonField` value.
+Or create an asynchronous client from the beginning:
```java
-import com.tryfinch.api.core.JsonField;
-import java.util.Optional;
-
-JsonField field = responseObj._field();
+import com.tryfinch.api.client.FinchClientAsync;
+import com.tryfinch.api.client.okhttp.FinchOkHttpClientAsync;
+import com.tryfinch.api.models.HrisDirectoryListPageAsync;
+import com.tryfinch.api.models.HrisDirectoryListParams;
+import java.util.concurrent.CompletableFuture;
-if (field.isMissing()) {
- // Value was not specified in the JSON response
-} else if (field.isNull()) {
- // Value was provided as a literal null
-} else {
- // See if value was provided as a string
- Optional jsonString = field.asString();
+FinchClientAsync client = FinchOkHttpClientAsync.builder()
+ // Configures using the `FINCH_CLIENT_ID`, `FINCH_CLIENT_SECRET` and `FINCH_WEBHOOK_SECRET` environment variables
+ .fromEnv()
+ .accessToken("My Access Token")
+ .build();
- // If the value given by the API did not match the shape that the SDK expects
- // you can deserialise into a custom type
- MyClass myObj = responseObj._field().asUnknown().orElseThrow().convert(MyClass.class);
-}
+HrisDirectoryListParams params = HrisDirectoryListParams.builder().build();
+CompletableFuture page = client.hris().directory().list(params);
```
-### Additional model properties
+The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
-Sometimes, the server response may include additional properties that are not yet available in this library's types. You can access them using the model's `_additionalProperties` method:
+## Error handling
-```java
-import com.tryfinch.api.core.JsonValue;
+The SDK throws custom unchecked exception types:
-JsonValue secret = operationSupportMatrix._additionalProperties().get("secret_field");
-```
+- [`FinchServiceException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/FinchServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:
----
+ | Status | Exception |
+ | ------ | ------------------------------- |
+ | 400 | `BadRequestException` |
+ | 401 | `AuthenticationException` |
+ | 403 | `PermissionDeniedException` |
+ | 404 | `NotFoundException` |
+ | 422 | `UnprocessableEntityException` |
+ | 429 | `RateLimitException` |
+ | 5xx | `InternalServerException` |
+ | others | `UnexpectedStatusCodeException` |
+
+- [`FinchIoException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/FinchIoException.kt): I/O networking errors.
+
+- [`FinchInvalidDataException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/FinchInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response.
+
+- [`FinchException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/FinchException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.
## Pagination
@@ -234,9 +245,21 @@ while (page != null) {
}
```
----
+## Logging
----
+The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
+
+Enable logging by setting the `FINCH_LOG` environment variable to `info`:
+
+```sh
+$ export FINCH_LOG=info
+```
+
+Or to `debug` for more verbose logging:
+
+```sh
+$ export FINCH_LOG=debug
+```
## Webhook Verification
@@ -248,36 +271,23 @@ both of which will raise an error if the signature is invalid.
Note that the "body" parameter must be the raw JSON string sent from the server (do not parse it first).
The `.unwrap()` method can parse this JSON for you.
----
-
-## Error handling
-
-This library throws exceptions in a single hierarchy for easy handling:
-
-- **`FinchException`** - Base exception for all exceptions
+## Network options
-- **`FinchServiceException`** - HTTP errors with a well-formed response body we were able to parse. The exception message and the `.debuggingRequestId()` will be set by the server.
+### Retries
- | 400 | BadRequestException |
- | ------ | ----------------------------- |
- | 401 | AuthenticationException |
- | 403 | PermissionDeniedException |
- | 404 | NotFoundException |
- | 422 | UnprocessableEntityException |
- | 429 | RateLimitException |
- | 5xx | InternalServerException |
- | others | UnexpectedStatusCodeException |
+The SDK automatically retries 2 times by default, with a short exponential backoff.
-- **`FinchIoException`** - I/O networking errors
-- **`FinchInvalidDataException`** - any other exceptions on the client side, e.g.:
- - We failed to serialize the request body
- - We failed to parse the response body (has access to response code and body)
+Only the following error types are retried:
-## Network options
+- Connection errors (for example, due to a network connectivity problem)
+- 408 Request Timeout
+- 409 Conflict
+- 429 Rate Limit
+- 5xx Internal
-### Retries
+The API may also explicitly instruct the SDK to retry or not retry a response.
-Requests that experience certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default. You can provide a `maxRetries` on the client builder to configure this:
+To set a custom number of retries, configure the client using the `maxRetries` method:
```java
import com.tryfinch.api.client.FinchClient;
@@ -285,13 +295,27 @@ import com.tryfinch.api.client.okhttp.FinchOkHttpClient;
FinchClient client = FinchOkHttpClient.builder()
.fromEnv()
+ .accessToken("My Access Token")
.maxRetries(4)
.build();
```
### Timeouts
-Requests time out after 1 minute by default. You can configure this on the client builder:
+Requests time out after 1 minute by default.
+
+To set a custom timeout, configure the method call using the `timeout` method:
+
+```java
+import com.tryfinch.api.models.HrisDirectoryListPage;
+import com.tryfinch.api.models.HrisDirectoryListParams;
+
+HrisDirectoryListPage page = client.hris().directory().list(
+ params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
+);
+```
+
+Or configure the default for all method calls at the client level:
```java
import com.tryfinch.api.client.FinchClient;
@@ -300,13 +324,14 @@ import java.time.Duration;
FinchClient client = FinchOkHttpClient.builder()
.fromEnv()
+ .accessToken("My Access Token")
.timeout(Duration.ofSeconds(30))
.build();
```
### Proxies
-Requests can be routed through a proxy. You can configure this on the client builder:
+To route requests through a proxy, configure the client using the `proxy` method:
```java
import com.tryfinch.api.client.FinchClient;
@@ -316,19 +341,22 @@ import java.net.Proxy;
FinchClient client = FinchOkHttpClient.builder()
.fromEnv()
- .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("example.com", 8080)))
+ .accessToken("My Access Token")
+ .proxy(new Proxy(
+ Proxy.Type.HTTP, new InetSocketAddress(
+ "https://example.com", 8080
+ )
+ ))
.build();
```
-## Making custom/undocumented requests
-
-This library is typed for convenient access to the documented API. If you need to access undocumented params or response properties, the library can still be used.
+## Undocumented API functionality
-### Undocumented request params
+The SDK is typed for convenient usage of the documented API. However, it also supports working with undocumented or not yet supported parts of the API.
-In [Example: creating a resource](#example-creating-a-resource) above, we used the `HrisDirectoryListParams.builder()` to pass to the `list` method of the `directory` service.
+### Parameters
-Sometimes, the API may support other properties that are not yet supported in the Java SDK types. In that case, you can attach them using raw setters:
+To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQueryParam`, or `putAdditionalBodyProperty` methods on any `Params` class:
```java
import com.tryfinch.api.core.JsonValue;
@@ -341,28 +369,137 @@ HrisDirectoryListParams params = HrisDirectoryListParams.builder()
.build();
```
-You can also use the `putAdditionalProperty` method on nested headers, query params, or body objects.
+These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods. You can also set undocumented parameters on nested headers, query params, or body classes using the `putAdditionalProperty` method. These properties can be accessed on the built object later using the `_additionalProperties()` method.
-### Undocumented response properties
+To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](finch-java-core/src/main/kotlin/com/tryfinch/api/core/JsonValue.kt) object to its setter:
-To access undocumented response properties, you can use `res._additionalProperties()` on a response object to get a map of untyped fields of type `Map`. You can then access fields like `res._additionalProperties().get("secret_prop").asString()` or use other helpers defined on the `JsonValue` class to extract it to a desired type.
+```java
+import com.tryfinch.api.models.HrisDirectoryListParams;
-## Logging
+HrisDirectoryListParams params = HrisDirectoryListParams.builder().build();
+```
-We use the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
+### Response properties
-You can enable logging by setting the environment variable `FINCH_LOG` to `info`.
+To access undocumented response properties, call the `_additionalProperties()` method:
-```sh
-$ export FINCH_LOG=info
+```java
+import com.tryfinch.api.core.JsonValue;
+import java.util.Map;
+
+Map additionalProperties = client.accessTokens().create(params)._additionalProperties();
+JsonValue secretPropertyValue = additionalProperties.get("secretProperty");
+
+String result = secretPropertyValue.accept(new JsonValue.Visitor<>() {
+ @Override
+ public String visitNull() {
+ return "It's null!";
+ }
+
+ @Override
+ public String visitBoolean(boolean value) {
+ return "It's a boolean!";
+ }
+
+ @Override
+ public String visitNumber(Number value) {
+ return "It's a number!";
+ }
+
+ // Other methods include `visitMissing`, `visitString`, `visitArray`, and `visitObject`
+ // The default implementation of each unimplemented method delegates to `visitDefault`, which throws by default, but can also be overridden
+});
```
-Or to `debug` for more verbose logging.
+To access a property's raw JSON value, which may be undocumented, call its `_` prefixed method:
-```sh
-$ export FINCH_LOG=debug
+```java
+import com.tryfinch.api.core.JsonField;
+import java.util.Optional;
+
+JsonField code = client.accessTokens().create(params)._code();
+
+if (code.isMissing()) {
+ // The property is absent from the JSON response
+} else if (code.isNull()) {
+ // The property was set to literal null
+} else {
+ // Check if value was provided as a string
+ // Other methods include `asNumber()`, `asBoolean()`, etc.
+ Optional jsonString = code.asString();
+
+ // Try to deserialize into a custom type
+ MyClass myObject = code.asUnknown().orElseThrow().convert(MyClass.class);
+}
```
+### Response validation
+
+In rare cases, the API may return a response that doesn't match the expected type. For example, the SDK may expect a property to contain a `String`, but the API could return something else.
+
+By default, the SDK will not throw an exception in this case. It will throw [`FinchInvalidDataException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/FinchInvalidDataException.kt) only if you directly access the property.
+
+If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
+
+```java
+import com.tryfinch.api.models.CreateAccessTokenResponse;
+
+CreateAccessTokenResponse createAccessTokenResponse = client.accessTokens().create(params).validate();
+```
+
+Or configure the method call to validate the response using the `responseValidation` method:
+
+```java
+import com.tryfinch.api.models.HrisDirectoryListPage;
+import com.tryfinch.api.models.HrisDirectoryListParams;
+
+HrisDirectoryListPage page = client.hris().directory().list(
+ params, RequestOptions.builder().responseValidation(true).build()
+);
+```
+
+Or configure the default for all method calls at the client level:
+
+```java
+import com.tryfinch.api.client.FinchClient;
+import com.tryfinch.api.client.okhttp.FinchOkHttpClient;
+
+FinchClient client = FinchOkHttpClient.builder()
+ .fromEnv()
+ .accessToken("My Access Token")
+ .responseValidation(true)
+ .build();
+```
+
+## FAQ
+
+### Why don't you use plain `enum` classes?
+
+Java `enum` classes are not trivially [forwards compatible](https://www.stainless.com/blog/making-java-enums-forwards-compatible). Using them in the SDK could cause runtime exceptions if the API is updated to respond with a new enum value.
+
+### Why do you represent fields using `JsonField` instead of just plain `T`?
+
+Using `JsonField` enables a few features:
+
+- Allowing usage of [undocumented API functionality](#undocumented-api-functionality)
+- Lazily [validating the API response against the expected shape](#response-validation)
+- Representing absent vs explicitly null values
+
+### Why don't you use [`data` classes](https://kotlinlang.org/docs/data-classes.html)?
+
+It is not [backwards compatible to add new fields to a data class](https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api) and we don't want to introduce a breaking change every time we add a field to a class.
+
+### Why don't you use checked exceptions?
+
+Checked exceptions are widely considered a mistake in the Java programming language. In fact, they were omitted from Kotlin for this reason.
+
+Checked exceptions:
+
+- Are verbose to handle
+- Encourage error handling at the wrong level of abstraction, where nothing can be done about the error
+- Are tedious to propagate due to the [function coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function)
+- Don't play well with lambdas (also due to the function coloring problem)
+
## Semantic versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
diff --git a/build.gradle.kts b/build.gradle.kts
index 12e8bf5d..4b46d1a3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,4 @@
allprojects {
group = "com.tryfinch.api"
- version = "4.0.2" // x-release-please-version
+ version = "4.1.0" // x-release-please-version
}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 8a1d7a10..d1ed374d 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
`kotlin-dsl`
- kotlin("jvm") version "2.1.0"
+ kotlin("jvm") version "2.1.10"
id("com.vanniktech.maven.publish") version "0.28.0"
}
@@ -10,7 +10,7 @@ repositories {
}
dependencies {
- implementation("com.diffplug.spotless:spotless-plugin-gradle:6.25.0")
- implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
+ implementation("com.diffplug.spotless:spotless-plugin-gradle:7.0.2")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10")
implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0")
}
diff --git a/buildSrc/src/main/kotlin/finch.java.gradle.kts b/buildSrc/src/main/kotlin/finch.java.gradle.kts
index a2c35b93..597b6e80 100644
--- a/buildSrc/src/main/kotlin/finch.java.gradle.kts
+++ b/buildSrc/src/main/kotlin/finch.java.gradle.kts
@@ -39,9 +39,13 @@ tasks.named("jar") {
}
}
-tasks.named("test") {
+tasks.withType().configureEach {
useJUnitPlatform()
+ // Run tests in parallel to some degree.
+ maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
+ forkEvery = 100
+
testLogging {
exceptionFormat = TestExceptionFormat.FULL
}
diff --git a/buildSrc/src/main/kotlin/finch.kotlin.gradle.kts b/buildSrc/src/main/kotlin/finch.kotlin.gradle.kts
index c431ae27..9bdebacd 100644
--- a/buildSrc/src/main/kotlin/finch.kotlin.gradle.kts
+++ b/buildSrc/src/main/kotlin/finch.kotlin.gradle.kts
@@ -1,4 +1,5 @@
import com.diffplug.gradle.spotless.SpotlessExtension
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
@@ -20,13 +21,19 @@ configure {
}
tasks.withType().configureEach {
- kotlinOptions {
+ compilerOptions {
freeCompilerArgs = listOf(
"-Xjvm-default=all",
"-Xjdk-release=1.8",
// Suppress deprecation warnings because we may still reference and test deprecated members.
"-Xsuppress-warning=DEPRECATION"
)
- jvmTarget = "1.8"
+ jvmTarget.set(JvmTarget.JVM_1_8)
}
}
+
+// Run tests in parallel to some degree.
+tasks.withType().configureEach {
+ maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
+ forkEvery = 100
+}
diff --git a/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/OkHttpClient.kt b/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/OkHttpClient.kt
index cd06aa82..7c59fad7 100644
--- a/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/OkHttpClient.kt
+++ b/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/OkHttpClient.kt
@@ -31,10 +31,7 @@ class OkHttpClient
private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val baseUrl: HttpUrl) :
HttpClient {
- override fun execute(
- request: HttpRequest,
- requestOptions: RequestOptions,
- ): HttpResponse {
+ override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
val call = newCall(request, requestOptions)
return try {
@@ -120,13 +117,13 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
) {
builder.header(
"X-Stainless-Read-Timeout",
- Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString()
+ Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
)
}
if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
builder.header(
"X-Stainless-Timeout",
- Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString()
+ Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
)
}
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClient.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClient.kt
index 424046b4..bf99f84a 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClient.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClient.kt
@@ -64,7 +64,7 @@ interface FinchClient {
clientId: String,
clientSecret: String,
code: String,
- redirectUri: String?
+ redirectUri: String?,
): String
fun getAuthUrl(products: String, redirectUri: String, sandbox: Boolean): String
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientAsync.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientAsync.kt
index fcba9566..e20d730b 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientAsync.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientAsync.kt
@@ -65,7 +65,7 @@ interface FinchClientAsync {
clientId: String,
clientSecret: String,
code: String,
- redirectUri: String?
+ redirectUri: String?,
): CompletableFuture
fun getAuthUrl(products: String, redirectUri: String, sandbox: Boolean): String
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientAsyncImpl.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientAsyncImpl.kt
index 2e0e7395..1a33fcc6 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientAsyncImpl.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientAsyncImpl.kt
@@ -38,9 +38,7 @@ import com.tryfinch.api.services.async.WebhookServiceAsyncImpl
import java.net.URLEncoder
import java.util.concurrent.CompletableFuture
-class FinchClientAsyncImpl(
- private val clientOptions: ClientOptions,
-) : FinchClientAsync {
+class FinchClientAsyncImpl(private val clientOptions: ClientOptions) : FinchClientAsync {
private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
@@ -121,7 +119,7 @@ class FinchClientAsyncImpl(
clientId: String,
clientSecret: String,
code: String,
- redirectUri: String?
+ redirectUri: String?,
): CompletableFuture {
if (clientOptions.clientId == null) {
throw FinchException("clientId must be set in order to call getAccessToken")
@@ -136,12 +134,7 @@ class FinchClientAsyncImpl(
.body(
json(
clientOptions.jsonMapper,
- GetAccessTokenParams(
- clientId,
- clientSecret,
- code,
- redirectUri,
- )
+ GetAccessTokenParams(clientId, clientSecret, code, redirectUri),
)
)
.build()
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientImpl.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientImpl.kt
index 84439057..3f10b2e8 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientImpl.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/client/FinchClientImpl.kt
@@ -37,9 +37,7 @@ import com.tryfinch.api.services.blocking.WebhookService
import com.tryfinch.api.services.blocking.WebhookServiceImpl
import java.net.URLEncoder
-class FinchClientImpl(
- private val clientOptions: ClientOptions,
-) : FinchClient {
+class FinchClientImpl(private val clientOptions: ClientOptions) : FinchClient {
private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
@@ -110,7 +108,7 @@ class FinchClientImpl(
clientId: String,
clientSecret: String,
code: String,
- redirectUri: String?
+ redirectUri: String?,
): String {
if (clientOptions.clientId == null) {
throw FinchException("clientId must be set in order to call getAccessToken")
@@ -125,12 +123,7 @@ class FinchClientImpl(
.body(
json(
clientOptions.jsonMapper,
- GetAccessTokenParams(
- clientId,
- clientSecret,
- code,
- redirectUri,
- )
+ GetAccessTokenParams(clientId, clientSecret, code, redirectUri),
)
)
.build()
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/BaseDeserializer.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/BaseDeserializer.kt
index cc970627..c850405c 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/BaseDeserializer.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/BaseDeserializer.kt
@@ -18,7 +18,7 @@ abstract class BaseDeserializer(type: KClass) :
override fun createContextual(
context: DeserializationContext,
- property: BeanProperty?
+ property: BeanProperty?,
): JsonDeserializer {
return this
}
@@ -32,7 +32,7 @@ abstract class BaseDeserializer(type: KClass) :
protected fun ObjectCodec.tryDeserialize(
node: JsonNode,
type: TypeReference,
- validate: (T) -> Unit = {}
+ validate: (T) -> Unit = {},
): T? {
return try {
readValue(treeAsTokens(node), type).apply(validate)
@@ -46,7 +46,7 @@ abstract class BaseDeserializer(type: KClass) :
protected fun ObjectCodec.tryDeserialize(
node: JsonNode,
type: JavaType,
- validate: (T) -> Unit = {}
+ validate: (T) -> Unit = {},
): T? {
return try {
readValue(treeAsTokens(node), type).apply(validate)
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/ClientOptions.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/ClientOptions.kt
index 15542787..f34021de 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/ClientOptions.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/ClientOptions.kt
@@ -212,7 +212,7 @@ private constructor(
if (!username.isEmpty() && !password.isEmpty()) {
headers.put(
"Authorization",
- "Basic ${Base64.getEncoder().encodeToString("$username:$password".toByteArray())}"
+ "Basic ${Base64.getEncoder().encodeToString("$username:$password".toByteArray())}",
)
}
}
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/HttpRequestBodies.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/HttpRequestBodies.kt
index 9361c66b..4d3d1862 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/HttpRequestBodies.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/HttpRequestBodies.kt
@@ -10,10 +10,7 @@ import java.io.OutputStream
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
@JvmSynthetic
-internal inline fun json(
- jsonMapper: JsonMapper,
- value: T,
-): HttpRequestBody {
+internal inline fun json(jsonMapper: JsonMapper, value: T): HttpRequestBody {
return object : HttpRequestBody {
private var cachedBytes: ByteArray? = null
@@ -49,7 +46,7 @@ internal inline fun json(
@JvmSynthetic
internal fun multipartFormData(
jsonMapper: JsonMapper,
- parts: Array?>
+ parts: Array?>,
): HttpRequestBody {
val builder = MultipartEntityBuilder.create()
parts.forEach { part ->
@@ -66,14 +63,14 @@ internal fun multipartFormData(
part.name,
buffer.toByteArray(),
part.contentType,
- part.filename
+ part.filename,
)
}
is Boolean ->
builder.addTextBody(
part.name,
if (part.value) "true" else "false",
- part.contentType
+ part.contentType,
)
is Int -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
is Long -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/PrepareRequest.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/PrepareRequest.kt
index 974ae9d8..6e1aaafb 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/PrepareRequest.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/PrepareRequest.kt
@@ -17,7 +17,7 @@ internal fun HttpRequest.prepare(clientOptions: ClientOptions, params: Params):
@JvmSynthetic
internal fun HttpRequest.prepareAsync(
clientOptions: ClientOptions,
- params: Params
+ params: Params,
): CompletableFuture =
// This async version exists to make it easier to add async specific preparation logic in the
// future.
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/RequestOptions.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/RequestOptions.kt
index b31943e2..cee1a369 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/RequestOptions.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/RequestOptions.kt
@@ -2,11 +2,7 @@ package com.tryfinch.api.core
import java.time.Duration
-class RequestOptions
-private constructor(
- val responseValidation: Boolean?,
- val timeout: Duration?,
-) {
+class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Duration?) {
fun applyDefaults(options: RequestOptions): RequestOptions {
return RequestOptions(
responseValidation = this.responseValidation ?: options.responseValidation,
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Values.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Values.kt
index 16ad91e7..0879a0f2 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Values.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Values.kt
@@ -59,36 +59,69 @@ sealed class JsonField {
fun asBoolean(): Optional =
when (this) {
is JsonBoolean -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Boolean)
else -> Optional.empty()
}
fun asNumber(): Optional =
when (this) {
is JsonNumber -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Number)
else -> Optional.empty()
}
fun asString(): Optional =
when (this) {
is JsonString -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? String)
else -> Optional.empty()
}
fun asStringOrThrow(): String =
- when (this) {
- is JsonString -> value
- else -> throw FinchInvalidDataException("Value is not a string")
- }
+ asString().orElseThrow { FinchInvalidDataException("Value is not a string") }
fun asArray(): Optional> =
when (this) {
is JsonArray -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? List<*>)?.map {
+ try {
+ JsonValue.from(it)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a list, but not all values are convertible to
+ // `JsonValue`.
+ return Optional.empty()
+ }
+ }
+ )
else -> Optional.empty()
}
fun asObject(): Optional