diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 794fd9f5..e2fe79ce 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -18,6 +18,6 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@67cbd7a15a6eeea0c3a0dffff4768fa5653de05c + - uses: amannn/action-semantic-pull-request@cfb60706e18bc85e8aec535e3c577abe8f70378e env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 37739249..538322eb 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -20,9 +20,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@cd7d8d697e10461458bc61a30d094dc601a8b017 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b - name: Set up JDK 8 - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 + uses: actions/setup-java@a1c6c9c8677803c9f4bd31e0f15ac0844258f955 with: java-version: '8' distribution: 'temurin' @@ -49,7 +49,7 @@ jobs: run: mvn --batch-mode --update-snapshots verify - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.1.0 + uses: codecov/codecov-action@v4.3.1 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 61177ed6..5825acba 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -10,17 +10,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the code - uses: actions/checkout@cd7d8d697e10461458bc61a30d094dc601a8b017 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b - name: Set up JDK 8 - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 + uses: actions/setup-java@a1c6c9c8677803c9f4bd31e0f15ac0844258f955 with: java-version: '8' distribution: 'temurin' cache: maven - name: Initialize CodeQL - uses: github/codeql-action/init@3d817349a4534f494b019aff837b9a577fdc5496 + uses: github/codeql-action/init@84d6ead480f493c32a39f012db4b9dfb02e8868b with: languages: java @@ -36,7 +36,7 @@ jobs: run: mvn --batch-mode --update-snapshots --activate-profiles e2e verify - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4.1.0 + uses: codecov/codecov-action@v4.3.1 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos flags: unittests # optional @@ -45,4 +45,4 @@ jobs: verbose: true # optional (default = false) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3d817349a4534f494b019aff837b9a577fdc5496 + uses: github/codeql-action/analyze@84d6ead480f493c32a39f012db4b9dfb02e8868b diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ab37998..26c8b868 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,10 +28,10 @@ jobs: # These steps are only run if this was a merged release-please PR - name: checkout if: ${{ steps.release.outputs.release_created }} - uses: actions/checkout@cd7d8d697e10461458bc61a30d094dc601a8b017 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b - name: Set up JDK 8 if: ${{ steps.release.outputs.release_created }} - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 + uses: actions/setup-java@a1c6c9c8677803c9f4bd31e0f15ac0844258f955 with: java-version: '8' distribution: 'temurin' diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml index c19702ee..397d5144 100644 --- a/.github/workflows/static-code-scanning.yaml +++ b/.github/workflows/static-code-scanning.yaml @@ -29,16 +29,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@cd7d8d697e10461458bc61a30d094dc601a8b017 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@3d817349a4534f494b019aff837b9a577fdc5496 + uses: github/codeql-action/init@84d6ead480f493c32a39f012db4b9dfb02e8868b with: languages: java - name: Autobuild - uses: github/codeql-action/autobuild@3d817349a4534f494b019aff837b9a577fdc5496 + uses: github/codeql-action/autobuild@84d6ead480f493c32a39f012db4b9dfb02e8868b - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3d817349a4534f494b019aff837b9a577fdc5496 + uses: github/codeql-action/analyze@84d6ead480f493c32a39f012db4b9dfb02e8868b diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 63f23943..57327365 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"1.7.6"} \ No newline at end of file +{".":"1.8.0"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5accb8c0..328e4800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,73 @@ # Changelog +## [1.8.0](https://github.com/open-feature/java-sdk/compare/v1.7.6...v1.8.0) (2024-05-03) + + +### ๐Ÿ› Bug Fixes + +* consistent method chainability ([#913](https://github.com/open-feature/java-sdk/issues/913)) ([d69cf5d](https://github.com/open-feature/java-sdk/commit/d69cf5d49b7fdc67c14c3b7750e5ba6173363fb0)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.16.1 ([#866](https://github.com/open-feature/java-sdk/issues/866)) ([6765b31](https://github.com/open-feature/java-sdk/commit/6765b31263298b2fffa1f87d06bfedd145eb81eb)) +* **deps:** update dependency io.cucumber:cucumber-bom to v7.17.0 ([#897](https://github.com/open-feature/java-sdk/issues/897)) ([d7f074d](https://github.com/open-feature/java-sdk/commit/d7f074d79b2c75d3365d45389c5b1d6bd2b2cf2e)) +* **deps:** update dependency org.slf4j:slf4j-api to v2.0.13 ([#888](https://github.com/open-feature/java-sdk/issues/888)) ([f524fd5](https://github.com/open-feature/java-sdk/commit/f524fd5c7d9968ce3538f9df5e66bee6cfc77dcd)) +* removed javax.nullable annotations ([#921](https://github.com/open-feature/java-sdk/issues/921)) ([cd7470d](https://github.com/open-feature/java-sdk/commit/cd7470dd7acc8118132abe68eddb1b3832c8ecc9)) +* shutdown method blocks until task executor shutdown completes ([#873](https://github.com/open-feature/java-sdk/issues/873)) ([8dec14f](https://github.com/open-feature/java-sdk/commit/8dec14fbeaf331b9dfcd98d8ffffcc0f5cc48c6f)) + + +### โœจ New Features + +* context propagation ([#848](https://github.com/open-feature/java-sdk/issues/848)) ([de5aa64](https://github.com/open-feature/java-sdk/commit/de5aa6420fe1652ab7d6e24c61d5a7fd306a4e43)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/checkout digest to 1d96c77 ([#896](https://github.com/open-feature/java-sdk/issues/896)) ([21b2e9b](https://github.com/open-feature/java-sdk/commit/21b2e9bb3b68423c34e0a979504167cb7786e4c7)) +* **deps:** update actions/checkout digest to 43045ae ([#903](https://github.com/open-feature/java-sdk/issues/903)) ([c7ebef3](https://github.com/open-feature/java-sdk/commit/c7ebef3b8fe8e47f3921d28eef6adf1e3eccc3ba)) +* **deps:** update actions/checkout digest to 44c2b7a ([#914](https://github.com/open-feature/java-sdk/issues/914)) ([8be250e](https://github.com/open-feature/java-sdk/commit/8be250e57ce131d59f4e563f46650451992e1e90)) +* **deps:** update actions/checkout digest to 8459bc0 ([#907](https://github.com/open-feature/java-sdk/issues/907)) ([5fe18df](https://github.com/open-feature/java-sdk/commit/5fe18df2d334deab7f318ad10c6f7ff98dbc0978)) +* **deps:** update actions/setup-java digest to a1c6c9c ([#919](https://github.com/open-feature/java-sdk/issues/919)) ([3f8c009](https://github.com/open-feature/java-sdk/commit/3f8c009139bd827f4d809d68c26cc20941d8c1e3)) +* **deps:** update amannn/action-semantic-pull-request digest to c24d6dd ([#904](https://github.com/open-feature/java-sdk/issues/904)) ([2cc9700](https://github.com/open-feature/java-sdk/commit/2cc9700af32806e00bd8f8b9d03b5937139a6b92)) +* **deps:** update amannn/action-semantic-pull-request digest to cfb6070 ([#908](https://github.com/open-feature/java-sdk/issues/908)) ([4f952fc](https://github.com/open-feature/java-sdk/commit/4f952fc4857158cc283d3d03fef98c4015084b14)) +* **deps:** update codecov/codecov-action action to v4.1.1 ([#870](https://github.com/open-feature/java-sdk/issues/870)) ([6a9a778](https://github.com/open-feature/java-sdk/commit/6a9a77817d66c433ac72b679156242ca81d79ff0)) +* **deps:** update codecov/codecov-action action to v4.2.0 ([#877](https://github.com/open-feature/java-sdk/issues/877)) ([7e236b8](https://github.com/open-feature/java-sdk/commit/7e236b8038ad19baacb5f80de0374379267b1180)) +* **deps:** update codecov/codecov-action action to v4.3.0 ([#886](https://github.com/open-feature/java-sdk/issues/886)) ([0464fa6](https://github.com/open-feature/java-sdk/commit/0464fa64bf652c7e2428f7a4896721cbf6694464)) +* **deps:** update codecov/codecov-action action to v4.3.1 ([#915](https://github.com/open-feature/java-sdk/issues/915)) ([3728fdd](https://github.com/open-feature/java-sdk/commit/3728fddde87cbaade48be4767a75b813cb10a0a1)) +* **deps:** update dependency com.github.spotbugs:spotbugs to v4.8.4 ([#881](https://github.com/open-feature/java-sdk/issues/881)) ([219afc1](https://github.com/open-feature/java-sdk/commit/219afc1358d11ec576c2a7877f09fb8d696901b9)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.8.4.0 ([#885](https://github.com/open-feature/java-sdk/issues/885)) ([8f00248](https://github.com/open-feature/java-sdk/commit/8f00248e6be546b85d17ed2fd06c6e4719fc09cd)) +* **deps:** update dependency com.google.guava:guava to v33.2.0-jre ([#917](https://github.com/open-feature/java-sdk/issues/917)) ([1a3a0b1](https://github.com/open-feature/java-sdk/commit/1a3a0b1952869a8326e643335c4e55f1c302d285)) +* **deps:** update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.2.2 ([#869](https://github.com/open-feature/java-sdk/issues/869)) ([bfe3b8d](https://github.com/open-feature/java-sdk/commit/bfe3b8d3127a2355bdf9ce75930ffe1d219f6945)) +* **deps:** update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.2.3 ([#887](https://github.com/open-feature/java-sdk/issues/887)) ([2c37cd5](https://github.com/open-feature/java-sdk/commit/2c37cd593c647d88e1615a7dd741e2309e230f04)) +* **deps:** update dependency org.apache.maven.plugins:maven-gpg-plugin to v3.2.4 ([#898](https://github.com/open-feature/java-sdk/issues/898)) ([b446fdc](https://github.com/open-feature/java-sdk/commit/b446fdc6f60f4363c2c1ba95f921ec466ed1cac7)) +* **deps:** update dependency org.apache.maven.plugins:maven-jar-plugin to v3.4.0 ([#890](https://github.com/open-feature/java-sdk/issues/890)) ([b8de9e8](https://github.com/open-feature/java-sdk/commit/b8de9e8de2372789667d46564c86453181f080ed)) +* **deps:** update dependency org.apache.maven.plugins:maven-jar-plugin to v3.4.1 ([#899](https://github.com/open-feature/java-sdk/issues/899)) ([af54031](https://github.com/open-feature/java-sdk/commit/af5403125ac7f78648d68c17004509c0238b1444)) +* **deps:** update dependency org.apache.maven.plugins:maven-source-plugin to v3.3.1 ([#879](https://github.com/open-feature/java-sdk/issues/879)) ([8a438c0](https://github.com/open-feature/java-sdk/commit/8a438c03c0ae534b6e91fd146f3e8ec06135e0a3)) +* **deps:** update dependency org.cyclonedx:cyclonedx-maven-plugin to v2.8.0 ([#864](https://github.com/open-feature/java-sdk/issues/864)) ([eb5c43d](https://github.com/open-feature/java-sdk/commit/eb5c43d82c3c984fa9a7c4b007d209187cf205f4)) +* **deps:** update dependency org.jacoco:jacoco-maven-plugin to v0.8.12 ([#875](https://github.com/open-feature/java-sdk/issues/875)) ([b21125d](https://github.com/open-feature/java-sdk/commit/b21125d9e28a4e8a6055ddb2da57e6f93336b8a6)) +* **deps:** update github/codeql-action digest to 0ad7791 ([#909](https://github.com/open-feature/java-sdk/issues/909)) ([99c4133](https://github.com/open-feature/java-sdk/commit/99c4133758fec73b568ee773549bbc85faf9b7e7)) +* **deps:** update github/codeql-action digest to 1c270d0 ([#880](https://github.com/open-feature/java-sdk/issues/880)) ([dd671ad](https://github.com/open-feature/java-sdk/commit/dd671adfa38be486dd1594b46a3b50869d7bbec1)) +* **deps:** update github/codeql-action digest to 21eac7c ([#883](https://github.com/open-feature/java-sdk/issues/883)) ([c1cd8f0](https://github.com/open-feature/java-sdk/commit/c1cd8f026d689dacbbb334eaeebcddd8e2a645b1)) +* **deps:** update github/codeql-action digest to 24a0170 ([#884](https://github.com/open-feature/java-sdk/issues/884)) ([8d77aa8](https://github.com/open-feature/java-sdk/commit/8d77aa8be2198425c52d1d6d6f2bf5bc03e90d25)) +* **deps:** update github/codeql-action digest to 24a95a0 ([#882](https://github.com/open-feature/java-sdk/issues/882)) ([dd6d406](https://github.com/open-feature/java-sdk/commit/dd6d406d9cc4fc57b2f4eaef27e7b988cd567dc1)) +* **deps:** update github/codeql-action digest to 24b71bd ([#892](https://github.com/open-feature/java-sdk/issues/892)) ([dcc6989](https://github.com/open-feature/java-sdk/commit/dcc698951c46d227f2f7d2bfb2595ba0d9cd3eaf)) +* **deps:** update github/codeql-action digest to 2b2cee5 ([#891](https://github.com/open-feature/java-sdk/issues/891)) ([be948e7](https://github.com/open-feature/java-sdk/commit/be948e710c55342edf91d0db561b2160680eb5d8)) +* **deps:** update github/codeql-action digest to 3bd9c3e ([#876](https://github.com/open-feature/java-sdk/issues/876)) ([c731b22](https://github.com/open-feature/java-sdk/commit/c731b22bae1e76427133959215af845fed150465)) +* **deps:** update github/codeql-action digest to 41857ba ([#916](https://github.com/open-feature/java-sdk/issues/916)) ([f364ca5](https://github.com/open-feature/java-sdk/commit/f364ca52d8d105b0c5432fdf4d6910ff55afee6b)) +* **deps:** update github/codeql-action digest to 4909c1f ([#902](https://github.com/open-feature/java-sdk/issues/902)) ([82cb6f6](https://github.com/open-feature/java-sdk/commit/82cb6f677147425dc47713a4630329dad7f42693)) +* **deps:** update github/codeql-action digest to 4ebadbc ([#911](https://github.com/open-feature/java-sdk/issues/911)) ([8edf1a6](https://github.com/open-feature/java-sdk/commit/8edf1a636a24d58c83791cb686ed4968c149ada0)) +* **deps:** update github/codeql-action digest to 7df281f ([#878](https://github.com/open-feature/java-sdk/issues/878)) ([e1b563a](https://github.com/open-feature/java-sdk/commit/e1b563a671263e4a30683298ac475407acab542b)) +* **deps:** update github/codeql-action digest to 82edfe2 ([#895](https://github.com/open-feature/java-sdk/issues/895)) ([d3ae425](https://github.com/open-feature/java-sdk/commit/d3ae4259e5c7bc32b5e093cffd79329624b114fb)) +* **deps:** update github/codeql-action digest to 84ba7fb ([#871](https://github.com/open-feature/java-sdk/issues/871)) ([fb57fab](https://github.com/open-feature/java-sdk/commit/fb57fab7d3199504865271541ecc5cc89adb9be4)) +* **deps:** update github/codeql-action digest to 84d6ead ([#920](https://github.com/open-feature/java-sdk/issues/920)) ([95cf8b4](https://github.com/open-feature/java-sdk/commit/95cf8b485728046b6b5146ba76c30143671e58e5)) +* **deps:** update github/codeql-action digest to 8fcfedf ([#912](https://github.com/open-feature/java-sdk/issues/912)) ([74c72ea](https://github.com/open-feature/java-sdk/commit/74c72eac90e70ec9cfbc1c1d559cc7c92dca1a0d)) +* **deps:** update github/codeql-action digest to 93b8232 ([#918](https://github.com/open-feature/java-sdk/issues/918)) ([0a3e053](https://github.com/open-feature/java-sdk/commit/0a3e0538f5177e47e77947f3def68b04831097e7)) +* **deps:** update github/codeql-action digest to 956f09c ([#868](https://github.com/open-feature/java-sdk/issues/868)) ([145bf61](https://github.com/open-feature/java-sdk/commit/145bf61504ac7c689429e2754643cea38cd7e901)) +* **deps:** update github/codeql-action digest to 99c9897 ([#874](https://github.com/open-feature/java-sdk/issues/874)) ([f97cdd7](https://github.com/open-feature/java-sdk/commit/f97cdd79f5d987c43fde480ed592bdc5b825d7a8)) +* **deps:** update github/codeql-action digest to b8e2556 ([#893](https://github.com/open-feature/java-sdk/issues/893)) ([9e6ba1d](https://github.com/open-feature/java-sdk/commit/9e6ba1da33c122026d6d99c512ed76ecd5776bd9)) +* **deps:** update github/codeql-action digest to c4fb451 ([#894](https://github.com/open-feature/java-sdk/issues/894)) ([7e24154](https://github.com/open-feature/java-sdk/commit/7e24154390b173b9d4b72317ad69a2c05618e252)) +* **deps:** update github/codeql-action digest to d30d1ca ([#889](https://github.com/open-feature/java-sdk/issues/889)) ([4cdd738](https://github.com/open-feature/java-sdk/commit/4cdd738f36b3089baff6a62ca1b48b51b50a9119)) +* **deps:** update github/codeql-action digest to dbf2b17 ([#905](https://github.com/open-feature/java-sdk/issues/905)) ([849a6c0](https://github.com/open-feature/java-sdk/commit/849a6c0a9f75c61a437744927093ddc72a78f843)) +* **deps:** update github/codeql-action digest to f45390c ([#901](https://github.com/open-feature/java-sdk/issues/901)) ([08712b4](https://github.com/open-feature/java-sdk/commit/08712b488ea80d509974fc5de61cc9fa28835159)) +* improve contrib guide ([#863](https://github.com/open-feature/java-sdk/issues/863)) ([46d04fe](https://github.com/open-feature/java-sdk/commit/46d04feb4ba909ac28e6acf25145958a045b231a)) + ## [1.7.6](https://github.com/open-feature/java-sdk/compare/v1.7.5...v1.7.6) (2024-03-22) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8acd316a..81d5d462 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,9 @@ be a jerk. We're not keen on vendor-specific stuff in this library, but if there are changes that need to happen in the spec to enable vendor-specific stuff in user code or other extension points, check out [the spec](https://github.com/open-feature/spec). -Any contributions you make are expected to be tested with unit tests. You can validate these work with `gradle test`, or the automation itself will run them for you when you make a PR. +Any contributions you make are expected to be tested with unit tests. You can validate these work with `mvn test`. +Further, it is recommended to verify code styling and static code analysis with `mvn verify -P !deploy`. +Regardless, the automation itself will run them for you when you open a PR. Your code is supposed to work with Java 8+. diff --git a/README.md b/README.md index 36095f06..b7fa813a 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ - - Release + + Release @@ -59,7 +59,7 @@ Note that this library is intended to be used in server-side contexts and has no dev.openfeature sdk - 1.7.6 + 1.8.0 ``` @@ -84,7 +84,7 @@ If you would like snapshot builds, this is the relevant repository information: ```groovy dependencies { - implementation 'dev.openfeature:sdk:1.7.6' + implementation 'dev.openfeature:sdk:1.8.0' } ``` @@ -120,16 +120,17 @@ See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs. ## ๐ŸŒŸ Features -| Status | Features | Description | -| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| โœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| โœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| โœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| โœ… | [Logging](#logging) | Integrate with popular logging packages. | -| โœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | -| โœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| โœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| โœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| Status | Features | Description | +| ------ |-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| โœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| โœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| โœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| โœ… | [Logging](#logging) | Integrate with popular logging packages. | +| โœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | +| โœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| โœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| โœ… | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). | +| โœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | Implemented: โœ… | In-progress: โš ๏ธ | Not implemented yet: โŒ @@ -272,6 +273,27 @@ This should only be called when your application is in the process of shutting d OpenFeatureAPI.getInstance().shutdown(); ``` +### Transaction Context Propagation +Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP). +Transaction context can be set where specific data is available (e.g. an auth service or request handler) and by using the transaction context propagator it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread). +By default, the `NoOpTransactionContextPropagator` is used, which doesn't store anything. +To register a `ThreadLocal` context propagator, you can use the `setTransactionContextPropagator` method as shown below. +```java +// registering the ThreadLocalTransactionContextPropagator +OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator()); +``` +Once you've registered a transaction context propagator, you can propagate the data into request scoped transaction context. + +```java +// adding userId to transaction context +OpenFeatureAPI api = OpenFeatureAPI.getInstance(); +Map transactionAttrs = new HashMap<>(); +transactionAttrs.put("userId", new Value("userId")); +EvaluationContext transactionCtx = new ImmutableContext(transactionAttrs); +api.setTransactionContext(apiCtx); +``` +Additionally, you can develop a custom transaction context propagator by implementing the `TransactionContextPropagator` interface and registering it as shown above. + ## Extending ### Develop a provider diff --git a/pom.xml b/pom.xml index 845824f6..b6fef55e 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ dev.openfeature sdk - 1.7.6 + 1.8.0 UTF-8 @@ -53,14 +53,14 @@ com.github.spotbugs spotbugs - 4.8.3 + 4.8.4 provided org.slf4j slf4j-api - 2.0.12 + 2.0.13 @@ -135,7 +135,7 @@ com.google.guava guava - 33.1.0-jre + 33.2.0-jre test @@ -154,7 +154,7 @@ io.cucumber cucumber-bom - 7.16.0 + 7.17.0 pom import @@ -175,7 +175,7 @@ org.cyclonedx cyclonedx-maven-plugin - 2.7.11 + 2.8.0 library 1.3 @@ -261,7 +261,7 @@ org.jacoco jacoco-maven-plugin - 0.8.11 + 0.8.12 @@ -321,7 +321,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.3.0 + 3.4.1 @@ -349,7 +349,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.8.3.1 + 4.8.4.0 spotbugs-exclusions.xml @@ -365,7 +365,7 @@ com.github.spotbugs spotbugs - 4.8.3 + 4.8.4 @@ -438,7 +438,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.0 + 3.3.1 attach-sources @@ -472,7 +472,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.1 + 3.2.4 sign-artifacts diff --git a/src/main/java/dev/openfeature/sdk/Client.java b/src/main/java/dev/openfeature/sdk/Client.java index ebca0b13..4494180a 100644 --- a/src/main/java/dev/openfeature/sdk/Client.java +++ b/src/main/java/dev/openfeature/sdk/Client.java @@ -18,7 +18,7 @@ public interface Client extends Features, EventBus { * Set the client-level evaluation context. * @param ctx Client level context. */ - void setEvaluationContext(EvaluationContext ctx); + Client setEvaluationContext(EvaluationContext ctx); /** * Adds hooks for evaluation. @@ -26,7 +26,7 @@ public interface Client extends Features, EventBus { * * @param hooks The hook to add. */ - void addHooks(Hook... hooks); + Client addHooks(Hook... hooks); /** * Fetch the hooks associated to this client. diff --git a/src/main/java/dev/openfeature/sdk/EventDetails.java b/src/main/java/dev/openfeature/sdk/EventDetails.java index d4ecac93..02b1964d 100644 --- a/src/main/java/dev/openfeature/sdk/EventDetails.java +++ b/src/main/java/dev/openfeature/sdk/EventDetails.java @@ -1,6 +1,5 @@ package dev.openfeature.sdk; -import edu.umd.cs.findbugs.annotations.Nullable; import lombok.Data; import lombok.experimental.SuperBuilder; @@ -19,8 +18,8 @@ static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventD static EventDetails fromProviderEventDetails( ProviderEventDetails providerEventDetails, - @Nullable String providerName, - @Nullable String clientName) { + String providerName, + String clientName) { return EventDetails.builder() .clientName(clientName) .providerName(providerName) diff --git a/src/main/java/dev/openfeature/sdk/EventSupport.java b/src/main/java/dev/openfeature/sdk/EventSupport.java index f9bb67dd..aa48b8b0 100644 --- a/src/main/java/dev/openfeature/sdk/EventSupport.java +++ b/src/main/java/dev/openfeature/sdk/EventSupport.java @@ -2,7 +2,6 @@ import lombok.extern.slf4j.Slf4j; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -12,6 +11,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** @@ -23,6 +23,7 @@ class EventSupport { // we use a v4 uuid as a "placeholder" for anonymous clients, since // ConcurrentHashMap doesn't support nulls private static final String defaultClientUuid = UUID.randomUUID().toString(); + private static final int SHUTDOWN_TIMEOUT_SECONDS = 3; private final Map handlerStores = new ConcurrentHashMap<>(); private final HandlerStore globalHandlerStore = new HandlerStore(); private final ExecutorService taskExecutor = Executors.newCachedThreadPool(runnable -> { @@ -39,7 +40,7 @@ class EventSupport { * @param event the event type * @param eventDetails the event details */ - public void runClientHandlers(@Nullable String clientName, ProviderEvent event, EventDetails eventDetails) { + public void runClientHandlers(String clientName, ProviderEvent event, EventDetails eventDetails) { clientName = Optional.ofNullable(clientName) .orElse(defaultClientUuid); @@ -72,7 +73,7 @@ public void runGlobalHandlers(ProviderEvent event, EventDetails eventDetails) { * @param event the event type * @param handler the handler function to run */ - public void addClientHandler(@Nullable String clientName, ProviderEvent event, Consumer handler) { + public void addClientHandler(String clientName, ProviderEvent event, Consumer handler) { final String name = Optional.ofNullable(clientName) .orElse(defaultClientUuid); @@ -146,13 +147,19 @@ public void runHandler(Consumer handler, EventDetails eventDetails } /** - * Stop the event handler task executor. + * Stop the event handler task executor and block until either termination has completed + * or timeout period has elapsed. */ public void shutdown() { + taskExecutor.shutdown(); try { - taskExecutor.shutdown(); - } catch (Exception e) { - log.warn("Exception while attempting to shutdown task executor", e); + if (!taskExecutor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + log.warn("Task executor did not terminate before the timeout period had elapsed"); + taskExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + taskExecutor.shutdownNow(); + Thread.currentThread().interrupt(); } } diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java index af48d877..4562ea1e 100644 --- a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java +++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java @@ -2,8 +2,6 @@ import java.util.Optional; -import javax.annotation.Nullable; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -23,12 +21,9 @@ public class FlagEvaluationDetails implements BaseEvaluation { private String flagKey; private T value; - @Nullable private String variant; - @Nullable private String reason; private ErrorCode errorCode; - @Nullable private String errorMessage; @Builder.Default private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build(); diff --git a/src/main/java/dev/openfeature/sdk/MutableContext.java b/src/main/java/dev/openfeature/sdk/MutableContext.java index b63f9b31..7de394f0 100644 --- a/src/main/java/dev/openfeature/sdk/MutableContext.java +++ b/src/main/java/dev/openfeature/sdk/MutableContext.java @@ -87,10 +87,11 @@ public MutableContext add(String key, List value) { /** * Override or set targeting key for this mutable context. Value should be non-null and non-empty to be accepted. */ - public void setTargetingKey(String targetingKey) { + public MutableContext setTargetingKey(String targetingKey) { if (targetingKey != null && !targetingKey.trim().isEmpty()) { this.add(TARGETING_KEY, targetingKey); } + return this; } diff --git a/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java b/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java new file mode 100644 index 00000000..a31b39b4 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java @@ -0,0 +1,24 @@ +package dev.openfeature.sdk; + +/** + * A {@link TransactionContextPropagator} that simply returns empty context. + */ +public class NoOpTransactionContextPropagator implements TransactionContextPropagator { + + /** + * {@inheritDoc} + * @return empty immutable context + */ + @Override + public EvaluationContext getTransactionContext() { + return new ImmutableContext(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setTransactionContext(EvaluationContext evaluationContext) { + + } +} diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index a7ba42b3..db28555a 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -7,8 +7,6 @@ import java.util.Set; import java.util.function.Consumer; -import javax.annotation.Nullable; - import dev.openfeature.sdk.exceptions.OpenFeatureError; import dev.openfeature.sdk.internal.AutoCloseableLock; import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; @@ -27,11 +25,13 @@ public class OpenFeatureAPI implements EventBus { private ProviderRepository providerRepository; private EventSupport eventSupport; private EvaluationContext evaluationContext; + private TransactionContextPropagator transactionContextPropagator; protected OpenFeatureAPI() { apiHooks = new ArrayList<>(); providerRepository = new ProviderRepository(); eventSupport = new EventSupport(); + transactionContextPropagator = new NoOpTransactionContextPropagator(); } private static class SingletonHolder { @@ -65,14 +65,14 @@ public Client getClient() { /** * {@inheritDoc} */ - public Client getClient(@Nullable String name) { + public Client getClient(String name) { return getClient(name, null); } /** * {@inheritDoc} */ - public Client getClient(@Nullable String name, @Nullable String version) { + public Client getClient(String name, String version) { return new OpenFeatureClient(this, name, version); @@ -81,10 +81,11 @@ public Client getClient(@Nullable String name, @Nullable String version) { /** * {@inheritDoc} */ - public void setEvaluationContext(EvaluationContext evaluationContext) { + public OpenFeatureAPI setEvaluationContext(EvaluationContext evaluationContext) { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { this.evaluationContext = evaluationContext; } + return this; } /** @@ -96,6 +97,46 @@ public EvaluationContext getEvaluationContext() { } } + /** + * Return the transaction context propagator. + */ + public TransactionContextPropagator getTransactionContextPropagator() { + try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { + return this.transactionContextPropagator; + } + } + + /** + * Sets the transaction context propagator. + * + * @throws IllegalArgumentException if {@code transactionContextPropagator} is null + */ + public void setTransactionContextPropagator(TransactionContextPropagator transactionContextPropagator) { + if (transactionContextPropagator == null) { + throw new IllegalArgumentException("Transaction context propagator cannot be null"); + } + try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + this.transactionContextPropagator = transactionContextPropagator; + } + } + + /** + * Returns the currently defined transaction context using the registered transaction + * context propagator. + * + * @return {@link EvaluationContext} The current transaction context + */ + EvaluationContext getTransactionContext() { + return this.transactionContextPropagator.getTransactionContext(); + } + + /** + * Sets the transaction context using the registered transaction context propagator. + */ + public void setTransactionContext(EvaluationContext evaluationContext) { + this.transactionContextPropagator.setTransactionContext(evaluationContext); + } + /** * Set the default provider. */ diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index 05d79d02..ce763a34 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -35,7 +35,7 @@ public class OpenFeatureClient implements Client { /** * Deprecated public constructor. Use OpenFeature.API.getClient() instead. - * + * * @param openFeatureAPI Backing global singleton * @param name Name of the client (used by observability tools). * @param version Version of the client (used by observability tools). @@ -43,7 +43,7 @@ public class OpenFeatureClient implements Client { * Clients created using it will not run event handlers. * Use the OpenFeatureAPI's getClient factory method instead. */ - @Deprecated() // TODO: eventually we will make this non-public + @Deprecated() // TODO: eventually we will make this non-public. See issue #872 public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String version) { this.openfeatureApi = openFeatureAPI; this.name = name; @@ -56,10 +56,11 @@ public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String vers * {@inheritDoc} */ @Override - public void addHooks(Hook... hooks) { + public OpenFeatureClient addHooks(Hook... hooks) { try (AutoCloseableLock __ = this.hooksLock.writeLockAutoCloseable()) { this.clientHooks.addAll(Arrays.asList(hooks)); } + return this; } /** @@ -76,10 +77,11 @@ public List getHooks() { * {@inheritDoc} */ @Override - public void setEvaluationContext(EvaluationContext evaluationContext) { + public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContext) { try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) { this.evaluationContext = evaluationContext; } + return this; } /** @@ -105,9 +107,6 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key FeatureProvider provider; try { - final EvaluationContext apiContext; - final EvaluationContext clientContext; - // openfeatureApi.getProvider() must be called once to maintain a consistent reference provider = openfeatureApi.getProvider(this.name); @@ -117,19 +116,9 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key hookCtx = HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), ctx, defaultValue); - // merge of: API.context, client.context, invocation.context - apiContext = openfeatureApi.getEvaluationContext() != null - ? openfeatureApi.getEvaluationContext() - : new ImmutableContext(); - clientContext = this.getEvaluationContext() != null - ? this.getEvaluationContext() - : new ImmutableContext(); - EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints); - EvaluationContext invocationCtx = ctx.merge(ctxFromHook); - - EvaluationContext mergedCtx = apiContext.merge(clientContext.merge(invocationCtx)); + EvaluationContext mergedCtx = mergeEvaluationContext(ctxFromHook, ctx); ProviderEvaluation providerEval = (ProviderEvaluation) createProviderEvaluation(type, key, defaultValue, provider, mergedCtx); @@ -157,6 +146,29 @@ private FlagEvaluationDetails evaluateFlag(FlagValueType type, String key return details; } + /** + * Merge hook and invocation contexts with API, transaction and client contexts. + * + * @param hookContext hook context + * @param invocationContext invocation context + * @return merged evaluation context + */ + private EvaluationContext mergeEvaluationContext( + EvaluationContext hookContext, + EvaluationContext invocationContext) { + final EvaluationContext apiContext = openfeatureApi.getEvaluationContext() != null + ? openfeatureApi.getEvaluationContext() + : new ImmutableContext(); + final EvaluationContext clientContext = this.getEvaluationContext() != null + ? this.getEvaluationContext() + : new ImmutableContext(); + final EvaluationContext transactionContext = openfeatureApi.getTransactionContext() != null + ? openfeatureApi.getTransactionContext() + : new ImmutableContext(); + + return apiContext.merge(transactionContext.merge(clientContext.merge(invocationContext.merge(hookContext)))); + } + private ProviderEvaluation createProviderEvaluation( FlagValueType type, String key, diff --git a/src/main/java/dev/openfeature/sdk/ProviderEvaluation.java b/src/main/java/dev/openfeature/sdk/ProviderEvaluation.java index c4720263..004f5cfd 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderEvaluation.java +++ b/src/main/java/dev/openfeature/sdk/ProviderEvaluation.java @@ -1,7 +1,5 @@ package dev.openfeature.sdk; -import javax.annotation.Nullable; - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -18,10 +16,10 @@ @AllArgsConstructor public class ProviderEvaluation implements BaseEvaluation { T value; - @Nullable String variant; - @Nullable private String reason; + String variant; + private String reason; ErrorCode errorCode; - @Nullable private String errorMessage; + private String errorMessage; @Builder.Default private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build(); } diff --git a/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java b/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java index 149c92a7..d28da9e5 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java +++ b/src/main/java/dev/openfeature/sdk/ProviderEventDetails.java @@ -2,8 +2,6 @@ import java.util.List; -import javax.annotation.Nullable; - import lombok.Data; import lombok.experimental.SuperBuilder; @@ -12,7 +10,7 @@ */ @Data @SuperBuilder(toBuilder = true) public class ProviderEventDetails { - @Nullable private List flagsChanged; - @Nullable private String message; - @Nullable private ImmutableMetadata eventMetadata; + private List flagsChanged; + private String message; + private ImmutableMetadata eventMetadata; } diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java index 8dee0a6f..1ebd9b4c 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java +++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java @@ -13,8 +13,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.annotation.Nullable; - import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.OpenFeatureError; import lombok.extern.slf4j.Slf4j; @@ -100,7 +98,7 @@ public void setProvider(String clientName, prepareAndInitializeProvider(clientName, provider, afterSet, afterInit, afterShutdown, afterError, waitForInit); } - private void prepareAndInitializeProvider(@Nullable String clientName, + private void prepareAndInitializeProvider(String clientName, FeatureProvider newProvider, Consumer afterSet, Consumer afterInit, diff --git a/src/main/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagator.java b/src/main/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagator.java new file mode 100644 index 00000000..59f92ceb --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagator.java @@ -0,0 +1,28 @@ +package dev.openfeature.sdk; + +/** + * A {@link ThreadLocalTransactionContextPropagator} is a transactional context propagator + * that uses a ThreadLocal to persist a transactional context for the duration of a single thread. + * + * @see TransactionContextPropagator + */ +public class ThreadLocalTransactionContextPropagator implements TransactionContextPropagator { + + private final ThreadLocal evaluationContextThreadLocal = new ThreadLocal<>(); + + /** + * {@inheritDoc} + */ + @Override + public EvaluationContext getTransactionContext() { + return this.evaluationContextThreadLocal.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setTransactionContext(EvaluationContext evaluationContext) { + this.evaluationContextThreadLocal.set(evaluationContext); + } +} diff --git a/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java b/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java new file mode 100644 index 00000000..05f7d3eb --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java @@ -0,0 +1,27 @@ +package dev.openfeature.sdk; + +/** + * {@link TransactionContextPropagator} is responsible for persisting a transactional context + * for the duration of a single transaction. + * Examples of potential transaction specific context include: a user id, user agent, IP. + * Transaction context is merged with evaluation context prior to flag evaluation. + *

