diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d4e261..2208be4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,10 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | @@ -40,14 +40,17 @@ jobs: build: timeout-minutes: 15 name: build + permissions: + contents: read + id-token: write runs-on: ${{ github.repository == 'stainless-sdks/phoebe-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | @@ -61,16 +64,31 @@ jobs: - name: Build SDK run: ./scripts/build + - name: Get GitHub OIDC Token + if: github.repository == 'stainless-sdks/phoebe-java' + id: github-oidc + uses: actions/github-script@v8 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Build and upload Maven artifacts + if: github.repository == 'stainless-sdks/phoebe-java' + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + PROJECT: phoebe-java + run: ./scripts/upload-artifacts test: timeout-minutes: 15 name: test runs-on: ${{ github.repository == 'stainless-sdks/phoebe-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml new file mode 100644 index 0000000..3a4ee30 --- /dev/null +++ b/.github/workflows/publish-sonatype.yml @@ -0,0 +1,41 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to Sonatype in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/phoebe-bird/phoebe-java/actions/workflows/publish-sonatype.yml +name: Publish Sonatype +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: | + 8 + 21 + cache: gradle + + - name: Set up Gradle + uses: gradle/gradle-build-action@v2 + + - name: Publish to Sonatype + run: |- + export -- GPG_SIGNING_KEY_ID + printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD" + GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')" + ./gradlew publish --no-configuration-cache + env: + SONATYPE_USERNAME: ${{ secrets.PHOEBE_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.PHOEBE_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }} + GPG_SIGNING_KEY: ${{ secrets.PHOEBE_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }} + GPG_SIGNING_PASSWORD: ${{ secrets.PHOEBE_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..3791b5c --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,24 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'phoebe-bird/phoebe-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v6 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + SONATYPE_USERNAME: ${{ secrets.PHOEBE_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.PHOEBE_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }} + GPG_SIGNING_KEY: ${{ secrets.PHOEBE_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }} + GPG_SIGNING_PASSWORD: ${{ secrets.PHOEBE_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..ba6c348 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0-alpha.1" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 87b8e22..8c14db5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 25 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/personal-ijyk2f%2Fphoebe-3974e6147f98a3ce3fc4684fda2671e7a7dc7476ecdf42af7a5dc88725cc8f04.yml openapi_spec_hash: 9808c815a0b204fdee17a0abca108ab5 -config_hash: 90be8240e7a9b625b33329bb412c0bea +config_hash: 17a571ff53308f9c391515734db807d2 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c8b184d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,83 @@ +# Changelog + +## 0.1.0-alpha.1 (2026-02-07) + +Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/phoebe-bird/phoebe-java/compare/v0.0.1-alpha.0...v0.1.0-alpha.1) + +### ⚠ BREAKING CHANGES + +* **client:** change precision of some numeric types +* **client:** extract auto pagination to shared classes +* **client:** **Migration:** - If you were referencing the `AutoPager` class on a specific `*Page` or `*PageAsync` type, then you should instead reference the shared `AutoPager` and `AutoPagerAsync` types, under the `core` package + - `AutoPagerAsync` now has different usage. You can call `.subscribe(...)` on the returned object instead to get called back each page item. You can also call `onCompleteFuture()` to get a future that completes when all items have been processed. Finally, you can call `.close()` on the returned object to stop auto-paginating early + - If you were referencing `getNextPage` or `getNextPageParams`: + - Swap to `nextPage()` and `nextPageParams()` + - Note that these both now return non-optional types (use `hasNextPage()` before calling these, since they will throw if it's impossible to get another page) + +### Features + +* **api:** manual updates ([d712abb](https://github.com/phoebe-bird/phoebe-java/commit/d712abbfc3f4e8e54e8391ae34616bae45510b27)) +* **client:** add `{QueryParams,Headers}#put(String, JsonValue)` methods ([31f7acd](https://github.com/phoebe-bird/phoebe-java/commit/31f7acd90ff3c222f4db984e29ef8a090ce557d6)) +* **client:** add `HttpRequest#url()` method ([a145b94](https://github.com/phoebe-bird/phoebe-java/commit/a145b94467e13633c59889cccd0c26b0b3789a44)) +* **client:** add a `withOptions` method ([008f2ee](https://github.com/phoebe-bird/phoebe-java/commit/008f2ee04e6216121f6bfdf31616a69f0143d55e)) +* **client:** add https config options ([74d088e](https://github.com/phoebe-bird/phoebe-java/commit/74d088e2486ecc1d43474dc0e928905c7eecdfe8)) +* **client:** allow configuring dispatcher executor service ([ef7c266](https://github.com/phoebe-bird/phoebe-java/commit/ef7c2660841ba59c269caa78b58f9a9f59fa162c)) +* **client:** allow configuring env via system properties ([f1de279](https://github.com/phoebe-bird/phoebe-java/commit/f1de279091ccdbf3ace91ed860f1d00345fb6c71)) +* **client:** allow providing some params positionally ([b24d6c0](https://github.com/phoebe-bird/phoebe-java/commit/b24d6c0cd3ecc5a55a382454f96bbf89a84fd3d7)) +* **client:** extract auto pagination to shared classes ([13f571f](https://github.com/phoebe-bird/phoebe-java/commit/13f571f7baf0caef24ce9c554b244476ed3e812b)) +* **client:** implement per-endpoint base URL support ([f493836](https://github.com/phoebe-bird/phoebe-java/commit/f4938363ced1c863b72b10c787c73c56ab6aac51)) +* **client:** send `X-Stainless-Kotlin-Version` header ([d9f18bd](https://github.com/phoebe-bird/phoebe-java/commit/d9f18bd66b86423c4ae710ea0987fe35f53ed457)) + + +### Bug Fixes + +* **client:** bump max requests per host to max requests (5 -> 64) ([73b4311](https://github.com/phoebe-bird/phoebe-java/commit/73b4311a77e11406a4c591ef9bf111ef84970605)) +* **client:** cancel okhttp call when future cancelled ([5823aba](https://github.com/phoebe-bird/phoebe-java/commit/5823abafce2a92fbb69a0f6c23db79a78b78f2a8)) +* **client:** disallow coercion from float to int ([d00944f](https://github.com/phoebe-bird/phoebe-java/commit/d00944fd323009658808104c8a4b3954211b3559)) +* **client:** don't close client on `withOptions` usage when original is gc'd ([3d3f673](https://github.com/phoebe-bird/phoebe-java/commit/3d3f673ecda4ce417d1ec954ea670cf629ae373a)) +* **client:** ensure error handling always occurs ([c4327a3](https://github.com/phoebe-bird/phoebe-java/commit/c4327a3c088df1b9398337114f3316f5e355a7f3)) +* **client:** fully respect max retries ([7c71f9d](https://github.com/phoebe-bird/phoebe-java/commit/7c71f9dc52f85d1a055e25f5d940ac1fc6d807b0)) +* **client:** preserve time zone in lenient date-time parsing ([50bb540](https://github.com/phoebe-bird/phoebe-java/commit/50bb540673b947b6f5b2da24bb3cac46e3ca498a)) +* **client:** remove `@MustBeClosed` for future returning methods ([a6e5c78](https://github.com/phoebe-bird/phoebe-java/commit/a6e5c78fcb9b3107791323d3c3da12791634939a)) +* **client:** send retry count header for max retries 0 ([7c71f9d](https://github.com/phoebe-bird/phoebe-java/commit/7c71f9dc52f85d1a055e25f5d940ac1fc6d807b0)) +* date time deserialization leniency ([9d5f042](https://github.com/phoebe-bird/phoebe-java/commit/9d5f042b85f9bdaec3a56ad6ea180a32f6a5f454)) + + +### Chores + +* **ci:** enable for pull requests ([30e9e4a](https://github.com/phoebe-bird/phoebe-java/commit/30e9e4af5c35b09de9c58934e263d3a6b667d3fa)) +* **ci:** only run for pushes and fork pull requests ([a3e1190](https://github.com/phoebe-bird/phoebe-java/commit/a3e119093c0f9c11ee28bef632aca42deb6635b2)) +* **ci:** upgrade `actions/github-script` ([43107c0](https://github.com/phoebe-bird/phoebe-java/commit/43107c0efbf857c052a1ce799f14a45add565375)) +* **ci:** upgrade `actions/setup-java` ([75d7ae3](https://github.com/phoebe-bird/phoebe-java/commit/75d7ae3dc40b246a7fe1e4a0316122c579779edd)) +* configure new SDK language ([30ab2b2](https://github.com/phoebe-bird/phoebe-java/commit/30ab2b26361936249dbea4db64787cfc7c6b7e2d)) +* **docs:** grammar improvements ([33fa7c5](https://github.com/phoebe-bird/phoebe-java/commit/33fa7c53610ea31ad671e5c5437fa6d4d02290a9)) +* **internal:** allow running specific example from cli ([56695c9](https://github.com/phoebe-bird/phoebe-java/commit/56695c9e55d80e78608e41f4f90297f13d100c22)) +* **internal:** clean up maven repo artifact script and add html documentation to repo root ([6a0f2ca](https://github.com/phoebe-bird/phoebe-java/commit/6a0f2cab131804296e2b29cc2e29c58de3603a2b)) +* **internal:** codegen related update ([fa74474](https://github.com/phoebe-bird/phoebe-java/commit/fa744740ab0421e1a3ed33aad5952fde2e80c4df)) +* **internal:** codegen related update ([78f6424](https://github.com/phoebe-bird/phoebe-java/commit/78f64241e6e1a7f11b3004ea3b40e282688e473d)) +* **internal:** codegen related update ([704a4e1](https://github.com/phoebe-bird/phoebe-java/commit/704a4e190b022602ad545cf926ef21ce4bac5d96)) +* **internal:** codegen related update ([e57fa55](https://github.com/phoebe-bird/phoebe-java/commit/e57fa556588b355f889962a6e57cf041a2ab7b5c)) +* **internal:** codegen related update ([44125db](https://github.com/phoebe-bird/phoebe-java/commit/44125db5522d48fb720c0753aeada71cab6e3105)) +* **internal:** correct cache invalidation for `SKIP_MOCK_TESTS` ([adfd7da](https://github.com/phoebe-bird/phoebe-java/commit/adfd7da05a09ef5622316700c031b6ac6ef4d05f)) +* **internal:** depend on packages directly in example ([7c71f9d](https://github.com/phoebe-bird/phoebe-java/commit/7c71f9dc52f85d1a055e25f5d940ac1fc6d807b0)) +* **internal:** improve maven repo docs ([da49cb7](https://github.com/phoebe-bird/phoebe-java/commit/da49cb77739c9becf5d45b12095298950f1e32f9)) +* **internal:** refactor delegating from client to options ([4ea1ff5](https://github.com/phoebe-bird/phoebe-java/commit/4ea1ff5ee41e8e81d62000b0bac199c47d645b8e)) +* **internal:** remove unnecessary `[...]` in `[@see](https://github.com/see)` ([bed463e](https://github.com/phoebe-bird/phoebe-java/commit/bed463e241505d386c28f49a7c0edbfaa9c4e3be)) +* **internal:** support uploading Maven repo artifacts to stainless package server ([37c89f8](https://github.com/phoebe-bird/phoebe-java/commit/37c89f8c816525461bce82b5550d796ff1ff3023)) +* **internal:** update `actions/checkout` version ([3a146bb](https://github.com/phoebe-bird/phoebe-java/commit/3a146bbdb3ab84f558391f3b8e6bc7e2c8fd77f6)) +* **internal:** update maven repo doc to include authentication ([cf7eb23](https://github.com/phoebe-bird/phoebe-java/commit/cf7eb239f388de322581a476a6e6845cebd5e019)) +* test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind[#3240](https://github.com/phoebe-bird/phoebe-java/issues/3240) in tests ([9d5f042](https://github.com/phoebe-bird/phoebe-java/commit/9d5f042b85f9bdaec3a56ad6ea180a32f6a5f454)) + + +### Documentation + +* fix missing readme comment ([4fd91a7](https://github.com/phoebe-bird/phoebe-java/commit/4fd91a77d9e1291a5d81ee0cc950d4a83e2ab753)) +* more code comments ([f8420f4](https://github.com/phoebe-bird/phoebe-java/commit/f8420f4dfd2e03fae3e61c5b7884f425c92b69d8)) +* prominently feature MCP server setup in root SDK readmes ([7754ce1](https://github.com/phoebe-bird/phoebe-java/commit/7754ce1495022f309ff26d5c2f06f3c4c8e913b7)) +* remove `$` for better copy-pasteabality ([fe8a020](https://github.com/phoebe-bird/phoebe-java/commit/fe8a02007be585c2c162442e968a4fe8909eb39e)) + + +### Refactors + +* **client:** change precision of some numeric types ([c593d52](https://github.com/phoebe-bird/phoebe-java/commit/c593d52bf58fc1324aef2f61d1411a0923b15a2e)) +* **internal:** minor `ClientOptionsTest` change ([b9a8ca8](https://github.com/phoebe-bird/phoebe-java/commit/b9a8ca82aa21e634734848084170f7930b906684)) diff --git a/LICENSE b/LICENSE index aa84a46..a69fada 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Phoebe + Copyright 2026 Phoebe 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 9a45101..13b5e6c 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,39 @@ # Phoebe Java API Library -[![Maven Central](https://img.shields.io/maven-central/v/com.phoebe.api/phoebe-java)](https://central.sonatype.com/artifact/com.phoebe.api/phoebe-java/0.0.1-alpha.0) -[![javadoc](https://javadoc.io/badge2/com.phoebe.api/phoebe-java/0.0.1-alpha.0/javadoc.svg)](https://javadoc.io/doc/com.phoebe.api/phoebe-java/0.0.1-alpha.0) + + +[![Maven Central](https://img.shields.io/maven-central/v/com.phoebe.api/phoebe-java)](https://central.sonatype.com/artifact/com.phoebe.api/phoebe-java/0.1.0-alpha.1) +[![javadoc](https://javadoc.io/badge2/com.phoebe.api/phoebe-java/0.1.0-alpha.1/javadoc.svg)](https://javadoc.io/doc/com.phoebe.api/phoebe-java/0.1.0-alpha.1) + + The Phoebe Java SDK provides convenient access to the [Phoebe REST API](https://science.ebird.org/en/use-ebird-data/download-ebird-data-products) from applications written in Java. It is generated with [Stainless](https://www.stainless.com/). -The REST API documentation can be found on [science.ebird.org](https://science.ebird.org/en/use-ebird-data/download-ebird-data-products). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.phoebe.api/phoebe-java/0.0.1-alpha.0). +## MCP Server + +Use the Phoebe MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. + +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=phoebe-ebird-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInBob2ViZS1lYmlyZC1tY3AiXSwiZW52Ijp7IkVCSVJEX0FQSV9LRVkiOiJNeSBBUEkgS2V5In19) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22phoebe-ebird-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22phoebe-ebird-mcp%22%5D%2C%22env%22%3A%7B%22EBIRD_API_KEY%22%3A%22My%20API%20Key%22%7D%7D) + +> Note: You may need to set environment variables in your MCP client. + + + +The REST API documentation can be found on [science.ebird.org](https://science.ebird.org/en/use-ebird-data/download-ebird-data-products). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.phoebe.api/phoebe-java/0.1.0-alpha.1). + + ## Installation + + ### Gradle ```kotlin -implementation("com.phoebe.api:phoebe-java:0.0.1-alpha.0") +implementation("com.phoebe.api:phoebe-java:0.1.0-alpha.1") ``` ### Maven @@ -23,10 +42,12 @@ implementation("com.phoebe.api:phoebe-java:0.0.1-alpha.0") com.phoebe.api phoebe-java - 0.0.1-alpha.0 + 0.1.0-alpha.1 ``` + + ## Requirements This library requires Java 8 or later. @@ -220,13 +241,13 @@ The SDK uses the standard [OkHttp logging interceptor](https://github.com/square Enable logging by setting the `PHOEBE_LOG` environment variable to `info`: ```sh -$ export PHOEBE_LOG=info +export PHOEBE_LOG=info ``` Or to `debug` for more verbose logging: ```sh -$ export PHOEBE_LOG=debug +export PHOEBE_LOG=debug ``` ## ProGuard and R8 @@ -246,6 +267,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t > [!CAUTION] > We make no guarantee that the SDK works correctly when the Jackson version check is disabled. +Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead. + ## Network options ### Retries @@ -583,4 +606,4 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con 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/stainless-sdks/phoebe-java/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/phoebe-bird/phoebe-java/issues) with questions, bugs, or suggestions. diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..3a6a7b4 --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${SONATYPE_USERNAME}" ]; then + errors+=("The SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +if [ -z "${SONATYPE_PASSWORD}" ]; then + errors+=("The SONATYPE_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +if [ -z "${GPG_SIGNING_KEY}" ]; then + errors+=("The GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +if [ -z "${GPG_SIGNING_PASSWORD}" ]; then + errors+=("The GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/build.gradle.kts b/build.gradle.kts index 95d3c75..dd5d657 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ repositories { allprojects { group = "com.phoebe.api" - version = "0.0.1-alpha.0" + version = "0.1.0-alpha.1" // x-release-please-version } subprojects { diff --git a/buildSrc/src/main/kotlin/phoebe.kotlin.gradle.kts b/buildSrc/src/main/kotlin/phoebe.kotlin.gradle.kts index 40e3ce8..9478991 100644 --- a/buildSrc/src/main/kotlin/phoebe.kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/phoebe.kotlin.gradle.kts @@ -33,6 +33,9 @@ kotlin { tasks.withType().configureEach { systemProperty("junit.jupiter.execution.parallel.enabled", true) systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") + + // `SKIP_MOCK_TESTS` affects which tests run so it must be added as input for proper cache invalidation. + inputs.property("skipMockTests", System.getenv("SKIP_MOCK_TESTS")).optional(true) } val ktfmt by configurations.creating diff --git a/buildSrc/src/main/kotlin/phoebe.publish.gradle.kts b/buildSrc/src/main/kotlin/phoebe.publish.gradle.kts index dd8ea61..a6386d7 100644 --- a/buildSrc/src/main/kotlin/phoebe.publish.gradle.kts +++ b/buildSrc/src/main/kotlin/phoebe.publish.gradle.kts @@ -27,9 +27,9 @@ configure { } scm { - connection.set("scm:git:git://github.com/stainless-sdks/phoebe-java.git") - developerConnection.set("scm:git:git://github.com/stainless-sdks/phoebe-java.git") - url.set("https://github.com/stainless-sdks/phoebe-java") + connection.set("scm:git:git://github.com/phoebe-bird/phoebe-java.git") + developerConnection.set("scm:git:git://github.com/phoebe-bird/phoebe-java.git") + url.set("https://github.com/phoebe-bird/phoebe-java") } versionMapping { @@ -40,6 +40,14 @@ configure { } } } + repositories { + if (project.hasProperty("publishLocal")) { + maven { + name = "LocalFileSystem" + url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo") + } + } + } } signing { diff --git a/phoebe-java-client-okhttp/build.gradle.kts b/phoebe-java-client-okhttp/build.gradle.kts index fc320fc..6ae5611 100644 --- a/phoebe-java-client-okhttp/build.gradle.kts +++ b/phoebe-java-client-okhttp/build.gradle.kts @@ -10,5 +10,6 @@ dependencies { implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") testImplementation(kotlin("test")) - testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation("org.assertj:assertj-core:3.27.7") + testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2") } diff --git a/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/OkHttpClient.kt b/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/OkHttpClient.kt index ad75691..6cc8418 100644 --- a/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/OkHttpClient.kt +++ b/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/OkHttpClient.kt @@ -13,12 +13,15 @@ import java.io.IOException import java.io.InputStream import java.net.Proxy import java.time.Duration +import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorService import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Call import okhttp3.Callback +import okhttp3.Dispatcher import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType @@ -29,8 +32,8 @@ import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor import okio.BufferedSink -class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpClient) : - HttpClient { +class OkHttpClient +private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient { override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse { val call = newCall(request, requestOptions) @@ -50,20 +53,25 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC ): CompletableFuture { val future = CompletableFuture() - request.body?.run { future.whenComplete { _, _ -> close() } } - - newCall(request, requestOptions) - .enqueue( - object : Callback { - override fun onResponse(call: Call, response: Response) { - future.complete(response.toResponse()) - } + val call = newCall(request, requestOptions) + call.enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + future.complete(response.toResponse()) + } - override fun onFailure(call: Call, e: IOException) { - future.completeExceptionally(PhoebeIoException("Request failed", e)) - } + override fun onFailure(call: Call, e: IOException) { + future.completeExceptionally(PhoebeIoException("Request failed", e)) } - ) + } + ) + + future.whenComplete { _, e -> + if (e is CancellationException) { + call.cancel() + } + request.body?.close() + } return future } @@ -194,6 +202,7 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC private var timeout: Timeout = Timeout.default() private var proxy: Proxy? = null + private var dispatcherExecutorService: ExecutorService? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null @@ -204,6 +213,10 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { this.sslSocketFactory = sslSocketFactory } @@ -219,12 +232,16 @@ class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpC fun build(): OkHttpClient = OkHttpClient( okhttp3.OkHttpClient.Builder() + // `RetryingHttpClient` handles retries if the user enabled them. + .retryOnConnectionFailure(false) .connectTimeout(timeout.connect()) .readTimeout(timeout.read()) .writeTimeout(timeout.write()) .callTimeout(timeout.request()) .proxy(proxy) .apply { + dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) } + val sslSocketFactory = sslSocketFactory val trustManager = trustManager if (sslSocketFactory != null && trustManager != null) { diff --git a/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/PhoebeOkHttpClient.kt b/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/PhoebeOkHttpClient.kt index 57aa49e..7e9e5ad 100644 --- a/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/PhoebeOkHttpClient.kt +++ b/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/PhoebeOkHttpClient.kt @@ -16,6 +16,7 @@ import java.net.Proxy import java.time.Clock import java.time.Duration import java.util.Optional +import java.util.concurrent.ExecutorService import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager @@ -44,11 +45,31 @@ class PhoebeOkHttpClient private constructor() { class Builder internal constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() + private var dispatcherExecutorService: ExecutorService? = null private var proxy: Proxy? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null + /** + * The executor service to use for running HTTP requests. + * + * Defaults to OkHttp's + * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104). + * + * This class takes ownership of the executor service and shuts it down when closed. + */ + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + + /** + * Alias for calling [Builder.dispatcherExecutorService] with + * `dispatcherExecutorService.orElse(null)`. + */ + fun dispatcherExecutorService(dispatcherExecutorService: Optional) = + dispatcherExecutorService(dispatcherExecutorService.getOrNull()) + fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ @@ -296,6 +317,7 @@ class PhoebeOkHttpClient private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .dispatcherExecutorService(dispatcherExecutorService) .sslSocketFactory(sslSocketFactory) .trustManager(trustManager) .hostnameVerifier(hostnameVerifier) diff --git a/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/PhoebeOkHttpClientAsync.kt b/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/PhoebeOkHttpClientAsync.kt index e133879..4fba04c 100644 --- a/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/PhoebeOkHttpClientAsync.kt +++ b/phoebe-java-client-okhttp/src/main/kotlin/com/phoebe/api/client/okhttp/PhoebeOkHttpClientAsync.kt @@ -16,6 +16,7 @@ import java.net.Proxy import java.time.Clock import java.time.Duration import java.util.Optional +import java.util.concurrent.ExecutorService import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager @@ -44,11 +45,31 @@ class PhoebeOkHttpClientAsync private constructor() { class Builder internal constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() + private var dispatcherExecutorService: ExecutorService? = null private var proxy: Proxy? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null + /** + * The executor service to use for running HTTP requests. + * + * Defaults to OkHttp's + * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104). + * + * This class takes ownership of the executor service and shuts it down when closed. + */ + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + + /** + * Alias for calling [Builder.dispatcherExecutorService] with + * `dispatcherExecutorService.orElse(null)`. + */ + fun dispatcherExecutorService(dispatcherExecutorService: Optional) = + dispatcherExecutorService(dispatcherExecutorService.getOrNull()) + fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ @@ -296,6 +317,7 @@ class PhoebeOkHttpClientAsync private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .dispatcherExecutorService(dispatcherExecutorService) .sslSocketFactory(sslSocketFactory) .trustManager(trustManager) .hostnameVerifier(hostnameVerifier) diff --git a/phoebe-java-client-okhttp/src/test/kotlin/com/phoebe/api/client/okhttp/OkHttpClientTest.kt b/phoebe-java-client-okhttp/src/test/kotlin/com/phoebe/api/client/okhttp/OkHttpClientTest.kt new file mode 100644 index 0000000..5ba90f8 --- /dev/null +++ b/phoebe-java-client-okhttp/src/test/kotlin/com/phoebe/api/client/okhttp/OkHttpClientTest.kt @@ -0,0 +1,44 @@ +package com.phoebe.api.client.okhttp + +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo +import com.github.tomakehurst.wiremock.junit5.WireMockTest +import com.phoebe.api.core.http.HttpMethod +import com.phoebe.api.core.http.HttpRequest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.ResourceLock + +@WireMockTest +@ResourceLock("https://github.com/wiremock/wiremock/issues/169") +internal class OkHttpClientTest { + + private lateinit var baseUrl: String + private lateinit var httpClient: OkHttpClient + + @BeforeEach + fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) { + baseUrl = wmRuntimeInfo.httpBaseUrl + httpClient = OkHttpClient.builder().build() + } + + @Test + fun executeAsync_whenFutureCancelled_cancelsUnderlyingCall() { + stubFor(post(urlPathEqualTo("/something")).willReturn(ok())) + val responseFuture = + httpClient.executeAsync( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build() + ) + val call = httpClient.okHttpClient.dispatcher.runningCalls().single() + + responseFuture.cancel(false) + + // Should have cancelled the underlying call + assertThat(call.isCanceled()).isTrue() + } +} diff --git a/phoebe-java-core/build.gradle.kts b/phoebe-java-core/build.gradle.kts index a65d57c..c5441df 100644 --- a/phoebe-java-core/build.gradle.kts +++ b/phoebe-java-core/build.gradle.kts @@ -5,14 +5,16 @@ plugins { configurations.all { resolutionStrategy { - // Compile and test against a lower Jackson version to ensure we're compatible with it. - // We publish with a higher version (see below) to ensure users depend on a secure version by default. - force("com.fasterxml.jackson.core:jackson-core:2.13.4") - force("com.fasterxml.jackson.core:jackson-databind:2.13.4") - force("com.fasterxml.jackson.core:jackson-annotations:2.13.4") - force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4") - force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4") - force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") + // Compile and test against a lower Jackson version to ensure we're compatible with it. Note that + // we generally support 2.13.4, but test against 2.14.0 because 2.13.4 has some annoying (but + // niche) bugs (users should upgrade if they encounter them). We publish with a higher version + // (see below) to ensure users depend on a secure version by default. + force("com.fasterxml.jackson.core:jackson-core:2.14.0") + force("com.fasterxml.jackson.core:jackson-databind:2.14.0") + force("com.fasterxml.jackson.core:jackson-annotations:2.14.0") + force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.0") + force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0") + force("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") } } @@ -31,7 +33,7 @@ dependencies { testImplementation(kotlin("test")) testImplementation(project(":phoebe-java-client-okhttp")) testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2") - testImplementation("org.assertj:assertj-core:3.25.3") + testImplementation("org.assertj:assertj-core:3.27.7") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3") testImplementation("org.junit-pioneer:junit-pioneer:1.9.1") diff --git a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/Check.kt b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/Check.kt index 295fcbc..e8c3e22 100644 --- a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/Check.kt +++ b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/Check.kt @@ -77,7 +77,7 @@ This can happen if you are either: Double-check that you are depending on compatible Jackson versions. -See https://www.github.com/stainless-sdks/phoebe-java#jackson for more information. +See https://www.github.com/phoebe-bird/phoebe-java#jackson for more information. """ .trimIndent() } diff --git a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/ClientOptions.kt b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/ClientOptions.kt index cadc2c5..9efbc61 100644 --- a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/ClientOptions.kt +++ b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/ClientOptions.kt @@ -402,6 +402,7 @@ private constructor( headers.put("X-Stainless-Package-Version", getPackageVersion()) headers.put("X-Stainless-Runtime", "JRE") headers.put("X-Stainless-Runtime-Version", getJavaVersion()) + headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString()) apiKey.let { if (!it.isEmpty()) { headers.put("X-eBirdApiToken", it) diff --git a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/ObjectMappers.kt b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/ObjectMappers.kt index 8c8d7e2..db714d1 100644 --- a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/ObjectMappers.kt +++ b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/ObjectMappers.kt @@ -24,7 +24,8 @@ import java.io.InputStream import java.time.DateTimeException import java.time.LocalDate import java.time.LocalDateTime -import java.time.ZonedDateTime +import java.time.OffsetDateTime +import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.temporal.ChronoField @@ -36,7 +37,7 @@ fun jsonMapper(): JsonMapper = .addModule( SimpleModule() .addSerializer(InputStreamSerializer) - .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer()) + .addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer()) ) .withCoercionConfig(LogicalType.Boolean) { it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) @@ -47,6 +48,7 @@ fun jsonMapper(): JsonMapper = } .withCoercionConfig(LogicalType.Integer) { it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) .setCoercion(CoercionInputShape.String, CoercionAction.Fail) .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) @@ -64,6 +66,12 @@ fun jsonMapper(): JsonMapper = .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) } + .withCoercionConfig(LogicalType.DateTime) { + it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) + } .withCoercionConfig(LogicalType.Array) { it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) @@ -124,10 +132,10 @@ private object InputStreamSerializer : BaseSerializer(InputStream:: } /** - * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes. + * A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes. */ -private class LenientLocalDateTimeDeserializer : - StdDeserializer(LocalDateTime::class.java) { +private class LenientOffsetDateTimeDeserializer : + StdDeserializer(OffsetDateTime::class.java) { companion object { @@ -141,7 +149,7 @@ private class LenientLocalDateTimeDeserializer : override fun logicalType(): LogicalType = LogicalType.DateTime - override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime { + override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime { val exceptions = mutableListOf() for (formatter in DATE_TIME_FORMATTERS) { @@ -150,17 +158,20 @@ private class LenientLocalDateTimeDeserializer : return when { !temporal.isSupported(ChronoField.HOUR_OF_DAY) -> - LocalDate.from(temporal).atStartOfDay() + LocalDate.from(temporal) + .atStartOfDay() + .atZone(ZoneId.of("UTC")) + .toOffsetDateTime() !temporal.isSupported(ChronoField.OFFSET_SECONDS) -> - LocalDateTime.from(temporal) - else -> ZonedDateTime.from(temporal).toLocalDateTime() + LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime() + else -> OffsetDateTime.from(temporal) } } catch (e: DateTimeException) { exceptions.add(e) } } - throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply { + throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply { exceptions.forEach { addSuppressed(it) } } } diff --git a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/http/HttpRequest.kt b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/http/HttpRequest.kt index e398c05..6ddee7f 100644 --- a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/http/HttpRequest.kt +++ b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/http/HttpRequest.kt @@ -2,6 +2,7 @@ package com.phoebe.api.core.http import com.phoebe.api.core.checkRequired import com.phoebe.api.core.toImmutable +import java.net.URLEncoder class HttpRequest private constructor( @@ -13,6 +14,35 @@ private constructor( @get:JvmName("body") val body: HttpRequestBody?, ) { + fun url(): String = buildString { + append(baseUrl) + + pathSegments.forEach { segment -> + if (!endsWith("/")) { + append("/") + } + append(URLEncoder.encode(segment, "UTF-8")) + } + + if (queryParams.isEmpty()) { + return@buildString + } + + append("?") + var isFirst = true + queryParams.keys().forEach { key -> + queryParams.values(key).forEach { value -> + if (!isFirst) { + append("&") + } + append(URLEncoder.encode(key, "UTF-8")) + append("=") + append(URLEncoder.encode(value, "UTF-8")) + isFirst = false + } + } + } + fun toBuilder(): Builder = Builder().from(this) override fun toString(): String = diff --git a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/http/RetryingHttpClient.kt b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/http/RetryingHttpClient.kt index d6041fe..5245d87 100644 --- a/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/http/RetryingHttpClient.kt +++ b/phoebe-java-core/src/main/kotlin/com/phoebe/api/core/http/RetryingHttpClient.kt @@ -31,10 +31,6 @@ private constructor( ) : HttpClient { override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse { - if (!isRetryable(request) || maxRetries <= 0) { - return httpClient.execute(request, requestOptions) - } - var modifiedRequest = maybeAddIdempotencyHeader(request) // Don't send the current retry count in the headers if the caller set their own value. @@ -48,6 +44,10 @@ private constructor( modifiedRequest = setRetryCountHeader(modifiedRequest, retries) } + if (!isRetryable(modifiedRequest)) { + return httpClient.execute(modifiedRequest, requestOptions) + } + val response = try { val response = httpClient.execute(modifiedRequest, requestOptions) @@ -75,10 +75,6 @@ private constructor( request: HttpRequest, requestOptions: RequestOptions, ): CompletableFuture { - if (!isRetryable(request) || maxRetries <= 0) { - return httpClient.executeAsync(request, requestOptions) - } - val modifiedRequest = maybeAddIdempotencyHeader(request) // Don't send the current retry count in the headers if the caller set their own value. @@ -94,8 +90,12 @@ private constructor( val requestWithRetryCount = if (shouldSendRetryCount) setRetryCountHeader(request, retries) else request - return httpClient - .executeAsync(requestWithRetryCount, requestOptions) + val responseFuture = httpClient.executeAsync(requestWithRetryCount, requestOptions) + if (!isRetryable(requestWithRetryCount)) { + return responseFuture + } + + return responseFuture .handleAsync( fun( response: HttpResponse?, diff --git a/phoebe-java-core/src/test/kotlin/com/phoebe/api/core/ObjectMappersTest.kt b/phoebe-java-core/src/test/kotlin/com/phoebe/api/core/ObjectMappersTest.kt index 35ca1ac..115b997 100644 --- a/phoebe-java-core/src/test/kotlin/com/phoebe/api/core/ObjectMappersTest.kt +++ b/phoebe-java-core/src/test/kotlin/com/phoebe/api/core/ObjectMappersTest.kt @@ -3,12 +3,14 @@ package com.phoebe.api.core import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.fasterxml.jackson.module.kotlin.readValue -import java.time.LocalDateTime +import java.time.LocalDate +import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.ZoneOffset import kotlin.reflect.KClass import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource import org.junitpioneer.jupiter.cartesian.CartesianTest @@ -46,11 +48,7 @@ internal class ObjectMappersTest { val VALID_CONVERSIONS = listOf( FLOAT to DOUBLE, - FLOAT to INTEGER, - FLOAT to LONG, DOUBLE to FLOAT, - DOUBLE to INTEGER, - DOUBLE to LONG, INTEGER to FLOAT, INTEGER to DOUBLE, INTEGER to LONG, @@ -58,14 +56,6 @@ internal class ObjectMappersTest { LONG to DOUBLE, LONG to INTEGER, CLASS to MAP, - // These aren't actually valid, but coercion configs don't work for String until - // v2.14.0: https://github.com/FasterXML/jackson-databind/issues/3240 - // We currently test on v2.13.4. - BOOLEAN to STRING, - FLOAT to STRING, - DOUBLE to STRING, - INTEGER to STRING, - LONG to STRING, ) } } @@ -84,19 +74,44 @@ internal class ObjectMappersTest { } } - enum class LenientLocalDateTimeTestCase(val string: String) { - DATE("1998-04-21"), - DATE_TIME("1998-04-21T04:00:00"), - ZONED_DATE_TIME_1("1998-04-21T04:00:00+03:00"), - ZONED_DATE_TIME_2("1998-04-21T04:00:00Z"), + enum class LenientOffsetDateTimeTestCase( + val string: String, + val expectedOffsetDateTime: OffsetDateTime, + ) { + DATE( + "1998-04-21", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(0, 0), ZoneOffset.UTC), + ), + DATE_TIME( + "1998-04-21T04:00:00", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC), + ), + ZONED_DATE_TIME_1( + "1998-04-21T04:00:00+03:00", + expectedOffsetDateTime = + OffsetDateTime.of( + LocalDate.of(1998, 4, 21), + LocalTime.of(4, 0), + ZoneOffset.ofHours(3), + ), + ), + ZONED_DATE_TIME_2( + "1998-04-21T04:00:00Z", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC), + ), } @ParameterizedTest @EnumSource - fun readLocalDateTime_lenient(testCase: LenientLocalDateTimeTestCase) { + fun readOffsetDateTime_lenient(testCase: LenientOffsetDateTimeTestCase) { val jsonMapper = jsonMapper() val json = jsonMapper.writeValueAsString(testCase.string) - assertDoesNotThrow { jsonMapper().readValue(json) } + val offsetDateTime = jsonMapper().readValue(json) + + assertThat(offsetDateTime).isEqualTo(testCase.expectedOffsetDateTime) } } diff --git a/phoebe-java-core/src/test/kotlin/com/phoebe/api/core/http/HttpRequestTest.kt b/phoebe-java-core/src/test/kotlin/com/phoebe/api/core/http/HttpRequestTest.kt new file mode 100644 index 0000000..063a971 --- /dev/null +++ b/phoebe-java-core/src/test/kotlin/com/phoebe/api/core/http/HttpRequestTest.kt @@ -0,0 +1,110 @@ +package com.phoebe.api.core.http + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +internal class HttpRequestTest { + + enum class UrlTestCase(val request: HttpRequest, val expectedUrl: String) { + BASE_URL_ONLY( + HttpRequest.builder().method(HttpMethod.GET).baseUrl("https://api.example.com").build(), + expectedUrl = "https://api.example.com", + ), + BASE_URL_WITH_TRAILING_SLASH( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com/") + .build(), + expectedUrl = "https://api.example.com/", + ), + SINGLE_PATH_SEGMENT( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .build(), + expectedUrl = "https://api.example.com/users", + ), + MULTIPLE_PATH_SEGMENTS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegments("users", "123", "profile") + .build(), + expectedUrl = "https://api.example.com/users/123/profile", + ), + PATH_SEGMENT_WITH_SPECIAL_CHARS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("user name") + .build(), + expectedUrl = "https://api.example.com/user+name", + ), + SINGLE_QUERY_PARAM( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .putQueryParam("limit", "10") + .build(), + expectedUrl = "https://api.example.com/users?limit=10", + ), + MULTIPLE_QUERY_PARAMS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .putQueryParam("limit", "10") + .putQueryParam("offset", "20") + .build(), + expectedUrl = "https://api.example.com/users?limit=10&offset=20", + ), + QUERY_PARAM_WITH_SPECIAL_CHARS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("search") + .putQueryParam("q", "hello world") + .build(), + expectedUrl = "https://api.example.com/search?q=hello+world", + ), + MULTIPLE_VALUES_SAME_PARAM( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .putQueryParams("tags", listOf("admin", "user")) + .build(), + expectedUrl = "https://api.example.com/users?tags=admin&tags=user", + ), + BASE_URL_WITH_TRAILING_SLASH_AND_PATH( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com/") + .addPathSegment("users") + .build(), + expectedUrl = "https://api.example.com/users", + ), + COMPLEX_URL( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl("https://api.example.com") + .addPathSegments("v1", "users", "123") + .putQueryParams("include", listOf("profile", "settings")) + .putQueryParam("format", "json") + .build(), + expectedUrl = + "https://api.example.com/v1/users/123?include=profile&include=settings&format=json", + ), + } + + @ParameterizedTest + @EnumSource + fun url(testCase: UrlTestCase) { + val actualUrl = testCase.request.url() + + assertThat(actualUrl).isEqualTo(testCase.expectedUrl) + } +} diff --git a/phoebe-java-proguard-test/build.gradle.kts b/phoebe-java-proguard-test/build.gradle.kts index 932d96a..248bccd 100644 --- a/phoebe-java-proguard-test/build.gradle.kts +++ b/phoebe-java-proguard-test/build.gradle.kts @@ -18,8 +18,8 @@ dependencies { testImplementation(project(":phoebe-java")) testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") - testImplementation("org.assertj:assertj-core:3.25.3") - testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") + testImplementation("org.assertj:assertj-core:3.27.7") + testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") } tasks.shadowJar { diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..8f98719 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,67 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "simple", + "extra-files": [ + "README.md", + "build.gradle.kts" + ] +} \ No newline at end of file diff --git a/scripts/build b/scripts/build index f406348..16a2b00 100755 --- a/scripts/build +++ b/scripts/build @@ -5,4 +5,4 @@ set -e cd "$(dirname "$0")/.." echo "==> Building classes" -./gradlew build testClasses -x test +./gradlew build testClasses "$@" -x test diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts new file mode 100755 index 0000000..10f3c70 --- /dev/null +++ b/scripts/upload-artifacts @@ -0,0 +1,193 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# ANSI Color Codes +GREEN='\033[32m' +RED='\033[31m' +NC='\033[0m' # No Color + +MAVEN_REPO_PATH="./build/local-maven-repo" + +log_error() { + local msg="$1" + local headers="$2" + local body="$3" + echo -e "${RED}${msg}${NC}" + [[ -f "$headers" ]] && echo -e "${RED}Headers:$(cat "$headers")${NC}" + echo -e "${RED}Body: ${body}${NC}" + exit 1 +} + +upload_file() { + local file_name="$1" + local tmp_headers + tmp_headers=$(mktemp) + + if [ -f "$file_name" ]; then + echo -e "${GREEN}Processing file: $file_name${NC}" + pkg_file_name="mvn${file_name#"${MAVEN_REPO_PATH}"}" + + # Get signed URL for uploading artifact file + signed_url_response=$(curl -X POST -G "$URL" \ + -sS --retry 5 \ + -D "$tmp_headers" \ + --data-urlencode "filename=$pkg_file_name" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + + # Validate JSON and extract URL + if ! signed_url=$(echo "$signed_url_response" | jq -e -r '.url' 2>/dev/null) || [[ "$signed_url" == "null" ]]; then + log_error "Failed to get valid signed URL" "$tmp_headers" "$signed_url_response" + fi + + # Set content-type based on file extension + local extension="${file_name##*.}" + local content_type + case "$extension" in + jar) content_type="application/java-archive" ;; + md5|sha1|sha256|sha512) content_type="text/plain" ;; + module) content_type="application/json" ;; + pom|xml) content_type="application/xml" ;; + html) content_type="text/html" ;; + *) content_type="application/octet-stream" ;; + esac + + # Upload file + upload_response=$(curl -v -X PUT \ + --retry 5 \ + --retry-all-errors \ + -D "$tmp_headers" \ + -H "Content-Type: $content_type" \ + --data-binary "@${file_name}" "$signed_url" 2>&1) + + if ! echo "$upload_response" | grep -q "HTTP/[0-9.]* 200"; then + log_error "Failed to upload artifact file" "$tmp_headers" "$upload_response" + fi + + # Insert small throttle to reduce rate limiting risk + sleep 0.1 + fi +} + +walk_tree() { + local current_dir="$1" + + for entry in "$current_dir"/*; do + # Check that entry is valid + [ -e "$entry" ] || [ -h "$entry" ] || continue + + if [ -d "$entry" ]; then + walk_tree "$entry" + else + upload_file "$entry" + fi + done +} + +generate_instructions() { + cat << EOF > "$MAVEN_REPO_PATH/index.html" + + + + Maven Repo + + +

Stainless SDK Maven Repository

+

This is the Maven repository for your Stainless Java SDK build.

+ +

Project configuration

+ +

The details depend on whether you're using Maven or Gradle as your build tool.

+ +

Maven

+ +

Add the following to your project's pom.xml:

+
<repositories>
+    <repository>
+        <id>stainless-sdk-repo</id>
+        <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+    </repository>
+</repositories>
+ +

Gradle

+

Add the following to your build.gradle file:

+
repositories {
+    maven {
+        url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+    }
+}
+ +
+

Configuring authentication (if required)

+ +

Some accounts may require authentication to access the repository. If so, use the + following instructions, replacing YOUR_STAINLESS_API_TOKEN with your actual token.

+ +

Maven with authentication

+ +

First, ensure you have the following in your Maven settings.xml for repo authentication:

+
<servers>
+    <server>
+        <id>stainless-sdk-repo</id>
+        <configuration>
+            <httpHeaders>
+                <property>
+                    <name>Authorization</name>
+                    <value>Bearer YOUR_STAINLESS_API_TOKEN</value>
+                </property>
+            </httpHeaders>
+        </configuration>
+    </server>
+</servers>
+ +

Then, add the following to your project's pom.xml:

+
<repositories>
+    <repository>
+        <id>stainless-sdk-repo</id>
+        <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+    </repository>
+</repositories>
+ +

Gradle with authentication

+

Add the following to your build.gradle file:

+
repositories {
+    maven {
+        url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+        credentials(HttpHeaderCredentials) {
+            name = "Authorization"
+            value = "Bearer YOUR_STAINLESS_API_TOKEN"
+        }
+        authentication {
+            header(HttpHeaderAuthentication)
+        }
+    }
+}
+
+ +

Using the repository

+

Once you've configured the repository, you can include dependencies from it as usual. See your + project README + for more details.

+ + +EOF + upload_file "${MAVEN_REPO_PATH}/index.html" + + echo "Configure maven or gradle to use the repo located at 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'" + echo "For more details, see the directions in https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn/index.html" +} + +cd "$(dirname "$0")/.." + +echo "::group::Creating local Maven content" +./gradlew publishMavenPublicationToLocalFileSystemRepository -PpublishLocal +echo "::endgroup::" + +echo "::group::Uploading to pkg.stainless.com" +walk_tree "$MAVEN_REPO_PATH" +echo "::endgroup::" + +echo "::group::Generating instructions" +generate_instructions +echo "::endgroup::"