diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7a7202d..05479a1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,14 @@
name: CI
on:
push:
- branches-ignore:
- - 'generated'
- - 'codegen/**'
- - 'integrated/**'
- - 'stl-preview-head/**'
- - 'stl-preview-base/**'
+ branches:
+ - '**'
+ - '!integrated/**'
+ - '!stl-preview-head/**'
+ - '!stl-preview-base/**'
+ - '!generated'
+ - '!codegen/**'
+ - 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
@@ -16,14 +18,14 @@ jobs:
lint:
timeout-minutes: 15
name: lint
- runs-on: ${{ github.repository == 'stainless-sdks/blooio-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
- if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: |
@@ -32,7 +34,7 @@ jobs:
cache: gradle
- name: Set up Gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
- name: Run lints
run: ./scripts/lint
@@ -40,14 +42,17 @@ jobs:
build:
timeout-minutes: 15
name: build
- runs-on: ${{ github.repository == 'stainless-sdks/blooio-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
- if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+ permissions:
+ contents: read
+ id-token: write
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: |
@@ -56,21 +61,40 @@ jobs:
cache: gradle
- name: Set up Gradle
- uses: gradle/actions/setup-gradle@v4
+ uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
- name: Build SDK
run: ./scripts/build
+ - name: Get GitHub OIDC Token
+ if: |-
+ github.repository == 'stainless-sdks/blooio-java' &&
+ !startsWith(github.ref, 'refs/heads/stl/')
+ id: github-oidc
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ with:
+ script: core.setOutput('github_token', await core.getIDToken());
+
+ - name: Build and upload Maven artifacts
+ if: |-
+ github.repository == 'stainless-sdks/blooio-java' &&
+ !startsWith(github.ref, 'refs/heads/stl/')
+ env:
+ URL: https://pkg.stainless.com/s
+ AUTH: ${{ steps.github-oidc.outputs.github_token }}
+ SHA: ${{ github.sha }}
+ PROJECT: blooio-java
+ run: ./scripts/upload-artifacts
test:
timeout-minutes: 15
name: test
- runs-on: ${{ github.repository == 'stainless-sdks/blooio-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: |
@@ -79,7 +103,7 @@ jobs:
cache: gradle
- name: Set up Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0
- name: Run tests
run: ./scripts/test
diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml
index 11f4929..bb99be6 100644
--- a/.github/workflows/publish-sonatype.yml
+++ b/.github/workflows/publish-sonatype.yml
@@ -14,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: |
@@ -26,7 +26,7 @@ jobs:
cache: gradle
- name: Set up Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0
- name: Publish to Sonatype
run: |-
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index 53c616f..06caa1a 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'Blooio/blooio-java-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check release environment
run: |
diff --git a/.gitignore b/.gitignore
index b1346e6..90b85e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.prism.log
+.stdy.log
.gradle
.idea
.kotlin
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2ed3b71..3d2ac0b 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.0.4"
+ ".": "0.1.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 5906eac..fa485b0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 12
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio%2Fblooio-2d1e4d94ef5b48617299b653eb3509f32fe4e9d6315ee9f3bac50b83635d736b.yml
-openapi_spec_hash: 27229154f39588fcc348a91e6b293664
-config_hash: 8cae0b4155e1a7413f8d8693d7253e37
+configured_endpoints: 54
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-4cfd8aadba2015ef9746f076452ef853f194ce40602854c29ce76842eff021a9.yml
+openapi_spec_hash: 8137fd23f3a30da82f83a8ac74a433f8
+config_hash: 839e191496287972d18d39dcab8b19b7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af9651d..0fb25e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,79 @@
# Changelog
+## 0.1.0 (2026-07-25)
+
+Full Changelog: [v0.0.4...v0.1.0](https://github.com/Blooio/blooio-java-sdk/compare/v0.0.4...v0.1.0)
+
+### Features
+
+* **api:** api update ([ee72ac8](https://github.com/Blooio/blooio-java-sdk/commit/ee72ac8987ab0bcfd54340dbe0390f6d4f8fed63))
+* **api:** api update ([7f51cf6](https://github.com/Blooio/blooio-java-sdk/commit/7f51cf6db5c2f96e794de5e08e17d120440b42e8))
+* **api:** api update ([932d816](https://github.com/Blooio/blooio-java-sdk/commit/932d8162c2cd61c1dc99420ffc57b7ba3cf3ec2b))
+* **api:** api update ([ddc3458](https://github.com/Blooio/blooio-java-sdk/commit/ddc345895a8f3d40499f587ae3f5f0d34c51d1cb))
+* **api:** api update ([771db4a](https://github.com/Blooio/blooio-java-sdk/commit/771db4a13cd1736ea91b9e61beb8a987628bcacc))
+* **api:** api update ([62bd13a](https://github.com/Blooio/blooio-java-sdk/commit/62bd13a9dcfbdd3df4b764fb1bbe49b4481641ab))
+* **api:** api update ([242656c](https://github.com/Blooio/blooio-java-sdk/commit/242656c18bf53215a8064e1675aa1f8376d9c785))
+* **api:** api update ([b2ef3c6](https://github.com/Blooio/blooio-java-sdk/commit/b2ef3c62456ee3161c147df9d4d2189d61ff51b8))
+* **api:** api update ([e80c968](https://github.com/Blooio/blooio-java-sdk/commit/e80c968881cf120159070e90dfc1b6c3245dcfa9))
+* **api:** api update ([42ee71f](https://github.com/Blooio/blooio-java-sdk/commit/42ee71ffdc0961088155f55f433beceb12021350))
+* **api:** api update ([80f6a3d](https://github.com/Blooio/blooio-java-sdk/commit/80f6a3d0d669e0cfd2d399ba44193eb0e78e84f5))
+* **api:** api update ([1c697d5](https://github.com/Blooio/blooio-java-sdk/commit/1c697d5bd9f91a4a495d5d71667de5271e8cac0f))
+* **api:** manual updates ([1ae97f1](https://github.com/Blooio/blooio-java-sdk/commit/1ae97f141b08fc16fba03a7d99d6eace0e512001))
+* **api:** manual updates ([e112bbf](https://github.com/Blooio/blooio-java-sdk/commit/e112bbf06bcdf1bb105d9bf924a29f652ac28eb4))
+* **client:** add `HttpRequest#url()` method ([dd2bca3](https://github.com/Blooio/blooio-java-sdk/commit/dd2bca38596e4ef7626ef23d1a73ce7678a4375e))
+* **client:** allow configuring dispatcher executor service ([4f0db30](https://github.com/Blooio/blooio-java-sdk/commit/4f0db3097b9b0c1876887115146216c0b3a50c30))
+* **client:** improve logging ([b1579ce](https://github.com/Blooio/blooio-java-sdk/commit/b1579ce887a34b5a401642abe28d25d16d1c9194))
+* **client:** more robust error parsing ([8d94c9e](https://github.com/Blooio/blooio-java-sdk/commit/8d94c9ea92fb107cb3bbf301db7132cf0e70feb5))
+* **client:** send `X-Stainless-Kotlin-Version` header ([f6d45cd](https://github.com/Blooio/blooio-java-sdk/commit/f6d45cddc950b7443fb661d4804c4986ef89c187))
+* **client:** support proxy authentication ([8e5c00e](https://github.com/Blooio/blooio-java-sdk/commit/8e5c00ecbd7d4ba6e9c3f06d0001c2a953ac564e))
+* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([17dc98d](https://github.com/Blooio/blooio-java-sdk/commit/17dc98df02fbf2f61b95baafaeab473e5d0aa1ba))
+
+
+### Bug Fixes
+
+* **client:** disallow coercion from float to int ([3ee28c5](https://github.com/Blooio/blooio-java-sdk/commit/3ee28c5e8bd033c3d96134fb13903941de2e8735))
+* **client:** fully respect max retries ([2ddb812](https://github.com/Blooio/blooio-java-sdk/commit/2ddb8126d441b260c80b9ef3b87a24f9ce20b5be))
+* **client:** preserve time zone in lenient date-time parsing ([573fcaf](https://github.com/Blooio/blooio-java-sdk/commit/573fcaf71930c387314fb18231334b542df8a47f))
+* **client:** send retry count header for max retries 0 ([2ddb812](https://github.com/Blooio/blooio-java-sdk/commit/2ddb8126d441b260c80b9ef3b87a24f9ce20b5be))
+* date time deserialization leniency ([b2ac60a](https://github.com/Blooio/blooio-java-sdk/commit/b2ac60a271022b4f389feee2e73467927b2c7a51))
+
+
+### Performance Improvements
+
+* **client:** create one json mapper ([6ef289e](https://github.com/Blooio/blooio-java-sdk/commit/6ef289e91429c1d6e5b7eea1746b0bb9ce24645b))
+
+
+### Chores
+
+* **ci:** upgrade `actions/github-script` ([a32a19d](https://github.com/Blooio/blooio-java-sdk/commit/a32a19dcfbeff710c7c6c0b4daaa8ed98107294a))
+* **ci:** upgrade `actions/setup-java` ([67eb04e](https://github.com/Blooio/blooio-java-sdk/commit/67eb04e66d48b47097af4e8d66f093688ff1f8ef))
+* **internal:** allow passing args to `./scripts/test` ([a8da670](https://github.com/Blooio/blooio-java-sdk/commit/a8da67050906f36094fdf4cf4fb278d6abe01f58))
+* **internal:** clean up maven repo artifact script and add html documentation to repo root ([d441da9](https://github.com/Blooio/blooio-java-sdk/commit/d441da9aa8607881a63e2ed14b9037fcccf1600d))
+* **internal:** codegen related update ([ccf0356](https://github.com/Blooio/blooio-java-sdk/commit/ccf03561744755a1bd800449143bc61b847b17ab))
+* **internal:** codegen related update ([58fcb73](https://github.com/Blooio/blooio-java-sdk/commit/58fcb730f98e0c1c1d8006b0aeb96f0aeb678a8b))
+* **internal:** codegen related update ([d939776](https://github.com/Blooio/blooio-java-sdk/commit/d9397764180aeb78502302531fbc19c928fa1ab5))
+* **internal:** codegen related update ([cdac773](https://github.com/Blooio/blooio-java-sdk/commit/cdac773c08fdbc1cfa3ddc812efaa40fb2da4a90))
+* **internal:** codegen related update ([d5926e0](https://github.com/Blooio/blooio-java-sdk/commit/d5926e0c1019ba3d777459623b0b847a286c7b97))
+* **internal:** correct cache invalidation for `SKIP_MOCK_TESTS` ([d3c3b9c](https://github.com/Blooio/blooio-java-sdk/commit/d3c3b9c5e26275edbc5897780f4338d88e65eb7d))
+* **internal:** depend on packages directly in example ([2ddb812](https://github.com/Blooio/blooio-java-sdk/commit/2ddb8126d441b260c80b9ef3b87a24f9ce20b5be))
+* **internal:** improve maven repo docs ([24c2d4b](https://github.com/Blooio/blooio-java-sdk/commit/24c2d4b5b8e3295a5218d2f999fd796fef269b88))
+* **internal:** remove mock server code ([2720979](https://github.com/Blooio/blooio-java-sdk/commit/2720979743e942537ebb7314e83b8f64bc8e3455))
+* **internal:** support uploading Maven repo artifacts to stainless package server ([d790509](https://github.com/Blooio/blooio-java-sdk/commit/d7905093b659e696e05478ee514fbbad370cc9d0))
+* **internal:** update `actions/checkout` version ([8cfab13](https://github.com/Blooio/blooio-java-sdk/commit/8cfab13629687564f829968a161b50843d8ddc73))
+* **internal:** update maven repo doc to include authentication ([0259720](https://github.com/Blooio/blooio-java-sdk/commit/025972007906b16f60bb8dc09ff3433c752f4be0))
+* **internal:** upgrade AssertJ ([0c278a7](https://github.com/Blooio/blooio-java-sdk/commit/0c278a7a62ba05270a09d4ad6d4e0c1eadebd280))
+* redact api-key headers in debug logs ([e7c56e6](https://github.com/Blooio/blooio-java-sdk/commit/e7c56e6299d79215ca5495603e92df1b358af702))
+* remove duplicated dokka setup ([c0c274e](https://github.com/Blooio/blooio-java-sdk/commit/c0c274e30b1bbee4384bd90848bb2ba78be70cae))
+* test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind[#3240](https://github.com/Blooio/blooio-java-sdk/issues/3240) in tests ([b2ac60a](https://github.com/Blooio/blooio-java-sdk/commit/b2ac60a271022b4f389feee2e73467927b2c7a51))
+* update mock server docs ([00e0cf5](https://github.com/Blooio/blooio-java-sdk/commit/00e0cf57e1fb18ddcc685aeea1cce58ac6decf0c))
+* update SDK settings ([c05f9fb](https://github.com/Blooio/blooio-java-sdk/commit/c05f9fb421415ac928f815b14c27c794cf0e771e))
+
+
+### Documentation
+
+* add comment for arbitrary value fields ([a636226](https://github.com/Blooio/blooio-java-sdk/commit/a636226690a94e032e7b7d6e8fcf9d2f373bcfc3))
+* clarify forwards compat behavior ([d05e295](https://github.com/Blooio/blooio-java-sdk/commit/d05e295ff73aeb1af17db057f1903375d8719bef))
+
## 0.0.4 (2025-12-03)
Full Changelog: [v0.0.3...v0.0.4](https://github.com/Blooio/blooio-java-sdk/compare/v0.0.3...v0.0.4)
diff --git a/LICENSE b/LICENSE
index 789f1c0..ce4ae4d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2025 Blooio
+ Copyright 2026 Blooio
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 0ec93d0..17f6b42 100644
--- a/README.md
+++ b/README.md
@@ -2,18 +2,18 @@
-[](https://central.sonatype.com/artifact/com.blooio.api/blooio-java/0.0.4)
-[](https://javadoc.io/doc/com.blooio.api/blooio-java/0.0.4)
+[](https://central.sonatype.com/artifact/com.blooio.api/blooio-java/0.1.0)
+[](https://javadoc.io/doc/com.blooio.api/blooio-java/0.1.0)
-The Blooio Java SDK provides convenient access to the Blooio REST API from applications written in Java.
+The Blooio Java SDK provides convenient access to the [Blooio REST API](https://blooio.com) from applications written in Java.
It is generated with [Stainless](https://www.stainless.com/).
-Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.blooio.api/blooio-java/0.0.4).
+The REST API documentation can be found on [blooio.com](https://blooio.com). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.blooio.api/blooio-java/0.1.0).
@@ -24,7 +24,7 @@ Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.blooio.api/blo
### Gradle
```kotlin
-implementation("com.blooio.api:blooio-java:0.0.4")
+implementation("com.blooio.api:blooio-java:0.1.0")
```
### Maven
@@ -33,7 +33,7 @@ implementation("com.blooio.api:blooio-java:0.0.4")
com.blooio.api
blooio-java
- 0.0.4
+ 0.1.0
```
@@ -98,10 +98,10 @@ BlooioClient client = BlooioOkHttpClient.builder()
See this table for the available options:
-| Setter | System property | Environment variable | Required | Default value |
-| --------- | ---------------- | -------------------- | -------- | ------------------------------ |
-| `apiKey` | `blooio.apiKey` | `BLOOIO_API_KEY` | true | - |
-| `baseUrl` | `blooio.baseUrl` | `BLOOIO_BASE_URL` | true | `"https://backend.blooio.com"` |
+| Setter | System property | Environment variable | Required | Default value |
+| --------- | ---------------- | -------------------- | -------- | ------------------------------------- |
+| `apiKey` | `blooio.apiKey` | `BLOOIO_API_KEY` | true | - |
+| `baseUrl` | `blooio.baseUrl` | `BLOOIO_BASE_URL` | true | `"https://backend.blooio.com/v2/api"` |
System properties take precedence over environment variables.
@@ -174,6 +174,70 @@ CompletableFuture me = client.me().retrieve();
The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
+## File uploads
+
+The SDK defines methods that accept files.
+
+To upload a file, pass a [`Path`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.html):
+
+```java
+import com.blooio.api.models.groups.icon.GroupIcon;
+import com.blooio.api.models.groups.icon.IconSetParams;
+import java.nio.file.Paths;
+
+IconSetParams params = IconSetParams.builder()
+ .groupId("grp_abc123def456")
+ .icon(Paths.get("/path/to/file"))
+ .build();
+GroupIcon groupIcon = client.groups().icon().set(params);
+```
+
+Or an arbitrary [`InputStream`](https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html):
+
+```java
+import com.blooio.api.models.groups.icon.GroupIcon;
+import com.blooio.api.models.groups.icon.IconSetParams;
+import java.net.URL;
+
+IconSetParams params = IconSetParams.builder()
+ .groupId("grp_abc123def456")
+ .icon(new URL("https://example.com//path/to/file").openStream())
+ .build();
+GroupIcon groupIcon = client.groups().icon().set(params);
+```
+
+Or a `byte[]` array:
+
+```java
+import com.blooio.api.models.groups.icon.GroupIcon;
+import com.blooio.api.models.groups.icon.IconSetParams;
+
+IconSetParams params = IconSetParams.builder()
+ .groupId("grp_abc123def456")
+ .icon("content".getBytes())
+ .build();
+GroupIcon groupIcon = client.groups().icon().set(params);
+```
+
+Note that when passing a non-`Path` its filename is unknown so it will not be included in the request. To manually set a filename, pass a [`MultipartField`](blooio-java-core/src/main/kotlin/com/blooio/api/core/Values.kt):
+
+```java
+import com.blooio.api.core.MultipartField;
+import com.blooio.api.models.groups.icon.GroupIcon;
+import com.blooio.api.models.groups.icon.IconSetParams;
+import java.io.InputStream;
+import java.net.URL;
+
+IconSetParams params = IconSetParams.builder()
+ .groupId("grp_abc123def456")
+ .icon(MultipartField.builder()
+ .value(new URL("https://example.com//path/to/file").openStream())
+ .filename("/path/to/file")
+ .build())
+ .build();
+GroupIcon groupIcon = client.groups().icon().set(params);
+```
+
## 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.
@@ -227,8 +291,6 @@ The SDK throws custom unchecked exception types:
## Logging
-The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
-
Enable logging by setting the `BLOOIO_LOG` environment variable to `info`:
```sh
@@ -241,6 +303,19 @@ Or to `debug` for more verbose logging:
export BLOOIO_LOG=debug
```
+Or configure the client manually using the `logLevel` method:
+
+```java
+import com.blooio.api.client.BlooioClient;
+import com.blooio.api.client.okhttp.BlooioOkHttpClient;
+import com.blooio.api.core.LogLevel;
+
+BlooioClient client = BlooioOkHttpClient.builder()
+ .fromEnv()
+ .logLevel(LogLevel.INFO)
+ .build();
+```
+
## ProGuard and R8
Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `blooio-java-core` is published with a [configuration file](blooio-java-core/src/main/resources/META-INF/proguard/blooio-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).
@@ -258,6 +333,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t
> [!CAUTION]
> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
+Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead.
+
## Network options
### Retries
@@ -331,6 +408,40 @@ BlooioClient client = BlooioOkHttpClient.builder()
.build();
```
+If the proxy responds with `407 Proxy Authentication Required`, supply credentials by also configuring `proxyAuthenticator`:
+
+```java
+import com.blooio.api.client.BlooioClient;
+import com.blooio.api.client.okhttp.BlooioOkHttpClient;
+import com.blooio.api.core.http.ProxyAuthenticator;
+
+BlooioClient client = BlooioOkHttpClient.builder()
+ .fromEnv()
+ .proxy(...)
+ // Or a custom implementation of `ProxyAuthenticator`.
+ .proxyAuthenticator(ProxyAuthenticator.basic("username", "password"))
+ .build();
+```
+
+### Connection pooling
+
+To customize the underlying OkHttp connection pool, configure the client using the `maxIdleConnections` and `keepAliveDuration` methods:
+
+```java
+import com.blooio.api.client.BlooioClient;
+import com.blooio.api.client.okhttp.BlooioOkHttpClient;
+import java.time.Duration;
+
+BlooioClient client = BlooioOkHttpClient.builder()
+ .fromEnv()
+ // If `maxIdleConnections` is set, then `keepAliveDuration` must be set, and vice versa.
+ .maxIdleConnections(10)
+ .keepAliveDuration(Duration.ofMinutes(2))
+ .build();
+```
+
+If both options are unset, OkHttp's default connection pool settings are used.
+
### HTTPS
> [!NOTE]
@@ -409,6 +520,21 @@ MeRetrieveParams params = MeRetrieveParams.builder()
These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods.
+To set undocumented parameters on _nested_ headers, query params, or body classes, call the `putAdditionalProperty` method on the nested class:
+
+```java
+import com.blooio.api.core.JsonValue;
+import com.blooio.api.models.me.numbers.contactcard.ContactCardUpdateParams;
+
+ContactCardUpdateParams params = ContactCardUpdateParams.builder()
+ .sharing(ContactCardUpdateParams.Sharing.builder()
+ .putAdditionalProperty("secretProperty", JsonValue.from("42"))
+ .build())
+ .build();
+```
+
+These properties can be accessed on the nested built object later using the `_additionalProperties()` method.
+
To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](blooio-java-core/src/main/kotlin/com/blooio/api/core/Values.kt) object to its setter:
```java
@@ -462,11 +588,11 @@ To forcibly omit a required parameter or property, pass [`JsonMissing`](blooio-j
```java
import com.blooio.api.core.JsonMissing;
-import com.blooio.api.models.contacts.ContactCheckCapabilitiesParams;
import com.blooio.api.models.me.MeRetrieveParams;
+import com.blooio.api.models.me.numbers.contactcard.ContactCardRetrieveParams;
-MeRetrieveParams params = ContactCheckCapabilitiesParams.builder()
- .contact(JsonMissing.of())
+MeRetrieveParams params = ContactCardRetrieveParams.builder()
+ .number(JsonMissing.of())
.build();
```
@@ -530,7 +656,9 @@ In rare cases, the API may return a response that doesn't match the expected typ
By default, the SDK will not throw an exception in this case. It will throw [`BlooioInvalidDataException`](blooio-java-core/src/main/kotlin/com/blooio/api/errors/BlooioInvalidDataException.kt) only if you directly access the property.
-If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
+Validating the response is _not_ forwards compatible with new types from the API for existing fields.
+
+If you would still prefer to check that the response is completely well-typed upfront, then either call `validate()`:
```java
import com.blooio.api.models.me.MeRetrieveResponse;
diff --git a/SECURITY.md b/SECURITY.md
index ea78ff0..4262c82 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -18,6 +18,10 @@ before making any information public.
If you encounter security issues that are not directly related to SDKs but pertain to the services
or products provided by Blooio, please follow the respective company's security reporting guidelines.
+### Blooio Terms and Policies
+
+Please contact support@blooio.com for any questions or concerns regarding the security of our services.
+
---
Thank you for helping us keep the SDKs and systems they interact with secure.
diff --git a/blooio-java-client-okhttp/build.gradle.kts b/blooio-java-client-okhttp/build.gradle.kts
index 3449d62..a403de3 100644
--- a/blooio-java-client-okhttp/build.gradle.kts
+++ b/blooio-java-client-okhttp/build.gradle.kts
@@ -7,9 +7,8 @@ dependencies {
api(project(":blooio-java-core"))
implementation("com.squareup.okhttp3:okhttp:4.12.0")
- implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
testImplementation(kotlin("test"))
- testImplementation("org.assertj:assertj-core:3.25.3")
+ testImplementation("org.assertj:assertj-core:3.27.7")
testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
}
diff --git a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClient.kt b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClient.kt
index 1d24917..ec4fcbc 100644
--- a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClient.kt
+++ b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClient.kt
@@ -5,10 +5,12 @@ package com.blooio.api.client.okhttp
import com.blooio.api.client.BlooioClient
import com.blooio.api.client.BlooioClientImpl
import com.blooio.api.core.ClientOptions
+import com.blooio.api.core.LogLevel
import com.blooio.api.core.Sleeper
import com.blooio.api.core.Timeout
import com.blooio.api.core.http.Headers
import com.blooio.api.core.http.HttpClient
+import com.blooio.api.core.http.ProxyAuthenticator
import com.blooio.api.core.http.QueryParams
import com.blooio.api.core.jsonMapper
import com.fasterxml.jackson.databind.json.JsonMapper
@@ -16,6 +18,7 @@ import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
+import java.util.concurrent.ExecutorService
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
@@ -44,16 +47,93 @@ class BlooioOkHttpClient private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var dispatcherExecutorService: ExecutorService? = null
private var proxy: Proxy? = null
+ private var proxyAuthenticator: ProxyAuthenticator? = null
+ private var maxIdleConnections: Int? = null
+ private var keepAliveDuration: Duration? = null
private var sslSocketFactory: SSLSocketFactory? = null
private var trustManager: X509TrustManager? = null
private var hostnameVerifier: HostnameVerifier? = null
+ /**
+ * The executor service to use for running HTTP requests.
+ *
+ * Defaults to OkHttp's
+ * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
+ *
+ * This class takes ownership of the executor service and shuts it down when closed.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ /**
+ * Alias for calling [Builder.dispatcherExecutorService] with
+ * `dispatcherExecutorService.orElse(null)`.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: Optional) =
+ dispatcherExecutorService(dispatcherExecutorService.getOrNull())
+
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+ /**
+ * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication
+ * Required`.
+ */
+ fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
+ this.proxyAuthenticator = proxyAuthenticator
+ }
+
+ /**
+ * Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`.
+ */
+ fun proxyAuthenticator(proxyAuthenticator: Optional) =
+ proxyAuthenticator(proxyAuthenticator.getOrNull())
+
+ /**
+ * The maximum number of idle connections kept by the underlying OkHttp connection pool.
+ *
+ * If this is set, then [keepAliveDuration] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int?) = apply {
+ this.maxIdleConnections = maxIdleConnections
+ }
+
+ /**
+ * Alias for [Builder.maxIdleConnections].
+ *
+ * This unboxed primitive overload exists for backwards compatibility.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int) =
+ maxIdleConnections(maxIdleConnections as Int?)
+
+ /**
+ * Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`.
+ */
+ fun maxIdleConnections(maxIdleConnections: Optional) =
+ maxIdleConnections(maxIdleConnections.getOrNull())
+
+ /**
+ * The keep-alive duration for idle connections in the underlying OkHttp connection pool.
+ *
+ * If this is set, then [maxIdleConnections] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
+ this.keepAliveDuration = keepAliveDuration
+ }
+
+ /** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */
+ fun keepAliveDuration(keepAliveDuration: Optional) =
+ keepAliveDuration(keepAliveDuration.getOrNull())
+
/**
* The socket factory used to secure HTTPS connections.
*
@@ -144,7 +224,7 @@ class BlooioOkHttpClient private constructor() {
/**
* The base URL to use for every request.
*
- * Defaults to the production environment: `https://backend.blooio.com`.
+ * Defaults to the production environment: `https://backend.blooio.com/v2/api`.
*/
fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
@@ -154,6 +234,9 @@ class BlooioOkHttpClient private constructor() {
/**
* Whether to call `validate` on every response before returning it.
*
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for
+ * existing fields.
+ *
* Defaults to false, which means the shape of the response will not be validated upfront.
* Instead, validation will only occur for the parts of the response that are accessed.
*/
@@ -195,7 +278,16 @@ class BlooioOkHttpClient private constructor() {
*/
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
- /** API key must be provided in the Authorization header as `Bearer YOUR_API_KEY`. */
+ /**
+ * The level at which to log request and response information.
+ *
+ * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
+ *
+ * Defaults to [LogLevel.fromEnv].
+ */
+ fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
+
+ /** API key authentication. Use your API key as the bearer token. */
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
@@ -297,6 +389,10 @@ class BlooioOkHttpClient private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
+ .proxyAuthenticator(proxyAuthenticator)
+ .maxIdleConnections(maxIdleConnections)
+ .keepAliveDuration(keepAliveDuration)
+ .dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
.hostnameVerifier(hostnameVerifier)
diff --git a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClientAsync.kt b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClientAsync.kt
index d5e7916..e11a635 100644
--- a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClientAsync.kt
+++ b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/BlooioOkHttpClientAsync.kt
@@ -5,10 +5,12 @@ package com.blooio.api.client.okhttp
import com.blooio.api.client.BlooioClientAsync
import com.blooio.api.client.BlooioClientAsyncImpl
import com.blooio.api.core.ClientOptions
+import com.blooio.api.core.LogLevel
import com.blooio.api.core.Sleeper
import com.blooio.api.core.Timeout
import com.blooio.api.core.http.Headers
import com.blooio.api.core.http.HttpClient
+import com.blooio.api.core.http.ProxyAuthenticator
import com.blooio.api.core.http.QueryParams
import com.blooio.api.core.jsonMapper
import com.fasterxml.jackson.databind.json.JsonMapper
@@ -16,6 +18,7 @@ import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
+import java.util.concurrent.ExecutorService
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
@@ -44,16 +47,93 @@ class BlooioOkHttpClientAsync private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var dispatcherExecutorService: ExecutorService? = null
private var proxy: Proxy? = null
+ private var proxyAuthenticator: ProxyAuthenticator? = null
+ private var maxIdleConnections: Int? = null
+ private var keepAliveDuration: Duration? = null
private var sslSocketFactory: SSLSocketFactory? = null
private var trustManager: X509TrustManager? = null
private var hostnameVerifier: HostnameVerifier? = null
+ /**
+ * The executor service to use for running HTTP requests.
+ *
+ * Defaults to OkHttp's
+ * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
+ *
+ * This class takes ownership of the executor service and shuts it down when closed.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ /**
+ * Alias for calling [Builder.dispatcherExecutorService] with
+ * `dispatcherExecutorService.orElse(null)`.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: Optional) =
+ dispatcherExecutorService(dispatcherExecutorService.getOrNull())
+
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+ /**
+ * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication
+ * Required`.
+ */
+ fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
+ this.proxyAuthenticator = proxyAuthenticator
+ }
+
+ /**
+ * Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`.
+ */
+ fun proxyAuthenticator(proxyAuthenticator: Optional) =
+ proxyAuthenticator(proxyAuthenticator.getOrNull())
+
+ /**
+ * The maximum number of idle connections kept by the underlying OkHttp connection pool.
+ *
+ * If this is set, then [keepAliveDuration] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int?) = apply {
+ this.maxIdleConnections = maxIdleConnections
+ }
+
+ /**
+ * Alias for [Builder.maxIdleConnections].
+ *
+ * This unboxed primitive overload exists for backwards compatibility.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int) =
+ maxIdleConnections(maxIdleConnections as Int?)
+
+ /**
+ * Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`.
+ */
+ fun maxIdleConnections(maxIdleConnections: Optional) =
+ maxIdleConnections(maxIdleConnections.getOrNull())
+
+ /**
+ * The keep-alive duration for idle connections in the underlying OkHttp connection pool.
+ *
+ * If this is set, then [maxIdleConnections] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
+ this.keepAliveDuration = keepAliveDuration
+ }
+
+ /** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */
+ fun keepAliveDuration(keepAliveDuration: Optional) =
+ keepAliveDuration(keepAliveDuration.getOrNull())
+
/**
* The socket factory used to secure HTTPS connections.
*
@@ -144,7 +224,7 @@ class BlooioOkHttpClientAsync private constructor() {
/**
* The base URL to use for every request.
*
- * Defaults to the production environment: `https://backend.blooio.com`.
+ * Defaults to the production environment: `https://backend.blooio.com/v2/api`.
*/
fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
@@ -154,6 +234,9 @@ class BlooioOkHttpClientAsync private constructor() {
/**
* Whether to call `validate` on every response before returning it.
*
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for
+ * existing fields.
+ *
* Defaults to false, which means the shape of the response will not be validated upfront.
* Instead, validation will only occur for the parts of the response that are accessed.
*/
@@ -195,7 +278,16 @@ class BlooioOkHttpClientAsync private constructor() {
*/
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
- /** API key must be provided in the Authorization header as `Bearer YOUR_API_KEY`. */
+ /**
+ * The level at which to log request and response information.
+ *
+ * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
+ *
+ * Defaults to [LogLevel.fromEnv].
+ */
+ fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
+
+ /** API key authentication. Use your API key as the bearer token. */
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
@@ -297,6 +389,10 @@ class BlooioOkHttpClientAsync private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
+ .proxyAuthenticator(proxyAuthenticator)
+ .maxIdleConnections(maxIdleConnections)
+ .keepAliveDuration(keepAliveDuration)
+ .dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
.hostnameVerifier(hostnameVerifier)
diff --git a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/OkHttpClient.kt b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/OkHttpClient.kt
index a5565a5..7403c9b 100644
--- a/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/OkHttpClient.kt
+++ b/blooio-java-client-okhttp/src/main/kotlin/com/blooio/api/client/okhttp/OkHttpClient.kt
@@ -8,18 +8,26 @@ import com.blooio.api.core.http.HttpMethod
import com.blooio.api.core.http.HttpRequest
import com.blooio.api.core.http.HttpRequestBody
import com.blooio.api.core.http.HttpResponse
+import com.blooio.api.core.http.ProxyAuthenticator
import com.blooio.api.errors.BlooioIoException
import java.io.IOException
import java.io.InputStream
+import java.io.OutputStream
import java.net.Proxy
import java.time.Duration
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.TimeUnit
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
+import kotlin.jvm.optionals.getOrNull
import okhttp3.Call
import okhttp3.Callback
+import okhttp3.ConnectionPool
+import okhttp3.Dispatcher
+import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
@@ -27,17 +35,18 @@ import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
-import okhttp3.logging.HttpLoggingInterceptor
import okio.BufferedSink
+import okio.buffer
+import okio.sink
class OkHttpClient
-private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient {
+internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient {
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
val call = newCall(request, requestOptions)
return try {
- call.execute().toResponse()
+ call.execute().toHttpResponse()
} catch (e: IOException) {
throw BlooioIoException("Request failed", e)
} finally {
@@ -55,7 +64,7 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
call.enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
- future.complete(response.toResponse())
+ future.complete(response.toHttpResponse())
}
override fun onFailure(call: Call, e: IOException) {
@@ -83,18 +92,6 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
val clientBuilder = okHttpClient.newBuilder()
- val logLevel =
- when (System.getenv("BLOOIO_LOG")?.lowercase()) {
- "info" -> HttpLoggingInterceptor.Level.BASIC
- "debug" -> HttpLoggingInterceptor.Level.BODY
- else -> null
- }
- if (logLevel != null) {
- clientBuilder.addNetworkInterceptor(
- HttpLoggingInterceptor().setLevel(logLevel).apply { redactHeader("Authorization") }
- )
- }
-
requestOptions.timeout?.let {
clientBuilder
.connectTimeout(it.connect())
@@ -107,89 +104,6 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
return client.newCall(request.toRequest(client))
}
- private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
- var body: RequestBody? = body?.toRequestBody()
- if (body == null && requiresBody(method)) {
- body = "".toRequestBody()
- }
-
- val builder = Request.Builder().url(toUrl()).method(method.name, body)
- headers.names().forEach { name ->
- headers.values(name).forEach { builder.addHeader(name, it) }
- }
-
- if (
- !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
- ) {
- builder.addHeader(
- "X-Stainless-Read-Timeout",
- Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
- )
- }
- if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
- builder.addHeader(
- "X-Stainless-Timeout",
- Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
- )
- }
-
- return builder.build()
- }
-
- /** `OkHttpClient` always requires a request body for some methods. */
- private fun requiresBody(method: HttpMethod): Boolean =
- when (method) {
- HttpMethod.POST,
- HttpMethod.PUT,
- HttpMethod.PATCH -> true
- else -> false
- }
-
- private fun HttpRequest.toUrl(): String {
- val builder = baseUrl.toHttpUrl().newBuilder()
- pathSegments.forEach(builder::addPathSegment)
- queryParams.keys().forEach { key ->
- queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
- }
-
- return builder.toString()
- }
-
- private fun HttpRequestBody.toRequestBody(): RequestBody {
- val mediaType = contentType()?.toMediaType()
- val length = contentLength()
-
- return object : RequestBody() {
- override fun contentType(): MediaType? = mediaType
-
- override fun contentLength(): Long = length
-
- override fun isOneShot(): Boolean = !repeatable()
-
- override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
- }
- }
-
- private fun Response.toResponse(): HttpResponse {
- val headers = headers.toHeaders()
-
- return object : HttpResponse {
- override fun statusCode(): Int = code
-
- override fun headers(): Headers = headers
-
- override fun body(): InputStream = body!!.byteStream()
-
- override fun close() = body!!.close()
- }
- }
-
- private fun okhttp3.Headers.toHeaders(): Headers {
- val headersBuilder = Headers.builder()
- forEach { (name, value) -> headersBuilder.put(name, value) }
- return headersBuilder.build()
- }
-
companion object {
@JvmStatic fun builder() = Builder()
}
@@ -198,6 +112,10 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
+ private var proxyAuthenticator: ProxyAuthenticator? = null
+ private var maxIdleConnections: Int? = null
+ private var keepAliveDuration: Duration? = null
+ private var dispatcherExecutorService: ExecutorService? = null
private var sslSocketFactory: SSLSocketFactory? = null
private var trustManager: X509TrustManager? = null
private var hostnameVerifier: HostnameVerifier? = null
@@ -208,6 +126,36 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+ fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
+ this.proxyAuthenticator = proxyAuthenticator
+ }
+
+ /**
+ * Sets the maximum number of idle connections kept by the underlying [ConnectionPool].
+ *
+ * If this is set, then [keepAliveDuration] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun maxIdleConnections(maxIdleConnections: Int?) = apply {
+ this.maxIdleConnections = maxIdleConnections
+ }
+
+ /**
+ * Sets the keep-alive duration for idle connections in the underlying [ConnectionPool].
+ *
+ * If this is set, then [maxIdleConnections] must also be set.
+ *
+ * If unset, then OkHttp's default is used.
+ */
+ fun keepAliveDuration(keepAliveDuration: Duration?) = apply {
+ this.keepAliveDuration = keepAliveDuration
+ }
+
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
this.sslSocketFactory = sslSocketFactory
}
@@ -223,12 +171,45 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
fun build(): OkHttpClient =
OkHttpClient(
okhttp3.OkHttpClient.Builder()
+ // `RetryingHttpClient` handles retries if the user enabled them.
+ .retryOnConnectionFailure(false)
.connectTimeout(timeout.connect())
.readTimeout(timeout.read())
.writeTimeout(timeout.write())
.callTimeout(timeout.request())
.proxy(proxy)
.apply {
+ proxyAuthenticator?.let { auth ->
+ proxyAuthenticator { route, response ->
+ auth
+ .authenticate(
+ route?.proxy ?: Proxy.NO_PROXY,
+ response.request.toHttpRequest(),
+ response.toHttpResponse(),
+ )
+ .getOrNull()
+ ?.toRequest(client = null)
+ }
+ }
+
+ dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) }
+
+ val maxIdleConnections = maxIdleConnections
+ val keepAliveDuration = keepAliveDuration
+ if (maxIdleConnections != null && keepAliveDuration != null) {
+ connectionPool(
+ ConnectionPool(
+ maxIdleConnections,
+ keepAliveDuration.toNanos(),
+ TimeUnit.NANOSECONDS,
+ )
+ )
+ } else {
+ check((maxIdleConnections != null) == (keepAliveDuration != null)) {
+ "Both or none of `maxIdleConnections` and `keepAliveDuration` must be set, but only one was set"
+ }
+ }
+
val sslSocketFactory = sslSocketFactory
val trustManager = trustManager
if (sslSocketFactory != null && trustManager != null) {
@@ -250,3 +231,126 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
)
}
}
+
+private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient?): Request {
+ var body: RequestBody? = body?.toRequestBody()
+ if (body == null && requiresBody(method)) {
+ body = "".toRequestBody()
+ }
+
+ val builder = Request.Builder().url(toUrl()).method(method.name, body)
+ headers.names().forEach { name -> headers.values(name).forEach { builder.addHeader(name, it) } }
+
+ if (client != null) {
+ if (
+ !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
+ ) {
+ builder.addHeader(
+ "X-Stainless-Read-Timeout",
+ Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+ if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
+ builder.addHeader(
+ "X-Stainless-Timeout",
+ Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+ }
+
+ return builder.build()
+}
+
+/** `OkHttpClient` always requires a request body for some methods. */
+private fun requiresBody(method: HttpMethod): Boolean =
+ when (method) {
+ HttpMethod.POST,
+ HttpMethod.PUT,
+ HttpMethod.PATCH -> true
+ else -> false
+ }
+
+private fun HttpRequest.toUrl(): String {
+ val builder = baseUrl.toHttpUrl().newBuilder()
+ pathSegments.forEach(builder::addPathSegment)
+ queryParams.keys().forEach { key ->
+ queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
+ }
+
+ return builder.toString()
+}
+
+private fun HttpRequestBody.toRequestBody(): RequestBody {
+ val mediaType = contentType()?.toMediaType()
+ val length = contentLength()
+
+ return object : RequestBody() {
+ override fun contentType(): MediaType? = mediaType
+
+ override fun contentLength(): Long = length
+
+ override fun isOneShot(): Boolean = !repeatable()
+
+ override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
+ }
+}
+
+private fun Request.toHttpRequest(): HttpRequest {
+ val builder = HttpRequest.builder().method(HttpMethod.valueOf(method)).baseUrl(url.toBaseUrl())
+ url.pathSegments.forEach(builder::addPathSegment)
+ url.queryParameterNames.forEach { name ->
+ url.queryParameterValues(name).filterNotNull().forEach { builder.putQueryParam(name, it) }
+ }
+ headers.forEach { (name, value) -> builder.putHeader(name, value) }
+ body?.let { builder.body(it.toHttpRequestBody()) }
+ return builder.build()
+}
+
+private fun HttpUrl.toBaseUrl(): String = buildString {
+ append(scheme).append("://").append(host)
+ if (port != HttpUrl.defaultPort(scheme)) {
+ append(":").append(port)
+ }
+}
+
+private fun RequestBody.toHttpRequestBody(): HttpRequestBody {
+ val mediaType = contentType()?.toString()
+ val length = contentLength()
+ val isOneShot = isOneShot()
+ val source = this
+ return object : HttpRequestBody {
+ override fun contentType(): String? = mediaType
+
+ override fun contentLength(): Long = length
+
+ override fun repeatable(): Boolean = !isOneShot
+
+ override fun writeTo(outputStream: OutputStream) {
+ val sink = outputStream.sink().buffer()
+ source.writeTo(sink)
+ sink.flush()
+ }
+
+ override fun close() {}
+ }
+}
+
+private fun Response.toHttpResponse(): HttpResponse {
+ val headers = headers.toHeaders()
+
+ return object : HttpResponse {
+ override fun statusCode(): Int = code
+
+ override fun headers(): Headers = headers
+
+ override fun body(): InputStream = body!!.byteStream()
+
+ override fun close() = body!!.close()
+ }
+}
+
+private fun okhttp3.Headers.toHeaders(): Headers {
+ val headersBuilder = Headers.builder()
+ forEach { (name, value) -> headersBuilder.put(name, value) }
+ return headersBuilder.build()
+}
diff --git a/blooio-java-core/build.gradle.kts b/blooio-java-core/build.gradle.kts
index 813acf4..18b3914 100644
--- a/blooio-java-core/build.gradle.kts
+++ b/blooio-java-core/build.gradle.kts
@@ -5,14 +5,16 @@ plugins {
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")
+ // Compile and test against a lower Jackson version to ensure we're compatible with it. Note that
+ // we generally support 2.13.4, but test against 2.14.0 because 2.13.4 has some annoying (but
+ // niche) bugs (users should upgrade if they encounter them). 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.14.0")
+ force("com.fasterxml.jackson.core:jackson-databind:2.14.0")
+ force("com.fasterxml.jackson.core:jackson-annotations:2.14.0")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.0")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0")
+ force("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0")
}
}
@@ -25,13 +27,11 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
- implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4")
- implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
testImplementation(kotlin("test"))
testImplementation(project(":blooio-java-client-okhttp"))
testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
- testImplementation("org.assertj:assertj-core:3.25.3")
+ testImplementation("org.assertj:assertj-core:3.27.7")
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")
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClient.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClient.kt
index 77dd88d..97d034c 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClient.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClient.kt
@@ -3,11 +3,14 @@
package com.blooio.api.client
import com.blooio.api.core.ClientOptions
-import com.blooio.api.services.blocking.BatchService
-import com.blooio.api.services.blocking.ConfigService
+import com.blooio.api.services.blocking.ChatService
import com.blooio.api.services.blocking.ContactService
+import com.blooio.api.services.blocking.FacetimeService
+import com.blooio.api.services.blocking.GroupService
+import com.blooio.api.services.blocking.LocationService
import com.blooio.api.services.blocking.MeService
-import com.blooio.api.services.blocking.MessageService
+import com.blooio.api.services.blocking.PhoneNumberService
+import com.blooio.api.services.blocking.WebhookService
import java.util.function.Consumer
/**
@@ -46,15 +49,30 @@ interface BlooioClient {
*/
fun withOptions(modifier: Consumer): BlooioClient
+ /** Authentication and account information */
fun me(): MeService
+ /** Manage contacts (phone numbers and emails) */
fun contacts(): ContactService
- fun messages(): MessageService
+ fun location(): LocationService
- fun config(): ConfigService
+ /** Initiate FaceTime calls */
+ fun facetime(): FacetimeService
- fun batches(): BatchService
+ /** Manage contact groups */
+ fun groups(): GroupService
+
+ /** Manage webhook subscriptions */
+ fun webhooks(): WebhookService
+
+ fun chats(): ChatService
+
+ /**
+ * Phone number validation, formatting, and NANPA geocoding. Requires an Enterprise plan
+ * (Dedicated Enterprise).
+ */
+ fun phoneNumbers(): PhoneNumberService
/**
* Closes this client, relinquishing any underlying resources.
@@ -79,14 +97,29 @@ interface BlooioClient {
*/
fun withOptions(modifier: Consumer): BlooioClient.WithRawResponse
+ /** Authentication and account information */
fun me(): MeService.WithRawResponse
+ /** Manage contacts (phone numbers and emails) */
fun contacts(): ContactService.WithRawResponse
- fun messages(): MessageService.WithRawResponse
+ fun location(): LocationService.WithRawResponse
- fun config(): ConfigService.WithRawResponse
+ /** Initiate FaceTime calls */
+ fun facetime(): FacetimeService.WithRawResponse
- fun batches(): BatchService.WithRawResponse
+ /** Manage contact groups */
+ fun groups(): GroupService.WithRawResponse
+
+ /** Manage webhook subscriptions */
+ fun webhooks(): WebhookService.WithRawResponse
+
+ fun chats(): ChatService.WithRawResponse
+
+ /**
+ * Phone number validation, formatting, and NANPA geocoding. Requires an Enterprise plan
+ * (Dedicated Enterprise).
+ */
+ fun phoneNumbers(): PhoneNumberService.WithRawResponse
}
}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientAsync.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientAsync.kt
index 83256cc..0a3eafc 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientAsync.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientAsync.kt
@@ -3,11 +3,14 @@
package com.blooio.api.client
import com.blooio.api.core.ClientOptions
-import com.blooio.api.services.async.BatchServiceAsync
-import com.blooio.api.services.async.ConfigServiceAsync
+import com.blooio.api.services.async.ChatServiceAsync
import com.blooio.api.services.async.ContactServiceAsync
+import com.blooio.api.services.async.FacetimeServiceAsync
+import com.blooio.api.services.async.GroupServiceAsync
+import com.blooio.api.services.async.LocationServiceAsync
import com.blooio.api.services.async.MeServiceAsync
-import com.blooio.api.services.async.MessageServiceAsync
+import com.blooio.api.services.async.PhoneNumberServiceAsync
+import com.blooio.api.services.async.WebhookServiceAsync
import java.util.function.Consumer
/**
@@ -46,15 +49,30 @@ interface BlooioClientAsync {
*/
fun withOptions(modifier: Consumer): BlooioClientAsync
+ /** Authentication and account information */
fun me(): MeServiceAsync
+ /** Manage contacts (phone numbers and emails) */
fun contacts(): ContactServiceAsync
- fun messages(): MessageServiceAsync
+ fun location(): LocationServiceAsync
- fun config(): ConfigServiceAsync
+ /** Initiate FaceTime calls */
+ fun facetime(): FacetimeServiceAsync
- fun batches(): BatchServiceAsync
+ /** Manage contact groups */
+ fun groups(): GroupServiceAsync
+
+ /** Manage webhook subscriptions */
+ fun webhooks(): WebhookServiceAsync
+
+ fun chats(): ChatServiceAsync
+
+ /**
+ * Phone number validation, formatting, and NANPA geocoding. Requires an Enterprise plan
+ * (Dedicated Enterprise).
+ */
+ fun phoneNumbers(): PhoneNumberServiceAsync
/**
* Closes this client, relinquishing any underlying resources.
@@ -81,14 +99,29 @@ interface BlooioClientAsync {
modifier: Consumer
): BlooioClientAsync.WithRawResponse
+ /** Authentication and account information */
fun me(): MeServiceAsync.WithRawResponse
+ /** Manage contacts (phone numbers and emails) */
fun contacts(): ContactServiceAsync.WithRawResponse
- fun messages(): MessageServiceAsync.WithRawResponse
+ fun location(): LocationServiceAsync.WithRawResponse
- fun config(): ConfigServiceAsync.WithRawResponse
+ /** Initiate FaceTime calls */
+ fun facetime(): FacetimeServiceAsync.WithRawResponse
- fun batches(): BatchServiceAsync.WithRawResponse
+ /** Manage contact groups */
+ fun groups(): GroupServiceAsync.WithRawResponse
+
+ /** Manage webhook subscriptions */
+ fun webhooks(): WebhookServiceAsync.WithRawResponse
+
+ fun chats(): ChatServiceAsync.WithRawResponse
+
+ /**
+ * Phone number validation, formatting, and NANPA geocoding. Requires an Enterprise plan
+ * (Dedicated Enterprise).
+ */
+ fun phoneNumbers(): PhoneNumberServiceAsync.WithRawResponse
}
}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientAsyncImpl.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientAsyncImpl.kt
index 764d2b2..fae4817 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientAsyncImpl.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientAsyncImpl.kt
@@ -4,16 +4,22 @@ package com.blooio.api.client
import com.blooio.api.core.ClientOptions
import com.blooio.api.core.getPackageVersion
-import com.blooio.api.services.async.BatchServiceAsync
-import com.blooio.api.services.async.BatchServiceAsyncImpl
-import com.blooio.api.services.async.ConfigServiceAsync
-import com.blooio.api.services.async.ConfigServiceAsyncImpl
+import com.blooio.api.services.async.ChatServiceAsync
+import com.blooio.api.services.async.ChatServiceAsyncImpl
import com.blooio.api.services.async.ContactServiceAsync
import com.blooio.api.services.async.ContactServiceAsyncImpl
+import com.blooio.api.services.async.FacetimeServiceAsync
+import com.blooio.api.services.async.FacetimeServiceAsyncImpl
+import com.blooio.api.services.async.GroupServiceAsync
+import com.blooio.api.services.async.GroupServiceAsyncImpl
+import com.blooio.api.services.async.LocationServiceAsync
+import com.blooio.api.services.async.LocationServiceAsyncImpl
import com.blooio.api.services.async.MeServiceAsync
import com.blooio.api.services.async.MeServiceAsyncImpl
-import com.blooio.api.services.async.MessageServiceAsync
-import com.blooio.api.services.async.MessageServiceAsyncImpl
+import com.blooio.api.services.async.PhoneNumberServiceAsync
+import com.blooio.api.services.async.PhoneNumberServiceAsyncImpl
+import com.blooio.api.services.async.WebhookServiceAsync
+import com.blooio.api.services.async.WebhookServiceAsyncImpl
import java.util.function.Consumer
class BlooioClientAsyncImpl(private val clientOptions: ClientOptions) : BlooioClientAsync {
@@ -39,16 +45,26 @@ class BlooioClientAsyncImpl(private val clientOptions: ClientOptions) : BlooioCl
ContactServiceAsyncImpl(clientOptionsWithUserAgent)
}
- private val messages: MessageServiceAsync by lazy {
- MessageServiceAsyncImpl(clientOptionsWithUserAgent)
+ private val location: LocationServiceAsync by lazy {
+ LocationServiceAsyncImpl(clientOptionsWithUserAgent)
}
- private val config: ConfigServiceAsync by lazy {
- ConfigServiceAsyncImpl(clientOptionsWithUserAgent)
+ private val facetime: FacetimeServiceAsync by lazy {
+ FacetimeServiceAsyncImpl(clientOptionsWithUserAgent)
}
- private val batches: BatchServiceAsync by lazy {
- BatchServiceAsyncImpl(clientOptionsWithUserAgent)
+ private val groups: GroupServiceAsync by lazy {
+ GroupServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val webhooks: WebhookServiceAsync by lazy {
+ WebhookServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val chats: ChatServiceAsync by lazy { ChatServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val phoneNumbers: PhoneNumberServiceAsync by lazy {
+ PhoneNumberServiceAsyncImpl(clientOptionsWithUserAgent)
}
override fun sync(): BlooioClient = sync
@@ -58,15 +74,30 @@ class BlooioClientAsyncImpl(private val clientOptions: ClientOptions) : BlooioCl
override fun withOptions(modifier: Consumer): BlooioClientAsync =
BlooioClientAsyncImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+ /** Authentication and account information */
override fun me(): MeServiceAsync = me
+ /** Manage contacts (phone numbers and emails) */
override fun contacts(): ContactServiceAsync = contacts
- override fun messages(): MessageServiceAsync = messages
+ override fun location(): LocationServiceAsync = location
- override fun config(): ConfigServiceAsync = config
+ /** Initiate FaceTime calls */
+ override fun facetime(): FacetimeServiceAsync = facetime
- override fun batches(): BatchServiceAsync = batches
+ /** Manage contact groups */
+ override fun groups(): GroupServiceAsync = groups
+
+ /** Manage webhook subscriptions */
+ override fun webhooks(): WebhookServiceAsync = webhooks
+
+ override fun chats(): ChatServiceAsync = chats
+
+ /**
+ * Phone number validation, formatting, and NANPA geocoding. Requires an Enterprise plan
+ * (Dedicated Enterprise).
+ */
+ override fun phoneNumbers(): PhoneNumberServiceAsync = phoneNumbers
override fun close() = clientOptions.close()
@@ -81,16 +112,28 @@ class BlooioClientAsyncImpl(private val clientOptions: ClientOptions) : BlooioCl
ContactServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
- private val messages: MessageServiceAsync.WithRawResponse by lazy {
- MessageServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ private val location: LocationServiceAsync.WithRawResponse by lazy {
+ LocationServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
- private val config: ConfigServiceAsync.WithRawResponse by lazy {
- ConfigServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ private val facetime: FacetimeServiceAsync.WithRawResponse by lazy {
+ FacetimeServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
- private val batches: BatchServiceAsync.WithRawResponse by lazy {
- BatchServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ private val groups: GroupServiceAsync.WithRawResponse by lazy {
+ GroupServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val webhooks: WebhookServiceAsync.WithRawResponse by lazy {
+ WebhookServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val chats: ChatServiceAsync.WithRawResponse by lazy {
+ ChatServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val phoneNumbers: PhoneNumberServiceAsync.WithRawResponse by lazy {
+ PhoneNumberServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
override fun withOptions(
@@ -100,14 +143,29 @@ class BlooioClientAsyncImpl(private val clientOptions: ClientOptions) : BlooioCl
clientOptions.toBuilder().apply(modifier::accept).build()
)
+ /** Authentication and account information */
override fun me(): MeServiceAsync.WithRawResponse = me
+ /** Manage contacts (phone numbers and emails) */
override fun contacts(): ContactServiceAsync.WithRawResponse = contacts
- override fun messages(): MessageServiceAsync.WithRawResponse = messages
+ override fun location(): LocationServiceAsync.WithRawResponse = location
+
+ /** Initiate FaceTime calls */
+ override fun facetime(): FacetimeServiceAsync.WithRawResponse = facetime
+
+ /** Manage contact groups */
+ override fun groups(): GroupServiceAsync.WithRawResponse = groups
+
+ /** Manage webhook subscriptions */
+ override fun webhooks(): WebhookServiceAsync.WithRawResponse = webhooks
- override fun config(): ConfigServiceAsync.WithRawResponse = config
+ override fun chats(): ChatServiceAsync.WithRawResponse = chats
- override fun batches(): BatchServiceAsync.WithRawResponse = batches
+ /**
+ * Phone number validation, formatting, and NANPA geocoding. Requires an Enterprise plan
+ * (Dedicated Enterprise).
+ */
+ override fun phoneNumbers(): PhoneNumberServiceAsync.WithRawResponse = phoneNumbers
}
}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientImpl.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientImpl.kt
index 50431c9..f8e0b0a 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientImpl.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/client/BlooioClientImpl.kt
@@ -4,16 +4,22 @@ package com.blooio.api.client
import com.blooio.api.core.ClientOptions
import com.blooio.api.core.getPackageVersion
-import com.blooio.api.services.blocking.BatchService
-import com.blooio.api.services.blocking.BatchServiceImpl
-import com.blooio.api.services.blocking.ConfigService
-import com.blooio.api.services.blocking.ConfigServiceImpl
+import com.blooio.api.services.blocking.ChatService
+import com.blooio.api.services.blocking.ChatServiceImpl
import com.blooio.api.services.blocking.ContactService
import com.blooio.api.services.blocking.ContactServiceImpl
+import com.blooio.api.services.blocking.FacetimeService
+import com.blooio.api.services.blocking.FacetimeServiceImpl
+import com.blooio.api.services.blocking.GroupService
+import com.blooio.api.services.blocking.GroupServiceImpl
+import com.blooio.api.services.blocking.LocationService
+import com.blooio.api.services.blocking.LocationServiceImpl
import com.blooio.api.services.blocking.MeService
import com.blooio.api.services.blocking.MeServiceImpl
-import com.blooio.api.services.blocking.MessageService
-import com.blooio.api.services.blocking.MessageServiceImpl
+import com.blooio.api.services.blocking.PhoneNumberService
+import com.blooio.api.services.blocking.PhoneNumberServiceImpl
+import com.blooio.api.services.blocking.WebhookService
+import com.blooio.api.services.blocking.WebhookServiceImpl
import java.util.function.Consumer
class BlooioClientImpl(private val clientOptions: ClientOptions) : BlooioClient {
@@ -37,11 +43,23 @@ class BlooioClientImpl(private val clientOptions: ClientOptions) : BlooioClient
private val contacts: ContactService by lazy { ContactServiceImpl(clientOptionsWithUserAgent) }
- private val messages: MessageService by lazy { MessageServiceImpl(clientOptionsWithUserAgent) }
+ private val location: LocationService by lazy {
+ LocationServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val facetime: FacetimeService by lazy {
+ FacetimeServiceImpl(clientOptionsWithUserAgent)
+ }
- private val config: ConfigService by lazy { ConfigServiceImpl(clientOptionsWithUserAgent) }
+ private val groups: GroupService by lazy { GroupServiceImpl(clientOptionsWithUserAgent) }
- private val batches: BatchService by lazy { BatchServiceImpl(clientOptionsWithUserAgent) }
+ private val webhooks: WebhookService by lazy { WebhookServiceImpl(clientOptionsWithUserAgent) }
+
+ private val chats: ChatService by lazy { ChatServiceImpl(clientOptionsWithUserAgent) }
+
+ private val phoneNumbers: PhoneNumberService by lazy {
+ PhoneNumberServiceImpl(clientOptionsWithUserAgent)
+ }
override fun async(): BlooioClientAsync = async
@@ -50,15 +68,30 @@ class BlooioClientImpl(private val clientOptions: ClientOptions) : BlooioClient
override fun withOptions(modifier: Consumer): BlooioClient =
BlooioClientImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+ /** Authentication and account information */
override fun me(): MeService = me
+ /** Manage contacts (phone numbers and emails) */
override fun contacts(): ContactService = contacts
- override fun messages(): MessageService = messages
+ override fun location(): LocationService = location
+
+ /** Initiate FaceTime calls */
+ override fun facetime(): FacetimeService = facetime
+
+ /** Manage contact groups */
+ override fun groups(): GroupService = groups
+
+ /** Manage webhook subscriptions */
+ override fun webhooks(): WebhookService = webhooks
- override fun config(): ConfigService = config
+ override fun chats(): ChatService = chats
- override fun batches(): BatchService = batches
+ /**
+ * Phone number validation, formatting, and NANPA geocoding. Requires an Enterprise plan
+ * (Dedicated Enterprise).
+ */
+ override fun phoneNumbers(): PhoneNumberService = phoneNumbers
override fun close() = clientOptions.close()
@@ -73,16 +106,28 @@ class BlooioClientImpl(private val clientOptions: ClientOptions) : BlooioClient
ContactServiceImpl.WithRawResponseImpl(clientOptions)
}
- private val messages: MessageService.WithRawResponse by lazy {
- MessageServiceImpl.WithRawResponseImpl(clientOptions)
+ private val location: LocationService.WithRawResponse by lazy {
+ LocationServiceImpl.WithRawResponseImpl(clientOptions)
}
- private val config: ConfigService.WithRawResponse by lazy {
- ConfigServiceImpl.WithRawResponseImpl(clientOptions)
+ private val facetime: FacetimeService.WithRawResponse by lazy {
+ FacetimeServiceImpl.WithRawResponseImpl(clientOptions)
}
- private val batches: BatchService.WithRawResponse by lazy {
- BatchServiceImpl.WithRawResponseImpl(clientOptions)
+ private val groups: GroupService.WithRawResponse by lazy {
+ GroupServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val webhooks: WebhookService.WithRawResponse by lazy {
+ WebhookServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val chats: ChatService.WithRawResponse by lazy {
+ ChatServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val phoneNumbers: PhoneNumberService.WithRawResponse by lazy {
+ PhoneNumberServiceImpl.WithRawResponseImpl(clientOptions)
}
override fun withOptions(
@@ -92,14 +137,29 @@ class BlooioClientImpl(private val clientOptions: ClientOptions) : BlooioClient
clientOptions.toBuilder().apply(modifier::accept).build()
)
+ /** Authentication and account information */
override fun me(): MeService.WithRawResponse = me
+ /** Manage contacts (phone numbers and emails) */
override fun contacts(): ContactService.WithRawResponse = contacts
- override fun messages(): MessageService.WithRawResponse = messages
+ override fun location(): LocationService.WithRawResponse = location
+
+ /** Initiate FaceTime calls */
+ override fun facetime(): FacetimeService.WithRawResponse = facetime
+
+ /** Manage contact groups */
+ override fun groups(): GroupService.WithRawResponse = groups
+
+ /** Manage webhook subscriptions */
+ override fun webhooks(): WebhookService.WithRawResponse = webhooks
- override fun config(): ConfigService.WithRawResponse = config
+ override fun chats(): ChatService.WithRawResponse = chats
- override fun batches(): BatchService.WithRawResponse = batches
+ /**
+ * Phone number validation, formatting, and NANPA geocoding. Requires an Enterprise plan
+ * (Dedicated Enterprise).
+ */
+ override fun phoneNumbers(): PhoneNumberService.WithRawResponse = phoneNumbers
}
}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/ClientOptions.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/ClientOptions.kt
index 815c3e2..540df33 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/ClientOptions.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/ClientOptions.kt
@@ -4,6 +4,7 @@ package com.blooio.api.core
import com.blooio.api.core.http.Headers
import com.blooio.api.core.http.HttpClient
+import com.blooio.api.core.http.LoggingHttpClient
import com.blooio.api.core.http.PhantomReachableClosingHttpClient
import com.blooio.api.core.http.QueryParams
import com.blooio.api.core.http.RetryingHttpClient
@@ -66,6 +67,9 @@ private constructor(
/**
* Whether to call `validate` on every response before returning it.
*
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for existing
+ * fields.
+ *
* Defaults to false, which means the shape of the response will not be validated upfront.
* Instead, validation will only occur for the parts of the response that are accessed.
*/
@@ -93,7 +97,15 @@ private constructor(
* Defaults to 2.
*/
@get:JvmName("maxRetries") val maxRetries: Int,
- /** API key must be provided in the Authorization header as `Bearer YOUR_API_KEY`. */
+ /**
+ * The level at which to log request and response information.
+ *
+ * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
+ *
+ * Defaults to [LogLevel.fromEnv].
+ */
+ @get:JvmName("logLevel") val logLevel: LogLevel,
+ /** API key authentication. Use your API key as the bearer token. */
@get:JvmName("apiKey") val apiKey: String,
) {
@@ -106,7 +118,7 @@ private constructor(
/**
* The base URL to use for every request.
*
- * Defaults to the production environment: `https://backend.blooio.com`.
+ * Defaults to the production environment: `https://backend.blooio.com/v2/api`.
*/
fun baseUrl(): String = baseUrl ?: PRODUCTION_URL
@@ -114,7 +126,7 @@ private constructor(
companion object {
- const val PRODUCTION_URL = "https://backend.blooio.com"
+ const val PRODUCTION_URL = "https://backend.blooio.com/v2/api"
/**
* Returns a mutable builder for constructing an instance of [ClientOptions].
@@ -149,6 +161,7 @@ private constructor(
private var responseValidation: Boolean = false
private var timeout: Timeout = Timeout.default()
private var maxRetries: Int = 2
+ private var logLevel: LogLevel = LogLevel.fromEnv()
private var apiKey: String? = null
@JvmSynthetic
@@ -164,6 +177,7 @@ private constructor(
responseValidation = clientOptions.responseValidation
timeout = clientOptions.timeout
maxRetries = clientOptions.maxRetries
+ logLevel = clientOptions.logLevel
apiKey = clientOptions.apiKey
}
@@ -220,7 +234,7 @@ private constructor(
/**
* The base URL to use for every request.
*
- * Defaults to the production environment: `https://backend.blooio.com`.
+ * Defaults to the production environment: `https://backend.blooio.com/v2/api`.
*/
fun baseUrl(baseUrl: String?) = apply { this.baseUrl = baseUrl }
@@ -230,6 +244,9 @@ private constructor(
/**
* Whether to call `validate` on every response before returning it.
*
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for
+ * existing fields.
+ *
* Defaults to false, which means the shape of the response will not be validated upfront.
* Instead, validation will only occur for the parts of the response that are accessed.
*/
@@ -271,7 +288,16 @@ private constructor(
*/
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
- /** API key must be provided in the Authorization header as `Bearer YOUR_API_KEY`. */
+ /**
+ * The level at which to log request and response information.
+ *
+ * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
+ *
+ * Defaults to [LogLevel.fromEnv].
+ */
+ fun logLevel(logLevel: LogLevel) = apply { this.logLevel = logLevel }
+
+ /** API key authentication. Use your API key as the bearer token. */
fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
fun headers(headers: Headers) = apply {
@@ -361,20 +387,29 @@ private constructor(
*
* See this table for the available options:
*
- * |Setter |System property |Environment variable|Required|Default value |
- * |---------|----------------|--------------------|--------|------------------------------|
- * |`apiKey` |`blooio.apiKey` |`BLOOIO_API_KEY` |true |- |
- * |`baseUrl`|`blooio.baseUrl`|`BLOOIO_BASE_URL` |true |`"https://backend.blooio.com"`|
+ * |Setter |System property |Environment variable|Required|Default value |
+ * |---------|----------------|--------------------|--------|-------------------------------------|
+ * |`apiKey` |`blooio.apiKey` |`BLOOIO_API_KEY` |true |- |
+ * |`baseUrl`|`blooio.baseUrl`|`BLOOIO_BASE_URL` |true |`"https://backend.blooio.com/v2/api"`|
*
* System properties take precedence over environment variables.
*/
fun fromEnv() = apply {
+ logLevel(LogLevel.fromEnv())
(System.getProperty("blooio.baseUrl") ?: System.getenv("BLOOIO_BASE_URL"))?.let {
baseUrl(it)
}
(System.getProperty("blooio.apiKey") ?: System.getenv("BLOOIO_API_KEY"))?.let {
apiKey(it)
}
+ System.getenv("BLOOIO_CUSTOM_HEADERS")?.let { customHeadersEnv ->
+ for (line in customHeadersEnv.split("\n")) {
+ val colon = line.indexOf(':')
+ if (colon >= 0) {
+ putHeader(line.substring(0, colon).trim(), line.substring(colon + 1).trim())
+ }
+ }
+ }
}
/**
@@ -404,18 +439,26 @@ private constructor(
headers.put("X-Stainless-Package-Version", getPackageVersion())
headers.put("X-Stainless-Runtime", "JRE")
headers.put("X-Stainless-Runtime-Version", getJavaVersion())
+ headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString())
+ // We replace after all the default headers to allow end-users to overwrite them.
+ headers.replaceAll(this.headers.build())
+ queryParams.replaceAll(this.queryParams.build())
apiKey.let {
if (!it.isEmpty()) {
- headers.put("Authorization", "Bearer $it")
+ headers.replace("Authorization", "Bearer $it")
}
}
- headers.replaceAll(this.headers.build())
- queryParams.replaceAll(this.queryParams.build())
return ClientOptions(
httpClient,
RetryingHttpClient.builder()
- .httpClient(httpClient)
+ .httpClient(
+ LoggingHttpClient.builder()
+ .httpClient(httpClient)
+ .clock(clock)
+ .level(logLevel)
+ .build()
+ )
.sleeper(sleeper)
.clock(clock)
.maxRetries(maxRetries)
@@ -430,6 +473,7 @@ private constructor(
responseValidation,
timeout,
maxRetries,
+ logLevel,
apiKey,
)
}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/LogLevel.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/LogLevel.kt
new file mode 100644
index 0000000..b03b996
--- /dev/null
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/LogLevel.kt
@@ -0,0 +1,33 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.blooio.api.core
+
+/** The level at which to log request and response information. */
+enum class LogLevel {
+ /** No logging. */
+ OFF,
+ /** Minimal request and response summary logs. No headers or bodies are logged. */
+ INFO,
+ /** [INFO] logs plus details about request failures. */
+ ERROR,
+ /**
+ * Full request and response logs. Sensitive headers are redacted, but sensitive data in request
+ * and response bodies may still be visible.
+ */
+ DEBUG;
+
+ /** Returns whether this level is at or higher than the given [level]. */
+ fun shouldLog(level: LogLevel): Boolean = ordinal >= level.ordinal
+
+ companion object {
+
+ /** Returns a [LogLevel] based on the `BLOOIO_LOG` environment variable. */
+ fun fromEnv() =
+ when (System.getenv("BLOOIO_LOG")?.lowercase()) {
+ "info" -> INFO
+ "error" -> ERROR
+ "debug" -> DEBUG
+ else -> OFF
+ }
+ }
+}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/ObjectMappers.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/ObjectMappers.kt
index 555296c..5bf6b78 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/ObjectMappers.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/ObjectMappers.kt
@@ -24,11 +24,14 @@ import java.io.InputStream
import java.time.DateTimeException
import java.time.LocalDate
import java.time.LocalDateTime
-import java.time.ZonedDateTime
+import java.time.OffsetDateTime
+import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoField
-fun jsonMapper(): JsonMapper =
+fun jsonMapper(): JsonMapper = JSON_MAPPER
+
+private val JSON_MAPPER: JsonMapper =
JsonMapper.builder()
.addModule(kotlinModule())
.addModule(Jdk8Module())
@@ -36,7 +39,7 @@ fun jsonMapper(): JsonMapper =
.addModule(
SimpleModule()
.addSerializer(InputStreamSerializer)
- .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
+ .addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer())
)
.withCoercionConfig(LogicalType.Boolean) {
it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
@@ -47,6 +50,7 @@ fun jsonMapper(): JsonMapper =
}
.withCoercionConfig(LogicalType.Integer) {
it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
@@ -64,6 +68,12 @@ fun jsonMapper(): JsonMapper =
.setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
}
+ .withCoercionConfig(LogicalType.DateTime) {
+ it.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)
@@ -124,10 +134,10 @@ private object InputStreamSerializer : BaseSerializer(InputStream::
}
/**
- * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
+ * A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes.
*/
-private class LenientLocalDateTimeDeserializer :
- StdDeserializer(LocalDateTime::class.java) {
+private class LenientOffsetDateTimeDeserializer :
+ StdDeserializer(OffsetDateTime::class.java) {
companion object {
@@ -141,7 +151,7 @@ private class LenientLocalDateTimeDeserializer :
override fun logicalType(): LogicalType = LogicalType.DateTime
- override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
+ override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime {
val exceptions = mutableListOf()
for (formatter in DATE_TIME_FORMATTERS) {
@@ -150,17 +160,20 @@ private class LenientLocalDateTimeDeserializer :
return when {
!temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
- LocalDate.from(temporal).atStartOfDay()
+ LocalDate.from(temporal)
+ .atStartOfDay()
+ .atZone(ZoneId.of("UTC"))
+ .toOffsetDateTime()
!temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
- LocalDateTime.from(temporal)
- else -> ZonedDateTime.from(temporal).toLocalDateTime()
+ LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime()
+ else -> OffsetDateTime.from(temporal)
}
} catch (e: DateTimeException) {
exceptions.add(e)
}
}
- throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
+ throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply {
exceptions.forEach { addSuppressed(it) }
}
}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/Properties.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/Properties.kt
index 5aa1fbd..60ca187 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/Properties.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/Properties.kt
@@ -34,9 +34,9 @@ fun getOsName(): String {
}
}
-fun getOsVersion(): String = System.getProperty("os.version", "unknown")
+fun getOsVersion(): String = System.getProperty("os.version", "unknown") ?: "unknown"
fun getPackageVersion(): String =
- BlooioClient::class.java.`package`.implementationVersion ?: "unknown"
+ BlooioClient::class.java.`package`?.implementationVersion ?: "unknown"
-fun getJavaVersion(): String = System.getProperty("java.version", "unknown")
+fun getJavaVersion(): String = System.getProperty("java.version", "unknown") ?: "unknown"
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/RequestOptions.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/RequestOptions.kt
index 33976b6..9547b58 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/RequestOptions.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/RequestOptions.kt
@@ -33,6 +33,15 @@ class RequestOptions private constructor(val responseValidation: Boolean?, val t
private var responseValidation: Boolean? = null
private var timeout: Timeout? = null
+ /**
+ * Whether to call `validate` on the response before returning it.
+ *
+ * Setting this to `true` is _not_ forwards compatible with new types from the API for
+ * existing fields.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
fun responseValidation(responseValidation: Boolean) = apply {
this.responseValidation = responseValidation
}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/Utils.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/Utils.kt
index 2cc7fdd..dde0e91 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/Utils.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/Utils.kt
@@ -5,6 +5,7 @@ package com.blooio.api.core
import com.blooio.api.errors.BlooioInvalidDataException
import java.util.Collections
import java.util.SortedMap
+import java.util.SortedSet
import java.util.concurrent.CompletableFuture
import java.util.concurrent.locks.Lock
@@ -16,6 +17,11 @@ internal fun T?.getOrThrow(name: String): T =
internal fun List.toImmutable(): List =
if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList())
+@JvmSynthetic
+internal fun > SortedSet.toImmutable(): SortedSet =
+ if (isEmpty()) Collections.emptySortedSet()
+ else Collections.unmodifiableSortedSet(toSortedSet(comparator() ?: Comparator.naturalOrder()))
+
@JvmSynthetic
internal fun Map.toImmutable(): Map =
if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/handlers/EmptyHandler.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/handlers/EmptyHandler.kt
deleted file mode 100644
index 1b2fcda..0000000
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/handlers/EmptyHandler.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-@file:JvmName("EmptyHandler")
-
-package com.blooio.api.core.handlers
-
-import com.blooio.api.core.http.HttpResponse
-import com.blooio.api.core.http.HttpResponse.Handler
-
-@JvmSynthetic internal fun emptyHandler(): Handler = EmptyHandlerInternal
-
-private object EmptyHandlerInternal : Handler {
- override fun handle(response: HttpResponse): Void? = null
-}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequest.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequest.kt
index 7c10228..5bb901b 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequest.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequest.kt
@@ -2,6 +2,7 @@ package com.blooio.api.core.http
import com.blooio.api.core.checkRequired
import com.blooio.api.core.toImmutable
+import java.net.URLEncoder
class HttpRequest
private constructor(
@@ -13,6 +14,35 @@ private constructor(
@get:JvmName("body") val body: HttpRequestBody?,
) {
+ fun url(): String = buildString {
+ append(baseUrl)
+
+ pathSegments.forEach { segment ->
+ if (!endsWith("/")) {
+ append("/")
+ }
+ append(URLEncoder.encode(segment, "UTF-8"))
+ }
+
+ if (queryParams.isEmpty()) {
+ return@buildString
+ }
+
+ append("?")
+ var isFirst = true
+ queryParams.keys().forEach { key ->
+ queryParams.values(key).forEach { value ->
+ if (!isFirst) {
+ append("&")
+ }
+ append(URLEncoder.encode(key, "UTF-8"))
+ append("=")
+ append(URLEncoder.encode(value, "UTF-8"))
+ isFirst = false
+ }
+ }
+ }
+
fun toBuilder(): Builder = Builder().from(this)
override fun toString(): String =
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequestBodies.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequestBodies.kt
index c2c707d..6febc5e 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequestBodies.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/HttpRequestBodies.kt
@@ -5,16 +5,16 @@
package com.blooio.api.core.http
import com.blooio.api.core.MultipartField
+import com.blooio.api.core.toImmutable
import com.blooio.api.errors.BlooioInvalidDataException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.node.JsonNodeType
+import java.io.ByteArrayInputStream
import java.io.InputStream
import java.io.OutputStream
+import java.util.UUID
import kotlin.jvm.optionals.getOrNull
-import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
-import org.apache.hc.core5.http.ContentType
-import org.apache.hc.core5.http.HttpEntity
@JvmSynthetic
internal inline fun json(jsonMapper: JsonMapper, value: T): HttpRequestBody =
@@ -37,92 +37,231 @@ internal fun multipartFormData(
jsonMapper: JsonMapper,
fields: Map>,
): HttpRequestBody =
- object : HttpRequestBody {
- private val entity: HttpEntity by lazy {
- MultipartEntityBuilder.create()
- .apply {
- fields.forEach { (name, field) ->
- val knownValue = field.value.asKnown().getOrNull()
- val parts =
- if (knownValue is InputStream) {
- // Read directly from the `InputStream` instead of reading it all
- // into memory due to the `jsonMapper` serialization below.
- sequenceOf(name to knownValue)
- } else {
- val node = jsonMapper.valueToTree(field.value)
- serializePart(name, node)
+ MultipartBody.Builder()
+ .apply {
+ fields.forEach { (name, field) ->
+ val knownValue = field.value.asKnown().getOrNull()
+ val parts =
+ if (knownValue is InputStream) {
+ // Read directly from the `InputStream` instead of reading it all
+ // into memory due to the `jsonMapper` serialization below.
+ sequenceOf(name to knownValue)
+ } else {
+ val node = jsonMapper.valueToTree(field.value)
+ serializePart(name, node)
+ }
+
+ parts.forEach { (name, bytes) ->
+ val partBody =
+ if (bytes is ByteArrayInputStream) {
+ val byteArray = bytes.readBytes()
+
+ object : HttpRequestBody {
+
+ override fun writeTo(outputStream: OutputStream) {
+ outputStream.write(byteArray)
+ }
+
+ override fun contentType(): String = field.contentType
+
+ override fun contentLength(): Long = byteArray.size.toLong()
+
+ override fun repeatable(): Boolean = true
+
+ override fun close() {}
}
+ } else {
+ object : HttpRequestBody {
+
+ override fun writeTo(outputStream: OutputStream) {
+ bytes.copyTo(outputStream)
+ }
+
+ override fun contentType(): String = field.contentType
+
+ override fun contentLength(): Long = -1L
- parts.forEach { (name, bytes) ->
- addBinaryBody(
- name,
- bytes,
- ContentType.parseLenient(field.contentType),
- field.filename().getOrNull(),
- )
+ override fun repeatable(): Boolean = false
+
+ override fun close() = bytes.close()
+ }
}
- }
+
+ addPart(
+ MultipartBody.Part.create(
+ name,
+ field.filename().getOrNull(),
+ field.contentType,
+ partBody,
+ )
+ )
}
- .build()
+ }
}
+ .build()
- private fun serializePart(
- name: String,
- node: JsonNode,
- ): Sequence> =
- when (node.nodeType) {
- JsonNodeType.MISSING,
- JsonNodeType.NULL -> emptySequence()
- JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
- JsonNodeType.STRING -> sequenceOf(name to node.textValue().inputStream())
- JsonNodeType.BOOLEAN ->
- sequenceOf(name to node.booleanValue().toString().inputStream())
- JsonNodeType.NUMBER ->
- sequenceOf(name to node.numberValue().toString().inputStream())
- JsonNodeType.ARRAY ->
- sequenceOf(
- name to
- node
- .elements()
- .asSequence()
- .mapNotNull { element ->
- when (element.nodeType) {
- JsonNodeType.MISSING,
- JsonNodeType.NULL -> null
- JsonNodeType.STRING -> node.textValue()
- JsonNodeType.BOOLEAN -> node.booleanValue().toString()
- JsonNodeType.NUMBER -> node.numberValue().toString()
- null,
- JsonNodeType.BINARY,
- JsonNodeType.ARRAY,
- JsonNodeType.OBJECT,
- JsonNodeType.POJO ->
- throw BlooioInvalidDataException(
- "Unexpected JsonNode type in array: ${node.nodeType}"
- )
- }
- }
- .joinToString(",")
- .inputStream()
- )
- JsonNodeType.OBJECT ->
- node.fields().asSequence().flatMap { (key, value) ->
- serializePart("$name[$key]", value)
- }
- JsonNodeType.POJO,
- null ->
- throw BlooioInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
+private fun serializePart(name: String, node: JsonNode): Sequence> =
+ when (node.nodeType) {
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL -> emptySequence()
+ JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
+ JsonNodeType.STRING -> sequenceOf(name to node.textValue().byteInputStream())
+ JsonNodeType.BOOLEAN -> sequenceOf(name to node.booleanValue().toString().byteInputStream())
+ JsonNodeType.NUMBER -> sequenceOf(name to node.numberValue().toString().byteInputStream())
+ JsonNodeType.ARRAY ->
+ sequenceOf(
+ name to
+ node
+ .elements()
+ .asSequence()
+ .mapNotNull { element ->
+ when (element.nodeType) {
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL -> null
+ JsonNodeType.STRING -> element.textValue()
+ JsonNodeType.BOOLEAN -> element.booleanValue().toString()
+ JsonNodeType.NUMBER -> element.numberValue().toString()
+ null,
+ JsonNodeType.BINARY,
+ JsonNodeType.ARRAY,
+ JsonNodeType.OBJECT,
+ JsonNodeType.POJO ->
+ throw BlooioInvalidDataException(
+ "Unexpected JsonNode type in array: ${element.nodeType}"
+ )
+ }
+ }
+ .joinToString(",")
+ .byteInputStream()
+ )
+ JsonNodeType.OBJECT ->
+ node.fields().asSequence().flatMap { (key, value) ->
+ serializePart("$name[$key]", value)
+ }
+ JsonNodeType.POJO,
+ null -> throw BlooioInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
+ }
+
+private class MultipartBody
+private constructor(private val boundary: String, private val parts: List) : HttpRequestBody {
+ private val boundaryBytes: ByteArray = boundary.toByteArray()
+ private val contentType = "multipart/form-data; boundary=$boundary"
+
+ // This must remain in sync with `contentLength`.
+ override fun writeTo(outputStream: OutputStream) {
+ parts.forEach { part ->
+ outputStream.write(DASHDASH)
+ outputStream.write(boundaryBytes)
+ outputStream.write(CRLF)
+
+ outputStream.write(CONTENT_DISPOSITION)
+ outputStream.write(part.contentDisposition.toByteArray())
+ outputStream.write(CRLF)
+
+ outputStream.write(CONTENT_TYPE)
+ outputStream.write(part.contentType.toByteArray())
+ outputStream.write(CRLF)
+
+ outputStream.write(CRLF)
+ part.body.writeTo(outputStream)
+ outputStream.write(CRLF)
+ }
+
+ outputStream.write(DASHDASH)
+ outputStream.write(boundaryBytes)
+ outputStream.write(DASHDASH)
+ outputStream.write(CRLF)
+ }
+
+ override fun contentType(): String = contentType
+
+ // This must remain in sync with `writeTo`.
+ override fun contentLength(): Long {
+ var byteCount = 0L
+
+ parts.forEach { part ->
+ val contentLength = part.body.contentLength()
+ if (contentLength == -1L) {
+ return -1L
}
- private fun String.inputStream(): InputStream = toByteArray().inputStream()
+ byteCount +=
+ DASHDASH.size +
+ boundaryBytes.size +
+ CRLF.size +
+ CONTENT_DISPOSITION.size +
+ part.contentDisposition.toByteArray().size +
+ CRLF.size +
+ CONTENT_TYPE.size +
+ part.contentType.toByteArray().size +
+ CRLF.size +
+ CRLF.size +
+ contentLength +
+ CRLF.size
+ }
- override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
+ byteCount += DASHDASH.size + boundaryBytes.size + DASHDASH.size + CRLF.size
+ return byteCount
+ }
- override fun contentType(): String = entity.contentType
+ override fun repeatable(): Boolean = parts.all { it.body.repeatable() }
- override fun contentLength(): Long = entity.contentLength
+ override fun close() {
+ parts.forEach { it.body.close() }
+ }
- override fun repeatable(): Boolean = entity.isRepeatable
+ class Builder {
+ private val boundary = UUID.randomUUID().toString()
+ private val parts: MutableList = mutableListOf()
- override fun close() = entity.close()
+ fun addPart(part: Part) = apply { parts.add(part) }
+
+ fun build() = MultipartBody(boundary, parts.toImmutable())
+ }
+
+ class Part
+ private constructor(
+ val contentDisposition: String,
+ val contentType: String,
+ val body: HttpRequestBody,
+ ) {
+ companion object {
+ fun create(
+ name: String,
+ filename: String?,
+ contentType: String,
+ body: HttpRequestBody,
+ ): Part {
+ val disposition = buildString {
+ append("form-data; name=")
+ appendQuotedString(name)
+ if (filename != null) {
+ append("; filename=")
+ appendQuotedString(filename)
+ }
+ }
+ return Part(disposition, contentType, body)
+ }
+ }
+ }
+
+ companion object {
+ private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte())
+ private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte())
+ private val CONTENT_DISPOSITION = "Content-Disposition: ".toByteArray()
+ private val CONTENT_TYPE = "Content-Type: ".toByteArray()
+
+ private fun StringBuilder.appendQuotedString(key: String) {
+ append('"')
+ for (ch in key) {
+ when (ch) {
+ '\n' -> append("%0A")
+ '\r' -> append("%0D")
+ '"' -> append("%22")
+ else -> append(ch)
+ }
+ }
+ append('"')
+ }
}
+}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/LoggingHttpClient.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/LoggingHttpClient.kt
new file mode 100644
index 0000000..727d765
--- /dev/null
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/LoggingHttpClient.kt
@@ -0,0 +1,628 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.blooio.api.core.http
+
+import com.blooio.api.core.LogLevel
+import com.blooio.api.core.RequestOptions
+import com.blooio.api.core.checkRequired
+import com.blooio.api.core.toImmutable
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import java.nio.charset.CharacterCodingException
+import java.nio.charset.Charset
+import java.nio.charset.CharsetDecoder
+import java.nio.charset.CodingErrorAction
+import java.nio.charset.StandardCharsets
+import java.time.Clock
+import java.time.Duration
+import java.time.OffsetDateTime
+import java.util.SortedSet
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionException
+import kotlin.time.toKotlinDuration
+
+/** A wrapper [HttpClient] around [httpClient] that logs request and response information. */
+class LoggingHttpClient
+private constructor(
+ /** The underlying [HttpClient] for making requests. */
+ @get:JvmName("httpClient") val httpClient: HttpClient,
+ /**
+ * Sensitive headers to redact from logs.
+ *
+ * Defaults to `Set.of("authorization", "api-key", "x-api-key", "cookie", "set-cookie")`.
+ */
+ @get:JvmName("redactedHeaders") val redactedHeaders: SortedSet,
+ /**
+ * The clock to use for measuring request and response durations.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ @get:JvmName("clock") val clock: Clock,
+ /**
+ * The log level to use.
+ *
+ * Pass [LogLevel.fromEnv] to read from environment variables.
+ */
+ @get:JvmName("level") val level: LogLevel,
+) : HttpClient {
+
+ override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
+ val loggingRequest = logRequest(request)
+
+ val before = OffsetDateTime.now(clock)
+ val response =
+ try {
+ httpClient.execute(loggingRequest, requestOptions)
+ } catch (e: Throwable) {
+ logFailure(e, Duration.between(before, OffsetDateTime.now(clock)))
+ throw e
+ }
+
+ val took = Duration.between(before, OffsetDateTime.now(clock))
+ return logResponse(response, took)
+ }
+
+ override fun executeAsync(
+ request: HttpRequest,
+ requestOptions: RequestOptions,
+ ): CompletableFuture {
+ val loggingRequest = logRequest(request)
+
+ val before = OffsetDateTime.now(clock)
+ val future =
+ try {
+ httpClient.executeAsync(loggingRequest, requestOptions)
+ } catch (e: Throwable) {
+ logFailure(e, Duration.between(before, OffsetDateTime.now(clock)))
+ throw e
+ }
+ return future.handle { response, error ->
+ val took = Duration.between(before, OffsetDateTime.now(clock))
+ if (error != null) {
+ logFailure(unwrapCompletionException(error), took)
+ throw error
+ }
+ logResponse(response, took)
+ }
+ }
+
+ private fun logRequest(request: HttpRequest): HttpRequest {
+ if (!level.shouldLog(LogLevel.INFO)) {
+ return request
+ }
+
+ System.err.println(
+ buildString {
+ append("--> ${request.method} ${request.url()}")
+ request.body?.let {
+ val length = it.contentLength()
+ append(if (length >= 0) " ($length-byte body)" else " (unknown-length body)")
+ }
+ }
+ )
+
+ if (!level.shouldLog(LogLevel.DEBUG)) {
+ return request
+ }
+
+ logHeaders(request.headers)
+
+ if (request.body == null) {
+ System.err.println("--> END ${request.method}")
+ System.err.println()
+ return request
+ }
+
+ return request
+ .toBuilder()
+ .body(LoggingHttpRequestBody(request.method, request.body))
+ .build()
+ }
+
+ private fun logResponse(response: HttpResponse, took: Duration): HttpResponse {
+ if (!level.shouldLog(LogLevel.INFO)) {
+ return response
+ }
+
+ val contentLength = response.headers().values("Content-Length").firstOrNull()?.toIntOrNull()
+ System.err.println(
+ "<-- ${response.statusCode()} (${
+ buildString {
+ append(took.format())
+ contentLength?.let { append(", $contentLength-byte body") }
+ }
+ })"
+ )
+
+ if (!level.shouldLog(LogLevel.DEBUG)) {
+ return response
+ }
+
+ logHeaders(response.headers())
+ return LoggingHttpResponse(response)
+ }
+
+ private fun logFailure(error: Throwable, took: Duration) {
+ if (!level.shouldLog(LogLevel.ERROR)) {
+ return
+ }
+
+ System.err.println(
+ buildString {
+ append("<-- !! ${error.javaClass.simpleName}")
+ error.message?.let { append(": $it") }
+ append(" (${took.format()})")
+ }
+ )
+ }
+
+ private fun unwrapCompletionException(error: Throwable): Throwable =
+ if (error is CompletionException && error.cause != null) error.cause!! else error
+
+ private fun logHeaders(headers: Headers) =
+ headers.names().forEach { name ->
+ headers.values(name).forEach { value ->
+ System.err.println("$name: ${if (redactedHeaders.contains(name)) "██" else value}")
+ }
+ }
+
+ override fun close() = httpClient.close()
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ /**
+ * Returns a mutable builder for constructing an instance of [LoggingHttpClient].
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * .level()
+ * ```
+ */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [LoggingHttpClient]. */
+ class Builder internal constructor() {
+
+ private var httpClient: HttpClient? = null
+ private var redactedHeaders: Set =
+ setOf("authorization", "api-key", "x-api-key", "cookie", "set-cookie")
+ private var clock: Clock = Clock.systemUTC()
+ private var level: LogLevel? = null
+
+ @JvmSynthetic
+ internal fun from(loggingHttpClient: LoggingHttpClient) = apply {
+ httpClient = loggingHttpClient.httpClient
+ redactedHeaders = loggingHttpClient.redactedHeaders
+ clock = loggingHttpClient.clock
+ level = loggingHttpClient.level
+ }
+
+ /** The underlying [HttpClient] for making requests. */
+ fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
+
+ /**
+ * Sensitive headers to redact from logs.
+ *
+ * Defaults to `Set.of("authorization", "api-key", "x-api-key", "cookie", "set-cookie")`.
+ */
+ fun redactedHeaders(redactedHeaders: Set) = apply {
+ this.redactedHeaders = redactedHeaders
+ }
+
+ /**
+ * The clock to use for measuring request and response durations.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ fun clock(clock: Clock) = apply { this.clock = clock }
+
+ /**
+ * The log level to use.
+ *
+ * Pass [LogLevel.fromEnv] to read from environment variables.
+ */
+ fun level(level: LogLevel) = apply { this.level = level }
+
+ /**
+ * Returns an immutable instance of [LoggingHttpClient].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * .level()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): LoggingHttpClient =
+ LoggingHttpClient(
+ checkRequired("httpClient", httpClient),
+ redactedHeaders.toSortedSet(String.CASE_INSENSITIVE_ORDER).toImmutable(),
+ clock,
+ checkRequired("level", level),
+ )
+ }
+}
+
+/**
+ * An [HttpRequestBody] wrapper that delegates to [body] while also logging line by line as it's
+ * written.
+ *
+ * The logging occurs in a streaming manner with minimal buffering.
+ */
+private class LoggingHttpRequestBody(
+ private val method: HttpMethod,
+ private val body: HttpRequestBody,
+) : HttpRequestBody {
+
+ private val charset by lazy { parseCharset(body.contentType()) }
+
+ override fun writeTo(outputStream: OutputStream) {
+ val loggingOutputStream = LoggingOutputStream(outputStream, charset)
+ body.writeTo(loggingOutputStream)
+
+ loggingOutputStream.flush()
+ System.err.println("--> END $method (${loggingOutputStream.writeCount()}-byte body)")
+ System.err.println()
+ }
+
+ override fun contentType(): String? = body.contentType()
+
+ override fun contentLength(): Long = body.contentLength()
+
+ override fun repeatable(): Boolean = body.repeatable()
+
+ override fun close() = body.close()
+}
+
+/**
+ * An [OutputStream] wrapper that delegates to [outputStream] while also logging bytes line by line
+ * as it's written to.
+ *
+ * The written content is assumed to be in the given [charset] and the logging occurs in a streaming
+ * manner with minimal buffering.
+ */
+private class LoggingOutputStream(private val outputStream: OutputStream, charset: Charset?) :
+ OutputStream() {
+
+ private val buffer = LoggingBuffer(charset)
+
+ fun writeCount() = buffer.writeCount()
+
+ override fun write(b: Int) {
+ outputStream.write(b)
+ buffer.write(b)
+ }
+
+ override fun write(b: ByteArray, off: Int, len: Int) {
+ outputStream.write(b, off, len)
+ for (i in off until off + len) {
+ buffer.write(b[i].toInt() and 0xFF)
+ }
+ }
+
+ /** Prints any currently buffered content. */
+ override fun flush() {
+ buffer.flush()
+ outputStream.flush()
+ }
+
+ override fun close() = outputStream.close()
+}
+
+/**
+ * An [HttpResponse] wrapper that delegates to [response] while also logging line-by-line as it's
+ * read.
+ *
+ * The logging occurs in a streaming manner with minimal buffering.
+ */
+private class LoggingHttpResponse(private val response: HttpResponse) : HttpResponse {
+
+ private val loggingBody: Lazy = lazy {
+ LoggingInputStream(
+ response.body(),
+ parseCharset(response.headers().values("Content-Type").firstOrNull()),
+ )
+ }
+
+ override fun statusCode(): Int = response.statusCode()
+
+ override fun headers(): Headers = response.headers()
+
+ override fun body(): InputStream = loggingBody.value
+
+ override fun close() {
+ if (loggingBody.isInitialized()) {
+ loggingBody.value.close()
+ }
+ response.close()
+ }
+}
+
+/**
+ * An [InputStream] wrapper that delegates to [inputStream] while also logging bytes line by line as
+ * it's read.
+ *
+ * The contents of [inputStream] are assumed to be in the given [charset] and the logging occurs in
+ * a streaming manner with minimal buffering.
+ */
+private class LoggingInputStream(private val inputStream: InputStream, charset: Charset?) :
+ InputStream() {
+
+ private var isDone = false
+ private val buffer = LoggingBuffer(charset)
+
+ override fun read(): Int {
+ if (isDone) {
+ return -1
+ }
+
+ val b = inputStream.read()
+
+ if (b == -1) {
+ markDone()
+ return b
+ }
+
+ buffer.write(b)
+ return b
+ }
+
+ override fun read(b: ByteArray, off: Int, len: Int): Int {
+ if (isDone) {
+ return -1
+ }
+
+ val bytesRead = inputStream.read(b, off, len)
+
+ if (bytesRead == -1) {
+ markDone()
+ return bytesRead
+ }
+
+ for (i in off until off + bytesRead) {
+ buffer.write(b[i].toInt() and 0xFF)
+ }
+ return bytesRead
+ }
+
+ override fun close() {
+ if (!isDone) {
+ markDone(closedEarly = true)
+ }
+ inputStream.close()
+ }
+
+ private fun markDone(closedEarly: Boolean = false) {
+ isDone = true
+ buffer.flush()
+ val suffix = if (closedEarly) ", closed early" else ""
+ System.err.println("<-- END HTTP (${buffer.writeCount()}-byte body$suffix)")
+ System.err.println()
+ }
+}
+
+/**
+ * A byte buffer that prints line by line, using the given [charset], as bytes are written to it.
+ *
+ * When [charset] is `null`, the buffer performs an upfront check to detect binary content. If
+ * non-whitespace ISO control characters are found in the first [PROBABLY_UTF8_CODE_POINT_LIMIT]
+ * code points, body logging is suppressed entirely.
+ */
+private class LoggingBuffer(charset: Charset?) {
+
+ private val charset = charset ?: StandardCharsets.UTF_8
+
+ private val decoder: CharsetDecoder =
+ this.charset
+ .newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
+ private var writeCount = 0
+ private val buffer = ByteArrayOutputStream(128)
+
+ /**
+ * Whether logging has been suppressed because the content doesn't appear to be readable text.
+ *
+ * This is only set when [charset] is `null` and the content fails the [isProbablyUtf8] check.
+ */
+ private var suppressed = false
+
+ /**
+ * Bytes accumulated for the [isProbablyUtf8] check before any lines are printed.
+ *
+ * Once the check passes (or [charset] is non-null), this is set to `null` and bytes flow
+ * directly to [buffer].
+ */
+ private var prefetchBuffer: ByteArrayOutputStream? =
+ if (charset != null) null else ByteArrayOutputStream(128)
+
+ fun writeCount() = writeCount
+
+ fun write(b: Int) {
+ if (writeCount == 0) {
+ // Print a newline before we start printing anything to separate the printed content
+ // from previous content.
+ System.err.println()
+ }
+
+ writeCount++
+
+ if (suppressed) {
+ return
+ }
+
+ val prefetch = prefetchBuffer
+ if (prefetch != null) {
+ prefetch.write(b)
+ // Continue accumulating until we have enough bytes to decide.
+ if (prefetch.size() < PROBABLY_UTF8_BYTE_LIMIT && b != '\n'.code) {
+ return
+ }
+ // We have enough bytes. Check if the content is probably UTF-8.
+ prefetchBuffer = null
+ val bytes = prefetch.toByteArray()
+ if (!isProbablyUtf8(bytes)) {
+ suppressed = true
+ System.err.println("(binary body omitted)")
+ return
+ }
+ // Content looks like UTF-8. Feed the accumulated bytes into the normal buffer.
+ for (byte in bytes) {
+ writeToBuffer(byte.toInt() and 0xFF)
+ }
+ return
+ }
+
+ writeToBuffer(b)
+ }
+
+ private fun writeToBuffer(b: Int) {
+ if (b == '\n'.code) {
+ flush()
+ return
+ }
+
+ buffer.write(b)
+ }
+
+ /** Prints any currently buffered content. */
+ fun flush() {
+ if (suppressed) {
+ return
+ }
+
+ // If we still have a prefetch buffer when flush is called (body was shorter than the
+ // limit), run the check now.
+ val prefetch = prefetchBuffer
+ if (prefetch != null) {
+ prefetchBuffer = null
+ val bytes = prefetch.toByteArray()
+ if (bytes.isEmpty()) {
+ return
+ }
+ if (!isProbablyUtf8(bytes)) {
+ suppressed = true
+ System.err.println("(binary body omitted)")
+ return
+ }
+ for (byte in bytes) {
+ writeToBuffer(byte.toInt() and 0xFF)
+ }
+ }
+
+ if (buffer.size() == 0) {
+ return
+ }
+
+ val line =
+ try {
+ decoder.decode(ByteBuffer.wrap(buffer.toByteArray()))
+ } catch (e: CharacterCodingException) {
+ "(omitted line is not valid $charset)"
+ }
+ buffer.reset()
+ System.err.println(line)
+ }
+}
+
+/** The maximum number of code points to sample when checking if content is probably UTF-8. */
+private const val PROBABLY_UTF8_CODE_POINT_LIMIT = 64
+
+/**
+ * The maximum number of bytes to accumulate before running the [isProbablyUtf8] check. UTF-8 code
+ * points are at most 4 bytes, so this accommodates [PROBABLY_UTF8_CODE_POINT_LIMIT] code points.
+ */
+private const val PROBABLY_UTF8_BYTE_LIMIT = PROBABLY_UTF8_CODE_POINT_LIMIT * 4
+
+/**
+ * Returns `true` if the given [bytes] probably contain human-readable UTF-8 text.
+ *
+ * Decodes up to [PROBABLY_UTF8_CODE_POINT_LIMIT] code points and returns `false` if any
+ * non-whitespace ISO control characters are found, or if the bytes are not valid UTF-8.
+ */
+private fun isProbablyUtf8(bytes: ByteArray): Boolean {
+ try {
+ val decoder =
+ StandardCharsets.UTF_8.newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
+ val charBuffer = decoder.decode(ByteBuffer.wrap(bytes))
+ var codePointCount = 0
+ var i = 0
+ while (i < charBuffer.length && codePointCount < PROBABLY_UTF8_CODE_POINT_LIMIT) {
+ val codePoint = Character.codePointAt(charBuffer, i)
+ if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
+ return false
+ }
+ i += Character.charCount(codePoint)
+ codePointCount++
+ }
+ return true
+ } catch (e: CharacterCodingException) {
+ return false
+ }
+}
+
+/** Returns the [Charset] in the given [contentType] string, or `null` if unspecified. */
+private fun parseCharset(contentType: String?): Charset? =
+ contentType
+ ?.split(";")
+ ?.drop(1)
+ ?.map { it.trim() }
+ ?.firstOrNull { it.startsWith("charset=", ignoreCase = true) }
+ ?.substringAfter("=")
+ ?.trim()
+ ?.removeSurrounding("\"")
+ ?.let { runCatching { charset(it) }.getOrNull() }
+
+/** Formats the [Duration] into a string like "1m 40s 467ms". */
+private fun Duration.format(): String =
+ toKotlinDuration().toComponents { days, hours, minutes, seconds, nanoseconds ->
+ buildString {
+ val milliseconds = nanoseconds / 1_000_000
+ if (days > 0) {
+ append("${days}d")
+ }
+ if (hours > 0) {
+ if (isNotEmpty()) {
+ append(" ")
+ }
+ append("${hours}h")
+ }
+ if (minutes > 0) {
+ if (isNotEmpty()) {
+ append(" ")
+ }
+ append("${minutes}m")
+ }
+ if (seconds > 0) {
+ if (isNotEmpty()) {
+ append(" ")
+ }
+ append("${seconds}s")
+ }
+ if (milliseconds > 0) {
+ if (isNotEmpty()) {
+ append(" ")
+ }
+ append("${milliseconds}ms")
+ }
+
+ if (isEmpty()) {
+ append("0s")
+ }
+ }
+ }
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/ProxyAuthenticator.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/ProxyAuthenticator.kt
new file mode 100644
index 0000000..8fa881a
--- /dev/null
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/ProxyAuthenticator.kt
@@ -0,0 +1,59 @@
+package com.blooio.api.core.http
+
+import java.net.Proxy
+import java.nio.charset.Charset
+import java.nio.charset.StandardCharsets
+import java.util.Base64
+import java.util.Optional
+
+/**
+ * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication Required`.
+ *
+ * Implementations inspect the 407 [response] (typically its `Proxy-Authenticate` header) and return
+ * the request to retry with a `Proxy-Authorization` header set, or [Optional.empty] to abandon
+ * authentication and surface the 407 to the caller.
+ *
+ * Implementations must be thread-safe; they may be invoked concurrently from multiple HTTP calls.
+ */
+fun interface ProxyAuthenticator {
+
+ /**
+ * @param proxy the proxy that produced the challenge, or [Proxy.NO_PROXY] if the route is not
+ * yet established
+ * @param request the request that produced [response]
+ * @param response the 407 challenge response
+ * @return the retry request to send (typically [request] with a `Proxy-Authorization` header
+ * added), or [Optional.empty] to abandon authentication
+ */
+ fun authenticate(
+ proxy: Proxy,
+ request: HttpRequest,
+ response: HttpResponse,
+ ): Optional
+
+ companion object {
+
+ /**
+ * A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the ISO-8859-1
+ * charset.
+ */
+ @JvmStatic
+ fun basic(username: String, password: String): ProxyAuthenticator =
+ basic(username, password, StandardCharsets.ISO_8859_1)
+
+ /**
+ * A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the given [charset].
+ */
+ @JvmStatic
+ fun basic(username: String, password: String, charset: Charset): ProxyAuthenticator {
+ val token =
+ Base64.getEncoder().encodeToString("$username:$password".toByteArray(charset))
+ val headerValue = "Basic $token"
+ return ProxyAuthenticator { _, request, _ ->
+ Optional.of(
+ request.toBuilder().putHeader("Proxy-Authorization", headerValue).build()
+ )
+ }
+ }
+ }
+}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/RetryingHttpClient.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/RetryingHttpClient.kt
index 6f34419..97644f6 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/RetryingHttpClient.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/core/http/RetryingHttpClient.kt
@@ -1,3 +1,5 @@
+// File generated from our OpenAPI spec by Stainless.
+
package com.blooio.api.core.http
import com.blooio.api.core.DefaultSleeper
@@ -31,10 +33,6 @@ private constructor(
) : HttpClient {
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
- if (!isRetryable(request) || maxRetries <= 0) {
- return httpClient.execute(request, requestOptions)
- }
-
var modifiedRequest = maybeAddIdempotencyHeader(request)
// Don't send the current retry count in the headers if the caller set their own value.
@@ -48,6 +46,10 @@ private constructor(
modifiedRequest = setRetryCountHeader(modifiedRequest, retries)
}
+ if (!isRetryable(modifiedRequest)) {
+ return httpClient.execute(modifiedRequest, requestOptions)
+ }
+
val response =
try {
val response = httpClient.execute(modifiedRequest, requestOptions)
@@ -75,10 +77,6 @@ private constructor(
request: HttpRequest,
requestOptions: RequestOptions,
): CompletableFuture {
- if (!isRetryable(request) || maxRetries <= 0) {
- return httpClient.executeAsync(request, requestOptions)
- }
-
val modifiedRequest = maybeAddIdempotencyHeader(request)
// Don't send the current retry count in the headers if the caller set their own value.
@@ -94,8 +92,12 @@ private constructor(
val requestWithRetryCount =
if (shouldSendRetryCount) setRetryCountHeader(request, retries) else request
- return httpClient
- .executeAsync(requestWithRetryCount, requestOptions)
+ val responseFuture = httpClient.executeAsync(requestWithRetryCount, requestOptions)
+ if (!isRetryable(requestWithRetryCount)) {
+ return responseFuture
+ }
+
+ return responseFuture
.handleAsync(
fun(
response: HttpResponse?,
@@ -199,7 +201,7 @@ private constructor(
?: headers.values("Retry-After").getOrNull(0)?.let { retryAfter ->
retryAfter.toFloatOrNull()?.times(TimeUnit.SECONDS.toNanos(1))
?: try {
- ChronoUnit.MILLIS.between(
+ ChronoUnit.NANOS.between(
OffsetDateTime.now(clock),
OffsetDateTime.parse(
retryAfter,
@@ -212,13 +214,8 @@ private constructor(
}
}
?.let { retryAfterNanos ->
- // If the API asks us to wait a certain amount of time (and it's a reasonable
- // amount), just
- // do what it says.
- val retryAfter = Duration.ofNanos(retryAfterNanos.toLong())
- if (retryAfter in Duration.ofNanos(0)..Duration.ofMinutes(1)) {
- return retryAfter
- }
+ // If the API asks us to wait a certain amount of time, do what it says.
+ return Duration.ofNanos(retryAfterNanos.toLong())
}
// Apply exponential backoff, but not more than the max.
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/BadRequestException.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/BadRequestException.kt
index 4474405..abc5aa2 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/BadRequestException.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/BadRequestException.kt
@@ -5,12 +5,16 @@ package com.blooio.api.errors
import com.blooio.api.core.JsonValue
import com.blooio.api.core.checkRequired
import com.blooio.api.core.http.Headers
+import com.blooio.api.core.jsonMapper
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
class BadRequestException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
- BlooioServiceException("400: $body", cause) {
+ BlooioServiceException(
+ "400: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 400
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/InternalServerException.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/InternalServerException.kt
index 8597337..dea69e8 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/InternalServerException.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/InternalServerException.kt
@@ -5,6 +5,7 @@ package com.blooio.api.errors
import com.blooio.api.core.JsonValue
import com.blooio.api.core.checkRequired
import com.blooio.api.core.http.Headers
+import com.blooio.api.core.jsonMapper
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@@ -14,7 +15,11 @@ private constructor(
private val headers: Headers,
private val body: JsonValue,
cause: Throwable?,
-) : BlooioServiceException("$statusCode: $body", cause) {
+) :
+ BlooioServiceException(
+ "$statusCode: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = statusCode
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/NotFoundException.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/NotFoundException.kt
index 6fd595e..a3f41c3 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/NotFoundException.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/NotFoundException.kt
@@ -5,12 +5,16 @@ package com.blooio.api.errors
import com.blooio.api.core.JsonValue
import com.blooio.api.core.checkRequired
import com.blooio.api.core.http.Headers
+import com.blooio.api.core.jsonMapper
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
class NotFoundException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
- BlooioServiceException("404: $body", cause) {
+ BlooioServiceException(
+ "404: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 404
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/PermissionDeniedException.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/PermissionDeniedException.kt
index 664a509..3c343bc 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/PermissionDeniedException.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/PermissionDeniedException.kt
@@ -5,12 +5,16 @@ package com.blooio.api.errors
import com.blooio.api.core.JsonValue
import com.blooio.api.core.checkRequired
import com.blooio.api.core.http.Headers
+import com.blooio.api.core.jsonMapper
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
class PermissionDeniedException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
- BlooioServiceException("403: $body", cause) {
+ BlooioServiceException(
+ "403: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 403
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/RateLimitException.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/RateLimitException.kt
index 6f2e923..9227447 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/RateLimitException.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/RateLimitException.kt
@@ -5,12 +5,16 @@ package com.blooio.api.errors
import com.blooio.api.core.JsonValue
import com.blooio.api.core.checkRequired
import com.blooio.api.core.http.Headers
+import com.blooio.api.core.jsonMapper
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
class RateLimitException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
- BlooioServiceException("429: $body", cause) {
+ BlooioServiceException(
+ "429: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 429
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnauthorizedException.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnauthorizedException.kt
index ffa0075..bd54fc0 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnauthorizedException.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnauthorizedException.kt
@@ -5,12 +5,16 @@ package com.blooio.api.errors
import com.blooio.api.core.JsonValue
import com.blooio.api.core.checkRequired
import com.blooio.api.core.http.Headers
+import com.blooio.api.core.jsonMapper
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
class UnauthorizedException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
- BlooioServiceException("401: $body", cause) {
+ BlooioServiceException(
+ "401: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 401
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnexpectedStatusCodeException.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnexpectedStatusCodeException.kt
index 03bae54..ff324db 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnexpectedStatusCodeException.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnexpectedStatusCodeException.kt
@@ -5,6 +5,7 @@ package com.blooio.api.errors
import com.blooio.api.core.JsonValue
import com.blooio.api.core.checkRequired
import com.blooio.api.core.http.Headers
+import com.blooio.api.core.jsonMapper
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@@ -14,7 +15,11 @@ private constructor(
private val headers: Headers,
private val body: JsonValue,
cause: Throwable?,
-) : BlooioServiceException("$statusCode: $body", cause) {
+) :
+ BlooioServiceException(
+ "$statusCode: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = statusCode
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnprocessableEntityException.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnprocessableEntityException.kt
index ba5ee08..bdd02cc 100644
--- a/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnprocessableEntityException.kt
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/errors/UnprocessableEntityException.kt
@@ -5,12 +5,16 @@ package com.blooio.api.errors
import com.blooio.api.core.JsonValue
import com.blooio.api.core.checkRequired
import com.blooio.api.core.http.Headers
+import com.blooio.api.core.jsonMapper
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
class UnprocessableEntityException
private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) :
- BlooioServiceException("422: $body", cause) {
+ BlooioServiceException(
+ "422: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 422
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/models/chats/ChatListParams.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/models/chats/ChatListParams.kt
new file mode 100644
index 0000000..cba7bbc
--- /dev/null
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/models/chats/ChatListParams.kt
@@ -0,0 +1,406 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.blooio.api.models.chats
+
+import com.blooio.api.core.Enum
+import com.blooio.api.core.JsonField
+import com.blooio.api.core.Params
+import com.blooio.api.core.http.Headers
+import com.blooio.api.core.http.QueryParams
+import com.blooio.api.errors.BlooioInvalidDataException
+import com.fasterxml.jackson.annotation.JsonCreator
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** List all unique conversations for the organization, sorted by most recent message. */
+class ChatListParams
+private constructor(
+ private val limit: Long?,
+ private val offset: Long?,
+ private val q: String?,
+ private val sort: Sort?,
+ private val additionalHeaders: Headers,
+ private val additionalQueryParams: QueryParams,
+) : Params {
+
+ /**
+ * Maximum number of items to return in a single response. Must be between 1 and 200; defaults
+ * to 50. Use together with `offset` to page through large result sets.
+ */
+ fun limit(): Optional = Optional.ofNullable(limit)
+
+ /**
+ * Number of items to skip before returning results. Combine with `limit` for page-based
+ * pagination (e.g. `offset=50&limit=50` returns the second page). Defaults to 0.
+ */
+ fun offset(): Optional = Optional.ofNullable(offset)
+
+ /** Search query (matches phone/email or contact name) */
+ fun q(): Optional = Optional.ofNullable(q)
+
+ /** Sort order */
+ fun sort(): Optional = Optional.ofNullable(sort)
+
+ /** Additional headers to send with the request. */
+ fun _additionalHeaders(): Headers = additionalHeaders
+
+ /** Additional query param to send with the request. */
+ fun _additionalQueryParams(): QueryParams = additionalQueryParams
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ @JvmStatic fun none(): ChatListParams = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [ChatListParams]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [ChatListParams]. */
+ class Builder internal constructor() {
+
+ private var limit: Long? = null
+ private var offset: Long? = null
+ private var q: String? = null
+ private var sort: Sort? = null
+ private var additionalHeaders: Headers.Builder = Headers.builder()
+ private var additionalQueryParams: QueryParams.Builder = QueryParams.builder()
+
+ @JvmSynthetic
+ internal fun from(chatListParams: ChatListParams) = apply {
+ limit = chatListParams.limit
+ offset = chatListParams.offset
+ q = chatListParams.q
+ sort = chatListParams.sort
+ additionalHeaders = chatListParams.additionalHeaders.toBuilder()
+ additionalQueryParams = chatListParams.additionalQueryParams.toBuilder()
+ }
+
+ /**
+ * Maximum number of items to return in a single response. Must be between 1 and 200;
+ * defaults to 50. Use together with `offset` to page through large result sets.
+ */
+ fun limit(limit: Long?) = apply { this.limit = limit }
+
+ /**
+ * Alias for [Builder.limit].
+ *
+ * This unboxed primitive overload exists for backwards compatibility.
+ */
+ fun limit(limit: Long) = limit(limit as Long?)
+
+ /** Alias for calling [Builder.limit] with `limit.orElse(null)`. */
+ fun limit(limit: Optional) = limit(limit.getOrNull())
+
+ /**
+ * Number of items to skip before returning results. Combine with `limit` for page-based
+ * pagination (e.g. `offset=50&limit=50` returns the second page). Defaults to 0.
+ */
+ fun offset(offset: Long?) = apply { this.offset = offset }
+
+ /**
+ * Alias for [Builder.offset].
+ *
+ * This unboxed primitive overload exists for backwards compatibility.
+ */
+ fun offset(offset: Long) = offset(offset as Long?)
+
+ /** Alias for calling [Builder.offset] with `offset.orElse(null)`. */
+ fun offset(offset: Optional) = offset(offset.getOrNull())
+
+ /** Search query (matches phone/email or contact name) */
+ fun q(q: String?) = apply { this.q = q }
+
+ /** Alias for calling [Builder.q] with `q.orElse(null)`. */
+ fun q(q: Optional) = q(q.getOrNull())
+
+ /** Sort order */
+ fun sort(sort: Sort?) = apply { this.sort = sort }
+
+ /** Alias for calling [Builder.sort] with `sort.orElse(null)`. */
+ fun sort(sort: Optional) = sort(sort.getOrNull())
+
+ fun additionalHeaders(additionalHeaders: Headers) = apply {
+ this.additionalHeaders.clear()
+ putAllAdditionalHeaders(additionalHeaders)
+ }
+
+ fun additionalHeaders(additionalHeaders: Map>) = apply {
+ this.additionalHeaders.clear()
+ putAllAdditionalHeaders(additionalHeaders)
+ }
+
+ fun putAdditionalHeader(name: String, value: String) = apply {
+ additionalHeaders.put(name, value)
+ }
+
+ fun putAdditionalHeaders(name: String, values: Iterable) = apply {
+ additionalHeaders.put(name, values)
+ }
+
+ fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply {
+ this.additionalHeaders.putAll(additionalHeaders)
+ }
+
+ fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply {
+ this.additionalHeaders.putAll(additionalHeaders)
+ }
+
+ fun replaceAdditionalHeaders(name: String, value: String) = apply {
+ additionalHeaders.replace(name, value)
+ }
+
+ fun replaceAdditionalHeaders(name: String, values: Iterable) = apply {
+ additionalHeaders.replace(name, values)
+ }
+
+ fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply {
+ this.additionalHeaders.replaceAll(additionalHeaders)
+ }
+
+ fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply {
+ this.additionalHeaders.replaceAll(additionalHeaders)
+ }
+
+ fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) }
+
+ fun removeAllAdditionalHeaders(names: Set) = apply {
+ additionalHeaders.removeAll(names)
+ }
+
+ fun additionalQueryParams(additionalQueryParams: QueryParams) = apply {
+ this.additionalQueryParams.clear()
+ putAllAdditionalQueryParams(additionalQueryParams)
+ }
+
+ fun additionalQueryParams(additionalQueryParams: Map>) = apply {
+ this.additionalQueryParams.clear()
+ putAllAdditionalQueryParams(additionalQueryParams)
+ }
+
+ fun putAdditionalQueryParam(key: String, value: String) = apply {
+ additionalQueryParams.put(key, value)
+ }
+
+ fun putAdditionalQueryParams(key: String, values: Iterable) = apply {
+ additionalQueryParams.put(key, values)
+ }
+
+ fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply {
+ this.additionalQueryParams.putAll(additionalQueryParams)
+ }
+
+ fun putAllAdditionalQueryParams(additionalQueryParams: Map>) =
+ apply {
+ this.additionalQueryParams.putAll(additionalQueryParams)
+ }
+
+ fun replaceAdditionalQueryParams(key: String, value: String) = apply {
+ additionalQueryParams.replace(key, value)
+ }
+
+ fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply {
+ additionalQueryParams.replace(key, values)
+ }
+
+ fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply {
+ this.additionalQueryParams.replaceAll(additionalQueryParams)
+ }
+
+ fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) =
+ apply {
+ this.additionalQueryParams.replaceAll(additionalQueryParams)
+ }
+
+ fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) }
+
+ fun removeAllAdditionalQueryParams(keys: Set) = apply {
+ additionalQueryParams.removeAll(keys)
+ }
+
+ /**
+ * Returns an immutable instance of [ChatListParams].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): ChatListParams =
+ ChatListParams(
+ limit,
+ offset,
+ q,
+ sort,
+ additionalHeaders.build(),
+ additionalQueryParams.build(),
+ )
+ }
+
+ override fun _headers(): Headers = additionalHeaders
+
+ override fun _queryParams(): QueryParams =
+ QueryParams.builder()
+ .apply {
+ limit?.let { put("limit", it.toString()) }
+ offset?.let { put("offset", it.toString()) }
+ q?.let { put("q", it) }
+ sort?.let { put("sort", it.toString()) }
+ putAll(additionalQueryParams)
+ }
+ .build()
+
+ /** Sort order */
+ class Sort @JsonCreator private constructor(private val value: JsonField) : Enum {
+
+ /**
+ * Returns this class instance's raw value.
+ *
+ * This is usually only useful if this instance was deserialized from data that doesn't
+ * match any known member, and you want to know that value. For example, if the SDK is on an
+ * older version than the API, then the API may respond with new members that the SDK is
+ * unaware of.
+ */
+ @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value
+
+ companion object {
+
+ @JvmField val RECENT = of("recent")
+
+ @JvmField val OLDEST = of("oldest")
+
+ @JvmStatic fun of(value: String) = Sort(JsonField.of(value))
+ }
+
+ /** An enum containing [Sort]'s known values. */
+ enum class Known {
+ RECENT,
+ OLDEST,
+ }
+
+ /**
+ * An enum containing [Sort]'s known values, as well as an [_UNKNOWN] member.
+ *
+ * An instance of [Sort] can contain an unknown value in a couple of cases:
+ * - It was deserialized from data that doesn't match any known member. For example, if the
+ * SDK is on an older version than the API, then the API may respond with new members that
+ * the SDK is unaware of.
+ * - It was constructed with an arbitrary value using the [of] method.
+ */
+ enum class Value {
+ RECENT,
+ OLDEST,
+ /** An enum member indicating that [Sort] was instantiated with an unknown value. */
+ _UNKNOWN,
+ }
+
+ /**
+ * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN]
+ * if the class was instantiated with an unknown value.
+ *
+ * Use the [known] method instead if you're certain the value is always known or if you want
+ * to throw for the unknown case.
+ */
+ fun value(): Value =
+ when (this) {
+ RECENT -> Value.RECENT
+ OLDEST -> Value.OLDEST
+ else -> Value._UNKNOWN
+ }
+
+ /**
+ * Returns an enum member corresponding to this class instance's value.
+ *
+ * Use the [value] method instead if you're uncertain the value is always known and don't
+ * want to throw for the unknown case.
+ *
+ * @throws BlooioInvalidDataException if this class instance's value is a not a known
+ * member.
+ */
+ fun known(): Known =
+ when (this) {
+ RECENT -> Known.RECENT
+ OLDEST -> Known.OLDEST
+ else -> throw BlooioInvalidDataException("Unknown Sort: $value")
+ }
+
+ /**
+ * Returns this class instance's primitive wire representation.
+ *
+ * This differs from the [toString] method because that method is primarily for debugging
+ * and generally doesn't throw.
+ *
+ * @throws BlooioInvalidDataException if this class instance's value does not have the
+ * expected primitive type.
+ */
+ fun asString(): String =
+ _value().asString().orElseThrow { BlooioInvalidDataException("Value is not a String") }
+
+ private var validated: Boolean = false
+
+ /**
+ * Validates that the types of all values in this object match their expected types
+ * recursively.
+ *
+ * This method is _not_ forwards compatible with new types from the API for existing fields.
+ *
+ * @throws BlooioInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): Sort = apply {
+ if (validated) {
+ return@apply
+ }
+
+ known()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: BlooioInvalidDataException) {
+ false
+ }
+
+ /**
+ * Returns a score indicating how many valid values are contained in this object
+ * recursively.
+ *
+ * Used for best match union deserialization.
+ */
+ @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Sort && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is ChatListParams &&
+ limit == other.limit &&
+ offset == other.offset &&
+ q == other.q &&
+ sort == other.sort &&
+ additionalHeaders == other.additionalHeaders &&
+ additionalQueryParams == other.additionalQueryParams
+ }
+
+ override fun hashCode(): Int =
+ Objects.hash(limit, offset, q, sort, additionalHeaders, additionalQueryParams)
+
+ override fun toString() =
+ "ChatListParams{limit=$limit, offset=$offset, q=$q, sort=$sort, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}"
+}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/models/chats/ChatListResponse.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/models/chats/ChatListResponse.kt
new file mode 100644
index 0000000..f306121
--- /dev/null
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/models/chats/ChatListResponse.kt
@@ -0,0 +1,1409 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.blooio.api.models.chats
+
+import com.blooio.api.core.Enum
+import com.blooio.api.core.ExcludeMissing
+import com.blooio.api.core.JsonField
+import com.blooio.api.core.JsonMissing
+import com.blooio.api.core.JsonValue
+import com.blooio.api.core.checkKnown
+import com.blooio.api.core.toImmutable
+import com.blooio.api.errors.BlooioInvalidDataException
+import com.blooio.api.models.contacts.Pagination
+import com.fasterxml.jackson.annotation.JsonAnyGetter
+import com.fasterxml.jackson.annotation.JsonAnySetter
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonProperty
+import java.util.Collections
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+class ChatListResponse
+@JsonCreator(mode = JsonCreator.Mode.DISABLED)
+private constructor(
+ private val chats: JsonField>,
+ private val pagination: JsonField,
+ private val additionalProperties: MutableMap,
+) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("chats") @ExcludeMissing chats: JsonField> = JsonMissing.of(),
+ @JsonProperty("pagination")
+ @ExcludeMissing
+ pagination: JsonField = JsonMissing.of(),
+ ) : this(chats, pagination, mutableMapOf())
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun chats(): Optional> = chats.getOptional("chats")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun pagination(): Optional = pagination.getOptional("pagination")
+
+ /**
+ * Returns the raw JSON value of [chats].
+ *
+ * Unlike [chats], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("chats") @ExcludeMissing fun _chats(): JsonField> = chats
+
+ /**
+ * Returns the raw JSON value of [pagination].
+ *
+ * Unlike [pagination], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("pagination")
+ @ExcludeMissing
+ fun _pagination(): JsonField = pagination
+
+ @JsonAnySetter
+ private fun putAdditionalProperty(key: String, value: JsonValue) {
+ additionalProperties.put(key, value)
+ }
+
+ @JsonAnyGetter
+ @ExcludeMissing
+ fun _additionalProperties(): Map =
+ Collections.unmodifiableMap(additionalProperties)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ /** Returns a mutable builder for constructing an instance of [ChatListResponse]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [ChatListResponse]. */
+ class Builder internal constructor() {
+
+ private var chats: JsonField>? = null
+ private var pagination: JsonField = JsonMissing.of()
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(chatListResponse: ChatListResponse) = apply {
+ chats = chatListResponse.chats.map { it.toMutableList() }
+ pagination = chatListResponse.pagination
+ additionalProperties = chatListResponse.additionalProperties.toMutableMap()
+ }
+
+ fun chats(chats: List) = chats(JsonField.of(chats))
+
+ /**
+ * Sets [Builder.chats] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.chats] with a well-typed `List` value instead.
+ * This method is primarily for setting the field to an undocumented or not yet supported
+ * value.
+ */
+ fun chats(chats: JsonField>) = apply {
+ this.chats = chats.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [Chat] to [chats].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addChat(chat: Chat) = apply {
+ chats =
+ (chats ?: JsonField.of(mutableListOf())).also { checkKnown("chats", it).add(chat) }
+ }
+
+ fun pagination(pagination: Pagination) = pagination(JsonField.of(pagination))
+
+ /**
+ * Sets [Builder.pagination] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.pagination] with a well-typed [Pagination] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun pagination(pagination: JsonField) = apply { this.pagination = pagination }
+
+ fun additionalProperties(additionalProperties: Map) = apply {
+ this.additionalProperties.clear()
+ putAllAdditionalProperties(additionalProperties)
+ }
+
+ fun putAdditionalProperty(key: String, value: JsonValue) = apply {
+ additionalProperties.put(key, value)
+ }
+
+ fun putAllAdditionalProperties(additionalProperties: Map) = apply {
+ this.additionalProperties.putAll(additionalProperties)
+ }
+
+ fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) }
+
+ fun removeAllAdditionalProperties(keys: Set) = apply {
+ keys.forEach(::removeAdditionalProperty)
+ }
+
+ /**
+ * Returns an immutable instance of [ChatListResponse].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): ChatListResponse =
+ ChatListResponse(
+ (chats ?: JsonMissing.of()).map { it.toImmutable() },
+ pagination,
+ additionalProperties.toMutableMap(),
+ )
+ }
+
+ private var validated: Boolean = false
+
+ /**
+ * Validates that the types of all values in this object match their expected types recursively.
+ *
+ * This method is _not_ forwards compatible with new types from the API for existing fields.
+ *
+ * @throws BlooioInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): ChatListResponse = apply {
+ if (validated) {
+ return@apply
+ }
+
+ chats().ifPresent { it.forEach { it.validate() } }
+ pagination().ifPresent { it.validate() }
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: BlooioInvalidDataException) {
+ false
+ }
+
+ /**
+ * Returns a score indicating how many valid values are contained in this object recursively.
+ *
+ * Used for best match union deserialization.
+ */
+ @JvmSynthetic
+ internal fun validity(): Int =
+ (chats.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
+ (pagination.asKnown().getOrNull()?.validity() ?: 0)
+
+ class Chat
+ @JsonCreator(mode = JsonCreator.Mode.DISABLED)
+ private constructor(
+ private val id: JsonField,
+ private val backgroundId: JsonField,
+ private val backgroundUrl: JsonField,
+ private val contact: JsonField,
+ private val groupId: JsonField,
+ private val groupName: JsonField,
+ private val inboundCount: JsonField,
+ private val isGroup: JsonField,
+ private val lastInboundTime: JsonField,
+ private val lastMessage: JsonField,
+ private val lastMessageTime: JsonField,
+ private val lastOutboundTime: JsonField,
+ private val memberCount: JsonField,
+ private val messageCount: JsonField,
+ private val outboundCount: JsonField,
+ private val type: JsonField,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("id") @ExcludeMissing id: JsonField = JsonMissing.of(),
+ @JsonProperty("background_id")
+ @ExcludeMissing
+ backgroundId: JsonField = JsonMissing.of(),
+ @JsonProperty("background_url")
+ @ExcludeMissing
+ backgroundUrl: JsonField = JsonMissing.of(),
+ @JsonProperty("contact") @ExcludeMissing contact: JsonField = JsonMissing.of(),
+ @JsonProperty("group_id") @ExcludeMissing groupId: JsonField = JsonMissing.of(),
+ @JsonProperty("group_name")
+ @ExcludeMissing
+ groupName: JsonField = JsonMissing.of(),
+ @JsonProperty("inbound_count")
+ @ExcludeMissing
+ inboundCount: JsonField = JsonMissing.of(),
+ @JsonProperty("is_group")
+ @ExcludeMissing
+ isGroup: JsonField = JsonMissing.of(),
+ @JsonProperty("last_inbound_time")
+ @ExcludeMissing
+ lastInboundTime: JsonField = JsonMissing.of(),
+ @JsonProperty("last_message")
+ @ExcludeMissing
+ lastMessage: JsonField = JsonMissing.of(),
+ @JsonProperty("last_message_time")
+ @ExcludeMissing
+ lastMessageTime: JsonField = JsonMissing.of(),
+ @JsonProperty("last_outbound_time")
+ @ExcludeMissing
+ lastOutboundTime: JsonField = JsonMissing.of(),
+ @JsonProperty("member_count")
+ @ExcludeMissing
+ memberCount: JsonField = JsonMissing.of(),
+ @JsonProperty("message_count")
+ @ExcludeMissing
+ messageCount: JsonField = JsonMissing.of(),
+ @JsonProperty("outbound_count")
+ @ExcludeMissing
+ outboundCount: JsonField = JsonMissing.of(),
+ @JsonProperty("type") @ExcludeMissing type: JsonField = JsonMissing.of(),
+ ) : this(
+ id,
+ backgroundId,
+ backgroundUrl,
+ contact,
+ groupId,
+ groupName,
+ inboundCount,
+ isGroup,
+ lastInboundTime,
+ lastMessage,
+ lastMessageTime,
+ lastOutboundTime,
+ memberCount,
+ messageCount,
+ outboundCount,
+ type,
+ mutableMapOf(),
+ )
+
+ /**
+ * Chat identifier (phone number, email, or group ID)
+ *
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun id(): Optional = id.getOptional("id")
+
+ /**
+ * Identifier for the active chat background
+ *
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun backgroundId(): Optional = backgroundId.getOptional("background_id")
+
+ /**
+ * Public URL of the chat background image (if one has been set via the API)
+ *
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun backgroundUrl(): Optional = backgroundUrl.getOptional("background_url")
+
+ /**
+ * Contact info (only for non-group chats)
+ *
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun contact(): Optional = contact.getOptional("contact")
+
+ /**
+ * Group ID (only for group chats)
+ *
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun groupId(): Optional = groupId.getOptional("group_id")
+
+ /**
+ * Group name (only for group chats)
+ *
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun groupName(): Optional = groupName.getOptional("group_name")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun inboundCount(): Optional = inboundCount.getOptional("inbound_count")
+
+ /**
+ * Whether this is a group chat
+ *
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun isGroup(): Optional = isGroup.getOptional("is_group")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun lastInboundTime(): Optional = lastInboundTime.getOptional("last_inbound_time")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun lastMessage(): Optional = lastMessage.getOptional("last_message")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun lastMessageTime(): Optional = lastMessageTime.getOptional("last_message_time")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun lastOutboundTime(): Optional = lastOutboundTime.getOptional("last_outbound_time")
+
+ /**
+ * Number of members (only for group chats)
+ *
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun memberCount(): Optional = memberCount.getOptional("member_count")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun messageCount(): Optional = messageCount.getOptional("message_count")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun outboundCount(): Optional = outboundCount.getOptional("outbound_count")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun type(): Optional = type.getOptional("type")
+
+ /**
+ * Returns the raw JSON value of [id].
+ *
+ * Unlike [id], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("id") @ExcludeMissing fun _id(): JsonField = id
+
+ /**
+ * Returns the raw JSON value of [backgroundId].
+ *
+ * Unlike [backgroundId], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("background_id")
+ @ExcludeMissing
+ fun _backgroundId(): JsonField = backgroundId
+
+ /**
+ * Returns the raw JSON value of [backgroundUrl].
+ *
+ * Unlike [backgroundUrl], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("background_url")
+ @ExcludeMissing
+ fun _backgroundUrl(): JsonField = backgroundUrl
+
+ /**
+ * Returns the raw JSON value of [contact].
+ *
+ * Unlike [contact], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("contact") @ExcludeMissing fun _contact(): JsonField = contact
+
+ /**
+ * Returns the raw JSON value of [groupId].
+ *
+ * Unlike [groupId], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("group_id") @ExcludeMissing fun _groupId(): JsonField = groupId
+
+ /**
+ * Returns the raw JSON value of [groupName].
+ *
+ * Unlike [groupName], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("group_name") @ExcludeMissing fun _groupName(): JsonField = groupName
+
+ /**
+ * Returns the raw JSON value of [inboundCount].
+ *
+ * Unlike [inboundCount], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("inbound_count")
+ @ExcludeMissing
+ fun _inboundCount(): JsonField = inboundCount
+
+ /**
+ * Returns the raw JSON value of [isGroup].
+ *
+ * Unlike [isGroup], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("is_group") @ExcludeMissing fun _isGroup(): JsonField = isGroup
+
+ /**
+ * Returns the raw JSON value of [lastInboundTime].
+ *
+ * Unlike [lastInboundTime], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("last_inbound_time")
+ @ExcludeMissing
+ fun _lastInboundTime(): JsonField = lastInboundTime
+
+ /**
+ * Returns the raw JSON value of [lastMessage].
+ *
+ * Unlike [lastMessage], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("last_message")
+ @ExcludeMissing
+ fun _lastMessage(): JsonField = lastMessage
+
+ /**
+ * Returns the raw JSON value of [lastMessageTime].
+ *
+ * Unlike [lastMessageTime], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("last_message_time")
+ @ExcludeMissing
+ fun _lastMessageTime(): JsonField = lastMessageTime
+
+ /**
+ * Returns the raw JSON value of [lastOutboundTime].
+ *
+ * Unlike [lastOutboundTime], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("last_outbound_time")
+ @ExcludeMissing
+ fun _lastOutboundTime(): JsonField = lastOutboundTime
+
+ /**
+ * Returns the raw JSON value of [memberCount].
+ *
+ * Unlike [memberCount], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("member_count")
+ @ExcludeMissing
+ fun _memberCount(): JsonField = memberCount
+
+ /**
+ * Returns the raw JSON value of [messageCount].
+ *
+ * Unlike [messageCount], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("message_count")
+ @ExcludeMissing
+ fun _messageCount(): JsonField = messageCount
+
+ /**
+ * Returns the raw JSON value of [outboundCount].
+ *
+ * Unlike [outboundCount], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("outbound_count")
+ @ExcludeMissing
+ fun _outboundCount(): JsonField = outboundCount
+
+ /**
+ * Returns the raw JSON value of [type].
+ *
+ * Unlike [type], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("type") @ExcludeMissing fun _type(): JsonField = type
+
+ @JsonAnySetter
+ private fun putAdditionalProperty(key: String, value: JsonValue) {
+ additionalProperties.put(key, value)
+ }
+
+ @JsonAnyGetter
+ @ExcludeMissing
+ fun _additionalProperties(): Map =
+ Collections.unmodifiableMap(additionalProperties)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ /** Returns a mutable builder for constructing an instance of [Chat]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Chat]. */
+ class Builder internal constructor() {
+
+ private var id: JsonField = JsonMissing.of()
+ private var backgroundId: JsonField = JsonMissing.of()
+ private var backgroundUrl: JsonField = JsonMissing.of()
+ private var contact: JsonField = JsonMissing.of()
+ private var groupId: JsonField = JsonMissing.of()
+ private var groupName: JsonField = JsonMissing.of()
+ private var inboundCount: JsonField = JsonMissing.of()
+ private var isGroup: JsonField = JsonMissing.of()
+ private var lastInboundTime: JsonField = JsonMissing.of()
+ private var lastMessage: JsonField = JsonMissing.of()
+ private var lastMessageTime: JsonField = JsonMissing.of()
+ private var lastOutboundTime: JsonField = JsonMissing.of()
+ private var memberCount: JsonField = JsonMissing.of()
+ private var messageCount: JsonField = JsonMissing.of()
+ private var outboundCount: JsonField = JsonMissing.of()
+ private var type: JsonField = JsonMissing.of()
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(chat: Chat) = apply {
+ id = chat.id
+ backgroundId = chat.backgroundId
+ backgroundUrl = chat.backgroundUrl
+ contact = chat.contact
+ groupId = chat.groupId
+ groupName = chat.groupName
+ inboundCount = chat.inboundCount
+ isGroup = chat.isGroup
+ lastInboundTime = chat.lastInboundTime
+ lastMessage = chat.lastMessage
+ lastMessageTime = chat.lastMessageTime
+ lastOutboundTime = chat.lastOutboundTime
+ memberCount = chat.memberCount
+ messageCount = chat.messageCount
+ outboundCount = chat.outboundCount
+ type = chat.type
+ additionalProperties = chat.additionalProperties.toMutableMap()
+ }
+
+ /** Chat identifier (phone number, email, or group ID) */
+ fun id(id: String) = id(JsonField.of(id))
+
+ /**
+ * Sets [Builder.id] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.id] with a well-typed [String] value instead. This
+ * method is primarily for setting the field to an undocumented or not yet supported
+ * value.
+ */
+ fun id(id: JsonField) = apply { this.id = id }
+
+ /** Identifier for the active chat background */
+ fun backgroundId(backgroundId: String?) =
+ backgroundId(JsonField.ofNullable(backgroundId))
+
+ /** Alias for calling [Builder.backgroundId] with `backgroundId.orElse(null)`. */
+ fun backgroundId(backgroundId: Optional) =
+ backgroundId(backgroundId.getOrNull())
+
+ /**
+ * Sets [Builder.backgroundId] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.backgroundId] with a well-typed [String] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun backgroundId(backgroundId: JsonField) = apply {
+ this.backgroundId = backgroundId
+ }
+
+ /** Public URL of the chat background image (if one has been set via the API) */
+ fun backgroundUrl(backgroundUrl: String?) =
+ backgroundUrl(JsonField.ofNullable(backgroundUrl))
+
+ /** Alias for calling [Builder.backgroundUrl] with `backgroundUrl.orElse(null)`. */
+ fun backgroundUrl(backgroundUrl: Optional) =
+ backgroundUrl(backgroundUrl.getOrNull())
+
+ /**
+ * Sets [Builder.backgroundUrl] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.backgroundUrl] with a well-typed [String] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun backgroundUrl(backgroundUrl: JsonField) = apply {
+ this.backgroundUrl = backgroundUrl
+ }
+
+ /** Contact info (only for non-group chats) */
+ fun contact(contact: Contact?) = contact(JsonField.ofNullable(contact))
+
+ /** Alias for calling [Builder.contact] with `contact.orElse(null)`. */
+ fun contact(contact: Optional) = contact(contact.getOrNull())
+
+ /**
+ * Sets [Builder.contact] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.contact] with a well-typed [Contact] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun contact(contact: JsonField) = apply { this.contact = contact }
+
+ /** Group ID (only for group chats) */
+ fun groupId(groupId: String?) = groupId(JsonField.ofNullable(groupId))
+
+ /** Alias for calling [Builder.groupId] with `groupId.orElse(null)`. */
+ fun groupId(groupId: Optional) = groupId(groupId.getOrNull())
+
+ /**
+ * Sets [Builder.groupId] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.groupId] with a well-typed [String] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun groupId(groupId: JsonField) = apply { this.groupId = groupId }
+
+ /** Group name (only for group chats) */
+ fun groupName(groupName: String?) = groupName(JsonField.ofNullable(groupName))
+
+ /** Alias for calling [Builder.groupName] with `groupName.orElse(null)`. */
+ fun groupName(groupName: Optional) = groupName(groupName.getOrNull())
+
+ /**
+ * Sets [Builder.groupName] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.groupName] with a well-typed [String] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun groupName(groupName: JsonField) = apply { this.groupName = groupName }
+
+ fun inboundCount(inboundCount: Long) = inboundCount(JsonField.of(inboundCount))
+
+ /**
+ * Sets [Builder.inboundCount] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.inboundCount] with a well-typed [Long] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun inboundCount(inboundCount: JsonField) = apply {
+ this.inboundCount = inboundCount
+ }
+
+ /** Whether this is a group chat */
+ fun isGroup(isGroup: Boolean) = isGroup(JsonField.of(isGroup))
+
+ /**
+ * Sets [Builder.isGroup] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.isGroup] with a well-typed [Boolean] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun isGroup(isGroup: JsonField) = apply { this.isGroup = isGroup }
+
+ fun lastInboundTime(lastInboundTime: Long?) =
+ lastInboundTime(JsonField.ofNullable(lastInboundTime))
+
+ /**
+ * Alias for [Builder.lastInboundTime].
+ *
+ * This unboxed primitive overload exists for backwards compatibility.
+ */
+ fun lastInboundTime(lastInboundTime: Long) = lastInboundTime(lastInboundTime as Long?)
+
+ /** Alias for calling [Builder.lastInboundTime] with `lastInboundTime.orElse(null)`. */
+ fun lastInboundTime(lastInboundTime: Optional) =
+ lastInboundTime(lastInboundTime.getOrNull())
+
+ /**
+ * Sets [Builder.lastInboundTime] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.lastInboundTime] with a well-typed [Long] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun lastInboundTime(lastInboundTime: JsonField) = apply {
+ this.lastInboundTime = lastInboundTime
+ }
+
+ fun lastMessage(lastMessage: LastMessage) = lastMessage(JsonField.of(lastMessage))
+
+ /**
+ * Sets [Builder.lastMessage] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.lastMessage] with a well-typed [LastMessage] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun lastMessage(lastMessage: JsonField) = apply {
+ this.lastMessage = lastMessage
+ }
+
+ fun lastMessageTime(lastMessageTime: Long) =
+ lastMessageTime(JsonField.of(lastMessageTime))
+
+ /**
+ * Sets [Builder.lastMessageTime] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.lastMessageTime] with a well-typed [Long] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun lastMessageTime(lastMessageTime: JsonField) = apply {
+ this.lastMessageTime = lastMessageTime
+ }
+
+ fun lastOutboundTime(lastOutboundTime: Long?) =
+ lastOutboundTime(JsonField.ofNullable(lastOutboundTime))
+
+ /**
+ * Alias for [Builder.lastOutboundTime].
+ *
+ * This unboxed primitive overload exists for backwards compatibility.
+ */
+ fun lastOutboundTime(lastOutboundTime: Long) =
+ lastOutboundTime(lastOutboundTime as Long?)
+
+ /**
+ * Alias for calling [Builder.lastOutboundTime] with `lastOutboundTime.orElse(null)`.
+ */
+ fun lastOutboundTime(lastOutboundTime: Optional) =
+ lastOutboundTime(lastOutboundTime.getOrNull())
+
+ /**
+ * Sets [Builder.lastOutboundTime] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.lastOutboundTime] with a well-typed [Long] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun lastOutboundTime(lastOutboundTime: JsonField) = apply {
+ this.lastOutboundTime = lastOutboundTime
+ }
+
+ /** Number of members (only for group chats) */
+ fun memberCount(memberCount: Long) = memberCount(JsonField.of(memberCount))
+
+ /**
+ * Sets [Builder.memberCount] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.memberCount] with a well-typed [Long] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun memberCount(memberCount: JsonField) = apply { this.memberCount = memberCount }
+
+ fun messageCount(messageCount: Long) = messageCount(JsonField.of(messageCount))
+
+ /**
+ * Sets [Builder.messageCount] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.messageCount] with a well-typed [Long] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun messageCount(messageCount: JsonField) = apply {
+ this.messageCount = messageCount
+ }
+
+ fun outboundCount(outboundCount: Long) = outboundCount(JsonField.of(outboundCount))
+
+ /**
+ * Sets [Builder.outboundCount] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.outboundCount] with a well-typed [Long] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun outboundCount(outboundCount: JsonField) = apply {
+ this.outboundCount = outboundCount
+ }
+
+ fun type(type: Type) = type(JsonField.of(type))
+
+ /**
+ * Sets [Builder.type] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.type] with a well-typed [Type] value instead. This
+ * method is primarily for setting the field to an undocumented or not yet supported
+ * value.
+ */
+ fun type(type: JsonField) = apply { this.type = type }
+
+ fun additionalProperties(additionalProperties: Map) = apply {
+ this.additionalProperties.clear()
+ putAllAdditionalProperties(additionalProperties)
+ }
+
+ fun putAdditionalProperty(key: String, value: JsonValue) = apply {
+ additionalProperties.put(key, value)
+ }
+
+ fun putAllAdditionalProperties(additionalProperties: Map) = apply {
+ this.additionalProperties.putAll(additionalProperties)
+ }
+
+ fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) }
+
+ fun removeAllAdditionalProperties(keys: Set) = apply {
+ keys.forEach(::removeAdditionalProperty)
+ }
+
+ /**
+ * Returns an immutable instance of [Chat].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Chat =
+ Chat(
+ id,
+ backgroundId,
+ backgroundUrl,
+ contact,
+ groupId,
+ groupName,
+ inboundCount,
+ isGroup,
+ lastInboundTime,
+ lastMessage,
+ lastMessageTime,
+ lastOutboundTime,
+ memberCount,
+ messageCount,
+ outboundCount,
+ type,
+ additionalProperties.toMutableMap(),
+ )
+ }
+
+ private var validated: Boolean = false
+
+ /**
+ * Validates that the types of all values in this object match their expected types
+ * recursively.
+ *
+ * This method is _not_ forwards compatible with new types from the API for existing fields.
+ *
+ * @throws BlooioInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): Chat = apply {
+ if (validated) {
+ return@apply
+ }
+
+ id()
+ backgroundId()
+ backgroundUrl()
+ contact().ifPresent { it.validate() }
+ groupId()
+ groupName()
+ inboundCount()
+ isGroup()
+ lastInboundTime()
+ lastMessage().ifPresent { it.validate() }
+ lastMessageTime()
+ lastOutboundTime()
+ memberCount()
+ messageCount()
+ outboundCount()
+ type().ifPresent { it.validate() }
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: BlooioInvalidDataException) {
+ false
+ }
+
+ /**
+ * Returns a score indicating how many valid values are contained in this object
+ * recursively.
+ *
+ * Used for best match union deserialization.
+ */
+ @JvmSynthetic
+ internal fun validity(): Int =
+ (if (id.asKnown().isPresent) 1 else 0) +
+ (if (backgroundId.asKnown().isPresent) 1 else 0) +
+ (if (backgroundUrl.asKnown().isPresent) 1 else 0) +
+ (contact.asKnown().getOrNull()?.validity() ?: 0) +
+ (if (groupId.asKnown().isPresent) 1 else 0) +
+ (if (groupName.asKnown().isPresent) 1 else 0) +
+ (if (inboundCount.asKnown().isPresent) 1 else 0) +
+ (if (isGroup.asKnown().isPresent) 1 else 0) +
+ (if (lastInboundTime.asKnown().isPresent) 1 else 0) +
+ (lastMessage.asKnown().getOrNull()?.validity() ?: 0) +
+ (if (lastMessageTime.asKnown().isPresent) 1 else 0) +
+ (if (lastOutboundTime.asKnown().isPresent) 1 else 0) +
+ (if (memberCount.asKnown().isPresent) 1 else 0) +
+ (if (messageCount.asKnown().isPresent) 1 else 0) +
+ (if (outboundCount.asKnown().isPresent) 1 else 0) +
+ (type.asKnown().getOrNull()?.validity() ?: 0)
+
+ /** Contact info (only for non-group chats) */
+ class Contact
+ @JsonCreator(mode = JsonCreator.Mode.DISABLED)
+ private constructor(
+ private val contactId: JsonField,
+ private val identifier: JsonField,
+ private val name: JsonField,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("contact_id")
+ @ExcludeMissing
+ contactId: JsonField = JsonMissing.of(),
+ @JsonProperty("identifier")
+ @ExcludeMissing
+ identifier: JsonField = JsonMissing.of(),
+ @JsonProperty("name") @ExcludeMissing name: JsonField = JsonMissing.of(),
+ ) : this(contactId, identifier, name, mutableMapOf())
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if
+ * the server responded with an unexpected value).
+ */
+ fun contactId(): Optional = contactId.getOptional("contact_id")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if
+ * the server responded with an unexpected value).
+ */
+ fun identifier(): Optional = identifier.getOptional("identifier")
+
+ /**
+ * @throws BlooioInvalidDataException if the JSON field has an unexpected type (e.g. if
+ * the server responded with an unexpected value).
+ */
+ fun name(): Optional = name.getOptional("name")
+
+ /**
+ * Returns the raw JSON value of [contactId].
+ *
+ * Unlike [contactId], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("contact_id")
+ @ExcludeMissing
+ fun _contactId(): JsonField = contactId
+
+ /**
+ * Returns the raw JSON value of [identifier].
+ *
+ * Unlike [identifier], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("identifier")
+ @ExcludeMissing
+ fun _identifier(): JsonField = identifier
+
+ /**
+ * Returns the raw JSON value of [name].
+ *
+ * Unlike [name], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("name") @ExcludeMissing fun _name(): JsonField = name
+
+ @JsonAnySetter
+ private fun putAdditionalProperty(key: String, value: JsonValue) {
+ additionalProperties.put(key, value)
+ }
+
+ @JsonAnyGetter
+ @ExcludeMissing
+ fun _additionalProperties(): Map =
+ Collections.unmodifiableMap(additionalProperties)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ /** Returns a mutable builder for constructing an instance of [Contact]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Contact]. */
+ class Builder internal constructor() {
+
+ private var contactId: JsonField = JsonMissing.of()
+ private var identifier: JsonField = JsonMissing.of()
+ private var name: JsonField = JsonMissing.of()
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(contact: Contact) = apply {
+ contactId = contact.contactId
+ identifier = contact.identifier
+ name = contact.name
+ additionalProperties = contact.additionalProperties.toMutableMap()
+ }
+
+ fun contactId(contactId: String) = contactId(JsonField.of(contactId))
+
+ /**
+ * Sets [Builder.contactId] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.contactId] with a well-typed [String] value
+ * instead. This method is primarily for setting the field to an undocumented or not
+ * yet supported value.
+ */
+ fun contactId(contactId: JsonField) = apply { this.contactId = contactId }
+
+ fun identifier(identifier: String) = identifier(JsonField.of(identifier))
+
+ /**
+ * Sets [Builder.identifier] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.identifier] with a well-typed [String] value
+ * instead. This method is primarily for setting the field to an undocumented or not
+ * yet supported value.
+ */
+ fun identifier(identifier: JsonField) = apply {
+ this.identifier = identifier
+ }
+
+ fun name(name: String?) = name(JsonField.ofNullable(name))
+
+ /** Alias for calling [Builder.name] with `name.orElse(null)`. */
+ fun name(name: Optional) = name(name.getOrNull())
+
+ /**
+ * Sets [Builder.name] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.name] with a well-typed [String] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun name(name: JsonField) = apply { this.name = name }
+
+ fun additionalProperties(additionalProperties: Map) = apply {
+ this.additionalProperties.clear()
+ putAllAdditionalProperties(additionalProperties)
+ }
+
+ fun putAdditionalProperty(key: String, value: JsonValue) = apply {
+ additionalProperties.put(key, value)
+ }
+
+ fun putAllAdditionalProperties(additionalProperties: Map) =
+ apply {
+ this.additionalProperties.putAll(additionalProperties)
+ }
+
+ fun removeAdditionalProperty(key: String) = apply {
+ additionalProperties.remove(key)
+ }
+
+ fun removeAllAdditionalProperties(keys: Set) = apply {
+ keys.forEach(::removeAdditionalProperty)
+ }
+
+ /**
+ * Returns an immutable instance of [Contact].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Contact =
+ Contact(contactId, identifier, name, additionalProperties.toMutableMap())
+ }
+
+ private var validated: Boolean = false
+
+ /**
+ * Validates that the types of all values in this object match their expected types
+ * recursively.
+ *
+ * This method is _not_ forwards compatible with new types from the API for existing
+ * fields.
+ *
+ * @throws BlooioInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): Contact = apply {
+ if (validated) {
+ return@apply
+ }
+
+ contactId()
+ identifier()
+ name()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: BlooioInvalidDataException) {
+ false
+ }
+
+ /**
+ * Returns a score indicating how many valid values are contained in this object
+ * recursively.
+ *
+ * Used for best match union deserialization.
+ */
+ @JvmSynthetic
+ internal fun validity(): Int =
+ (if (contactId.asKnown().isPresent) 1 else 0) +
+ (if (identifier.asKnown().isPresent) 1 else 0) +
+ (if (name.asKnown().isPresent) 1 else 0)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Contact &&
+ contactId == other.contactId &&
+ identifier == other.identifier &&
+ name == other.name &&
+ additionalProperties == other.additionalProperties
+ }
+
+ private val hashCode: Int by lazy {
+ Objects.hash(contactId, identifier, name, additionalProperties)
+ }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() =
+ "Contact{contactId=$contactId, identifier=$identifier, name=$name, additionalProperties=$additionalProperties}"
+ }
+
+ class Type @JsonCreator private constructor(private val value: JsonField) : Enum {
+
+ /**
+ * Returns this class instance's raw value.
+ *
+ * This is usually only useful if this instance was deserialized from data that doesn't
+ * match any known member, and you want to know that value. For example, if the SDK is
+ * on an older version than the API, then the API may respond with new members that the
+ * SDK is unaware of.
+ */
+ @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value
+
+ companion object {
+
+ @JvmField val PHONE = of("phone")
+
+ @JvmField val EMAIL = of("email")
+
+ @JvmField val GROUP = of("group")
+
+ @JvmStatic fun of(value: String) = Type(JsonField.of(value))
+ }
+
+ /** An enum containing [Type]'s known values. */
+ enum class Known {
+ PHONE,
+ EMAIL,
+ GROUP,
+ }
+
+ /**
+ * An enum containing [Type]'s known values, as well as an [_UNKNOWN] member.
+ *
+ * An instance of [Type] can contain an unknown value in a couple of cases:
+ * - It was deserialized from data that doesn't match any known member. For example, if
+ * the SDK is on an older version than the API, then the API may respond with new
+ * members that the SDK is unaware of.
+ * - It was constructed with an arbitrary value using the [of] method.
+ */
+ enum class Value {
+ PHONE,
+ EMAIL,
+ GROUP,
+ /** An enum member indicating that [Type] was instantiated with an unknown value. */
+ _UNKNOWN,
+ }
+
+ /**
+ * Returns an enum member corresponding to this class instance's value, or
+ * [Value._UNKNOWN] if the class was instantiated with an unknown value.
+ *
+ * Use the [known] method instead if you're certain the value is always known or if you
+ * want to throw for the unknown case.
+ */
+ fun value(): Value =
+ when (this) {
+ PHONE -> Value.PHONE
+ EMAIL -> Value.EMAIL
+ GROUP -> Value.GROUP
+ else -> Value._UNKNOWN
+ }
+
+ /**
+ * Returns an enum member corresponding to this class instance's value.
+ *
+ * Use the [value] method instead if you're uncertain the value is always known and
+ * don't want to throw for the unknown case.
+ *
+ * @throws BlooioInvalidDataException if this class instance's value is a not a known
+ * member.
+ */
+ fun known(): Known =
+ when (this) {
+ PHONE -> Known.PHONE
+ EMAIL -> Known.EMAIL
+ GROUP -> Known.GROUP
+ else -> throw BlooioInvalidDataException("Unknown Type: $value")
+ }
+
+ /**
+ * Returns this class instance's primitive wire representation.
+ *
+ * This differs from the [toString] method because that method is primarily for
+ * debugging and generally doesn't throw.
+ *
+ * @throws BlooioInvalidDataException if this class instance's value does not have the
+ * expected primitive type.
+ */
+ fun asString(): String =
+ _value().asString().orElseThrow {
+ BlooioInvalidDataException("Value is not a String")
+ }
+
+ private var validated: Boolean = false
+
+ /**
+ * Validates that the types of all values in this object match their expected types
+ * recursively.
+ *
+ * This method is _not_ forwards compatible with new types from the API for existing
+ * fields.
+ *
+ * @throws BlooioInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): Type = apply {
+ if (validated) {
+ return@apply
+ }
+
+ known()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: BlooioInvalidDataException) {
+ false
+ }
+
+ /**
+ * Returns a score indicating how many valid values are contained in this object
+ * recursively.
+ *
+ * Used for best match union deserialization.
+ */
+ @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Type && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Chat &&
+ id == other.id &&
+ backgroundId == other.backgroundId &&
+ backgroundUrl == other.backgroundUrl &&
+ contact == other.contact &&
+ groupId == other.groupId &&
+ groupName == other.groupName &&
+ inboundCount == other.inboundCount &&
+ isGroup == other.isGroup &&
+ lastInboundTime == other.lastInboundTime &&
+ lastMessage == other.lastMessage &&
+ lastMessageTime == other.lastMessageTime &&
+ lastOutboundTime == other.lastOutboundTime &&
+ memberCount == other.memberCount &&
+ messageCount == other.messageCount &&
+ outboundCount == other.outboundCount &&
+ type == other.type &&
+ additionalProperties == other.additionalProperties
+ }
+
+ private val hashCode: Int by lazy {
+ Objects.hash(
+ id,
+ backgroundId,
+ backgroundUrl,
+ contact,
+ groupId,
+ groupName,
+ inboundCount,
+ isGroup,
+ lastInboundTime,
+ lastMessage,
+ lastMessageTime,
+ lastOutboundTime,
+ memberCount,
+ messageCount,
+ outboundCount,
+ type,
+ additionalProperties,
+ )
+ }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() =
+ "Chat{id=$id, backgroundId=$backgroundId, backgroundUrl=$backgroundUrl, contact=$contact, groupId=$groupId, groupName=$groupName, inboundCount=$inboundCount, isGroup=$isGroup, lastInboundTime=$lastInboundTime, lastMessage=$lastMessage, lastMessageTime=$lastMessageTime, lastOutboundTime=$lastOutboundTime, memberCount=$memberCount, messageCount=$messageCount, outboundCount=$outboundCount, type=$type, additionalProperties=$additionalProperties}"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is ChatListResponse &&
+ chats == other.chats &&
+ pagination == other.pagination &&
+ additionalProperties == other.additionalProperties
+ }
+
+ private val hashCode: Int by lazy { Objects.hash(chats, pagination, additionalProperties) }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() =
+ "ChatListResponse{chats=$chats, pagination=$pagination, additionalProperties=$additionalProperties}"
+}
diff --git a/blooio-java-core/src/main/kotlin/com/blooio/api/models/chats/ChatMarkAsReadParams.kt b/blooio-java-core/src/main/kotlin/com/blooio/api/models/chats/ChatMarkAsReadParams.kt
new file mode 100644
index 0000000..67be4e1
--- /dev/null
+++ b/blooio-java-core/src/main/kotlin/com/blooio/api/models/chats/ChatMarkAsReadParams.kt
@@ -0,0 +1,229 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.blooio.api.models.chats
+
+import com.blooio.api.core.JsonValue
+import com.blooio.api.core.Params
+import com.blooio.api.core.http.Headers
+import com.blooio.api.core.http.QueryParams
+import com.blooio.api.core.toImmutable
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** Mark all messages in a chat as read. This sends a read receipt to the sender. */
+class ChatMarkAsReadParams
+private constructor(
+ private val chatId: String?,
+ private val additionalHeaders: Headers,
+ private val additionalQueryParams: QueryParams,
+ private val additionalBodyProperties: Map,
+) : Params {
+
+ fun chatId(): Optional = Optional.ofNullable(chatId)
+
+ /** Additional body properties to send with the request. */
+ fun _additionalBodyProperties(): Map = additionalBodyProperties
+
+ /** Additional headers to send with the request. */
+ fun _additionalHeaders(): Headers = additionalHeaders
+
+ /** Additional query param to send with the request. */
+ fun _additionalQueryParams(): QueryParams = additionalQueryParams
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ @JvmStatic fun none(): ChatMarkAsReadParams = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [ChatMarkAsReadParams]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [ChatMarkAsReadParams]. */
+ class Builder internal constructor() {
+
+ private var chatId: String? = null
+ private var additionalHeaders: Headers.Builder = Headers.builder()
+ private var additionalQueryParams: QueryParams.Builder = QueryParams.builder()
+ private var additionalBodyProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(chatMarkAsReadParams: ChatMarkAsReadParams) = apply {
+ chatId = chatMarkAsReadParams.chatId
+ additionalHeaders = chatMarkAsReadParams.additionalHeaders.toBuilder()
+ additionalQueryParams = chatMarkAsReadParams.additionalQueryParams.toBuilder()
+ additionalBodyProperties = chatMarkAsReadParams.additionalBodyProperties.toMutableMap()
+ }
+
+ fun chatId(chatId: String?) = apply { this.chatId = chatId }
+
+ /** Alias for calling [Builder.chatId] with `chatId.orElse(null)`. */
+ fun chatId(chatId: Optional) = chatId(chatId.getOrNull())
+
+ fun additionalHeaders(additionalHeaders: Headers) = apply {
+ this.additionalHeaders.clear()
+ putAllAdditionalHeaders(additionalHeaders)
+ }
+
+ fun additionalHeaders(additionalHeaders: Map>) = apply {
+ this.additionalHeaders.clear()
+ putAllAdditionalHeaders(additionalHeaders)
+ }
+
+ fun putAdditionalHeader(name: String, value: String) = apply {
+ additionalHeaders.put(name, value)
+ }
+
+ fun putAdditionalHeaders(name: String, values: Iterable) = apply {
+ additionalHeaders.put(name, values)
+ }
+
+ fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply {
+ this.additionalHeaders.putAll(additionalHeaders)
+ }
+
+ fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply {
+ this.additionalHeaders.putAll(additionalHeaders)
+ }
+
+ fun replaceAdditionalHeaders(name: String, value: String) = apply {
+ additionalHeaders.replace(name, value)
+ }
+
+ fun replaceAdditionalHeaders(name: String, values: Iterable) = apply {
+ additionalHeaders.replace(name, values)
+ }
+
+ fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply {
+ this.additionalHeaders.replaceAll(additionalHeaders)
+ }
+
+ fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply {
+ this.additionalHeaders.replaceAll(additionalHeaders)
+ }
+
+ fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) }
+
+ fun removeAllAdditionalHeaders(names: Set) = apply {
+ additionalHeaders.removeAll(names)
+ }
+
+ fun additionalQueryParams(additionalQueryParams: QueryParams) = apply {
+ this.additionalQueryParams.clear()
+ putAllAdditionalQueryParams(additionalQueryParams)
+ }
+
+ fun additionalQueryParams(additionalQueryParams: Map>) = apply {
+ this.additionalQueryParams.clear()
+ putAllAdditionalQueryParams(additionalQueryParams)
+ }
+
+ fun putAdditionalQueryParam(key: String, value: String) = apply {
+ additionalQueryParams.put(key, value)
+ }
+
+ fun putAdditionalQueryParams(key: String, values: Iterable) = apply {
+ additionalQueryParams.put(key, values)
+ }
+
+ fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply {
+ this.additionalQueryParams.putAll(additionalQueryParams)
+ }
+
+ fun putAllAdditionalQueryParams(additionalQueryParams: Map>) =
+ apply {
+ this.additionalQueryParams.putAll(additionalQueryParams)
+ }
+
+ fun replaceAdditionalQueryParams(key: String, value: String) = apply {
+ additionalQueryParams.replace(key, value)
+ }
+
+ fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply {
+ additionalQueryParams.replace(key, values)
+ }
+
+ fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply {
+ this.additionalQueryParams.replaceAll(additionalQueryParams)
+ }
+
+ fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) =
+ apply {
+ this.additionalQueryParams.replaceAll(additionalQueryParams)
+ }
+
+ fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) }
+
+ fun removeAllAdditionalQueryParams(keys: Set) = apply {
+ additionalQueryParams.removeAll(keys)
+ }
+
+ fun additionalBodyProperties(additionalBodyProperties: Map) = apply {
+ this.additionalBodyProperties.clear()
+ putAllAdditionalBodyProperties(additionalBodyProperties)
+ }
+
+ fun putAdditionalBodyProperty(key: String, value: JsonValue) = apply {
+ additionalBodyProperties.put(key, value)
+ }
+
+ fun putAllAdditionalBodyProperties(additionalBodyProperties: Map) =
+ apply {
+ this.additionalBodyProperties.putAll(additionalBodyProperties)
+ }
+
+ fun removeAdditionalBodyProperty(key: String) = apply {
+ additionalBodyProperties.remove(key)
+ }
+
+ fun removeAllAdditionalBodyProperties(keys: Set) = apply {
+ keys.forEach(::removeAdditionalBodyProperty)
+ }
+
+ /**
+ * Returns an immutable instance of [ChatMarkAsReadParams].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): ChatMarkAsReadParams =
+ ChatMarkAsReadParams(
+ chatId,
+ additionalHeaders.build(),
+ additionalQueryParams.build(),
+ additionalBodyProperties.toImmutable(),
+ )
+ }
+
+ fun _body(): Optional