diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b5c77a38..f72394e3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,14 +12,9 @@ jobs:
lint:
name: lint
runs-on: ubuntu-latest
-
-
steps:
- uses: actions/checkout@v4
- - name: Validate Gradle wrapper
- uses: gradle/actions/wrapper-validation@v3
-
- name: Set up Java
uses: actions/setup-java@v4
with:
@@ -30,9 +25,7 @@ jobs:
cache: gradle
- name: Set up Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/actions/setup-gradle@v4
- name: Run lints
run: ./scripts/lint
-
-
diff --git a/.github/workflows/create-releases.yml b/.github/workflows/create-releases.yml
deleted file mode 100644
index 58107af0..00000000
--- a/.github/workflows/create-releases.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-name: Create releases
-on:
- schedule:
- - cron: '0 5 * * *' # every day at 5am UTC
- push:
- branches:
- - main
-
-jobs:
- release:
- name: release
- if: github.ref == 'refs/heads/main' && github.repository == 'openlayer-ai/openlayer-java'
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - uses: stainless-api/trigger-release-please@v1
- id: release
- with:
- repo: ${{ github.event.repository.full_name }}
- stainless-api-key: ${{ secrets.STAINLESS_API_KEY }}
-
- - name: Set up Java
- if: ${{ steps.release.outputs.releases_created }}
- uses: actions/setup-java@v3
- with:
- distribution: temurin
- java-version: |
- 8
- 17
- cache: gradle
-
- - name: Set up Gradle
- if: ${{ steps.release.outputs.releases_created }}
- uses: gradle/gradle-build-action@v2
-
- - name: Publish to Sonatype
- if: ${{ steps.release.outputs.releases_created }}
- run: |
- ./gradlew --parallel --no-daemon publish
- env:
- SONATYPE_USERNAME: ${{ secrets.OPENLAYER_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
- SONATYPE_PASSWORD: ${{ secrets.OPENLAYER_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
- GPG_SIGNING_KEY_ID: ${{ secrets.OPENLAYER_SONATYPE_GPG_SIGNING_KEY_ID || secrets.GPG_SIGNING_KEY_ID }}
- GPG_SIGNING_KEY: ${{ secrets.OPENLAYER_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
- GPG_SIGNING_PASSWORD: ${{ secrets.OPENLAYER_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
diff --git a/.github/workflows/handle-release-pr-title-edit.yml b/.github/workflows/handle-release-pr-title-edit.yml
deleted file mode 100644
index ac2a6802..00000000
--- a/.github/workflows/handle-release-pr-title-edit.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Handle release PR title edits
-on:
- pull_request:
- types:
- - edited
- - unlabeled
-
-jobs:
- update_pr_content:
- name: Update pull request content
- if: |
- ((github.event.action == 'edited' && github.event.changes.title.from != github.event.pull_request.title) ||
- (github.event.action == 'unlabeled' && github.event.label.name == 'autorelease: custom version')) &&
- startsWith(github.event.pull_request.head.ref, 'release-please--') &&
- github.event.pull_request.state == 'open' &&
- github.event.sender.login != 'stainless-bot' &&
- github.event.sender.login != 'stainless-app' &&
- github.repository == 'openlayer-ai/openlayer-java'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: stainless-api/trigger-release-please@v1
- with:
- repo: ${{ github.event.repository.full_name }}
- stainless-api-key: ${{ secrets.STAINLESS_API_KEY }}
diff --git a/.gitignore b/.gitignore
index 39c31e3e..4e81838d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
.prism.log
.gradle
.idea
+.kotlin
build
codegen.log
kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index ee49ac2d..fd0ccba9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.1.0-alpha.11"
+ ".": "0.1.0-alpha.12"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index dd473053..5fc516db 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1,3 @@
-configured_endpoints: 14
+configured_endpoints: 15
+openapi_spec_hash: 9a0b363025305f6b086bcdfe43274830
+config_hash: 21fb9730d1cdc9e3fd38724c4774b894
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab2c9931..f6b6d739 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,97 @@
# Changelog
+## 0.1.0-alpha.12 (2025-04-10)
+
+Full Changelog: [v0.1.0-alpha.11...v0.1.0-alpha.12](https://github.com/openlayer-ai/openlayer-java/compare/v0.1.0-alpha.11...v0.1.0-alpha.12)
+
+### ⚠ BREAKING CHANGES
+
+* **client:** refactor exception structure and methods ([#93](https://github.com/openlayer-ai/openlayer-java/issues/93))
+* **client:** move classes into subpackages and shorten names ([#80](https://github.com/openlayer-ai/openlayer-java/issues/80))
+
+### Features
+
+* **api:** add endpoint to retrieve commit by id ([#77](https://github.com/openlayer-ai/openlayer-java/issues/77)) ([5087297](https://github.com/openlayer-ai/openlayer-java/commit/50872978eab416d1395b8619b965e7135d2aef65))
+* **api:** api update ([cedc29f](https://github.com/openlayer-ai/openlayer-java/commit/cedc29f79a787b41f0474355d38e2ef94c044123))
+* **client:** add `close` method ([207ba78](https://github.com/openlayer-ai/openlayer-java/commit/207ba785d6409b4a6b5ede70f3944313cd625b6c))
+* **client:** add enum validation method ([0c69b0d](https://github.com/openlayer-ai/openlayer-java/commit/0c69b0d67f818d7511495b196978e1d9a6f6de7d))
+* **client:** expose request body setter and getter ([#106](https://github.com/openlayer-ai/openlayer-java/issues/106)) ([8725baa](https://github.com/openlayer-ai/openlayer-java/commit/8725baa3ddd8d7fe53c607cd9d2e7f14373b37df))
+* **client:** make datetime deserialization more lenient ([#105](https://github.com/openlayer-ai/openlayer-java/issues/105)) ([ac0fa47](https://github.com/openlayer-ai/openlayer-java/commit/ac0fa470239a5ad32e3d372354f63da231a98480))
+* **client:** make union deserialization more robust ([#104](https://github.com/openlayer-ai/openlayer-java/issues/104)) ([0c69b0d](https://github.com/openlayer-ai/openlayer-java/commit/0c69b0d67f818d7511495b196978e1d9a6f6de7d))
+* **client:** support a lower jackson version ([#99](https://github.com/openlayer-ai/openlayer-java/issues/99)) ([86e9363](https://github.com/openlayer-ai/openlayer-java/commit/86e936338872071e1dcc80f0549adcb21f8aa205))
+* **client:** support setting base URL via env var ([08e6ac1](https://github.com/openlayer-ai/openlayer-java/commit/08e6ac1c7327d8c832035b29242b10c8678f2543))
+* **client:** throw on incompatible jackson version ([86e9363](https://github.com/openlayer-ai/openlayer-java/commit/86e936338872071e1dcc80f0549adcb21f8aa205))
+
+
+### Bug Fixes
+
+* **client:** bump to better jackson version ([fc11f45](https://github.com/openlayer-ai/openlayer-java/commit/fc11f45df797c93281e670e35ecd00c6530d0af4))
+* **client:** don't call `validate()` during deserialization if we don't have to ([#101](https://github.com/openlayer-ai/openlayer-java/issues/101)) ([5ce9c06](https://github.com/openlayer-ai/openlayer-java/commit/5ce9c0686bbf66a4f0e3e2acad3356664671d287))
+* **client:** limit json deserialization coercion ([#102](https://github.com/openlayer-ai/openlayer-java/issues/102)) ([15fddc3](https://github.com/openlayer-ai/openlayer-java/commit/15fddc34beea081f9afb69dc87302b65c1e82340))
+* **client:** make some classes and constructors non-public ([882eb61](https://github.com/openlayer-ai/openlayer-java/commit/882eb61286bb52c15224a36677a934523ccb4fb3))
+* **client:** map deserialization bug ([17fc1d7](https://github.com/openlayer-ai/openlayer-java/commit/17fc1d796ea7b17d34c0b14374fff3420a14397e))
+* **client:** return `Optional<T>` instead of `Optional extends T>` ([#109](https://github.com/openlayer-ai/openlayer-java/issues/109)) ([c2c1501](https://github.com/openlayer-ai/openlayer-java/commit/c2c150161ea17067c68e3ce7c54deebdf2461c4a))
+* **client:** support kotlin 1.8 runtime ([#90](https://github.com/openlayer-ai/openlayer-java/issues/90)) ([f2b112b](https://github.com/openlayer-ai/openlayer-java/commit/f2b112b2f218ba39307aad1ed17c0d6114142d3a))
+* pluralize `list` response variables ([#103](https://github.com/openlayer-ai/openlayer-java/issues/103)) ([c1cde72](https://github.com/openlayer-ai/openlayer-java/commit/c1cde726f563abe38a5188647c6f05df01d034e8))
+
+
+### Performance Improvements
+
+* **client:** cached parsed type in `HttpResponseFor` ([#107](https://github.com/openlayer-ai/openlayer-java/issues/107)) ([3bd7a4c](https://github.com/openlayer-ai/openlayer-java/commit/3bd7a4cc174f22a5e2ef4510ee1576563ad608c2))
+
+
+### Chores
+
+* **client:** move classes into subpackages and shorten names ([#80](https://github.com/openlayer-ai/openlayer-java/issues/80)) ([6e35cbf](https://github.com/openlayer-ai/openlayer-java/commit/6e35cbf67eab70e90be376b4835a9b000f265cf9))
+* **client:** refactor exception structure and methods ([#93](https://github.com/openlayer-ai/openlayer-java/issues/93)) ([d1ee889](https://github.com/openlayer-ai/openlayer-java/commit/d1ee889f1b5a4b8e97fff690f0c4db997441278f))
+* **client:** remove unnecessary json state from some query param classes ([0c69b0d](https://github.com/openlayer-ai/openlayer-java/commit/0c69b0d67f818d7511495b196978e1d9a6f6de7d))
+* **internal:** add invalid json deserialization tests ([0c69b0d](https://github.com/openlayer-ai/openlayer-java/commit/0c69b0d67f818d7511495b196978e1d9a6f6de7d))
+* **internal:** add json roundtripping tests ([0c69b0d](https://github.com/openlayer-ai/openlayer-java/commit/0c69b0d67f818d7511495b196978e1d9a6f6de7d))
+* **internal:** add missing release please block ([#92](https://github.com/openlayer-ai/openlayer-java/issues/92)) ([722165b](https://github.com/openlayer-ai/openlayer-java/commit/722165bcfa13936fc3ed70592833293495f1ed20))
+* **internal:** codegen related update ([c23eea2](https://github.com/openlayer-ai/openlayer-java/commit/c23eea21cd8a3895c7ed616998516df64ac1b9dd))
+* **internal:** codegen related update ([2885a5a](https://github.com/openlayer-ai/openlayer-java/commit/2885a5a402fa4cea4e5363a5dbec1bd255a9060b))
+* **internal:** codegen related update ([a62fb1f](https://github.com/openlayer-ai/openlayer-java/commit/a62fb1f2042721f6ce4eb84ff33bfd9704829266))
+* **internal:** codegen related update ([ca04676](https://github.com/openlayer-ai/openlayer-java/commit/ca0467688568fe952dc57739c6306209f68761f8))
+* **internal:** codegen related update ([#79](https://github.com/openlayer-ai/openlayer-java/issues/79)) ([3ee21d4](https://github.com/openlayer-ai/openlayer-java/commit/3ee21d4a05dddebac33326fd9c0cbde11bf39062))
+* **internal:** codegen related update ([#85](https://github.com/openlayer-ai/openlayer-java/issues/85)) ([3d4d199](https://github.com/openlayer-ai/openlayer-java/commit/3d4d19974440297e5b1f4fdd220ae2c29adfad00))
+* **internal:** delete duplicate tests ([ece3d6d](https://github.com/openlayer-ai/openlayer-java/commit/ece3d6d3f438671e2b4fab916f9295c668d2f137))
+* **internal:** delete unused methods and annotations ([#100](https://github.com/openlayer-ai/openlayer-java/issues/100)) ([17fc1d7](https://github.com/openlayer-ai/openlayer-java/commit/17fc1d796ea7b17d34c0b14374fff3420a14397e))
+* **internal:** expand CI branch coverage ([273f4a7](https://github.com/openlayer-ai/openlayer-java/commit/273f4a712c63235e663b5e2ddba53797f2cf441c))
+* **internal:** fix example formatting ([#95](https://github.com/openlayer-ai/openlayer-java/issues/95)) ([5c782e5](https://github.com/openlayer-ai/openlayer-java/commit/5c782e5d75daafb3eea58bbe5c834a3cf3444783))
+* **internal:** generate more tests ([db068dc](https://github.com/openlayer-ai/openlayer-java/commit/db068dc6e79b6a3fa77fe7b22a9a7bf31b61b8d9))
+* **internal:** make multipart assertions more robust ([ac5dc3b](https://github.com/openlayer-ai/openlayer-java/commit/ac5dc3bb775247428b484019d4f5a936877dac0e))
+* **internal:** reduce CI branch coverage ([3169980](https://github.com/openlayer-ai/openlayer-java/commit/3169980b67081c80cadb5af4869982a2a3eadedd))
+* **internal:** refactor `ErrorHandlingTest` ([2fca9f1](https://github.com/openlayer-ai/openlayer-java/commit/2fca9f1dbdc492be75166cc9727660dffd325ce8))
+* **internal:** refactor enum query param serialization ([#91](https://github.com/openlayer-ai/openlayer-java/issues/91)) ([b928602](https://github.com/openlayer-ai/openlayer-java/commit/b92860288151baab73f6ad26ffbd0976a3f54aab))
+* **internal:** refactor query param serialization impl and tests ([#87](https://github.com/openlayer-ai/openlayer-java/issues/87)) ([301bc32](https://github.com/openlayer-ai/openlayer-java/commit/301bc32db49ad7d0b417b98996f11ed445c2501c))
+* **internal:** refactor some test assertions ([ece3d6d](https://github.com/openlayer-ai/openlayer-java/commit/ece3d6d3f438671e2b4fab916f9295c668d2f137))
+* **internal:** reformat some tests ([#89](https://github.com/openlayer-ai/openlayer-java/issues/89)) ([db068dc](https://github.com/openlayer-ai/openlayer-java/commit/db068dc6e79b6a3fa77fe7b22a9a7bf31b61b8d9))
+* **internal:** remove extra empty newlines ([#81](https://github.com/openlayer-ai/openlayer-java/issues/81)) ([9eba32c](https://github.com/openlayer-ai/openlayer-java/commit/9eba32cbaa34b9c97ac7efd81533066229470ea5))
+* **internal:** remove some unnecessary `constructor` keywords ([882eb61](https://github.com/openlayer-ai/openlayer-java/commit/882eb61286bb52c15224a36677a934523ccb4fb3))
+* **internal:** remove unnecessary `assertNotNull` calls ([ac5dc3b](https://github.com/openlayer-ai/openlayer-java/commit/ac5dc3bb775247428b484019d4f5a936877dac0e))
+* **internal:** remove unnecessary import ([#96](https://github.com/openlayer-ai/openlayer-java/issues/96)) ([4852f0f](https://github.com/openlayer-ai/openlayer-java/commit/4852f0fc5face081a9d899a940ac15ee771df2e4))
+* **internal:** remove workflow ([54e8c73](https://github.com/openlayer-ai/openlayer-java/commit/54e8c738a650f838cdc25c71ad355c17b6b23867))
+* **internal:** rename `getPathParam` ([#88](https://github.com/openlayer-ai/openlayer-java/issues/88)) ([ece3d6d](https://github.com/openlayer-ai/openlayer-java/commit/ece3d6d3f438671e2b4fab916f9295c668d2f137))
+* **internal:** reorder some params methodsc ([ece3d6d](https://github.com/openlayer-ai/openlayer-java/commit/ece3d6d3f438671e2b4fab916f9295c668d2f137))
+* **internal:** swap from `getNullable` to `getOptional` ([#108](https://github.com/openlayer-ai/openlayer-java/issues/108)) ([3975381](https://github.com/openlayer-ai/openlayer-java/commit/3975381f2e6a9183a11200ca5a2e1ff5a260e784))
+* **internal:** version bump ([4937ab9](https://github.com/openlayer-ai/openlayer-java/commit/4937ab9a3d67bdb756b8632dd934d4330e3b6f5a))
+* **tests:** improve enum examples ([#111](https://github.com/openlayer-ai/openlayer-java/issues/111)) ([b8c855b](https://github.com/openlayer-ai/openlayer-java/commit/b8c855b09d13118ca76e7c053f5a1c76f33c4640))
+
+
+### Documentation
+
+* `async` and `sync` method comments ([0083c41](https://github.com/openlayer-ai/openlayer-java/commit/0083c41a6cc82a368e64ec0a812935597fa330e5))
+* add `build` method comments ([#86](https://github.com/openlayer-ai/openlayer-java/issues/86)) ([e6da0bf](https://github.com/openlayer-ai/openlayer-java/commit/e6da0bfc278f0405e7a875e53574abff11033bea))
+* add client documentation ([ad4f217](https://github.com/openlayer-ai/openlayer-java/commit/ad4f217728a1fc01dacc4587004a086350f4ccc4))
+* add comments to `JsonField` classes ([c2c1501](https://github.com/openlayer-ai/openlayer-java/commit/c2c150161ea17067c68e3ce7c54deebdf2461c4a))
+* builder, enum, and union comments ([5f394f5](https://github.com/openlayer-ai/openlayer-java/commit/5f394f52f0d57b38f9664f135e58fa89f98a19d5))
+* deduplicate and refine comments ([#84](https://github.com/openlayer-ai/openlayer-java/issues/84)) ([687dd63](https://github.com/openlayer-ai/openlayer-java/commit/687dd630eed0499e0b786c0df214c8671ae43402))
+* document how to forcibly omit required field ([f7bf9f7](https://github.com/openlayer-ai/openlayer-java/commit/f7bf9f790253baaf2b8f9a192eac238db160e62b))
+* minor readme tweak ([#98](https://github.com/openlayer-ai/openlayer-java/issues/98)) ([dd9ea15](https://github.com/openlayer-ai/openlayer-java/commit/dd9ea155bb0a7f67ab500de13ee44def664d253e))
+* refine comments on multipart params ([#94](https://github.com/openlayer-ai/openlayer-java/issues/94)) ([ac5dc3b](https://github.com/openlayer-ai/openlayer-java/commit/ac5dc3bb775247428b484019d4f5a936877dac0e))
+* swap examples used in readme ([#110](https://github.com/openlayer-ai/openlayer-java/issues/110)) ([f7bf9f7](https://github.com/openlayer-ai/openlayer-java/commit/f7bf9f790253baaf2b8f9a192eac238db160e62b))
+* update readme exception docs ([#97](https://github.com/openlayer-ai/openlayer-java/issues/97)) ([7eafb98](https://github.com/openlayer-ai/openlayer-java/commit/7eafb987ad975163a4bda700271c3c752f6b4430))
+
## 0.1.0-alpha.11 (2024-12-20)
Full Changelog: [v0.1.0-alpha.10...v0.1.0-alpha.11](https://github.com/openlayer-ai/openlayer-java/compare/v0.1.0-alpha.10...v0.1.0-alpha.11)
diff --git a/LICENSE b/LICENSE
index 82530825..ac864c56 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2024 Openlayer
+ Copyright 2025 Openlayer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index ed0596e7..aab9176b 100644
--- a/README.md
+++ b/README.md
@@ -2,203 +2,310 @@
-[](https://central.sonatype.com/artifact/com.openlayer.api/openlayer-java/0.1.0-alpha.11)
+[](https://central.sonatype.com/artifact/com.openlayer.api/openlayer-java/0.1.0-alpha.12)
+[](https://javadoc.io/doc/com.openlayer.api/openlayer-java/0.1.0-alpha.12)
-The Openlayer Java SDK provides convenient access to the Openlayer REST API from applications written in Java. It includes helper classes with helpful types and documentation for every request and response property.
+The Openlayer Java SDK provides convenient access to the [Openlayer REST API](https://openlayer.com/docs/api-reference/rest/overview) from applications written in Java.
-The Openlayer Java SDK is similar to the Openlayer Kotlin SDK but with minor differences that make it more ergonomic for use in Java, such as `Optional` instead of nullable values, `Stream` instead of `Sequence`, and `CompletableFuture` instead of suspend functions.
+It is generated with [Stainless](https://www.stainless.com/).
-It is generated with [Stainless](https://www.stainlessapi.com/).
-
-## Documentation
-
-The REST API documentation can be found on [openlayer.com](https://openlayer.com/docs/api-reference/rest/overview).
-
----
+
-## Getting started
+The REST API documentation can be found on [openlayer.com](https://openlayer.com/docs/api-reference/rest/overview). Javadocs are also available on [javadoc.io](https://javadoc.io/doc/com.openlayer.api/openlayer-java/0.1.0-alpha.12).
-### Install dependencies
+
-#### Gradle
+## Installation
+### Gradle
+
```kotlin
-implementation("com.openlayer.api:openlayer-java:0.1.0-alpha.11")
+implementation("com.openlayer.api:openlayer-java:0.1.0-alpha.12")
```
-#### Maven
+### Maven
```xml
- com.openlayer.api
- openlayer-java
- 0.1.0-alpha.11
+ com.openlayer.api
+ openlayer-java
+ 0.1.0-alpha.12
```
-### Configure the client
+## Requirements
-Use `OpenlayerOkHttpClient.builder()` to configure the client.
+This library requires Java 8 or later.
-Alternately, set the environment with `OPENLAYER_API_KEY`, and use `OpenlayerOkHttpClient.fromEnv()` to read from the environment.
+## Usage
```java
import com.openlayer.api.client.OpenlayerClient;
import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+// Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
OpenlayerClient client = OpenlayerOkHttpClient.fromEnv();
-// Note: you can also call fromEnv() from the client builder, for example if you need to set additional properties
+DataStreamParams params = DataStreamParams.builder()
+ .inferencePipelineId("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .config(DataStreamParams.Config.LlmData.builder()
+ .addInputVariableName("user_query")
+ .outputColumnName("output")
+ .numOfTokenColumnName("tokens")
+ .costColumnName("cost")
+ .timestampColumnName("timestamp")
+ .build())
+ .addRow(DataStreamParams.Row.builder()
+ .putAdditionalProperty("user_query", JsonValue.from("what is the meaning of life?"))
+ .putAdditionalProperty("output", JsonValue.from("42"))
+ .putAdditionalProperty("tokens", JsonValue.from(7))
+ .putAdditionalProperty("cost", JsonValue.from(0.02))
+ .putAdditionalProperty("timestamp", JsonValue.from(1610000000))
+ .build())
+ .build();
+DataStreamResponse response = client.inferencePipelines().data().stream(params);
+```
+
+## Client configuration
+
+Configure the client using environment variables:
+
+```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+
+// Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
+OpenlayerClient client = OpenlayerOkHttpClient.fromEnv();
+```
+
+Or manually:
+
+```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+
+OpenlayerClient client = OpenlayerOkHttpClient.builder()
+ .apiKey("My API Key")
+ .build();
+```
+
+Or using a combination of the two approaches:
+
+```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+
OpenlayerClient client = OpenlayerOkHttpClient.builder()
+ // Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
.fromEnv()
- // ... set properties on the builder
+ .apiKey("My API Key")
.build();
```
-| Property | Environment variable | Required | Default value |
-| -------- | -------------------- | -------- | ------------- |
-| apiKey | `OPENLAYER_API_KEY` | false | — |
+See this table for the available options:
+
+| Setter | Environment variable | Required | Default value |
+| --------- | -------------------- | -------- | -------------------------------- |
+| `apiKey` | `OPENLAYER_API_KEY` | false | - |
+| `baseUrl` | `OPENLAYER_BASE_URL` | true | `"https://api.openlayer.com/v1"` |
+
+> [!TIP]
+> Don't create more than one client in the same application. Each client has a connection pool and
+> thread pools, which are more efficient to share between requests.
-Read the documentation for more configuration options.
+## Requests and responses
----
+To send a request to the Openlayer API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
-### Example: creating a resource
+For example, `client.inferencePipelines().data().stream(...)` should be called with an instance of `DataStreamParams`, and it will return an instance of `DataStreamResponse`.
-To create a new inference pipeline data, first use the `InferencePipelineDataStreamParams` builder to specify attributes, then pass that to the `stream` method of the `data` service.
+## Immutability
+
+Each class in the SDK has an associated [builder](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) or factory method for constructing it.
+
+Each class is [immutable](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html) once constructed. If the class has an associated builder, then it has a `toBuilder()` method, which can be used to convert it back to a builder for making a modified copy.
+
+Because each class is immutable, builder modification will _never_ affect already built class instances.
+
+## Asynchronous execution
+
+The default client is synchronous. To switch to asynchronous execution, call the `async()` method:
```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
import com.openlayer.api.core.JsonValue;
-import com.openlayer.api.models.InferencePipelineDataStreamParams;
-import com.openlayer.api.models.InferencePipelineDataStreamResponse;
-import java.util.List;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+import java.util.concurrent.CompletableFuture;
+
+// Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
+OpenlayerClient client = OpenlayerOkHttpClient.fromEnv();
-InferencePipelineDataStreamParams params = InferencePipelineDataStreamParams.builder()
+DataStreamParams params = DataStreamParams.builder()
.inferencePipelineId("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
- .config(InferencePipelineDataStreamParams.Config.ofLlmData(InferencePipelineDataStreamParams.Config.LlmData.builder()
- .inputVariableNames(List.of("user_query"))
+ .config(DataStreamParams.Config.LlmData.builder()
+ .addInputVariableName("user_query")
.outputColumnName("output")
.numOfTokenColumnName("tokens")
.costColumnName("cost")
.timestampColumnName("timestamp")
- .build()))
- .rows(List.of(InferencePipelineDataStreamParams.Row.builder()
+ .build())
+ .addRow(DataStreamParams.Row.builder()
.putAdditionalProperty("user_query", JsonValue.from("what is the meaning of life?"))
.putAdditionalProperty("output", JsonValue.from("42"))
.putAdditionalProperty("tokens", JsonValue.from(7))
.putAdditionalProperty("cost", JsonValue.from(0.02))
.putAdditionalProperty("timestamp", JsonValue.from(1610000000))
- .build()))
+ .build())
.build();
-InferencePipelineDataStreamResponse response = client.inferencePipelines().data().stream(params);
+CompletableFuture response = client.async().inferencePipelines().data().stream(params);
```
----
+Or create an asynchronous client from the beginning:
+
+```java
+import com.openlayer.api.client.OpenlayerClientAsync;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClientAsync;
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+import java.util.concurrent.CompletableFuture;
-## Requests
+// Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
+OpenlayerClientAsync client = OpenlayerOkHttpClientAsync.fromEnv();
+
+DataStreamParams params = DataStreamParams.builder()
+ .inferencePipelineId("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .config(DataStreamParams.Config.LlmData.builder()
+ .addInputVariableName("user_query")
+ .outputColumnName("output")
+ .numOfTokenColumnName("tokens")
+ .costColumnName("cost")
+ .timestampColumnName("timestamp")
+ .build())
+ .addRow(DataStreamParams.Row.builder()
+ .putAdditionalProperty("user_query", JsonValue.from("what is the meaning of life?"))
+ .putAdditionalProperty("output", JsonValue.from("42"))
+ .putAdditionalProperty("tokens", JsonValue.from(7))
+ .putAdditionalProperty("cost", JsonValue.from(0.02))
+ .putAdditionalProperty("timestamp", JsonValue.from(1610000000))
+ .build())
+ .build();
+CompletableFuture response = client.inferencePipelines().data().stream(params);
+```
-### Parameters and bodies
+The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
-To make a request to the Openlayer API, you generally build an instance of the appropriate `Params` class.
+## Raw responses
-In [Example: creating a resource](#example-creating-a-resource) above, we used the `InferencePipelineDataStreamParams.builder()` to pass to the `stream` method of the `data` service.
+The SDK defines methods that deserialize responses into instances of Java classes. However, these methods don't provide access to the response headers, status code, or the raw response body.
-Sometimes, the API may support other properties that are not yet supported in the Java SDK types. In that case, you can attach them using the `putAdditionalProperty` method.
+To access this data, prefix any HTTP method call on a client or service with `withRawResponse()`:
```java
import com.openlayer.api.core.JsonValue;
-import com.openlayer.api.models.InferencePipelineDataStreamParams;
+import com.openlayer.api.core.http.Headers;
+import com.openlayer.api.core.http.HttpResponseFor;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
-InferencePipelineDataStreamParams params = InferencePipelineDataStreamParams.builder()
- // ... normal properties
- .putAdditionalProperty("secret_param", JsonValue.from("4242"))
+DataStreamParams params = DataStreamParams.builder()
+ .inferencePipelineId("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .config(DataStreamParams.Config.LlmData.builder()
+ .addInputVariableName("user_query")
+ .outputColumnName("output")
+ .numOfTokenColumnName("tokens")
+ .costColumnName("cost")
+ .timestampColumnName("timestamp")
+ .build())
+ .addRow(DataStreamParams.Row.builder()
+ .putAdditionalProperty("user_query", JsonValue.from("what is the meaning of life?"))
+ .putAdditionalProperty("output", JsonValue.from("42"))
+ .putAdditionalProperty("tokens", JsonValue.from(7))
+ .putAdditionalProperty("cost", JsonValue.from(0.02))
+ .putAdditionalProperty("timestamp", JsonValue.from(1610000000))
+ .build())
.build();
-```
-
-## Responses
+HttpResponseFor response = client.inferencePipelines().data().withRawResponse().stream(params);
-### Response validation
+int statusCode = response.statusCode();
+Headers headers = response.headers();
+```
-When receiving a response, the Openlayer Java SDK will deserialize it into instances of the typed model classes. In rare cases, the API may return a response property that doesn't match the expected Java type. If you directly access the mistaken property, the SDK will throw an unchecked `OpenlayerInvalidDataException` at runtime. If you would prefer to check in advance that that response is completely well-typed, call `.validate()` on the returned model.
+You can still deserialize the response into an instance of a Java class if needed:
```java
-import com.openlayer.api.models.InferencePipelineDataStreamResponse;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
-InferencePipelineDataStreamResponse response = client.inferencePipelines().data().stream().validate();
+DataStreamResponse parsedResponse = response.parse();
```
-### Response properties as JSON
-
-In rare cases, you may want to access the underlying JSON value for a response property rather than using the typed version provided by this SDK. Each model property has a corresponding JSON version, with an underscore before the method name, which returns a `JsonField` value.
+## Error handling
-```java
-import com.openlayer.api.core.JsonField;
-import java.util.Optional;
+The SDK throws custom unchecked exception types:
-JsonField field = responseObj._field();
+- [`OpenlayerServiceException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:
-if (field.isMissing()) {
- // Value was not specified in the JSON response
-} else if (field.isNull()) {
- // Value was provided as a literal null
-} else {
- // See if value was provided as a string
- Optional jsonString = field.asString();
+ | Status | Exception |
+ | ------ | -------------------------------------------------------------------------------------------------------------------------------- |
+ | 400 | [`BadRequestException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/BadRequestException.kt) |
+ | 401 | [`UnauthorizedException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/UnauthorizedException.kt) |
+ | 403 | [`PermissionDeniedException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/PermissionDeniedException.kt) |
+ | 404 | [`NotFoundException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/NotFoundException.kt) |
+ | 422 | [`UnprocessableEntityException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/UnprocessableEntityException.kt) |
+ | 429 | [`RateLimitException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/RateLimitException.kt) |
+ | 5xx | [`InternalServerException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/InternalServerException.kt) |
+ | others | [`UnexpectedStatusCodeException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/UnexpectedStatusCodeException.kt) |
- // If the value given by the API did not match the shape that the SDK expects
- // you can deserialise into a custom type
- MyClass myObj = responseObj._field().asUnknown().orElseThrow().convert(MyClass.class);
-}
-```
+- [`OpenlayerIoException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerIoException.kt): I/O networking errors.
-### Additional model properties
+- [`OpenlayerInvalidDataException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response.
-Sometimes, the server response may include additional properties that are not yet available in this library's types. You can access them using the model's `_additionalProperties` method:
+- [`OpenlayerException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.
-```java
-import com.openlayer.api.core.JsonValue;
+## Logging
-JsonValue secret = projectCreateResponse._additionalProperties().get("secret_field");
-```
+The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
----
+Enable logging by setting the `OPENLAYER_LOG` environment variable to `info`:
----
+```sh
+$ export OPENLAYER_LOG=info
+```
-## Error handling
+Or to `debug` for more verbose logging:
-This library throws exceptions in a single hierarchy for easy handling:
+```sh
+$ export OPENLAYER_LOG=debug
+```
-- **`OpenlayerException`** - Base exception for all exceptions
+## Network options
-- **`OpenlayerServiceException`** - HTTP errors with a well-formed response body we were able to parse. The exception message and the `.debuggingRequestId()` will be set by the server.
+### Retries
- | 400 | BadRequestException |
- | ------ | ----------------------------- |
- | 401 | AuthenticationException |
- | 403 | PermissionDeniedException |
- | 404 | NotFoundException |
- | 422 | UnprocessableEntityException |
- | 429 | RateLimitException |
- | 5xx | InternalServerException |
- | others | UnexpectedStatusCodeException |
+The SDK automatically retries 2 times by default, with a short exponential backoff.
-- **`OpenlayerIoException`** - I/O networking errors
-- **`OpenlayerInvalidDataException`** - any other exceptions on the client side, e.g.:
- - We failed to serialize the request body
- - We failed to parse the response body (has access to response code and body)
+Only the following error types are retried:
-## Network options
+- Connection errors (for example, due to a network connectivity problem)
+- 408 Request Timeout
+- 409 Conflict
+- 429 Rate Limit
+- 5xx Internal
-### Retries
+The API may also explicitly instruct the SDK to retry or not retry a response.
-Requests that experience certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default. You can provide a `maxRetries` on the client builder to configure this:
+To set a custom number of retries, configure the client using the `maxRetries` method:
```java
import com.openlayer.api.client.OpenlayerClient;
@@ -212,7 +319,21 @@ OpenlayerClient client = OpenlayerOkHttpClient.builder()
### Timeouts
-Requests time out after 1 minute by default. You can configure this on the client builder:
+Requests time out after 1 minute by default.
+
+To set a custom timeout, configure the method call using the `timeout` method:
+
+```java
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+
+DataStreamResponse response = client.inferencePipelines().data().stream(
+ params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
+);
+```
+
+Or configure the default for all method calls at the client level:
```java
import com.openlayer.api.client.OpenlayerClient;
@@ -227,7 +348,7 @@ OpenlayerClient client = OpenlayerOkHttpClient.builder()
### Proxies
-Requests can be routed through a proxy. You can configure this on the client builder:
+To route requests through a proxy, configure the client using the `proxy` method:
```java
import com.openlayer.api.client.OpenlayerClient;
@@ -237,56 +358,261 @@ import java.net.Proxy;
OpenlayerClient client = OpenlayerOkHttpClient.builder()
.fromEnv()
- .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("example.com", 8080)))
+ .proxy(new Proxy(
+ Proxy.Type.HTTP, new InetSocketAddress(
+ "https://example.com", 8080
+ )
+ ))
.build();
```
-## Making custom/undocumented requests
+## Undocumented API functionality
-This library is typed for convenient access to the documented API. If you need to access undocumented params or response properties, the library can still be used.
+The SDK is typed for convenient usage of the documented API. However, it also supports working with undocumented or not yet supported parts of the API.
-### Undocumented request params
+### Parameters
-To make requests using undocumented parameters, you can provide or override parameters on the params object while building it.
+To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQueryParam`, or `putAdditionalBodyProperty` methods on any `Params` class:
-```kotlin
-FooCreateParams address = FooCreateParams.builder()
- .id("my_id")
- .putAdditionalProperty("secret_prop", JsonValue.from("hello"))
+```java
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+
+DataStreamParams params = DataStreamParams.builder()
+ .putAdditionalHeader("Secret-Header", "42")
+ .putAdditionalQueryParam("secret_query_param", "42")
+ .putAdditionalBodyProperty("secretProperty", JsonValue.from("42"))
.build();
```
-### Undocumented response properties
+These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods.
-To access undocumented response properties, you can use `res._additionalProperties()` on a response object to get a map of untyped fields of type `Map`. You can then access fields like `._additionalProperties().get("secret_prop").asString()` or use other helpers defined on the `JsonValue` class to extract it to a desired type.
+To set undocumented parameters on _nested_ headers, query params, or body classes, call the `putAdditionalProperty` method on the nested class:
-## Logging
+```java
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.projects.ProjectCreateParams;
-We use the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
+ProjectCreateParams params = ProjectCreateParams.builder()
+ .links(ProjectCreateParams.Links.builder()
+ .putAdditionalProperty("secretProperty", JsonValue.from("42"))
+ .build())
+ .build();
+```
-You can enable logging by setting the environment variable `OPENLAYER_LOG` to `info`.
+These properties can be accessed on the nested built object later using the `_additionalProperties()` method.
-```sh
-$ export OPENLAYER_LOG=info
+To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt) object to its setter:
+
+```java
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+
+DataStreamParams params = DataStreamParams.builder()
+ .inferencePipelineId("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .config(DataStreamParams.Config.LlmData.builder()
+ .addInputVariableName("user_query")
+ .outputColumnName("output")
+ .numOfTokenColumnName("tokens")
+ .costColumnName("cost")
+ .timestampColumnName("timestamp")
+ .build())
+ .rows(JsonValue.from(42))
+ .build();
```
-Or to `debug` for more verbose logging.
+The most straightforward way to create a [`JsonValue`](openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt) is using its `from(...)` method:
-```sh
-$ export OPENLAYER_LOG=debug
+```java
+import com.openlayer.api.core.JsonValue;
+import java.util.List;
+import java.util.Map;
+
+// Create primitive JSON values
+JsonValue nullValue = JsonValue.from(null);
+JsonValue booleanValue = JsonValue.from(true);
+JsonValue numberValue = JsonValue.from(42);
+JsonValue stringValue = JsonValue.from("Hello World!");
+
+// Create a JSON array value equivalent to `["Hello", "World"]`
+JsonValue arrayValue = JsonValue.from(List.of(
+ "Hello", "World"
+));
+
+// Create a JSON object value equivalent to `{ "a": 1, "b": 2 }`
+JsonValue objectValue = JsonValue.from(Map.of(
+ "a", 1,
+ "b", 2
+));
+
+// Create an arbitrarily nested JSON equivalent to:
+// {
+// "a": [1, 2],
+// "b": [3, 4]
+// }
+JsonValue complexValue = JsonValue.from(Map.of(
+ "a", List.of(
+ 1, 2
+ ),
+ "b", List.of(
+ 3, 4
+ )
+));
+```
+
+Normally a `Builder` class's `build` method will throw [`IllegalStateException`](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html) if any required parameter or property is unset.
+
+To forcibly omit a required parameter or property, pass [`JsonMissing`](openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt):
+
+```java
+import com.openlayer.api.core.JsonMissing;
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+
+DataStreamParams params = DataStreamParams.builder()
+ .config(DataStreamParams.Config.LlmData.builder()
+ .outputColumnName("output")
+ .build())
+ .addRow(DataStreamParams.Row.builder()
+ .putAdditionalProperty("user_query", JsonValue.from("bar"))
+ .putAdditionalProperty("output", JsonValue.from("bar"))
+ .putAdditionalProperty("tokens", JsonValue.from("bar"))
+ .putAdditionalProperty("cost", JsonValue.from("bar"))
+ .putAdditionalProperty("timestamp", JsonValue.from("bar"))
+ .build())
+ .inferencePipelineId(JsonMissing.of())
+ .build();
+```
+
+### Response properties
+
+To access undocumented response properties, call the `_additionalProperties()` method:
+
+```java
+import com.openlayer.api.core.JsonValue;
+import java.util.Map;
+
+Map additionalProperties = client.inferencePipelines().data().stream(params)._additionalProperties();
+JsonValue secretPropertyValue = additionalProperties.get("secretProperty");
+
+String result = secretPropertyValue.accept(new JsonValue.Visitor<>() {
+ @Override
+ public String visitNull() {
+ return "It's null!";
+ }
+
+ @Override
+ public String visitBoolean(boolean value) {
+ return "It's a boolean!";
+ }
+
+ @Override
+ public String visitNumber(Number value) {
+ return "It's a number!";
+ }
+
+ // Other methods include `visitMissing`, `visitString`, `visitArray`, and `visitObject`
+ // The default implementation of each unimplemented method delegates to `visitDefault`, which throws by default, but can also be overridden
+});
```
+To access a property's raw JSON value, which may be undocumented, call its `_` prefixed method:
+
+```java
+import com.openlayer.api.core.JsonField;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import java.util.Optional;
+
+JsonField config = client.inferencePipelines().data().stream(params)._config();
+
+if (config.isMissing()) {
+ // The property is absent from the JSON response
+} else if (config.isNull()) {
+ // The property was set to literal null
+} else {
+ // Check if value was provided as a string
+ // Other methods include `asNumber()`, `asBoolean()`, etc.
+ Optional jsonString = config.asString();
+
+ // Try to deserialize into a custom type
+ MyClass myObject = config.asUnknown().orElseThrow().convert(MyClass.class);
+}
+```
+
+### Response validation
+
+In rare cases, the API may return a response that doesn't match the expected type. For example, the SDK may expect a property to contain a `String`, but the API could return something else.
+
+By default, the SDK will not throw an exception in this case. It will throw [`OpenlayerInvalidDataException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerInvalidDataException.kt) only if you directly access the property.
+
+If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
+
+```java
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+
+DataStreamResponse response = client.inferencePipelines().data().stream(params).validate();
+```
+
+Or configure the method call to validate the response using the `responseValidation` method:
+
+```java
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+
+DataStreamResponse response = client.inferencePipelines().data().stream(
+ params, RequestOptions.builder().responseValidation(true).build()
+);
+```
+
+Or configure the default for all method calls at the client level:
+
+```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+
+OpenlayerClient client = OpenlayerOkHttpClient.builder()
+ .fromEnv()
+ .responseValidation(true)
+ .build();
+```
+
+## FAQ
+
+### Why don't you use plain `enum` classes?
+
+Java `enum` classes are not trivially [forwards compatible](https://www.stainless.com/blog/making-java-enums-forwards-compatible). Using them in the SDK could cause runtime exceptions if the API is updated to respond with a new enum value.
+
+### Why do you represent fields using `JsonField` instead of just plain `T`?
+
+Using `JsonField` enables a few features:
+
+- Allowing usage of [undocumented API functionality](#undocumented-api-functionality)
+- Lazily [validating the API response against the expected shape](#response-validation)
+- Representing absent vs explicitly null values
+
+### Why don't you use [`data` classes](https://kotlinlang.org/docs/data-classes.html)?
+
+It is not [backwards compatible to add new fields to a data class](https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api) and we don't want to introduce a breaking change every time we add a field to a class.
+
+### Why don't you use checked exceptions?
+
+Checked exceptions are widely considered a mistake in the Java programming language. In fact, they were omitted from Kotlin for this reason.
+
+Checked exceptions:
+
+- Are verbose to handle
+- Encourage error handling at the wrong level of abstraction, where nothing can be done about the error
+- Are tedious to propagate due to the [function coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function)
+- Don't play well with lambdas (also due to the function coloring problem)
+
## Semantic versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
-1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_.
+1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
2. Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
We are keen for your feedback; please open an [issue](https://www.github.com/openlayer-ai/openlayer-java/issues) with questions, bugs, or suggestions.
-
-## Requirements
-
-This library requires Java 8 or later.
diff --git a/SECURITY.md b/SECURITY.md
index 6dfa13e4..8614b059 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,9 +2,9 @@
## Reporting Security Issues
-This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
-To report a security issue, please contact the Stainless team at security@stainlessapi.com.
+To report a security issue, please contact the Stainless team at security@stainless.com.
## Responsible Disclosure
diff --git a/build.gradle.kts b/build.gradle.kts
index 6466dce7..e74d61c0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,10 +1,23 @@
plugins {
+ id("org.jetbrains.dokka") version "2.0.0"
+}
+repositories {
+ mavenCentral()
}
allprojects {
group = "com.openlayer.api"
- version = "0.1.0-alpha.11" // x-release-please-version
+ version = "0.1.0-alpha.12" // x-release-please-version
}
+subprojects {
+ apply(plugin = "org.jetbrains.dokka")
+}
+// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks
+tasks.named("dokkaJavadocCollector").configure {
+ subprojects.flatMap { it.tasks }
+ .filter { it.project.name != "openlayer-java" && it.name == "dokkaJavadocJar" }
+ .forEach { mustRunAfter(it) }
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 493cb327..778c89de 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
`kotlin-dsl`
- kotlin("jvm") version "1.9.22"
+ kotlin("jvm") version "1.9.20"
id("com.vanniktech.maven.publish") version "0.28.0"
}
@@ -10,7 +10,7 @@ repositories {
}
dependencies {
- implementation("com.diffplug.spotless:spotless-plugin-gradle:6.25.0")
- implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
+ implementation("com.diffplug.spotless:spotless-plugin-gradle:7.0.2")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0")
}
diff --git a/buildSrc/src/main/kotlin/openlayer.java.gradle.kts b/buildSrc/src/main/kotlin/openlayer.java.gradle.kts
index 32a150ed..e39d9ac6 100644
--- a/buildSrc/src/main/kotlin/openlayer.java.gradle.kts
+++ b/buildSrc/src/main/kotlin/openlayer.java.gradle.kts
@@ -1,9 +1,5 @@
import com.diffplug.gradle.spotless.SpotlessExtension
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
-import com.vanniktech.maven.publish.JavaLibrary
-import com.vanniktech.maven.publish.JavadocJar
-import com.vanniktech.maven.publish.MavenPublishBaseExtension
-import com.vanniktech.maven.publish.SonatypeHost
plugins {
`java-library`
@@ -27,6 +23,9 @@ java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
+
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType().configureEach {
@@ -43,9 +42,13 @@ tasks.named("jar") {
}
}
-tasks.named("test") {
+tasks.withType().configureEach {
useJUnitPlatform()
+ // Run tests in parallel to some degree.
+ maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
+ forkEvery = 100
+
testLogging {
exceptionFormat = TestExceptionFormat.FULL
}
diff --git a/buildSrc/src/main/kotlin/openlayer.kotlin.gradle.kts b/buildSrc/src/main/kotlin/openlayer.kotlin.gradle.kts
index b0bd08ab..f8d2c677 100644
--- a/buildSrc/src/main/kotlin/openlayer.kotlin.gradle.kts
+++ b/buildSrc/src/main/kotlin/openlayer.kotlin.gradle.kts
@@ -1,6 +1,6 @@
import com.diffplug.gradle.spotless.SpotlessExtension
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import com.vanniktech.maven.publish.*
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
plugins {
id("openlayer.java")
@@ -11,6 +11,20 @@ kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
+
+ compilerOptions {
+ freeCompilerArgs = listOf(
+ "-Xjvm-default=all",
+ "-Xjdk-release=1.8",
+ // Suppress deprecation warnings because we may still reference and test deprecated members.
+ // TODO: Replace with `-Xsuppress-warning=DEPRECATION` once we use Kotlin compiler 2.1.0+.
+ "-nowarn",
+ )
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ languageVersion.set(KotlinVersion.KOTLIN_1_8)
+ apiVersion.set(KotlinVersion.KOTLIN_1_8)
+ coreLibrariesVersion = "1.8.0"
+ }
}
configure {
@@ -20,10 +34,8 @@ configure {
}
}
-tasks.withType().configureEach {
- kotlinOptions {
- allWarningsAsErrors = true
- freeCompilerArgs = listOf("-Xjvm-default=all", "-Xjdk-release=1.8")
- jvmTarget = "1.8"
- }
+// Run tests in parallel to some degree.
+tasks.withType().configureEach {
+ maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
+ forkEvery = 100
}
diff --git a/buildSrc/src/main/kotlin/openlayer.publish.gradle.kts b/buildSrc/src/main/kotlin/openlayer.publish.gradle.kts
index d159a4fa..5f23d62a 100644
--- a/buildSrc/src/main/kotlin/openlayer.publish.gradle.kts
+++ b/buildSrc/src/main/kotlin/openlayer.publish.gradle.kts
@@ -1,10 +1,5 @@
-import org.gradle.api.publish.PublishingExtension
-import org.gradle.api.publish.maven.MavenPublication
-import org.gradle.kotlin.dsl.configure
-import org.gradle.kotlin.dsl.register
-import org.gradle.kotlin.dsl.get
-import com.vanniktech.maven.publish.JavaLibrary
import com.vanniktech.maven.publish.JavadocJar
+import com.vanniktech.maven.publish.KotlinJvm
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import com.vanniktech.maven.publish.SonatypeHost
@@ -25,7 +20,13 @@ configure {
signAllPublications()
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
- this.coordinates(project.group.toString(), project.name, project.version.toString())
+ coordinates(project.group.toString(), project.name, project.version.toString())
+ configure(
+ KotlinJvm(
+ javadocJar = JavadocJar.Dokka("dokkaJavadoc"),
+ sourcesJar = true,
+ )
+ )
pom {
name.set("Openlayer API")
diff --git a/gradle.properties b/gradle.properties
index a3bc58f2..0c8d4ded 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,5 @@
org.gradle.caching=true
-org.gradle.jvmargs=-Xmx4g
org.gradle.parallel=true
+org.gradle.daemon=false
+org.gradle.jvmargs=-Xmx4g
kotlin.daemon.jvmargs=-Xmx4g
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e6441136..a4b76b95 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b82aa23a..cea7a793 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index 1aa94a42..f3b75f3b 100755
--- a/gradlew
+++ b/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
diff --git a/gradlew.bat b/gradlew.bat
index 25da30db..9d21a218 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
diff --git a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt
index cd4ff7dc..83f7a9cf 100644
--- a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt
+++ b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt
@@ -1,6 +1,8 @@
package com.openlayer.api.client.okhttp
import com.openlayer.api.core.RequestOptions
+import com.openlayer.api.core.Timeout
+import com.openlayer.api.core.checkRequired
import com.openlayer.api.core.http.Headers
import com.openlayer.api.core.http.HttpClient
import com.openlayer.api.core.http.HttpMethod
@@ -30,38 +32,8 @@ class OkHttpClient
private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val baseUrl: HttpUrl) :
HttpClient {
- private fun getClient(requestOptions: RequestOptions): okhttp3.OkHttpClient {
- val clientBuilder = okHttpClient.newBuilder()
-
- val logLevel =
- when (System.getenv("OPENLAYER_LOG")?.lowercase()) {
- "info" -> HttpLoggingInterceptor.Level.BASIC
- "debug" -> HttpLoggingInterceptor.Level.BODY
- else -> null
- }
- if (logLevel != null) {
- clientBuilder.addNetworkInterceptor(
- HttpLoggingInterceptor().setLevel(logLevel).apply { redactHeader("Authorization") }
- )
- }
-
- val timeout = requestOptions.timeout
- if (timeout != null) {
- clientBuilder
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
- }
-
- return clientBuilder.build()
- }
-
- override fun execute(
- request: HttpRequest,
- requestOptions: RequestOptions,
- ): HttpResponse {
- val call = getClient(requestOptions).newCall(request.toRequest())
+ override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
+ val call = newCall(request, requestOptions)
return try {
call.execute().toResponse()
@@ -80,18 +52,18 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
request.body?.run { future.whenComplete { _, _ -> close() } }
- val call = getClient(requestOptions).newCall(request.toRequest())
- call.enqueue(
- object : Callback {
- override fun onResponse(call: Call, response: Response) {
- future.complete(response.toResponse())
- }
+ newCall(request, requestOptions)
+ .enqueue(
+ object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ future.complete(response.toResponse())
+ }
- override fun onFailure(call: Call, e: IOException) {
- future.completeExceptionally(OpenlayerIoException("Request failed", e))
+ override fun onFailure(call: Call, e: IOException) {
+ future.completeExceptionally(OpenlayerIoException("Request failed", e))
+ }
}
- }
- )
+ )
return future
}
@@ -102,10 +74,36 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
okHttpClient.cache?.close()
}
- private fun HttpRequest.toRequest(): Request {
+ private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
+ val clientBuilder = okHttpClient.newBuilder()
+
+ val logLevel =
+ when (System.getenv("OPENLAYER_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())
+ .readTimeout(it.read())
+ .writeTimeout(it.write())
+ .callTimeout(it.request())
+ }
+
+ val client = clientBuilder.build()
+ return client.newCall(request.toRequest(client))
+ }
+
+ private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
var body: RequestBody? = body?.toRequestBody()
- // OkHttpClient always requires a request body for PUT and POST methods.
- if (body == null && (method == HttpMethod.PUT || method == HttpMethod.POST)) {
+ if (body == null && requiresBody(method)) {
body = "".toRequestBody()
}
@@ -114,9 +112,33 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
headers.values(name).forEach { builder.header(name, it) }
}
+ if (
+ !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
+ ) {
+ builder.header(
+ "X-Stainless-Read-Timeout",
+ Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+ if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
+ builder.header(
+ "X-Stainless-Timeout",
+ Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+
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 {
url?.let {
return it
@@ -170,29 +192,30 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ class Builder internal constructor() {
private var baseUrl: HttpUrl? = null
- // The default timeout is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl.toHttpUrl() }
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
fun build(): OkHttpClient =
OkHttpClient(
okhttp3.OkHttpClient.Builder()
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
+ .connectTimeout(timeout.connect())
+ .readTimeout(timeout.read())
+ .writeTimeout(timeout.write())
+ .callTimeout(timeout.request())
.proxy(proxy)
.build(),
- checkNotNull(baseUrl) { "`baseUrl` is required but was not set" },
+ checkRequired("baseUrl", baseUrl),
)
}
}
diff --git a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt
index 6369d219..84b59f0f 100644
--- a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt
+++ b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt
@@ -6,32 +6,43 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.openlayer.api.client.OpenlayerClient
import com.openlayer.api.client.OpenlayerClientImpl
import com.openlayer.api.core.ClientOptions
+import com.openlayer.api.core.Timeout
import com.openlayer.api.core.http.Headers
import com.openlayer.api.core.http.QueryParams
import java.net.Proxy
import java.time.Clock
import java.time.Duration
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
class OpenlayerOkHttpClient private constructor() {
companion object {
+ /** Returns a mutable builder for constructing an instance of [OpenlayerOkHttpClient]. */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): OpenlayerClient = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [OpenlayerOkHttpClient]. */
+ class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // The default timeout for the client is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
@@ -118,7 +129,19 @@ class OpenlayerOkHttpClient private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply {
+ clientOptions.timeout(timeout)
+ this.timeout = timeout
+ }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
@@ -130,14 +153,22 @@ class OpenlayerOkHttpClient private constructor() {
fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
+ /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
+ fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
+
fun fromEnv() = apply { clientOptions.fromEnv() }
+ /**
+ * Returns an immutable instance of [OpenlayerClient].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
fun build(): OpenlayerClient =
OpenlayerClientImpl(
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(baseUrl)
+ .baseUrl(clientOptions.baseUrl())
.timeout(timeout)
.proxy(proxy)
.build()
diff --git a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt
index 3bd8ace4..ed7c1612 100644
--- a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt
+++ b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt
@@ -6,32 +6,45 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.openlayer.api.client.OpenlayerClientAsync
import com.openlayer.api.client.OpenlayerClientAsyncImpl
import com.openlayer.api.core.ClientOptions
+import com.openlayer.api.core.Timeout
import com.openlayer.api.core.http.Headers
import com.openlayer.api.core.http.QueryParams
import java.net.Proxy
import java.time.Clock
import java.time.Duration
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
class OpenlayerOkHttpClientAsync private constructor() {
companion object {
+ /**
+ * Returns a mutable builder for constructing an instance of [OpenlayerOkHttpClientAsync].
+ */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): OpenlayerClientAsync = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [OpenlayerOkHttpClientAsync]. */
+ class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // The default timeout for the client is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
@@ -118,7 +131,19 @@ class OpenlayerOkHttpClientAsync private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply {
+ clientOptions.timeout(timeout)
+ this.timeout = timeout
+ }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
@@ -130,14 +155,22 @@ class OpenlayerOkHttpClientAsync private constructor() {
fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
+ /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
+ fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
+
fun fromEnv() = apply { clientOptions.fromEnv() }
+ /**
+ * Returns an immutable instance of [OpenlayerClientAsync].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
fun build(): OpenlayerClientAsync =
OpenlayerClientAsyncImpl(
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(baseUrl)
+ .baseUrl(clientOptions.baseUrl())
.timeout(timeout)
.proxy(proxy)
.build()
diff --git a/openlayer-java-core/build.gradle.kts b/openlayer-java-core/build.gradle.kts
index cbb56c6f..5e14f894 100644
--- a/openlayer-java-core/build.gradle.kts
+++ b/openlayer-java-core/build.gradle.kts
@@ -3,14 +3,28 @@ plugins {
id("openlayer.publish")
}
+configurations.all {
+ resolutionStrategy {
+ // Compile and test against a lower Jackson version to ensure we're compatible with it.
+ // We publish with a higher version (see below) to ensure users depend on a secure version by default.
+ force("com.fasterxml.jackson.core:jackson-core:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
+ force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
+ }
+}
+
dependencies {
- api("com.fasterxml.jackson.core:jackson-core:2.18.1")
- api("com.fasterxml.jackson.core:jackson-databind:2.18.1")
+ api("com.fasterxml.jackson.core:jackson-core:2.18.2")
+ api("com.fasterxml.jackson.core:jackson-databind:2.18.2")
+ api("com.google.errorprone:error_prone_annotations:2.33.0")
- implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.1")
- implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.1")
- implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.1")
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.1")
+ implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2")
+ 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")
@@ -20,6 +34,7 @@ dependencies {
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
+ testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
testImplementation("org.mockito:mockito-core:5.14.2")
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClient.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClient.kt
index b7d18d42..7697f0c8 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClient.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClient.kt
@@ -7,10 +7,35 @@ import com.openlayer.api.services.blocking.InferencePipelineService
import com.openlayer.api.services.blocking.ProjectService
import com.openlayer.api.services.blocking.StorageService
+/**
+ * A client for interacting with the Openlayer REST API synchronously. You can also switch to
+ * asynchronous execution via the [async] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
interface OpenlayerClient {
+ /**
+ * Returns a version of this client that uses asynchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
fun async(): OpenlayerClientAsync
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
fun projects(): ProjectService
fun commits(): CommitService
@@ -18,4 +43,29 @@ interface OpenlayerClient {
fun inferencePipelines(): InferencePipelineService
fun storage(): StorageService
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /** A view of [OpenlayerClient] that provides access to raw HTTP responses for each method. */
+ interface WithRawResponse {
+
+ fun projects(): ProjectService.WithRawResponse
+
+ fun commits(): CommitService.WithRawResponse
+
+ fun inferencePipelines(): InferencePipelineService.WithRawResponse
+
+ fun storage(): StorageService.WithRawResponse
+ }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsync.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsync.kt
index d9a6d7a1..cd90e546 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsync.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsync.kt
@@ -7,10 +7,35 @@ import com.openlayer.api.services.async.InferencePipelineServiceAsync
import com.openlayer.api.services.async.ProjectServiceAsync
import com.openlayer.api.services.async.StorageServiceAsync
+/**
+ * A client for interacting with the Openlayer REST API asynchronously. You can also switch to
+ * synchronous execution via the [sync] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
interface OpenlayerClientAsync {
+ /**
+ * Returns a version of this client that uses synchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
fun sync(): OpenlayerClient
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
fun projects(): ProjectServiceAsync
fun commits(): CommitServiceAsync
@@ -18,4 +43,31 @@ interface OpenlayerClientAsync {
fun inferencePipelines(): InferencePipelineServiceAsync
fun storage(): StorageServiceAsync
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /**
+ * A view of [OpenlayerClientAsync] that provides access to raw HTTP responses for each method.
+ */
+ interface WithRawResponse {
+
+ fun projects(): ProjectServiceAsync.WithRawResponse
+
+ fun commits(): CommitServiceAsync.WithRawResponse
+
+ fun inferencePipelines(): InferencePipelineServiceAsync.WithRawResponse
+
+ fun storage(): StorageServiceAsync.WithRawResponse
+ }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt
index 9335e18a..1a5e1785 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt
@@ -13,10 +13,7 @@ import com.openlayer.api.services.async.ProjectServiceAsyncImpl
import com.openlayer.api.services.async.StorageServiceAsync
import com.openlayer.api.services.async.StorageServiceAsyncImpl
-class OpenlayerClientAsyncImpl
-constructor(
- private val clientOptions: ClientOptions,
-) : OpenlayerClientAsync {
+class OpenlayerClientAsyncImpl(private val clientOptions: ClientOptions) : OpenlayerClientAsync {
private val clientOptionsWithUserAgent =
if (clientOptions.headers.names().contains("User-Agent")) clientOptions
@@ -29,6 +26,10 @@ constructor(
// Pass the original clientOptions so that this client sets its own User-Agent.
private val sync: OpenlayerClient by lazy { OpenlayerClientImpl(clientOptions) }
+ private val withRawResponse: OpenlayerClientAsync.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
private val projects: ProjectServiceAsync by lazy {
ProjectServiceAsyncImpl(clientOptionsWithUserAgent)
}
@@ -47,6 +48,8 @@ constructor(
override fun sync(): OpenlayerClient = sync
+ override fun withRawResponse(): OpenlayerClientAsync.WithRawResponse = withRawResponse
+
override fun projects(): ProjectServiceAsync = projects
override fun commits(): CommitServiceAsync = commits
@@ -54,4 +57,35 @@ constructor(
override fun inferencePipelines(): InferencePipelineServiceAsync = inferencePipelines
override fun storage(): StorageServiceAsync = storage
+
+ override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ OpenlayerClientAsync.WithRawResponse {
+
+ private val projects: ProjectServiceAsync.WithRawResponse by lazy {
+ ProjectServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val commits: CommitServiceAsync.WithRawResponse by lazy {
+ CommitServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val inferencePipelines: InferencePipelineServiceAsync.WithRawResponse by lazy {
+ InferencePipelineServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val storage: StorageServiceAsync.WithRawResponse by lazy {
+ StorageServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun projects(): ProjectServiceAsync.WithRawResponse = projects
+
+ override fun commits(): CommitServiceAsync.WithRawResponse = commits
+
+ override fun inferencePipelines(): InferencePipelineServiceAsync.WithRawResponse =
+ inferencePipelines
+
+ override fun storage(): StorageServiceAsync.WithRawResponse = storage
+ }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt
index 4577ea48..f11b7575 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt
@@ -13,10 +13,7 @@ import com.openlayer.api.services.blocking.ProjectServiceImpl
import com.openlayer.api.services.blocking.StorageService
import com.openlayer.api.services.blocking.StorageServiceImpl
-class OpenlayerClientImpl
-constructor(
- private val clientOptions: ClientOptions,
-) : OpenlayerClient {
+class OpenlayerClientImpl(private val clientOptions: ClientOptions) : OpenlayerClient {
private val clientOptionsWithUserAgent =
if (clientOptions.headers.names().contains("User-Agent")) clientOptions
@@ -29,6 +26,10 @@ constructor(
// Pass the original clientOptions so that this client sets its own User-Agent.
private val async: OpenlayerClientAsync by lazy { OpenlayerClientAsyncImpl(clientOptions) }
+ private val withRawResponse: OpenlayerClient.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
private val projects: ProjectService by lazy { ProjectServiceImpl(clientOptionsWithUserAgent) }
private val commits: CommitService by lazy { CommitServiceImpl(clientOptionsWithUserAgent) }
@@ -41,6 +42,8 @@ constructor(
override fun async(): OpenlayerClientAsync = async
+ override fun withRawResponse(): OpenlayerClient.WithRawResponse = withRawResponse
+
override fun projects(): ProjectService = projects
override fun commits(): CommitService = commits
@@ -48,4 +51,35 @@ constructor(
override fun inferencePipelines(): InferencePipelineService = inferencePipelines
override fun storage(): StorageService = storage
+
+ override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ OpenlayerClient.WithRawResponse {
+
+ private val projects: ProjectService.WithRawResponse by lazy {
+ ProjectServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val commits: CommitService.WithRawResponse by lazy {
+ CommitServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val inferencePipelines: InferencePipelineService.WithRawResponse by lazy {
+ InferencePipelineServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val storage: StorageService.WithRawResponse by lazy {
+ StorageServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun projects(): ProjectService.WithRawResponse = projects
+
+ override fun commits(): CommitService.WithRawResponse = commits
+
+ override fun inferencePipelines(): InferencePipelineService.WithRawResponse =
+ inferencePipelines
+
+ override fun storage(): StorageService.WithRawResponse = storage
+ }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/BaseDeserializer.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/BaseDeserializer.kt
index 0573e162..e624381e 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/BaseDeserializer.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/BaseDeserializer.kt
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
-import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
@@ -18,7 +17,7 @@ abstract class BaseDeserializer(type: KClass) :
override fun createContextual(
context: DeserializationContext,
- property: BeanProperty?
+ property: BeanProperty?,
): JsonDeserializer {
return this
}
@@ -29,31 +28,17 @@ abstract class BaseDeserializer(type: KClass) :
protected abstract fun ObjectCodec.deserialize(node: JsonNode): T
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: TypeReference,
- validate: (T) -> Unit = {}
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: TypeReference): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: JavaType,
- validate: (T) -> Unit = {}
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: JavaType): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Check.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Check.kt
new file mode 100644
index 00000000..efcc5521
--- /dev/null
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Check.kt
@@ -0,0 +1,91 @@
+@file:JvmName("Check")
+
+package com.openlayer.api.core
+
+import com.fasterxml.jackson.core.Version
+import com.fasterxml.jackson.core.util.VersionUtil
+
+fun checkRequired(name: String, value: T?): T =
+ checkNotNull(value) { "`$name` is required, but was not set" }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: JsonField): T =
+ value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: MultipartField): T =
+ value.value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkLength(name: String, value: String, length: Int): String =
+ value.also {
+ check(it.length == length) { "`$name` must have length $length, but was ${it.length}" }
+ }
+
+@JvmSynthetic
+internal fun checkMinLength(name: String, value: String, minLength: Int): String =
+ value.also {
+ check(it.length >= minLength) {
+ if (minLength == 1) "`$name` must be non-empty, but was empty"
+ else "`$name` must have at least length $minLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkMaxLength(name: String, value: String, maxLength: Int): String =
+ value.also {
+ check(it.length <= maxLength) {
+ "`$name` must have at most length $maxLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkJacksonVersionCompatibility() {
+ val incompatibleJacksonVersions =
+ RUNTIME_JACKSON_VERSIONS.mapNotNull {
+ val badVersionReason = BAD_JACKSON_VERSIONS[it.toString()]
+ when {
+ it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
+ it to "incompatible major version"
+ it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
+ it to "minor version too low"
+ it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
+ it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
+ it to "patch version too low"
+ badVersionReason != null -> it to badVersionReason
+ else -> null
+ }
+ }
+ check(incompatibleJacksonVersions.isEmpty()) {
+ """
+This SDK depends on Jackson version $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:
+
+${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
+ "- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
+}.joinToString("\n")}
+
+This can happen if you are either:
+1. Directly depending on different Jackson versions
+2. Depending on some library that depends on different Jackson versions, potentially transitively
+
+Double-check that you are depending on compatible Jackson versions.
+ """
+ .trimIndent()
+ }
+}
+
+private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
+private val BAD_JACKSON_VERSIONS: Map =
+ mapOf("2.18.1" to "due to https://github.com/FasterXML/jackson-databind/issues/4639")
+private val RUNTIME_JACKSON_VERSIONS: List =
+ listOf(
+ com.fasterxml.jackson.core.json.PackageVersion.VERSION,
+ com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
+ com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
+ )
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ClientOptions.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ClientOptions.kt
index 9dbd51c6..37bbbb54 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ClientOptions.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ClientOptions.kt
@@ -9,65 +9,107 @@ import com.openlayer.api.core.http.PhantomReachableClosingHttpClient
import com.openlayer.api.core.http.QueryParams
import com.openlayer.api.core.http.RetryingHttpClient
import java.time.Clock
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
class ClientOptions
private constructor(
private val originalHttpClient: HttpClient,
@get:JvmName("httpClient") val httpClient: HttpClient,
+ @get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
@get:JvmName("clock") val clock: Clock,
@get:JvmName("baseUrl") val baseUrl: String,
@get:JvmName("headers") val headers: Headers,
@get:JvmName("queryParams") val queryParams: QueryParams,
@get:JvmName("responseValidation") val responseValidation: Boolean,
+ @get:JvmName("timeout") val timeout: Timeout,
@get:JvmName("maxRetries") val maxRetries: Int,
- @get:JvmName("apiKey") val apiKey: String?,
+ private val apiKey: String?,
) {
+ init {
+ if (checkJacksonVersionCompatibility) {
+ checkJacksonVersionCompatibility()
+ }
+ }
+
+ fun apiKey(): Optional = Optional.ofNullable(apiKey)
+
fun toBuilder() = Builder().from(this)
companion object {
const val PRODUCTION_URL = "https://api.openlayer.com/v1"
+ /**
+ * Returns a mutable builder for constructing an instance of [ClientOptions].
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * ```
+ */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [ClientOptions]. */
+ class Builder internal constructor() {
private var httpClient: HttpClient? = null
+ private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
private var clock: Clock = Clock.systemUTC()
private var baseUrl: String = PRODUCTION_URL
private var headers: Headers.Builder = Headers.builder()
private var queryParams: QueryParams.Builder = QueryParams.builder()
private var responseValidation: Boolean = false
+ private var timeout: Timeout = Timeout.default()
private var maxRetries: Int = 2
private var apiKey: String? = null
@JvmSynthetic
internal fun from(clientOptions: ClientOptions) = apply {
httpClient = clientOptions.originalHttpClient
+ checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
clock = clientOptions.clock
baseUrl = clientOptions.baseUrl
headers = clientOptions.headers.toBuilder()
queryParams = clientOptions.queryParams.toBuilder()
responseValidation = clientOptions.responseValidation
+ timeout = clientOptions.timeout
maxRetries = clientOptions.maxRetries
apiKey = clientOptions.apiKey
}
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
+ }
+
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
fun clock(clock: Clock) = apply { this.clock = clock }
fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl }
+ fun responseValidation(responseValidation: Boolean) = apply {
+ this.responseValidation = responseValidation
+ }
+
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+
+ fun apiKey(apiKey: String?) = apply { this.apiKey = apiKey }
+
+ /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
+ fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
+
fun headers(headers: Headers) = apply {
this.headers.clear()
putAllHeaders(headers)
@@ -148,18 +190,27 @@ private constructor(
fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
- fun responseValidation(responseValidation: Boolean) = apply {
- this.responseValidation = responseValidation
- }
-
- fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
-
- fun apiKey(apiKey: String?) = apply { this.apiKey = apiKey }
-
- fun fromEnv() = apply { System.getenv("OPENLAYER_API_KEY")?.let { apiKey(it) } }
-
+ fun baseUrl(): String = baseUrl
+
+ fun fromEnv() = apply {
+ System.getenv("OPENLAYER_BASE_URL")?.let { baseUrl(it) }
+ System.getenv("OPENLAYER_API_KEY")?.let { apiKey(it) }
+ }
+
+ /**
+ * Returns an immutable instance of [ClientOptions].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
fun build(): ClientOptions {
- checkNotNull(httpClient) { "`httpClient` is required but was not set" }
+ val httpClient = checkRequired("httpClient", httpClient)
val headers = Headers.builder()
val queryParams = QueryParams.builder()
@@ -179,20 +230,22 @@ private constructor(
queryParams.replaceAll(this.queryParams.build())
return ClientOptions(
- httpClient!!,
+ httpClient,
PhantomReachableClosingHttpClient(
RetryingHttpClient.builder()
- .httpClient(httpClient!!)
+ .httpClient(httpClient)
.clock(clock)
.maxRetries(maxRetries)
.build()
),
+ checkJacksonVersionCompatibility,
jsonMapper,
clock,
baseUrl,
headers.build(),
queryParams.build(),
responseValidation,
+ timeout,
maxRetries,
apiKey,
)
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/HttpRequestBodies.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/HttpRequestBodies.kt
deleted file mode 100644
index 8838f20c..00000000
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/HttpRequestBodies.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-@file:JvmName("HttpRequestBodies")
-
-package com.openlayer.api.core
-
-import com.fasterxml.jackson.databind.json.JsonMapper
-import com.openlayer.api.core.http.HttpRequestBody
-import com.openlayer.api.errors.OpenlayerException
-import java.io.ByteArrayOutputStream
-import java.io.OutputStream
-import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
-
-@JvmSynthetic
-internal inline fun json(
- jsonMapper: JsonMapper,
- value: T,
-): HttpRequestBody {
- return object : HttpRequestBody {
- private var cachedBytes: ByteArray? = null
-
- private fun serialize(): ByteArray {
- if (cachedBytes != null) return cachedBytes!!
-
- val buffer = ByteArrayOutputStream()
- try {
- jsonMapper.writeValue(buffer, value)
- cachedBytes = buffer.toByteArray()
- return cachedBytes!!
- } catch (e: Exception) {
- throw OpenlayerException("Error writing request", e)
- }
- }
-
- override fun writeTo(outputStream: OutputStream) {
- outputStream.write(serialize())
- }
-
- override fun contentType(): String = "application/json"
-
- override fun contentLength(): Long {
- return serialize().size.toLong()
- }
-
- override fun repeatable(): Boolean = true
-
- override fun close() {}
- }
-}
-
-@JvmSynthetic
-internal fun multipartFormData(
- jsonMapper: JsonMapper,
- parts: Array?>
-): HttpRequestBody {
- val builder = MultipartEntityBuilder.create()
- parts.forEach { part ->
- if (part?.value != null) {
- when (part.value) {
- is JsonValue -> {
- val buffer = ByteArrayOutputStream()
- try {
- jsonMapper.writeValue(buffer, part.value)
- } catch (e: Exception) {
- throw OpenlayerException("Error serializing value to json", e)
- }
- builder.addBinaryBody(
- part.name,
- buffer.toByteArray(),
- part.contentType,
- part.filename
- )
- }
- is Boolean ->
- builder.addTextBody(
- part.name,
- if (part.value) "true" else "false",
- part.contentType
- )
- is Int -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is Long -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is Double -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is ByteArray ->
- builder.addBinaryBody(part.name, part.value, part.contentType, part.filename)
- is String -> builder.addTextBody(part.name, part.value, part.contentType)
- is Enum -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- else ->
- throw IllegalArgumentException(
- "Unsupported content type: ${part.value::class.java.simpleName}"
- )
- }
- }
- }
- val entity = builder.build()
-
- return object : HttpRequestBody {
- override fun writeTo(outputStream: OutputStream) {
- try {
- return entity.writeTo(outputStream)
- } catch (e: Exception) {
- throw OpenlayerException("Error writing request", e)
- }
- }
-
- override fun contentType(): String = entity.contentType
-
- override fun contentLength(): Long = -1
-
- override fun repeatable(): Boolean = entity.isRepeatable
-
- override fun close() = entity.close()
- }
-}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ObjectMappers.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ObjectMappers.kt
index 2a22f38a..1438d8ea 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ObjectMappers.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ObjectMappers.kt
@@ -3,23 +3,165 @@
package com.openlayer.api.core
import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.databind.cfg.CoercionAction.Fail
-import com.fasterxml.jackson.databind.cfg.CoercionInputShape.Integer
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.cfg.CoercionAction
+import com.fasterxml.jackson.databind.cfg.CoercionInputShape
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.databind.type.LogicalType
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
-import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
+import com.fasterxml.jackson.module.kotlin.kotlinModule
+import java.io.InputStream
+import java.time.DateTimeException
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
fun jsonMapper(): JsonMapper =
- jacksonMapperBuilder()
+ JsonMapper.builder()
+ .addModule(kotlinModule())
.addModule(Jdk8Module())
.addModule(JavaTimeModule())
+ .addModule(
+ SimpleModule()
+ .addSerializer(InputStreamSerializer)
+ .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
+ )
+ .withCoercionConfig(LogicalType.Boolean) {
+ it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Integer) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Float) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Textual) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Array) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Collection) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Map) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.POJO) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ }
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
- .withCoercionConfig(String::class.java) { it.setCoercion(Integer, Fail) }
+ .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
+ .disable(MapperFeature.AUTO_DETECT_CREATORS)
+ .disable(MapperFeature.AUTO_DETECT_FIELDS)
+ .disable(MapperFeature.AUTO_DETECT_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_SETTERS)
.build()
+
+/** A serializer that serializes [InputStream] to bytes. */
+private object InputStreamSerializer : BaseSerializer(InputStream::class) {
+
+ private fun readResolve(): Any = InputStreamSerializer
+
+ override fun serialize(
+ value: InputStream?,
+ gen: JsonGenerator?,
+ serializers: SerializerProvider?,
+ ) {
+ if (value == null) {
+ gen?.writeNull()
+ } else {
+ value.use { gen?.writeBinary(it.readBytes()) }
+ }
+ }
+}
+
+/**
+ * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
+ */
+private class LenientLocalDateTimeDeserializer :
+ StdDeserializer(LocalDateTime::class.java) {
+
+ companion object {
+
+ private val DATE_TIME_FORMATTERS =
+ listOf(
+ DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+ DateTimeFormatter.ISO_LOCAL_DATE,
+ DateTimeFormatter.ISO_ZONED_DATE_TIME,
+ )
+ }
+
+ override fun logicalType(): LogicalType = LogicalType.DateTime
+
+ override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
+ val exceptions = mutableListOf()
+
+ for (formatter in DATE_TIME_FORMATTERS) {
+ try {
+ val temporal = formatter.parse(p.text)
+
+ return when {
+ !temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
+ LocalDate.from(temporal).atStartOfDay()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal)
+ else -> ZonedDateTime.from(temporal).toLocalDateTime()
+ }
+ } catch (e: DateTimeException) {
+ exceptions.add(e)
+ }
+ }
+
+ throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
+ exceptions.forEach { addSuppressed(it) }
+ }
+ }
+}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Params.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Params.kt
new file mode 100644
index 00000000..62cbb730
--- /dev/null
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Params.kt
@@ -0,0 +1,16 @@
+package com.openlayer.api.core
+
+import com.openlayer.api.core.http.Headers
+import com.openlayer.api.core.http.QueryParams
+
+/** An interface representing parameters passed to a service method. */
+interface Params {
+ /** The full set of headers in the parameters, including both fixed and additional headers. */
+ fun _headers(): Headers
+
+ /**
+ * The full set of query params in the parameters, including both fixed and additional query
+ * params.
+ */
+ fun _queryParams(): QueryParams
+}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/PrepareRequest.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/PrepareRequest.kt
new file mode 100644
index 00000000..adf85bd8
--- /dev/null
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/PrepareRequest.kt
@@ -0,0 +1,24 @@
+@file:JvmName("PrepareRequest")
+
+package com.openlayer.api.core
+
+import com.openlayer.api.core.http.HttpRequest
+import java.util.concurrent.CompletableFuture
+
+@JvmSynthetic
+internal fun HttpRequest.prepare(clientOptions: ClientOptions, params: Params): HttpRequest =
+ toBuilder()
+ .putAllQueryParams(clientOptions.queryParams)
+ .replaceAllQueryParams(params._queryParams())
+ .putAllHeaders(clientOptions.headers)
+ .replaceAllHeaders(params._headers())
+ .build()
+
+@JvmSynthetic
+internal fun HttpRequest.prepareAsync(
+ clientOptions: ClientOptions,
+ params: Params,
+): CompletableFuture =
+ // This async version exists to make it easier to add async specific preparation logic in the
+ // future.
+ CompletableFuture.completedFuture(prepare(clientOptions, params))
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/RequestOptions.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/RequestOptions.kt
index 2b862a37..5ca3fddb 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/RequestOptions.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/RequestOptions.kt
@@ -2,17 +2,7 @@ package com.openlayer.api.core
import java.time.Duration
-class RequestOptions
-private constructor(
- val responseValidation: Boolean?,
- val timeout: Duration?,
-) {
- fun applyDefaults(options: RequestOptions): RequestOptions {
- return RequestOptions(
- responseValidation = this.responseValidation ?: options.responseValidation,
- timeout = this.timeout ?: options.timeout,
- )
- }
+class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Timeout?) {
companion object {
@@ -20,21 +10,37 @@ private constructor(
@JvmStatic fun none() = NONE
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions): RequestOptions =
+ builder()
+ .responseValidation(clientOptions.responseValidation)
+ .timeout(clientOptions.timeout)
+ .build()
+
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ fun applyDefaults(options: RequestOptions): RequestOptions =
+ RequestOptions(
+ responseValidation = responseValidation ?: options.responseValidation,
+ timeout =
+ if (options.timeout != null && timeout != null) timeout.assign(options.timeout)
+ else timeout ?: options.timeout,
+ )
+
+ class Builder internal constructor() {
+
private var responseValidation: Boolean? = null
- private var timeout: Duration? = null
+ private var timeout: Timeout? = null
fun responseValidation(responseValidation: Boolean) = apply {
this.responseValidation = responseValidation
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
- fun build(): RequestOptions {
- return RequestOptions(responseValidation, timeout)
- }
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ fun build(): RequestOptions = RequestOptions(responseValidation, timeout)
}
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Timeout.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Timeout.kt
new file mode 100644
index 00000000..e07cad54
--- /dev/null
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Timeout.kt
@@ -0,0 +1,167 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.openlayer.api.core
+
+import java.time.Duration
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** A class containing timeouts for various processing phases of a request. */
+class Timeout
+private constructor(
+ private val connect: Duration?,
+ private val read: Duration?,
+ private val write: Duration?,
+ private val request: Duration?,
+) {
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(): Duration = connect ?: Duration.ofMinutes(1)
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(): Duration = read ?: request()
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(): Duration = write ?: request()
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as well
+ * as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(): Duration = request ?: Duration.ofMinutes(1)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ @JvmStatic fun default() = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [Timeout]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Timeout]. */
+ class Builder internal constructor() {
+
+ private var connect: Duration? = null
+ private var read: Duration? = null
+ private var write: Duration? = null
+ private var request: Duration? = null
+
+ @JvmSynthetic
+ internal fun from(timeout: Timeout) = apply {
+ connect = timeout.connect
+ read = timeout.read
+ write = timeout.write
+ request = timeout.request
+ }
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(connect: Duration?) = apply { this.connect = connect }
+
+ /** Alias for calling [Builder.connect] with `connect.orElse(null)`. */
+ fun connect(connect: Optional) = connect(connect.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(read: Duration?) = apply { this.read = read }
+
+ /** Alias for calling [Builder.read] with `read.orElse(null)`. */
+ fun read(read: Optional) = read(read.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(write: Duration?) = apply { this.write = write }
+
+ /** Alias for calling [Builder.write] with `write.orElse(null)`. */
+ fun write(write: Optional) = write(write.getOrNull())
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as
+ * well as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(request: Duration?) = apply { this.request = request }
+
+ /** Alias for calling [Builder.request] with `request.orElse(null)`. */
+ fun request(request: Optional) = request(request.getOrNull())
+
+ /**
+ * Returns an immutable instance of [Timeout].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Timeout = Timeout(connect, read, write, request)
+ }
+
+ @JvmSynthetic
+ internal fun assign(target: Timeout): Timeout =
+ target
+ .toBuilder()
+ .apply {
+ connect?.let(this::connect)
+ read?.let(this::read)
+ write?.let(this::write)
+ request?.let(this::request)
+ }
+ .build()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return /* spotless:off */ other is Timeout && connect == other.connect && read == other.read && write == other.write && request == other.request /* spotless:on */
+ }
+
+ override fun hashCode(): Int = /* spotless:off */ Objects.hash(connect, read, write, request) /* spotless:on */
+
+ override fun toString() =
+ "Timeout{connect=$connect, read=$read, write=$write, request=$request}"
+}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Utils.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Utils.kt
index 7c5ab9d7..4482ae3d 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Utils.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Utils.kt
@@ -16,11 +16,77 @@ internal fun List.toImmutable(): List =
@JvmSynthetic
internal fun Map.toImmutable(): Map =
- if (isEmpty()) Collections.emptyMap() else Collections.unmodifiableMap(toMap())
+ if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())
+
+@JvmSynthetic internal fun immutableEmptyMap(): Map = Collections.emptyMap()
@JvmSynthetic
internal fun , V> SortedMap.toImmutable(): SortedMap =
if (isEmpty()) Collections.emptySortedMap()
else Collections.unmodifiableSortedMap(toSortedMap(comparator()))
+/**
+ * Returns all elements that yield the largest value for the given function, or an empty list if
+ * there are zero elements.
+ *
+ * This is similar to [Sequence.maxByOrNull] except it returns _all_ elements that yield the largest
+ * value; not just the first one.
+ */
+@JvmSynthetic
+internal fun > Sequence.allMaxBy(selector: (T) -> R): List {
+ var maxValue: R? = null
+ val maxElements = mutableListOf()
+
+ val iterator = iterator()
+ while (iterator.hasNext()) {
+ val element = iterator.next()
+ val value = selector(element)
+ if (maxValue == null || value > maxValue) {
+ maxValue = value
+ maxElements.clear()
+ maxElements.add(element)
+ } else if (value == maxValue) {
+ maxElements.add(element)
+ }
+ }
+
+ return maxElements
+}
+
+/**
+ * Returns whether [this] is equal to [other].
+ *
+ * This differs from [Object.equals] because it also deeply equates arrays based on their contents,
+ * even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic
+internal infix fun Any?.contentEquals(other: Any?): Boolean =
+ arrayOf(this).contentDeepEquals(arrayOf(other))
+
+/**
+ * Returns a hash of the given sequence of [values].
+ *
+ * This differs from [java.util.Objects.hash] because it also deeply hashes arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic internal fun contentHash(vararg values: Any?): Int = values.contentDeepHashCode()
+
+/**
+ * Returns a [String] representation of [this].
+ *
+ * This differs from [Object.toString] because it also deeply stringifies arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic
+internal fun Any?.contentToString(): String {
+ var string = arrayOf(this).contentDeepToString()
+ if (string.startsWith('[')) {
+ string = string.substring(1)
+ }
+ if (string.endsWith(']')) {
+ string = string.substring(0, string.length - 1)
+ }
+ return string
+}
+
internal interface Enum
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt
index fc2653eb..88f25c14 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt
@@ -1,8 +1,6 @@
package com.openlayer.api.core
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
-import com.fasterxml.jackson.annotation.JsonAutoDetect
-import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.core.JsonGenerator
@@ -27,68 +25,145 @@ import com.fasterxml.jackson.databind.node.JsonNodeType.POJO
import com.fasterxml.jackson.databind.node.JsonNodeType.STRING
import com.fasterxml.jackson.databind.ser.std.NullSerializer
import com.openlayer.api.errors.OpenlayerInvalidDataException
-import java.nio.charset.Charset
+import java.io.InputStream
import java.util.Objects
import java.util.Optional
-import org.apache.hc.core5.http.ContentType
+/**
+ * A class representing a serializable JSON field.
+ *
+ * It can either be a [KnownValue] value of type [T], matching the type the SDK expects, or an
+ * arbitrary JSON value that bypasses the type system (via [JsonValue]).
+ */
@JsonDeserialize(using = JsonField.Deserializer::class)
sealed class JsonField {
+ /**
+ * Returns whether this field is missing, which means it will be omitted from the serialized
+ * JSON entirely.
+ */
fun isMissing(): Boolean = this is JsonMissing
+ /** Whether this field is explicitly set to `null`. */
fun isNull(): Boolean = this is JsonNull
- fun asKnown(): Optional =
- when (this) {
- is KnownValue -> Optional.of(value)
- else -> Optional.empty()
- }
+ /**
+ * Returns an [Optional] containing this field's "known" value, meaning it matches the type the
+ * SDK expects, or an empty [Optional] if this field contains an arbitrary [JsonValue].
+ *
+ * This is the opposite of [asUnknown].
+ */
+ fun asKnown():
+ Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > = Optional.ofNullable((this as? KnownValue)?.value)
/**
- * If the "known" value (i.e. matching the type that the SDK expects) is returned by the API
- * then this method will return an empty `Optional`, otherwise the returned `Optional` is given
- * a `JsonValue`.
+ * Returns an [Optional] containing this field's arbitrary [JsonValue], meaning it mismatches
+ * the type the SDK expects, or an empty [Optional] if this field contains a "known" value.
+ *
+ * This is the opposite of [asKnown].
*/
- fun asUnknown(): Optional =
- when (this) {
- is JsonValue -> Optional.of(this)
- else -> Optional.empty()
- }
+ fun asUnknown(): Optional = Optional.ofNullable(this as? JsonValue)
+ /**
+ * Returns an [Optional] containing this field's boolean value, or an empty [Optional] if it
+ * doesn't contain a boolean.
+ *
+ * This method checks for both a [KnownValue] containing a boolean and for [JsonBoolean].
+ */
fun asBoolean(): Optional =
when (this) {
is JsonBoolean -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Boolean)
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's numerical value, or an empty [Optional] if it
+ * doesn't contain a number.
+ *
+ * This method checks for both a [KnownValue] containing a number and for [JsonNumber].
+ */
fun asNumber(): Optional =
when (this) {
is JsonNumber -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Number)
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's string value, or an empty [Optional] if it
+ * doesn't contain a string.
+ *
+ * This method checks for both a [KnownValue] containing a string and for [JsonString].
+ */
fun asString(): Optional =
when (this) {
is JsonString -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? String)
else -> Optional.empty()
}
fun asStringOrThrow(): String =
- when (this) {
- is JsonString -> value
- else -> throw OpenlayerInvalidDataException("Value is not a string")
- }
+ asString().orElseThrow { OpenlayerInvalidDataException("Value is not a string") }
+ /**
+ * Returns an [Optional] containing this field's list value, or an empty [Optional] if it
+ * doesn't contain a list.
+ *
+ * This method checks for both a [KnownValue] containing a list and for [JsonArray].
+ */
fun asArray(): Optional> =
when (this) {
is JsonArray -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? List<*>)?.map {
+ try {
+ JsonValue.from(it)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a list, but not all values are convertible to
+ // `JsonValue`.
+ return Optional.empty()
+ }
+ }
+ )
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's map value, or an empty [Optional] if it doesn't
+ * contain a map.
+ *
+ * This method checks for both a [KnownValue] containing a map and for [JsonObject].
+ */
fun asObject(): Optional