diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 8f885bd8..df591de7 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@00282d63cda40a6eaf3e9d0cbb1ac4384de896e8 + - uses: amannn/action-semantic-pull-request@47b15d52c5c30e94a17ec87eb8dd51ff5221fed9 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 84bceb8b..3b7477a5 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -20,9 +20,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@72f2cec99f417b1a1c5e2e88945068983b7965f9 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Set up JDK 8 - uses: actions/setup-java@4075bfc1b51bf22876335ae1cd589602d60d8758 + uses: actions/setup-java@78078da0cd035d0d177cc2cb696e05d96fba7d11 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@398b9de041a7e69750d45077b10c5912201a3466 + uses: codecov/codecov-action@v3.1.4 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 8b018bdb..1693197f 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@72f2cec99f417b1a1c5e2e88945068983b7965f9 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Set up JDK 8 - uses: actions/setup-java@4075bfc1b51bf22876335ae1cd589602d60d8758 + uses: actions/setup-java@78078da0cd035d0d177cc2cb696e05d96fba7d11 with: java-version: '8' distribution: 'temurin' cache: maven - name: Initialize CodeQL - uses: github/codeql-action/init@43750fe4fc4f068f04f2215206e6f6a29c78c763 + uses: github/codeql-action/init@9734ecd5b5d89514b0ce942a73586adcbe42b6ee 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@398b9de041a7e69750d45077b10c5912201a3466 + uses: codecov/codecov-action@v3.1.4 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@43750fe4fc4f068f04f2215206e6f6a29c78c763 + uses: github/codeql-action/analyze@9734ecd5b5d89514b0ce942a73586adcbe42b6ee diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90f5d59c..b15ee568 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: # Release-please creates a PR that tracks all changes steps: - - uses: google-github-actions/release-please-action@01b3219546e6e7c4cfdaece8cd06efa464f49e2a + - uses: google-github-actions/release-please-action@2921787898ea2925c9eec03a32aa7404a75399e5 id: release with: command: manifest @@ -29,10 +29,10 @@ jobs: # These steps are only run if this was a merged release-please PR - name: checkout if: ${{ steps.release.outputs.releases_created }} - uses: actions/checkout@72f2cec99f417b1a1c5e2e88945068983b7965f9 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Set up JDK 8 if: ${{ steps.release.outputs.releases_created }} - uses: actions/setup-java@4075bfc1b51bf22876335ae1cd589602d60d8758 + uses: actions/setup-java@78078da0cd035d0d177cc2cb696e05d96fba7d11 with: java-version: '8' distribution: 'temurin' diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml index 1190722f..5e9f9d8f 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@72f2cec99f417b1a1c5e2e88945068983b7965f9 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@43750fe4fc4f068f04f2215206e6f6a29c78c763 + uses: github/codeql-action/init@9734ecd5b5d89514b0ce942a73586adcbe42b6ee with: languages: java - name: Autobuild - uses: github/codeql-action/autobuild@43750fe4fc4f068f04f2215206e6f6a29c78c763 + uses: github/codeql-action/autobuild@9734ecd5b5d89514b0ce942a73586adcbe42b6ee - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@43750fe4fc4f068f04f2215206e6f6a29c78c763 + uses: github/codeql-action/analyze@9734ecd5b5d89514b0ce942a73586adcbe42b6ee diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f1ef058b..0e757992 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"1.6.1"} \ No newline at end of file +{".":"1.7.0"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f1571c6a..903cf395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,87 @@ # Changelog +## [1.7.0](https://github.com/open-feature/java-sdk/compare/v1.6.1...v1.7.0) (2023-10-24) + + +### ๐Ÿ› Bug Fixes + +* **deps:** update dependency org.projectlombok:lombok to v1.18.30 ([#625](https://github.com/open-feature/java-sdk/issues/625)) ([23bb497](https://github.com/open-feature/java-sdk/commit/23bb4974aa7245972a31a0146701a37d3fd7e1b4)) +* null handling with Structure, Value ([#663](https://github.com/open-feature/java-sdk/issues/663)) ([3ab330a](https://github.com/open-feature/java-sdk/commit/3ab330a7595aab120007fba5c719ea6e6afac937)) +* RejectedExecutionException on shutdown ([#652](https://github.com/open-feature/java-sdk/issues/652)) ([8c595b0](https://github.com/open-feature/java-sdk/commit/8c595b0227f6e186d31f8104cbbb125c8b06e2a4)) + + +### โœจ New Features + +* implement spec 0.7.0 changes ([#655](https://github.com/open-feature/java-sdk/issues/655)) ([fe5a20f](https://github.com/open-feature/java-sdk/commit/fe5a20f2c39c776a68f5533ee950b96adf548231)) + * run any event handler immediately if the provider is in the associated state, not just ready + * add providerName to event details + * add STALE provider state + + +### ๐Ÿงน Chore + +* **deps:** update actions/checkout digest to 7cdaf2f ([#646](https://github.com/open-feature/java-sdk/issues/646)) ([8eff8d9](https://github.com/open-feature/java-sdk/commit/8eff8d9effe9471e7a98affdca23f78681f0fdcc)) +* **deps:** update actions/checkout digest to 8530928 ([#653](https://github.com/open-feature/java-sdk/issues/653)) ([b9312cf](https://github.com/open-feature/java-sdk/commit/b9312cfd43cfbbfce0e47ea9e41c471421f1f107)) +* **deps:** update actions/checkout digest to 8ade135 ([#628](https://github.com/open-feature/java-sdk/issues/628)) ([f819ead](https://github.com/open-feature/java-sdk/commit/f819ead57704e77643ea9861b8fdea002e1e4e55)) +* **deps:** update actions/checkout digest to b4ffde6 ([#658](https://github.com/open-feature/java-sdk/issues/658)) ([a0190b4](https://github.com/open-feature/java-sdk/commit/a0190b4311527dcd525925e6b69b112d384c65f0)) +* **deps:** update actions/setup-java digest to 0ab4596 ([#626](https://github.com/open-feature/java-sdk/issues/626)) ([059572a](https://github.com/open-feature/java-sdk/commit/059572a3d86291ff5c16aef7e841c4f60049ea19)) +* **deps:** update actions/setup-java digest to 78078da ([#657](https://github.com/open-feature/java-sdk/issues/657)) ([ed9ffb3](https://github.com/open-feature/java-sdk/commit/ed9ffb307b09f9da276c098a9064aea5f7dc38fd)) +* **deps:** update amannn/action-semantic-pull-request digest to 47b15d5 ([#631](https://github.com/open-feature/java-sdk/issues/631)) ([f86899b](https://github.com/open-feature/java-sdk/commit/f86899b10886765d9844bbda69da63ea6e91ccf8)) +* **deps:** update codecov/codecov-action digest to b65fbdc ([#614](https://github.com/open-feature/java-sdk/issues/614)) ([f0734f7](https://github.com/open-feature/java-sdk/commit/f0734f7c91371567bc3c2db059e7beab7cb50641)) +* **deps:** update codecov/codecov-action digest to c4cf8a4 ([#611](https://github.com/open-feature/java-sdk/issues/611)) ([c05609a](https://github.com/open-feature/java-sdk/commit/c05609ae4c6554d0795869990bb8414cabfcd7e8)) +* **deps:** update codecov/codecov-action digest to c9e0f0b ([#608](https://github.com/open-feature/java-sdk/issues/608)) ([29efc6c](https://github.com/open-feature/java-sdk/commit/29efc6c62f43fabc0b3f90aa190eaff6475fd6aa)) +* **deps:** update codecov/codecov-action digest to c9e4b73 ([#609](https://github.com/open-feature/java-sdk/issues/609)) ([af55f21](https://github.com/open-feature/java-sdk/commit/af55f216876191b95ddbd7b268ec92b0625d7c93)) +* **deps:** update dependency com.github.spotbugs:spotbugs-maven-plugin to v4.7.3.6 ([#630](https://github.com/open-feature/java-sdk/issues/630)) ([d5a9867](https://github.com/open-feature/java-sdk/commit/d5a9867365d62bda51b87ff1d13e4f4daaee87cd)) +* **deps:** update dependency com.google.guava:guava to v32.1.3-jre ([#648](https://github.com/open-feature/java-sdk/issues/648)) ([8b5d8a5](https://github.com/open-feature/java-sdk/commit/8b5d8a5f319f26734465d25e2c9b4689b4390eb3)) +* **deps:** update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.3.1 ([#668](https://github.com/open-feature/java-sdk/issues/668)) ([75ff31e](https://github.com/open-feature/java-sdk/commit/75ff31e354824d9f3e2c9f808ae56333a62bdf23)) +* **deps:** update dependency org.apache.maven.plugins:maven-dependency-plugin to v3.6.1 ([#665](https://github.com/open-feature/java-sdk/issues/665)) ([2554026](https://github.com/open-feature/java-sdk/commit/2554026e5816f4a10183fed3b59b9a83bc9a8f25)) +* **deps:** update dependency org.apache.maven.plugins:maven-failsafe-plugin to v3.2.1 ([#666](https://github.com/open-feature/java-sdk/issues/666)) ([72f168b](https://github.com/open-feature/java-sdk/commit/72f168b97c47f8662c7ffb40c24f1f6def8cba2c)) +* **deps:** update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.6.0 ([#620](https://github.com/open-feature/java-sdk/issues/620)) ([59c1c27](https://github.com/open-feature/java-sdk/commit/59c1c277aa81d879ebeb00d061bf2f65bd774f8c)) +* **deps:** update dependency org.apache.maven.plugins:maven-surefire-plugin to v3.2.1 ([#667](https://github.com/open-feature/java-sdk/issues/667)) ([9dc2cb4](https://github.com/open-feature/java-sdk/commit/9dc2cb4e05163d8658b29526c7ee38ec87df439e)) +* **deps:** update github/codeql-action digest to 01b8760 ([#623](https://github.com/open-feature/java-sdk/issues/623)) ([210a9dc](https://github.com/open-feature/java-sdk/commit/210a9dcc06b62af77925678f6f2357fbc96d6c36)) +* **deps:** update github/codeql-action digest to 0d5c2e0 ([#643](https://github.com/open-feature/java-sdk/issues/643)) ([5737439](https://github.com/open-feature/java-sdk/commit/5737439269421671c5ef60efe5e01b56f2db71a3)) +* **deps:** update github/codeql-action digest to 27cb1e1 ([#633](https://github.com/open-feature/java-sdk/issues/633)) ([ab18516](https://github.com/open-feature/java-sdk/commit/ab185168bb36f07c545c671735e1bf416d49c040)) +* **deps:** update github/codeql-action digest to 2cc1651 ([#634](https://github.com/open-feature/java-sdk/issues/634)) ([a671984](https://github.com/open-feature/java-sdk/commit/a671984dbd5933e01e0db56f8661ae0bcd50f899)) +* **deps:** update github/codeql-action digest to 3078f51 ([#629](https://github.com/open-feature/java-sdk/issues/629)) ([ead77c1](https://github.com/open-feature/java-sdk/commit/ead77c113c64d5cb6f6d7f61d4e459cc0e8e3d5f)) +* **deps:** update github/codeql-action digest to 3dd4ad8 ([#641](https://github.com/open-feature/java-sdk/issues/641)) ([15dae81](https://github.com/open-feature/java-sdk/commit/15dae81665a564ea72d1bce492e9e61112b5101f)) +* **deps:** update github/codeql-action digest to 3f7850a ([#642](https://github.com/open-feature/java-sdk/issues/642)) ([c1dec9e](https://github.com/open-feature/java-sdk/commit/c1dec9e6c55262d60cc2303e8790e7ed4d56aee7)) +* **deps:** update github/codeql-action digest to 4254f3a ([#621](https://github.com/open-feature/java-sdk/issues/621)) ([7e100de](https://github.com/open-feature/java-sdk/commit/7e100de6d67b64a4b39246d02f2826ba824072b0)) +* **deps:** update github/codeql-action digest to 49aaa9a ([#627](https://github.com/open-feature/java-sdk/issues/627)) ([59a792f](https://github.com/open-feature/java-sdk/commit/59a792f836a01ac5ca7dd9792307c7a44366ed0a)) +* **deps:** update github/codeql-action digest to 4a368f6 ([#660](https://github.com/open-feature/java-sdk/issues/660)) ([c625721](https://github.com/open-feature/java-sdk/commit/c625721132da03e2b79836d47861eb40da3f6b71)) +* **deps:** update github/codeql-action digest to 4ab9237 ([#644](https://github.com/open-feature/java-sdk/issues/644)) ([ed415a7](https://github.com/open-feature/java-sdk/commit/ed415a7fcf9d2f97986488af4f7e4f7bf79ce53b)) +* **deps:** update github/codeql-action digest to 5f18c9a ([#617](https://github.com/open-feature/java-sdk/issues/617)) ([f887fe8](https://github.com/open-feature/java-sdk/commit/f887fe8656a20b4fcf1931cca79887234e3b00f9)) +* **deps:** update github/codeql-action digest to 6347027 ([#661](https://github.com/open-feature/java-sdk/issues/661)) ([799a5ba](https://github.com/open-feature/java-sdk/commit/799a5bae5bb250e46febcb32abd20eecfa7c9124)) +* **deps:** update github/codeql-action digest to 650a85e ([#632](https://github.com/open-feature/java-sdk/issues/632)) ([ac78477](https://github.com/open-feature/java-sdk/commit/ac784779ceb8cba8f4fc9be212b3d5eb6328ccce)) +* **deps:** update github/codeql-action digest to 6a6a824 ([#607](https://github.com/open-feature/java-sdk/issues/607)) ([fd774e3](https://github.com/open-feature/java-sdk/commit/fd774e35476f696318d630ca101756bc8a0e3dfd)) +* **deps:** update github/codeql-action digest to 77bbb99 ([#659](https://github.com/open-feature/java-sdk/issues/659)) ([7bd35c1](https://github.com/open-feature/java-sdk/commit/7bd35c10694d9aaffe7c70a8ae72490097ee0e1b)) +* **deps:** update github/codeql-action digest to 82ba90b ([#651](https://github.com/open-feature/java-sdk/issues/651)) ([39a2c18](https://github.com/open-feature/java-sdk/commit/39a2c18b2ed9ae2f05ecdc0d5ad312cd4689621e)) +* **deps:** update github/codeql-action digest to 8a2cbab ([#649](https://github.com/open-feature/java-sdk/issues/649)) ([b77e44e](https://github.com/open-feature/java-sdk/commit/b77e44e98377e725c2bbc9e75896de77531a3573)) +* **deps:** update github/codeql-action digest to 8e0b1c7 ([#624](https://github.com/open-feature/java-sdk/issues/624)) ([9684687](https://github.com/open-feature/java-sdk/commit/9684687c2d3268659db987c0a1f86710436df3f7)) +* **deps:** update github/codeql-action digest to 8efd40b ([#612](https://github.com/open-feature/java-sdk/issues/612)) ([27fbe45](https://github.com/open-feature/java-sdk/commit/27fbe4521c1c2bd7055aa273aa18b8c416f129e9)) +* **deps:** update github/codeql-action digest to 90f8ed1 ([#638](https://github.com/open-feature/java-sdk/issues/638)) ([5d35c9d](https://github.com/open-feature/java-sdk/commit/5d35c9dcf278874856e66dae160050b1d53082ed)) +* **deps:** update github/codeql-action digest to 9734ecd ([#664](https://github.com/open-feature/java-sdk/issues/664)) ([f838369](https://github.com/open-feature/java-sdk/commit/f838369217137f6132d02fa9b11f52630876197a)) +* **deps:** update github/codeql-action digest to a291b7c ([#662](https://github.com/open-feature/java-sdk/issues/662)) ([fb4d369](https://github.com/open-feature/java-sdk/commit/fb4d369615d89cb04b9a20c15048d09439356b89)) +* **deps:** update github/codeql-action digest to a370ce3 ([#637](https://github.com/open-feature/java-sdk/issues/637)) ([899191a](https://github.com/open-feature/java-sdk/commit/899191a1712b6f07e02fc9882208419d798a071f)) +* **deps:** update github/codeql-action digest to a67b110 ([#645](https://github.com/open-feature/java-sdk/issues/645)) ([3a1d138](https://github.com/open-feature/java-sdk/commit/3a1d13842e7ee5e8ae67967b418da063a78d9852)) +* **deps:** update github/codeql-action digest to b686e07 ([#640](https://github.com/open-feature/java-sdk/issues/640)) ([e99f7c4](https://github.com/open-feature/java-sdk/commit/e99f7c44f7413ecc7faab94730539cb670d565ef)) +* **deps:** update github/codeql-action digest to c459726 ([#619](https://github.com/open-feature/java-sdk/issues/619)) ([ee8a411](https://github.com/open-feature/java-sdk/commit/ee8a411026324fb9e0f88520ef6e61164f73052a)) +* **deps:** update github/codeql-action digest to c6c77c8 ([#613](https://github.com/open-feature/java-sdk/issues/613)) ([7a7c7e8](https://github.com/open-feature/java-sdk/commit/7a7c7e868f16f453548bf35dbd6fbc40fb316a3c)) +* **deps:** update github/codeql-action digest to d859d17 ([#654](https://github.com/open-feature/java-sdk/issues/654)) ([1a7f7de](https://github.com/open-feature/java-sdk/commit/1a7f7de6e4ab0971acd574c91ac196960df37391)) +* **deps:** update github/codeql-action digest to dd1128f ([#622](https://github.com/open-feature/java-sdk/issues/622)) ([a0342b7](https://github.com/open-feature/java-sdk/commit/a0342b7e203d62fa3e66d3ac25a40a148181fd36)) +* **deps:** update github/codeql-action digest to e7a6fa9 ([#635](https://github.com/open-feature/java-sdk/issues/635)) ([b6d4e7e](https://github.com/open-feature/java-sdk/commit/b6d4e7eec03511119fd0fd81b588ce00bcad4a8f)) +* **deps:** update github/codeql-action digest to e982de4 ([#615](https://github.com/open-feature/java-sdk/issues/615)) ([41e9ebb](https://github.com/open-feature/java-sdk/commit/41e9ebb8181f7022fac078f9557233a7c55086f8)) +* **deps:** update github/codeql-action digest to f3051ed ([#618](https://github.com/open-feature/java-sdk/issues/618)) ([514d463](https://github.com/open-feature/java-sdk/commit/514d4632457ef705f3adf451b11d4799e4ed15df)) +* **deps:** update google-github-actions/release-please-action digest to 2921787 ([#639](https://github.com/open-feature/java-sdk/issues/639)) ([c2da0b1](https://github.com/open-feature/java-sdk/commit/c2da0b16310b1135bbb443af694a112ce51718c3)) +* **deps:** update google-github-actions/release-please-action digest to 48f9873 ([#605](https://github.com/open-feature/java-sdk/issues/605)) ([692c368](https://github.com/open-feature/java-sdk/commit/692c368d74f51933a1af051d88216f62d7bf93ff)) +* **deps:** update google-github-actions/release-please-action digest to 4c5670f ([#636](https://github.com/open-feature/java-sdk/issues/636)) ([1fb173d](https://github.com/open-feature/java-sdk/commit/1fb173db669f587a7960a4fc2b9a3fada94d1dc4)) +* disable action pinning, revert codecov ([#616](https://github.com/open-feature/java-sdk/issues/616)) ([bdddeb1](https://github.com/open-feature/java-sdk/commit/bdddeb19b23e82b366d113fed4c24b7f8559600b)) + + +### ๐Ÿ“š Documentation + +* document setProviderAndWait in README ([#610](https://github.com/open-feature/java-sdk/issues/610)) ([818131b](https://github.com/open-feature/java-sdk/commit/818131b77e48f985bc9e115085f49f228891393b)) +* Update README.md ([#604](https://github.com/open-feature/java-sdk/issues/604)) ([6fd752d](https://github.com/open-feature/java-sdk/commit/6fd752d59d303bac06953bdf3b82e59054166e71)) + ## [1.6.1](https://github.com/open-feature/java-sdk/compare/v1.6.0...v1.6.1) (2023-09-09) diff --git a/README.md b/README.md index ca9626f6..8d05c903 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@

