diff --git a/.github/component_owners.yml b/.github/component_owners.yml index efdb668be..1eee12a13 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -33,6 +33,9 @@ components: - davejohnston providers/statsig: - liran2000 + providers/prefab: + - liran2000 + - semanticart ignored-authors: - renovate-bot diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 84c9dd5cd..614046ec0 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -12,6 +12,7 @@ "providers/harness": "0.0.4-alpha", "providers/statsig": "0.0.3", "providers/ofrep": "0.1.5", + "providers/prefab": "0.0.1", "tests/flagd": "1.4.1", "providers/go-feature-flag-in-process": "0.1.0" } diff --git a/providers/prefab/CHANGELOG.md b/providers/prefab/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/providers/prefab/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/providers/prefab/LICENSE b/providers/prefab/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/providers/prefab/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/providers/prefab/README.md b/providers/prefab/README.md new file mode 100644 index 000000000..319c9e3de --- /dev/null +++ b/providers/prefab/README.md @@ -0,0 +1,77 @@ +# Unofficial Prefab OpenFeature Provider for GO + +[Prefab](https://www.prefab.cloud/) OpenFeature Provider can provide usage for Prefab via OpenFeature GO SDK. + +## Installation + +To use the provider, you'll need to install [Prefab Go client](https://github.com/prefab-cloud/prefab-cloud-go) and Prefab provider. You can install the packages using the following command + +```shell +go get github.com/prefab-cloud/prefab-cloud-go +go get github.com/open-feature/go-sdk-contrib/providers/prefab +``` + +## Usage +Prefab OpenFeature Provider is using Prefab GO SDK. + +### Usage Example + +```go +import ( + prefabProvider "github.com/open-feature/go-sdk-contrib/providers/prefab/pkg" + of "github.com/open-feature/go-sdk/openfeature" + prefab "github.com/prefab-cloud/prefab-cloud-go/pkg" +) + +var provider *prefabProvider.Provider +var ofClient *of.Client + +providerConfig := prefabProvider.ProviderConfig{ + APIKey: "YOUR_API_KEY", +} + +var err error +provider, err = prefabProvider.NewProvider(providerConfig) +if err != nil { + fmt.Printf("Error during new provider: %v\n", err) + os.Exit(1) +} +err = provider.Init(of.EvaluationContext{}) +if err != nil { + fmt.Printf("Error during provider init: %v\n", err) + os.Exit(1) +} + +of.SetProvider(provider) +ofClient = of.NewClient("my-app") + +evalCtx := of.NewEvaluationContext( + "", + map[string]interface{}{ + "user.key": "key1", + "team.domain": "prefab.cloud", + "team.description": "team1", + }, +) +enabled, _ := ofClient.BooleanValue(context.Background(), "always_on_gate", false, evalCtx) +fmt.Printf("enabled: %v\n", enabled) +value, _ := ofClient.StringValue(context.Background(), "string", "fallback", evalCtx) +fmt.Printf("value: %v\n", value) +slice, _ := ofClient.ObjectValueDetails(context.Background(), "sample_list", []string{"a2", "b2"}, evalCtx) +fmt.Printf("slice: %v\n", slice) + +of.Shutdown() + +``` +See [provider_test.go](./pkg/provider_test.go) for more information. + +## Notes + +Some Prefab custom operations are supported from the Prefab client via PrefabClient. + +## Prefab Provider Tests Strategies + +Unit test based on Prefab yaml config file. +Can be enhanced pending [JSON dump data source](https://github.com/prefab-cloud/prefab-cloud-go/blob/0e3d5a4ba7171bbc4484cc99ccaad4c0c32d7e81/README.md?plain=1#L58) +JSON evaluation not tested properly until then. +See [provider_test.go](./pkg/provider_test.go) for more information. diff --git a/providers/prefab/go.mod b/providers/prefab/go.mod new file mode 100644 index 000000000..6fb63fd06 --- /dev/null +++ b/providers/prefab/go.mod @@ -0,0 +1,26 @@ +module github.com/open-feature/go-sdk-contrib/providers/prefab + +go 1.21 + +require ( + github.com/open-feature/go-sdk v1.12.0 + github.com/prefab-cloud/prefab-cloud-go v0.0.5 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/r3labs/sse/v2 v2.10.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/sosodev/duration v1.3.1 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/net v0.26.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/providers/prefab/go.sum b/providers/prefab/go.sum new file mode 100644 index 000000000..2ba021aeb --- /dev/null +++ b/providers/prefab/go.sum @@ -0,0 +1,61 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/open-feature/go-sdk v1.12.0 h1:V0MAG3lC9o7Pmq0gxlqtKpoasDTm3to9vuvZKyUhhPk= +github.com/open-feature/go-sdk v1.12.0/go.mod h1:UDNuwVrwY5FRHIluVRYzvxuS3nBkhjE6o4tlwFuHxiI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prefab-cloud/prefab-cloud-go v0.0.5 h1:OVrVc2DO1ATPODzYmF/ZsxCXf7zyQVt/rXz+iRjBoYs= +github.com/prefab-cloud/prefab-cloud-go v0.0.5/go.mod h1:pM9jInvIOCqPGQkABII+ir3rU4tsPw6uIxG22DpcbD4= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/providers/prefab/internal/util.go b/providers/prefab/internal/util.go new file mode 100644 index 000000000..3ae634624 --- /dev/null +++ b/providers/prefab/internal/util.go @@ -0,0 +1,47 @@ +package internal + +import ( + "fmt" + "strings" + + of "github.com/open-feature/go-sdk/openfeature" + prefab "github.com/prefab-cloud/prefab-cloud-go/pkg" +) + +func ToPrefabContext(evalCtx of.FlattenedContext) (prefab.ContextSet, error) { + if len(evalCtx) == 0 { + return prefab.ContextSet{}, nil + } + prefabContext := prefab.NewContextSet() + for k, v := range evalCtx { + // val, ok := toStr(v) + parts := strings.SplitN(k, ".", 2) + if len(parts) < 2 { + return *prefabContext, fmt.Errorf("context key structure should be in the form of x.y: %s", k) + } + key, subkey := parts[0], parts[1] + if _, exists := prefabContext.Data[key]; !exists { + prefabContext.WithNamedContextValues(key, map[string]interface{}{ + subkey: v, + }) + } else { + prefabContext.Data[key].Data[subkey] = v + } + } + return *prefabContext, nil +} + +func toStr(val interface{}) (string, bool) { + switch v := val.(type) { + case string: + return v, true + case int, int8, int16, int32, int64: + return fmt.Sprintf("%d", v), true + case float32, float64: + return fmt.Sprintf("%.6f", v), true + case bool: + return fmt.Sprintf("%t", v), true + default: + return "", false + } +} diff --git a/providers/prefab/pkg/enabled.yaml b/providers/prefab/pkg/enabled.yaml new file mode 100644 index 000000000..9cafac220 --- /dev/null +++ b/providers/prefab/pkg/enabled.yaml @@ -0,0 +1,43 @@ +sample_int: 123 +sample_double: 12.12 +sample_bool: true +false_value: false +zero_value: 0 +sample_to_override: Foo +prefab.log_level: debug +sample: test sample value +enabled_flag: true +disabled_flag: false +flag_with_a_value: { "feature_flag": "true", value: "all-features" } +in_lookup_key: { "feature_flag": "true", value: true, criteria: { operator: LOOKUP_KEY_IN, values: [ "abc123", "xyz987" ] } } +test1: { "feature_flag": "true", value: "new-version", criteria: { operator: PROP_IS_ONE_OF, property: "domain", values: [ "prefab.cloud", "example.com" ] } } +nested: + values: + _: top level + string: nested value + + +nested2: + _: the value + +log-level: + _: warn + cloud.prefab.client: warn + tests: + _: debug + capitalized: INFO + uncapitalized: info + nested: + _: warn + deeply: error + +example: + nested: + path: hello + +example2.nested.path: hello2 + +sample_list: +- a +- b +sample_json: { "feature_flag": "true","value": "all-features" } diff --git a/providers/prefab/pkg/provider.go b/providers/prefab/pkg/provider.go new file mode 100644 index 000000000..0e017ff09 --- /dev/null +++ b/providers/prefab/pkg/provider.go @@ -0,0 +1,317 @@ +package prefab + +import ( + "context" + "fmt" + + "github.com/open-feature/go-sdk-contrib/providers/prefab/internal" + of "github.com/open-feature/go-sdk/openfeature" + prefab "github.com/prefab-cloud/prefab-cloud-go/pkg" +) + +const providerNotReady = "Provider not ready" +const generalError = "general error" + +type Provider struct { + providerConfig ProviderConfig + PrefabClient *prefab.Client + status of.State +} + +func NewProvider(providerConfig ProviderConfig) (*Provider, error) { + provider := &Provider{ + providerConfig: providerConfig, + status: of.NotReadyState, + } + return provider, nil +} + +func (p *Provider) Init(evaluationContext of.EvaluationContext) error { + var prefabClient *prefab.Client + var err error + + if p.providerConfig.APIKey != "" { + prefabClient, err = prefab.NewClient(prefab.WithAPIKey(p.providerConfig.APIKey)) + } else if p.providerConfig.APIURLs != nil { + prefabClient, err = prefab.NewClient(prefab.WithAPIURLs(p.providerConfig.APIURLs)) + } else if p.providerConfig.Sources != nil { + prefabClient, err = prefab.NewClient(prefab.WithOfflineSources(p.providerConfig.Sources)) + } else { + err = fmt.Errorf("provider config missing fields") + } + + if err != nil { + p.status = of.ErrorState + } else { + p.status = of.ReadyState + p.PrefabClient = prefabClient + } + return err +} + +func (p *Provider) Status() of.State { + return p.status +} + +func (p *Provider) Shutdown() { + // no Shutdown method on p.PrefabClient + p.status = of.NotReadyState +} + +// provider does not have any hooks, returns empty slice +func (p *Provider) Hooks() []of.Hook { + return []of.Hook{} +} + +// Metadata returns value of Metadata (name of current service, exposed to openfeature sdk) +func (p *Provider) Metadata() of.Metadata { + return of.Metadata{ + Name: "prefab", + } +} + +func (p *Provider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail { + shouldReturn, returnValue := verifyStateBoolean(p, defaultValue) + if shouldReturn { + return returnValue + } + + prefabContext, err := internal.ToPrefabContext(evalCtx) + if err != nil { + return of.BoolResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewInvalidContextResolutionError(err.Error()), + Reason: of.ErrorReason, + }, + } + } + + value, _ := p.PrefabClient.GetBoolValueWithDefault(flag, prefabContext, defaultValue) + return of.BoolResolutionDetail{ + Value: value, + } +} + +func verifyStateBoolean(p *Provider, defaultValue bool) (bool, of.BoolResolutionDetail) { + if p.status != of.ReadyState { + if p.status == of.NotReadyState { + return true, of.BoolResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewProviderNotReadyResolutionError(providerNotReady), + Reason: of.ErrorReason, + }, + } + } else { + return true, of.BoolResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError(generalError), + Reason: of.ErrorReason, + }, + } + } + } + return false, of.BoolResolutionDetail{} +} + +func (p *Provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail { + shouldReturn, returnValue := verifyStateFloat(p, defaultValue) + if shouldReturn { + return returnValue + } + + prefabContext, err := internal.ToPrefabContext(evalCtx) + if err != nil { + return of.FloatResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewInvalidContextResolutionError(err.Error()), + Reason: of.ErrorReason, + }, + } + } + + value, _ := p.PrefabClient.GetFloatValueWithDefault(flag, prefabContext, defaultValue) + return of.FloatResolutionDetail{ + Value: value, + ProviderResolutionDetail: of.ProviderResolutionDetail{}, + } +} + +func verifyStateFloat(p *Provider, defaultValue float64) (bool, of.FloatResolutionDetail) { + if p.status != of.ReadyState { + if p.status == of.NotReadyState { + return true, of.FloatResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewProviderNotReadyResolutionError(providerNotReady), + Reason: of.ErrorReason, + }, + } + } else { + return true, of.FloatResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError(generalError), + Reason: of.ErrorReason, + }, + } + } + } + return false, of.FloatResolutionDetail{} +} + +func (p *Provider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx of.FlattenedContext) of.IntResolutionDetail { + shouldReturn, returnValue := verifyStateInt(p, defaultValue) + if shouldReturn { + return returnValue + } + + prefabContext, err := internal.ToPrefabContext(evalCtx) + if err != nil { + return of.IntResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewInvalidContextResolutionError(err.Error()), + Reason: of.ErrorReason, + }, + } + } + + value, _ := p.PrefabClient.GetIntValueWithDefault(flag, prefabContext, defaultValue) + return of.IntResolutionDetail{ + Value: value, + ProviderResolutionDetail: of.ProviderResolutionDetail{}, + } +} + +func verifyStateInt(p *Provider, defaultValue int64) (bool, of.IntResolutionDetail) { + if p.status != of.ReadyState { + if p.status == of.NotReadyState { + return true, of.IntResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewProviderNotReadyResolutionError(providerNotReady), + Reason: of.ErrorReason, + }, + } + } else { + return true, of.IntResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError(generalError), + Reason: of.ErrorReason, + }, + } + } + } + return false, of.IntResolutionDetail{} +} + +func (p *Provider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx of.FlattenedContext) of.StringResolutionDetail { + + shouldReturn, returnValue := verifyStateString(p, defaultValue) + if shouldReturn { + return returnValue + } + + prefabContext, err := internal.ToPrefabContext(evalCtx) + if err != nil { + return of.StringResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewInvalidContextResolutionError(err.Error()), + Reason: of.ErrorReason, + }, + } + } + + value, _ := p.PrefabClient.GetStringValueWithDefault(flag, prefabContext, defaultValue) + return of.StringResolutionDetail{ + Value: value, + ProviderResolutionDetail: of.ProviderResolutionDetail{}, + } +} + +func verifyStateString(p *Provider, defaultValue string) (bool, of.StringResolutionDetail) { + if p.status != of.ReadyState { + if p.status == of.NotReadyState { + return true, of.StringResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewProviderNotReadyResolutionError(providerNotReady), + Reason: of.ErrorReason, + }, + } + } else { + return true, of.StringResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError(generalError), + Reason: of.ErrorReason, + }, + } + } + } + return false, of.StringResolutionDetail{} +} + +func (p *Provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { + shouldReturn, returnValue := verifyStateObject(p, defaultValue) + if shouldReturn { + return returnValue + } + + prefabContext, err := internal.ToPrefabContext(evalCtx) + if err != nil { + return of.InterfaceResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewInvalidContextResolutionError(err.Error()), + Reason: of.ErrorReason, + }, + } + } + + var value interface{} + switch castedDefaultValue := defaultValue.(type) { + case map[string]interface{}: + value, _ = p.PrefabClient.GetJSONValueWithDefault(flag, prefabContext, castedDefaultValue) + case []string: + value, _ = p.PrefabClient.GetStringSliceValueWithDefault(flag, prefabContext, castedDefaultValue) + case string: + value, _ = p.PrefabClient.GetStringValueWithDefault(flag, prefabContext, castedDefaultValue) + default: + value = defaultValue + } + + return of.InterfaceResolutionDetail{ + Value: value, + ProviderResolutionDetail: of.ProviderResolutionDetail{}, + } +} + +func verifyStateObject(p *Provider, defaultValue interface{}) (bool, of.InterfaceResolutionDetail) { + if p.status != of.ReadyState { + if p.status == of.NotReadyState { + return true, of.InterfaceResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewProviderNotReadyResolutionError(providerNotReady), + Reason: of.ErrorReason, + }, + } + } else { + return true, of.InterfaceResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError(generalError), + Reason: of.ErrorReason, + }, + } + } + } + return false, of.InterfaceResolutionDetail{} +} diff --git a/providers/prefab/pkg/provider_config.go b/providers/prefab/pkg/provider_config.go new file mode 100644 index 000000000..1481c7bbf --- /dev/null +++ b/providers/prefab/pkg/provider_config.go @@ -0,0 +1,12 @@ +package prefab + +type ProviderConfig struct { + Configs map[string]interface{} + APIKey string + APIURLs []string + Sources []string + // EnvironmentNames []string + // ProjectEnvID int64 + // InitializationTimeoutSeconds float64 + // OnInitializationFailure OnInitializationFailure +} diff --git a/providers/prefab/pkg/provider_test.go b/providers/prefab/pkg/provider_test.go new file mode 100644 index 000000000..ab99eda54 --- /dev/null +++ b/providers/prefab/pkg/provider_test.go @@ -0,0 +1,390 @@ +package prefab_test + +import ( + "context" + "fmt" + "os" + "reflect" + "testing" + + "github.com/open-feature/go-sdk-contrib/providers/prefab/internal" + prefabProvider "github.com/open-feature/go-sdk-contrib/providers/prefab/pkg" + of "github.com/open-feature/go-sdk/openfeature" + prefab "github.com/prefab-cloud/prefab-cloud-go/pkg" + + "github.com/stretchr/testify/require" +) + +const () + +var provider *prefabProvider.Provider +var ofClient *of.Client + +func TestBooleanEvaluation(t *testing.T) { + + flattenedContext := map[string]interface{}{} + + resolution := provider.BooleanEvaluation(context.Background(), "sample_bool", false, flattenedContext) + if resolution.Value != true { + t.Fatalf("Expected one of the variant payloads") + } + + t.Run("evalCtx empty", func(t *testing.T) { + resolution := provider.BooleanEvaluation(context.Background(), "non-existing", false, nil) + require.Equal(t, false, resolution.Value) + }) + + evalCtx := of.NewEvaluationContext( + "", + map[string]interface{}{}, + ) + enabled, err := ofClient.BooleanValue(context.Background(), "sample_bool", false, evalCtx) + require.Nil(t, err) + require.Equal(t, true, enabled) + +} + +// TODO handle conditional feature flags based on context where json/yaml parsing is implemented +// func TestBooleanEvaluationByUser(t *testing.T) { +// providerConfig := prefabProvider.ProviderConfig{ +// Sources: []string{"datafile://enabled.yaml"}, +// } + +// provider, err := prefabProvider.NewProvider(providerConfig) +// require.Nil(t, err) +// err = provider.Init(of.EvaluationContext{}) +// require.Nil(t, err) + +// ctx := context.Background() + +// evalCtx := map[string]interface{}{ +// "user.key": "key1", +// "team.domain": "prefab.cloud", +// "team.description": "team1", +// } + +// resolution := provider.BooleanEvaluation(ctx, "test1", false, evalCtx) +// if resolution.Value != true { +// t.Fatalf("Expected one of the variant payloads") +// } + +// evalCtx = map[string]interface{}{ +// "user.key": "key1", +// "team.domain": "other.com", +// } + +// resolution = provider.BooleanEvaluation(ctx, "test1", false, evalCtx) +// if resolution.Value != false { +// t.Fatalf("Expected false") +// } +// } + +func TestFloatEvaluation(t *testing.T) { + + flattenedContext := map[string]interface{}{} + + resolution := provider.FloatEvaluation(context.Background(), "sample_double", 1.2, flattenedContext) + if resolution.Value != 12.12 { + t.Fatalf("Expected one of the variant payloads") + } + + t.Run("evalCtx empty", func(t *testing.T) { + resolution := provider.FloatEvaluation(context.Background(), "non-existing", 1.2, nil) + require.Equal(t, 1.2, resolution.Value) + }) + + evalCtx := of.NewEvaluationContext( + "", + map[string]interface{}{}, + ) + value, err := ofClient.FloatValue(context.Background(), "sample_double", 1.2, evalCtx) + require.Nil(t, err) + require.Equal(t, 12.12, value) + +} + +func TestIntEvaluation(t *testing.T) { + + flattenedContext := map[string]interface{}{} + + resolution := provider.IntEvaluation(context.Background(), "sample_int", 1, flattenedContext) + if resolution.Value != 123 { + t.Fatalf("Expected one of the variant payloads") + } + + t.Run("evalCtx empty", func(t *testing.T) { + resolution := provider.IntEvaluation(context.Background(), "non-existing", 1, nil) + require.Equal(t, int64(1), resolution.Value) + }) + + evalCtx := of.NewEvaluationContext( + "", + map[string]interface{}{}, + ) + value, err := ofClient.IntValue(context.Background(), "sample_int", 1, evalCtx) + require.Nil(t, err) + require.Equal(t, int64(123), value) + +} + +func TestStringEvaluation(t *testing.T) { + + flattenedContext := map[string]interface{}{} + + resolution := provider.StringEvaluation(context.Background(), "sample", "default", flattenedContext) + if resolution.Value != "test sample value" { + t.Fatalf("Expected one of the variant payloads") + } + + t.Run("nil evalCtx", func(t *testing.T) { + resolution := provider.StringEvaluation(context.Background(), "non-existing", "default", nil) + require.Equal(t, "default", resolution.Value) + }) + + evalCtx := of.NewEvaluationContext( + "", + map[string]interface{}{}, + ) + value, err := ofClient.StringValue(context.Background(), "sample", "default", evalCtx) + require.Nil(t, err) + require.Equal(t, "test sample value", value) + +} + +// TODO test and enable when json/yaml parsing is implemented +func TestObjectEvaluation(t *testing.T) { + + flattenedContext := map[string]interface{}{} + + t.Run("example.nested.path", func(t *testing.T) { + resolution := provider.ObjectEvaluation(context.Background(), "example.nested.path", "default", flattenedContext) + require.Equal(t, "hello", resolution.Value) + }) + + evalCtx := of.NewEvaluationContext( + "", + map[string]interface{}{}, + ) + + t.Run("example.nested.path", func(t *testing.T) { + value, err := ofClient.ObjectValueDetails(context.Background(), "example.nested.path", "default", evalCtx) + require.Equal(t, "hello", value.Value) + require.Nil(t, err) + }) + + t.Run("sample_list", func(t *testing.T) { + value, err := ofClient.ObjectValueDetails(context.Background(), "sample_list", []string{"a2", "b2"}, evalCtx) + require.Equal(t, []string{"a", "b"}, value.Value) + require.Nil(t, err) + }) + + // TODO + // t.Run("sample_json", func(t *testing.T) { + // value, err := ofClient.ObjectValueDetails(context.Background(), "sample_json", map[string]interface{}{ + // "nested": "value", + // }, evalCtx) + // require.Equal(t, []string{"a", "b"}, value.Value) + // require.Nil(t, err) + // }) + +} + +// Converts non-empty FlattenedContext to ContextSet correctly +func TestConvertsNonEmptyFlattenedContextToContextSet(t *testing.T) { + evalCtx := of.FlattenedContext{ + "user.name": "John", + "user.age": 30, + "device.type": "mobile", + } + expected := prefab.NewContextSet() + expected.WithNamedContextValues("user", map[string]interface{}{ + "name": "John", + "age": 30, + }) + expected.WithNamedContextValues("device", map[string]interface{}{ + "type": "mobile", + }) + + result, err := internal.ToPrefabContext(evalCtx) + require.Nil(t, err) + + if !reflect.DeepEqual(result, *expected) { + t.Errorf("expected %v, got %v", *expected, result) + } +} + +func TestUninitializedProviderStates(t *testing.T) { + + flattenedContext := map[string]interface{}{} + + providerConfig := prefabProvider.ProviderConfig{ + Sources: []string{"datafile://enabled.yaml"}, + } + uninitializedProvider, _ := prefabProvider.NewProvider(providerConfig) + + boolRes := uninitializedProvider.BooleanEvaluation(context.Background(), "sample_bool", false, flattenedContext) + require.Equal(t, of.ProviderNotReadyCode, boolRes.ResolutionDetail().ErrorCode) + + intRes := uninitializedProvider.IntEvaluation(context.Background(), "sample_int", 0, flattenedContext) + require.Equal(t, of.ProviderNotReadyCode, intRes.ResolutionDetail().ErrorCode) + + floatRes := uninitializedProvider.FloatEvaluation(context.Background(), "sample_float", 0, flattenedContext) + require.Equal(t, of.ProviderNotReadyCode, floatRes.ResolutionDetail().ErrorCode) + + strRes := uninitializedProvider.StringEvaluation(context.Background(), "sample_string", "default", flattenedContext) + require.Equal(t, of.ProviderNotReadyCode, strRes.ResolutionDetail().ErrorCode) + + objRes := uninitializedProvider.ObjectEvaluation(context.Background(), "sample_string", "default", flattenedContext) + require.Equal(t, of.ProviderNotReadyCode, objRes.ResolutionDetail().ErrorCode) +} + +func TestErrorProviderStates(t *testing.T) { + + flattenedContext := map[string]interface{}{} + + providerConfig := prefabProvider.ProviderConfig{ + Sources: []string{"datafile://non-existing.yaml"}, + } + errorProvider, _ := prefabProvider.NewProvider(providerConfig) + errorProvider.Init(of.EvaluationContext{}) + + boolRes := errorProvider.BooleanEvaluation(context.Background(), "sample_bool", false, flattenedContext) + require.Equal(t, of.GeneralCode, boolRes.ResolutionDetail().ErrorCode) + + intRes := errorProvider.IntEvaluation(context.Background(), "sample_int", 0, flattenedContext) + require.Equal(t, of.GeneralCode, intRes.ResolutionDetail().ErrorCode) + + floatRes := errorProvider.FloatEvaluation(context.Background(), "sample_float", 0, flattenedContext) + require.Equal(t, of.GeneralCode, floatRes.ResolutionDetail().ErrorCode) + + strRes := errorProvider.StringEvaluation(context.Background(), "sample_string", "default", flattenedContext) + require.Equal(t, of.GeneralCode, strRes.ResolutionDetail().ErrorCode) + + objRes := errorProvider.ObjectEvaluation(context.Background(), "sample_string", "default", flattenedContext) + require.Equal(t, of.GeneralCode, objRes.ResolutionDetail().ErrorCode) + + providerConfig = prefabProvider.ProviderConfig{} + errorProvider, _ = prefabProvider.NewProvider(providerConfig) + errorProvider.Init(of.EvaluationContext{}) +} + +func TestEvaluationMethods(t *testing.T) { + err := of.SetProvider(provider) + require.Nil(t, err) + + evalCtx := of.NewEvaluationContext( + "", + map[string]interface{}{ + "user.id": "123", + }, + ) + + tests := []struct { + flag string + defaultValue interface{} + evalCtx of.EvaluationContext + expected interface{} + expectedErrorCode of.ErrorCode + }{ + {"sample_bool", false, evalCtx, true, ""}, + {"sample_double", 0.0, evalCtx, 12.12, ""}, + {"sample_int", int64(42999), evalCtx, int64(123), ""}, + {"sample", "default_value", evalCtx, "test sample value", ""}, + // TODO + // {"flag_with_a_value", map[string]interface{}{"key": "value999"}, evalCtx, map[string]interface{}{"key1": "value1"}, ""}, + {"sample_list", []string{"fallback1", "fallback2"}, evalCtx, []string{"a", "b"}, ""}, + {"invalid_user_context_bool", false, of.NewEvaluationContext( + "", + map[string]interface{}{ + "invalid": "123", + }, + ), false, of.InvalidContextCode}, + {"invalid_user_context_int", int64(43), of.NewEvaluationContext( + "", + map[string]interface{}{ + "invalid": "123", + }, + ), int64(43), of.InvalidContextCode}, + {"invalid_user_context_float", 1.2, of.NewEvaluationContext( + "", + map[string]interface{}{ + "invalid": "123", + }, + ), 1.2, of.InvalidContextCode}, + {"invalid_user_context_string", "a", of.NewEvaluationContext( + "", + map[string]interface{}{ + "invalid": "123", + }, + ), "a", of.InvalidContextCode}, + // {"invalid_user_context_object", "a", of.NewEvaluationContext( + // "", + // map[string]interface{}{ + // "invalid": "123", + // }, + // ), "a", of.InvalidContextCode}, + {"empty_context", int64(43), evalCtx, int64(43), ""}, + } + + for _, test := range tests { + fmt.Println("test: {}", test) + rt := reflect.TypeOf(test.expected) + switch rt.Kind() { + case reflect.Bool: + res, _ := ofClient.BooleanValueDetails(context.Background(), test.flag, test.defaultValue.(bool), test.evalCtx) + require.Equal(t, test.expected, res.Value) + require.Equal(t, test.expectedErrorCode, res.ErrorCode) + case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64: + res, _ := ofClient.IntValueDetails(context.Background(), test.flag, test.defaultValue.(int64), test.evalCtx) + require.Equal(t, test.expected, res.Value) + require.Equal(t, test.expectedErrorCode, res.ErrorCode) + case reflect.Float32, reflect.Float64: + res, _ := ofClient.FloatValueDetails(context.Background(), test.flag, test.defaultValue.(float64), test.evalCtx) + require.Equal(t, test.expected, res.Value) + require.Equal(t, test.expectedErrorCode, res.ErrorCode) + case reflect.String: + res, _ := ofClient.StringValueDetails(context.Background(), test.flag, test.defaultValue.(string), test.evalCtx) + require.Equal(t, test.expected, res.Value) + require.Equal(t, test.expectedErrorCode, res.ErrorCode) + default: + res, _ := ofClient.ObjectValueDetails(context.Background(), test.flag, test.defaultValue, test.evalCtx) + require.Equal(t, test.expected, res.Value) + require.Equal(t, test.expectedErrorCode, res.ErrorCode) + } + } +} + +func cleanup() { + provider.Shutdown() +} + +func TestMain(m *testing.M) { + + providerConfig := prefabProvider.ProviderConfig{ + Sources: []string{"datafile://enabled.yaml"}, + } + + var err error + provider, err = prefabProvider.NewProvider(providerConfig) + if err != nil { + fmt.Printf("Error during new provider: %v\n", err) + os.Exit(1) + } + err = provider.Init(of.EvaluationContext{}) + if err != nil { + fmt.Printf("Error during provider init: %v\n", err) + os.Exit(1) + } + + of.SetProvider(provider) + ofClient = of.NewClient("my-app") + + fmt.Printf("provider: %v\n", provider) + + // Run the tests + exitCode := m.Run() + + cleanup() + + os.Exit(exitCode) +} diff --git a/release-please-config.json b/release-please-config.json index 729787cfa..cd8110bae 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -108,6 +108,14 @@ "versioning": "default", "extra-files": [] }, + "providers/prefab": { + "release-type": "go", + "package-name": "providers/prefab", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [] + }, "providers/ofrep": { "release-type": "go", "package-name": "providers/ofrep",