Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Total confidence #48

Merged
merged 10 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,31 @@ jobs:
with:
go-version: '1.20'

- name: Build
run: cd pkg/provider && go build -v .
- name: Build Provider
run: cd provider && go build -v .

- name: Test
run: cd pkg/provider && go test -v
- name: Build Confidence
run: cd confidence && go build -v .

- name: Test Provider
run: cd provider && go test -v

- name: Test Confidence
run: cd confidence && go test -v
- name: Run gofmt
run: |
modules=("confidence" "demo" "demo-open-feature" "provider")
fmt_issues=""
for module in "${modules[@]}"; do
fmt_output=$(gofmt -l "$module")
if [ -n "$fmt_output" ]; then
fmt_issues+="$fmt_output"$'\n'
fi
done

if [ -n "$fmt_issues" ]; then
echo "The following files are not properly formatted:"
echo "$fmt_issues"
echo "Please run 'gofmt -w .' in the respective module directories to format your code."
exit 1
fi
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Default ignored files
*/shelf/
*/workspace.xml
# Editor-based HTTP Client requests
*/httpRequests/
# Datasource local storage ignored files
*/dataSources/
*/dataSources.local.xml
*/vcs.xmlg
*/.fleet
33 changes: 33 additions & 0 deletions confidence/EventUploader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package confidence

import (
"bytes"
"context"
"encoding/json"
"net/http"
)

type EventUploader struct {
client http.Client
config APIConfig
}

func (e EventUploader) upload(ctx context.Context, request EventBatchRequest) {
jsonRequest, err := json.Marshal(request)
if err != nil {
return
}

payload := bytes.NewBuffer(jsonRequest)
req, err := http.NewRequestWithContext(ctx,
http.MethodPost, "https://events.eu.confidence.dev/v1/events:publish", payload)
if err != nil {
return
}

resp, err := e.client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
}
218 changes: 218 additions & 0 deletions confidence/confidence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package confidence

import (
"context"
"fmt"
"net/http"
"reflect"
"strings"
"sync"
"time"
)

type FlagResolver interface {
resolveFlag(ctx context.Context, flag string, defaultValue interface{},
evalCtx map[string]interface{}, expectedKind reflect.Kind) InterfaceResolutionDetail
}

type ContextProvider interface {
GetContext() map[string]interface{}
}

var (
SDK_ID = "SDK_ID_GO_CONFIDENCE"
SDK_VERSION = "0.1.8" // x-release-please-version
)

type Confidence struct {
parent ContextProvider
uploader EventUploader
contextMap map[string]interface{}
Config APIConfig
ResolveClient ResolveClient
}

func (e Confidence) GetContext() map[string]interface{} {
currentMap := map[string]interface{}{}
parentMap := make(map[string]interface{})
if e.parent != nil {
parentMap = e.parent.GetContext()
}
for key, value := range parentMap {
currentMap[key] = value
}
for key, value := range e.contextMap {
if value == nil {
delete(currentMap, key)
} else {
currentMap[key] = value
}
}
return currentMap
}

type ConfidenceBuilder struct {
confidence Confidence
}

func (e ConfidenceBuilder) SetAPIConfig(config APIConfig) ConfidenceBuilder {
e.confidence.Config = config
return e
}

func (e ConfidenceBuilder) SetResolveClient(client ResolveClient) ConfidenceBuilder {
e.confidence.ResolveClient = client
return e
}

func (e ConfidenceBuilder) Build() Confidence {
if e.confidence.ResolveClient == nil {
e.confidence.ResolveClient = HttpResolveClient{Client: &http.Client{}, Config: e.confidence.Config}
}
e.confidence.contextMap = make(map[string]interface{})
return e.confidence
}

func NewConfidenceBuilder() ConfidenceBuilder {
return ConfidenceBuilder{
confidence: Confidence{},
}
}

func (e Confidence) PutContext(key string, value interface{}) {
e.contextMap[key] = value
nicklasl marked this conversation as resolved.
Show resolved Hide resolved
}

func (e Confidence) Track(ctx context.Context, eventName string, message map[string]interface{}) *sync.WaitGroup {
newMap := e.GetContext()

for key, value := range message {
newMap[key] = value
}

var wg sync.WaitGroup
wg.Add(1)
go func() {
currentTime := time.Now()
iso8601Time := currentTime.Format(time.RFC3339)
event := Event{
EventDefinition: fmt.Sprintf("eventDefinitions/%s", eventName),
EventTime: iso8601Time,
Payload: newMap,
}
batch := EventBatchRequest{
CclientSecret: e.Config.APIKey,
Sdk: sdk{SDK_ID, SDK_VERSION},
SendTime: iso8601Time,
Events: []Event{event},
}
e.uploader.upload(ctx, batch)
wg.Done()
}()
return &wg
}

func (e Confidence) WithContext(context map[string]interface{}) Confidence {
newMap := map[string]interface{}{}
for key, value := range e.GetContext() {
newMap[key] = value
}

for key, value := range context {
newMap[key] = value
}

return Confidence{
parent: &e,
contextMap: newMap,
Config: e.Config,
ResolveClient: e.ResolveClient,
}
}

