diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 121ace97..94046873 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,4 +52,3 @@ jobs:
- name: Run tests
run: ./scripts/test
-
diff --git a/.gitignore b/.gitignore
index 39c31e3e..4e81838d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
.prism.log
.gradle
.idea
+.kotlin
build
codegen.log
kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index bd7f3844..8e76abb5 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "4.2.0"
+ ".": "5.0.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index bed01067..e3e52833 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,2 +1,4 @@
-configured_endpoints: 40
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-7a816d4a5f0039230590a6662f3513d5756344ca662761ecbc49016593f65836.yml
+configured_endpoints: 45
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-14d375aab89e6b212fe459805a42d6ea7d7da8eae2037ae710a187d06911be1d.yml
+openapi_spec_hash: 08b86ecbec3323717d48e4aaee48ed54
+config_hash: ce10384813f68ba3fed61c7b601b396b
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4a094f3..be5fb27e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,107 @@
# Changelog
+## 5.0.0 (2025-04-08)
+
+Full Changelog: [v4.2.0...v5.0.0](https://github.com/Finch-API/finch-api-java/compare/v4.2.0...v5.0.0)
+
+### ⚠ BREAKING CHANGES
+
+* **client:** refactor exception structure and methods ([#505](https://github.com/Finch-API/finch-api-java/issues/505))
+* **client:** refactor multipart formdata impl ([#473](https://github.com/Finch-API/finch-api-java/issues/473))
+
+### Features
+
+* **api:** add new endpoints for pay statement items ([#526](https://github.com/Finch-API/finch-api-java/issues/526)) ([1ab19a3](https://github.com/Finch-API/finch-api-java/commit/1ab19a34148407ce143df4d49697af386e17f560))
+* **api:** api update ([#513](https://github.com/Finch-API/finch-api-java/issues/513)) ([5849824](https://github.com/Finch-API/finch-api-java/commit/5849824f4a7704255935cd6e13c1761427b8ca62))
+* **api:** api update ([#515](https://github.com/Finch-API/finch-api-java/issues/515)) ([dffd823](https://github.com/Finch-API/finch-api-java/commit/dffd823f5649b18ca44e06d5f0f8bcb68ba43879))
+* **api:** api update ([#517](https://github.com/Finch-API/finch-api-java/issues/517)) ([0123042](https://github.com/Finch-API/finch-api-java/commit/01230422dedfd1f4432fc84e4ed517eaf31319a9))
+* **api:** api update ([#520](https://github.com/Finch-API/finch-api-java/issues/520)) ([a6b3092](https://github.com/Finch-API/finch-api-java/commit/a6b3092d8ce9e17f78f267d36467ca2057e6ac1f))
+* **api:** api update ([#523](https://github.com/Finch-API/finch-api-java/issues/523)) ([842bb3f](https://github.com/Finch-API/finch-api-java/commit/842bb3fa15300004eea197bb5a8f57365f1eee78))
+* **api:** manual updates ([#477](https://github.com/Finch-API/finch-api-java/issues/477)) ([83f00bd](https://github.com/Finch-API/finch-api-java/commit/83f00bd2aadd9049da1d8365dfa9bdd03d52a159))
+* **api:** manual updates ([#527](https://github.com/Finch-API/finch-api-java/issues/527)) ([b5a2343](https://github.com/Finch-API/finch-api-java/commit/b5a2343b89278949689abdfb5b1303713ac57fbf))
+* **client:** accept `InputStream` and `Path` for file params ([#479](https://github.com/Finch-API/finch-api-java/issues/479)) ([721cd2c](https://github.com/Finch-API/finch-api-java/commit/721cd2c2696a8929f3b7bbe27ba5df82f872df4f))
+* **client:** add enum validation method ([1bd0c25](https://github.com/Finch-API/finch-api-java/commit/1bd0c2538ce50578fcd7ae51c944cdfb0736aa9b))
+* **client:** allow configuring timeouts granularly ([#470](https://github.com/Finch-API/finch-api-java/issues/470)) ([4d3b414](https://github.com/Finch-API/finch-api-java/commit/4d3b414c120045834dc829e12cbc185c88fe2764))
+* **client:** detect binary incompatible jackson versions ([#480](https://github.com/Finch-API/finch-api-java/issues/480)) ([fcf521a](https://github.com/Finch-API/finch-api-java/commit/fcf521a7e262ad96d7f3703114b221a4d2622d50))
+* **client:** expose request body setter and getter ([#524](https://github.com/Finch-API/finch-api-java/issues/524)) ([1b97fe6](https://github.com/Finch-API/finch-api-java/commit/1b97fe6e3dc9b0e83c9f267fd37e985258b958e5))
+* **client:** make datetime deserialization more lenient ([#522](https://github.com/Finch-API/finch-api-java/issues/522)) ([c7265b7](https://github.com/Finch-API/finch-api-java/commit/c7265b7fec4baacc24dba2415a9bb734731a46e3))
+* **client:** make union deserialization more robust ([#521](https://github.com/Finch-API/finch-api-java/issues/521)) ([1bd0c25](https://github.com/Finch-API/finch-api-java/commit/1bd0c2538ce50578fcd7ae51c944cdfb0736aa9b))
+* **client:** support a lower jackson version ([#512](https://github.com/Finch-API/finch-api-java/issues/512)) ([2897011](https://github.com/Finch-API/finch-api-java/commit/289701192191a42ca4f28eabb6ac73651f7d9909))
+* **client:** support raw response access ([#471](https://github.com/Finch-API/finch-api-java/issues/471)) ([481c99c](https://github.com/Finch-API/finch-api-java/commit/481c99cf2bc062b137c99a69bfce289c3e570a5a))
+* **client:** throw on incompatible jackson version ([2897011](https://github.com/Finch-API/finch-api-java/commit/289701192191a42ca4f28eabb6ac73651f7d9909))
+* generate and publish docs ([#481](https://github.com/Finch-API/finch-api-java/issues/481)) ([c5c4196](https://github.com/Finch-API/finch-api-java/commit/c5c4196d609a6db22887e580ad14323506f0aa2a))
+
+
+### Bug Fixes
+
+* **client:** don't call `validate()` during deserialization if we don't have to ([#516](https://github.com/Finch-API/finch-api-java/issues/516)) ([e988e9d](https://github.com/Finch-API/finch-api-java/commit/e988e9da5f57c5d201021842312fd60ae4c41ae1))
+* **client:** limit json deserialization coercion ([#518](https://github.com/Finch-API/finch-api-java/issues/518)) ([0bd7dd5](https://github.com/Finch-API/finch-api-java/commit/0bd7dd5974c4d30f0adc02b6af1f4f227d2c743e))
+* **client:** map deserialization bug ([8d03e6d](https://github.com/Finch-API/finch-api-java/commit/8d03e6dd61fa910bcd9788a58ad4cbd0141356ac))
+* **client:** return `Optional<T>` instead of `Optional extends T>` ([#529](https://github.com/Finch-API/finch-api-java/issues/529)) ([8b601dc](https://github.com/Finch-API/finch-api-java/commit/8b601dc398296e351a1f0590ef858063fec9f48a))
+* **client:** support kotlin 1.8 runtime ([#502](https://github.com/Finch-API/finch-api-java/issues/502)) ([0fb2cbc](https://github.com/Finch-API/finch-api-java/commit/0fb2cbc5711394b6f823e5e9c985cd64b11bbc21))
+* compilation error ([ebc6f30](https://github.com/Finch-API/finch-api-java/commit/ebc6f30543123460b1c6f297c6c1ad32453ff77c))
+* pluralize `list` response variables ([#519](https://github.com/Finch-API/finch-api-java/issues/519)) ([17c4e8f](https://github.com/Finch-API/finch-api-java/commit/17c4e8fbbf4698f67ddcfedb383d677e5e5a9d1d))
+* **tests:** fix jackson attributes ([3ccd785](https://github.com/Finch-API/finch-api-java/commit/3ccd785da8291d41d74aa71bfaa604b5a38295b6))
+
+
+### Performance Improvements
+
+* **client:** cached parsed type in `HttpResponseFor` ([#525](https://github.com/Finch-API/finch-api-java/issues/525)) ([16616bf](https://github.com/Finch-API/finch-api-java/commit/16616bf6a5dab5996375d41d9b8404fb2928ac89))
+
+
+### Chores
+
+* **client:** expose `Optional`, not nullable, from `ClientOptions` ([#476](https://github.com/Finch-API/finch-api-java/issues/476)) ([fc827bc](https://github.com/Finch-API/finch-api-java/commit/fc827bc6bd38e80bf7f09b241e559e46d7adcf2a))
+* **client:** refactor exception structure and methods ([#505](https://github.com/Finch-API/finch-api-java/issues/505)) ([4be4f06](https://github.com/Finch-API/finch-api-java/commit/4be4f068408cf19a55f8fcc1b64aac6fc24fd73d))
+* **client:** refactor multipart formdata impl ([#473](https://github.com/Finch-API/finch-api-java/issues/473)) ([7cb2a7f](https://github.com/Finch-API/finch-api-java/commit/7cb2a7f642d7bcaf67deec5d5f355682fe9b8e3e))
+* **client:** remove unnecessary json state from some query param classes ([1bd0c25](https://github.com/Finch-API/finch-api-java/commit/1bd0c2538ce50578fcd7ae51c944cdfb0736aa9b))
+* **internal:** add `.kotlin` to `.gitignore` ([#483](https://github.com/Finch-API/finch-api-java/issues/483)) ([473058a](https://github.com/Finch-API/finch-api-java/commit/473058a82d86a318c3c467d60e6cedfcd416fb22))
+* **internal:** add generated comment ([#496](https://github.com/Finch-API/finch-api-java/issues/496)) ([9543169](https://github.com/Finch-API/finch-api-java/commit/9543169b8b1af2c39666c6be1df6ea40f0e1a67f))
+* **internal:** add invalid json deserialization tests ([1bd0c25](https://github.com/Finch-API/finch-api-java/commit/1bd0c2538ce50578fcd7ae51c944cdfb0736aa9b))
+* **internal:** add json roundtripping tests ([1bd0c25](https://github.com/Finch-API/finch-api-java/commit/1bd0c2538ce50578fcd7ae51c944cdfb0736aa9b))
+* **internal:** add some tests for union classes ([#501](https://github.com/Finch-API/finch-api-java/issues/501)) ([4aff498](https://github.com/Finch-API/finch-api-java/commit/4aff4981754d9336e20b29713c5b95a03819dfec))
+* **internal:** codegen related update ([60e5c13](https://github.com/Finch-API/finch-api-java/commit/60e5c131ff4b41265efd39bff4c29a6c1a4df442))
+* **internal:** codegen related update ([#469](https://github.com/Finch-API/finch-api-java/issues/469)) ([a308954](https://github.com/Finch-API/finch-api-java/commit/a3089549cd0743446d2b906db5fcd830f579ddd8))
+* **internal:** codegen related update ([#488](https://github.com/Finch-API/finch-api-java/issues/488)) ([5742de2](https://github.com/Finch-API/finch-api-java/commit/5742de269c36314324b0fb8f4214efb87af09dff))
+* **internal:** codegen related update ([#504](https://github.com/Finch-API/finch-api-java/issues/504)) ([9be38e0](https://github.com/Finch-API/finch-api-java/commit/9be38e0fce500731d023902658ec0fa8926b27e1))
+* **internal:** codegen related update ([#531](https://github.com/Finch-API/finch-api-java/issues/531)) ([433f15a](https://github.com/Finch-API/finch-api-java/commit/433f15ad426ef4e24d4ca9c0bf7254d2b6a859bc))
+* **internal:** delete duplicate tests ([d1b1bd2](https://github.com/Finch-API/finch-api-java/commit/d1b1bd2847f0ffa069f9d9bce5702f093b129797))
+* **internal:** delete unused methods and annotations ([#514](https://github.com/Finch-API/finch-api-java/issues/514)) ([8d03e6d](https://github.com/Finch-API/finch-api-java/commit/8d03e6dd61fa910bcd9788a58ad4cbd0141356ac))
+* **internal:** don't use `JvmOverloads` in interfaces ([0d38744](https://github.com/Finch-API/finch-api-java/commit/0d387447276d8cbe1dad8c4ab69fc6d72e5b36d6))
+* **internal:** fix example formatting ([#508](https://github.com/Finch-API/finch-api-java/issues/508)) ([89c25c1](https://github.com/Finch-API/finch-api-java/commit/89c25c19eca3db5ef766e64e605291d6dad69d05))
+* **internal:** generate more tests ([35fd786](https://github.com/Finch-API/finch-api-java/commit/35fd7864ce86a276116c74475f5654579f5fd7a2))
+* **internal:** make multipart assertions more robust ([f0417a8](https://github.com/Finch-API/finch-api-java/commit/f0417a891f12e01d7040af3ccb07e13f07ca8034))
+* **internal:** make test classes internal ([#495](https://github.com/Finch-API/finch-api-java/issues/495)) ([2048a6a](https://github.com/Finch-API/finch-api-java/commit/2048a6a29711b2436894c0498bcd97c89ce7ac4c))
+* **internal:** reenable warnings as errors ([#485](https://github.com/Finch-API/finch-api-java/issues/485)) ([0d38744](https://github.com/Finch-API/finch-api-java/commit/0d387447276d8cbe1dad8c4ab69fc6d72e5b36d6))
+* **internal:** refactor enum query param serialization ([#503](https://github.com/Finch-API/finch-api-java/issues/503)) ([8894d83](https://github.com/Finch-API/finch-api-java/commit/8894d8307db6b7bdc92319b6f356addec96c64a5))
+* **internal:** refactor query param serialization impl and tests ([#498](https://github.com/Finch-API/finch-api-java/issues/498)) ([6da412b](https://github.com/Finch-API/finch-api-java/commit/6da412b3e388bc9034c4ec7bdfd5579657b39a47))
+* **internal:** refactor some test assertions ([d1b1bd2](https://github.com/Finch-API/finch-api-java/commit/d1b1bd2847f0ffa069f9d9bce5702f093b129797))
+* **internal:** reformat some tests ([#500](https://github.com/Finch-API/finch-api-java/issues/500)) ([35fd786](https://github.com/Finch-API/finch-api-java/commit/35fd7864ce86a276116c74475f5654579f5fd7a2))
+* **internal:** remove unnecessary `assertNotNull` calls ([f0417a8](https://github.com/Finch-API/finch-api-java/commit/f0417a891f12e01d7040af3ccb07e13f07ca8034))
+* **internal:** remove unnecessary import ([#509](https://github.com/Finch-API/finch-api-java/issues/509)) ([4b12e68](https://github.com/Finch-API/finch-api-java/commit/4b12e6826583668726201f446ccb63bd4e97d51c))
+* **internal:** rename `getPathParam` ([#499](https://github.com/Finch-API/finch-api-java/issues/499)) ([d1b1bd2](https://github.com/Finch-API/finch-api-java/commit/d1b1bd2847f0ffa069f9d9bce5702f093b129797))
+* **internal:** reorder some params methodsc ([d1b1bd2](https://github.com/Finch-API/finch-api-java/commit/d1b1bd2847f0ffa069f9d9bce5702f093b129797))
+* **internal:** swap from `getNullable` to `getOptional` ([#528](https://github.com/Finch-API/finch-api-java/issues/528)) ([70033bd](https://github.com/Finch-API/finch-api-java/commit/70033bd2f85bbb1cccefbff8eb86193f912e92c2))
+* **internal:** use `getOrNull` instead of `orElse(null)` ([#484](https://github.com/Finch-API/finch-api-java/issues/484)) ([9c0806b](https://github.com/Finch-API/finch-api-java/commit/9c0806b3134e745134ee74cbeda97d2e80584c57))
+* **tests:** improve enum examples ([#532](https://github.com/Finch-API/finch-api-java/issues/532)) ([775392d](https://github.com/Finch-API/finch-api-java/commit/775392dc621cd9a1f297897b00aba7264c294e2a))
+
+
+### Documentation
+
+* add `build` method comments ([#497](https://github.com/Finch-API/finch-api-java/issues/497)) ([d199989](https://github.com/Finch-API/finch-api-java/commit/d199989894e98456e329085b6904bc58f889bae6))
+* add comments to `JsonField` classes ([8b601dc](https://github.com/Finch-API/finch-api-java/commit/8b601dc398296e351a1f0590ef858063fec9f48a))
+* add raw response readme documentation ([#474](https://github.com/Finch-API/finch-api-java/issues/474)) ([81a5824](https://github.com/Finch-API/finch-api-java/commit/81a5824dbff19ae8328a747aed49dc5ea1fbc2c8))
+* deduplicate and refine comments ([#494](https://github.com/Finch-API/finch-api-java/issues/494)) ([651b10e](https://github.com/Finch-API/finch-api-java/commit/651b10e981e1c1e182fce79205176e637930473b))
+* document `JsonValue` construction in readme ([#487](https://github.com/Finch-API/finch-api-java/issues/487)) ([e790ad5](https://github.com/Finch-API/finch-api-java/commit/e790ad57e21491a0edcada3ae2d544f3ae7eb35e))
+* document how to forcibly omit required field ([7989f32](https://github.com/Finch-API/finch-api-java/commit/7989f32f6893b9374514ccda99c88d8ab093bf11))
+* minor readme tweak ([#511](https://github.com/Finch-API/finch-api-java/issues/511)) ([12e8d47](https://github.com/Finch-API/finch-api-java/commit/12e8d474f44c2e32c74eed6beb57a8fa2080de03))
+* note required fields in `builder` javadoc ([#475](https://github.com/Finch-API/finch-api-java/issues/475)) ([2d0c4c8](https://github.com/Finch-API/finch-api-java/commit/2d0c4c8f7d3b629db18937db95415474b10f8bc9))
+* refine comments on multipart params ([#507](https://github.com/Finch-API/finch-api-java/issues/507)) ([f0417a8](https://github.com/Finch-API/finch-api-java/commit/f0417a891f12e01d7040af3ccb07e13f07ca8034))
+* revise readme docs about nested params ([#486](https://github.com/Finch-API/finch-api-java/issues/486)) ([3682e62](https://github.com/Finch-API/finch-api-java/commit/3682e62316dcbac48b8c2a7e3b1df8f19ae5d96e))
+* swap examples used in readme ([#530](https://github.com/Finch-API/finch-api-java/issues/530)) ([7989f32](https://github.com/Finch-API/finch-api-java/commit/7989f32f6893b9374514ccda99c88d8ab093bf11))
+* update readme exception docs ([#510](https://github.com/Finch-API/finch-api-java/issues/510)) ([949341a](https://github.com/Finch-API/finch-api-java/commit/949341a607502df838f4c062d4a4afaaa4bdf2b8))
+* update URLs from stainlessapi.com to stainless.com ([#467](https://github.com/Finch-API/finch-api-java/issues/467)) ([238b853](https://github.com/Finch-API/finch-api-java/commit/238b853b930f203759499a7d9540c35b586a3915))
+
## 4.2.0 (2025-02-27)
Full Changelog: [v4.1.0...v4.2.0](https://github.com/Finch-API/finch-api-java/compare/v4.1.0...v4.2.0)
diff --git a/README.md b/README.md
index a4e73200..e0b9c13b 100644
--- a/README.md
+++ b/README.md
@@ -2,19 +2,22 @@
-[](https://central.sonatype.com/artifact/com.tryfinch.api/finch-java/4.2.0)
+[](https://central.sonatype.com/artifact/com.tryfinch.api/finch-java/5.0.0)
+[](https://javadoc.io/doc/com.tryfinch.api/finch-java/5.0.0)
-The Finch Java SDK provides convenient access to the Finch REST API from applications written in Java.
+The Finch Java SDK provides convenient access to the [Finch REST API](https://developer.tryfinch.com/) from applications written in Java.
The Finch Java SDK is similar to the Finch Kotlin SDK but with minor differences that make it more ergonomic for use in Java, such as `Optional` instead of nullable values, `Stream` instead of `Sequence`, and `CompletableFuture` instead of suspend functions.
-It is generated with [Stainless](https://www.stainlessapi.com/).
+It is generated with [Stainless](https://www.stainless.com/).
-The REST API documentation can be found [in the Finch Documentation Center](https://developer.tryfinch.com/).
+
+
+The REST API documentation can be found on [developer.tryfinch.com](https://developer.tryfinch.com/). Javadocs are also available on [javadoc.io](https://javadoc.io/doc/com.tryfinch.api/finch-java/5.0.0).
----
+
## Installation
@@ -23,16 +26,16 @@ The REST API documentation can be found [in the Finch Documentation Center](htt
### Gradle
```kotlin
-implementation("com.tryfinch.api:finch-java:4.2.0")
+implementation("com.tryfinch.api:finch-java:5.0.0")
```
### Maven
```xml
- com.tryfinch.api
- finch-java
- 4.2.0
+ com.tryfinch.api
+ finch-java
+ 5.0.0
```
@@ -164,22 +167,48 @@ CompletableFuture page = client.hris().directory().l
The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
+## Raw responses
+
+The SDK defines methods that deserialize responses into instances of Java classes. However, these methods don't provide access to the response headers, status code, or the raw response body.
+
+To access this data, prefix any HTTP method call on a client or service with `withRawResponse()`:
+
+```java
+import com.tryfinch.api.core.http.Headers;
+import com.tryfinch.api.core.http.HttpResponseFor;
+import com.tryfinch.api.models.HrisDirectoryListPage;
+import com.tryfinch.api.models.HrisDirectoryListParams;
+
+HttpResponseFor page = client.hris().directory().withRawResponse().list();
+
+int statusCode = page.statusCode();
+Headers headers = page.headers();
+```
+
+You can still deserialize the response into an instance of a Java class if needed:
+
+```java
+import com.tryfinch.api.models.HrisDirectoryListPage;
+
+HrisDirectoryListPage parsedPage = page.parse();
+```
+
## Error handling
The SDK throws custom unchecked exception types:
- [`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` |
+ | Status | Exception |
+ | ------ | --------------------------------------------------------------------------------------------------------------------------- |
+ | 400 | [`BadRequestException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/BadRequestException.kt) |
+ | 401 | [`UnauthorizedException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/UnauthorizedException.kt) |
+ | 403 | [`PermissionDeniedException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/PermissionDeniedException.kt) |
+ | 404 | [`NotFoundException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/NotFoundException.kt) |
+ | 422 | [`UnprocessableEntityException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/UnprocessableEntityException.kt) |
+ | 429 | [`RateLimitException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/RateLimitException.kt) |
+ | 5xx | [`InternalServerException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/InternalServerException.kt) |
+ | others | [`UnexpectedStatusCodeException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/UnexpectedStatusCodeException.kt) |
- [`FinchIoException`](finch-java-core/src/main/kotlin/com/tryfinch/api/errors/FinchIoException.kt): I/O networking errors.
@@ -361,9 +390,9 @@ HrisDirectoryListParams params = HrisDirectoryListParams.builder()
.build();
```
-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.
+These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods.
-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 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/Values.kt) object to its setter:
```java
import com.tryfinch.api.models.HrisDirectoryListParams;
@@ -371,6 +400,59 @@ import com.tryfinch.api.models.HrisDirectoryListParams;
HrisDirectoryListParams params = HrisDirectoryListParams.builder().build();
```
+The most straightforward way to create a [`JsonValue`](finch-java-core/src/main/kotlin/com/tryfinch/api/core/Values.kt) is using its `from(...)` method:
+
+```java
+import com.tryfinch.api.core.JsonValue;
+import java.util.List;
+import java.util.Map;
+
+// Create primitive JSON values
+JsonValue nullValue = JsonValue.from(null);
+JsonValue booleanValue = JsonValue.from(true);
+JsonValue numberValue = JsonValue.from(42);
+JsonValue stringValue = JsonValue.from("Hello World!");
+
+// Create a JSON array value equivalent to `["Hello", "World"]`
+JsonValue arrayValue = JsonValue.from(List.of(
+ "Hello", "World"
+));
+
+// Create a JSON object value equivalent to `{ "a": 1, "b": 2 }`
+JsonValue objectValue = JsonValue.from(Map.of(
+ "a", 1,
+ "b", 2
+));
+
+// Create an arbitrarily nested JSON equivalent to:
+// {
+// "a": [1, 2],
+// "b": [3, 4]
+// }
+JsonValue complexValue = JsonValue.from(Map.of(
+ "a", List.of(
+ 1, 2
+ ),
+ "b", List.of(
+ 3, 4
+ )
+));
+```
+
+Normally a `Builder` class's `build` method will throw [`IllegalStateException`](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html) if any required parameter or property is unset.
+
+To forcibly omit a required parameter or property, pass [`JsonMissing`](finch-java-core/src/main/kotlin/com/tryfinch/api/core/Values.kt):
+
+```java
+import com.tryfinch.api.core.JsonMissing;
+import com.tryfinch.api.models.AccessTokenCreateParams;
+import com.tryfinch.api.models.HrisDirectoryListParams;
+
+HrisDirectoryListParams params = AccessTokenCreateParams.builder()
+ .code(JsonMissing.of())
+ .build();
+```
+
### Response properties
To access undocumented response properties, call the `_additionalProperties()` method:
diff --git a/SECURITY.md b/SECURITY.md
index 6cef554f..b6499508 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,9 +2,9 @@
## Reporting Security Issues
-This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
-To report a security issue, please contact the Stainless team at security@stainlessapi.com.
+To report a security issue, please contact the Stainless team at security@stainless.com.
## Responsible Disclosure
diff --git a/build.gradle.kts b/build.gradle.kts
index 5b66da78..371fde06 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,23 @@
+plugins {
+ id("org.jetbrains.dokka") version "2.0.0"
+}
+
+repositories {
+ mavenCentral()
+}
+
allprojects {
group = "com.tryfinch.api"
- version = "4.2.0" // x-release-please-version
+ version = "5.0.0" // x-release-please-version
+}
+
+subprojects {
+ apply(plugin = "org.jetbrains.dokka")
+}
+
+// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks
+tasks.named("dokkaJavadocCollector").configure {
+ subprojects.flatMap { it.tasks }
+ .filter { it.project.name != "finch-java" && it.name == "dokkaJavadocJar" }
+ .forEach { mustRunAfter(it) }
}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index d1ed374d..778c89de 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
`kotlin-dsl`
- kotlin("jvm") version "2.1.10"
+ kotlin("jvm") version "1.9.20"
id("com.vanniktech.maven.publish") version "0.28.0"
}
@@ -11,6 +11,6 @@ repositories {
dependencies {
implementation("com.diffplug.spotless:spotless-plugin-gradle:7.0.2")
- implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
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 597b6e80..e39d9ac6 100644
--- a/buildSrc/src/main/kotlin/finch.java.gradle.kts
+++ b/buildSrc/src/main/kotlin/finch.java.gradle.kts
@@ -23,6 +23,9 @@ java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
+
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType().configureEach {
diff --git a/buildSrc/src/main/kotlin/finch.kotlin.gradle.kts b/buildSrc/src/main/kotlin/finch.kotlin.gradle.kts
index 9bdebacd..2fcf511b 100644
--- a/buildSrc/src/main/kotlin/finch.kotlin.gradle.kts
+++ b/buildSrc/src/main/kotlin/finch.kotlin.gradle.kts
@@ -1,6 +1,6 @@
import com.diffplug.gradle.spotless.SpotlessExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
plugins {
id("finch.java")
@@ -11,6 +11,20 @@ kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
+
+ compilerOptions {
+ freeCompilerArgs = listOf(
+ "-Xjvm-default=all",
+ "-Xjdk-release=1.8",
+ // Suppress deprecation warnings because we may still reference and test deprecated members.
+ // TODO: Replace with `-Xsuppress-warning=DEPRECATION` once we use Kotlin compiler 2.1.0+.
+ "-nowarn",
+ )
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ languageVersion.set(KotlinVersion.KOTLIN_1_8)
+ apiVersion.set(KotlinVersion.KOTLIN_1_8)
+ coreLibrariesVersion = "1.8.0"
+ }
}
configure {
@@ -20,18 +34,6 @@ configure {
}
}
-tasks.withType().configureEach {
- 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.set(JvmTarget.JVM_1_8)
- }
-}
-
// Run tests in parallel to some degree.
tasks.withType().configureEach {
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
diff --git a/buildSrc/src/main/kotlin/finch.publish.gradle.kts b/buildSrc/src/main/kotlin/finch.publish.gradle.kts
index e193185b..814dfc3e 100644
--- a/buildSrc/src/main/kotlin/finch.publish.gradle.kts
+++ b/buildSrc/src/main/kotlin/finch.publish.gradle.kts
@@ -1,3 +1,5 @@
+import com.vanniktech.maven.publish.JavadocJar
+import com.vanniktech.maven.publish.KotlinJvm
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import com.vanniktech.maven.publish.SonatypeHost
@@ -19,6 +21,12 @@ configure {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
coordinates(project.group.toString(), project.name, project.version.toString())
+ configure(
+ KotlinJvm(
+ javadocJar = JavadocJar.Dokka("dokkaJavadoc"),
+ sourcesJar = true,
+ )
+ )
pom {
name.set("API Reference")
diff --git a/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/FinchOkHttpClient.kt b/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/FinchOkHttpClient.kt
index 9cb8ed19..3e365dbe 100644
--- a/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/FinchOkHttpClient.kt
+++ b/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/FinchOkHttpClient.kt
@@ -6,17 +6,20 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.tryfinch.api.client.FinchClient
import com.tryfinch.api.client.FinchClientImpl
import com.tryfinch.api.core.ClientOptions
+import com.tryfinch.api.core.Timeout
import com.tryfinch.api.core.http.Headers
import com.tryfinch.api.core.http.QueryParams
import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
class FinchOkHttpClient private constructor() {
companion object {
+ /** Returns a mutable builder for constructing an instance of [FinchOkHttpClient]. */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): FinchClient = builder().fromEnv().build()
@@ -27,8 +30,7 @@ class FinchOkHttpClient private constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // The default timeout for the client is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
fun baseUrl(baseUrl: String) = apply {
@@ -36,6 +38,17 @@ class FinchOkHttpClient private constructor() {
this.baseUrl = baseUrl
}
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
+ }
+
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
@@ -120,7 +133,19 @@ class FinchOkHttpClient private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply {
+ clientOptions.timeout(timeout)
+ this.timeout = timeout
+ }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
@@ -132,25 +157,34 @@ class FinchOkHttpClient private constructor() {
fun accessToken(accessToken: String?) = apply { clientOptions.accessToken(accessToken) }
- fun accessToken(accessToken: Optional) = accessToken(accessToken.orElse(null))
+ /** Alias for calling [Builder.accessToken] with `accessToken.orElse(null)`. */
+ fun accessToken(accessToken: Optional) = accessToken(accessToken.getOrNull())
fun clientId(clientId: String?) = apply { clientOptions.clientId(clientId) }
- fun clientId(clientId: Optional) = clientId(clientId.orElse(null))
+ /** Alias for calling [Builder.clientId] with `clientId.orElse(null)`. */
+ fun clientId(clientId: Optional) = clientId(clientId.getOrNull())
fun clientSecret(clientSecret: String?) = apply { clientOptions.clientSecret(clientSecret) }
- fun clientSecret(clientSecret: Optional) = clientSecret(clientSecret.orElse(null))
+ /** Alias for calling [Builder.clientSecret] with `clientSecret.orElse(null)`. */
+ fun clientSecret(clientSecret: Optional) = clientSecret(clientSecret.getOrNull())
fun webhookSecret(webhookSecret: String?) = apply {
clientOptions.webhookSecret(webhookSecret)
}
+ /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */
fun webhookSecret(webhookSecret: Optional) =
- webhookSecret(webhookSecret.orElse(null))
+ webhookSecret(webhookSecret.getOrNull())
fun fromEnv() = apply { clientOptions.fromEnv() }
+ /**
+ * Returns an immutable instance of [FinchClient].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
fun build(): FinchClient =
FinchClientImpl(
clientOptions
diff --git a/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/FinchOkHttpClientAsync.kt b/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/FinchOkHttpClientAsync.kt
index 29934ab4..0a25d955 100644
--- a/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/FinchOkHttpClientAsync.kt
+++ b/finch-java-client-okhttp/src/main/kotlin/com/tryfinch/api/client/okhttp/FinchOkHttpClientAsync.kt
@@ -6,17 +6,20 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.tryfinch.api.client.FinchClientAsync
import com.tryfinch.api.client.FinchClientAsyncImpl
import com.tryfinch.api.core.ClientOptions
+import com.tryfinch.api.core.Timeout
import com.tryfinch.api.core.http.Headers
import com.tryfinch.api.core.http.QueryParams
import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
class FinchOkHttpClientAsync private constructor() {
companion object {
+ /** Returns a mutable builder for constructing an instance of [FinchOkHttpClientAsync]. */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): FinchClientAsync = builder().fromEnv().build()
@@ -27,8 +30,7 @@ class FinchOkHttpClientAsync private constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // The default timeout for the client is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
fun baseUrl(baseUrl: String) = apply {
@@ -36,6 +38,17 @@ class FinchOkHttpClientAsync private constructor() {
this.baseUrl = baseUrl
}
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
+ }
+
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
@@ -120,7 +133,19 @@ class FinchOkHttpClientAsync private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply {
+ clientOptions.timeout(timeout)
+ this.timeout = timeout
+ }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
@@ -132,25 +157,34 @@ class FinchOkHttpClientAsync private constructor() {
fun accessToken(accessToken: String?) = apply { clientOptions.accessToken(accessToken) }
- fun accessToken(accessToken: Optional) = accessToken(accessToken.orElse(null))
+ /** Alias for calling [Builder.accessToken] with `accessToken.orElse(null)`. */
+ fun accessToken(accessToken: Optional) = accessToken(accessToken.getOrNull())
fun clientId(clientId: String?) = apply { clientOptions.clientId(clientId) }
- fun clientId(clientId: Optional) = clientId(clientId.orElse(null))
+ /** Alias for calling [Builder.clientId] with `clientId.orElse(null)`. */
+ fun clientId(clientId: Optional) = clientId(clientId.getOrNull())
fun clientSecret(clientSecret: String?) = apply { clientOptions.clientSecret(clientSecret) }
- fun clientSecret(clientSecret: Optional) = clientSecret(clientSecret.orElse(null))
+ /** Alias for calling [Builder.clientSecret] with `clientSecret.orElse(null)`. */
+ fun clientSecret(clientSecret: Optional) = clientSecret(clientSecret.getOrNull())
fun webhookSecret(webhookSecret: String?) = apply {
clientOptions.webhookSecret(webhookSecret)
}
+ /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */
fun webhookSecret(webhookSecret: Optional) =
- webhookSecret(webhookSecret.orElse(null))
+ webhookSecret(webhookSecret.getOrNull())
fun fromEnv() = apply { clientOptions.fromEnv() }
+ /**
+ * Returns an immutable instance of [FinchClientAsync].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
fun build(): FinchClientAsync =
FinchClientAsyncImpl(
clientOptions
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 7c59fad7..be523572 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
@@ -1,6 +1,7 @@
package com.tryfinch.api.client.okhttp
import com.tryfinch.api.core.RequestOptions
+import com.tryfinch.api.core.Timeout
import com.tryfinch.api.core.checkRequired
import com.tryfinch.api.core.http.Headers
import com.tryfinch.api.core.http.HttpClient
@@ -88,13 +89,12 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
)
}
- val timeout = requestOptions.timeout
- if (timeout != null) {
+ requestOptions.timeout?.let {
clientBuilder
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
+ .connectTimeout(it.connect())
+ .readTimeout(it.read())
+ .writeTimeout(it.write())
+ .callTimeout(it.request())
}
val client = clientBuilder.build()
@@ -195,23 +195,24 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
class Builder internal constructor() {
private var baseUrl: HttpUrl? = null
- // The default timeout is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl.toHttpUrl() }
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
fun build(): OkHttpClient =
OkHttpClient(
okhttp3.OkHttpClient.Builder()
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
+ .connectTimeout(timeout.connect())
+ .readTimeout(timeout.read())
+ .writeTimeout(timeout.write())
+ .callTimeout(timeout.request())
.proxy(proxy)
.build(),
checkRequired("baseUrl", baseUrl),
diff --git a/finch-java-core/build.gradle.kts b/finch-java-core/build.gradle.kts
index af4554e6..cc2d57aa 100644
--- a/finch-java-core/build.gradle.kts
+++ b/finch-java-core/build.gradle.kts
@@ -3,9 +3,23 @@ plugins {
id("finch.publish")
}
+configurations.all {
+ resolutionStrategy {
+ // Compile and test against a lower Jackson version to ensure we're compatible with it.
+ // We publish with a higher version (see below) to ensure users depend on a secure version by default.
+ force("com.fasterxml.jackson.core:jackson-core:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
+ force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
+ }
+}
+
dependencies {
api("com.fasterxml.jackson.core:jackson-core:2.18.1")
api("com.fasterxml.jackson.core:jackson-databind:2.18.1")
+ api("com.google.errorprone:error_prone_annotations:2.33.0")
implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.1")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.1")
@@ -20,6 +34,7 @@ dependencies {
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
+ testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
testImplementation("org.mockito:mockito-core:5.14.2")
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
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 bf99f84a..4d6662df 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
@@ -38,6 +38,11 @@ interface FinchClient {
*/
fun async(): FinchClientAsync
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
fun accessTokens(): AccessTokenService
fun hris(): HrisService
@@ -100,4 +105,26 @@ interface FinchClient {
* method.
*/
fun close()
+
+ /** A view of [FinchClient] that provides access to raw HTTP responses for each method. */
+ interface WithRawResponse {
+
+ fun accessTokens(): AccessTokenService.WithRawResponse
+
+ fun hris(): HrisService.WithRawResponse
+
+ fun providers(): ProviderService.WithRawResponse
+
+ fun account(): AccountService.WithRawResponse
+
+ fun requestForwarding(): RequestForwardingService.WithRawResponse
+
+ fun jobs(): JobService.WithRawResponse
+
+ fun sandbox(): SandboxService.WithRawResponse
+
+ fun payroll(): PayrollService.WithRawResponse
+
+ fun connect(): ConnectService.WithRawResponse
+ }
}
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 e20d730b..d829a593 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
@@ -39,6 +39,11 @@ interface FinchClientAsync {
*/
fun sync(): FinchClient
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
fun accessTokens(): AccessTokenServiceAsync
fun hris(): HrisServiceAsync
@@ -101,4 +106,26 @@ interface FinchClientAsync {
* method.
*/
fun close()
+
+ /** A view of [FinchClientAsync] that provides access to raw HTTP responses for each method. */
+ interface WithRawResponse {
+
+ fun accessTokens(): AccessTokenServiceAsync.WithRawResponse
+
+ fun hris(): HrisServiceAsync.WithRawResponse
+
+ fun providers(): ProviderServiceAsync.WithRawResponse
+
+ fun account(): AccountServiceAsync.WithRawResponse
+
+ fun requestForwarding(): RequestForwardingServiceAsync.WithRawResponse
+
+ fun jobs(): JobServiceAsync.WithRawResponse
+
+ fun sandbox(): SandboxServiceAsync.WithRawResponse
+
+ fun payroll(): PayrollServiceAsync.WithRawResponse
+
+ fun connect(): ConnectServiceAsync.WithRawResponse
+ }
}
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 1a33fcc6..23a0f4e0 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
@@ -4,6 +4,7 @@ package com.tryfinch.api.client
import com.fasterxml.jackson.annotation.JsonProperty
import com.tryfinch.api.core.ClientOptions
+import com.tryfinch.api.core.JsonValue
import com.tryfinch.api.core.getPackageVersion
import com.tryfinch.api.core.handlers.errorHandler
import com.tryfinch.api.core.handlers.jsonHandler
@@ -11,8 +12,7 @@ import com.tryfinch.api.core.handlers.withErrorHandler
import com.tryfinch.api.core.http.HttpMethod
import com.tryfinch.api.core.http.HttpRequest
import com.tryfinch.api.core.http.HttpResponse.Handler
-import com.tryfinch.api.core.json
-import com.tryfinch.api.errors.FinchError
+import com.tryfinch.api.core.http.json
import com.tryfinch.api.errors.FinchException
import com.tryfinch.api.models.*
import com.tryfinch.api.services.async.AccessTokenServiceAsync
@@ -40,7 +40,7 @@ import java.util.concurrent.CompletableFuture
class FinchClientAsyncImpl(private val clientOptions: ClientOptions) : FinchClientAsync {
- private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
+ private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
private val clientOptionsWithUserAgent =
if (clientOptions.headers.names().contains("User-Agent")) clientOptions
@@ -52,6 +52,10 @@ class FinchClientAsyncImpl(private val clientOptions: ClientOptions) : FinchClie
private val sync: FinchClient by lazy { FinchClientImpl(clientOptions) }
+ private val withRawResponse: FinchClientAsync.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
private val accessTokens: AccessTokenServiceAsync by lazy {
AccessTokenServiceAsyncImpl(clientOptionsWithUserAgent)
}
@@ -93,6 +97,8 @@ class FinchClientAsyncImpl(private val clientOptions: ClientOptions) : FinchClie
override fun sync(): FinchClient = sync
+ override fun withRawResponse(): FinchClientAsync.WithRawResponse = withRawResponse
+
override fun accessTokens(): AccessTokenServiceAsync = accessTokens
override fun hris(): HrisServiceAsync = hris
@@ -121,10 +127,10 @@ class FinchClientAsyncImpl(private val clientOptions: ClientOptions) : FinchClie
code: String,
redirectUri: String?,
): CompletableFuture {
- if (clientOptions.clientId == null) {
+ if (!clientOptions.clientId().isPresent) {
throw FinchException("clientId must be set in order to call getAccessToken")
}
- if (clientOptions.clientSecret == null) {
+ if (!clientOptions.clientSecret().isPresent) {
throw FinchException("clientSecret must be set in order to call getAccessToken")
}
val request =
@@ -144,11 +150,11 @@ class FinchClientAsyncImpl(private val clientOptions: ClientOptions) : FinchClie
}
override fun getAuthUrl(products: String, redirectUri: String, sandbox: Boolean): String {
- if (clientOptions.clientId == null) {
+ if (!clientOptions.clientId().isPresent) {
throw FinchException("Expected the clientId to be set in order to call getAuthUrl")
}
return "https://connect.tryfinch.com/authorize" +
- "?client_id=${URLEncoder.encode(clientOptions.clientId, Charsets.UTF_8.name())}" +
+ "?client_id=${URLEncoder.encode(clientOptions.clientId().get(), Charsets.UTF_8.name())}" +
"&products=${URLEncoder.encode(products, Charsets.UTF_8.name())}" +
"&redirect_uri=${URLEncoder.encode(redirectUri, Charsets.UTF_8.name())}" +
"&sandbox=${if (sandbox) "true" else "false"}"
@@ -162,9 +168,9 @@ class FinchClientAsyncImpl(private val clientOptions: ClientOptions) : FinchClie
.clock(clientOptions.clock)
.baseUrl(clientOptions.baseUrl)
.accessToken(accessToken)
- .apply { clientOptions.clientId?.let(::clientId) }
- .apply { clientOptions.clientSecret?.let(::clientSecret) }
- .apply { clientOptions.webhookSecret?.let(::webhookSecret) }
+ .clientId(clientOptions.clientId())
+ .clientSecret(clientOptions.clientSecret())
+ .webhookSecret(clientOptions.webhookSecret())
.headers(clientOptions.headers)
.responseValidation(clientOptions.responseValidation)
.build()
@@ -172,21 +178,80 @@ class FinchClientAsyncImpl(private val clientOptions: ClientOptions) : FinchClie
}
private data class GetAccessTokenParams(
- @JsonProperty("client_id") val clientId: String,
- @JsonProperty("client_secret") val clientSecret: String,
- @JsonProperty("code") val code: String,
- @JsonProperty("redirect_uri") val redirectUri: String?,
+ @get:JsonProperty("client_id") val clientId: String,
+ @get:JsonProperty("client_secret") val clientSecret: String,
+ @get:JsonProperty("code") val code: String,
+ @get:JsonProperty("redirect_uri") val redirectUri: String?,
)
private data class GetAccessTokenResponse(
- @JsonProperty("access_token") val accessToken: String,
- @JsonProperty("account_id") val accountId: String,
- @JsonProperty("client_type") val clientType: String,
- @JsonProperty("company_id") val companyId: String,
- @JsonProperty("connection_type") val connectionType: String,
- @JsonProperty("products") val products: List,
- @JsonProperty("provider_id") val providerId: String,
+ @get:JsonProperty("access_token") val accessToken: String,
+ @get:JsonProperty("account_id") val accountId: String,
+ @get:JsonProperty("client_type") val clientType: String,
+ @get:JsonProperty("company_id") val companyId: String,
+ @get:JsonProperty("connection_type") val connectionType: String,
+ @get:JsonProperty("products") val products: List,
+ @get:JsonProperty("provider_id") val providerId: String,
)
override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ FinchClientAsync.WithRawResponse {
+
+ private val accessTokens: AccessTokenServiceAsync.WithRawResponse by lazy {
+ AccessTokenServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val hris: HrisServiceAsync.WithRawResponse by lazy {
+ HrisServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val providers: ProviderServiceAsync.WithRawResponse by lazy {
+ ProviderServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val account: AccountServiceAsync.WithRawResponse by lazy {
+ AccountServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val requestForwarding: RequestForwardingServiceAsync.WithRawResponse by lazy {
+ RequestForwardingServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val jobs: JobServiceAsync.WithRawResponse by lazy {
+ JobServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val sandbox: SandboxServiceAsync.WithRawResponse by lazy {
+ SandboxServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val payroll: PayrollServiceAsync.WithRawResponse by lazy {
+ PayrollServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val connect: ConnectServiceAsync.WithRawResponse by lazy {
+ ConnectServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun accessTokens(): AccessTokenServiceAsync.WithRawResponse = accessTokens
+
+ override fun hris(): HrisServiceAsync.WithRawResponse = hris
+
+ override fun providers(): ProviderServiceAsync.WithRawResponse = providers
+
+ override fun account(): AccountServiceAsync.WithRawResponse = account
+
+ override fun requestForwarding(): RequestForwardingServiceAsync.WithRawResponse =
+ requestForwarding
+
+ override fun jobs(): JobServiceAsync.WithRawResponse = jobs
+
+ override fun sandbox(): SandboxServiceAsync.WithRawResponse = sandbox
+
+ override fun payroll(): PayrollServiceAsync.WithRawResponse = payroll
+
+ override fun connect(): ConnectServiceAsync.WithRawResponse = connect
+ }
}
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 3f10b2e8..766d1631 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
@@ -4,6 +4,7 @@ package com.tryfinch.api.client
import com.fasterxml.jackson.annotation.JsonProperty
import com.tryfinch.api.core.ClientOptions
+import com.tryfinch.api.core.JsonValue
import com.tryfinch.api.core.getPackageVersion
import com.tryfinch.api.core.handlers.errorHandler
import com.tryfinch.api.core.handlers.jsonHandler
@@ -11,8 +12,7 @@ import com.tryfinch.api.core.handlers.withErrorHandler
import com.tryfinch.api.core.http.HttpMethod
import com.tryfinch.api.core.http.HttpRequest
import com.tryfinch.api.core.http.HttpResponse.Handler
-import com.tryfinch.api.core.json
-import com.tryfinch.api.errors.FinchError
+import com.tryfinch.api.core.http.json
import com.tryfinch.api.errors.FinchException
import com.tryfinch.api.models.*
import com.tryfinch.api.services.blocking.AccessTokenService
@@ -39,7 +39,7 @@ import java.net.URLEncoder
class FinchClientImpl(private val clientOptions: ClientOptions) : FinchClient {
- private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
+ private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
private val clientOptionsWithUserAgent =
if (clientOptions.headers.names().contains("User-Agent")) clientOptions
@@ -51,6 +51,10 @@ class FinchClientImpl(private val clientOptions: ClientOptions) : FinchClient {
private val async: FinchClientAsync by lazy { FinchClientAsyncImpl(clientOptions) }
+ private val withRawResponse: FinchClient.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
private val accessTokens: AccessTokenService by lazy {
AccessTokenServiceImpl(clientOptionsWithUserAgent)
}
@@ -82,6 +86,8 @@ class FinchClientImpl(private val clientOptions: ClientOptions) : FinchClient {
override fun async(): FinchClientAsync = async
+ override fun withRawResponse(): FinchClient.WithRawResponse = withRawResponse
+
override fun accessTokens(): AccessTokenService = accessTokens
override fun hris(): HrisService = hris
@@ -110,10 +116,10 @@ class FinchClientImpl(private val clientOptions: ClientOptions) : FinchClient {
code: String,
redirectUri: String?,
): String {
- if (clientOptions.clientId == null) {
+ if (!clientOptions.clientId().isPresent) {
throw FinchException("clientId must be set in order to call getAccessToken")
}
- if (clientOptions.clientSecret == null) {
+ if (!clientOptions.clientSecret().isPresent) {
throw FinchException("clientSecret must be set in order to call getAccessToken")
}
val request =
@@ -133,11 +139,11 @@ class FinchClientImpl(private val clientOptions: ClientOptions) : FinchClient {
}
override fun getAuthUrl(products: String, redirectUri: String, sandbox: Boolean): String {
- if (clientOptions.clientId == null) {
+ if (!clientOptions.clientId().isPresent) {
throw FinchException("Expected the clientId to be set in order to call getAuthUrl")
}
return "https://connect.tryfinch.com/authorize" +
- "?client_id=${URLEncoder.encode(clientOptions.clientId, Charsets.UTF_8.name())}" +
+ "?client_id=${URLEncoder.encode(clientOptions.clientId().get(), Charsets.UTF_8.name())}" +
"&products=${URLEncoder.encode(products, Charsets.UTF_8.name())}" +
"&redirect_uri=${URLEncoder.encode(redirectUri, Charsets.UTF_8.name())}" +
"&sandbox=${if (sandbox) "true" else "false"}"
@@ -151,9 +157,9 @@ class FinchClientImpl(private val clientOptions: ClientOptions) : FinchClient {
.clock(clientOptions.clock)
.baseUrl(clientOptions.baseUrl)
.accessToken(accessToken)
- .apply { clientOptions.clientId?.let(::clientId) }
- .apply { clientOptions.clientSecret?.let(::clientSecret) }
- .apply { clientOptions.webhookSecret?.let(::webhookSecret) }
+ .clientId(clientOptions.clientId())
+ .clientSecret(clientOptions.clientSecret())
+ .webhookSecret(clientOptions.webhookSecret())
.headers(clientOptions.headers)
.responseValidation(clientOptions.responseValidation)
.build()
@@ -161,21 +167,80 @@ class FinchClientImpl(private val clientOptions: ClientOptions) : FinchClient {
}
private data class GetAccessTokenParams(
- @JsonProperty("client_id") val clientId: String,
- @JsonProperty("client_secret") val clientSecret: String,
- @JsonProperty("code") val code: String,
- @JsonProperty("redirect_uri") val redirectUri: String?,
+ @get:JsonProperty("client_id") val clientId: String,
+ @get:JsonProperty("client_secret") val clientSecret: String,
+ @get:JsonProperty("code") val code: String,
+ @get:JsonProperty("redirect_uri") val redirectUri: String?,
)
private data class GetAccessTokenResponse(
- @JsonProperty("access_token") val accessToken: String,
- @JsonProperty("account_id") val accountId: String,
- @JsonProperty("client_type") val clientType: String,
- @JsonProperty("company_id") val companyId: String,
- @JsonProperty("connection_type") val connectionType: String,
- @JsonProperty("products") val products: List,
- @JsonProperty("provider_id") val providerId: String,
+ @get:JsonProperty("access_token") val accessToken: String,
+ @get:JsonProperty("account_id") val accountId: String,
+ @get:JsonProperty("client_type") val clientType: String,
+ @get:JsonProperty("company_id") val companyId: String,
+ @get:JsonProperty("connection_type") val connectionType: String,
+ @get:JsonProperty("products") val products: List,
+ @get:JsonProperty("provider_id") val providerId: String,
)
override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ FinchClient.WithRawResponse {
+
+ private val accessTokens: AccessTokenService.WithRawResponse by lazy {
+ AccessTokenServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val hris: HrisService.WithRawResponse by lazy {
+ HrisServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val providers: ProviderService.WithRawResponse by lazy {
+ ProviderServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val account: AccountService.WithRawResponse by lazy {
+ AccountServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val requestForwarding: RequestForwardingService.WithRawResponse by lazy {
+ RequestForwardingServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val jobs: JobService.WithRawResponse by lazy {
+ JobServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val sandbox: SandboxService.WithRawResponse by lazy {
+ SandboxServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val payroll: PayrollService.WithRawResponse by lazy {
+ PayrollServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val connect: ConnectService.WithRawResponse by lazy {
+ ConnectServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun accessTokens(): AccessTokenService.WithRawResponse = accessTokens
+
+ override fun hris(): HrisService.WithRawResponse = hris
+
+ override fun providers(): ProviderService.WithRawResponse = providers
+
+ override fun account(): AccountService.WithRawResponse = account
+
+ override fun requestForwarding(): RequestForwardingService.WithRawResponse =
+ requestForwarding
+
+ override fun jobs(): JobService.WithRawResponse = jobs
+
+ override fun sandbox(): SandboxService.WithRawResponse = sandbox
+
+ override fun payroll(): PayrollService.WithRawResponse = payroll
+
+ override fun connect(): ConnectService.WithRawResponse = connect
+ }
}
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 c850405c..315d2cb2 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
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
-import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
@@ -29,31 +28,17 @@ abstract class BaseDeserializer(type: KClass) :
protected abstract fun ObjectCodec.deserialize(node: JsonNode): T
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: TypeReference,
- validate: (T) -> Unit = {},
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: TypeReference): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: JavaType,
- validate: (T) -> Unit = {},
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: JavaType): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
}
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Check.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Check.kt
index 38ad8b3e..5918fb36 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Check.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Check.kt
@@ -2,9 +2,24 @@
package com.tryfinch.api.core
+import com.fasterxml.jackson.core.Version
+import com.fasterxml.jackson.core.util.VersionUtil
+
fun checkRequired(name: String, value: T?): T =
checkNotNull(value) { "`$name` is required, but was not set" }
+@JvmSynthetic
+internal fun checkKnown(name: String, value: JsonField): T =
+ value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: MultipartField): T =
+ value.value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
@JvmSynthetic
internal fun checkLength(name: String, value: String, length: Int): String =
value.also {
@@ -27,3 +42,46 @@ internal fun checkMaxLength(name: String, value: String, maxLength: Int): String
"`$name` must have at most length $maxLength, but was ${it.length}"
}
}
+
+@JvmSynthetic
+internal fun checkJacksonVersionCompatibility() {
+ val incompatibleJacksonVersions =
+ RUNTIME_JACKSON_VERSIONS.mapNotNull {
+ when {
+ it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
+ it to "incompatible major version"
+ it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
+ it to "minor version too low"
+ it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
+ it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
+ it to "patch version too low"
+ else -> null
+ }
+ }
+ check(incompatibleJacksonVersions.isEmpty()) {
+ """
+This SDK depends on Jackson version $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:
+
+${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
+ "- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
+}.joinToString("\n")}
+
+This can happen if you are either:
+1. Directly depending on different Jackson versions
+2. Depending on some library that depends on different Jackson versions, potentially transitively
+
+Double-check that you are depending on compatible Jackson versions.
+ """
+ .trimIndent()
+ }
+}
+
+private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
+private val RUNTIME_JACKSON_VERSIONS: List =
+ listOf(
+ com.fasterxml.jackson.core.json.PackageVersion.VERSION,
+ com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
+ com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
+ )
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 f34021de..ee2132fe 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
@@ -11,30 +11,55 @@ import com.tryfinch.api.core.http.RetryingHttpClient
import java.time.Clock
import java.util.Base64
import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
class ClientOptions
private constructor(
private val originalHttpClient: HttpClient,
@get:JvmName("httpClient") val httpClient: HttpClient,
+ @get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
@get:JvmName("clock") val clock: Clock,
@get:JvmName("baseUrl") val baseUrl: String,
@get:JvmName("headers") val headers: Headers,
@get:JvmName("queryParams") val queryParams: QueryParams,
@get:JvmName("responseValidation") val responseValidation: Boolean,
+ @get:JvmName("timeout") val timeout: Timeout,
@get:JvmName("maxRetries") val maxRetries: Int,
- @get:JvmName("accessToken") val accessToken: String?,
- @get:JvmName("clientId") val clientId: String?,
- @get:JvmName("clientSecret") val clientSecret: String?,
- @get:JvmName("webhookSecret") val webhookSecret: String?,
+ private val accessToken: String?,
+ private val clientId: String?,
+ private val clientSecret: String?,
+ private val webhookSecret: String?,
) {
+ init {
+ if (checkJacksonVersionCompatibility) {
+ checkJacksonVersionCompatibility()
+ }
+ }
+
+ fun accessToken(): Optional = Optional.ofNullable(accessToken)
+
+ fun clientId(): Optional = Optional.ofNullable(clientId)
+
+ fun clientSecret(): Optional = Optional.ofNullable(clientSecret)
+
+ fun webhookSecret(): Optional = Optional.ofNullable(webhookSecret)
+
fun toBuilder() = Builder().from(this)
companion object {
const val PRODUCTION_URL = "https://api.tryfinch.com"
+ /**
+ * Returns a mutable builder for constructing an instance of [ClientOptions].
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * ```
+ */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build()
@@ -44,12 +69,14 @@ private constructor(
class Builder internal constructor() {
private var httpClient: HttpClient? = null
+ private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
private var clock: Clock = Clock.systemUTC()
private var baseUrl: String = PRODUCTION_URL
private var headers: Headers.Builder = Headers.builder()
private var queryParams: QueryParams.Builder = QueryParams.builder()
private var responseValidation: Boolean = false
+ private var timeout: Timeout = Timeout.default()
private var maxRetries: Int = 2
private var accessToken: String? = null
private var clientId: String? = null
@@ -59,12 +86,14 @@ private constructor(
@JvmSynthetic
internal fun from(clientOptions: ClientOptions) = apply {
httpClient = clientOptions.originalHttpClient
+ checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
clock = clientOptions.clock
baseUrl = clientOptions.baseUrl
headers = clientOptions.headers.toBuilder()
queryParams = clientOptions.queryParams.toBuilder()
responseValidation = clientOptions.responseValidation
+ timeout = clientOptions.timeout
maxRetries = clientOptions.maxRetries
accessToken = clientOptions.accessToken
clientId = clientOptions.clientId
@@ -74,6 +103,10 @@ private constructor(
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
+ }
+
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
fun clock(clock: Clock) = apply { this.clock = clock }
@@ -84,24 +117,30 @@ private constructor(
this.responseValidation = responseValidation
}
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
fun accessToken(accessToken: String?) = apply { this.accessToken = accessToken }
- fun accessToken(accessToken: Optional) = accessToken(accessToken.orElse(null))
+ /** Alias for calling [Builder.accessToken] with `accessToken.orElse(null)`. */
+ fun accessToken(accessToken: Optional) = accessToken(accessToken.getOrNull())
fun clientId(clientId: String?) = apply { this.clientId = clientId }
- fun clientId(clientId: Optional) = clientId(clientId.orElse(null))
+ /** Alias for calling [Builder.clientId] with `clientId.orElse(null)`. */
+ fun clientId(clientId: Optional) = clientId(clientId.getOrNull())
fun clientSecret(clientSecret: String?) = apply { this.clientSecret = clientSecret }
- fun clientSecret(clientSecret: Optional) = clientSecret(clientSecret.orElse(null))
+ /** Alias for calling [Builder.clientSecret] with `clientSecret.orElse(null)`. */
+ fun clientSecret(clientSecret: Optional) = clientSecret(clientSecret.getOrNull())
fun webhookSecret(webhookSecret: String?) = apply { this.webhookSecret = webhookSecret }
+ /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */
fun webhookSecret(webhookSecret: Optional) =
- webhookSecret(webhookSecret.orElse(null))
+ webhookSecret(webhookSecret.getOrNull())
fun headers(headers: Headers) = apply {
this.headers.clear()
@@ -189,6 +228,18 @@ private constructor(
System.getenv("FINCH_WEBHOOK_SECRET")?.let { webhookSecret(it) }
}
+ /**
+ * Returns an immutable instance of [ClientOptions].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
fun build(): ClientOptions {
val httpClient = checkRequired("httpClient", httpClient)
@@ -229,12 +280,14 @@ private constructor(
.maxRetries(maxRetries)
.build()
),
+ checkJacksonVersionCompatibility,
jsonMapper,
clock,
baseUrl,
headers.build(),
queryParams.build(),
responseValidation,
+ timeout,
maxRetries,
accessToken,
clientId,
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
deleted file mode 100644
index 4d3d1862..00000000
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/HttpRequestBodies.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-@file:JvmName("HttpRequestBodies")
-
-package com.tryfinch.api.core
-
-import com.fasterxml.jackson.databind.json.JsonMapper
-import com.tryfinch.api.core.http.HttpRequestBody
-import com.tryfinch.api.errors.FinchException
-import java.io.ByteArrayOutputStream
-import java.io.OutputStream
-import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
-
-@JvmSynthetic
-internal inline fun json(jsonMapper: JsonMapper, value: T): HttpRequestBody {
- return object : HttpRequestBody {
- private var cachedBytes: ByteArray? = null
-
- private fun serialize(): ByteArray {
- if (cachedBytes != null) return cachedBytes!!
-
- val buffer = ByteArrayOutputStream()
- try {
- jsonMapper.writeValue(buffer, value)
- cachedBytes = buffer.toByteArray()
- return cachedBytes!!
- } catch (e: Exception) {
- throw FinchException("Error writing request", e)
- }
- }
-
- override fun writeTo(outputStream: OutputStream) {
- outputStream.write(serialize())
- }
-
- override fun contentType(): String = "application/json"
-
- override fun contentLength(): Long {
- return serialize().size.toLong()
- }
-
- override fun repeatable(): Boolean = true
-
- override fun close() {}
- }
-}
-
-@JvmSynthetic
-internal fun multipartFormData(
- jsonMapper: JsonMapper,
- parts: Array?>,
-): HttpRequestBody {
- val builder = MultipartEntityBuilder.create()
- parts.forEach { part ->
- if (part?.value != null) {
- when (part.value) {
- is JsonValue -> {
- val buffer = ByteArrayOutputStream()
- try {
- jsonMapper.writeValue(buffer, part.value)
- } catch (e: Exception) {
- throw FinchException("Error serializing value to json", e)
- }
- builder.addBinaryBody(
- part.name,
- buffer.toByteArray(),
- part.contentType,
- part.filename,
- )
- }
- is Boolean ->
- builder.addTextBody(
- part.name,
- if (part.value) "true" else "false",
- part.contentType,
- )
- is Int -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is Long -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is Double -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is ByteArray ->
- builder.addBinaryBody(part.name, part.value, part.contentType, part.filename)
- is String -> builder.addTextBody(part.name, part.value, part.contentType)
- is Enum -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- else ->
- throw IllegalArgumentException(
- "Unsupported content type: ${part.value::class.java.simpleName}"
- )
- }
- }
- }
- val entity = builder.build()
-
- return object : HttpRequestBody {
- override fun writeTo(outputStream: OutputStream) {
- try {
- return entity.writeTo(outputStream)
- } catch (e: Exception) {
- throw FinchException("Error writing request", e)
- }
- }
-
- override fun contentType(): String = entity.contentType
-
- override fun contentLength(): Long = -1
-
- override fun repeatable(): Boolean = entity.isRepeatable
-
- override fun close() = entity.close()
- }
-}
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/ObjectMappers.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/ObjectMappers.kt
index 90d2ce0d..67e2acb4 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/ObjectMappers.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/ObjectMappers.kt
@@ -3,23 +3,165 @@
package com.tryfinch.api.core
import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.databind.cfg.CoercionAction.Fail
-import com.fasterxml.jackson.databind.cfg.CoercionInputShape.Integer
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.cfg.CoercionAction
+import com.fasterxml.jackson.databind.cfg.CoercionInputShape
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.databind.type.LogicalType
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
-import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
+import com.fasterxml.jackson.module.kotlin.kotlinModule
+import java.io.InputStream
+import java.time.DateTimeException
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
fun jsonMapper(): JsonMapper =
- jacksonMapperBuilder()
+ JsonMapper.builder()
+ .addModule(kotlinModule())
.addModule(Jdk8Module())
.addModule(JavaTimeModule())
+ .addModule(
+ SimpleModule()
+ .addSerializer(InputStreamSerializer)
+ .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
+ )
+ .withCoercionConfig(LogicalType.Boolean) {
+ it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Integer) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Float) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Textual) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Array) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Collection) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Map) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.POJO) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ }
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
- .withCoercionConfig(String::class.java) { it.setCoercion(Integer, Fail) }
+ .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
+ .disable(MapperFeature.AUTO_DETECT_CREATORS)
+ .disable(MapperFeature.AUTO_DETECT_FIELDS)
+ .disable(MapperFeature.AUTO_DETECT_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_SETTERS)
.build()
+
+/** A serializer that serializes [InputStream] to bytes. */
+private object InputStreamSerializer : BaseSerializer(InputStream::class) {
+
+ private fun readResolve(): Any = InputStreamSerializer
+
+ override fun serialize(
+ value: InputStream?,
+ gen: JsonGenerator?,
+ serializers: SerializerProvider?,
+ ) {
+ if (value == null) {
+ gen?.writeNull()
+ } else {
+ value.use { gen?.writeBinary(it.readBytes()) }
+ }
+ }
+}
+
+/**
+ * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
+ */
+private class LenientLocalDateTimeDeserializer :
+ StdDeserializer(LocalDateTime::class.java) {
+
+ companion object {
+
+ private val DATE_TIME_FORMATTERS =
+ listOf(
+ DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+ DateTimeFormatter.ISO_LOCAL_DATE,
+ DateTimeFormatter.ISO_ZONED_DATE_TIME,
+ )
+ }
+
+ override fun logicalType(): LogicalType = LogicalType.DateTime
+
+ override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
+ val exceptions = mutableListOf()
+
+ for (formatter in DATE_TIME_FORMATTERS) {
+ try {
+ val temporal = formatter.parse(p.text)
+
+ return when {
+ !temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
+ LocalDate.from(temporal).atStartOfDay()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal)
+ else -> ZonedDateTime.from(temporal).toLocalDateTime()
+ }
+ } catch (e: DateTimeException) {
+ exceptions.add(e)
+ }
+ }
+
+ throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
+ exceptions.forEach { addSuppressed(it) }
+ }
+ }
+}
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 cee1a369..89fdd644 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,13 +2,7 @@ package com.tryfinch.api.core
import java.time.Duration
-class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Duration?) {
- fun applyDefaults(options: RequestOptions): RequestOptions {
- return RequestOptions(
- responseValidation = this.responseValidation ?: options.responseValidation,
- timeout = this.timeout ?: options.timeout,
- )
- }
+class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Timeout?) {
companion object {
@@ -16,22 +10,37 @@ class RequestOptions private constructor(val responseValidation: Boolean?, val t
@JvmStatic fun none() = NONE
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions): RequestOptions =
+ builder()
+ .responseValidation(clientOptions.responseValidation)
+ .timeout(clientOptions.timeout)
+ .build()
+
@JvmStatic fun builder() = Builder()
}
+ fun applyDefaults(options: RequestOptions): RequestOptions =
+ RequestOptions(
+ responseValidation = responseValidation ?: options.responseValidation,
+ timeout =
+ if (options.timeout != null && timeout != null) timeout.assign(options.timeout)
+ else timeout ?: options.timeout,
+ )
+
class Builder internal constructor() {
private var responseValidation: Boolean? = null
- private var timeout: Duration? = null
+ private var timeout: Timeout? = null
fun responseValidation(responseValidation: Boolean) = apply {
this.responseValidation = responseValidation
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
- fun build(): RequestOptions {
- return RequestOptions(responseValidation, timeout)
- }
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ fun build(): RequestOptions = RequestOptions(responseValidation, timeout)
}
}
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Timeout.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Timeout.kt
new file mode 100644
index 00000000..eb2f4b95
--- /dev/null
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Timeout.kt
@@ -0,0 +1,167 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.tryfinch.api.core
+
+import java.time.Duration
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** A class containing timeouts for various processing phases of a request. */
+class Timeout
+private constructor(
+ private val connect: Duration?,
+ private val read: Duration?,
+ private val write: Duration?,
+ private val request: Duration?,
+) {
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(): Duration = connect ?: Duration.ofMinutes(1)
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(): Duration = read ?: request()
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(): Duration = write ?: request()
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as well
+ * as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(): Duration = request ?: Duration.ofMinutes(1)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ @JvmStatic fun default() = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [Timeout]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Timeout]. */
+ class Builder internal constructor() {
+
+ private var connect: Duration? = null
+ private var read: Duration? = null
+ private var write: Duration? = null
+ private var request: Duration? = null
+
+ @JvmSynthetic
+ internal fun from(timeout: Timeout) = apply {
+ connect = timeout.connect
+ read = timeout.read
+ write = timeout.write
+ request = timeout.request
+ }
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(connect: Duration?) = apply { this.connect = connect }
+
+ /** Alias for calling [Builder.connect] with `connect.orElse(null)`. */
+ fun connect(connect: Optional) = connect(connect.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(read: Duration?) = apply { this.read = read }
+
+ /** Alias for calling [Builder.read] with `read.orElse(null)`. */
+ fun read(read: Optional) = read(read.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(write: Duration?) = apply { this.write = write }
+
+ /** Alias for calling [Builder.write] with `write.orElse(null)`. */
+ fun write(write: Optional) = write(write.getOrNull())
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as
+ * well as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(request: Duration?) = apply { this.request = request }
+
+ /** Alias for calling [Builder.request] with `request.orElse(null)`. */
+ fun request(request: Optional) = request(request.getOrNull())
+
+ /**
+ * Returns an immutable instance of [Timeout].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Timeout = Timeout(connect, read, write, request)
+ }
+
+ @JvmSynthetic
+ internal fun assign(target: Timeout): Timeout =
+ target
+ .toBuilder()
+ .apply {
+ connect?.let(this::connect)
+ read?.let(this::read)
+ write?.let(this::write)
+ request?.let(this::request)
+ }
+ .build()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return /* spotless:off */ other is Timeout && connect == other.connect && read == other.read && write == other.write && request == other.request /* spotless:on */
+ }
+
+ override fun hashCode(): Int = /* spotless:off */ Objects.hash(connect, read, write, request) /* spotless:on */
+
+ override fun toString() =
+ "Timeout{connect=$connect, read=$read, write=$write, request=$request}"
+}
diff --git a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Utils.kt b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Utils.kt
index 979a3580..3acb9ed3 100644
--- a/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Utils.kt
+++ b/finch-java-core/src/main/kotlin/com/tryfinch/api/core/Utils.kt
@@ -26,6 +26,34 @@ internal fun , V> SortedMap.toImmutable(): SortedMap> Sequence.allMaxBy(selector: (T) -> R): List {
+ var maxValue: R? = null
+ val maxElements = mutableListOf()
+
+ val iterator = iterator()
+ while (iterator.hasNext()) {
+ val element = iterator.next()
+ val value = selector(element)
+ if (maxValue == null || value > maxValue) {
+ maxValue = value
+ maxElements.clear()
+ maxElements.add(element)
+ } else if (value == maxValue) {
+ maxElements.add(element)
+ }
+ }
+
+ return maxElements
+}
+
/**
* Returns whether [this] is equal to [other].
*
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 95574500..82a56e0e 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
@@ -1,8 +1,6 @@
package com.tryfinch.api.core
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
-import com.fasterxml.jackson.annotation.JsonAutoDetect
-import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.core.JsonGenerator
@@ -27,35 +25,55 @@ import com.fasterxml.jackson.databind.node.JsonNodeType.POJO
import com.fasterxml.jackson.databind.node.JsonNodeType.STRING
import com.fasterxml.jackson.databind.ser.std.NullSerializer
import com.tryfinch.api.errors.FinchInvalidDataException
-import java.nio.charset.Charset
+import java.io.InputStream
import java.util.Objects
import java.util.Optional
-import org.apache.hc.core5.http.ContentType
+/**
+ * A class representing a serializable JSON field.
+ *
+ * It can either be a [KnownValue] value of type [T], matching the type the SDK expects, or an
+ * arbitrary JSON value that bypasses the type system (via [JsonValue]).
+ */
@JsonDeserialize(using = JsonField.Deserializer::class)
sealed class JsonField {
+ /**
+ * Returns whether this field is missing, which means it will be omitted from the serialized
+ * JSON entirely.
+ */
fun isMissing(): Boolean = this is JsonMissing
+ /** Whether this field is explicitly set to `null`. */
fun isNull(): Boolean = this is JsonNull
- fun asKnown(): Optional =
- when (this) {
- is KnownValue -> Optional.of(value)
- else -> Optional.empty()
- }
+ /**
+ * Returns an [Optional] containing this field's "known" value, meaning it matches the type the
+ * SDK expects, or an empty [Optional] if this field contains an arbitrary [JsonValue].
+ *
+ * This is the opposite of [asUnknown].
+ */
+ fun asKnown():
+ Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > = Optional.ofNullable((this as? KnownValue)?.value)
/**
- * If the "known" value (i.e. matching the type that the SDK expects) is returned by the API
- * then this method will return an empty `Optional`, otherwise the returned `Optional` is given
- * a `JsonValue`.
+ * Returns an [Optional] containing this field's arbitrary [JsonValue], meaning it mismatches
+ * the type the SDK expects, or an empty [Optional] if this field contains a "known" value.
+ *
+ * This is the opposite of [asKnown].
*/
- fun asUnknown(): Optional =
- when (this) {
- is JsonValue -> Optional.of(this)
- else -> Optional.empty()
- }
+ fun asUnknown(): Optional = Optional.ofNullable(this as? JsonValue)
+ /**
+ * Returns an [Optional] containing this field's boolean value, or an empty [Optional] if it
+ * doesn't contain a boolean.
+ *
+ * This method checks for both a [KnownValue] containing a boolean and for [JsonBoolean].
+ */
fun asBoolean(): Optional =
when (this) {
is JsonBoolean -> Optional.of(value)
@@ -63,6 +81,12 @@ sealed class JsonField {
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's numerical value, or an empty [Optional] if it
+ * doesn't contain a number.
+ *
+ * This method checks for both a [KnownValue] containing a number and for [JsonNumber].
+ */
fun asNumber(): Optional =
when (this) {
is JsonNumber -> Optional.of(value)
@@ -70,6 +94,12 @@ sealed class JsonField {
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's string value, or an empty [Optional] if it
+ * doesn't contain a string.
+ *
+ * This method checks for both a [KnownValue] containing a string and for [JsonString].
+ */
fun asString(): Optional =
when (this) {
is JsonString -> Optional.of(value)
@@ -80,6 +110,12 @@ sealed class JsonField {
fun asStringOrThrow(): String =
asString().orElseThrow { FinchInvalidDataException("Value is not a string") }
+ /**
+ * Returns an [Optional] containing this field's list value, or an empty [Optional] if it
+ * doesn't contain a list.
+ *
+ * This method checks for both a [KnownValue] containing a list and for [JsonArray].
+ */
fun asArray(): Optional> =
when (this) {
is JsonArray -> Optional.of(values)
@@ -98,6 +134,12 @@ sealed class JsonField {
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's map value, or an empty [Optional] if it doesn't
+ * contain a map.
+ *
+ * This method checks for both a [KnownValue] containing a map and for [JsonObject].
+ */
fun asObject(): Optional