+ * The precedence of merging context can be seen in + * the specification. + *

+ */ +public interface TransactionContextPropagator { + + /** + * Returns the currently defined transaction context using the registered transaction + * context propagator. + * + * @return {@link EvaluationContext} The current transaction context + */ + EvaluationContext getTransactionContext(); + + /** + * Sets the transaction context. + */ + void setTransactionContext(EvaluationContext evaluationContext); +} diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java index e2e00881..60b6ee13 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -302,41 +302,107 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { @Specification(number="3.2.1.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.") @Specification(number="3.2.2.1", text="The API MUST have a method for setting the global evaluation context.") - @Specification(number="3.2.3", text="Evaluation context MUST be merged in the order: API (global; lowest precedence) - client - invocation - before hooks (highest precedence), with duplicate values being overwritten.") + @Specification(number="3.2.3", text="Evaluation context MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten.") @Test void multi_layer_context_merges_correctly() { DoSomethingProvider provider = new DoSomethingProvider(); FeatureProviderTestUtils.setFeatureProvider(provider); + TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); + api.setTransactionContextPropagator(transactionContextPropagator); - Map attributes = new HashMap<>(); - attributes.put("common", new Value("1")); - attributes.put("common2", new Value("1")); - attributes.put("api", new Value("2")); - EvaluationContext apiCtx = new ImmutableContext(attributes); + Map apiAttributes = new HashMap<>(); + apiAttributes.put("common1", new Value("1")); + apiAttributes.put("common2", new Value("1")); + apiAttributes.put("common3", new Value("1")); + apiAttributes.put("api", new Value("1")); + EvaluationContext apiCtx = new ImmutableContext(apiAttributes); api.setEvaluationContext(apiCtx); + Map transactionAttributes = new HashMap<>(); + // overwrite value from api context + transactionAttributes.put("common1", new Value("2")); + transactionAttributes.put("common4", new Value("2")); + transactionAttributes.put("common5", new Value("2")); + transactionAttributes.put("transaction", new Value("2")); + EvaluationContext transactionCtx = new ImmutableContext(transactionAttributes); + + api.setTransactionContext(transactionCtx); + Client c = api.getClient(); - Map attributes1 = new HashMap<>(); - attributes.put("common", new Value("3")); - attributes.put("common2", new Value("3")); - attributes.put("client", new Value("4")); - attributes.put("common", new Value("5")); - attributes.put("invocation", new Value("6")); - EvaluationContext clientCtx = new ImmutableContext(attributes); + Map clientAttributes = new HashMap<>(); + // overwrite value from api context + clientAttributes.put("common2", new Value("3")); + // overwrite value from transaction context + clientAttributes.put("common4", new Value("3")); + clientAttributes.put("common6", new Value("3")); + clientAttributes.put("client", new Value("3")); + EvaluationContext clientCtx = new ImmutableContext(clientAttributes); c.setEvaluationContext(clientCtx); - EvaluationContext invocationCtx = new ImmutableContext(); + Map invocationAttributes = new HashMap<>(); + // overwrite value from api context + invocationAttributes.put("common3", new Value("4")); + // overwrite value from transaction context + invocationAttributes.put("common5", new Value("4")); + // overwrite value from api client context + invocationAttributes.put("common6", new Value("4")); + invocationAttributes.put("invocation", new Value("4")); + EvaluationContext invocationCtx = new ImmutableContext(invocationAttributes); // dosomethingprovider inverts this value. assertTrue(c.getBooleanValue("key", false, invocationCtx)); EvaluationContext merged = provider.getMergedContext(); - assertEquals("6", merged.getValue("invocation").asString()); - assertEquals("5", merged.getValue("common").asString(), "invocation merge is incorrect"); - assertEquals("4", merged.getValue("client").asString()); + assertEquals("1", merged.getValue("api").asString()); + assertEquals("2", merged.getValue("transaction").asString()); + assertEquals("3", merged.getValue("client").asString()); + assertEquals("4", merged.getValue("invocation").asString()); + assertEquals("2", merged.getValue("common1").asString(), "transaction merge is incorrect"); assertEquals("3", merged.getValue("common2").asString(), "api client merge is incorrect"); - assertEquals("2", merged.getValue("api").asString()); + assertEquals("4", merged.getValue("common3").asString(), "invocation merge is incorrect"); + assertEquals("3", merged.getValue("common4").asString(), "api client merge is incorrect"); + assertEquals("4", merged.getValue("common5").asString(), "invocation merge is incorrect"); + assertEquals("4", merged.getValue("common6").asString(), "invocation merge is incorrect"); + + } + + @Specification(number="3.3.1.1", text="The API SHOULD have a method for setting a transaction context propagator.") + @Test void setting_transaction_context_propagator() { + DoSomethingProvider provider = new DoSomethingProvider(); + FeatureProviderTestUtils.setFeatureProvider(provider); + + TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); + api.setTransactionContextPropagator(transactionContextPropagator); + assertEquals(transactionContextPropagator, api.getTransactionContextPropagator()); + } + + @Specification(number="3.3.1.2.1", text="The API MUST have a method for setting the evaluation context of the transaction context propagator for the current transaction.") + @Test void setting_transaction_context() { + DoSomethingProvider provider = new DoSomethingProvider(); + FeatureProviderTestUtils.setFeatureProvider(provider); + + TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); + api.setTransactionContextPropagator(transactionContextPropagator); + + Map attributes = new HashMap<>(); + attributes.put("common", new Value("1")); + EvaluationContext transactionContext = new ImmutableContext(attributes); + + api.setTransactionContext(transactionContext); + assertEquals(transactionContext, transactionContextPropagator.getTransactionContext()); + } + + @Specification(number="3.3.1.2.2", text="A transaction context propagator MUST have a method for setting the evaluation context of the current transaction.") + @Specification(number="3.3.1.2.3", text="A transaction context propagator MUST have a method for getting the evaluation context of the current transaction.") + @Test void transaction_context_propagator_setting_context() { + TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); + + Map attributes = new HashMap<>(); + attributes.put("common", new Value("1")); + EvaluationContext transactionContext = new ImmutableContext(attributes); + transactionContextPropagator.setTransactionContext(transactionContext); + assertEquals(transactionContext, transactionContextPropagator.getTransactionContext()); } @Specification(number="1.3.4", text="The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.") @@ -355,6 +421,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { @Specification(number="1.3.2.1", text="The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns the flag value.") @Specification(number="3.2.2.2", text="The Client and invocation MUST NOT have a method for supplying evaluation context.") @Specification(number="3.2.4.1", text="When the global evaluation context is set, the on context changed handler MUST run.") + @Specification(number="3.3.2.1", text="The API MUST NOT have a method for setting a transaction context propagator.") @Test void not_applicable_for_dynamic_context() {} } diff --git a/src/test/java/dev/openfeature/sdk/HookSpecTest.java b/src/test/java/dev/openfeature/sdk/HookSpecTest.java index def331db..d0e173b2 100644 --- a/src/test/java/dev/openfeature/sdk/HookSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSpecTest.java @@ -1,5 +1,21 @@ package dev.openfeature.sdk; +import dev.openfeature.sdk.fixtures.HookFixtures; +import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,23 +29,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import dev.openfeature.sdk.testutils.FeatureProviderTestUtils; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.InOrder; - -import dev.openfeature.sdk.fixtures.HookFixtures; -import lombok.SneakyThrows; - class HookSpecTest implements HookFixtures { @AfterEach void emptyApiHooks() { @@ -500,7 +499,7 @@ public void finallyAfter(HookContext ctx, Map hints) { .hook(hook) .build()); - ArgumentCaptor captor = ArgumentCaptor.forClass(MutableContext.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(ImmutableContext.class); verify(provider).getBooleanEvaluation(any(), any(), captor.capture()); EvaluationContext ec = captor.getValue(); assertEquals("works", ec.getValue("test").asString()); diff --git a/src/test/java/dev/openfeature/sdk/LockingTest.java b/src/test/java/dev/openfeature/sdk/LockingTest.java index f58795ad..ddfa9c07 100644 --- a/src/test/java/dev/openfeature/sdk/LockingTest.java +++ b/src/test/java/dev/openfeature/sdk/LockingTest.java @@ -185,6 +185,20 @@ void getContextShouldReadLockAndUnlock() { verify(apiLock.readLock()).unlock(); } + @Test + void setTransactionalContextPropagatorShouldWriteLockAndUnlock() { + api.setTransactionContextPropagator(new NoOpTransactionContextPropagator()); + verify(apiLock.writeLock()).lock(); + verify(apiLock.writeLock()).unlock(); + } + + @Test + void getTransactionalContextPropagatorShouldReadLockAndUnlock() { + api.getTransactionContextPropagator(); + verify(apiLock.readLock()).lock(); + verify(apiLock.readLock()).unlock(); + } + @Test void clearHooksShouldWriteLockAndUnlock() { diff --git a/src/test/java/dev/openfeature/sdk/MutableContextTest.java b/src/test/java/dev/openfeature/sdk/MutableContextTest.java index 1d462b12..df21e6ec 100644 --- a/src/test/java/dev/openfeature/sdk/MutableContextTest.java +++ b/src/test/java/dev/openfeature/sdk/MutableContextTest.java @@ -116,4 +116,19 @@ void mergeShouldRetainItsSubkeysWhenOverridingContextHasNoTargetingKey() { Structure value = key1.asStructure(); assertArrayEquals(new Object[]{"key1_1"}, value.keySet().toArray()); } + + @DisplayName("Ensure mutations are chainable") + @Test + void shouldAllowChainingOfMutations() { + MutableContext context = new MutableContext(); + context.add("key1", "val1") + .add("key2", 2) + .setTargetingKey("TARGETING_KEY") + .add("key3", 3.0); + + assertEquals("TARGETING_KEY", context.getTargetingKey()); + assertEquals("val1", context.getValue("key1").asString()); + assertEquals(2, context.getValue("key2").asInteger()); + assertEquals(3.0, context.getValue("key3").asDouble()); + } } diff --git a/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java b/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java new file mode 100644 index 00000000..06b7e93c --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java @@ -0,0 +1,29 @@ +package dev.openfeature.sdk; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class NoOpTransactionContextPropagatorTest { + + NoOpTransactionContextPropagator contextPropagator = new NoOpTransactionContextPropagator(); + + @Test + public void emptyTransactionContext() { + EvaluationContext result = contextPropagator.getTransactionContext(); + assertTrue(result.asMap().isEmpty()); + } + + @Test + public void setTransactionContext() { + Map transactionAttrs = new HashMap<>(); + transactionAttrs.put("userId", new Value("userId")); + EvaluationContext transactionCtx = new ImmutableContext(transactionAttrs); + contextPropagator.setTransactionContext(transactionCtx); + EvaluationContext result = contextPropagator.getTransactionContext(); + assertTrue(result.asMap().isEmpty()); + } +} \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java index e19a10ae..eceace2b 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Collections; +import java.util.HashMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -73,4 +74,17 @@ void settingDefaultProviderToNullErrors() { void settingNamedClientProviderToNullErrors() { assertThatCode(() -> api.setProvider(CLIENT_NAME, null)).isInstanceOf(IllegalArgumentException.class); } + + @Test + void settingTransactionalContextPropagatorToNullErrors() { + assertThatCode(() -> api.setTransactionContextPropagator(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void setEvaluationContextShouldAllowChaining() { + OpenFeatureClient client = new OpenFeatureClient(api, "name", "version"); + EvaluationContext ctx = new ImmutableContext("targeting key", new HashMap<>()); + OpenFeatureClient result = client.setEvaluationContext(ctx); + assertEquals(client, result); + } } diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java index 9036576d..d6340a84 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java @@ -3,12 +3,18 @@ import java.util.*; import dev.openfeature.sdk.fixtures.HookFixtures; + import org.junit.jupiter.api.*; import org.mockito.Mockito; import org.simplify4u.slf4jmock.LoggerMock; import org.slf4j.Logger; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; class OpenFeatureClientTest implements HookFixtures { @@ -67,4 +73,28 @@ void mergeContextTest() { assertThat(result.getValue()).isTrue(); } + + @Test + @DisplayName("addHooks should allow chaining by returning the same client instance") + void addHooksShouldAllowChaining() { + OpenFeatureAPI api = mock(OpenFeatureAPI.class); + OpenFeatureClient client = new OpenFeatureClient(api, "name", "version"); + Hook hook1 = Mockito.mock(Hook.class); + Hook hook2 = Mockito.mock(Hook.class); + + OpenFeatureClient result = client.addHooks(hook1, hook2); + assertEquals(client, result); + } + + @Test + @DisplayName("setEvaluationContext should allow chaining by returning the same client instance") + void setEvaluationContextShouldAllowChaining() { + OpenFeatureAPI api = mock(OpenFeatureAPI.class); + OpenFeatureClient client = new OpenFeatureClient(api, "name", "version"); + EvaluationContext ctx = new ImmutableContext("targeting key", new HashMap<>()); + + OpenFeatureClient result = client.setEvaluationContext(ctx); + assertEquals(client, result); + } + } diff --git a/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java b/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java new file mode 100644 index 00000000..531205c1 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java @@ -0,0 +1,57 @@ +package dev.openfeature.sdk; + +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; + +import static org.junit.jupiter.api.Assertions.*; + +public class ThreadLocalTransactionContextPropagatorTest { + + ThreadLocalTransactionContextPropagator contextPropagator = new ThreadLocalTransactionContextPropagator(); + + @Test + public void setTransactionContextOneThread() { + EvaluationContext firstContext = new ImmutableContext(); + contextPropagator.setTransactionContext(firstContext); + assertSame(firstContext, contextPropagator.getTransactionContext()); + EvaluationContext secondContext = new ImmutableContext(); + contextPropagator.setTransactionContext(secondContext); + assertNotSame(firstContext, contextPropagator.getTransactionContext()); + assertSame(secondContext, contextPropagator.getTransactionContext()); + } + + @Test + public void emptyTransactionContext() { + EvaluationContext result = contextPropagator.getTransactionContext(); + assertNull(result); + } + + @SneakyThrows + @Test + public void setTransactionContextTwoThreads() { + EvaluationContext firstContext = new ImmutableContext(); + EvaluationContext secondContext = new ImmutableContext(); + + Callable callable = () -> { + assertNull(contextPropagator.getTransactionContext()); + contextPropagator.setTransactionContext(secondContext); + EvaluationContext transactionContext = contextPropagator.getTransactionContext(); + assertSame(secondContext, transactionContext); + return transactionContext; + }; + contextPropagator.setTransactionContext(firstContext); + EvaluationContext firstThreadContext = contextPropagator.getTransactionContext(); + assertSame(firstContext, firstThreadContext); + + FutureTask futureTask = new FutureTask<>(callable); + Thread thread = new Thread(futureTask); + thread.start(); + EvaluationContext secondThreadContext = futureTask.get(); + + assertSame(secondContext, secondThreadContext); + assertSame(firstContext, contextPropagator.getTransactionContext()); + } +} \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/fixtures/HookFixtures.java b/src/test/java/dev/openfeature/sdk/fixtures/HookFixtures.java index 1d1de1ef..9886c383 100644 --- a/src/test/java/dev/openfeature/sdk/fixtures/HookFixtures.java +++ b/src/test/java/dev/openfeature/sdk/fixtures/HookFixtures.java @@ -1,6 +1,10 @@ package dev.openfeature.sdk.fixtures; -import dev.openfeature.sdk.*; +import dev.openfeature.sdk.BooleanHook; +import dev.openfeature.sdk.DoubleHook; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.IntegerHook; +import dev.openfeature.sdk.StringHook; import static org.mockito.Mockito.spy; diff --git a/version.txt b/version.txt index de28578a..27f9cd32 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.7.6 +1.8.0