- - Specification + + Specification - - Release + + Release @@ -39,7 +39,7 @@

-[OpenFeature](https://openfeature.dev) is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. +[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. ## ๐Ÿš€ Quick start @@ -59,7 +59,7 @@ Note that this library is intended to be used in server-side contexts and has no dev.openfeature sdk - 1.6.1 + 1.7.0 ``` @@ -84,7 +84,7 @@ If you would like snapshot builds, this is the relevant repository information: ```groovy dependencies { - implementation 'dev.openfeature:sdk:1.6.1' + implementation 'dev.openfeature:sdk:1.7.0' } ``` @@ -104,7 +104,7 @@ public void example(){ // configure a provider OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(new InMemoryProvider(myFlags)); + api.setProviderAndWait(new InMemoryProvider(myFlags)); // create a client Client client = api.getClient(); @@ -140,10 +140,23 @@ Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. Once you've added a provider as a dependency, it can be registered with OpenFeature like this: + +#### Synchronous + +To register a provider in a blocking manner to ensure it is ready before further actions are taken, you can use the `setProviderAndWait` method as shown below: + +```java + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + api.setProviderAndWait(new MyProvider()); +``` + +#### Asynchronous + +To register a provider in a non-blocking manner, you can use the `setProvider` method as shown below: ```java OpenFeatureAPI.getInstance().setProvider(new MyProvider()); -``` +``` In some situations, it may be beneficial to register multiple providers in the same application. This is possible using [named clients](#named-clients), which is covered in more details below. @@ -209,7 +222,7 @@ The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) f Clients can be given a name. A name is a logical identifier which can be used to associate clients with a particular provider. -If a name has no associated provider, the global provider is used. +If a name has no associated provider, the global provider is used. ```java FeatureProvider scopedProvider = new MyProvider(); @@ -225,6 +238,9 @@ Client clientDefault = OpenFeatureAPI.getInstance().getClient(); Client clientNamed = OpenFeatureAPI.getInstance().getClient("clientForCache"); ``` +Named providers can be set in a blocking or non-blocking way. +For more details, please refer to the [providers](#providers) section. + ### Eventing Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. diff --git a/pom.xml b/pom.xml index 5d9800dd..cd0a4942 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ dev.openfeature sdk - 1.6.1 + 1.7.0 UTF-8 @@ -45,7 +45,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 provided @@ -53,7 +53,7 @@ com.github.spotbugs spotbugs - 4.7.3 + 4.8.0 provided @@ -135,7 +135,7 @@ com.google.guava guava - 32.1.2-jre + 32.1.3-jre test @@ -200,7 +200,7 @@ maven-dependency-plugin - 3.6.0 + 3.6.1 verify @@ -235,7 +235,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.1.2 + 3.2.1 ${surefireArgLine} @@ -250,7 +250,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.1.2 + 3.2.1 ${surefireArgLine} @@ -349,7 +349,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.7.3.5 + 4.7.3.6 spotbugs-exclusions.xml @@ -365,7 +365,7 @@ com.github.spotbugs spotbugs - 4.7.3 + 4.8.0 @@ -382,7 +382,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.3.0 + 3.3.1 checkstyle.xml UTF-8 @@ -452,7 +452,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + 3.6.0 true all,-missing diff --git a/renovate.json b/renovate.json index 62574107..d3b4a0c6 100644 --- a/renovate.json +++ b/renovate.json @@ -1,8 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base", - "helpers:pinGitHubActionDigests" + "config:base" ], "packageRules": [ { diff --git a/spotbugs-exclusions.xml b/spotbugs-exclusions.xml index 8105db97..66032ad0 100644 --- a/spotbugs-exclusions.xml +++ b/spotbugs-exclusions.xml @@ -26,6 +26,26 @@ + + Added in spotbugs 4.8.0 - EventProvider shares a name with something from the standard lib (confusing), but change would be breaking + + + + + Added in spotbugs 4.8.0 - Metadata shares a name with something from the standard lib (confusing), but change would be breaking + + + + + Added in spotbugs 4.8.0 - Reason shares a name with something from the standard lib (confusing), but change would be breaking + + + + + Added in spotbugs 4.8.0 - FlagValueType.STRING shares a name with something from the standard lib (confusing), but change would be breaking + + + diff --git a/src/main/java/dev/openfeature/sdk/AbstractStructure.java b/src/main/java/dev/openfeature/sdk/AbstractStructure.java new file mode 100644 index 00000000..a7d7c2ea --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/AbstractStructure.java @@ -0,0 +1,35 @@ +package dev.openfeature.sdk; + +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings({ "PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType" }) +abstract class AbstractStructure implements Structure { + + protected final Map attributes; + + AbstractStructure() { + this.attributes = new HashMap<>(); + } + + AbstractStructure(Map attributes) { + this.attributes = attributes; + } + + /** + * Get all values as their underlying primitives types. + * + * @return all attributes on the structure into a Map + */ + @Override + public Map asObjectMap() { + return attributes + .entrySet() + .stream() + // custom collector, workaround for Collectors.toMap in JDK8 + // https://bugs.openjdk.org/browse/JDK-8148463 + .collect(HashMap::new, + (accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())), + HashMap::putAll); + } +} diff --git a/src/main/java/dev/openfeature/sdk/EventDetails.java b/src/main/java/dev/openfeature/sdk/EventDetails.java index 3f6db159..d4ecac93 100644 --- a/src/main/java/dev/openfeature/sdk/EventDetails.java +++ b/src/main/java/dev/openfeature/sdk/EventDetails.java @@ -7,19 +7,23 @@ /** * The details of a particular event. */ -@Data @SuperBuilder(toBuilder = true) +@Data +@SuperBuilder(toBuilder = true) public class EventDetails extends ProviderEventDetails { private String clientName; + private String providerName; - static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails) { - return EventDetails.fromProviderEventDetails(providerEventDetails, null); + static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails, String providerName) { + return EventDetails.fromProviderEventDetails(providerEventDetails, providerName, null); } static EventDetails fromProviderEventDetails( ProviderEventDetails providerEventDetails, + @Nullable String providerName, @Nullable String clientName) { return EventDetails.builder() .clientName(clientName) + .providerName(providerName) .flagsChanged(providerEventDetails.getFlagsChanged()) .eventMetadata(providerEventDetails.getEventMetadata()) .message(providerEventDetails.getMessage()) diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java index b324c07c..af48d877 100644 --- a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java +++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java @@ -1,5 +1,7 @@ package dev.openfeature.sdk; +import java.util.Optional; + import javax.annotation.Nullable; import lombok.AllArgsConstructor; @@ -8,7 +10,8 @@ import lombok.NoArgsConstructor; /** - * Contains information about how the provider resolved a flag, including the resolved value. + * Contains information about how the provider resolved a flag, including the + * resolved value. * * @param the type of the flag being evaluated. */ @@ -20,11 +23,15 @@ public class FlagEvaluationDetails implements BaseEvaluation { private String flagKey; private T value; - @Nullable private String variant; - @Nullable private String reason; + @Nullable + private String variant; + @Nullable + private String reason; private ErrorCode errorCode; - @Nullable private String errorMessage; - @Builder.Default private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build(); + @Nullable + private String errorMessage; + @Builder.Default + private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build(); /** * Generate detail payload from the provider response. @@ -42,7 +49,8 @@ public static FlagEvaluationDetails from(ProviderEvaluation providerEv .reason(providerEval.getReason()) .errorMessage(providerEval.getErrorMessage()) .errorCode(providerEval.getErrorCode()) - .flagMetadata(providerEval.getFlagMetadata()) + .flagMetadata( + Optional.ofNullable(providerEval.getFlagMetadata()).orElse(ImmutableMetadata.builder().build())) .build(); } } diff --git a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java index 731a55b1..7ea1ef65 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java @@ -1,32 +1,32 @@ package dev.openfeature.sdk; -import lombok.EqualsAndHashCode; -import lombok.ToString; - import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.ToString; /** - * {@link ImmutableStructure} represents a potentially nested object type which is used to represent + * {@link ImmutableStructure} represents a potentially nested object type which + * is used to represent * structured data. - * The ImmutableStructure is a Structure implementation which is threadsafe, and whose attributes can - * not be modified after instantiation. + * The ImmutableStructure is a Structure implementation which is threadsafe, and + * whose attributes can + * not be modified after instantiation. All references are clones. */ @ToString @EqualsAndHashCode -@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"}) -public final class ImmutableStructure implements Structure { - - private final Map attributes; +@SuppressWarnings({ "PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType" }) +public final class ImmutableStructure extends AbstractStructure { /** * create an immutable structure with the empty attributes. */ public ImmutableStructure() { - this(new HashMap<>()); + super(); } /** @@ -35,10 +35,14 @@ public ImmutableStructure() { * @param attributes attributes. */ public ImmutableStructure(Map attributes) { - Map copy = attributes.entrySet() + super(new HashMap<>(attributes.entrySet() .stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().clone())); - this.attributes = new HashMap<>(copy); + .collect(HashMap::new, + (accumulated, entry) -> accumulated.put(entry.getKey(), + Optional.ofNullable(entry.getValue()) + .map(e -> e.clone()) + .orElse(null)), + HashMap::putAll))); } @Override @@ -63,25 +67,11 @@ public Map asMap() { return attributes .entrySet() .stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> getValue(e.getKey()) - )); - } - - /** - * Get all values, with primitives types. - * - * @return all attributes on the structure into a Map - */ - @Override - public Map asObjectMap() { - return attributes - .entrySet() - .stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> convertValue(getValue(e.getKey())) - )); + .collect(HashMap::new, + (accumulated, entry) -> accumulated.put(entry.getKey(), + Optional.ofNullable(entry.getValue()) + .map(e -> e.clone()) + .orElse(null)), + HashMap::putAll); } } diff --git a/src/main/java/dev/openfeature/sdk/MutableStructure.java b/src/main/java/dev/openfeature/sdk/MutableStructure.java index 343fe2c9..3c4f34dd 100644 --- a/src/main/java/dev/openfeature/sdk/MutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/MutableStructure.java @@ -1,14 +1,13 @@ package dev.openfeature.sdk; -import lombok.EqualsAndHashCode; -import lombok.ToString; - import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.ToString; /** * {@link MutableStructure} represents a potentially nested object type which is used to represent @@ -19,16 +18,14 @@ @ToString @EqualsAndHashCode @SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"}) -public class MutableStructure implements Structure { - - protected final Map attributes; +public class MutableStructure extends AbstractStructure { public MutableStructure() { - this.attributes = new HashMap<>(); + super(); } public MutableStructure(Map attributes) { - this.attributes = new HashMap<>(attributes); + super(attributes); } @Override @@ -92,20 +89,4 @@ public MutableStructure add(String key, List value) { public Map asMap() { return new HashMap<>(this.attributes); } - - /** - * Get all values, with primitives types. - * - * @return all attributes on the structure into a Map - */ - @Override - public Map asObjectMap() { - return attributes - .entrySet() - .stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> convertValue(getValue(e.getKey())) - )); - } } diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index 47c09388..7448ad78 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -100,12 +101,12 @@ public EvaluationContext getEvaluationContext() { public void setProvider(FeatureProvider provider) { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { providerRepository.setProvider( - provider, + provider, this::attachEventProvider, this::emitReady, this::detachEventProvider, this::emitError, - false); + false); } } @@ -118,12 +119,12 @@ public void setProvider(FeatureProvider provider) { public void setProvider(String clientName, FeatureProvider provider) { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { providerRepository.setProvider(clientName, - provider, - this::attachEventProvider, - this::emitReady, - this::detachEventProvider, - this::emitError, - false); + provider, + this::attachEventProvider, + this::emitReady, + this::detachEventProvider, + this::emitError, + false); } } @@ -133,12 +134,12 @@ public void setProvider(String clientName, FeatureProvider provider) { public void setProviderAndWait(FeatureProvider provider) { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { providerRepository.setProvider( - provider, - this::attachEventProvider, - this::emitReady, - this::detachEventProvider, - this::emitError, - true); + provider, + this::attachEventProvider, + this::emitReady, + this::detachEventProvider, + this::emitError, + true); } } @@ -151,18 +152,18 @@ public void setProviderAndWait(FeatureProvider provider) { public void setProviderAndWait(String clientName, FeatureProvider provider) { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { providerRepository.setProvider(clientName, - provider, - this::attachEventProvider, - this::emitReady, - this::detachEventProvider, - this::emitError, - true); + provider, + this::attachEventProvider, + this::emitReady, + this::detachEventProvider, + this::emitError, + true); } } private void attachEventProvider(FeatureProvider provider) { if (provider instanceof EventProvider) { - ((EventProvider)provider).attach((p, event, details) -> { + ((EventProvider) provider).attach((p, event, details) -> { runHandlersForProvider(p, event, details); }); } @@ -174,7 +175,7 @@ private void emitReady(FeatureProvider provider) { private void detachEventProvider(FeatureProvider provider) { if (provider instanceof EventProvider) { - ((EventProvider)provider).detach(); + ((EventProvider) provider).detach(); } } @@ -229,9 +230,10 @@ public void clearHooks() { /** * Shut down and reset the current status of OpenFeature API. - * This call cleans up all active providers and attempts to shut down internal event handling mechanisms. + * This call cleans up all active providers and attempts to shut down internal + * event handling mechanisms. * Once shut down is complete, API is reset and ready to use again. - * */ + */ public void shutdown() { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { providerRepository.shutdown(); @@ -302,9 +304,9 @@ void removeHandler(String clientName, ProviderEvent event, Consumer handler) { try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { - // if the provider is READY, run immediately - if (ProviderEvent.PROVIDER_READY.equals(event) - && ProviderState.READY.equals(this.providerRepository.getProvider(clientName).getState())) { + // if the provider is in the state associated with event, run immediately + if (Optional.ofNullable(this.providerRepository.getProvider(clientName).getState()) + .orElse(ProviderState.READY).matchesEvent(event)) { eventSupport.runHandler(handler, EventDetails.builder().clientName(clientName).build()); } eventSupport.addClientHandler(clientName, event, handler); @@ -315,30 +317,36 @@ void addHandler(String clientName, ProviderEvent event, Consumer h * Runs the handlers associated with a particular provider. * * @param provider the provider from where this event originated - * @param event the event type - * @param details the event details + * @param event the event type + * @param details the event details */ private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) { try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { - + List clientNamesForProvider = providerRepository - .getClientNamesForProvider(provider); - + .getClientNamesForProvider(provider); + + final String providerName = Optional.ofNullable(provider.getMetadata()) + .map(metadata -> metadata.getName()) + .orElse(null); + // run the global handlers - eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details)); + eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details, providerName)); // run the handlers associated with named clients for this provider - clientNamesForProvider.forEach(name -> { - eventSupport.runClientHandlers(name, event, EventDetails.fromProviderEventDetails(details, name)); + clientNamesForProvider.forEach(name -> { + eventSupport.runClientHandlers(name, event, + EventDetails.fromProviderEventDetails(details, providerName, name)); }); - + if (providerRepository.isDefaultProvider(provider)) { // run handlers for clients that have no bound providers (since this is the default) Set allClientNames = eventSupport.getAllClientNames(); Set boundClientNames = providerRepository.getAllBoundClientNames(); allClientNames.removeAll(boundClientNames); allClientNames.forEach(name -> { - eventSupport.runClientHandlers(name, event, EventDetails.fromProviderEventDetails(details, name)); + eventSupport.runClientHandlers(name, event, + EventDetails.fromProviderEventDetails(details, providerName, name)); }); } } diff --git a/src/main/java/dev/openfeature/sdk/ProviderRepository.java b/src/main/java/dev/openfeature/sdk/ProviderRepository.java index f3967e26..2ca3b21f 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderRepository.java +++ b/src/main/java/dev/openfeature/sdk/ProviderRepository.java @@ -171,16 +171,6 @@ public void shutdown() { .concat(Stream.of(this.defaultProvider.get()), this.providers.values().stream()) .distinct() .forEach(this::shutdownProvider); - setProvider(new NoOpProvider(), - (FeatureProvider fp) -> { - }, - (FeatureProvider fp) -> { - }, - (FeatureProvider fp) -> { - }, - (FeatureProvider fp, - String message) -> { - }, false); this.providers.clear(); taskExecutor.shutdown(); } diff --git a/src/main/java/dev/openfeature/sdk/ProviderState.java b/src/main/java/dev/openfeature/sdk/ProviderState.java index 6685f8fe..a66d4e94 100644 --- a/src/main/java/dev/openfeature/sdk/ProviderState.java +++ b/src/main/java/dev/openfeature/sdk/ProviderState.java @@ -4,5 +4,17 @@ * Indicates the state of the provider. */ public enum ProviderState { - READY, NOT_READY, ERROR; + READY, NOT_READY, ERROR, STALE; + + /** + * Returns true if the passed ProviderEvent maps to this ProviderState. + * + * @param event event to compare + * @return boolean if matches. + */ + boolean matchesEvent(ProviderEvent event) { + return this == READY && event == ProviderEvent.PROVIDER_READY + || this == STALE && event == ProviderEvent.PROVIDER_STALE + || this == ERROR && event == ProviderEvent.PROVIDER_ERROR; + } } diff --git a/src/main/java/dev/openfeature/sdk/Structure.java b/src/main/java/dev/openfeature/sdk/Structure.java index 46274e70..6ceaf592 100644 --- a/src/main/java/dev/openfeature/sdk/Structure.java +++ b/src/main/java/dev/openfeature/sdk/Structure.java @@ -48,12 +48,17 @@ public interface Structure { Map asObjectMap(); /** - * convertValue is converting the object type Value in a primitive type. + * Converts the Value into its equivalent primitive type. * * @param value - Value object to convert - * @return an Object containing the primitive type. + * @return an Object containing the primitive type, or null. */ default Object convertValue(Value value) { + + if (value == null || value.isNull()) { + return null; + } + if (value.isBoolean()) { return value.asBoolean(); } @@ -85,15 +90,14 @@ default Object convertValue(Value value) { if (value.isStructure()) { Structure s = value.asStructure(); return s.asMap() - .keySet() + .entrySet() .stream() - .collect( - Collectors.toMap( - key -> key, - key -> convertValue(s.getValue(key)) - ) - ); + .collect(HashMap::new, + (accumulated, entry) -> accumulated.put(entry.getKey(), + convertValue(entry.getValue())), + HashMap::putAll); } + throw new ValueNotConvertableError(); } @@ -134,7 +138,9 @@ default Map merge(Function map) { return new MutableStructure(map.entrySet().stream() - .filter(e -> e.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue())))); + .collect(HashMap::new, + (accumulated, entry) -> accumulated.put(entry.getKey(), + objectToValue(entry.getValue())), + HashMap::putAll)); } } diff --git a/src/main/java/dev/openfeature/sdk/Value.java b/src/main/java/dev/openfeature/sdk/Value.java index 8be50179..59e4a9cf 100644 --- a/src/main/java/dev/openfeature/sdk/Value.java +++ b/src/main/java/dev/openfeature/sdk/Value.java @@ -19,11 +19,15 @@ */ @ToString @EqualsAndHashCode -@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"}) +@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType", "checkstyle:NoFinalizer"}) public class Value implements Cloneable { private final Object innerObject; + protected final void finalize() { + // DO NOT REMOVE, spotbugs: CT_CONSTRUCTOR_THROW + } + /** * Construct a new null Value. */ @@ -271,11 +275,7 @@ protected Value clone() { return new Value(copy); } if (this.isStructure()) { - Map copy = this.asStructure().asMap().entrySet().stream().collect(Collectors.toMap( - Map.Entry::getKey, - e -> e.getValue().clone() - )); - return new Value(new ImmutableStructure(copy)); + return new Value(new ImmutableStructure(this.asStructure().asMap())); } if (this.isInstant()) { Instant copy = Instant.ofEpochMilli(this.asInstant().toEpochMilli()); @@ -294,7 +294,7 @@ public static Value objectToValue(Object object) { if (object instanceof Value) { return (Value) object; } else if (object == null) { - return null; + return new Value(); } else if (object instanceof String) { return new Value((String) object); } else if (object instanceof Boolean) { diff --git a/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java b/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java index 8d1c4514..4fdc433b 100644 --- a/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java +++ b/src/test/java/dev/openfeature/sdk/DoSomethingProvider.java @@ -4,10 +4,19 @@ class DoSomethingProvider implements FeatureProvider { static final String name = "Something"; // Flag evaluation metadata - static final ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build(); + static final ImmutableMetadata DEFAULT_METADATA = ImmutableMetadata.builder().build(); + private ImmutableMetadata flagMetadata; private EvaluationContext savedContext; + public DoSomethingProvider() { + this.flagMetadata = DEFAULT_METADATA; + } + + public DoSomethingProvider(ImmutableMetadata flagMetadata) { + this.flagMetadata = flagMetadata; + } + EvaluationContext getMergedContext() { return savedContext; } diff --git a/src/test/java/dev/openfeature/sdk/EventsTest.java b/src/test/java/dev/openfeature/sdk/EventsTest.java index 70f81657..f9f8e4b8 100644 --- a/src/test/java/dev/openfeature/sdk/EventsTest.java +++ b/src/test/java/dev/openfeature/sdk/EventsTest.java @@ -24,7 +24,7 @@ class EventsTest { - private static final int TIMEOUT = 200; + private static final int TIMEOUT = 300; private static final int INIT_DELAY = TIMEOUT / 2; @AfterAll @@ -470,7 +470,7 @@ void handlersRunIfOneThrows() throws Exception { @Test @DisplayName("should have all properties") @Specification(number = "5.2.4", text = "The handler function MUST accept a event details parameter.") - @Specification(number = "5.2.3", text = "The event details MUST contain the client name associated with the event.") + @Specification(number = "5.2.3", text = "The `event details` MUST contain the `provider name` associated with the event.") void shouldHaveAllProperties() throws Exception { final Consumer handler1 = mockHandler(); final Consumer handler2 = mockHandler(); @@ -514,9 +514,9 @@ void shouldHaveAllProperties() throws Exception { @Test @DisplayName("if the provider is ready handlers must run immediately") - @Specification(number = "5.3.3", text = "PROVIDER_READY handlers attached after the provider is already in a ready state MUST run immediately.") - void readyMustRunImmediately() throws Exception { - final String name = "readyMustRunImmediately"; + @Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.") + void matchingReadyEventsMustRunImmediately() throws Exception { + final String name = "matchingEventsMustRunImmediately"; final Consumer handler = mockHandler(); // provider which is already ready @@ -529,6 +529,40 @@ void readyMustRunImmediately() throws Exception { verify(handler, timeout(TIMEOUT)).accept(any()); } + @Test + @DisplayName("if the provider is ready handlers must run immediately") + @Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.") + void matchingStaleEventsMustRunImmediately() throws Exception { + final String name = "matchingEventsMustRunImmediately"; + final Consumer handler = mockHandler(); + + // provider which is already stale + TestEventsProvider provider = new TestEventsProvider(ProviderState.STALE); + OpenFeatureAPI.getInstance().setProvider(name, provider); + + // should run even thought handler was added after stale + Client client = OpenFeatureAPI.getInstance().getClient(name); + client.onProviderStale(handler); + verify(handler, timeout(TIMEOUT)).accept(any()); + } + + @Test + @DisplayName("if the provider is ready handlers must run immediately") + @Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.") + void matchingErrorEventsMustRunImmediately() throws Exception { + final String name = "matchingEventsMustRunImmediately"; + final Consumer handler = mockHandler(); + + // provider which is already in error + TestEventsProvider provider = new TestEventsProvider(ProviderState.ERROR); + OpenFeatureAPI.getInstance().setProvider(name, provider); + + // should run even thought handler was added after error + Client client = OpenFeatureAPI.getInstance().getClient(name); + client.onProviderError(handler); + verify(handler, timeout(TIMEOUT)).accept(any()); + } + @Test @DisplayName("must persist across changes") @Specification(number = "5.2.6", text = "Event handlers MUST persist across provider changes.") @@ -560,6 +594,7 @@ void mustPersistAcrossChanges() throws Exception { @Nested class HandlerRemoval { + @Specification(number="5.2.7", text="The API and client MUST provide a function allowing the removal of event handlers.") @Test @DisplayName("should not run removed events") void removedEventsShouldNotRun() { diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java index 35eb0769..52c36dc5 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -1,10 +1,11 @@ package dev.openfeature.sdk; -import static dev.openfeature.sdk.DoSomethingProvider.flagMetadata; +import static dev.openfeature.sdk.DoSomethingProvider.DEFAULT_METADATA; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -140,8 +141,8 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { assertTrue(hooks.contains(m2)); } - @Specification(number="1.3.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), evaluation context (optional), and evaluation options (optional), which returns the flag value.") - @Specification(number="1.3.2.1", text="The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.") + @Specification(number="1.3.1.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), evaluation context (optional), and evaluation options (optional), which returns the flag value.") + @Specification(number="1.3.3.1", text="The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.") @Test void value_flags() { FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); @@ -169,12 +170,12 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { assertEquals(null, c.getObjectValue(key, new Value(), new ImmutableContext(), FlagEvaluationOptions.builder().build())); } - @Specification(number="1.4.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure.") - @Specification(number="1.4.2", text="The evaluation details structure's value field MUST contain the evaluated flag value.") - @Specification(number="1.4.3.1", text="The evaluation details structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped value field.") - @Specification(number="1.4.4", text="The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method.") - @Specification(number="1.4.5", text="In cases of normal execution, the evaluation details structure's variant field MUST contain the value of the variant field in the flag resolution structure returned by the configured provider, if the field is set.") - @Specification(number="1.4.6", text="In cases of normal execution, the evaluation details structure's reason field MUST contain the value of the reason field in the flag resolution structure returned by the configured provider, if the field is set.") + @Specification(number="1.4.1.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns an evaluation details structure.") + @Specification(number="1.4.3", text="The evaluation details structure's value field MUST contain the evaluated flag value.") + @Specification(number="1.4.4.1", text="The evaluation details structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped value field.") + @Specification(number="1.4.5", text="The evaluation details structure's flag key field MUST contain the flag key argument passed to the detailed flag evaluation method.") + @Specification(number="1.4.6", text="In cases of normal execution, the evaluation details structure's variant field MUST contain the value of the variant field in the flag resolution structure returned by the configured provider, if the field is set.") + @Specification(number="1.4.7", text="In cases of normal execution, the `evaluation details` structure's `reason` field MUST contain the value of the `reason` field in the `flag resolution` structure returned by the configured `provider`, if the field is set.") @Test void detail_flags() { FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider()); Client c = api.getClient(); @@ -184,7 +185,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { .flagKey(key) .value(false) .variant(null) - .flagMetadata(flagMetadata) + .flagMetadata(DEFAULT_METADATA) .build(); assertEquals(bd, c.getBooleanDetails(key, true)); assertEquals(bd, c.getBooleanDetails(key, true, new ImmutableContext())); @@ -194,7 +195,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { .flagKey(key) .value("tset") .variant(null) - .flagMetadata(flagMetadata) + .flagMetadata(DEFAULT_METADATA) .build(); assertEquals(sd, c.getStringDetails(key, "test")); assertEquals(sd, c.getStringDetails(key, "test", new ImmutableContext())); @@ -203,7 +204,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { FlagEvaluationDetails id = FlagEvaluationDetails.builder() .flagKey(key) .value(400) - .flagMetadata(flagMetadata) + .flagMetadata(DEFAULT_METADATA) .build(); assertEquals(id, c.getIntegerDetails(key, 4)); assertEquals(id, c.getIntegerDetails(key, 4, new ImmutableContext())); @@ -212,7 +213,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { FlagEvaluationDetails dd = FlagEvaluationDetails.builder() .flagKey(key) .value(40.0) - .flagMetadata(flagMetadata) + .flagMetadata(DEFAULT_METADATA) .build(); assertEquals(dd, c.getDoubleDetails(key, .4)); assertEquals(dd, c.getDoubleDetails(key, .4, new ImmutableContext())); @@ -234,9 +235,10 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { verify(invocationHook, times(1)).before(any(), any()); } - @Specification(number="1.4.9", text="Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the default value in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.") - @Specification(number="1.4.7", text="In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.") - @Specification(number="1.4.12", text="In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.") + @Specification(number="1.4.8", text="In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.") + @Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the `reason` field in the `evaluation details` SHOULD indicate an error.") + @Specification(number="1.4.10", text="Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the `default value` in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.") + @Specification(number="1.4.13", text="In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.") @Test void broken_provider() { FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); Client c = api.getClient(); @@ -246,7 +248,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage()); } - @Specification(number="1.4.10", text="In the case of abnormal execution, the client SHOULD log an informative error message.") + @Specification(number="1.4.11", text="In the case of abnormal execution, the client SHOULD log an informative error message.") @Test void log_on_error() throws NotImplementedException { FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); Client c = api.getClient(); @@ -269,7 +271,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { assertEquals("test", c2.getMetadata().getName()); } - @Specification(number="1.4.8", text="In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.") + @Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.") @Test void reason_is_error_when_there_are_errors() { FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider()); Client c = api.getClient(); @@ -277,8 +279,17 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { assertEquals(Reason.ERROR.toString(), result.getReason()); } - @Specification(number="3.2.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.") - @Specification(number="3.2.2", 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="1.4.14", text="If the flag metadata field in the flag resolution structure returned by the configured provider is set, the evaluation details structure's flag metadata field MUST contain that value. Otherwise, it MUST contain an empty record.") + @Test void flag_metadata_passed() { + FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider(null)); + Client c = api.getClient(); + FlagEvaluationDetails result = c.getBooleanDetails("test", false); + assertNotNull(result.getFlagMetadata()); + } + + @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.") @Test void multi_layer_context_merges_correctly() { DoSomethingProvider provider = new DoSomethingProvider(); FeatureProviderTestUtils.setFeatureProvider(provider); @@ -315,13 +326,22 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { } - @Specification(number="1.3.3", 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.") + @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.") @Test void type_system_prevents_this() {} @Specification(number="1.1.7", text="The client creation function MUST NOT throw, or otherwise abnormally terminate.") @Test void constructor_does_not_throw() {} - @Specification(number="1.4.11", text="The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.") + @Specification(number="1.4.12", text="The client SHOULD provide asynchronous or non-blocking mechanisms for flag evaluation.") @Test void one_thread_per_request_model() {} + @Specification(number="1.4.14.1", text="Condition: Flag metadata MUST be immutable.") + @Test void compiler_enforced() {} + + @Specification(number="1.4.2.1", text="The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns an evaluation details structure.") + @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.") + @Test void not_applicable_for_dynamic_context() {} + } diff --git a/src/test/java/dev/openfeature/sdk/HookContextTest.java b/src/test/java/dev/openfeature/sdk/HookContextTest.java index 14a2ef2b..763069fd 100644 --- a/src/test/java/dev/openfeature/sdk/HookContextTest.java +++ b/src/test/java/dev/openfeature/sdk/HookContextTest.java @@ -23,4 +23,7 @@ class HookContextTest { assertTrue(Metadata.class.isAssignableFrom(hc.getProviderMetadata().getClass())); } + @Specification(number="4.3.3.1", text="The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters. It has no return value.") + @Test void not_applicable_for_dynamic_context() {} + } \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/HookSpecTest.java b/src/test/java/dev/openfeature/sdk/HookSpecTest.java index d1daa705..def331db 100644 --- a/src/test/java/dev/openfeature/sdk/HookSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSpecTest.java @@ -154,7 +154,7 @@ void emptyApiHooks() { .build(); } - @Specification(number="4.3.2", text="The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters and returns either an evaluation context or nothing.") + @Specification(number="4.3.2.1", text="The before stage MUST run before flag resolution occurs. It accepts a hook context (required) and hook hints (optional) as parameters and returns either an evaluation context or nothing.") @Test void before_runs_ahead_of_evaluation() { OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProvider(new AlwaysBrokenProvider()); @@ -181,11 +181,11 @@ void emptyApiHooks() { verify(h, times(0)).error(any(), any(), any()); } + @Specification(number="4.3.6", text="The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.") + @Specification(number="4.3.7", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value.") + @Specification(number="4.3.8", text="The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and hook hints (optional). There is no return value.") @Specification(number="4.4.1", text="The API, Client, Provider, and invocation MUST have a method for registering hooks.") - @Specification(number="4.3.5", text="The after stage MUST run after flag resolution occurs. It accepts a hook context (required), flag evaluation details (required) and hook hints (optional). It has no return value.") @Specification(number="4.4.2", text="Hooks MUST be evaluated in the following order: - before: API, Client, Invocation, Provider - after: Provider, Invocation, Client, API - error (if applicable): Provider, Invocation, Client, API - finally: Provider, Invocation, Client, API") - @Specification(number="4.3.6", text="The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value.") - @Specification(number="4.3.7", text="The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required) and hook hints (optional). There is no return value.") @Test void hook_eval_order() { List evalOrder = new ArrayList<>(); OpenFeatureAPI api = OpenFeatureAPI.getInstance(); @@ -446,7 +446,7 @@ public void finallyAfter(HookContext ctx, Map hints) { } @Specification(number = "4.1.4", text = "The evaluation context MUST be mutable only within the before hook.") - @Specification(number = "4.3.3", text = "Any evaluation context returned from a before hook MUST be passed to subsequent before hooks (via HookContext).") + @Specification(number = "4.3.4", text = "Any `evaluation context` returned from a `before` hook MUST be passed to subsequent `before` hooks (via `HookContext`).") @Test void beforeContextUpdated() { EvaluationContext ctx = new ImmutableContext(); Hook hook = mockBooleanHook(); @@ -471,7 +471,7 @@ public void finallyAfter(HookContext ctx, Map hints) { } - @Specification(number="4.3.4", text="When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context.") + @Specification(number="4.3.5", text="When before hooks have finished executing, any resulting evaluation context MUST be merged with the existing evaluation context.") @Test void mergeHappensCorrectly() { Map attributes= new HashMap<>(); attributes.put("test", new Value("works")); @@ -561,7 +561,7 @@ private Client getClient(FeatureProvider provider) { @Specification(number="4.3.1", text="Hooks MUST specify at least one stage.") @Test void default_methods_so_impossible() {} - @Specification(number="4.3.8.1", text="Instead of finally, finallyAfter SHOULD be used.") + @Specification(number="4.3.9.1", text="Instead of finally, finallyAfter SHOULD be used.") @SneakyThrows @Test void doesnt_use_finally() { assertThatCode(() -> Hook.class.getMethod("finally", HookContext.class, Map.class)) diff --git a/src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java b/src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java index d7453452..491b5069 100644 --- a/src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java +++ b/src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java @@ -122,4 +122,11 @@ void GettingAMissingValueShouldReturnNull() { assertEquals(expected, structure.asObjectMap()); } + + @Test + void constructorHandlesNullValue() { + HashMap attrs = new HashMap<>(); + attrs.put("null", null); + new ImmutableStructure(attrs); + } } diff --git a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java index 0ab5e371..270ac77e 100644 --- a/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/InitializeBehaviorSpecTest.java @@ -30,7 +30,7 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredProviderBeforeUsingItForFlagE verify(featureProvider, timeout(1000)).initialize(any()); } - @Specification(number = "1.4.9", text = "Methods, functions, or operations on the client MUST NOT throw " + @Specification(number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw " + "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the " + "`default value` in the event of abnormal execution. Exceptions include functions or methods for " + "the purposes for configuration or setup.") @@ -65,7 +65,7 @@ void mustCallInitializeFunctionOfTheNewlyRegisteredNamedProviderBeforeUsingItFor verify(featureProvider, timeout(1000)).initialize(any()); } - @Specification(number = "1.4.9", text = "Methods, functions, or operations on the client MUST NOT throw " + @Specification(number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw " + "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the " + "`default value` in the event of abnormal execution. Exceptions include functions or methods for " + "the purposes for configuration or setup.") diff --git a/src/test/java/dev/openfeature/sdk/LockingTest.java b/src/test/java/dev/openfeature/sdk/LockingTest.java index d9601e85..f58795ad 100644 --- a/src/test/java/dev/openfeature/sdk/LockingTest.java +++ b/src/test/java/dev/openfeature/sdk/LockingTest.java @@ -12,9 +12,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; +@Isolated() class LockingTest { private static OpenFeatureAPI api; @@ -26,11 +28,12 @@ class LockingTest { @BeforeAll static void beforeAll() { api = OpenFeatureAPI.getInstance(); + OpenFeatureAPI.getInstance().setProvider("LockingTest", new NoOpProvider()); } @BeforeEach void beforeEach() { - client = (OpenFeatureClient) api.getClient(); + client = (OpenFeatureClient) api.getClient("LockingTest"); apiLock = setupLock(apiLock, mockInnerReadLock(), mockInnerWriteLock()); OpenFeatureAPI.lock = apiLock; diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java index a49bf643..10b38e6c 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java @@ -27,6 +27,19 @@ void namedProviderTest() { .isEqualTo(api.getProviderMetadata("namedProviderTest").getName()); } + @Specification(number="1.1.3", text="The API MUST provide a function to bind a given provider to one or more client names. If the client-name already has a bound provider, it is overwritten with the new mapping.") + @Test + void namedProviderOverwrittenTest() { + String name = "namedProviderOverwrittenTest"; + FeatureProvider provider1 = new NoOpProvider(); + FeatureProvider provider2 = new DoSomethingProvider(); + FeatureProviderTestUtils.setFeatureProvider(name, provider1); + FeatureProviderTestUtils.setFeatureProvider(name, provider2); + + assertThat(OpenFeatureAPI.getInstance().getProvider(name).getMetadata().getName()) + .isEqualTo(DoSomethingProvider.name); + } + @Test void settingDefaultProviderToNullErrors() { assertThatCode(() -> api.setProvider(null)).isInstanceOf(IllegalArgumentException.class); diff --git a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java index 0d4ae5d6..5b475dc3 100644 --- a/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java +++ b/src/test/java/dev/openfeature/sdk/ProviderRepositoryTest.java @@ -296,15 +296,6 @@ void shouldShutdownAllFeatureProvidersOnShutdown() { setFeatureProvider(ANOTHER_CLIENT_NAME, featureProvider2); providerRepository.shutdown(); - - await() - .pollDelay(Duration.ofMillis(1)) - .atMost(Duration.ofSeconds(TIMEOUT)) - .untilAsserted(() -> { - assertThat(providerRepository.getProvider()).isInstanceOf(NoOpProvider.class); - assertThat(providerRepository.getProvider(CLIENT_NAME)).isInstanceOf(NoOpProvider.class); - assertThat(providerRepository.getProvider(ANOTHER_CLIENT_NAME)).isInstanceOf(NoOpProvider.class); - }); verify(featureProvider1, timeout(TIMEOUT)).shutdown(); verify(featureProvider2, timeout(TIMEOUT)).shutdown(); } diff --git a/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java b/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java index f5e5e6a4..a87cc517 100644 --- a/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/ProviderSpecTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -39,7 +40,7 @@ void flag_value_set() { } - @Specification(number = "2.2.5", text = "The `provider` SHOULD populate the `resolution details` structure's `reason` field with `\"STATIC\"`, `\"DEFAULT\",` `\"TARGETING_MATCH\"`, `\"SPLIT\"`, `\"CACHED\"`, `\"DISABLED\"`, `\"UNKNOWN\"`, `\"ERROR\"` or some other string indicating the semantic reason for the returned flag value.") + @Specification(number = "2.2.5", text = "The `provider` SHOULD populate the `resolution details` structure's `reason` field with `\"STATIC\"`, `\"DEFAULT\",` `\"TARGETING_MATCH\"`, `\"SPLIT\"`, `\"CACHED\"`, `\"DISABLED\"`, `\"UNKNOWN\"`, `\"STALE\"`, `\"ERROR\"` or some other string indicating the semantic reason for the returned flag value.") @Test void has_reason() { ProviderEvaluation result = p.getBooleanEvaluation("key", false, new ImmutableContext()); @@ -76,10 +77,50 @@ void variant_set() { assertNotNull(boolean_result.getReason()); } + @Specification(number = "2.2.10", text = "`flag metadata` MUST be a structure supporting the definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number`.") + @Test + void flag_metadata_structure() { + ImmutableMetadata metadata = ImmutableMetadata.builder() + .addBoolean("bool", true) + .addDouble("double", 1.1d) + .addFloat("float", 2.2f) + .addInteger("int", 3) + .addLong("long", 1l) + .addString("string", "str") + .build(); + + assertEquals(true, metadata.getBoolean("bool")); + assertEquals(1.1d, metadata.getDouble("double")); + assertEquals(2.2f, metadata.getFloat("float")); + assertEquals(3, metadata.getInteger("int")); + assertEquals(1l, metadata.getLong("long")); + assertEquals("str", metadata.getString("string")); + } + @Specification(number = "2.3.1", text = "The provider interface MUST define a provider hook mechanism which can be optionally implemented in order to add hook instances to the evaluation life-cycle.") @Specification(number = "4.4.1", text = "The API, Client, Provider, and invocation MUST have a method for registering hooks.") @Test void provider_hooks() { assertEquals(0, p.getProviderHooks().size()); } + + @Specification(number = "2.4.2", text = "The provider MAY define a status field/accessor which indicates the readiness of the provider, with possible values NOT_READY, READY, or ERROR.") + @Test + void defines_status() { + assertTrue(p.getState() instanceof ProviderState); + } + + @Specification(number = "2.4.3", text = "The provider MUST set its status field/accessor to READY if its initialize function terminates normally.") + @Specification(number = "2.4.4", text = "The provider MUST set its status field to ERROR if its initialize function terminates abnormally.") + @Specification(number = "2.2.9", text = "The provider SHOULD populate the resolution details structure's flag metadata field.") + @Specification(number = "2.4.1", text = "The provider MAY define an initialize function which accepts the global evaluation context as an argument and performs initialization logic relevant to the provider.") + @Specification(number = "2.5.1", text = "The provider MAY define a mechanism to gracefully shutdown and dispose of resources.") + @Test + void provider_responsibility() { + } + + @Specification(number = "2.6.1", text = "The provider MAY define an on context changed handler, which takes an argument for the previous context and the newly set context, in order to respond to an evaluation context change.") + @Test + void not_applicable_for_dynamic_context() { + } } diff --git a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java index 8659ce78..b0a0a9d1 100644 --- a/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/ShutdownBehaviorSpecTest.java @@ -35,7 +35,7 @@ void mustInvokeShutdownMethodOnPreviouslyRegisteredProviderOnceItShouldNotBeUsed verify(featureProvider, timeout(1000)).shutdown(); } - @Specification(number = "1.4.9", text = "Methods, functions, or operations on the client MUST NOT throw " + @Specification(number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw " + "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the " + "`default value` in the event of abnormal execution. Exceptions include functions or methods for " + "the purposes for configuration or setup.") @@ -68,7 +68,7 @@ void mustInvokeShutdownMethodOnPreviouslyRegisteredProviderOnceItShouldNotBeUsed verify(featureProvider, timeout(1000)).shutdown(); } - @Specification(number = "1.4.9", text = "Methods, functions, or operations on the client MUST NOT throw " + @Specification(number = "1.4.10", text = "Methods, functions, or operations on the client MUST NOT throw " + "exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the " + "`default value` in the event of abnormal execution. Exceptions include functions or methods for " + "the purposes for configuration or setup.") diff --git a/src/test/java/dev/openfeature/sdk/StructureTest.java b/src/test/java/dev/openfeature/sdk/StructureTest.java index 8f188911..16747ee0 100644 --- a/src/test/java/dev/openfeature/sdk/StructureTest.java +++ b/src/test/java/dev/openfeature/sdk/StructureTest.java @@ -98,6 +98,20 @@ void mapToStructureTest() { assertEquals(new Value(Instant.ofEpochSecond(0)), res.getValue("Instant")); assertEquals(new HashMap<>(), res.getValue("Map").asStructure().asMap()); assertEquals(new Value(immutableContext), res.getValue("ImmutableContext")); - assertNull(res.getValue("nullKey")); + assertEquals(new Value(), res.getValue("nullKey")); + } + + @Test + void asObjectHandlesNullValue() { + Map map = new HashMap<>(); + map.put("null", new Value((String)null)); + ImmutableStructure structure = new ImmutableStructure(map); + assertNull(structure.asObjectMap().get("null")); + } + + @Test + void convertValueHandlesNullValue() { + ImmutableStructure structure = new ImmutableStructure(); + assertNull(structure.convertValue(new Value((String)null))); } } diff --git a/src/test/java/dev/openfeature/sdk/ValueTest.java b/src/test/java/dev/openfeature/sdk/ValueTest.java index 53513afd..816190ab 100644 --- a/src/test/java/dev/openfeature/sdk/ValueTest.java +++ b/src/test/java/dev/openfeature/sdk/ValueTest.java @@ -1,5 +1,6 @@ package dev.openfeature.sdk; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -142,4 +143,9 @@ class Something {} assertThrows(InstantiationException.class, ()-> new Value(list)); } + + @Test public void noOpFinalize() { + Value val = new Value(); + assertDoesNotThrow(val::finalize); // does nothing, but we want to defined in and make it final. + } } diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java b/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java index 25650bf6..af239644 100644 --- a/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java +++ b/src/test/java/dev/openfeature/sdk/testutils/TestEventsProvider.java @@ -16,6 +16,13 @@ public class TestEventsProvider extends EventProvider { private ProviderState state = ProviderState.NOT_READY; private boolean shutDown = false; private int initTimeoutMs = 0; + private String name = "test"; + private Metadata metadata = new Metadata() { + @Override + public String getName() { + return name; + } + }; @Override public ProviderState getState() { @@ -64,7 +71,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { @Override public Metadata getMetadata() { - throw new UnsupportedOperationException("Unimplemented method 'getMetadata'"); + return this.metadata; } @Override diff --git a/version.txt b/version.txt index 9c6d6293..bd8bf882 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.6.1 +1.7.0