func (e Confidence) GetBoolFlag(ctx context.Context, flag string, defaultValue bool) BoolResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Bool)
return ToBoolResolutionDetail(resp, defaultValue)
}

func (e Confidence) GetBoolValue(ctx context.Context, flag string, defaultValue bool) bool {
return e.GetBoolFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) GetIntFlag(ctx context.Context, flag string, defaultValue int64) IntResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Int64)
return ToIntResolutionDetail(resp, defaultValue)
}

func (e Confidence) GetIntValue(ctx context.Context, flag string, defaultValue int64) int64 {
return e.GetIntFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) GetDoubleFlag(ctx context.Context, flag string, defaultValue float64) FloatResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Float64)
return ToFloatResolutionDetail(resp, defaultValue)
}

func (e Confidence) GetDoubleValue(ctx context.Context, flag string, defaultValue float64) float64 {
return e.GetDoubleFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) GetStringFlag(ctx context.Context, flag string, defaultValue string) StringResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.String)
return ToStringResolutionDetail(resp, defaultValue)
}

func (e Confidence) GetStringValue(ctx context.Context, flag string, defaultValue string) string {
return e.GetStringFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) GetObjectFlag(ctx context.Context, flag string, defaultValue string) InterfaceResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Map)
return resp
}

func (e Confidence) GetObjectValue(ctx context.Context, flag string, defaultValue string) interface{} {
return e.GetObjectFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) ResolveFlag(ctx context.Context, flag string, defaultValue interface{}, expectedKind reflect.Kind) InterfaceResolutionDetail {
flagName, propertyPath := splitFlagString(flag)

requestFlagName := fmt.Sprintf("flags/%s", flagName)
resp, err := e.ResolveClient.SendResolveRequest(ctx,
ResolveRequest{ClientSecret: e.Config.APIKey,
Flags: []string{requestFlagName}, Apply: true, EvaluationContext: e.contextMap,
Sdk: sdk{Id: SDK_ID, Version: SDK_VERSION}})

if err != nil {
return processResolveError(err, defaultValue)
}
if len(resp.ResolvedFlags) == 0 {
return InterfaceResolutionDetail{
Value: defaultValue,
ResolutionDetail: ResolutionDetail{
Variant: "",
Reason: ErrorReason,
ErrorCode: FlagNotFoundCode,
ErrorMessage: "Flag not found",
FlagMetadata: nil,
},
}
}

resolvedFlag := resp.ResolvedFlags[0]
if resolvedFlag.Flag != requestFlagName {
return InterfaceResolutionDetail{
Value: defaultValue,
ResolutionDetail: ResolutionDetail{
Variant: "",
Reason: ErrorReason,
ErrorCode: FlagNotFoundCode,
ErrorMessage: fmt.Sprintf("unexpected flag '%s' from remote", strings.TrimPrefix(resolvedFlag.Flag, "flags/")),
FlagMetadata: nil,
},
}
}

return processResolvedFlag(resolvedFlag, defaultValue, expectedKind, propertyPath)
}
50 changes: 50 additions & 0 deletions confidence/confidence_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package confidence

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestContextIsInConfidenceObject(t *testing.T) {
client := create_confidence(t, templateResponse())
client.PutContext("hello", "hey")
assert.Equal(t, client.GetContext(), map[string]interface{}{"hello": "hey"})
}

func TestWithContextIsInChildContext(t *testing.T) {
client := create_confidence(t, templateResponse())
client.PutContext("hello", "hey")
child := client.WithContext(map[string]interface{}{"west": "world"})
assert.Equal(t, child.GetContext(), map[string]interface{}{"hello": "hey", "west": "world"})
client.PutContext("hello2", "hey2")
assert.Equal(t, child.GetContext(), map[string]interface{}{"hello": "hey", "west": "world", "hello2": "hey2"})
}

func TestChildContextOverrideParentContext(t *testing.T) {
client := create_confidence(t, templateResponse())
client.PutContext("hello", "hey")
child := client.WithContext(map[string]interface{}{"hello": "boom"})
assert.Equal(t, child.GetContext(), map[string]interface{}{"hello": "boom"})
assert.Equal(t, client.GetContext(), map[string]interface{}{"hello": "hey"})
}

func TestChildContextRemoveParentContext(t *testing.T) {
client := create_confidence(t, templateResponse())
client.PutContext("hello", "hey")
child := client.WithContext(map[string]interface{}{})
child.PutContext("hello", nil)
assert.Equal(t, child.GetContext(), map[string]interface{}{})
}

func create_confidence(t *testing.T, response ResolveResponse) *Confidence {
config := APIConfig{
APIKey: "apiKey",
Region: APIRegionGlobal,
}
return &Confidence{
Config: config,
ResolveClient: MockResolveClient{MockedResponse: response, MockedError: nil, TestingT: t},
contextMap: make(map[string]interface{}),
}
}
Loading
Loading