diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 93f2c0fea..9b6843d51 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/**'
@@ -17,13 +19,13 @@ jobs:
timeout-minutes: 15
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/orb-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
- if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+ 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@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
- uses: actions/setup-java@v5
+ 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
@@ -44,13 +46,13 @@ jobs:
contents: read
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/orb-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
- if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+ 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@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
- uses: actions/setup-java@v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: |
@@ -59,20 +61,24 @@ 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/orb-java'
+ if: |-
+ github.repository == 'stainless-sdks/orb-java' &&
+ !startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
- uses: actions/github-script@v6
+ 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/orb-java'
+ if: |-
+ github.repository == 'stainless-sdks/orb-java' &&
+ !startsWith(github.ref, 'refs/heads/stl/')
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
@@ -85,10 +91,10 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/orb-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
- uses: actions/setup-java@v5
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: |
@@ -97,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 e92f9580b..074e72029 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@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Java
- uses: actions/setup-java@v5
+ 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 ddfc1fdeb..c4079ee94 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'orbcorp/orb-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check release environment
run: |
diff --git a/.gitignore b/.gitignore
index b1346e6d1..90b85e944 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 eb4e0dba7..caf148712 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.10.0"
+ ".": "1.11.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 666bff36b..f3bbb69bb 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 126
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/orb%2Forb-c131de17773b450eb1ec03ca001f94d3777e35347234869a7efee083003e1513.yml
-openapi_spec_hash: 5d2d4a3a9ada1c381efb318b6897994d
-config_hash: bcf82bddb691f6be773ac6cae8c03b9a
+configured_endpoints: 139
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/orb/orb-45297c0f1067cd444a30371110c69f876accf55f303a3dce9a4f74863b59f18f.yml
+openapi_spec_hash: cd2f638b98c6e89342397fef860166b7
+config_hash: c01c1191b1cd696c7ca855ff6d28a8df
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2985ce5a0..b22ae8292 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,107 @@
# Changelog
+## 1.11.0 (2026-05-22)
+
+Full Changelog: [v1.10.0...v1.11.0](https://github.com/orbcorp/orb-java/compare/v1.10.0...v1.11.0)
+
+### Features
+
+* **api:** api update ([a9e8d7c](https://github.com/orbcorp/orb-java/commit/a9e8d7ce96ffc47877d07adf2e4e48da25872633))
+* **api:** api update ([72265aa](https://github.com/orbcorp/orb-java/commit/72265aab772192c5f713b53fa3f3314e66ba4ee2))
+* **api:** api update ([a42565e](https://github.com/orbcorp/orb-java/commit/a42565ef43485d9f55f62e5fde0e3ebcc162f1b5))
+* **api:** api update ([df70a48](https://github.com/orbcorp/orb-java/commit/df70a48bf466522c5540cef316fd63d0b4591e22))
+* **api:** api update ([7d720b4](https://github.com/orbcorp/orb-java/commit/7d720b40d5f2c01f9b884ef53264f9b60efd0c5b))
+* **api:** api update ([ed6d44c](https://github.com/orbcorp/orb-java/commit/ed6d44c4aa8444bd084257ebd86fb78153bcb2c7))
+* **api:** api update ([1b50fd9](https://github.com/orbcorp/orb-java/commit/1b50fd98c61acd7d7ac75617c817e7f32c62ea23))
+* **api:** api update ([7f58f4b](https://github.com/orbcorp/orb-java/commit/7f58f4b19d30b44e8e2ee69e4e93264960b1d433))
+* **api:** api update ([4435313](https://github.com/orbcorp/orb-java/commit/4435313a9f7fe6e44b4b43d72b540e33ba102c42))
+* **api:** api update ([79cd441](https://github.com/orbcorp/orb-java/commit/79cd4418a453844f56ee7ba4d2e369357fb4db85))
+* **api:** api update ([d5b17d8](https://github.com/orbcorp/orb-java/commit/d5b17d8165c5ce42c1ad5cc6ff9970b90667da01))
+* **api:** api update ([d875ca5](https://github.com/orbcorp/orb-java/commit/d875ca51fd171ccb6f68e671cd80b1ba08b4e56d))
+* **api:** api update ([5bdd885](https://github.com/orbcorp/orb-java/commit/5bdd8852877fe90b5f0b8cd6a4120ab0316a9719))
+* **api:** api update ([e8b8d9a](https://github.com/orbcorp/orb-java/commit/e8b8d9afb5b6340f8a2d16d549e34823fb245dbd))
+* **api:** api update ([1581833](https://github.com/orbcorp/orb-java/commit/15818339bc1d1b2098f3fe0d9c06d8fa4afa0055))
+* **api:** api update ([1152bfb](https://github.com/orbcorp/orb-java/commit/1152bfb0f075438d170bdee2e4c3b02327501057))
+* **api:** api update ([308f7b2](https://github.com/orbcorp/orb-java/commit/308f7b2ed70c479fd7b43e61ffc83e534e137f98))
+* **api:** api update ([36d0db0](https://github.com/orbcorp/orb-java/commit/36d0db01c83dfacb297970d8fc48afad1c81d376))
+* **api:** api update ([bac4aa0](https://github.com/orbcorp/orb-java/commit/bac4aa009a547a636faef396a183d4da5c573933))
+* **api:** api update ([037dbcd](https://github.com/orbcorp/orb-java/commit/037dbcde5fbff819b3a65b43bdc92332a88d96e5))
+* **api:** api update ([014ce03](https://github.com/orbcorp/orb-java/commit/014ce038155006c8874869d82da451b427b5a177))
+* **api:** api update ([4ed5e39](https://github.com/orbcorp/orb-java/commit/4ed5e399e2aeb4ba507314f37ebe510a7f4dcecf))
+* **api:** api update ([522cf9b](https://github.com/orbcorp/orb-java/commit/522cf9bee689650a217f54f12d93f1c548994fa0))
+* **api:** api update ([7661c8c](https://github.com/orbcorp/orb-java/commit/7661c8c5892a01032cf4b24a74b309a2eac77f0c))
+* **api:** api update ([ededc4d](https://github.com/orbcorp/orb-java/commit/ededc4dcf7a0dcbe132f60e4e06b7c0b3c538caa))
+* **api:** api update ([3b82472](https://github.com/orbcorp/orb-java/commit/3b8247255097a5250b47059ca568429a26e0f285))
+* **api:** api update ([c310e36](https://github.com/orbcorp/orb-java/commit/c310e36ffe22fafdd5ba9a00728e7816a8d0e1e6))
+* **api:** api update ([a80ec0b](https://github.com/orbcorp/orb-java/commit/a80ec0b34dabf9595dbae9dfc4b4ed6c914cf5d2))
+* **api:** api update ([69429b9](https://github.com/orbcorp/orb-java/commit/69429b99a2aa15ce52ebd45f7a2a531395f3dc94))
+* **api:** api update ([49d56e2](https://github.com/orbcorp/orb-java/commit/49d56e2d1ef966ebf6ad4508c81b43a19e7cf9c5))
+* **api:** api update ([1e5f897](https://github.com/orbcorp/orb-java/commit/1e5f8972d4dd8c4c955e8980b0c83901a2594bd6))
+* **api:** api update ([eaca79d](https://github.com/orbcorp/orb-java/commit/eaca79da23a8cad0430abcb0319c8a37a5bbfc2a))
+* **api:** manual updates ([5ba9c33](https://github.com/orbcorp/orb-java/commit/5ba9c33bba4f76344046e7cd93946fe96befaf34))
+* **client:** add connection pooling option ([21b4047](https://github.com/orbcorp/orb-java/commit/21b4047e592f1d92782f6aacba9ba0f4d8d893d2))
+* **client:** add more convenience service method overloads ([f56884d](https://github.com/orbcorp/orb-java/commit/f56884dcd6437584dabd311bf0583c6145515e60))
+* **client:** improve logging ([1ca2d2f](https://github.com/orbcorp/orb-java/commit/1ca2d2f249f2554e0737c5c251d6c01b74f36770))
+* **client:** more robust error parsing ([c56994a](https://github.com/orbcorp/orb-java/commit/c56994a7ff8e3d80d514796134161d031ab85c44))
+* **client:** send `X-Stainless-Kotlin-Version` header ([10357a9](https://github.com/orbcorp/orb-java/commit/10357a946d98ffd0cb26409f81af477b4c4638b2))
+* **client:** support proxy authentication ([00a20b6](https://github.com/orbcorp/orb-java/commit/00a20b682a4ba959024b745c40a5713a2dc8e7ab))
+* support setting headers via env ([9b9d608](https://github.com/orbcorp/orb-java/commit/9b9d6087bc5ef54cdf57ff73bda39b2460a908a5))
+
+
+### Bug Fixes
+
+* **client:** allow updating header/query affecting fields in `toBuilder()` ([fc7192d](https://github.com/orbcorp/orb-java/commit/fc7192dcba2ed208109219a7df87608648ecbf8e))
+* **client:** incorrect `Retry-After` parsing ([154bdc7](https://github.com/orbcorp/orb-java/commit/154bdc7e119b92a57a9798edf942564f288141a8))
+* **client:** preserve time zone in lenient date-time parsing ([928bfae](https://github.com/orbcorp/orb-java/commit/928bfae14d9e8353f052404bf80873f1bd60d167))
+* fix request delays for retrying to be more respectful of high requested delays ([185d18d](https://github.com/orbcorp/orb-java/commit/185d18dd791129bdbcf9c7c0fe12cdeb34474045))
+
+
+### Performance Improvements
+
+* **client:** create one json mapper ([bb2637a](https://github.com/orbcorp/orb-java/commit/bb2637a17a9ef89db3855320700d963fa5c04623))
+
+
+### Chores
+
+* **ci:** skip lint on metadata-only changes ([0d80e8a](https://github.com/orbcorp/orb-java/commit/0d80e8ab48e47fa1a00fefb89ef5203849d1b22e))
+* **ci:** skip uploading artifacts on stainless-internal branches ([2b236e9](https://github.com/orbcorp/orb-java/commit/2b236e90a64e621b2f772208c4048ccb1f26c066))
+* **ci:** upgrade `actions/github-script` ([7850067](https://github.com/orbcorp/orb-java/commit/7850067ac12719de79ff00ada02962f035782791))
+* **docs:** add missing descriptions ([d101825](https://github.com/orbcorp/orb-java/commit/d101825fc5db427bb9d36c6b82154919eea85103))
+* drop apache dependency ([edcdb51](https://github.com/orbcorp/orb-java/commit/edcdb514ff66cba287aab7ec5dec282078abb45f))
+* **internal:** allow passing args to `./scripts/test` ([ffbc747](https://github.com/orbcorp/orb-java/commit/ffbc7473823b4ea0ccd27b9cfb85f8a5c165bb47))
+* **internal:** bump palantir-java-format ([82f17f8](https://github.com/orbcorp/orb-java/commit/82f17f8f9081527292cc9ca2bc150d10b2b9dfc1))
+* **internal:** codegen related update ([489bdd6](https://github.com/orbcorp/orb-java/commit/489bdd632da69ee3bafce9d5364861821f235c2b))
+* **internal:** codegen related update ([b8b2251](https://github.com/orbcorp/orb-java/commit/b8b2251ef940a4d3a5276a9287bfc628fcc7fa2b))
+* **internal:** codegen related update ([46abca4](https://github.com/orbcorp/orb-java/commit/46abca478b99884a07ac874f56f5ab7f615d9162))
+* **internal:** correct cache invalidation for `SKIP_MOCK_TESTS` ([4a49c05](https://github.com/orbcorp/orb-java/commit/4a49c05fd112adb44adebff7481d4076ede46058))
+* **internal:** expand imports ([c1195af](https://github.com/orbcorp/orb-java/commit/c1195afe5fc1798dbfd9c8c183501d1e4f33db75))
+* **internal:** make `OkHttp` constructor internal ([05a1acc](https://github.com/orbcorp/orb-java/commit/05a1acc5dd4128fe698db34626ebaaed97ad17f6))
+* **internal:** tweak CI branches ([282b2c1](https://github.com/orbcorp/orb-java/commit/282b2c15ade0d105ae975f9df2476cc647d51f04))
+* **internal:** update `TestServerExtension` comment ([3447469](https://github.com/orbcorp/orb-java/commit/34474693bac76cf4855636d09bac7001108f2ccc))
+* **internal:** update gitignore ([6b9c515](https://github.com/orbcorp/orb-java/commit/6b9c515adf31e37d75ff7507aa5e5b0f6083ce47))
+* **internal:** update maven repo doc to include authentication ([9aa3e27](https://github.com/orbcorp/orb-java/commit/9aa3e2745cf3794784ee229af579142c6d560182))
+* **internal:** update multipart form array serialization ([bd754d3](https://github.com/orbcorp/orb-java/commit/bd754d3554fc55b3a5f52c79111a714ed5d5c236))
+* **internal:** update retry delay tests ([8792a79](https://github.com/orbcorp/orb-java/commit/8792a79536e435e2e4f51c75aaa75129f0db3e3c))
+* **internal:** upgrade AssertJ ([719f036](https://github.com/orbcorp/orb-java/commit/719f036e81d72bf5c80a3e53de1e6fcdee49f15e))
+* make `Properties` more resilient to `null` ([39c553f](https://github.com/orbcorp/orb-java/commit/39c553f7b878162f7affd567e9c0450d1411e653))
+* redact api-key headers in debug logs ([1064c76](https://github.com/orbcorp/orb-java/commit/1064c76a2ef0ab90ed01fae2e89225709ab1873f))
+* **test:** do not count install time for mock server timeout ([efdd6eb](https://github.com/orbcorp/orb-java/commit/efdd6ebf4a150e21f6edc283f5cd8f91ed049b12))
+* **tests:** bump steady to v0.19.4 ([477914f](https://github.com/orbcorp/orb-java/commit/477914f0d8d4ee5c36aa22083629d6e7b3041ae4))
+* **tests:** bump steady to v0.19.5 ([f8a5c1e](https://github.com/orbcorp/orb-java/commit/f8a5c1eabcfb0a78273e78b8e3ca017a0368a098))
+* **tests:** bump steady to v0.19.6 ([26951e7](https://github.com/orbcorp/orb-java/commit/26951e743f106261e4c2f85cf8851e89fd628771))
+* **tests:** bump steady to v0.19.7 ([a8de590](https://github.com/orbcorp/orb-java/commit/a8de590eb5afb9a0ab68e9b50e1ba6cd2ef0e68f))
+* **tests:** bump steady to v0.20.1 ([c9dba44](https://github.com/orbcorp/orb-java/commit/c9dba44e6ff1302d5668d3490ba38799fb86a848))
+* **tests:** bump steady to v0.20.2 ([d480c4b](https://github.com/orbcorp/orb-java/commit/d480c4b2d7fa3ba231ee40b4b79fb3b6d5b6c820))
+* **tests:** bump steady to v0.22.1 ([7a01a25](https://github.com/orbcorp/orb-java/commit/7a01a2582e692add20034607a09ee313fc470f6c))
+* **tests:** update mock server to steady ([3b92540](https://github.com/orbcorp/orb-java/commit/3b92540a62694a2865ef60e244f04b2f6abe4ba2))
+
+
+### Documentation
+
+* add comment for arbitrary value fields ([e2f42d1](https://github.com/orbcorp/orb-java/commit/e2f42d1f5d5b951c50798e943a64e14ff134b4f2))
+* clarify forwards compat behavior ([16f10e5](https://github.com/orbcorp/orb-java/commit/16f10e56f568f802c64570477ed9edf082b5e5d0))
+* improve examples ([8e6ac31](https://github.com/orbcorp/orb-java/commit/8e6ac31400bc21c291599235a1c8048a5a9c61b5))
+
## 1.10.0 (2026-01-17)
Full Changelog: [v1.9.0...v1.10.0](https://github.com/orbcorp/orb-java/compare/v1.9.0...v1.10.0)
diff --git a/README.md b/README.md
index be99484ce..222c8dd1c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-[](https://central.sonatype.com/artifact/com.withorb.api/orb-java/1.10.0)
+[](https://central.sonatype.com/artifact/com.withorb.api/orb-java/1.11.0)
@@ -19,7 +19,7 @@ The REST API documentation can be found on [docs.withorb.com](https://docs.witho
### Gradle
```kotlin
-implementation("com.withorb.api:orb-java:1.10.0")
+implementation("com.withorb.api:orb-java:1.11.0")
```
### Maven
@@ -28,7 +28,7 @@ implementation("com.withorb.api:orb-java:1.10.0")
com.withorb.api
orb-java
- 1.10.0
+ 1.11.0
```
@@ -339,8 +339,6 @@ while (true) {
## Logging
-The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
-
Enable logging by setting the `ORB_LOG` environment variable to `info`:
```sh
@@ -353,6 +351,19 @@ Or to `debug` for more verbose logging:
export ORB_LOG=debug
```
+Or configure the client manually using the `logLevel` method:
+
+```java
+import com.withorb.api.client.OrbClient;
+import com.withorb.api.client.okhttp.OrbOkHttpClient;
+import com.withorb.api.core.LogLevel;
+
+OrbClient client = OrbOkHttpClient.builder()
+ .fromEnv()
+ .logLevel(LogLevel.INFO)
+ .build();
+```
+
## Webhook Verification
We provide helper methods for verifying that a webhook request came from Orb, and not a malicious third party.
@@ -457,6 +468,40 @@ OrbClient client = OrbOkHttpClient.builder()
.build();
```
+If the proxy responds with `407 Proxy Authentication Required`, supply credentials by also configuring `proxyAuthenticator`:
+
+```java
+import com.withorb.api.client.OrbClient;
+import com.withorb.api.client.okhttp.OrbOkHttpClient;
+import com.withorb.api.core.http.ProxyAuthenticator;
+
+OrbClient client = OrbOkHttpClient.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.withorb.api.client.OrbClient;
+import com.withorb.api.client.okhttp.OrbOkHttpClient;
+import java.time.Duration;
+
+OrbClient client = OrbOkHttpClient.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]
@@ -676,7 +721,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 [`OrbInvalidDataException`](orb-java-core/src/main/kotlin/com/withorb/api/errors/OrbInvalidDataException.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.withorb.api.models.Customer;
diff --git a/build.gradle.kts b/build.gradle.kts
index 8e1585b2b..81989492b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,6 @@
allprojects {
group = "com.withorb.api"
- version = "1.10.0" // x-release-please-version
+ version = "1.11.0" // x-release-please-version
}
subprojects {
diff --git a/buildSrc/src/main/kotlin/orb.java.gradle.kts b/buildSrc/src/main/kotlin/orb.java.gradle.kts
index 70fc33f41..8f4f902a6 100644
--- a/buildSrc/src/main/kotlin/orb.java.gradle.kts
+++ b/buildSrc/src/main/kotlin/orb.java.gradle.kts
@@ -45,7 +45,7 @@ tasks.withType().configureEach {
val palantir by configurations.creating
dependencies {
- palantir("com.palantir.javaformat:palantir-java-format:2.73.0")
+ palantir("com.palantir.javaformat:palantir-java-format:2.89.0")
}
fun registerPalantir(
diff --git a/buildSrc/src/main/kotlin/orb.kotlin.gradle.kts b/buildSrc/src/main/kotlin/orb.kotlin.gradle.kts
index b908b3bed..9f10a4f06 100644
--- a/buildSrc/src/main/kotlin/orb.kotlin.gradle.kts
+++ b/buildSrc/src/main/kotlin/orb.kotlin.gradle.kts
@@ -33,11 +33,14 @@ kotlin {
tasks.withType().configureEach {
systemProperty("junit.jupiter.execution.parallel.enabled", true)
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
+
+ // `SKIP_MOCK_TESTS` affects which tests run so it must be added as input for proper cache invalidation.
+ inputs.property("skipMockTests", System.getenv("SKIP_MOCK_TESTS")).optional(true)
}
val ktfmt by configurations.creating
dependencies {
- ktfmt("com.facebook:ktfmt:0.56")
+ ktfmt("com.facebook:ktfmt:0.61")
}
fun registerKtfmt(
diff --git a/examples/.keep b/examples/.keep
deleted file mode 100644
index d8c73e937..000000000
--- a/examples/.keep
+++ /dev/null
@@ -1,4 +0,0 @@
-File generated from our OpenAPI spec by Stainless.
-
-This directory can be used to store example files demonstrating usage of this SDK.
-It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
\ No newline at end of file
diff --git a/orb-java-client-okhttp/build.gradle.kts b/orb-java-client-okhttp/build.gradle.kts
index 856cade40..08c23763b 100644
--- a/orb-java-client-okhttp/build.gradle.kts
+++ b/orb-java-client-okhttp/build.gradle.kts
@@ -7,9 +7,8 @@ dependencies {
api(project(":orb-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/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OkHttpClient.kt b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OkHttpClient.kt
index 4d54698c3..1f736002f 100644
--- a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OkHttpClient.kt
+++ b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OkHttpClient.kt
@@ -8,20 +8,26 @@ import com.withorb.api.core.http.HttpMethod
import com.withorb.api.core.http.HttpRequest
import com.withorb.api.core.http.HttpRequestBody
import com.withorb.api.core.http.HttpResponse
+import com.withorb.api.core.http.ProxyAuthenticator
import com.withorb.api.errors.OrbIoException
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
@@ -29,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 OrbIoException("Request failed", e)
} finally {
@@ -57,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) {
@@ -85,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("ORB_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())
@@ -109,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()
}
@@ -200,6 +112,9 @@ 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
@@ -211,6 +126,32 @@ 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
}
@@ -238,8 +179,37 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
.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) {
@@ -261,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/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClient.kt b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClient.kt
index efc83cf9b..90fc352e1 100644
--- a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClient.kt
+++ b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClient.kt
@@ -6,11 +6,13 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.withorb.api.client.OrbClient
import com.withorb.api.client.OrbClientImpl
import com.withorb.api.core.ClientOptions
+import com.withorb.api.core.LogLevel
import com.withorb.api.core.Sleeper
import com.withorb.api.core.Timeout
import com.withorb.api.core.http.AsyncStreamResponse
import com.withorb.api.core.http.Headers
import com.withorb.api.core.http.HttpClient
+import com.withorb.api.core.http.ProxyAuthenticator
import com.withorb.api.core.http.QueryParams
import com.withorb.api.core.jsonMapper
import java.net.Proxy
@@ -49,6 +51,9 @@ class OrbOkHttpClient private 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
@@ -77,6 +82,60 @@ class OrbOkHttpClient private constructor() {
/** 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.
*
@@ -188,6 +247,9 @@ class OrbOkHttpClient 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.
*/
@@ -229,6 +291,15 @@ class OrbOkHttpClient private constructor() {
*/
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+ /**
+ * 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) }
+
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
fun webhookSecret(webhookSecret: String?) = apply {
@@ -338,6 +409,9 @@ class OrbOkHttpClient private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
+ .proxyAuthenticator(proxyAuthenticator)
+ .maxIdleConnections(maxIdleConnections)
+ .keepAliveDuration(keepAliveDuration)
.dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
diff --git a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClientAsync.kt b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClientAsync.kt
index 0a71facec..c8a415906 100644
--- a/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClientAsync.kt
+++ b/orb-java-client-okhttp/src/main/kotlin/com/withorb/api/client/okhttp/OrbOkHttpClientAsync.kt
@@ -6,11 +6,13 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.withorb.api.client.OrbClientAsync
import com.withorb.api.client.OrbClientAsyncImpl
import com.withorb.api.core.ClientOptions
+import com.withorb.api.core.LogLevel
import com.withorb.api.core.Sleeper
import com.withorb.api.core.Timeout
import com.withorb.api.core.http.AsyncStreamResponse
import com.withorb.api.core.http.Headers
import com.withorb.api.core.http.HttpClient
+import com.withorb.api.core.http.ProxyAuthenticator
import com.withorb.api.core.http.QueryParams
import com.withorb.api.core.jsonMapper
import java.net.Proxy
@@ -49,6 +51,9 @@ class OrbOkHttpClientAsync private 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
@@ -77,6 +82,60 @@ class OrbOkHttpClientAsync private constructor() {
/** 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.
*
@@ -188,6 +247,9 @@ class OrbOkHttpClientAsync 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.
*/
@@ -229,6 +291,15 @@ class OrbOkHttpClientAsync private constructor() {
*/
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+ /**
+ * 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) }
+
fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
fun webhookSecret(webhookSecret: String?) = apply {
@@ -338,6 +409,9 @@ class OrbOkHttpClientAsync private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
+ .proxyAuthenticator(proxyAuthenticator)
+ .maxIdleConnections(maxIdleConnections)
+ .keepAliveDuration(keepAliveDuration)
.dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
diff --git a/orb-java-core/build.gradle.kts b/orb-java-core/build.gradle.kts
index 086a58f55..72660d18e 100644
--- a/orb-java-core/build.gradle.kts
+++ b/orb-java-core/build.gradle.kts
@@ -27,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(":orb-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/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClient.kt b/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClient.kt
index b3cc3efcc..6a41f268b 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClient.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClient.kt
@@ -14,6 +14,8 @@ import com.withorb.api.services.blocking.EventService
import com.withorb.api.services.blocking.InvoiceLineItemService
import com.withorb.api.services.blocking.InvoiceService
import com.withorb.api.services.blocking.ItemService
+import com.withorb.api.services.blocking.LicenseService
+import com.withorb.api.services.blocking.LicenseTypeService
import com.withorb.api.services.blocking.MetricService
import com.withorb.api.services.blocking.PlanService
import com.withorb.api.services.blocking.PriceService
@@ -61,40 +63,137 @@ interface OrbClient {
fun topLevel(): TopLevelService
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be subscribed
+ * to by a customer. Plans define the billing behavior of the subscription. You can see more
+ * about how to configure prices in the [Price resource](/reference/price).
+ */
fun beta(): BetaService
+ /**
+ * A coupon represents a reusable discount configuration that can be applied either as a fixed
+ * or percentage amount to an invoice or subscription. Coupons are activated using a redemption
+ * code, which applies the discount to a subscription or invoice. The duration of a coupon
+ * determines how long it remains available for use by end users.
+ */
fun coupons(): CouponService
+ /**
+ * The [Credit Note](/invoicing/credit-notes) resource represents a credit that has been applied
+ * to a particular invoice.
+ */
fun creditNotes(): CreditNoteService
+ /**
+ * A customer is a buyer of your products, and the other party to the billing relationship.
+ *
+ * In Orb, customers are assigned system generated identifiers automatically, but it's often
+ * desirable to have these match existing identifiers in your system. To avoid having to
+ * denormalize Orb ID information, you can pass in an `external_customer_id` with your own
+ * identifier. See [Customer ID Aliases](/events-and-metrics/customer-aliases) for further
+ * information about how these aliases work in Orb.
+ *
+ * In addition to having an identifier in your system, a customer may exist in a payment
+ * provider solution like Stripe. Use the `payment_provider_id` and the `payment_provider` enum
+ * field to express this mapping.
+ *
+ * A customer also has a timezone (from the standard
+ * [IANA timezone database](https://www.iana.org/time-zones)), which defaults to your account's
+ * timezone. See [Timezone localization](/essentials/timezones) for information on what this
+ * timezone parameter influences within Orb.
+ */
fun customers(): CustomerService
+ /**
+ * The [Event](/core-concepts#event) resource represents a usage event that has been created for
+ * a customer. Events are the core of Orb's usage-based billing model, and are used to calculate
+ * the usage charges for a given billing period.
+ */
fun events(): EventService
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of an
+ * action, such as a cancellation.
+ */
fun invoiceLineItems(): InvoiceLineItemService
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of an
+ * action, such as a cancellation.
+ */
fun invoices(): InvoiceService
+ /**
+ * The Item resource represents a sellable product or good. Items are associated with all line
+ * items, billable metrics, and prices and are used for defining external sync behavior for
+ * invoices and tax calculation purposes.
+ */
fun items(): ItemService
+ /**
+ * The Metric resource represents a calculation of a quantity based on events. Metrics are
+ * defined by the query that transforms raw usage events into meaningful values for your
+ * customers.
+ */
fun metrics(): MetricService
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be subscribed
+ * to by a customer. Plans define the billing behavior of the subscription. You can see more
+ * about how to configure prices in the [Price resource](/reference/price).
+ */
fun plans(): PlanService
+ /**
+ * The Price resource represents a price that can be billed on a subscription, resulting in a
+ * charge on an invoice in the form of an invoice line item. Prices take a quantity and
+ * determine an amount to bill.
+ *
+ * Orb supports a few different pricing models out of the box. Each of these models is
+ * serialized differently in a given Price object. The model_type field determines the key for
+ * the configuration object that is present.
+ *
+ * For more on the types of prices, see
+ * [the core concepts documentation](/core-concepts#plan-and-price)
+ */
fun prices(): PriceService
fun subscriptions(): SubscriptionService
fun webhooks(): WebhookService
+ /**
+ * [Alerts within Orb](/product-catalog/configuring-alerts) monitor spending, usage, or credit
+ * balance and trigger webhooks when a threshold is exceeded.
+ *
+ * Alerts created through the API can be scoped to either customers or subscriptions.
+ */
fun alerts(): AlertService
fun dimensionalPriceGroups(): DimensionalPriceGroupService
fun subscriptionChanges(): SubscriptionChangeService
+ /**
+ * The [Credit Ledger Entry resource](/product-catalog/prepurchase) models prepaid credits
+ * within Orb.
+ */
fun creditBlocks(): CreditBlockService
+ /**
+ * The LicenseType resource represents a type of license that can be assigned to users. License
+ * types are used during billing by grouping metrics on the configured grouping key.
+ */
+ fun licenseTypes(): LicenseTypeService
+
+ fun licenses(): LicenseService
+
/**
* Closes this client, relinquishing any underlying resources.
*
@@ -120,36 +219,133 @@ interface OrbClient {
fun topLevel(): TopLevelService.WithRawResponse
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be
+ * subscribed to by a customer. Plans define the billing behavior of the subscription. You
+ * can see more about how to configure prices in the [Price resource](/reference/price).
+ */
fun beta(): BetaService.WithRawResponse
+ /**
+ * A coupon represents a reusable discount configuration that can be applied either as a
+ * fixed or percentage amount to an invoice or subscription. Coupons are activated using a
+ * redemption code, which applies the discount to a subscription or invoice. The duration of
+ * a coupon determines how long it remains available for use by end users.
+ */
fun coupons(): CouponService.WithRawResponse
+ /**
+ * The [Credit Note](/invoicing/credit-notes) resource represents a credit that has been
+ * applied to a particular invoice.
+ */
fun creditNotes(): CreditNoteService.WithRawResponse
+ /**
+ * A customer is a buyer of your products, and the other party to the billing relationship.
+ *
+ * In Orb, customers are assigned system generated identifiers automatically, but it's often
+ * desirable to have these match existing identifiers in your system. To avoid having to
+ * denormalize Orb ID information, you can pass in an `external_customer_id` with your own
+ * identifier. See [Customer ID Aliases](/events-and-metrics/customer-aliases) for further
+ * information about how these aliases work in Orb.
+ *
+ * In addition to having an identifier in your system, a customer may exist in a payment
+ * provider solution like Stripe. Use the `payment_provider_id` and the `payment_provider`
+ * enum field to express this mapping.
+ *
+ * A customer also has a timezone (from the standard
+ * [IANA timezone database](https://www.iana.org/time-zones)), which defaults to your
+ * account's timezone. See [Timezone localization](/essentials/timezones) for information on
+ * what this timezone parameter influences within Orb.
+ */
fun customers(): CustomerService.WithRawResponse
+ /**
+ * The [Event](/core-concepts#event) resource represents a usage event that has been created
+ * for a customer. Events are the core of Orb's usage-based billing model, and are used to
+ * calculate the usage charges for a given billing period.
+ */
fun events(): EventService.WithRawResponse
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of
+ * an action, such as a cancellation.
+ */
fun invoiceLineItems(): InvoiceLineItemService.WithRawResponse
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of
+ * an action, such as a cancellation.
+ */
fun invoices(): InvoiceService.WithRawResponse
+ /**
+ * The Item resource represents a sellable product or good. Items are associated with all
+ * line items, billable metrics, and prices and are used for defining external sync behavior
+ * for invoices and tax calculation purposes.
+ */
fun items(): ItemService.WithRawResponse
+ /**
+ * The Metric resource represents a calculation of a quantity based on events. Metrics are
+ * defined by the query that transforms raw usage events into meaningful values for your
+ * customers.
+ */
fun metrics(): MetricService.WithRawResponse
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be
+ * subscribed to by a customer. Plans define the billing behavior of the subscription. You
+ * can see more about how to configure prices in the [Price resource](/reference/price).
+ */
fun plans(): PlanService.WithRawResponse
+ /**
+ * The Price resource represents a price that can be billed on a subscription, resulting in
+ * a charge on an invoice in the form of an invoice line item. Prices take a quantity and
+ * determine an amount to bill.
+ *
+ * Orb supports a few different pricing models out of the box. Each of these models is
+ * serialized differently in a given Price object. The model_type field determines the key
+ * for the configuration object that is present.
+ *
+ * For more on the types of prices, see
+ * [the core concepts documentation](/core-concepts#plan-and-price)
+ */
fun prices(): PriceService.WithRawResponse
fun subscriptions(): SubscriptionService.WithRawResponse
+ /**
+ * [Alerts within Orb](/product-catalog/configuring-alerts) monitor spending, usage, or
+ * credit balance and trigger webhooks when a threshold is exceeded.
+ *
+ * Alerts created through the API can be scoped to either customers or subscriptions.
+ */
fun alerts(): AlertService.WithRawResponse
fun dimensionalPriceGroups(): DimensionalPriceGroupService.WithRawResponse
fun subscriptionChanges(): SubscriptionChangeService.WithRawResponse
+ /**
+ * The [Credit Ledger Entry resource](/product-catalog/prepurchase) models prepaid credits
+ * within Orb.
+ */
fun creditBlocks(): CreditBlockService.WithRawResponse
+
+ /**
+ * The LicenseType resource represents a type of license that can be assigned to users.
+ * License types are used during billing by grouping metrics on the configured grouping key.
+ */
+ fun licenseTypes(): LicenseTypeService.WithRawResponse
+
+ fun licenses(): LicenseService.WithRawResponse
}
}
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientAsync.kt b/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientAsync.kt
index a8b535120..3857f7297 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientAsync.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientAsync.kt
@@ -14,6 +14,8 @@ import com.withorb.api.services.async.EventServiceAsync
import com.withorb.api.services.async.InvoiceLineItemServiceAsync
import com.withorb.api.services.async.InvoiceServiceAsync
import com.withorb.api.services.async.ItemServiceAsync
+import com.withorb.api.services.async.LicenseServiceAsync
+import com.withorb.api.services.async.LicenseTypeServiceAsync
import com.withorb.api.services.async.MetricServiceAsync
import com.withorb.api.services.async.PlanServiceAsync
import com.withorb.api.services.async.PriceServiceAsync
@@ -60,38 +62,135 @@ interface OrbClientAsync {
fun topLevel(): TopLevelServiceAsync
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be subscribed
+ * to by a customer. Plans define the billing behavior of the subscription. You can see more
+ * about how to configure prices in the [Price resource](/reference/price).
+ */
fun beta(): BetaServiceAsync
+ /**
+ * A coupon represents a reusable discount configuration that can be applied either as a fixed
+ * or percentage amount to an invoice or subscription. Coupons are activated using a redemption
+ * code, which applies the discount to a subscription or invoice. The duration of a coupon
+ * determines how long it remains available for use by end users.
+ */
fun coupons(): CouponServiceAsync
+ /**
+ * The [Credit Note](/invoicing/credit-notes) resource represents a credit that has been applied
+ * to a particular invoice.
+ */
fun creditNotes(): CreditNoteServiceAsync
+ /**
+ * A customer is a buyer of your products, and the other party to the billing relationship.
+ *
+ * In Orb, customers are assigned system generated identifiers automatically, but it's often
+ * desirable to have these match existing identifiers in your system. To avoid having to
+ * denormalize Orb ID information, you can pass in an `external_customer_id` with your own
+ * identifier. See [Customer ID Aliases](/events-and-metrics/customer-aliases) for further
+ * information about how these aliases work in Orb.
+ *
+ * In addition to having an identifier in your system, a customer may exist in a payment
+ * provider solution like Stripe. Use the `payment_provider_id` and the `payment_provider` enum
+ * field to express this mapping.
+ *
+ * A customer also has a timezone (from the standard
+ * [IANA timezone database](https://www.iana.org/time-zones)), which defaults to your account's
+ * timezone. See [Timezone localization](/essentials/timezones) for information on what this
+ * timezone parameter influences within Orb.
+ */
fun customers(): CustomerServiceAsync
+ /**
+ * The [Event](/core-concepts#event) resource represents a usage event that has been created for
+ * a customer. Events are the core of Orb's usage-based billing model, and are used to calculate
+ * the usage charges for a given billing period.
+ */
fun events(): EventServiceAsync
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of an
+ * action, such as a cancellation.
+ */
fun invoiceLineItems(): InvoiceLineItemServiceAsync
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of an
+ * action, such as a cancellation.
+ */
fun invoices(): InvoiceServiceAsync
+ /**
+ * The Item resource represents a sellable product or good. Items are associated with all line
+ * items, billable metrics, and prices and are used for defining external sync behavior for
+ * invoices and tax calculation purposes.
+ */
fun items(): ItemServiceAsync
+ /**
+ * The Metric resource represents a calculation of a quantity based on events. Metrics are
+ * defined by the query that transforms raw usage events into meaningful values for your
+ * customers.
+ */
fun metrics(): MetricServiceAsync
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be subscribed
+ * to by a customer. Plans define the billing behavior of the subscription. You can see more
+ * about how to configure prices in the [Price resource](/reference/price).
+ */
fun plans(): PlanServiceAsync
+ /**
+ * The Price resource represents a price that can be billed on a subscription, resulting in a
+ * charge on an invoice in the form of an invoice line item. Prices take a quantity and
+ * determine an amount to bill.
+ *
+ * Orb supports a few different pricing models out of the box. Each of these models is
+ * serialized differently in a given Price object. The model_type field determines the key for
+ * the configuration object that is present.
+ *
+ * For more on the types of prices, see
+ * [the core concepts documentation](/core-concepts#plan-and-price)
+ */
fun prices(): PriceServiceAsync
fun subscriptions(): SubscriptionServiceAsync
+ /**
+ * [Alerts within Orb](/product-catalog/configuring-alerts) monitor spending, usage, or credit
+ * balance and trigger webhooks when a threshold is exceeded.
+ *
+ * Alerts created through the API can be scoped to either customers or subscriptions.
+ */
fun alerts(): AlertServiceAsync
fun dimensionalPriceGroups(): DimensionalPriceGroupServiceAsync
fun subscriptionChanges(): SubscriptionChangeServiceAsync
+ /**
+ * The [Credit Ledger Entry resource](/product-catalog/prepurchase) models prepaid credits
+ * within Orb.
+ */
fun creditBlocks(): CreditBlockServiceAsync
+ /**
+ * The LicenseType resource represents a type of license that can be assigned to users. License
+ * types are used during billing by grouping metrics on the configured grouping key.
+ */
+ fun licenseTypes(): LicenseTypeServiceAsync
+
+ fun licenses(): LicenseServiceAsync
+
/**
* Closes this client, relinquishing any underlying resources.
*
@@ -117,36 +216,133 @@ interface OrbClientAsync {
fun topLevel(): TopLevelServiceAsync.WithRawResponse
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be
+ * subscribed to by a customer. Plans define the billing behavior of the subscription. You
+ * can see more about how to configure prices in the [Price resource](/reference/price).
+ */
fun beta(): BetaServiceAsync.WithRawResponse
+ /**
+ * A coupon represents a reusable discount configuration that can be applied either as a
+ * fixed or percentage amount to an invoice or subscription. Coupons are activated using a
+ * redemption code, which applies the discount to a subscription or invoice. The duration of
+ * a coupon determines how long it remains available for use by end users.
+ */
fun coupons(): CouponServiceAsync.WithRawResponse
+ /**
+ * The [Credit Note](/invoicing/credit-notes) resource represents a credit that has been
+ * applied to a particular invoice.
+ */
fun creditNotes(): CreditNoteServiceAsync.WithRawResponse
+ /**
+ * A customer is a buyer of your products, and the other party to the billing relationship.
+ *
+ * In Orb, customers are assigned system generated identifiers automatically, but it's often
+ * desirable to have these match existing identifiers in your system. To avoid having to
+ * denormalize Orb ID information, you can pass in an `external_customer_id` with your own
+ * identifier. See [Customer ID Aliases](/events-and-metrics/customer-aliases) for further
+ * information about how these aliases work in Orb.
+ *
+ * In addition to having an identifier in your system, a customer may exist in a payment
+ * provider solution like Stripe. Use the `payment_provider_id` and the `payment_provider`
+ * enum field to express this mapping.
+ *
+ * A customer also has a timezone (from the standard
+ * [IANA timezone database](https://www.iana.org/time-zones)), which defaults to your
+ * account's timezone. See [Timezone localization](/essentials/timezones) for information on
+ * what this timezone parameter influences within Orb.
+ */
fun customers(): CustomerServiceAsync.WithRawResponse
+ /**
+ * The [Event](/core-concepts#event) resource represents a usage event that has been created
+ * for a customer. Events are the core of Orb's usage-based billing model, and are used to
+ * calculate the usage charges for a given billing period.
+ */
fun events(): EventServiceAsync.WithRawResponse
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of
+ * an action, such as a cancellation.
+ */
fun invoiceLineItems(): InvoiceLineItemServiceAsync.WithRawResponse
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of
+ * an action, such as a cancellation.
+ */
fun invoices(): InvoiceServiceAsync.WithRawResponse
+ /**
+ * The Item resource represents a sellable product or good. Items are associated with all
+ * line items, billable metrics, and prices and are used for defining external sync behavior
+ * for invoices and tax calculation purposes.
+ */
fun items(): ItemServiceAsync.WithRawResponse
+ /**
+ * The Metric resource represents a calculation of a quantity based on events. Metrics are
+ * defined by the query that transforms raw usage events into meaningful values for your
+ * customers.
+ */
fun metrics(): MetricServiceAsync.WithRawResponse
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be
+ * subscribed to by a customer. Plans define the billing behavior of the subscription. You
+ * can see more about how to configure prices in the [Price resource](/reference/price).
+ */
fun plans(): PlanServiceAsync.WithRawResponse
+ /**
+ * The Price resource represents a price that can be billed on a subscription, resulting in
+ * a charge on an invoice in the form of an invoice line item. Prices take a quantity and
+ * determine an amount to bill.
+ *
+ * Orb supports a few different pricing models out of the box. Each of these models is
+ * serialized differently in a given Price object. The model_type field determines the key
+ * for the configuration object that is present.
+ *
+ * For more on the types of prices, see
+ * [the core concepts documentation](/core-concepts#plan-and-price)
+ */
fun prices(): PriceServiceAsync.WithRawResponse
fun subscriptions(): SubscriptionServiceAsync.WithRawResponse
+ /**
+ * [Alerts within Orb](/product-catalog/configuring-alerts) monitor spending, usage, or
+ * credit balance and trigger webhooks when a threshold is exceeded.
+ *
+ * Alerts created through the API can be scoped to either customers or subscriptions.
+ */
fun alerts(): AlertServiceAsync.WithRawResponse
fun dimensionalPriceGroups(): DimensionalPriceGroupServiceAsync.WithRawResponse
fun subscriptionChanges(): SubscriptionChangeServiceAsync.WithRawResponse
+ /**
+ * The [Credit Ledger Entry resource](/product-catalog/prepurchase) models prepaid credits
+ * within Orb.
+ */
fun creditBlocks(): CreditBlockServiceAsync.WithRawResponse
+
+ /**
+ * The LicenseType resource represents a type of license that can be assigned to users.
+ * License types are used during billing by grouping metrics on the configured grouping key.
+ */
+ fun licenseTypes(): LicenseTypeServiceAsync.WithRawResponse
+
+ fun licenses(): LicenseServiceAsync.WithRawResponse
}
}
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientAsyncImpl.kt b/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientAsyncImpl.kt
index ba4b5c44a..d20aa3a60 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientAsyncImpl.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientAsyncImpl.kt
@@ -26,6 +26,10 @@ import com.withorb.api.services.async.InvoiceServiceAsync
import com.withorb.api.services.async.InvoiceServiceAsyncImpl
import com.withorb.api.services.async.ItemServiceAsync
import com.withorb.api.services.async.ItemServiceAsyncImpl
+import com.withorb.api.services.async.LicenseServiceAsync
+import com.withorb.api.services.async.LicenseServiceAsyncImpl
+import com.withorb.api.services.async.LicenseTypeServiceAsync
+import com.withorb.api.services.async.LicenseTypeServiceAsyncImpl
import com.withorb.api.services.async.MetricServiceAsync
import com.withorb.api.services.async.MetricServiceAsyncImpl
import com.withorb.api.services.async.PlanServiceAsync
@@ -119,6 +123,14 @@ class OrbClientAsyncImpl(private val clientOptions: ClientOptions) : OrbClientAs
CreditBlockServiceAsyncImpl(clientOptionsWithUserAgent)
}
+ private val licenseTypes: LicenseTypeServiceAsync by lazy {
+ LicenseTypeServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val licenses: LicenseServiceAsync by lazy {
+ LicenseServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
override fun sync(): OrbClient = sync
override fun withRawResponse(): OrbClientAsync.WithRawResponse = withRawResponse
@@ -128,30 +140,115 @@ class OrbClientAsyncImpl(private val clientOptions: ClientOptions) : OrbClientAs
override fun topLevel(): TopLevelServiceAsync = topLevel
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be subscribed
+ * to by a customer. Plans define the billing behavior of the subscription. You can see more
+ * about how to configure prices in the [Price resource](/reference/price).
+ */
override fun beta(): BetaServiceAsync = beta
+ /**
+ * A coupon represents a reusable discount configuration that can be applied either as a fixed
+ * or percentage amount to an invoice or subscription. Coupons are activated using a redemption
+ * code, which applies the discount to a subscription or invoice. The duration of a coupon
+ * determines how long it remains available for use by end users.
+ */
override fun coupons(): CouponServiceAsync = coupons
+ /**
+ * The [Credit Note](/invoicing/credit-notes) resource represents a credit that has been applied
+ * to a particular invoice.
+ */
override fun creditNotes(): CreditNoteServiceAsync = creditNotes
+ /**
+ * A customer is a buyer of your products, and the other party to the billing relationship.
+ *
+ * In Orb, customers are assigned system generated identifiers automatically, but it's often
+ * desirable to have these match existing identifiers in your system. To avoid having to
+ * denormalize Orb ID information, you can pass in an `external_customer_id` with your own
+ * identifier. See [Customer ID Aliases](/events-and-metrics/customer-aliases) for further
+ * information about how these aliases work in Orb.
+ *
+ * In addition to having an identifier in your system, a customer may exist in a payment
+ * provider solution like Stripe. Use the `payment_provider_id` and the `payment_provider` enum
+ * field to express this mapping.
+ *
+ * A customer also has a timezone (from the standard
+ * [IANA timezone database](https://www.iana.org/time-zones)), which defaults to your account's
+ * timezone. See [Timezone localization](/essentials/timezones) for information on what this
+ * timezone parameter influences within Orb.
+ */
override fun customers(): CustomerServiceAsync = customers
+ /**
+ * The [Event](/core-concepts#event) resource represents a usage event that has been created for
+ * a customer. Events are the core of Orb's usage-based billing model, and are used to calculate
+ * the usage charges for a given billing period.
+ */
override fun events(): EventServiceAsync = events
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of an
+ * action, such as a cancellation.
+ */
override fun invoiceLineItems(): InvoiceLineItemServiceAsync = invoiceLineItems
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of an
+ * action, such as a cancellation.
+ */
override fun invoices(): InvoiceServiceAsync = invoices
+ /**
+ * The Item resource represents a sellable product or good. Items are associated with all line
+ * items, billable metrics, and prices and are used for defining external sync behavior for
+ * invoices and tax calculation purposes.
+ */
override fun items(): ItemServiceAsync = items
+ /**
+ * The Metric resource represents a calculation of a quantity based on events. Metrics are
+ * defined by the query that transforms raw usage events into meaningful values for your
+ * customers.
+ */
override fun metrics(): MetricServiceAsync = metrics
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be subscribed
+ * to by a customer. Plans define the billing behavior of the subscription. You can see more
+ * about how to configure prices in the [Price resource](/reference/price).
+ */
override fun plans(): PlanServiceAsync = plans
+ /**
+ * The Price resource represents a price that can be billed on a subscription, resulting in a
+ * charge on an invoice in the form of an invoice line item. Prices take a quantity and
+ * determine an amount to bill.
+ *
+ * Orb supports a few different pricing models out of the box. Each of these models is
+ * serialized differently in a given Price object. The model_type field determines the key for
+ * the configuration object that is present.
+ *
+ * For more on the types of prices, see
+ * [the core concepts documentation](/core-concepts#plan-and-price)
+ */
override fun prices(): PriceServiceAsync = prices
override fun subscriptions(): SubscriptionServiceAsync = subscriptions
+ /**
+ * [Alerts within Orb](/product-catalog/configuring-alerts) monitor spending, usage, or credit
+ * balance and trigger webhooks when a threshold is exceeded.
+ *
+ * Alerts created through the API can be scoped to either customers or subscriptions.
+ */
override fun alerts(): AlertServiceAsync = alerts
override fun dimensionalPriceGroups(): DimensionalPriceGroupServiceAsync =
@@ -159,8 +256,20 @@ class OrbClientAsyncImpl(private val clientOptions: ClientOptions) : OrbClientAs
override fun subscriptionChanges(): SubscriptionChangeServiceAsync = subscriptionChanges
+ /**
+ * The [Credit Ledger Entry resource](/product-catalog/prepurchase) models prepaid credits
+ * within Orb.
+ */
override fun creditBlocks(): CreditBlockServiceAsync = creditBlocks
+ /**
+ * The LicenseType resource represents a type of license that can be assigned to users. License
+ * types are used during billing by grouping metrics on the configured grouping key.
+ */
+ override fun licenseTypes(): LicenseTypeServiceAsync = licenseTypes
+
+ override fun licenses(): LicenseServiceAsync = licenses
+
override fun close() = clientOptions.close()
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
@@ -235,6 +344,14 @@ class OrbClientAsyncImpl(private val clientOptions: ClientOptions) : OrbClientAs
CreditBlockServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
+ private val licenseTypes: LicenseTypeServiceAsync.WithRawResponse by lazy {
+ LicenseTypeServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val licenses: LicenseServiceAsync.WithRawResponse by lazy {
+ LicenseServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
override fun withOptions(
modifier: Consumer
): OrbClientAsync.WithRawResponse =
@@ -244,31 +361,116 @@ class OrbClientAsyncImpl(private val clientOptions: ClientOptions) : OrbClientAs
override fun topLevel(): TopLevelServiceAsync.WithRawResponse = topLevel
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be
+ * subscribed to by a customer. Plans define the billing behavior of the subscription. You
+ * can see more about how to configure prices in the [Price resource](/reference/price).
+ */
override fun beta(): BetaServiceAsync.WithRawResponse = beta
+ /**
+ * A coupon represents a reusable discount configuration that can be applied either as a
+ * fixed or percentage amount to an invoice or subscription. Coupons are activated using a
+ * redemption code, which applies the discount to a subscription or invoice. The duration of
+ * a coupon determines how long it remains available for use by end users.
+ */
override fun coupons(): CouponServiceAsync.WithRawResponse = coupons
+ /**
+ * The [Credit Note](/invoicing/credit-notes) resource represents a credit that has been
+ * applied to a particular invoice.
+ */
override fun creditNotes(): CreditNoteServiceAsync.WithRawResponse = creditNotes
+ /**
+ * A customer is a buyer of your products, and the other party to the billing relationship.
+ *
+ * In Orb, customers are assigned system generated identifiers automatically, but it's often
+ * desirable to have these match existing identifiers in your system. To avoid having to
+ * denormalize Orb ID information, you can pass in an `external_customer_id` with your own
+ * identifier. See [Customer ID Aliases](/events-and-metrics/customer-aliases) for further
+ * information about how these aliases work in Orb.
+ *
+ * In addition to having an identifier in your system, a customer may exist in a payment
+ * provider solution like Stripe. Use the `payment_provider_id` and the `payment_provider`
+ * enum field to express this mapping.
+ *
+ * A customer also has a timezone (from the standard
+ * [IANA timezone database](https://www.iana.org/time-zones)), which defaults to your
+ * account's timezone. See [Timezone localization](/essentials/timezones) for information on
+ * what this timezone parameter influences within Orb.
+ */
override fun customers(): CustomerServiceAsync.WithRawResponse = customers
+ /**
+ * The [Event](/core-concepts#event) resource represents a usage event that has been created
+ * for a customer. Events are the core of Orb's usage-based billing model, and are used to
+ * calculate the usage charges for a given billing period.
+ */
override fun events(): EventServiceAsync.WithRawResponse = events
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of
+ * an action, such as a cancellation.
+ */
override fun invoiceLineItems(): InvoiceLineItemServiceAsync.WithRawResponse =
invoiceLineItems
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of
+ * an action, such as a cancellation.
+ */
override fun invoices(): InvoiceServiceAsync.WithRawResponse = invoices
+ /**
+ * The Item resource represents a sellable product or good. Items are associated with all
+ * line items, billable metrics, and prices and are used for defining external sync behavior
+ * for invoices and tax calculation purposes.
+ */
override fun items(): ItemServiceAsync.WithRawResponse = items
+ /**
+ * The Metric resource represents a calculation of a quantity based on events. Metrics are
+ * defined by the query that transforms raw usage events into meaningful values for your
+ * customers.
+ */
override fun metrics(): MetricServiceAsync.WithRawResponse = metrics
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be
+ * subscribed to by a customer. Plans define the billing behavior of the subscription. You
+ * can see more about how to configure prices in the [Price resource](/reference/price).
+ */
override fun plans(): PlanServiceAsync.WithRawResponse = plans
+ /**
+ * The Price resource represents a price that can be billed on a subscription, resulting in
+ * a charge on an invoice in the form of an invoice line item. Prices take a quantity and
+ * determine an amount to bill.
+ *
+ * Orb supports a few different pricing models out of the box. Each of these models is
+ * serialized differently in a given Price object. The model_type field determines the key
+ * for the configuration object that is present.
+ *
+ * For more on the types of prices, see
+ * [the core concepts documentation](/core-concepts#plan-and-price)
+ */
override fun prices(): PriceServiceAsync.WithRawResponse = prices
override fun subscriptions(): SubscriptionServiceAsync.WithRawResponse = subscriptions
+ /**
+ * [Alerts within Orb](/product-catalog/configuring-alerts) monitor spending, usage, or
+ * credit balance and trigger webhooks when a threshold is exceeded.
+ *
+ * Alerts created through the API can be scoped to either customers or subscriptions.
+ */
override fun alerts(): AlertServiceAsync.WithRawResponse = alerts
override fun dimensionalPriceGroups(): DimensionalPriceGroupServiceAsync.WithRawResponse =
@@ -277,6 +479,18 @@ class OrbClientAsyncImpl(private val clientOptions: ClientOptions) : OrbClientAs
override fun subscriptionChanges(): SubscriptionChangeServiceAsync.WithRawResponse =
subscriptionChanges
+ /**
+ * The [Credit Ledger Entry resource](/product-catalog/prepurchase) models prepaid credits
+ * within Orb.
+ */
override fun creditBlocks(): CreditBlockServiceAsync.WithRawResponse = creditBlocks
+
+ /**
+ * The LicenseType resource represents a type of license that can be assigned to users.
+ * License types are used during billing by grouping metrics on the configured grouping key.
+ */
+ override fun licenseTypes(): LicenseTypeServiceAsync.WithRawResponse = licenseTypes
+
+ override fun licenses(): LicenseServiceAsync.WithRawResponse = licenses
}
}
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientImpl.kt b/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientImpl.kt
index 4fa187d47..a39b97ca1 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientImpl.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/client/OrbClientImpl.kt
@@ -26,6 +26,10 @@ import com.withorb.api.services.blocking.InvoiceService
import com.withorb.api.services.blocking.InvoiceServiceImpl
import com.withorb.api.services.blocking.ItemService
import com.withorb.api.services.blocking.ItemServiceImpl
+import com.withorb.api.services.blocking.LicenseService
+import com.withorb.api.services.blocking.LicenseServiceImpl
+import com.withorb.api.services.blocking.LicenseTypeService
+import com.withorb.api.services.blocking.LicenseTypeServiceImpl
import com.withorb.api.services.blocking.MetricService
import com.withorb.api.services.blocking.MetricServiceImpl
import com.withorb.api.services.blocking.PlanService
@@ -111,6 +115,12 @@ class OrbClientImpl(private val clientOptions: ClientOptions) : OrbClient {
CreditBlockServiceImpl(clientOptionsWithUserAgent)
}
+ private val licenseTypes: LicenseTypeService by lazy {
+ LicenseTypeServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val licenses: LicenseService by lazy { LicenseServiceImpl(clientOptionsWithUserAgent) }
+
override fun async(): OrbClientAsync = async
override fun withRawResponse(): OrbClient.WithRawResponse = withRawResponse
@@ -120,40 +130,137 @@ class OrbClientImpl(private val clientOptions: ClientOptions) : OrbClient {
override fun topLevel(): TopLevelService = topLevel
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be subscribed
+ * to by a customer. Plans define the billing behavior of the subscription. You can see more
+ * about how to configure prices in the [Price resource](/reference/price).
+ */
override fun beta(): BetaService = beta
+ /**
+ * A coupon represents a reusable discount configuration that can be applied either as a fixed
+ * or percentage amount to an invoice or subscription. Coupons are activated using a redemption
+ * code, which applies the discount to a subscription or invoice. The duration of a coupon
+ * determines how long it remains available for use by end users.
+ */
override fun coupons(): CouponService = coupons
+ /**
+ * The [Credit Note](/invoicing/credit-notes) resource represents a credit that has been applied
+ * to a particular invoice.
+ */
override fun creditNotes(): CreditNoteService = creditNotes
+ /**
+ * A customer is a buyer of your products, and the other party to the billing relationship.
+ *
+ * In Orb, customers are assigned system generated identifiers automatically, but it's often
+ * desirable to have these match existing identifiers in your system. To avoid having to
+ * denormalize Orb ID information, you can pass in an `external_customer_id` with your own
+ * identifier. See [Customer ID Aliases](/events-and-metrics/customer-aliases) for further
+ * information about how these aliases work in Orb.
+ *
+ * In addition to having an identifier in your system, a customer may exist in a payment
+ * provider solution like Stripe. Use the `payment_provider_id` and the `payment_provider` enum
+ * field to express this mapping.
+ *
+ * A customer also has a timezone (from the standard
+ * [IANA timezone database](https://www.iana.org/time-zones)), which defaults to your account's
+ * timezone. See [Timezone localization](/essentials/timezones) for information on what this
+ * timezone parameter influences within Orb.
+ */
override fun customers(): CustomerService = customers
+ /**
+ * The [Event](/core-concepts#event) resource represents a usage event that has been created for
+ * a customer. Events are the core of Orb's usage-based billing model, and are used to calculate
+ * the usage charges for a given billing period.
+ */
override fun events(): EventService = events
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of an
+ * action, such as a cancellation.
+ */
override fun invoiceLineItems(): InvoiceLineItemService = invoiceLineItems
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of an
+ * action, such as a cancellation.
+ */
override fun invoices(): InvoiceService = invoices
+ /**
+ * The Item resource represents a sellable product or good. Items are associated with all line
+ * items, billable metrics, and prices and are used for defining external sync behavior for
+ * invoices and tax calculation purposes.
+ */
override fun items(): ItemService = items
+ /**
+ * The Metric resource represents a calculation of a quantity based on events. Metrics are
+ * defined by the query that transforms raw usage events into meaningful values for your
+ * customers.
+ */
override fun metrics(): MetricService = metrics
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be subscribed
+ * to by a customer. Plans define the billing behavior of the subscription. You can see more
+ * about how to configure prices in the [Price resource](/reference/price).
+ */
override fun plans(): PlanService = plans
+ /**
+ * The Price resource represents a price that can be billed on a subscription, resulting in a
+ * charge on an invoice in the form of an invoice line item. Prices take a quantity and
+ * determine an amount to bill.
+ *
+ * Orb supports a few different pricing models out of the box. Each of these models is
+ * serialized differently in a given Price object. The model_type field determines the key for
+ * the configuration object that is present.
+ *
+ * For more on the types of prices, see
+ * [the core concepts documentation](/core-concepts#plan-and-price)
+ */
override fun prices(): PriceService = prices
override fun subscriptions(): SubscriptionService = subscriptions
override fun webhooks(): WebhookService = webhooks
+ /**
+ * [Alerts within Orb](/product-catalog/configuring-alerts) monitor spending, usage, or credit
+ * balance and trigger webhooks when a threshold is exceeded.
+ *
+ * Alerts created through the API can be scoped to either customers or subscriptions.
+ */
override fun alerts(): AlertService = alerts
override fun dimensionalPriceGroups(): DimensionalPriceGroupService = dimensionalPriceGroups
override fun subscriptionChanges(): SubscriptionChangeService = subscriptionChanges
+ /**
+ * The [Credit Ledger Entry resource](/product-catalog/prepurchase) models prepaid credits
+ * within Orb.
+ */
override fun creditBlocks(): CreditBlockService = creditBlocks
+ /**
+ * The LicenseType resource represents a type of license that can be assigned to users. License
+ * types are used during billing by grouping metrics on the configured grouping key.
+ */
+ override fun licenseTypes(): LicenseTypeService = licenseTypes
+
+ override fun licenses(): LicenseService = licenses
+
override fun close() = clientOptions.close()
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
@@ -227,6 +334,14 @@ class OrbClientImpl(private val clientOptions: ClientOptions) : OrbClient {
CreditBlockServiceImpl.WithRawResponseImpl(clientOptions)
}
+ private val licenseTypes: LicenseTypeService.WithRawResponse by lazy {
+ LicenseTypeServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val licenses: LicenseService.WithRawResponse by lazy {
+ LicenseServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
override fun withOptions(
modifier: Consumer
): OrbClient.WithRawResponse =
@@ -236,30 +351,115 @@ class OrbClientImpl(private val clientOptions: ClientOptions) : OrbClient {
override fun topLevel(): TopLevelService.WithRawResponse = topLevel
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be
+ * subscribed to by a customer. Plans define the billing behavior of the subscription. You
+ * can see more about how to configure prices in the [Price resource](/reference/price).
+ */
override fun beta(): BetaService.WithRawResponse = beta
+ /**
+ * A coupon represents a reusable discount configuration that can be applied either as a
+ * fixed or percentage amount to an invoice or subscription. Coupons are activated using a
+ * redemption code, which applies the discount to a subscription or invoice. The duration of
+ * a coupon determines how long it remains available for use by end users.
+ */
override fun coupons(): CouponService.WithRawResponse = coupons
+ /**
+ * The [Credit Note](/invoicing/credit-notes) resource represents a credit that has been
+ * applied to a particular invoice.
+ */
override fun creditNotes(): CreditNoteService.WithRawResponse = creditNotes
+ /**
+ * A customer is a buyer of your products, and the other party to the billing relationship.
+ *
+ * In Orb, customers are assigned system generated identifiers automatically, but it's often
+ * desirable to have these match existing identifiers in your system. To avoid having to
+ * denormalize Orb ID information, you can pass in an `external_customer_id` with your own
+ * identifier. See [Customer ID Aliases](/events-and-metrics/customer-aliases) for further
+ * information about how these aliases work in Orb.
+ *
+ * In addition to having an identifier in your system, a customer may exist in a payment
+ * provider solution like Stripe. Use the `payment_provider_id` and the `payment_provider`
+ * enum field to express this mapping.
+ *
+ * A customer also has a timezone (from the standard
+ * [IANA timezone database](https://www.iana.org/time-zones)), which defaults to your
+ * account's timezone. See [Timezone localization](/essentials/timezones) for information on
+ * what this timezone parameter influences within Orb.
+ */
override fun customers(): CustomerService.WithRawResponse = customers
+ /**
+ * The [Event](/core-concepts#event) resource represents a usage event that has been created
+ * for a customer. Events are the core of Orb's usage-based billing model, and are used to
+ * calculate the usage charges for a given billing period.
+ */
override fun events(): EventService.WithRawResponse = events
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of
+ * an action, such as a cancellation.
+ */
override fun invoiceLineItems(): InvoiceLineItemService.WithRawResponse = invoiceLineItems
+ /**
+ * An [`Invoice`](/core-concepts#invoice) is a fundamental billing entity, representing the
+ * request for payment for a single subscription. This includes a set of line items, which
+ * correspond to prices in the subscription's plan and can represent fixed recurring fees or
+ * usage-based fees. They are generated at the end of a billing period, or as the result of
+ * an action, such as a cancellation.
+ */
override fun invoices(): InvoiceService.WithRawResponse = invoices
+ /**
+ * The Item resource represents a sellable product or good. Items are associated with all
+ * line items, billable metrics, and prices and are used for defining external sync behavior
+ * for invoices and tax calculation purposes.
+ */
override fun items(): ItemService.WithRawResponse = items
+ /**
+ * The Metric resource represents a calculation of a quantity based on events. Metrics are
+ * defined by the query that transforms raw usage events into meaningful values for your
+ * customers.
+ */
override fun metrics(): MetricService.WithRawResponse = metrics
+ /**
+ * The [Plan](/core-concepts#plan-and-price) resource represents a plan that can be
+ * subscribed to by a customer. Plans define the billing behavior of the subscription. You
+ * can see more about how to configure prices in the [Price resource](/reference/price).
+ */
override fun plans(): PlanService.WithRawResponse = plans
+ /**
+ * The Price resource represents a price that can be billed on a subscription, resulting in
+ * a charge on an invoice in the form of an invoice line item. Prices take a quantity and
+ * determine an amount to bill.
+ *
+ * Orb supports a few different pricing models out of the box. Each of these models is
+ * serialized differently in a given Price object. The model_type field determines the key
+ * for the configuration object that is present.
+ *
+ * For more on the types of prices, see
+ * [the core concepts documentation](/core-concepts#plan-and-price)
+ */
override fun prices(): PriceService.WithRawResponse = prices
override fun subscriptions(): SubscriptionService.WithRawResponse = subscriptions
+ /**
+ * [Alerts within Orb](/product-catalog/configuring-alerts) monitor spending, usage, or
+ * credit balance and trigger webhooks when a threshold is exceeded.
+ *
+ * Alerts created through the API can be scoped to either customers or subscriptions.
+ */
override fun alerts(): AlertService.WithRawResponse = alerts
override fun dimensionalPriceGroups(): DimensionalPriceGroupService.WithRawResponse =
@@ -268,6 +468,18 @@ class OrbClientImpl(private val clientOptions: ClientOptions) : OrbClient {
override fun subscriptionChanges(): SubscriptionChangeService.WithRawResponse =
subscriptionChanges
+ /**
+ * The [Credit Ledger Entry resource](/product-catalog/prepurchase) models prepaid credits
+ * within Orb.
+ */
override fun creditBlocks(): CreditBlockService.WithRawResponse = creditBlocks
+
+ /**
+ * The LicenseType resource represents a type of license that can be assigned to users.
+ * License types are used during billing by grouping metrics on the configured grouping key.
+ */
+ override fun licenseTypes(): LicenseTypeService.WithRawResponse = licenseTypes
+
+ override fun licenses(): LicenseService.WithRawResponse = licenses
}
}
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/core/ClientOptions.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/ClientOptions.kt
index cd66b87cb..ba7547902 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/core/ClientOptions.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/ClientOptions.kt
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.withorb.api.core.http.AsyncStreamResponse
import com.withorb.api.core.http.Headers
import com.withorb.api.core.http.HttpClient
+import com.withorb.api.core.http.LoggingHttpClient
import com.withorb.api.core.http.PhantomReachableClosingHttpClient
import com.withorb.api.core.http.QueryParams
import com.withorb.api.core.http.RetryingHttpClient
@@ -80,6 +81,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.
*/
@@ -107,6 +111,14 @@ private constructor(
* Defaults to 2.
*/
@get:JvmName("maxRetries") val maxRetries: Int,
+ /**
+ * 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,
@get:JvmName("apiKey") val apiKey: String,
private val webhookSecret: String?,
) {
@@ -166,6 +178,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
private var webhookSecret: String? = null
@@ -183,6 +196,7 @@ private constructor(
responseValidation = clientOptions.responseValidation
timeout = clientOptions.timeout
maxRetries = clientOptions.maxRetries
+ logLevel = clientOptions.logLevel
apiKey = clientOptions.apiKey
webhookSecret = clientOptions.webhookSecret
}
@@ -264,6 +278,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.
*/
@@ -305,6 +322,15 @@ private constructor(
*/
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+ /**
+ * 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 }
+
fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
fun webhookSecret(webhookSecret: String?) = apply { this.webhookSecret = webhookSecret }
@@ -409,6 +435,7 @@ private constructor(
* System properties take precedence over environment variables.
*/
fun fromEnv() = apply {
+ logLevel(LogLevel.fromEnv())
(System.getProperty("orb.baseUrl") ?: System.getenv("ORB_BASE_URL"))?.let {
baseUrl(it)
}
@@ -416,6 +443,14 @@ private constructor(
(System.getProperty("orb.webhookSecret") ?: System.getenv("ORB_WEBHOOK_SECRET"))?.let {
webhookSecret(it)
}
+ System.getenv("ORB_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())
+ }
+ }
+ }
}
/**
@@ -463,18 +498,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)
@@ -491,6 +534,7 @@ private constructor(
responseValidation,
timeout,
maxRetries,
+ logLevel,
apiKey,
webhookSecret,
)
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/core/LogLevel.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/LogLevel.kt
new file mode 100644
index 000000000..5232bb6e4
--- /dev/null
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/LogLevel.kt
@@ -0,0 +1,33 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.withorb.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 `ORB_LOG` environment variable. */
+ fun fromEnv() =
+ when (System.getenv("ORB_LOG")?.lowercase()) {
+ "info" -> INFO
+ "error" -> ERROR
+ "debug" -> DEBUG
+ else -> OFF
+ }
+ }
+}
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/core/ObjectMappers.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/ObjectMappers.kt
index 1373ca6ce..5361d4e66 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/core/ObjectMappers.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/ObjectMappers.kt
@@ -25,11 +25,13 @@ import java.time.DateTimeException
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.OffsetDateTime
-import java.time.ZonedDateTime
+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())
@@ -157,14 +159,15 @@ private class LenientOffsetDateTimeDeserializer :
val temporal = formatter.parse(p.text)
return when {
- !temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
- LocalDate.from(temporal).atStartOfDay()
- !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
- LocalDateTime.from(temporal)
- else -> ZonedDateTime.from(temporal).toLocalDateTime()
- }
- .atZone(context.timeZone.toZoneId())
- .toOffsetDateTime()
+ !temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
+ LocalDate.from(temporal)
+ .atStartOfDay()
+ .atZone(ZoneId.of("UTC"))
+ .toOffsetDateTime()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime()
+ else -> OffsetDateTime.from(temporal)
+ }
} catch (e: DateTimeException) {
exceptions.add(e)
}
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/core/Properties.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/Properties.kt
index f0f63ba44..e21bb1e93 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/core/Properties.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/Properties.kt
@@ -34,8 +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 = OrbClient::class.java.`package`.implementationVersion ?: "unknown"
+fun getPackageVersion(): String =
+ OrbClient::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/orb-java-core/src/main/kotlin/com/withorb/api/core/RequestOptions.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/RequestOptions.kt
index 1f3b64291..c4be16ab4 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/core/RequestOptions.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/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/orb-java-core/src/main/kotlin/com/withorb/api/core/Utils.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/Utils.kt
index c392401d6..2b3f5319a 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/core/Utils.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/Utils.kt
@@ -6,6 +6,7 @@ import com.withorb.api.core.http.Headers
import com.withorb.api.errors.OrbInvalidDataException
import java.util.Collections
import java.util.SortedMap
+import java.util.SortedSet
import java.util.concurrent.CompletableFuture
import java.util.concurrent.locks.Lock
@@ -17,6 +18,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/orb-java-core/src/main/kotlin/com/withorb/api/core/http/HttpRequestBodies.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/http/HttpRequestBodies.kt
index 3463de9ea..4b7b62d15 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/core/http/HttpRequestBodies.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/http/HttpRequestBodies.kt
@@ -8,13 +8,13 @@ import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.node.JsonNodeType
import com.withorb.api.core.MultipartField
+import com.withorb.api.core.toImmutable
import com.withorb.api.errors.OrbInvalidDataException
+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,69 +37,207 @@ 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
- parts.forEach { (name, bytes) ->
- addBinaryBody(
- name,
- bytes,
- ContentType.parseLenient(field.contentType),
- field.filename().getOrNull(),
- )
+ override fun contentLength(): Long = -1L
+
+ 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().byteInputStream())
+ JsonNodeType.BOOLEAN -> sequenceOf(name to node.booleanValue().toString().byteInputStream())
+ JsonNodeType.NUMBER -> sequenceOf(name to node.numberValue().toString().byteInputStream())
+ JsonNodeType.ARRAY ->
+ node.elements().asSequence().flatMap { element -> serializePart("$name[]", element) }
+ JsonNodeType.OBJECT ->
+ node.fields().asSequence().flatMap { (key, value) ->
+ serializePart("$name[$key]", value)
+ }
+ JsonNodeType.POJO,
+ null -> throw OrbInvalidDataException("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().inputStream())
- JsonNodeType.BOOLEAN ->
- sequenceOf(name to node.booleanValue().toString().inputStream())
- JsonNodeType.NUMBER ->
- sequenceOf(name to node.numberValue().toString().inputStream())
- JsonNodeType.ARRAY ->
- node.elements().asSequence().flatMap { element ->
- serializePart("$name[]", element)
- }
- JsonNodeType.OBJECT ->
- node.fields().asSequence().flatMap { (key, value) ->
- serializePart("$name[$key]", value)
- }
- JsonNodeType.POJO,
- null -> throw OrbInvalidDataException("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/orb-java-core/src/main/kotlin/com/withorb/api/core/http/LoggingHttpClient.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/http/LoggingHttpClient.kt
new file mode 100644
index 000000000..f528b6634
--- /dev/null
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/http/LoggingHttpClient.kt
@@ -0,0 +1,628 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.withorb.api.core.http
+
+import com.withorb.api.core.LogLevel
+import com.withorb.api.core.RequestOptions
+import com.withorb.api.core.checkRequired
+import com.withorb.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/orb-java-core/src/main/kotlin/com/withorb/api/core/http/ProxyAuthenticator.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/http/ProxyAuthenticator.kt
new file mode 100644
index 000000000..cb2fbd5ab
--- /dev/null
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/http/ProxyAuthenticator.kt
@@ -0,0 +1,59 @@
+package com.withorb.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/orb-java-core/src/main/kotlin/com/withorb/api/core/http/RetryingHttpClient.kt b/orb-java-core/src/main/kotlin/com/withorb/api/core/http/RetryingHttpClient.kt
index 6bd384bc4..22841dcf3 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/core/http/RetryingHttpClient.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/core/http/RetryingHttpClient.kt
@@ -1,3 +1,5 @@
+// File generated from our OpenAPI spec by Stainless.
+
package com.withorb.api.core.http
import com.withorb.api.core.DefaultSleeper
@@ -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/orb-java-core/src/main/kotlin/com/withorb/api/errors/BadRequestException.kt b/orb-java-core/src/main/kotlin/com/withorb/api/errors/BadRequestException.kt
index 157ea6dd1..ca0378ea1 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/errors/BadRequestException.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/errors/BadRequestException.kt
@@ -5,12 +5,16 @@ package com.withorb.api.errors
import com.withorb.api.core.JsonValue
import com.withorb.api.core.checkRequired
import com.withorb.api.core.http.Headers
+import com.withorb.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?) :
- OrbServiceException("400: $body", cause) {
+ OrbServiceException(
+ "400: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 400
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/errors/InternalServerException.kt b/orb-java-core/src/main/kotlin/com/withorb/api/errors/InternalServerException.kt
index ad38f2699..f7232b72e 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/errors/InternalServerException.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/errors/InternalServerException.kt
@@ -5,6 +5,7 @@ package com.withorb.api.errors
import com.withorb.api.core.JsonValue
import com.withorb.api.core.checkRequired
import com.withorb.api.core.http.Headers
+import com.withorb.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?,
-) : OrbServiceException("$statusCode: $body", cause) {
+) :
+ OrbServiceException(
+ "$statusCode: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = statusCode
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/errors/NotFoundException.kt b/orb-java-core/src/main/kotlin/com/withorb/api/errors/NotFoundException.kt
index f059ba106..a819cf2d9 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/errors/NotFoundException.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/errors/NotFoundException.kt
@@ -5,12 +5,16 @@ package com.withorb.api.errors
import com.withorb.api.core.JsonValue
import com.withorb.api.core.checkRequired
import com.withorb.api.core.http.Headers
+import com.withorb.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?) :
- OrbServiceException("404: $body", cause) {
+ OrbServiceException(
+ "404: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 404
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/errors/PermissionDeniedException.kt b/orb-java-core/src/main/kotlin/com/withorb/api/errors/PermissionDeniedException.kt
index 774d2ae37..48085c283 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/errors/PermissionDeniedException.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/errors/PermissionDeniedException.kt
@@ -5,12 +5,16 @@ package com.withorb.api.errors
import com.withorb.api.core.JsonValue
import com.withorb.api.core.checkRequired
import com.withorb.api.core.http.Headers
+import com.withorb.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?) :
- OrbServiceException("403: $body", cause) {
+ OrbServiceException(
+ "403: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 403
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/errors/RateLimitException.kt b/orb-java-core/src/main/kotlin/com/withorb/api/errors/RateLimitException.kt
index 536ea6ed1..f9d68fd4e 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/errors/RateLimitException.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/errors/RateLimitException.kt
@@ -5,12 +5,16 @@ package com.withorb.api.errors
import com.withorb.api.core.JsonValue
import com.withorb.api.core.checkRequired
import com.withorb.api.core.http.Headers
+import com.withorb.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?) :
- OrbServiceException("429: $body", cause) {
+ OrbServiceException(
+ "429: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 429
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnauthorizedException.kt b/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnauthorizedException.kt
index 6ac99c31b..4277108d1 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnauthorizedException.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnauthorizedException.kt
@@ -5,12 +5,16 @@ package com.withorb.api.errors
import com.withorb.api.core.JsonValue
import com.withorb.api.core.checkRequired
import com.withorb.api.core.http.Headers
+import com.withorb.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?) :
- OrbServiceException("401: $body", cause) {
+ OrbServiceException(
+ "401: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 401
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnexpectedStatusCodeException.kt b/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnexpectedStatusCodeException.kt
index 75648438a..e66648e6f 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnexpectedStatusCodeException.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnexpectedStatusCodeException.kt
@@ -5,6 +5,7 @@ package com.withorb.api.errors
import com.withorb.api.core.JsonValue
import com.withorb.api.core.checkRequired
import com.withorb.api.core.http.Headers
+import com.withorb.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?,
-) : OrbServiceException("$statusCode: $body", cause) {
+) :
+ OrbServiceException(
+ "$statusCode: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = statusCode
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnprocessableEntityException.kt b/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnprocessableEntityException.kt
index 51ad3846a..00e45bd7f 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnprocessableEntityException.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/errors/UnprocessableEntityException.kt
@@ -5,12 +5,16 @@ package com.withorb.api.errors
import com.withorb.api.core.JsonValue
import com.withorb.api.core.checkRequired
import com.withorb.api.core.http.Headers
+import com.withorb.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?) :
- OrbServiceException("422: $body", cause) {
+ OrbServiceException(
+ "422: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}",
+ cause,
+ ) {
override fun statusCode(): Int = 422
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/AccountingProviderConfig.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/AccountingProviderConfig.kt
index bfbf6c224..faf36a35e 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/AccountingProviderConfig.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/AccountingProviderConfig.kt
@@ -6,6 +6,7 @@ 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 com.withorb.api.core.Enum
import com.withorb.api.core.ExcludeMissing
import com.withorb.api.core.JsonField
import com.withorb.api.core.JsonMissing
@@ -14,12 +15,13 @@ import com.withorb.api.core.checkRequired
import com.withorb.api.errors.OrbInvalidDataException
import java.util.Collections
import java.util.Objects
+import kotlin.jvm.optionals.getOrNull
class AccountingProviderConfig
@JsonCreator(mode = JsonCreator.Mode.DISABLED)
private constructor(
private val externalProviderId: JsonField,
- private val providerType: JsonField,
+ private val providerType: JsonField,
private val additionalProperties: MutableMap,
) {
@@ -30,7 +32,7 @@ private constructor(
externalProviderId: JsonField = JsonMissing.of(),
@JsonProperty("provider_type")
@ExcludeMissing
- providerType: JsonField = JsonMissing.of(),
+ providerType: JsonField = JsonMissing.of(),
) : this(externalProviderId, providerType, mutableMapOf())
/**
@@ -43,7 +45,7 @@ private constructor(
* @throws OrbInvalidDataException if the JSON field has an unexpected type or is unexpectedly
* missing or null (e.g. if the server responded with an unexpected value).
*/
- fun providerType(): String = providerType.getRequired("provider_type")
+ fun providerType(): ProviderType = providerType.getRequired("provider_type")
/**
* Returns the raw JSON value of [externalProviderId].
@@ -62,7 +64,7 @@ private constructor(
*/
@JsonProperty("provider_type")
@ExcludeMissing
- fun _providerType(): JsonField = providerType
+ fun _providerType(): JsonField = providerType
@JsonAnySetter
private fun putAdditionalProperty(key: String, value: JsonValue) {
@@ -94,7 +96,7 @@ private constructor(
class Builder internal constructor() {
private var externalProviderId: JsonField? = null
- private var providerType: JsonField? = null
+ private var providerType: JsonField? = null
private var additionalProperties: MutableMap = mutableMapOf()
@JvmSynthetic
@@ -118,16 +120,16 @@ private constructor(
this.externalProviderId = externalProviderId
}
- fun providerType(providerType: String) = providerType(JsonField.of(providerType))
+ fun providerType(providerType: ProviderType) = providerType(JsonField.of(providerType))
/**
* Sets [Builder.providerType] to an arbitrary JSON value.
*
- * You should usually call [Builder.providerType] with a well-typed [String] value instead.
- * This method is primarily for setting the field to an undocumented or not yet supported
- * value.
+ * You should usually call [Builder.providerType] with a well-typed [ProviderType] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
*/
- fun providerType(providerType: JsonField) = apply {
+ fun providerType(providerType: JsonField) = apply {
this.providerType = providerType
}
@@ -173,13 +175,21 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its expected
+ * type.
+ */
fun validate(): AccountingProviderConfig = apply {
if (validated) {
return@apply
}
externalProviderId()
- providerType()
+ providerType().validate()
validated = true
}
@@ -199,7 +209,143 @@ private constructor(
@JvmSynthetic
internal fun validity(): Int =
(if (externalProviderId.asKnown().isPresent) 1 else 0) +
- (if (providerType.asKnown().isPresent) 1 else 0)
+ (providerType.asKnown().getOrNull()?.validity() ?: 0)
+
+ class ProviderType @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 QUICKBOOKS = of("quickbooks")
+
+ @JvmField val NETSUITE = of("netsuite")
+
+ @JvmStatic fun of(value: String) = ProviderType(JsonField.of(value))
+ }
+
+ /** An enum containing [ProviderType]'s known values. */
+ enum class Known {
+ QUICKBOOKS,
+ NETSUITE,
+ }
+
+ /**
+ * An enum containing [ProviderType]'s known values, as well as an [_UNKNOWN] member.
+ *
+ * An instance of [ProviderType] 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 {
+ QUICKBOOKS,
+ NETSUITE,
+ /**
+ * An enum member indicating that [ProviderType] 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) {
+ QUICKBOOKS -> Value.QUICKBOOKS
+ NETSUITE -> Value.NETSUITE
+ 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 OrbInvalidDataException if this class instance's value is a not a known member.
+ */
+ fun known(): Known =
+ when (this) {
+ QUICKBOOKS -> Known.QUICKBOOKS
+ NETSUITE -> Known.NETSUITE
+ else -> throw OrbInvalidDataException("Unknown ProviderType: $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 OrbInvalidDataException if this class instance's value does not have the expected
+ * primitive type.
+ */
+ fun asString(): String =
+ _value().asString().orElseThrow { OrbInvalidDataException("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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): ProviderType = apply {
+ if (validated) {
+ return@apply
+ }
+
+ known()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: OrbInvalidDataException) {
+ 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 ProviderType && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+ }
override fun equals(other: Any?): Boolean {
if (this === other) {
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/Address.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/Address.kt
index de9c14aed..14f1b5584 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/Address.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/Address.kt
@@ -300,6 +300,14 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its expected
+ * type.
+ */
fun validate(): Address = apply {
if (validated) {
return@apply
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/AddressInput.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/AddressInput.kt
index 9dbc4e7fe..18803360c 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/AddressInput.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/AddressInput.kt
@@ -275,6 +275,14 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its expected
+ * type.
+ */
fun validate(): AddressInput = apply {
if (validated) {
return@apply
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/AdjustmentInterval.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/AdjustmentInterval.kt
index 17115a571..19246cee9 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/AdjustmentInterval.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/AdjustmentInterval.kt
@@ -335,6 +335,14 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its expected
+ * type.
+ */
fun validate(): AdjustmentInterval = apply {
if (validated) {
return@apply
@@ -419,6 +427,35 @@ private constructor(
fun _json(): Optional = Optional.ofNullable(_json)
+ /**
+ * Maps this instance's current variant to a value of type [T] using the given [visitor].
+ *
+ * Note that this method is _not_ forwards compatible with new variants from the API, unless
+ * [visitor] overrides [Visitor.unknown]. To handle variants not known to this version of
+ * the SDK gracefully, consider overriding [Visitor.unknown]:
+ * ```java
+ * import com.withorb.api.core.JsonValue;
+ * import java.util.Optional;
+ *
+ * Optional result = adjustment.accept(new Adjustment.Visitor>() {
+ * @Override
+ * public Optional visitUsageDiscount(PlanPhaseUsageDiscountAdjustment usageDiscount) {
+ * return Optional.of(usageDiscount.toString());
+ * }
+ *
+ * // ...
+ *
+ * @Override
+ * public Optional unknown(JsonValue json) {
+ * // Or inspect the `json`.
+ * return Optional.empty();
+ * }
+ * });
+ * ```
+ *
+ * @throws OrbInvalidDataException if [Visitor.unknown] is not overridden in [visitor] and
+ * the current variant is unknown.
+ */
fun accept(visitor: Visitor): T =
when {
usageDiscount != null -> visitor.visitUsageDiscount(usageDiscount)
@@ -431,6 +468,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Adjustment = apply {
if (validated) {
return@apply
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/AffectedBlock.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/AffectedBlock.kt
index 342db5eb0..6a70fa987 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/AffectedBlock.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/AffectedBlock.kt
@@ -263,6 +263,14 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its expected
+ * type.
+ */
fun validate(): AffectedBlock = apply {
if (validated) {
return@apply
@@ -497,6 +505,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Filter = apply {
if (validated) {
return@apply
@@ -636,6 +653,16 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Field = apply {
if (validated) {
return@apply
@@ -765,6 +792,16 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Operator = apply {
if (validated) {
return@apply
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/AggregatedCost.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/AggregatedCost.kt
index 132a3db87..3cb77d5d4 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/AggregatedCost.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/AggregatedCost.kt
@@ -292,6 +292,14 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its expected
+ * type.
+ */
fun validate(): AggregatedCost = apply {
if (validated) {
return@apply
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/Alert.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/Alert.kt
index f35084b4a..05f58ec8a 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/Alert.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/Alert.kt
@@ -41,6 +41,10 @@ private constructor(
private val thresholds: JsonField>,
private val type: JsonField,
private val balanceAlertStatus: JsonField>,
+ private val groupingKeys: JsonField>,
+ private val licenseType: JsonField,
+ private val priceFilters: JsonField>,
+ private val thresholdOverrides: JsonField>,
private val additionalProperties: MutableMap,
) {
@@ -67,6 +71,18 @@ private constructor(
@JsonProperty("balance_alert_status")
@ExcludeMissing
balanceAlertStatus: JsonField> = JsonMissing.of(),
+ @JsonProperty("grouping_keys")
+ @ExcludeMissing
+ groupingKeys: JsonField> = JsonMissing.of(),
+ @JsonProperty("license_type")
+ @ExcludeMissing
+ licenseType: JsonField = JsonMissing.of(),
+ @JsonProperty("price_filters")
+ @ExcludeMissing
+ priceFilters: JsonField> = JsonMissing.of(),
+ @JsonProperty("threshold_overrides")
+ @ExcludeMissing
+ thresholdOverrides: JsonField> = JsonMissing.of(),
) : this(
id,
createdAt,
@@ -79,6 +95,10 @@ private constructor(
thresholds,
type,
balanceAlertStatus,
+ groupingKeys,
+ licenseType,
+ priceFilters,
+ thresholdOverrides,
mutableMapOf(),
)
@@ -171,6 +191,42 @@ private constructor(
fun balanceAlertStatus(): Optional> =
balanceAlertStatus.getOptional("balance_alert_status")
+ /**
+ * The property keys to group cost alerts by. Only present for cost alerts with grouping
+ * enabled.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the server
+ * responded with an unexpected value).
+ */
+ fun groupingKeys(): Optional> = groupingKeys.getOptional("grouping_keys")
+
+ /**
+ * Minified license type for alert serialization.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the server
+ * responded with an unexpected value).
+ */
+ fun licenseType(): Optional = licenseType.getOptional("license_type")
+
+ /**
+ * Filters scoping which prices are included in grouped cost alert evaluation.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the server
+ * responded with an unexpected value).
+ */
+ fun priceFilters(): Optional> = priceFilters.getOptional("price_filters")
+
+ /**
+ * Per-group threshold overrides. Each override maps a specific combination of grouping_keys
+ * values to a replacement threshold list. Only present for grouped cost alerts that have at
+ * least one override.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the server
+ * responded with an unexpected value).
+ */
+ fun thresholdOverrides(): Optional> =
+ thresholdOverrides.getOptional("threshold_overrides")
+
/**
* Returns the raw JSON value of [id].
*
@@ -259,6 +315,43 @@ private constructor(
@ExcludeMissing
fun _balanceAlertStatus(): JsonField> = balanceAlertStatus
+ /**
+ * Returns the raw JSON value of [groupingKeys].
+ *
+ * Unlike [groupingKeys], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("grouping_keys")
+ @ExcludeMissing
+ fun _groupingKeys(): JsonField> = groupingKeys
+
+ /**
+ * Returns the raw JSON value of [licenseType].
+ *
+ * Unlike [licenseType], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("license_type")
+ @ExcludeMissing
+ fun _licenseType(): JsonField = licenseType
+
+ /**
+ * Returns the raw JSON value of [priceFilters].
+ *
+ * Unlike [priceFilters], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("price_filters")
+ @ExcludeMissing
+ fun _priceFilters(): JsonField> = priceFilters
+
+ /**
+ * Returns the raw JSON value of [thresholdOverrides].
+ *
+ * Unlike [thresholdOverrides], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("threshold_overrides")
+ @ExcludeMissing
+ fun _thresholdOverrides(): JsonField> = thresholdOverrides
+
@JsonAnySetter
private fun putAdditionalProperty(key: String, value: JsonValue) {
additionalProperties.put(key, value)
@@ -307,6 +400,10 @@ private constructor(
private var thresholds: JsonField>? = null
private var type: JsonField? = null
private var balanceAlertStatus: JsonField>? = null
+ private var groupingKeys: JsonField>? = null
+ private var licenseType: JsonField = JsonMissing.of()
+ private var priceFilters: JsonField>? = null
+ private var thresholdOverrides: JsonField>? = null
private var additionalProperties: MutableMap = mutableMapOf()
@JvmSynthetic
@@ -322,6 +419,10 @@ private constructor(
thresholds = alert.thresholds.map { it.toMutableList() }
type = alert.type
balanceAlertStatus = alert.balanceAlertStatus.map { it.toMutableList() }
+ groupingKeys = alert.groupingKeys.map { it.toMutableList() }
+ licenseType = alert.licenseType
+ priceFilters = alert.priceFilters.map { it.toMutableList() }
+ thresholdOverrides = alert.thresholdOverrides.map { it.toMutableList() }
additionalProperties = alert.additionalProperties.toMutableMap()
}
@@ -510,6 +611,125 @@ private constructor(
}
}
+ /**
+ * The property keys to group cost alerts by. Only present for cost alerts with grouping
+ * enabled.
+ */
+ fun groupingKeys(groupingKeys: List?) =
+ groupingKeys(JsonField.ofNullable(groupingKeys))
+
+ /** Alias for calling [Builder.groupingKeys] with `groupingKeys.orElse(null)`. */
+ fun groupingKeys(groupingKeys: Optional>) =
+ groupingKeys(groupingKeys.getOrNull())
+
+ /**
+ * Sets [Builder.groupingKeys] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.groupingKeys] with a well-typed `List` value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun groupingKeys(groupingKeys: JsonField>) = apply {
+ this.groupingKeys = groupingKeys.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [String] to [groupingKeys].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addGroupingKey(groupingKey: String) = apply {
+ groupingKeys =
+ (groupingKeys ?: JsonField.of(mutableListOf())).also {
+ checkKnown("groupingKeys", it).add(groupingKey)
+ }
+ }
+
+ /** Minified license type for alert serialization. */
+ fun licenseType(licenseType: LicenseType?) = licenseType(JsonField.ofNullable(licenseType))
+
+ /** Alias for calling [Builder.licenseType] with `licenseType.orElse(null)`. */
+ fun licenseType(licenseType: Optional) = licenseType(licenseType.getOrNull())
+
+ /**
+ * Sets [Builder.licenseType] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.licenseType] with a well-typed [LicenseType] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun licenseType(licenseType: JsonField) = apply {
+ this.licenseType = licenseType
+ }
+
+ /** Filters scoping which prices are included in grouped cost alert evaluation. */
+ fun priceFilters(priceFilters: List?) =
+ priceFilters(JsonField.ofNullable(priceFilters))
+
+ /** Alias for calling [Builder.priceFilters] with `priceFilters.orElse(null)`. */
+ fun priceFilters(priceFilters: Optional>) =
+ priceFilters(priceFilters.getOrNull())
+
+ /**
+ * Sets [Builder.priceFilters] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.priceFilters] with a well-typed `List`
+ * value instead. This method is primarily for setting the field to an undocumented or not
+ * yet supported value.
+ */
+ fun priceFilters(priceFilters: JsonField>) = apply {
+ this.priceFilters = priceFilters.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [PriceFilter] to [priceFilters].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addPriceFilter(priceFilter: PriceFilter) = apply {
+ priceFilters =
+ (priceFilters ?: JsonField.of(mutableListOf())).also {
+ checkKnown("priceFilters", it).add(priceFilter)
+ }
+ }
+
+ /**
+ * Per-group threshold overrides. Each override maps a specific combination of grouping_keys
+ * values to a replacement threshold list. Only present for grouped cost alerts that have at
+ * least one override.
+ */
+ fun thresholdOverrides(thresholdOverrides: List?) =
+ thresholdOverrides(JsonField.ofNullable(thresholdOverrides))
+
+ /**
+ * Alias for calling [Builder.thresholdOverrides] with `thresholdOverrides.orElse(null)`.
+ */
+ fun thresholdOverrides(thresholdOverrides: Optional>) =
+ thresholdOverrides(thresholdOverrides.getOrNull())
+
+ /**
+ * Sets [Builder.thresholdOverrides] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.thresholdOverrides] with a well-typed
+ * `List` value instead. This method is primarily for setting the field
+ * to an undocumented or not yet supported value.
+ */
+ fun thresholdOverrides(thresholdOverrides: JsonField>) = apply {
+ this.thresholdOverrides = thresholdOverrides.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [ThresholdOverride] to [thresholdOverrides].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addThresholdOverride(thresholdOverride: ThresholdOverride) = apply {
+ thresholdOverrides =
+ (thresholdOverrides ?: JsonField.of(mutableListOf())).also {
+ checkKnown("thresholdOverrides", it).add(thresholdOverride)
+ }
+ }
+
fun additionalProperties(additionalProperties: Map) = apply {
this.additionalProperties.clear()
putAllAdditionalProperties(additionalProperties)
@@ -563,12 +783,24 @@ private constructor(
checkRequired("thresholds", thresholds).map { it.toImmutable() },
checkRequired("type", type),
(balanceAlertStatus ?: JsonMissing.of()).map { it.toImmutable() },
+ (groupingKeys ?: JsonMissing.of()).map { it.toImmutable() },
+ licenseType,
+ (priceFilters ?: JsonMissing.of()).map { it.toImmutable() },
+ (thresholdOverrides ?: JsonMissing.of()).map { it.toImmutable() },
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 OrbInvalidDataException if any value type in this object doesn't match its expected
+ * type.
+ */
fun validate(): Alert = apply {
if (validated) {
return@apply
@@ -585,6 +817,10 @@ private constructor(
thresholds().ifPresent { it.forEach { it.validate() } }
type().validate()
balanceAlertStatus().ifPresent { it.forEach { it.validate() } }
+ groupingKeys()
+ licenseType().ifPresent { it.validate() }
+ priceFilters().ifPresent { it.forEach { it.validate() } }
+ thresholdOverrides().ifPresent { it.forEach { it.validate() } }
validated = true
}
@@ -613,7 +849,11 @@ private constructor(
(subscription.asKnown().getOrNull()?.validity() ?: 0) +
(thresholds.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
(type.asKnown().getOrNull()?.validity() ?: 0) +
- (balanceAlertStatus.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0)
+ (balanceAlertStatus.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
+ (groupingKeys.asKnown().getOrNull()?.size ?: 0) +
+ (licenseType.asKnown().getOrNull()?.validity() ?: 0) +
+ (priceFilters.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
+ (thresholdOverrides.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0)
/** The metric the alert applies to. */
class Metric
@@ -726,6 +966,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Metric = apply {
if (validated) {
return@apply
@@ -1008,6 +1257,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Plan = apply {
if (validated) {
return@apply
@@ -1089,6 +1347,9 @@ private constructor(
@JvmField val COST_EXCEEDED = of("cost_exceeded")
+ @JvmField
+ val LICENSE_BALANCE_THRESHOLD_REACHED = of("license_balance_threshold_reached")
+
@JvmStatic fun of(value: String) = Type(JsonField.of(value))
}
@@ -1099,6 +1360,7 @@ private constructor(
CREDIT_BALANCE_RECOVERED,
USAGE_EXCEEDED,
COST_EXCEEDED,
+ LICENSE_BALANCE_THRESHOLD_REACHED,
}
/**
@@ -1116,6 +1378,7 @@ private constructor(
CREDIT_BALANCE_RECOVERED,
USAGE_EXCEEDED,
COST_EXCEEDED,
+ LICENSE_BALANCE_THRESHOLD_REACHED,
/** An enum member indicating that [Type] was instantiated with an unknown value. */
_UNKNOWN,
}
@@ -1134,6 +1397,7 @@ private constructor(
CREDIT_BALANCE_RECOVERED -> Value.CREDIT_BALANCE_RECOVERED
USAGE_EXCEEDED -> Value.USAGE_EXCEEDED
COST_EXCEEDED -> Value.COST_EXCEEDED
+ LICENSE_BALANCE_THRESHOLD_REACHED -> Value.LICENSE_BALANCE_THRESHOLD_REACHED
else -> Value._UNKNOWN
}
@@ -1152,6 +1416,7 @@ private constructor(
CREDIT_BALANCE_RECOVERED -> Known.CREDIT_BALANCE_RECOVERED
USAGE_EXCEEDED -> Known.USAGE_EXCEEDED
COST_EXCEEDED -> Known.COST_EXCEEDED
+ LICENSE_BALANCE_THRESHOLD_REACHED -> Known.LICENSE_BALANCE_THRESHOLD_REACHED
else -> throw OrbInvalidDataException("Unknown Type: $value")
}
@@ -1169,6 +1434,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Type = apply {
if (validated) {
return@apply
@@ -1368,6 +1642,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): BalanceAlertStatus = apply {
if (validated) {
return@apply
@@ -1418,6 +1701,982 @@ private constructor(
"BalanceAlertStatus{inAlert=$inAlert, thresholdValue=$thresholdValue, additionalProperties=$additionalProperties}"
}
+ /** Minified license type for alert serialization. */
+ class LicenseType
+ @JsonCreator(mode = JsonCreator.Mode.DISABLED)
+ private constructor(
+ private val id: JsonField,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("id") @ExcludeMissing id: JsonField = JsonMissing.of()
+ ) : this(id, mutableMapOf())
+
+ /**
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected value).
+ */
+ fun id(): String = id.getRequired("id")
+
+ /**
+ * 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
+
+ @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 [LicenseType].
+ *
+ * The following fields are required:
+ * ```java
+ * .id()
+ * ```
+ */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [LicenseType]. */
+ class Builder internal constructor() {
+
+ private var id: JsonField? = null
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(licenseType: LicenseType) = apply {
+ id = licenseType.id
+ additionalProperties = licenseType.additionalProperties.toMutableMap()
+ }
+
+ 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 }
+
+ 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 [LicenseType].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .id()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): LicenseType =
+ LicenseType(checkRequired("id", id), 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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): LicenseType = apply {
+ if (validated) {
+ return@apply
+ }
+
+ id()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: OrbInvalidDataException) {
+ 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)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is LicenseType &&
+ id == other.id &&
+ additionalProperties == other.additionalProperties
+ }
+
+ private val hashCode: Int by lazy { Objects.hash(id, additionalProperties) }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() = "LicenseType{id=$id, additionalProperties=$additionalProperties}"
+ }
+
+ class PriceFilter
+ @JsonCreator(mode = JsonCreator.Mode.DISABLED)
+ private constructor(
+ private val field: JsonField,
+ private val operator: JsonField,
+ private val values: JsonField>,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("field") @ExcludeMissing field: JsonField = JsonMissing.of(),
+ @JsonProperty("operator")
+ @ExcludeMissing
+ operator: JsonField = JsonMissing.of(),
+ @JsonProperty("values")
+ @ExcludeMissing
+ values: JsonField> = JsonMissing.of(),
+ ) : this(field, operator, values, mutableMapOf())
+
+ /**
+ * The property of the price to filter on.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected value).
+ */
+ fun field(): Field = field.getRequired("field")
+
+ /**
+ * Should prices that match the filter be included or excluded.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected value).
+ */
+ fun operator(): Operator = operator.getRequired("operator")
+
+ /**
+ * The IDs or values that match this filter.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected value).
+ */
+ fun values(): List = values.getRequired("values")
+
+ /**
+ * Returns the raw JSON value of [field].
+ *
+ * Unlike [field], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("field") @ExcludeMissing fun _field(): JsonField = field
+
+ /**
+ * Returns the raw JSON value of [operator].
+ *
+ * Unlike [operator], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("operator") @ExcludeMissing fun _operator(): JsonField = operator
+
+ /**
+ * Returns the raw JSON value of [values].
+ *
+ * Unlike [values], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("values") @ExcludeMissing fun _values(): JsonField> = values
+
+ @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 [PriceFilter].
+ *
+ * The following fields are required:
+ * ```java
+ * .field()
+ * .operator()
+ * .values()
+ * ```
+ */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [PriceFilter]. */
+ class Builder internal constructor() {
+
+ private var field: JsonField? = null
+ private var operator: JsonField? = null
+ private var values: JsonField>? = null
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(priceFilter: PriceFilter) = apply {
+ field = priceFilter.field
+ operator = priceFilter.operator
+ values = priceFilter.values.map { it.toMutableList() }
+ additionalProperties = priceFilter.additionalProperties.toMutableMap()
+ }
+
+ /** The property of the price to filter on. */
+ fun field(field: Field) = field(JsonField.of(field))
+
+ /**
+ * Sets [Builder.field] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.field] with a well-typed [Field] value instead. This
+ * method is primarily for setting the field to an undocumented or not yet supported
+ * value.
+ */
+ fun field(field: JsonField) = apply { this.field = field }
+
+ /** Should prices that match the filter be included or excluded. */
+ fun operator(operator: Operator) = operator(JsonField.of(operator))
+
+ /**
+ * Sets [Builder.operator] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.operator] with a well-typed [Operator] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun operator(operator: JsonField) = apply { this.operator = operator }
+
+ /** The IDs or values that match this filter. */
+ fun values(values: List) = values(JsonField.of(values))
+
+ /**
+ * Sets [Builder.values] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.values] with a well-typed `List` value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun values(values: JsonField>) = apply {
+ this.values = values.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [String] to [values].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addValue(value: String) = apply {
+ values =
+ (values ?: JsonField.of(mutableListOf())).also {
+ checkKnown("values", it).add(value)
+ }
+ }
+
+ 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 [PriceFilter].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .field()
+ * .operator()
+ * .values()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): PriceFilter =
+ PriceFilter(
+ checkRequired("field", field),
+ checkRequired("operator", operator),
+ checkRequired("values", values).map { it.toImmutable() },
+ 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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): PriceFilter = apply {
+ if (validated) {
+ return@apply
+ }
+
+ field().validate()
+ operator().validate()
+ values()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: OrbInvalidDataException) {
+ 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 =
+ (field.asKnown().getOrNull()?.validity() ?: 0) +
+ (operator.asKnown().getOrNull()?.validity() ?: 0) +
+ (values.asKnown().getOrNull()?.size ?: 0)
+
+ /** The property of the price to filter on. */
+ class Field @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 PRICE_ID = of("price_id")
+
+ @JvmField val ITEM_ID = of("item_id")
+
+ @JvmField val PRICE_TYPE = of("price_type")
+
+ @JvmField val CURRENCY = of("currency")
+
+ @JvmField val PRICING_UNIT_ID = of("pricing_unit_id")
+
+ @JvmStatic fun of(value: String) = Field(JsonField.of(value))
+ }
+
+ /** An enum containing [Field]'s known values. */
+ enum class Known {
+ PRICE_ID,
+ ITEM_ID,
+ PRICE_TYPE,
+ CURRENCY,
+ PRICING_UNIT_ID,
+ }
+
+ /**
+ * An enum containing [Field]'s known values, as well as an [_UNKNOWN] member.
+ *
+ * An instance of [Field] 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 {
+ PRICE_ID,
+ ITEM_ID,
+ PRICE_TYPE,
+ CURRENCY,
+ PRICING_UNIT_ID,
+ /**
+ * An enum member indicating that [Field] 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) {
+ PRICE_ID -> Value.PRICE_ID
+ ITEM_ID -> Value.ITEM_ID
+ PRICE_TYPE -> Value.PRICE_TYPE
+ CURRENCY -> Value.CURRENCY
+ PRICING_UNIT_ID -> Value.PRICING_UNIT_ID
+ 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 OrbInvalidDataException if this class instance's value is a not a known
+ * member.
+ */
+ fun known(): Known =
+ when (this) {
+ PRICE_ID -> Known.PRICE_ID
+ ITEM_ID -> Known.ITEM_ID
+ PRICE_TYPE -> Known.PRICE_TYPE
+ CURRENCY -> Known.CURRENCY
+ PRICING_UNIT_ID -> Known.PRICING_UNIT_ID
+ else -> throw OrbInvalidDataException("Unknown Field: $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 OrbInvalidDataException if this class instance's value does not have the
+ * expected primitive type.
+ */
+ fun asString(): String =
+ _value().asString().orElseThrow { OrbInvalidDataException("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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): Field = apply {
+ if (validated) {
+ return@apply
+ }
+
+ known()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: OrbInvalidDataException) {
+ 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 Field && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+ }
+
+ /** Should prices that match the filter be included or excluded. */
+ class Operator @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 INCLUDES = of("includes")
+
+ @JvmField val EXCLUDES = of("excludes")
+
+ @JvmStatic fun of(value: String) = Operator(JsonField.of(value))
+ }
+
+ /** An enum containing [Operator]'s known values. */
+ enum class Known {
+ INCLUDES,
+ EXCLUDES,
+ }
+
+ /**
+ * An enum containing [Operator]'s known values, as well as an [_UNKNOWN] member.
+ *
+ * An instance of [Operator] 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 {
+ INCLUDES,
+ EXCLUDES,
+ /**
+ * An enum member indicating that [Operator] 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) {
+ INCLUDES -> Value.INCLUDES
+ EXCLUDES -> Value.EXCLUDES
+ 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 OrbInvalidDataException if this class instance's value is a not a known
+ * member.
+ */
+ fun known(): Known =
+ when (this) {
+ INCLUDES -> Known.INCLUDES
+ EXCLUDES -> Known.EXCLUDES
+ else -> throw OrbInvalidDataException("Unknown Operator: $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 OrbInvalidDataException if this class instance's value does not have the
+ * expected primitive type.
+ */
+ fun asString(): String =
+ _value().asString().orElseThrow { OrbInvalidDataException("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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): Operator = apply {
+ if (validated) {
+ return@apply
+ }
+
+ known()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: OrbInvalidDataException) {
+ 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 Operator && 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 PriceFilter &&
+ field == other.field &&
+ operator == other.operator &&
+ values == other.values &&
+ additionalProperties == other.additionalProperties
+ }
+
+ private val hashCode: Int by lazy {
+ Objects.hash(field, operator, values, additionalProperties)
+ }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() =
+ "PriceFilter{field=$field, operator=$operator, values=$values, additionalProperties=$additionalProperties}"
+ }
+
+ /**
+ * A per-group threshold override on a grouped cost alert.
+ *
+ * An empty `thresholds` list means the group is silenced (never fires). A non-empty list fully
+ * replaces the default thresholds for that group.
+ */
+ class ThresholdOverride
+ @JsonCreator(mode = JsonCreator.Mode.DISABLED)
+ private constructor(
+ private val groupValues: JsonField>,
+ private val thresholds: JsonField>,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("group_values")
+ @ExcludeMissing
+ groupValues: JsonField> = JsonMissing.of(),
+ @JsonProperty("thresholds")
+ @ExcludeMissing
+ thresholds: JsonField> = JsonMissing.of(),
+ ) : this(groupValues, thresholds, mutableMapOf())
+
+ /**
+ * The values of the grouping keys that identify this group. The list length matches the
+ * alert's grouping_keys.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected value).
+ */
+ fun groupValues(): List = groupValues.getRequired("group_values")
+
+ /**
+ * The thresholds applied to this group. An empty list means the group is silenced.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected value).
+ */
+ fun thresholds(): List = thresholds.getRequired("thresholds")
+
+ /**
+ * Returns the raw JSON value of [groupValues].
+ *
+ * Unlike [groupValues], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("group_values")
+ @ExcludeMissing
+ fun _groupValues(): JsonField> = groupValues
+
+ /**
+ * Returns the raw JSON value of [thresholds].
+ *
+ * Unlike [thresholds], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ @JsonProperty("thresholds")
+ @ExcludeMissing
+ fun _thresholds(): JsonField> = thresholds
+
+ @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 [ThresholdOverride].
+ *
+ * The following fields are required:
+ * ```java
+ * .groupValues()
+ * .thresholds()
+ * ```
+ */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [ThresholdOverride]. */
+ class Builder internal constructor() {
+
+ private var groupValues: JsonField>? = null
+ private var thresholds: JsonField>? = null
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(thresholdOverride: ThresholdOverride) = apply {
+ groupValues = thresholdOverride.groupValues.map { it.toMutableList() }
+ thresholds = thresholdOverride.thresholds.map { it.toMutableList() }
+ additionalProperties = thresholdOverride.additionalProperties.toMutableMap()
+ }
+
+ /**
+ * The values of the grouping keys that identify this group. The list length matches the
+ * alert's grouping_keys.
+ */
+ fun groupValues(groupValues: List) = groupValues(JsonField.of(groupValues))
+
+ /**
+ * Sets [Builder.groupValues] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.groupValues] with a well-typed `List` value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun groupValues(groupValues: JsonField>) = apply {
+ this.groupValues = groupValues.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [String] to [groupValues].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addGroupValue(groupValue: String) = apply {
+ groupValues =
+ (groupValues ?: JsonField.of(mutableListOf())).also {
+ checkKnown("groupValues", it).add(groupValue)
+ }
+ }
+
+ /** The thresholds applied to this group. An empty list means the group is silenced. */
+ fun thresholds(thresholds: List) = thresholds(JsonField.of(thresholds))
+
+ /**
+ * Sets [Builder.thresholds] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.thresholds] with a well-typed `List`
+ * value instead. This method is primarily for setting the field to an undocumented or
+ * not yet supported value.
+ */
+ fun thresholds(thresholds: JsonField>) = apply {
+ this.thresholds = thresholds.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [Threshold] to [thresholds].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addThreshold(threshold: Threshold) = apply {
+ thresholds =
+ (thresholds ?: JsonField.of(mutableListOf())).also {
+ checkKnown("thresholds", it).add(threshold)
+ }
+ }
+
+ 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 [ThresholdOverride].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .groupValues()
+ * .thresholds()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): ThresholdOverride =
+ ThresholdOverride(
+ checkRequired("groupValues", groupValues).map { it.toImmutable() },
+ checkRequired("thresholds", thresholds).map { it.toImmutable() },
+ 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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
+ fun validate(): ThresholdOverride = apply {
+ if (validated) {
+ return@apply
+ }
+
+ groupValues()
+ thresholds().forEach { it.validate() }
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: OrbInvalidDataException) {
+ 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 =
+ (groupValues.asKnown().getOrNull()?.size ?: 0) +
+ (thresholds.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is ThresholdOverride &&
+ groupValues == other.groupValues &&
+ thresholds == other.thresholds &&
+ additionalProperties == other.additionalProperties
+ }
+
+ private val hashCode: Int by lazy {
+ Objects.hash(groupValues, thresholds, additionalProperties)
+ }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() =
+ "ThresholdOverride{groupValues=$groupValues, thresholds=$thresholds, additionalProperties=$additionalProperties}"
+ }
+
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
@@ -1435,6 +2694,10 @@ private constructor(
thresholds == other.thresholds &&
type == other.type &&
balanceAlertStatus == other.balanceAlertStatus &&
+ groupingKeys == other.groupingKeys &&
+ licenseType == other.licenseType &&
+ priceFilters == other.priceFilters &&
+ thresholdOverrides == other.thresholdOverrides &&
additionalProperties == other.additionalProperties
}
@@ -1451,6 +2714,10 @@ private constructor(
thresholds,
type,
balanceAlertStatus,
+ groupingKeys,
+ licenseType,
+ priceFilters,
+ thresholdOverrides,
additionalProperties,
)
}
@@ -1458,5 +2725,5 @@ private constructor(
override fun hashCode(): Int = hashCode
override fun toString() =
- "Alert{id=$id, createdAt=$createdAt, currency=$currency, customer=$customer, enabled=$enabled, metric=$metric, plan=$plan, subscription=$subscription, thresholds=$thresholds, type=$type, balanceAlertStatus=$balanceAlertStatus, additionalProperties=$additionalProperties}"
+ "Alert{id=$id, createdAt=$createdAt, currency=$currency, customer=$customer, enabled=$enabled, metric=$metric, plan=$plan, subscription=$subscription, thresholds=$thresholds, type=$type, balanceAlertStatus=$balanceAlertStatus, groupingKeys=$groupingKeys, licenseType=$licenseType, priceFilters=$priceFilters, thresholdOverrides=$thresholdOverrides, additionalProperties=$additionalProperties}"
}
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForCustomerParams.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForCustomerParams.kt
index 23046502e..31b7856df 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForCustomerParams.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForCustomerParams.kt
@@ -546,6 +546,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Body = apply {
if (validated) {
return@apply
@@ -692,6 +701,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Type = apply {
if (validated) {
return@apply
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForExternalCustomerParams.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForExternalCustomerParams.kt
index e33329641..8d52e9869 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForExternalCustomerParams.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForExternalCustomerParams.kt
@@ -555,6 +555,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Body = apply {
if (validated) {
return@apply
@@ -701,6 +710,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Type = apply {
if (validated) {
return@apply
diff --git a/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForSubscriptionParams.kt b/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForSubscriptionParams.kt
index 7329b98c4..ce907a9de 100644
--- a/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForSubscriptionParams.kt
+++ b/orb-java-core/src/main/kotlin/com/withorb/api/models/AlertCreateForSubscriptionParams.kt
@@ -61,6 +61,14 @@ private constructor(
*/
fun type(): Type = body.type()
+ /**
+ * The property keys to group cost alerts by. Only applicable for cost_exceeded alerts.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the server
+ * responded with an unexpected value).
+ */
+ fun groupingKeys(): Optional> = body.groupingKeys()
+
/**
* The metric to track usage for.
*
@@ -69,6 +77,35 @@ private constructor(
*/
fun metricId(): Optional = body.metricId()
+ /**
+ * Filters to scope which prices are included in grouped cost alert evaluation. Supports
+ * filtering by price_id, item_id, or price_type with includes/excludes operators. Only
+ * applicable when grouping_keys is set.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the server
+ * responded with an unexpected value).
+ */
+ fun priceFilters(): Optional> = body.priceFilters()
+
+ /**
+ * The pricing unit to use for grouped cost alerts. Required when grouping_keys is set.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the server
+ * responded with an unexpected value).
+ */
+ fun pricingUnitId(): Optional = body.pricingUnitId()
+
+ /**
+ * Per-group threshold overrides. Each override maps a specific combination of grouping_keys
+ * values to a list of thresholds that fully replaces the default thresholds for that group. An
+ * empty thresholds list silences the group. Groups without an override use the default
+ * thresholds. Only applicable when grouping_keys is set.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the server
+ * responded with an unexpected value).
+ */
+ fun thresholdOverrides(): Optional> = body.thresholdOverrides()
+
/**
* Returns the raw JSON value of [thresholds].
*
@@ -83,6 +120,13 @@ private constructor(
*/
fun _type(): JsonField = body._type()
+ /**
+ * Returns the raw JSON value of [groupingKeys].
+ *
+ * Unlike [groupingKeys], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ fun _groupingKeys(): JsonField> = body._groupingKeys()
+
/**
* Returns the raw JSON value of [metricId].
*
@@ -90,6 +134,28 @@ private constructor(
*/
fun _metricId(): JsonField = body._metricId()
+ /**
+ * Returns the raw JSON value of [priceFilters].
+ *
+ * Unlike [priceFilters], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ fun _priceFilters(): JsonField> = body._priceFilters()
+
+ /**
+ * Returns the raw JSON value of [pricingUnitId].
+ *
+ * Unlike [pricingUnitId], this method doesn't throw if the JSON field has an unexpected type.
+ */
+ fun _pricingUnitId(): JsonField = body._pricingUnitId()
+
+ /**
+ * Returns the raw JSON value of [thresholdOverrides].
+ *
+ * Unlike [thresholdOverrides], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ fun _thresholdOverrides(): JsonField> = body._thresholdOverrides()
+
fun _additionalBodyProperties(): Map = body._additionalProperties()
/** Additional headers to send with the request. */
@@ -146,7 +212,10 @@ private constructor(
* Otherwise, it's more convenient to use the top-level setters instead:
* - [thresholds]
* - [type]
+ * - [groupingKeys]
* - [metricId]
+ * - [priceFilters]
+ * - etc.
*/
fun body(body: Body) = apply { this.body = body.toBuilder() }
@@ -182,6 +251,31 @@ private constructor(
*/
fun type(type: JsonField) = apply { body.type(type) }
+ /** The property keys to group cost alerts by. Only applicable for cost_exceeded alerts. */
+ fun groupingKeys(groupingKeys: List?) = apply { body.groupingKeys(groupingKeys) }
+
+ /** Alias for calling [Builder.groupingKeys] with `groupingKeys.orElse(null)`. */
+ fun groupingKeys(groupingKeys: Optional>) =
+ groupingKeys(groupingKeys.getOrNull())
+
+ /**
+ * Sets [Builder.groupingKeys] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.groupingKeys] with a well-typed `List` value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun groupingKeys(groupingKeys: JsonField>) = apply {
+ body.groupingKeys(groupingKeys)
+ }
+
+ /**
+ * Adds a single [String] to [groupingKeys].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addGroupingKey(groupingKey: String) = apply { body.addGroupingKey(groupingKey) }
+
/** The metric to track usage for. */
fun metricId(metricId: String?) = apply { body.metricId(metricId) }
@@ -196,6 +290,91 @@ private constructor(
*/
fun metricId(metricId: JsonField) = apply { body.metricId(metricId) }
+ /**
+ * Filters to scope which prices are included in grouped cost alert evaluation. Supports
+ * filtering by price_id, item_id, or price_type with includes/excludes operators. Only
+ * applicable when grouping_keys is set.
+ */
+ fun priceFilters(priceFilters: List?) = apply {
+ body.priceFilters(priceFilters)
+ }
+
+ /** Alias for calling [Builder.priceFilters] with `priceFilters.orElse(null)`. */
+ fun priceFilters(priceFilters: Optional>) =
+ priceFilters(priceFilters.getOrNull())
+
+ /**
+ * Sets [Builder.priceFilters] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.priceFilters] with a well-typed `List`
+ * value instead. This method is primarily for setting the field to an undocumented or not
+ * yet supported value.
+ */
+ fun priceFilters(priceFilters: JsonField>) = apply {
+ body.priceFilters(priceFilters)
+ }
+
+ /**
+ * Adds a single [PriceFilter] to [priceFilters].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addPriceFilter(priceFilter: PriceFilter) = apply { body.addPriceFilter(priceFilter) }
+
+ /** The pricing unit to use for grouped cost alerts. Required when grouping_keys is set. */
+ fun pricingUnitId(pricingUnitId: String?) = apply { body.pricingUnitId(pricingUnitId) }
+
+ /** Alias for calling [Builder.pricingUnitId] with `pricingUnitId.orElse(null)`. */
+ fun pricingUnitId(pricingUnitId: Optional) =
+ pricingUnitId(pricingUnitId.getOrNull())
+
+ /**
+ * Sets [Builder.pricingUnitId] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.pricingUnitId] with a well-typed [String] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet supported
+ * value.
+ */
+ fun pricingUnitId(pricingUnitId: JsonField) = apply {
+ body.pricingUnitId(pricingUnitId)
+ }
+
+ /**
+ * Per-group threshold overrides. Each override maps a specific combination of grouping_keys
+ * values to a list of thresholds that fully replaces the default thresholds for that group.
+ * An empty thresholds list silences the group. Groups without an override use the default
+ * thresholds. Only applicable when grouping_keys is set.
+ */
+ fun thresholdOverrides(thresholdOverrides: List?) = apply {
+ body.thresholdOverrides(thresholdOverrides)
+ }
+
+ /**
+ * Alias for calling [Builder.thresholdOverrides] with `thresholdOverrides.orElse(null)`.
+ */
+ fun thresholdOverrides(thresholdOverrides: Optional>) =
+ thresholdOverrides(thresholdOverrides.getOrNull())
+
+ /**
+ * Sets [Builder.thresholdOverrides] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.thresholdOverrides] with a well-typed
+ * `List` value instead. This method is primarily for setting the field
+ * to an undocumented or not yet supported value.
+ */
+ fun thresholdOverrides(thresholdOverrides: JsonField>) = apply {
+ body.thresholdOverrides(thresholdOverrides)
+ }
+
+ /**
+ * Adds a single [ThresholdOverride] to [thresholdOverrides].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addThresholdOverride(thresholdOverride: ThresholdOverride) = apply {
+ body.addThresholdOverride(thresholdOverride)
+ }
+
fun additionalBodyProperties(additionalBodyProperties: Map) = apply {
body.additionalProperties(additionalBodyProperties)
}
@@ -352,7 +531,11 @@ private constructor(
private constructor(
private val thresholds: JsonField>,
private val type: JsonField,
+ private val groupingKeys: JsonField>,
private val metricId: JsonField,
+ private val priceFilters: JsonField>,
+ private val pricingUnitId: JsonField,
+ private val thresholdOverrides: JsonField>,
private val additionalProperties: MutableMap,
) {
@@ -362,10 +545,31 @@ private constructor(
@ExcludeMissing
thresholds: JsonField> = JsonMissing.of(),
@JsonProperty("type") @ExcludeMissing type: JsonField = JsonMissing.of(),
+ @JsonProperty("grouping_keys")
+ @ExcludeMissing
+ groupingKeys: JsonField> = JsonMissing.of(),
@JsonProperty("metric_id")
@ExcludeMissing
metricId: JsonField = JsonMissing.of(),
- ) : this(thresholds, type, metricId, mutableMapOf())
+ @JsonProperty("price_filters")
+ @ExcludeMissing
+ priceFilters: JsonField> = JsonMissing.of(),
+ @JsonProperty("pricing_unit_id")
+ @ExcludeMissing
+ pricingUnitId: JsonField = JsonMissing.of(),
+ @JsonProperty("threshold_overrides")
+ @ExcludeMissing
+ thresholdOverrides: JsonField> = JsonMissing.of(),
+ ) : this(
+ thresholds,
+ type,
+ groupingKeys,
+ metricId,
+ priceFilters,
+ pricingUnitId,
+ thresholdOverrides,
+ mutableMapOf(),
+ )
/**
* The thresholds that define the values at which the alert will be triggered.
@@ -383,6 +587,14 @@ private constructor(
*/
fun type(): Type = type.getRequired("type")
+ /**
+ * The property keys to group cost alerts by. Only applicable for cost_exceeded alerts.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun groupingKeys(): Optional> = groupingKeys.getOptional("grouping_keys")
+
/**
* The metric to track usage for.
*
@@ -391,6 +603,36 @@ private constructor(
*/
fun metricId(): Optional = metricId.getOptional("metric_id")
+ /**
+ * Filters to scope which prices are included in grouped cost alert evaluation. Supports
+ * filtering by price_id, item_id, or price_type with includes/excludes operators. Only
+ * applicable when grouping_keys is set.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun priceFilters(): Optional> = priceFilters.getOptional("price_filters")
+
+ /**
+ * The pricing unit to use for grouped cost alerts. Required when grouping_keys is set.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun pricingUnitId(): Optional = pricingUnitId.getOptional("pricing_unit_id")
+
+ /**
+ * Per-group threshold overrides. Each override maps a specific combination of grouping_keys
+ * values to a list of thresholds that fully replaces the default thresholds for that group.
+ * An empty thresholds list silences the group. Groups without an override use the default
+ * thresholds. Only applicable when grouping_keys is set.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun thresholdOverrides(): Optional> =
+ thresholdOverrides.getOptional("threshold_overrides")
+
/**
* Returns the raw JSON value of [thresholds].
*
@@ -407,6 +649,16 @@ private constructor(
*/
@JsonProperty("type") @ExcludeMissing fun _type(): JsonField = type
+ /**
+ * Returns the raw JSON value of [groupingKeys].
+ *
+ * Unlike [groupingKeys], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("grouping_keys")
+ @ExcludeMissing
+ fun _groupingKeys(): JsonField> = groupingKeys
+
/**
* Returns the raw JSON value of [metricId].
*
@@ -414,6 +666,36 @@ private constructor(
*/
@JsonProperty("metric_id") @ExcludeMissing fun _metricId(): JsonField = metricId
+ /**
+ * Returns the raw JSON value of [priceFilters].
+ *
+ * Unlike [priceFilters], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("price_filters")
+ @ExcludeMissing
+ fun _priceFilters(): JsonField> = priceFilters
+
+ /**
+ * Returns the raw JSON value of [pricingUnitId].
+ *
+ * Unlike [pricingUnitId], this method doesn't throw if the JSON field has an unexpected
+ * type.
+ */
+ @JsonProperty("pricing_unit_id")
+ @ExcludeMissing
+ fun _pricingUnitId(): JsonField = pricingUnitId
+
+ /**
+ * Returns the raw JSON value of [thresholdOverrides].
+ *
+ * Unlike [thresholdOverrides], this method doesn't throw if the JSON field has an
+ * unexpected type.
+ */
+ @JsonProperty("threshold_overrides")
+ @ExcludeMissing
+ fun _thresholdOverrides(): JsonField> = thresholdOverrides
+
@JsonAnySetter
private fun putAdditionalProperty(key: String, value: JsonValue) {
additionalProperties.put(key, value)
@@ -445,14 +727,22 @@ private constructor(
private var thresholds: JsonField>? = null
private var type: JsonField? = null
+ private var groupingKeys: JsonField>? = null
private var metricId: JsonField = JsonMissing.of()
+ private var priceFilters: JsonField>? = null
+ private var pricingUnitId: JsonField = JsonMissing.of()
+ private var thresholdOverrides: JsonField>? = null
private var additionalProperties: MutableMap = mutableMapOf()
@JvmSynthetic
internal fun from(body: Body) = apply {
thresholds = body.thresholds.map { it.toMutableList() }
type = body.type
+ groupingKeys = body.groupingKeys.map { it.toMutableList() }
metricId = body.metricId
+ priceFilters = body.priceFilters.map { it.toMutableList() }
+ pricingUnitId = body.pricingUnitId
+ thresholdOverrides = body.thresholdOverrides.map { it.toMutableList() }
additionalProperties = body.additionalProperties.toMutableMap()
}
@@ -494,6 +784,39 @@ private constructor(
*/
fun type(type: JsonField) = apply { this.type = type }
+ /**
+ * The property keys to group cost alerts by. Only applicable for cost_exceeded alerts.
+ */
+ fun groupingKeys(groupingKeys: List?) =
+ groupingKeys(JsonField.ofNullable(groupingKeys))
+
+ /** Alias for calling [Builder.groupingKeys] with `groupingKeys.orElse(null)`. */
+ fun groupingKeys(groupingKeys: Optional>) =
+ groupingKeys(groupingKeys.getOrNull())
+
+ /**
+ * Sets [Builder.groupingKeys] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.groupingKeys] with a well-typed `List` value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun groupingKeys(groupingKeys: JsonField>) = apply {
+ this.groupingKeys = groupingKeys.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [String] to [groupingKeys].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addGroupingKey(groupingKey: String) = apply {
+ groupingKeys =
+ (groupingKeys ?: JsonField.of(mutableListOf())).also {
+ checkKnown("groupingKeys", it).add(groupingKey)
+ }
+ }
+
/** The metric to track usage for. */
fun metricId(metricId: String?) = metricId(JsonField.ofNullable(metricId))
@@ -509,6 +832,102 @@ private constructor(
*/
fun metricId(metricId: JsonField) = apply { this.metricId = metricId }
+ /**
+ * Filters to scope which prices are included in grouped cost alert evaluation. Supports
+ * filtering by price_id, item_id, or price_type with includes/excludes operators. Only
+ * applicable when grouping_keys is set.
+ */
+ fun priceFilters(priceFilters: List?) =
+ priceFilters(JsonField.ofNullable(priceFilters))
+
+ /** Alias for calling [Builder.priceFilters] with `priceFilters.orElse(null)`. */
+ fun priceFilters(priceFilters: Optional>) =
+ priceFilters(priceFilters.getOrNull())
+
+ /**
+ * Sets [Builder.priceFilters] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.priceFilters] with a well-typed `List`
+ * value instead. This method is primarily for setting the field to an undocumented or
+ * not yet supported value.
+ */
+ fun priceFilters(priceFilters: JsonField>) = apply {
+ this.priceFilters = priceFilters.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [PriceFilter] to [priceFilters].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addPriceFilter(priceFilter: PriceFilter) = apply {
+ priceFilters =
+ (priceFilters ?: JsonField.of(mutableListOf())).also {
+ checkKnown("priceFilters", it).add(priceFilter)
+ }
+ }
+
+ /**
+ * The pricing unit to use for grouped cost alerts. Required when grouping_keys is set.
+ */
+ fun pricingUnitId(pricingUnitId: String?) =
+ pricingUnitId(JsonField.ofNullable(pricingUnitId))
+
+ /** Alias for calling [Builder.pricingUnitId] with `pricingUnitId.orElse(null)`. */
+ fun pricingUnitId(pricingUnitId: Optional) =
+ pricingUnitId(pricingUnitId.getOrNull())
+
+ /**
+ * Sets [Builder.pricingUnitId] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.pricingUnitId] with a well-typed [String] value
+ * instead. This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun pricingUnitId(pricingUnitId: JsonField) = apply {
+ this.pricingUnitId = pricingUnitId
+ }
+
+ /**
+ * Per-group threshold overrides. Each override maps a specific combination of
+ * grouping_keys values to a list of thresholds that fully replaces the default
+ * thresholds for that group. An empty thresholds list silences the group. Groups
+ * without an override use the default thresholds. Only applicable when grouping_keys is
+ * set.
+ */
+ fun thresholdOverrides(thresholdOverrides: List?) =
+ thresholdOverrides(JsonField.ofNullable(thresholdOverrides))
+
+ /**
+ * Alias for calling [Builder.thresholdOverrides] with
+ * `thresholdOverrides.orElse(null)`.
+ */
+ fun thresholdOverrides(thresholdOverrides: Optional>) =
+ thresholdOverrides(thresholdOverrides.getOrNull())
+
+ /**
+ * Sets [Builder.thresholdOverrides] to an arbitrary JSON value.
+ *
+ * You should usually call [Builder.thresholdOverrides] with a well-typed
+ * `List` value instead. This method is primarily for setting the
+ * field to an undocumented or not yet supported value.
+ */
+ fun thresholdOverrides(thresholdOverrides: JsonField>) = apply {
+ this.thresholdOverrides = thresholdOverrides.map { it.toMutableList() }
+ }
+
+ /**
+ * Adds a single [ThresholdOverride] to [thresholdOverrides].
+ *
+ * @throws IllegalStateException if the field was previously set to a non-list.
+ */
+ fun addThresholdOverride(thresholdOverride: ThresholdOverride) = apply {
+ thresholdOverrides =
+ (thresholdOverrides ?: JsonField.of(mutableListOf())).also {
+ checkKnown("thresholdOverrides", it).add(thresholdOverride)
+ }
+ }
+
fun additionalProperties(additionalProperties: Map) = apply {
this.additionalProperties.clear()
putAllAdditionalProperties(additionalProperties)
@@ -545,13 +964,26 @@ private constructor(
Body(
checkRequired("thresholds", thresholds).map { it.toImmutable() },
checkRequired("type", type),
+ (groupingKeys ?: JsonMissing.of()).map { it.toImmutable() },
metricId,
+ (priceFilters ?: JsonMissing.of()).map { it.toImmutable() },
+ pricingUnitId,
+ (thresholdOverrides ?: JsonMissing.of()).map { it.toImmutable() },
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Body = apply {
if (validated) {
return@apply
@@ -559,7 +991,11 @@ private constructor(
thresholds().forEach { it.validate() }
type().validate()
+ groupingKeys()
metricId()
+ priceFilters().ifPresent { it.forEach { it.validate() } }
+ pricingUnitId()
+ thresholdOverrides().ifPresent { it.forEach { it.validate() } }
validated = true
}
@@ -581,7 +1017,11 @@ private constructor(
internal fun validity(): Int =
(thresholds.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
(type.asKnown().getOrNull()?.validity() ?: 0) +
- (if (metricId.asKnown().isPresent) 1 else 0)
+ (groupingKeys.asKnown().getOrNull()?.size ?: 0) +
+ (if (metricId.asKnown().isPresent) 1 else 0) +
+ (priceFilters.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) +
+ (if (pricingUnitId.asKnown().isPresent) 1 else 0) +
+ (thresholdOverrides.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0)
override fun equals(other: Any?): Boolean {
if (this === other) {
@@ -591,18 +1031,31 @@ private constructor(
return other is Body &&
thresholds == other.thresholds &&
type == other.type &&
+ groupingKeys == other.groupingKeys &&
metricId == other.metricId &&
+ priceFilters == other.priceFilters &&
+ pricingUnitId == other.pricingUnitId &&
+ thresholdOverrides == other.thresholdOverrides &&
additionalProperties == other.additionalProperties
}
private val hashCode: Int by lazy {
- Objects.hash(thresholds, type, metricId, additionalProperties)
+ Objects.hash(
+ thresholds,
+ type,
+ groupingKeys,
+ metricId,
+ priceFilters,
+ pricingUnitId,
+ thresholdOverrides,
+ additionalProperties,
+ )
}
override fun hashCode(): Int = hashCode
override fun toString() =
- "Body{thresholds=$thresholds, type=$type, metricId=$metricId, additionalProperties=$additionalProperties}"
+ "Body{thresholds=$thresholds, type=$type, groupingKeys=$groupingKeys, metricId=$metricId, priceFilters=$priceFilters, pricingUnitId=$pricingUnitId, thresholdOverrides=$thresholdOverrides, additionalProperties=$additionalProperties}"
}
/** The type of alert to create. This must be a valid alert type. */
@@ -692,6 +1145,15 @@ private constructor(
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 OrbInvalidDataException if any value type in this object doesn't match its
+ * expected type.
+ */
fun validate(): Type = apply {
if (validated) {
return@apply
@@ -730,6 +1192,823 @@ private constructor(
override fun toString() = value.toString()
}
+ class PriceFilter
+ @JsonCreator(mode = JsonCreator.Mode.DISABLED)
+ private constructor(
+ private val field: JsonField,
+ private val operator: JsonField,
+ private val values: JsonField>,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ @JsonCreator
+ private constructor(
+ @JsonProperty("field") @ExcludeMissing field: JsonField = JsonMissing.of(),
+ @JsonProperty("operator")
+ @ExcludeMissing
+ operator: JsonField = JsonMissing.of(),
+ @JsonProperty("values")
+ @ExcludeMissing
+ values: JsonField> = JsonMissing.of(),
+ ) : this(field, operator, values, mutableMapOf())
+
+ /**
+ * The property of the price to filter on.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected value).
+ */
+ fun field(): Field = field.getRequired("field")
+
+ /**
+ * Should prices that match the filter be included or excluded.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected value).
+ */
+ fun operator(): Operator = operator.getRequired("operator")
+
+ /**
+ * The IDs or values that match this filter.
+ *
+ * @throws OrbInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected value).
+ */
+ fun values(): List