diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3da5977..a1fcf5e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ defaults: jobs: lint: - name: Lint files + name: Lint runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v3 @@ -24,36 +24,16 @@ jobs: uses: golangci/golangci-lint-action@v3 with: version: latest - - build: - name: Build binary + test: runs-on: 'ubuntu-latest' - strategy: - matrix: - goosarch: - - 'darwin/amd64' - - 'darwin/arm64' - - 'linux/amd64' - - 'linux/arm' - - 'linux/arm64' - steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - name: Setup + uses: actions/setup-go@v3 with: go-version: '1.17' check-latest: true - - name: Get OS and arch info - run: | - GOOSARCH=${{matrix.goosarch}} - GOOS=${GOOSARCH%/*} - GOARCH=${GOOSARCH#*/} - BINARY_NAME=${{github.repository}}-$GOOS-$GOARCH - echo "BINARY_NAME=$BINARY_NAME" >> $GITHUB_ENV - echo "GOOS=$GOOS" >> $GITHUB_ENV - echo "GOARCH=$GOARCH" >> $GITHUB_ENV - - name: Build + - name: Test run: | - cd pkg/experiment - go build -o "$BINARY_NAME" -v + go test ./... diff --git a/Makefile b/Makefile index 0f45172..e404b79 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,10 @@ xpmt: clean - CGO_ENABLED=1 go build -o xpmt cmd/xpmt/main.go + go build -o xpmt cmd/xpmt/main.go docker: docker build -t experiment . --progress plain docker run -it --rm --name experiment-run experiment -release: copy-lib-release xpmt - -debug: copy-lib-debug xpmt - clean: rm -f xpmt @@ -19,44 +15,13 @@ darwin: darwin-amd64 darwin-arm64 linux: linux-amd64 linux-arm64 darwin-amd64: - CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o cmd/xpmt/bin/darwin/amd64/xpmt cmd/xpmt/main.go + go build -o cmd/xpmt/bin/darwin/amd64/xpmt cmd/xpmt/main.go darwin-arm64: - CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o cmd/xpmt/bin/darwin/arm64/xpmt cmd/xpmt/main.go + go build -o cmd/xpmt/bin/darwin/arm64/xpmt cmd/xpmt/main.go linux-amd64: - CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o cmd/xpmt/bin/linux/amd64/xpmt cmd/xpmt/main.go + go build -o cmd/xpmt/bin/linux/amd64/xpmt cmd/xpmt/main.go linux-arm64: - CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -o cmd/xpmt/bin/linux/arm64/xpmt cmd/xpmt/main.go - -# expects experiment-evaluation lives in same directory and experiment-go-server -copy-lib-debug: - # macosX64 - cp ../experiment-evaluation/evaluation-interop/build/bin/macosX64/debugStatic/libevaluation_interop_api.h internal/evaluation/lib/macosX64/ - cp ../experiment-evaluation/evaluation-interop/build/bin/macosX64/debugStatic/libevaluation_interop.a internal/evaluation/lib/macosX64/ - # macosArm64 - cp ../experiment-evaluation/evaluation-interop/build/bin/macosArm64/debugStatic/libevaluation_interop_api.h internal/evaluation/lib/macosArm64/ - cp ../experiment-evaluation/evaluation-interop/build/bin/macosArm64/debugStatic/libevaluation_interop.a internal/evaluation/lib/macosArm64/ - # linuxX64 - cp ../experiment-evaluation/evaluation-interop/build/bin/linuxX64/debugStatic/libevaluation_interop_api.h internal/evaluation/lib/linuxX64/ - cp ../experiment-evaluation/evaluation-interop/build/bin/linuxX64/debugStatic/libevaluation_interop.a internal/evaluation/lib/linuxX64/ - # linuxArm64 - cp ../experiment-evaluation/evaluation-interop/build/bin/linuxArm64/debugStatic/libevaluation_interop_api.h internal/evaluation/lib/linuxArm64/ - cp ../experiment-evaluation/evaluation-interop/build/bin/linuxArm64/debugStatic/libevaluation_interop.a internal/evaluation/lib/linuxArm64/ - - -# expects experiment-evaluation lives in same directory and experiment-go-server -copy-lib-release: - # macosX64 - cp ../experiment-evaluation/evaluation-interop/build/bin/macosX64/releaseStatic/libevaluation_interop_api.h internal/evaluation/lib/macosX64/ - cp ../experiment-evaluation/evaluation-interop/build/bin/macosX64/releaseStatic/libevaluation_interop.a internal/evaluation/lib/macosX64/ - # macosArm64 - cp ../experiment-evaluation/evaluation-interop/build/bin/macosArm64/releaseStatic/libevaluation_interop_api.h internal/evaluation/lib/macosArm64/ - cp ../experiment-evaluation/evaluation-interop/build/bin/macosArm64/releaseStatic/libevaluation_interop.a internal/evaluation/lib/macosArm64/ - # linuxX64 - cp ../experiment-evaluation/evaluation-interop/build/bin/linuxX64/releaseStatic/libevaluation_interop_api.h internal/evaluation/lib/linuxX64/ - cp ../experiment-evaluation/evaluation-interop/build/bin/linuxX64/releaseStatic/libevaluation_interop.a internal/evaluation/lib/linuxX64/ - # linuxArm64 - cp ../experiment-evaluation/evaluation-interop/build/bin/linuxArm64/releaseStatic/libevaluation_interop_api.h internal/evaluation/lib/linuxArm64/ - cp ../experiment-evaluation/evaluation-interop/build/bin/linuxArm64/releaseStatic/libevaluation_interop.a internal/evaluation/lib/linuxArm64/ + go build -o cmd/xpmt/bin/linux/arm64/xpmt cmd/xpmt/main.go diff --git a/README.md b/README.md index e62e02c..300c716 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,14 @@ The `xpmt` command-line interface tool allows you to make Experiment SDK calls f ### Build -**Makefile currently only builds for macos (amd64 & x64), add a line to the `Makefile` to support your OS and Architecture.** - ``` make xpmt ``` ### Run -!!!warning Setting the deployment key - All examples below assume the `EXPERIMENT_KEY` environment variable has been set. Alternatively, use the `-k` - flag to set the key in the command. +> **Warning** All examples below assume the `EXPERIMENT_KEY` environment variable has been set. Alternatively, use the `-k` +flag to set the key in the command. #### Subcommands * `fetch`: fetch variants for a user from the server @@ -112,4 +109,4 @@ Fetch variants for a user given an experiment user JSON object ./xpmt evaluate -u '{"user_id":"user@company.com","user_properties":{"premium":true}}' ``` -> Note: must use single quotes around JSON object string \ No newline at end of file +> Note: must use single quotes around JSON object string diff --git a/cmd/xpmt/main.go b/cmd/xpmt/main.go index e227f22..9d3d656 100644 --- a/cmd/xpmt/main.go +++ b/cmd/xpmt/main.go @@ -5,7 +5,6 @@ import ( "flag" "fmt" "os" - "strconv" "time" "github.com/amplitude/experiment-go-server/pkg/experiment" @@ -19,7 +18,6 @@ func main() { fmt.Printf("Available commands:\n" + " fetch\n" + " flags\n" + - " rules\n" + " evaluate\n") return } @@ -28,8 +26,6 @@ func main() { fetch() case "flags": flags() - case "rules": - rules() case "evaluate": evaluate() default: @@ -141,59 +137,13 @@ func flags() { } client := local.Initialize(*apiKey, config) - flags, err := client.Flags() + flags, err := client.FlagsV2() if err != nil { fmt.Printf("error: %v\n", err) os.Exit(1) return } - println(*flags) -} - -func rules() { - rulesCmd := flag.NewFlagSet("rules", flag.ExitOnError) - apiKey := rulesCmd.String("k", "", "Api key for authorization, or use EXPERIMENT_KEY env var.") - url := rulesCmd.String("url", "", "The server url to use to fetch variants from.") - debug := rulesCmd.Bool("debug", false, "Log additional debug output to std out.") - staging := rulesCmd.Bool("staging", false, "Use skylab staging environment.") - _ = rulesCmd.Parse(os.Args[2:]) - - if len(os.Args) == 3 && os.Args[1] == "--help" { - rulesCmd.Usage() - return - } - - if apiKey == nil || *apiKey == "" { - envKey := os.Getenv("EXPERIMENT_KEY") - if envKey == "" { - rulesCmd.Usage() - fmt.Printf("error: must set experiment api key using cli flag or EXPERIMENT_KEY env var\n") - os.Exit(1) - return - } - apiKey = &envKey - } - - config := &local.Config{ - Debug: *debug, - } - - if *url != "" { - config.ServerUrl = *url - } else if *staging { - config.ServerUrl = "https://skylab-api.staging.amplitude.com" - } - - client := local.Initialize(*apiKey, config) - variants, err := client.Rules() - if err != nil { - fmt.Printf("error: %v\n", err) - os.Exit(1) - return - } - b, _ := json.Marshal(variants) - st, _ := strconv.Unquote(string(b)) - fmt.Println(st) + println(flags) } func evaluate() { @@ -270,7 +220,7 @@ func evaluate() { // fmt.Println(duration) //} - variants, err := client.Evaluate(user, nil) + variants, err := client.EvaluateV2(user, nil) if err != nil { fmt.Printf("error: %v\n", err) os.Exit(1) diff --git a/go.mod b/go.mod index 05bb72a..d282c4d 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,6 @@ module github.com/amplitude/experiment-go-server go 1.12 +require github.com/spaolacci/murmur3 v1.1.0 + require github.com/amplitude/analytics-go v1.0.1 diff --git a/go.sum b/go.sum index c239cf9..40e774e 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= diff --git a/internal/evaluation/context.go b/internal/evaluation/context.go new file mode 100644 index 0000000..62d4147 --- /dev/null +++ b/internal/evaluation/context.go @@ -0,0 +1,61 @@ +package evaluation + +import "github.com/amplitude/experiment-go-server/pkg/experiment" + +func UserToContext(user *experiment.User) map[string]interface{} { + if user == nil { + return nil + } + context := make(map[string]interface{}) + userMap := make(map[string]interface{}) + if len(user.UserId) != 0 { + userMap["user_id"] = user.UserId + } + if len(user.DeviceId) != 0 { + userMap["device_id"] = user.DeviceId + } + if len(user.Country) != 0 { + userMap["country"] = user.Country + } + if len(user.Region) != 0 { + userMap["region"] = user.Region + } + if len(user.Dma) != 0 { + userMap["dma"] = user.Dma + } + if len(user.City) != 0 { + userMap["city"] = user.City + } + if len(user.Language) != 0 { + userMap["language"] = user.Language + } + if len(user.Platform) != 0 { + userMap["platform"] = user.Platform + } + if len(user.Version) != 0 { + userMap["version"] = user.Version + } + if len(user.Os) != 0 { + userMap["os"] = user.Os + } + if len(user.DeviceManufacturer) != 0 { + userMap["device_manufacturer"] = user.DeviceManufacturer + } + if len(user.DeviceBrand) != 0 { + userMap["device_brand"] = user.DeviceBrand + } + if len(user.DeviceModel) != 0 { + userMap["device_model"] = user.DeviceModel + } + if len(user.Carrier) != 0 { + userMap["carrier"] = user.Carrier + } + if len(user.Library) != 0 { + userMap["library"] = user.Library + } + if len(user.UserProperties) != 0 { + userMap["user_properties"] = user.UserProperties + } + context["user"] = userMap + return context +} diff --git a/internal/evaluation/engine.go b/internal/evaluation/engine.go new file mode 100644 index 0000000..12797b2 --- /dev/null +++ b/internal/evaluation/engine.go @@ -0,0 +1,505 @@ +package evaluation + +import ( + "encoding/json" + "fmt" + "github.com/amplitude/experiment-go-server/internal/logger" + "github.com/spaolacci/murmur3" + "reflect" + "regexp" + "strconv" + "strings" +) + +type Engine struct { + log *logger.Log +} + +type target struct { + context map[string]interface{} + result map[string]Variant +} + +func NewEngine(log *logger.Log) *Engine { + return &Engine{log} +} + +func (e *Engine) Evaluate(context map[string]interface{}, flags []*Flag) map[string]Variant { + e.log.Debug("Evaluating %v flags with context %v", len(flags), context) + results := make(map[string]Variant) + target := &target{context, results} + for _, flag := range flags { + // Evaluate flag and update results + variant := e.evaluateFlag(target, flag) + if variant != nil { + results[flag.Key] = *variant + } else { + e.log.Debug("Flag %v evaluation returned nil result", flag.Key) + } + } + e.log.Debug("Evaluation completed. %v", results) + return results +} + +func (e *Engine) evaluateFlag(target *target, flag *Flag) *Variant { + e.log.Verbose("Evaluating flag %v with target %v", flag, target) + var result *Variant + for _, segment := range flag.Segments { + result = e.evaluateSegment(target, flag, segment) + if result != nil { + // Merge all metadata into the result + metadata := mergeMetadata([]map[string]interface{}{flag.Metadata, segment.Metadata, result.Metadata}) + result = &Variant{result.Key, result.Value, result.Payload, metadata} + e.log.Verbose("Flag evaluation returned result %v on segment %v", result, segment) + break + } + } + return result +} + +func (e *Engine) evaluateSegment(target *target, flag *Flag, segment *Segment) *Variant { + e.log.Verbose("Evaluating segment %v with target %") + if segment.Conditions == nil { + e.log.Verbose("Segment conditions are nil, bucketing target") + // Null conditions always match + variantKey := e.bucket(target, segment) + return flag.Variants[variantKey] + } + // Outer list logic is "or" (||) + for _, conditions := range segment.Conditions { + match := true + // Inner list logic is "and" (&&) + for _, condition := range conditions { + match = e.matchCondition(target, condition) + if !match { + e.log.Verbose("Segment condition %v did not match target", condition) + break + } else { + e.log.Verbose("Segment condition %v matched target", condition) + } + } + // On match bucket the user + if match { + e.log.Verbose("Segment conditions matched, bucketing target") + variantKey := e.bucket(target, segment) + return flag.Variants[variantKey] + } + } + return nil +} + +func (e *Engine) matchCondition(target *target, condition *Condition) bool { + propValue := selectEach(target, condition.Selector) + // We need special matching for null properties and set type prop values + // and operators. All other values are matched as strings, since the + // filter values are always strings. + if propValue == nil { + return matchNull(condition.Op, condition.Values) + } else if isSetOperator(condition.Op) { + propValueStringList, err := coerceStringList(propValue) + if err != nil { + return false + } + return matchSet(propValueStringList, condition.Op, condition.Values) + } else { + propValueString := coerceString(propValue) + if propValueString == nil { + return false + } + return matchString(*propValueString, condition.Op, condition.Values) + } +} + +func (e *Engine) getHash(key string) uint64 { + return uint64(murmur3.Sum32WithSeed([]byte(key), 0)) +} + +func (e *Engine) bucket(target *target, segment *Segment) string { + e.log.Verbose("Bucketing segment %v with target %v", segment, target) + if segment.Bucket == nil { + // A nil bucket means the segment is fully rolled out. Select the default variant. + e.log.Verbose("Segment bucket is nil, returning default variant %v", segment.Variant) + return segment.Variant + } + // Select the bucketing value + bucketingValue := coerceString(selectEach(target, segment.Bucket.Selector)) + e.log.Verbose("Selected bucketing value %v from target", bucketingValue) + if bucketingValue == nil || len(*bucketingValue) == 0 { + // A nil or empty bucketing value cannot be bucketed. Select the default variant. + e.log.Verbose("Selected bucketing value is nil or empty") + return segment.Variant + } + // Salt and hash the value, and compute the allocation and distribution values. + keyToHash := fmt.Sprintf("%v/%v", segment.Bucket.Salt, *bucketingValue) + hash := e.getHash(keyToHash) + allocationValue := hash % 100 + distributionValue := hash / 100 + for _, allocation := range segment.Bucket.Allocations { + allocationStart := allocation.Range[0] + allocationEnd := allocation.Range[1] + if allocationValue >= allocationStart && allocationValue < allocationEnd { + for _, distribution := range allocation.Distributions { + distributionStart := distribution.Range[0] + distributionEnd := distribution.Range[1] + if distributionValue >= distributionStart && distributionValue < distributionEnd { + e.log.Verbose("Bucketing hit allocation and distribution, returning variant %v", distribution.Variant) + return distribution.Variant + } + } + } + } + return segment.Variant +} + +func mergeMetadata(metadata []map[string]interface{}) map[string]interface{} { + mergedMetadata := make(map[string]interface{}) + for _, m := range metadata { + for k, v := range m { + mergedMetadata[k] = v + } + } + if len(mergedMetadata) == 0 { + return nil + } else { + return mergedMetadata + } +} + +func matchNull(op string, filterValues []string) bool { + containsNone := containsNone(filterValues) + switch op { + case OpIs, OpContains, OpLessThan, OpLessThanEquals, OpGreaterThan, + OpGreaterThanEquals, OpVersionLessThan, OpVersionLessThanEquals, + OpVersionGreaterThan, OpVersionGreaterThanEquals, OpSetIs, OpSetContains, + OpSetContainsAny: + return containsNone + case OpIsNot, OpDoesNotContain, OpSetDoesNotContain, OpSetDoesNotContainAny: + return !containsNone + case OpRegexMatch: + return false + case OpRegexDoesNotMatch, OpSetIsNot: + return true + default: + return false + } +} + +func matchSet(propValues []string, op string, filterValues []string) bool { + switch op { + case OpSetIs: + return matchesSetIs(propValues, filterValues) + case OpSetIsNot: + return !matchesSetIs(propValues, filterValues) + case OpSetContains: + return matchesSetContainsAll(propValues, filterValues) + case OpSetDoesNotContain: + return !matchesSetContainsAll(propValues, filterValues) + case OpSetContainsAny: + return matchesSetContainsAny(propValues, filterValues) + case OpSetDoesNotContainAny: + return !matchesSetContainsAny(propValues, filterValues) + default: + return false + } +} + +func matchString(propValue string, op string, filterValues []string) bool { + switch op { + case OpIs: + return matchesIs(propValue, filterValues) + case OpIsNot: + return !matchesIs(propValue, filterValues) + case OpContains: + return matchesContains(propValue, filterValues) + case OpDoesNotContain: + return !matchesContains(propValue, filterValues) + case OpLessThan, OpLessThanEquals, OpGreaterThan, OpGreaterThanEquals: + return compare(propValue, op, filterValues) + case OpVersionLessThan, OpVersionLessThanEquals, OpVersionGreaterThan, + OpVersionGreaterThanEquals: + return compareVersion(propValue, op, filterValues) + case OpRegexMatch: + return matchesRegex(propValue, filterValues) + case OpRegexDoesNotMatch: + return !matchesRegex(propValue, filterValues) + default: + return false + } +} + +func matchesIs(propValue string, filterValues []string) bool { + if containsBooleans(filterValues) { + propValueLower := strings.ToLower(propValue) + if propValueLower == "true" || propValueLower == "false" { + for _, filterValue := range filterValues { + filterValueLower := strings.ToLower(filterValue) + if propValueLower == filterValueLower { + return true + } + } + } + } + for _, filterValue := range filterValues { + if filterValue == propValue { + return true + } + } + return false +} + +func matchesContains(propValue string, filterValues []string) bool { + for _, filterValue := range filterValues { + propValueLower := strings.ToLower(propValue) + filterValueLower := strings.ToLower(filterValue) + if strings.Contains(propValueLower, filterValueLower) { + return true + } + } + return false +} + +func matchesSetIs(propValues, filterValues []string) bool { + if propValues == nil && filterValues == nil { + return true + } else if propValues == nil || filterValues == nil { + return false + } + m1 := make(map[string]bool) + m2 := make(map[string]bool) + maxLen := len(filterValues) + if len(propValues) > len(filterValues) { + maxLen = len(propValues) + } + for i := 0; i < maxLen; i++ { + if i < len(propValues) { + m1[propValues[i]] = true + } + if i < len(filterValues) { + m2[filterValues[i]] = true + } + } + if len(m1) != len(m2) { + return false + } + for k := range m1 { + if _, ok := m2[k]; !ok { + return false + } + } + return true +} + +func matchesSetContainsAll(propValues []string, filterValues []string) bool { + for _, filterValue := range filterValues { + if !matchesIs(filterValue, propValues) { + return false + } + } + return true +} + +func matchesSetContainsAny(propValues []string, filterValues []string) bool { + for _, filterValue := range filterValues { + if matchesIs(filterValue, propValues) { + return true + } + } + return false +} + +func compare(propValue string, op string, filterValues []string) bool { + // Attempt to parse the propValue as a number + propValueNumber, err := strconv.ParseFloat(propValue, 64) + if err == nil { + // Attempt to parse filterValues as numbers + var filterValueNumbers []float64 + for _, filterValue := range filterValues { + filterValueNumber, err := strconv.ParseFloat(filterValue, 64) + if err != nil { + continue + } + filterValueNumbers = append(filterValueNumbers, filterValueNumber) + } + if filterValueNumbers != nil { + // Prop value and at least one filter value can be compared as numbers + return compareNumber(propValueNumber, op, filterValueNumbers) + } + } + + // Compare strings + return compareString(propValue, op, filterValues) +} + +func compareVersion(propValue string, op string, filterValues []string) bool { + // Attempt to parse the propValue as a version + propValueVersion := parseVersion(propValue) + if propValueVersion == nil { + // Fall back on string comparison + return compareString(propValue, op, filterValues) + } + // Attempt to parse filterValues as versions + var filterValueVersions []version + for _, filterValue := range filterValues { + filterValueVersion := parseVersion(filterValue) + if filterValueVersion == nil { + continue + } + filterValueVersions = append(filterValueVersions, *filterValueVersion) + } + if filterValueVersions == nil { + // Fall back on string comparison + return compareString(propValue, op, filterValues) + } + // Prop value and at least one filter value can be compared as versions + for _, filterValueVersion := range filterValueVersions { + compareResult := versionCompare(*propValueVersion, filterValueVersion) + var result bool + switch op { + case OpVersionLessThan: + result = compareResult < 0 + case OpVersionLessThanEquals: + result = compareResult <= 0 + case OpVersionGreaterThan: + result = compareResult > 0 + case OpVersionGreaterThanEquals: + result = compareResult >= 0 + default: + result = false + } + if result { + return true + } + } + return false +} + +func compareString(propValue string, op string, filterValues []string) bool { + for _, filterValue := range filterValues { + var result bool + switch op { + case OpLessThan: + result = propValue < filterValue + case OpLessThanEquals: + result = propValue <= filterValue + case OpGreaterThan: + result = propValue > filterValue + case OpGreaterThanEquals: + result = propValue >= filterValue + default: + result = false + } + if result { + return true + } + } + return false +} + +func compareNumber(propValue float64, op string, filterValues []float64) bool { + for _, filterValue := range filterValues { + var result bool + switch op { + case OpLessThan: + result = propValue < filterValue + case OpLessThanEquals: + result = propValue <= filterValue + case OpGreaterThan: + result = propValue > filterValue + case OpGreaterThanEquals: + result = propValue >= filterValue + default: + result = false + } + if result { + return true + } + } + return false +} + +func matchesRegex(propValue string, filterValues []string) bool { + for _, filterValue := range filterValues { + match, _ := regexp.MatchString(filterValue, propValue) + if match { + return true + } + } + return false +} + +func containsNone(filterValues []string) bool { + for _, filterValue := range filterValues { + if filterValue == "(none)" { + return true + } + } + return false +} + +func containsBooleans(filterValues []string) bool { + for _, filterValue := range filterValues { + filterValueLower := strings.ToLower(filterValue) + if filterValueLower == "true" || filterValueLower == "false" { + return true + } + } + return false +} + +func coerceString(value interface{}) *string { + if value == nil { + return nil + } + kind := reflect.TypeOf(value).Kind() + if kind == reflect.Map || kind == reflect.Slice || kind == reflect.Array { + b, err := json.Marshal(value) + if err == nil { + s := string(b) + return &s + } + } + s := fmt.Sprintf("%v", value) + return &s +} + +func coerceStringList(value interface{}) ([]string, error) { + // Convert a list to a list of strings + _, ok := value.([]string) + if ok { + return value.([]string), nil + } + // Fall back to reflection for slices with unexpected value types + kind := reflect.TypeOf(value).Kind() + if kind == reflect.Slice || kind == reflect.Array { + var stringList []string + sliceValue := reflect.ValueOf(value) + for i := 0; i < sliceValue.Len(); i++ { + e := sliceValue.Index(i).Interface() + stringValue := coerceString(e) + if stringValue != nil { + stringList = append(stringList, *stringValue) + } + } + return stringList, nil + } else { + // Parse a string as json array and convert to list of strings, or + // return null if the string could not be parsed as a json array. + stringValue := fmt.Sprintf("%v", value) + var stringList []string + err := json.Unmarshal([]byte(stringValue), &stringList) + if err != nil { + return nil, err + } + return stringList, nil + } +} + +func isSetOperator(op string) bool { + switch op { + case OpSetIs, OpSetIsNot, OpSetContains, OpSetDoesNotContain, + OpSetContainsAny, OpSetDoesNotContainAny: + return true + default: + return false + } +} diff --git a/internal/evaluation/engine_test.go b/internal/evaluation/engine_test.go new file mode 100644 index 0000000..27296ce --- /dev/null +++ b/internal/evaluation/engine_test.go @@ -0,0 +1,831 @@ +package evaluation + +import ( + "context" + "encoding/json" + "fmt" + "github.com/amplitude/experiment-go-server/internal/logger" + "github.com/amplitude/experiment-go-server/pkg/experiment" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "time" + + //"os" + "testing" +) + +const deploymentKey = "server-NgJxxvg8OGwwBsWVXqyxQbdiflbhvugy" + +var flags []*Flag +var engine = &Engine{logger.New(false)} + +func init() { + rawFlags, err := getFlagConfigsRaw() + if err != nil { + panic(err) + } + err = json.Unmarshal(rawFlags, &flags) + if err != nil { + panic(err) + } +} + +// Basic Tests + +func TestOff(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_id": "user_id", + "device_id": "device_id", + }) + result := engine.Evaluate(user, flags)["test-off"] + if result.Key != "off" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestOn(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_id": "user_id", + "device_id": "device_id", + }) + result := engine.Evaluate(user, flags)["test-on"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +// Opinionated Segment Tests + +func TestIndividualInclusionsMatch(t *testing.T) { + // Match User ID + user := userContext(map[string]interface{}{ + "user_id": "user_id", + }) + result := engine.Evaluate(user, flags)["test-individual-inclusions"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + if result.Metadata["segmentName"] != "individual-inclusions" { + t.Fatalf("unexpected segment result %v", result.Metadata["segmentName"]) + } + // Match Device ID + user = userContext(map[string]interface{}{ + "device_id": "device_id", + }) + result = engine.Evaluate(user, flags)["test-individual-inclusions"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + if result.Metadata["segmentName"] != "individual-inclusions" { + t.Fatalf("unexpected segment result %v", result.Metadata["segmentName"]) + } + // Doesn't Match User ID + user = userContext(map[string]interface{}{ + "user_id": "not_user_id", + }) + result = engine.Evaluate(user, flags)["test-individual-inclusions"] + if result.Key != "off" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + // Doesn't Match Device ID + user = userContext(map[string]interface{}{ + "device_id": "not_device_id", + }) + result = engine.Evaluate(user, flags)["test-individual-inclusions"] + if result.Key != "off" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestFlagDependenciesOn(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_id": "user_id", + "device_id": "device_id", + }) + result := engine.Evaluate(user, flags)["test-flag-dependencies-on"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestFlagDependenciesOff(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_id": "user_id", + "device_id": "device_id", + }) + result := engine.Evaluate(user, flags)["test-flag-dependencies-off"] + if result.Key != "off" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + if result.Metadata["segmentName"] != "flag-dependencies" { + t.Fatalf("unexpected segment result %v", result.Metadata["segmentName"]) + } +} + +func TestStickyBucketing(t *testing.T) { + // On + user := userContext(map[string]interface{}{ + "user_id": "user_id", + "device_id": "device_id", + "user_properties": map[string]interface{}{ + "[Experiment] test-sticky-bucketing": "on", + }, + }) + + result := engine.Evaluate(user, flags)["test-sticky-bucketing"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + if result.Metadata["segmentName"] != "sticky-bucketing" { + t.Fatalf("unexpected segment result %v", result.Metadata["segmentName"]) + } + // Off + user = userContext(map[string]interface{}{ + "user_id": "user_id", + "device_id": "device_id", + "user_properties": map[string]interface{}{ + "[Experiment] test-sticky-bucketing": "off", + }, + }) + result = engine.Evaluate(user, flags)["test-sticky-bucketing"] + if result.Key != "off" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + if result.Metadata["segmentName"] != "All Other Users" { + t.Fatalf("unexpected segment result %v", result.Metadata["segmentName"]) + } + // Non-variant + user = userContext(map[string]interface{}{ + "user_id": "user_id", + "device_id": "device_id", + "user_properties": map[string]interface{}{ + "[Experiment] test-sticky-bucketing": "not-a-variant", + }, + }) + result = engine.Evaluate(user, flags)["test-sticky-bucketing"] + if result.Key != "off" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + if result.Metadata["segmentName"] != "All Other Users" { + t.Fatalf("unexpected segment result %v", result.Metadata["segmentName"]) + } +} + +// Experiment and Flag Segment Tests + +func TestExperiment(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_id": "user_id", + "device_id": "device_id", + }) + result := engine.Evaluate(user, flags)["test-experiment"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + if result.Metadata["experimentKey"] != "exp-1" { + t.Fatalf("unexpected experiment key result %v", result.Metadata["experimentKey"]) + } +} + +func TestFlag(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_id": "user_id", + "device_id": "device_id", + }) + result := engine.Evaluate(user, flags)["test-flag"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + if result.Metadata["experimentKey"] != nil { + t.Fatalf("unexpected experiment key result %v", result.Metadata["experimentKey"]) + } +} + +// Conditional Logic Tests + +func TestMultipleConditionsAndValues(t *testing.T) { + // All match, on + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key-1": "value-1", + "key-2": "value-2", + "key-3": "value-3", + }, + }) + result := engine.Evaluate(user, flags)["test-multiple-conditions-and-values"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + // Some match, off + user = userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key-1": "value-1", + "key-2": "value-2", + }, + }) + result = engine.Evaluate(user, flags)["test-multiple-conditions-and-values"] + if result.Key != "off" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +// Condition Property Targeting Tests + +func TestAmplitudePropertyTargeting(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_id": "user_id", + }) + result := engine.Evaluate(user, flags)["test-amplitude-property-targeting"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestCohortTargeting(t *testing.T) { + // User in cohort + user := userContext(map[string]interface{}{ + "cohort_ids": []string{"u0qtvwla", "12345678"}, + }) + result := engine.Evaluate(user, flags)["test-cohort-targeting"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + + // User not in cohort + user = userContext(map[string]interface{}{ + "cohort_ids": []string{"12345678", "87654321"}, + }) + result = engine.Evaluate(user, flags)["test-cohort-targeting"] + if result.Key != "off" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestGroupNameTargeting(t *testing.T) { + user := groupContext(map[string]interface{}{ + "org name": map[string]interface{}{ + "group_name": "amplitude", + }, + }) + result := engine.Evaluate(user, flags)["test-group-name-targeting"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestGroupPropertyTargeting(t *testing.T) { + user := groupContext(map[string]interface{}{ + "org name": map[string]interface{}{ + "group_name": "amplitude", + "group_properties": map[string]interface{}{ + "org plan": "enterprise2", + }, + }, + }) + result := engine.Evaluate(user, flags)["test-group-property-targeting"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +// Bucketing Tests + +func TestAmplitudeIdBucketing(t *testing.T) { + user := userContext(map[string]interface{}{ + "amplitude_id": "1234567890", + }) + result := engine.Evaluate(user, flags)["test-amplitude-id-bucketing"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestUserIdBucketing(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_id": "user_id", + }) + result := engine.Evaluate(user, flags)["test-user-id-bucketing"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestDeviceIdBucketing(t *testing.T) { + user := userContext(map[string]interface{}{ + "device_id": "device_id", + }) + result := engine.Evaluate(user, flags)["test-device-id-bucketing"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestCustomUserPropertyBucketing(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "value", + }, + }) + result := engine.Evaluate(user, flags)["test-custom-user-property-bucketing"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestGroupNameBucketing(t *testing.T) { + user := groupContext(map[string]interface{}{ + "org name": map[string]interface{}{ + "group_name": "amplitude", + }, + }) + result := engine.Evaluate(user, flags)["test-group-name-bucketing"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestGroupPropertyBucketing(t *testing.T) { + user := groupContext(map[string]interface{}{ + "org name": map[string]interface{}{ + "group_name": "amplitude", + "group_properties": map[string]interface{}{ + "org plan": "enterprise2", + }, + }, + }) + result := engine.Evaluate(user, flags)["test-group-property-bucketing"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +// Bucketing Allocation Tests + +func Test1PercentAllocation(t *testing.T) { + on := 0 + for i := 0; i < 10000; i++ { + user := userContext(map[string]interface{}{ + "device_id": strconv.Itoa(i + 1), + }) + result := engine.Evaluate(user, flags)["test-1-percent-allocation"] + if result.Key == "on" { + on++ + } + } + if on != 107 { + t.Fatalf("unexpected evaluation result %v", on) + } +} + +func Test50PercentAllocation(t *testing.T) { + on := 0 + for i := 0; i < 10000; i++ { + user := userContext(map[string]interface{}{ + "device_id": strconv.Itoa(i + 1), + }) + result := engine.Evaluate(user, flags)["test-50-percent-allocation"] + if result.Key == "on" { + on++ + } + } + if on != 5009 { + t.Fatalf("unexpected evaluation result %v", on) + } +} + +func Test99PercentAllocation(t *testing.T) { + on := 0 + for i := 0; i < 10000; i++ { + user := userContext(map[string]interface{}{ + "device_id": strconv.Itoa(i + 1), + }) + result := engine.Evaluate(user, flags)["test-99-percent-allocation"] + if result.Key == "on" { + on++ + } + } + if on != 9900 { + t.Fatalf("unexpected evaluation result %v", on) + } +} + +// Bucketing Distribution Tests + +func Test1PercentDistribution(t *testing.T) { + control := 0 + treatment := 0 + for i := 0; i < 10000; i++ { + user := userContext(map[string]interface{}{ + "device_id": strconv.Itoa(i + 1), + }) + result := engine.Evaluate(user, flags)["test-1-percent-distribution"] + switch result.Key { + case "control": + control++ + case "treatment": + treatment++ + default: + t.Fatalf("unexpected variant %v", result.Key) + } + } + if control != 106 { + t.Fatalf("unexpected evaluation result %v", control) + } + if treatment != 9894 { + t.Fatalf("unexpected evaluation result %v", treatment) + } +} + +func Test50PercentDistribution(t *testing.T) { + control := 0 + treatment := 0 + for i := 0; i < 10000; i++ { + user := userContext(map[string]interface{}{ + "device_id": strconv.Itoa(i + 1), + }) + result := engine.Evaluate(user, flags)["test-50-percent-distribution"] + switch result.Key { + case "control": + control++ + case "treatment": + treatment++ + default: + t.Fatalf("unexpected variant %v", result.Key) + } + } + if control != 4990 { + t.Fatalf("unexpected evaluation result %v", control) + } + if treatment != 5010 { + t.Fatalf("unexpected evaluation result %v", treatment) + } +} + +func Test99PercentDistribution(t *testing.T) { + control := 0 + treatment := 0 + for i := 0; i < 10000; i++ { + user := userContext(map[string]interface{}{ + "device_id": strconv.Itoa(i + 1), + }) + result := engine.Evaluate(user, flags)["test-99-percent-distribution"] + switch result.Key { + case "control": + control++ + case "treatment": + treatment++ + default: + t.Fatalf("unexpected variant %v", result.Key) + } + } + if control != 9909 { + t.Fatalf("unexpected evaluation result %v", control) + } + if treatment != 91 { + t.Fatalf("unexpected evaluation result %v", treatment) + } +} + +func TestMultipleDistributions(t *testing.T) { + a := 0 + b := 0 + c := 0 + d := 0 + for i := 0; i < 10000; i++ { + user := userContext(map[string]interface{}{ + "device_id": strconv.Itoa(i + 1), + }) + result := engine.Evaluate(user, flags)["test-multiple-distributions"] + switch result.Key { + case "a": + a++ + case "b": + b++ + case "c": + c++ + case "d": + d++ + default: + t.Fatalf("unexpected variant %v", result.Key) + } + } + if a != 2444 { + t.Fatalf("unexpected evaluation result %v", a) + } + if b != 2634 { + t.Fatalf("unexpected evaluation result %v", b) + } + if c != 2447 { + t.Fatalf("unexpected evaluation result %v", c) + } + if d != 2475 { + t.Fatalf("unexpected evaluation result %v", d) + } +} + +// Operator Tests + +func TestIs(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "value", + }, + }) + result := engine.Evaluate(user, flags)["test-is"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestIsNot(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "value", + }, + }) + result := engine.Evaluate(user, flags)["test-is-not"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestContains(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "value", + }, + }) + result := engine.Evaluate(user, flags)["test-contains"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestDoesNotContain(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "value", + }, + }) + result := engine.Evaluate(user, flags)["test-does-not-contain"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestLess(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "-1", + }, + }) + result := engine.Evaluate(user, flags)["test-less"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestLessOrEqual(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "0", + }, + }) + result := engine.Evaluate(user, flags)["test-less-or-equal"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestGreater(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "1", + }, + }) + result := engine.Evaluate(user, flags)["test-greater"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestGreaterOrEqual(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "0", + }, + }) + result := engine.Evaluate(user, flags)["test-greater-or-equal"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestVersionLess(t *testing.T) { + user := userContext(map[string]interface{}{ + "version": "1.9.0", + }) + result := engine.Evaluate(user, flags)["test-version-less"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestVersionLessOrEqual(t *testing.T) { + user := userContext(map[string]interface{}{ + "version": "1.10.0", + }) + result := engine.Evaluate(user, flags)["test-version-less-or-equal"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestVersionGreater(t *testing.T) { + user := userContext(map[string]interface{}{ + "version": "1.10.0", + }) + result := engine.Evaluate(user, flags)["test-version-greater"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestVersionGreaterOrEqual(t *testing.T) { + user := userContext(map[string]interface{}{ + "version": "1.9.0", + }) + result := engine.Evaluate(user, flags)["test-version-greater-or-equal"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestSetIs(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": []int{1, 2, 3}, + }, + }) + result := engine.Evaluate(user, flags)["test-set-is"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestSetIsNot(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": []int{1, 2}, + }, + }) + result := engine.Evaluate(user, flags)["test-set-is-not"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestSetContains(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": []int{1, 2, 3, 4}, + }, + }) + result := engine.Evaluate(user, flags)["test-set-contains"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestSetDoesNotContain(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": []int{1, 2, 4}, + }, + }) + result := engine.Evaluate(user, flags)["test-set-does-not-contain"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestSetContainsAny(t *testing.T) { + user := userContext(map[string]interface{}{ + "cohort_ids": []string{"u0qtvwla", "12345678"}, + }) + result := engine.Evaluate(user, flags)["test-set-contains-any"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestSetDoesNotContainAny(t *testing.T) { + user := userContext(map[string]interface{}{ + "cohort_ids": []string{"12345678", "87654321"}, + }) + result := engine.Evaluate(user, flags)["test-set-does-not-contain-any"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestGlobMatch(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "/path/1/2/3/end", + }, + }) + result := engine.Evaluate(user, flags)["test-glob-match"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +func TestGlobDoesNotMatch(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "key": "/path/1/2/3", + }, + }) + result := engine.Evaluate(user, flags)["test-glob-does-not-match"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +// Test specific functionality + +func TestIsWithBooleans(t *testing.T) { + user := userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "true": "TRUE", + "false": "FALSE", + }, + }) + result := engine.Evaluate(user, flags)["test-is-with-booleans"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + user = userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "true": "True", + "false": "False", + }, + }) + result = engine.Evaluate(user, flags)["test-is-with-booleans"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } + user = userContext(map[string]interface{}{ + "user_properties": map[string]interface{}{ + "true": "true", + "false": "false", + }, + }) + result = engine.Evaluate(user, flags)["test-is-with-booleans"] + if result.Key != "on" { + t.Fatalf("unexpected evaluation result %v", result.Key) + } +} + +// Util + +func userContext(u map[string]interface{}) map[string]interface{} { + return map[string]interface{}{"user": u} +} + +func groupContext(g map[string]interface{}) map[string]interface{} { + return map[string]interface{}{"groups": g} +} + +func getFlagConfigsRaw() ([]byte, error) { + client := &http.Client{} + endpoint, err := url.Parse("https://api.lab.amplitude.com/") + if err != nil { + return nil, err + } + endpoint.Path = "sdk/v2/flags" + endpoint.RawQuery = "eval_mode=remote" + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + req, err := http.NewRequest("GET", endpoint.String(), nil) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + req.Header.Set("Authorization", fmt.Sprintf("Api-Key %s", deploymentKey)) + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + req.Header.Set("X-Amp-Exp-Library", fmt.Sprintf("experiment-go-server/%v", experiment.VERSION)) + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} diff --git a/internal/evaluation/evaluation.go b/internal/evaluation/evaluation.go deleted file mode 100644 index 1d0a3a0..0000000 --- a/internal/evaluation/evaluation.go +++ /dev/null @@ -1,49 +0,0 @@ -package evaluation - -/* -#cgo darwin,amd64 CFLAGS: -I${SRCDIR}/lib/macosX64 -#cgo darwin,amd64 LDFLAGS: -framework Foundation -lstdc++ -L${SRCDIR}/lib/macosX64 -levaluation_interop - -#cgo darwin,arm64 CFLAGS: -I${SRCDIR}/lib/macosArm64 -#cgo darwin,arm64 LDFLAGS: -framework Foundation -lstdc++ -L${SRCDIR}/lib/macosArm64 -levaluation_interop - -#cgo linux,amd64 CFLAGS: -I${SRCDIR}/lib/linuxX64 -#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/lib/linuxX64 -levaluation_interop -lstdc++ -lpthread -lc -ldl -lm - -#cgo linux,arm64 CFLAGS: -I${SRCDIR}/lib/linuxArm64 -#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/lib/linuxArm64 -levaluation_interop -lstdc++ -lpthread -lc -ldl -lm - -#include "libevaluation_interop_api.h" -#include - -typedef const char * (*evaluate) (const char * r, const char * u); -typedef void (*DisposeString) (const char* s); - -const char * bridge_evaluate(evaluate f, const char * r, const char * u) -{ - return f(r, u); -} - -void bridge_dispose(DisposeString f, const char * s) -{ - return f(s); -} -*/ -import "C" -import ( - "unsafe" -) - -var lib = C.libevaluation_interop_symbols() -var root = lib.kotlin.root - -func Evaluate(rules, user string) string { - rulesCString := C.CString(rules) - userCString := C.CString(user) - resultCString := C.bridge_evaluate(root.evaluate, rulesCString, userCString) - result := C.GoString(resultCString) - C.bridge_dispose(lib.DisposeString, resultCString) - C.free(unsafe.Pointer(rulesCString)) - C.free(unsafe.Pointer(userCString)) - return result -} diff --git a/internal/evaluation/lib/.DS_Store b/internal/evaluation/lib/.DS_Store deleted file mode 100644 index d1da882..0000000 Binary files a/internal/evaluation/lib/.DS_Store and /dev/null differ diff --git a/internal/evaluation/lib/linuxArm64/libevaluation_interop.a b/internal/evaluation/lib/linuxArm64/libevaluation_interop.a deleted file mode 100644 index ede75e4..0000000 Binary files a/internal/evaluation/lib/linuxArm64/libevaluation_interop.a and /dev/null differ diff --git a/internal/evaluation/lib/linuxArm64/libevaluation_interop_api.h b/internal/evaluation/lib/linuxArm64/libevaluation_interop_api.h deleted file mode 100644 index ea18cd4..0000000 --- a/internal/evaluation/lib/linuxArm64/libevaluation_interop_api.h +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef KONAN_LIBEVALUATION_INTEROP_H -#define KONAN_LIBEVALUATION_INTEROP_H -#ifdef __cplusplus -extern "C" { -#endif -#ifdef __cplusplus -typedef bool libevaluation_interop_KBoolean; -#else -typedef _Bool libevaluation_interop_KBoolean; -#endif -typedef unsigned short libevaluation_interop_KChar; -typedef signed char libevaluation_interop_KByte; -typedef short libevaluation_interop_KShort; -typedef int libevaluation_interop_KInt; -typedef long long libevaluation_interop_KLong; -typedef unsigned char libevaluation_interop_KUByte; -typedef unsigned short libevaluation_interop_KUShort; -typedef unsigned int libevaluation_interop_KUInt; -typedef unsigned long long libevaluation_interop_KULong; -typedef float libevaluation_interop_KFloat; -typedef double libevaluation_interop_KDouble; -typedef float __attribute__ ((__vector_size__ (16))) libevaluation_interop_KVector128; -typedef void* libevaluation_interop_KNativePtr; -struct libevaluation_interop_KType; -typedef struct libevaluation_interop_KType libevaluation_interop_KType; - -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Byte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Short; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Int; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Long; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Float; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Double; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Char; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Boolean; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Unit; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UByte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UShort; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UInt; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_ULong; - - -typedef struct { - /* Service functions. */ - void (*DisposeStablePointer)(libevaluation_interop_KNativePtr ptr); - void (*DisposeString)(const char* string); - libevaluation_interop_KBoolean (*IsInstance)(libevaluation_interop_KNativePtr ref, const libevaluation_interop_KType* type); - libevaluation_interop_kref_kotlin_Byte (*createNullableByte)(libevaluation_interop_KByte); - libevaluation_interop_KByte (*getNonNullValueOfByte)(libevaluation_interop_kref_kotlin_Byte); - libevaluation_interop_kref_kotlin_Short (*createNullableShort)(libevaluation_interop_KShort); - libevaluation_interop_KShort (*getNonNullValueOfShort)(libevaluation_interop_kref_kotlin_Short); - libevaluation_interop_kref_kotlin_Int (*createNullableInt)(libevaluation_interop_KInt); - libevaluation_interop_KInt (*getNonNullValueOfInt)(libevaluation_interop_kref_kotlin_Int); - libevaluation_interop_kref_kotlin_Long (*createNullableLong)(libevaluation_interop_KLong); - libevaluation_interop_KLong (*getNonNullValueOfLong)(libevaluation_interop_kref_kotlin_Long); - libevaluation_interop_kref_kotlin_Float (*createNullableFloat)(libevaluation_interop_KFloat); - libevaluation_interop_KFloat (*getNonNullValueOfFloat)(libevaluation_interop_kref_kotlin_Float); - libevaluation_interop_kref_kotlin_Double (*createNullableDouble)(libevaluation_interop_KDouble); - libevaluation_interop_KDouble (*getNonNullValueOfDouble)(libevaluation_interop_kref_kotlin_Double); - libevaluation_interop_kref_kotlin_Char (*createNullableChar)(libevaluation_interop_KChar); - libevaluation_interop_KChar (*getNonNullValueOfChar)(libevaluation_interop_kref_kotlin_Char); - libevaluation_interop_kref_kotlin_Boolean (*createNullableBoolean)(libevaluation_interop_KBoolean); - libevaluation_interop_KBoolean (*getNonNullValueOfBoolean)(libevaluation_interop_kref_kotlin_Boolean); - libevaluation_interop_kref_kotlin_Unit (*createNullableUnit)(void); - libevaluation_interop_kref_kotlin_UByte (*createNullableUByte)(libevaluation_interop_KUByte); - libevaluation_interop_KUByte (*getNonNullValueOfUByte)(libevaluation_interop_kref_kotlin_UByte); - libevaluation_interop_kref_kotlin_UShort (*createNullableUShort)(libevaluation_interop_KUShort); - libevaluation_interop_KUShort (*getNonNullValueOfUShort)(libevaluation_interop_kref_kotlin_UShort); - libevaluation_interop_kref_kotlin_UInt (*createNullableUInt)(libevaluation_interop_KUInt); - libevaluation_interop_KUInt (*getNonNullValueOfUInt)(libevaluation_interop_kref_kotlin_UInt); - libevaluation_interop_kref_kotlin_ULong (*createNullableULong)(libevaluation_interop_KULong); - libevaluation_interop_KULong (*getNonNullValueOfULong)(libevaluation_interop_kref_kotlin_ULong); - - /* User functions. */ - struct { - struct { - const char* (*evaluate)(const char* rules, const char* user); - } root; - } kotlin; -} libevaluation_interop_ExportedSymbols; -extern libevaluation_interop_ExportedSymbols* libevaluation_interop_symbols(void); -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif /* KONAN_LIBEVALUATION_INTEROP_H */ diff --git a/internal/evaluation/lib/linuxX64/libevaluation_interop.a b/internal/evaluation/lib/linuxX64/libevaluation_interop.a deleted file mode 100644 index 1d57e68..0000000 Binary files a/internal/evaluation/lib/linuxX64/libevaluation_interop.a and /dev/null differ diff --git a/internal/evaluation/lib/linuxX64/libevaluation_interop_api.h b/internal/evaluation/lib/linuxX64/libevaluation_interop_api.h deleted file mode 100644 index ea18cd4..0000000 --- a/internal/evaluation/lib/linuxX64/libevaluation_interop_api.h +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef KONAN_LIBEVALUATION_INTEROP_H -#define KONAN_LIBEVALUATION_INTEROP_H -#ifdef __cplusplus -extern "C" { -#endif -#ifdef __cplusplus -typedef bool libevaluation_interop_KBoolean; -#else -typedef _Bool libevaluation_interop_KBoolean; -#endif -typedef unsigned short libevaluation_interop_KChar; -typedef signed char libevaluation_interop_KByte; -typedef short libevaluation_interop_KShort; -typedef int libevaluation_interop_KInt; -typedef long long libevaluation_interop_KLong; -typedef unsigned char libevaluation_interop_KUByte; -typedef unsigned short libevaluation_interop_KUShort; -typedef unsigned int libevaluation_interop_KUInt; -typedef unsigned long long libevaluation_interop_KULong; -typedef float libevaluation_interop_KFloat; -typedef double libevaluation_interop_KDouble; -typedef float __attribute__ ((__vector_size__ (16))) libevaluation_interop_KVector128; -typedef void* libevaluation_interop_KNativePtr; -struct libevaluation_interop_KType; -typedef struct libevaluation_interop_KType libevaluation_interop_KType; - -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Byte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Short; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Int; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Long; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Float; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Double; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Char; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Boolean; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Unit; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UByte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UShort; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UInt; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_ULong; - - -typedef struct { - /* Service functions. */ - void (*DisposeStablePointer)(libevaluation_interop_KNativePtr ptr); - void (*DisposeString)(const char* string); - libevaluation_interop_KBoolean (*IsInstance)(libevaluation_interop_KNativePtr ref, const libevaluation_interop_KType* type); - libevaluation_interop_kref_kotlin_Byte (*createNullableByte)(libevaluation_interop_KByte); - libevaluation_interop_KByte (*getNonNullValueOfByte)(libevaluation_interop_kref_kotlin_Byte); - libevaluation_interop_kref_kotlin_Short (*createNullableShort)(libevaluation_interop_KShort); - libevaluation_interop_KShort (*getNonNullValueOfShort)(libevaluation_interop_kref_kotlin_Short); - libevaluation_interop_kref_kotlin_Int (*createNullableInt)(libevaluation_interop_KInt); - libevaluation_interop_KInt (*getNonNullValueOfInt)(libevaluation_interop_kref_kotlin_Int); - libevaluation_interop_kref_kotlin_Long (*createNullableLong)(libevaluation_interop_KLong); - libevaluation_interop_KLong (*getNonNullValueOfLong)(libevaluation_interop_kref_kotlin_Long); - libevaluation_interop_kref_kotlin_Float (*createNullableFloat)(libevaluation_interop_KFloat); - libevaluation_interop_KFloat (*getNonNullValueOfFloat)(libevaluation_interop_kref_kotlin_Float); - libevaluation_interop_kref_kotlin_Double (*createNullableDouble)(libevaluation_interop_KDouble); - libevaluation_interop_KDouble (*getNonNullValueOfDouble)(libevaluation_interop_kref_kotlin_Double); - libevaluation_interop_kref_kotlin_Char (*createNullableChar)(libevaluation_interop_KChar); - libevaluation_interop_KChar (*getNonNullValueOfChar)(libevaluation_interop_kref_kotlin_Char); - libevaluation_interop_kref_kotlin_Boolean (*createNullableBoolean)(libevaluation_interop_KBoolean); - libevaluation_interop_KBoolean (*getNonNullValueOfBoolean)(libevaluation_interop_kref_kotlin_Boolean); - libevaluation_interop_kref_kotlin_Unit (*createNullableUnit)(void); - libevaluation_interop_kref_kotlin_UByte (*createNullableUByte)(libevaluation_interop_KUByte); - libevaluation_interop_KUByte (*getNonNullValueOfUByte)(libevaluation_interop_kref_kotlin_UByte); - libevaluation_interop_kref_kotlin_UShort (*createNullableUShort)(libevaluation_interop_KUShort); - libevaluation_interop_KUShort (*getNonNullValueOfUShort)(libevaluation_interop_kref_kotlin_UShort); - libevaluation_interop_kref_kotlin_UInt (*createNullableUInt)(libevaluation_interop_KUInt); - libevaluation_interop_KUInt (*getNonNullValueOfUInt)(libevaluation_interop_kref_kotlin_UInt); - libevaluation_interop_kref_kotlin_ULong (*createNullableULong)(libevaluation_interop_KULong); - libevaluation_interop_KULong (*getNonNullValueOfULong)(libevaluation_interop_kref_kotlin_ULong); - - /* User functions. */ - struct { - struct { - const char* (*evaluate)(const char* rules, const char* user); - } root; - } kotlin; -} libevaluation_interop_ExportedSymbols; -extern libevaluation_interop_ExportedSymbols* libevaluation_interop_symbols(void); -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif /* KONAN_LIBEVALUATION_INTEROP_H */ diff --git a/internal/evaluation/lib/macosArm64/libevaluation_interop.a b/internal/evaluation/lib/macosArm64/libevaluation_interop.a deleted file mode 100644 index 7aef925..0000000 Binary files a/internal/evaluation/lib/macosArm64/libevaluation_interop.a and /dev/null differ diff --git a/internal/evaluation/lib/macosArm64/libevaluation_interop_api.h b/internal/evaluation/lib/macosArm64/libevaluation_interop_api.h deleted file mode 100644 index ea18cd4..0000000 --- a/internal/evaluation/lib/macosArm64/libevaluation_interop_api.h +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef KONAN_LIBEVALUATION_INTEROP_H -#define KONAN_LIBEVALUATION_INTEROP_H -#ifdef __cplusplus -extern "C" { -#endif -#ifdef __cplusplus -typedef bool libevaluation_interop_KBoolean; -#else -typedef _Bool libevaluation_interop_KBoolean; -#endif -typedef unsigned short libevaluation_interop_KChar; -typedef signed char libevaluation_interop_KByte; -typedef short libevaluation_interop_KShort; -typedef int libevaluation_interop_KInt; -typedef long long libevaluation_interop_KLong; -typedef unsigned char libevaluation_interop_KUByte; -typedef unsigned short libevaluation_interop_KUShort; -typedef unsigned int libevaluation_interop_KUInt; -typedef unsigned long long libevaluation_interop_KULong; -typedef float libevaluation_interop_KFloat; -typedef double libevaluation_interop_KDouble; -typedef float __attribute__ ((__vector_size__ (16))) libevaluation_interop_KVector128; -typedef void* libevaluation_interop_KNativePtr; -struct libevaluation_interop_KType; -typedef struct libevaluation_interop_KType libevaluation_interop_KType; - -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Byte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Short; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Int; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Long; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Float; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Double; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Char; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Boolean; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Unit; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UByte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UShort; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UInt; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_ULong; - - -typedef struct { - /* Service functions. */ - void (*DisposeStablePointer)(libevaluation_interop_KNativePtr ptr); - void (*DisposeString)(const char* string); - libevaluation_interop_KBoolean (*IsInstance)(libevaluation_interop_KNativePtr ref, const libevaluation_interop_KType* type); - libevaluation_interop_kref_kotlin_Byte (*createNullableByte)(libevaluation_interop_KByte); - libevaluation_interop_KByte (*getNonNullValueOfByte)(libevaluation_interop_kref_kotlin_Byte); - libevaluation_interop_kref_kotlin_Short (*createNullableShort)(libevaluation_interop_KShort); - libevaluation_interop_KShort (*getNonNullValueOfShort)(libevaluation_interop_kref_kotlin_Short); - libevaluation_interop_kref_kotlin_Int (*createNullableInt)(libevaluation_interop_KInt); - libevaluation_interop_KInt (*getNonNullValueOfInt)(libevaluation_interop_kref_kotlin_Int); - libevaluation_interop_kref_kotlin_Long (*createNullableLong)(libevaluation_interop_KLong); - libevaluation_interop_KLong (*getNonNullValueOfLong)(libevaluation_interop_kref_kotlin_Long); - libevaluation_interop_kref_kotlin_Float (*createNullableFloat)(libevaluation_interop_KFloat); - libevaluation_interop_KFloat (*getNonNullValueOfFloat)(libevaluation_interop_kref_kotlin_Float); - libevaluation_interop_kref_kotlin_Double (*createNullableDouble)(libevaluation_interop_KDouble); - libevaluation_interop_KDouble (*getNonNullValueOfDouble)(libevaluation_interop_kref_kotlin_Double); - libevaluation_interop_kref_kotlin_Char (*createNullableChar)(libevaluation_interop_KChar); - libevaluation_interop_KChar (*getNonNullValueOfChar)(libevaluation_interop_kref_kotlin_Char); - libevaluation_interop_kref_kotlin_Boolean (*createNullableBoolean)(libevaluation_interop_KBoolean); - libevaluation_interop_KBoolean (*getNonNullValueOfBoolean)(libevaluation_interop_kref_kotlin_Boolean); - libevaluation_interop_kref_kotlin_Unit (*createNullableUnit)(void); - libevaluation_interop_kref_kotlin_UByte (*createNullableUByte)(libevaluation_interop_KUByte); - libevaluation_interop_KUByte (*getNonNullValueOfUByte)(libevaluation_interop_kref_kotlin_UByte); - libevaluation_interop_kref_kotlin_UShort (*createNullableUShort)(libevaluation_interop_KUShort); - libevaluation_interop_KUShort (*getNonNullValueOfUShort)(libevaluation_interop_kref_kotlin_UShort); - libevaluation_interop_kref_kotlin_UInt (*createNullableUInt)(libevaluation_interop_KUInt); - libevaluation_interop_KUInt (*getNonNullValueOfUInt)(libevaluation_interop_kref_kotlin_UInt); - libevaluation_interop_kref_kotlin_ULong (*createNullableULong)(libevaluation_interop_KULong); - libevaluation_interop_KULong (*getNonNullValueOfULong)(libevaluation_interop_kref_kotlin_ULong); - - /* User functions. */ - struct { - struct { - const char* (*evaluate)(const char* rules, const char* user); - } root; - } kotlin; -} libevaluation_interop_ExportedSymbols; -extern libevaluation_interop_ExportedSymbols* libevaluation_interop_symbols(void); -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif /* KONAN_LIBEVALUATION_INTEROP_H */ diff --git a/internal/evaluation/lib/macosX64/libevaluation_interop.a b/internal/evaluation/lib/macosX64/libevaluation_interop.a deleted file mode 100644 index 7cec907..0000000 Binary files a/internal/evaluation/lib/macosX64/libevaluation_interop.a and /dev/null differ diff --git a/internal/evaluation/lib/macosX64/libevaluation_interop_api.h b/internal/evaluation/lib/macosX64/libevaluation_interop_api.h deleted file mode 100644 index ea18cd4..0000000 --- a/internal/evaluation/lib/macosX64/libevaluation_interop_api.h +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef KONAN_LIBEVALUATION_INTEROP_H -#define KONAN_LIBEVALUATION_INTEROP_H -#ifdef __cplusplus -extern "C" { -#endif -#ifdef __cplusplus -typedef bool libevaluation_interop_KBoolean; -#else -typedef _Bool libevaluation_interop_KBoolean; -#endif -typedef unsigned short libevaluation_interop_KChar; -typedef signed char libevaluation_interop_KByte; -typedef short libevaluation_interop_KShort; -typedef int libevaluation_interop_KInt; -typedef long long libevaluation_interop_KLong; -typedef unsigned char libevaluation_interop_KUByte; -typedef unsigned short libevaluation_interop_KUShort; -typedef unsigned int libevaluation_interop_KUInt; -typedef unsigned long long libevaluation_interop_KULong; -typedef float libevaluation_interop_KFloat; -typedef double libevaluation_interop_KDouble; -typedef float __attribute__ ((__vector_size__ (16))) libevaluation_interop_KVector128; -typedef void* libevaluation_interop_KNativePtr; -struct libevaluation_interop_KType; -typedef struct libevaluation_interop_KType libevaluation_interop_KType; - -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Byte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Short; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Int; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Long; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Float; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Double; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Char; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Boolean; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Unit; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UByte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UShort; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UInt; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_ULong; - - -typedef struct { - /* Service functions. */ - void (*DisposeStablePointer)(libevaluation_interop_KNativePtr ptr); - void (*DisposeString)(const char* string); - libevaluation_interop_KBoolean (*IsInstance)(libevaluation_interop_KNativePtr ref, const libevaluation_interop_KType* type); - libevaluation_interop_kref_kotlin_Byte (*createNullableByte)(libevaluation_interop_KByte); - libevaluation_interop_KByte (*getNonNullValueOfByte)(libevaluation_interop_kref_kotlin_Byte); - libevaluation_interop_kref_kotlin_Short (*createNullableShort)(libevaluation_interop_KShort); - libevaluation_interop_KShort (*getNonNullValueOfShort)(libevaluation_interop_kref_kotlin_Short); - libevaluation_interop_kref_kotlin_Int (*createNullableInt)(libevaluation_interop_KInt); - libevaluation_interop_KInt (*getNonNullValueOfInt)(libevaluation_interop_kref_kotlin_Int); - libevaluation_interop_kref_kotlin_Long (*createNullableLong)(libevaluation_interop_KLong); - libevaluation_interop_KLong (*getNonNullValueOfLong)(libevaluation_interop_kref_kotlin_Long); - libevaluation_interop_kref_kotlin_Float (*createNullableFloat)(libevaluation_interop_KFloat); - libevaluation_interop_KFloat (*getNonNullValueOfFloat)(libevaluation_interop_kref_kotlin_Float); - libevaluation_interop_kref_kotlin_Double (*createNullableDouble)(libevaluation_interop_KDouble); - libevaluation_interop_KDouble (*getNonNullValueOfDouble)(libevaluation_interop_kref_kotlin_Double); - libevaluation_interop_kref_kotlin_Char (*createNullableChar)(libevaluation_interop_KChar); - libevaluation_interop_KChar (*getNonNullValueOfChar)(libevaluation_interop_kref_kotlin_Char); - libevaluation_interop_kref_kotlin_Boolean (*createNullableBoolean)(libevaluation_interop_KBoolean); - libevaluation_interop_KBoolean (*getNonNullValueOfBoolean)(libevaluation_interop_kref_kotlin_Boolean); - libevaluation_interop_kref_kotlin_Unit (*createNullableUnit)(void); - libevaluation_interop_kref_kotlin_UByte (*createNullableUByte)(libevaluation_interop_KUByte); - libevaluation_interop_KUByte (*getNonNullValueOfUByte)(libevaluation_interop_kref_kotlin_UByte); - libevaluation_interop_kref_kotlin_UShort (*createNullableUShort)(libevaluation_interop_KUShort); - libevaluation_interop_KUShort (*getNonNullValueOfUShort)(libevaluation_interop_kref_kotlin_UShort); - libevaluation_interop_kref_kotlin_UInt (*createNullableUInt)(libevaluation_interop_KUInt); - libevaluation_interop_KUInt (*getNonNullValueOfUInt)(libevaluation_interop_kref_kotlin_UInt); - libevaluation_interop_kref_kotlin_ULong (*createNullableULong)(libevaluation_interop_KULong); - libevaluation_interop_KULong (*getNonNullValueOfULong)(libevaluation_interop_kref_kotlin_ULong); - - /* User functions. */ - struct { - struct { - const char* (*evaluate)(const char* rules, const char* user); - } root; - } kotlin; -} libevaluation_interop_ExportedSymbols; -extern libevaluation_interop_ExportedSymbols* libevaluation_interop_symbols(void); -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif /* KONAN_LIBEVALUATION_INTEROP_H */ diff --git a/internal/evaluation/selectable.go b/internal/evaluation/selectable.go new file mode 100644 index 0000000..a5e4ebf --- /dev/null +++ b/internal/evaluation/selectable.go @@ -0,0 +1,68 @@ +package evaluation + +import "reflect" + +type selectable interface { + Select(selector string) interface{} +} + +func (t target) Select(selector string) interface{} { + switch selector { + case "context": + return t.context + case "result": + return t.result + default: + return nil + } +} + +func (v Variant) Select(selector string) interface{} { + switch selector { + case "key": + return v.Key + case "value": + return v.Value + case "payload": + return v.Payload + case "metadata": + return v.Metadata + default: + return nil + } +} + +func selectEach(s interface{}, selector []string) interface{} { + if s == nil || selector == nil || len(selector) == 0 { + return nil + } + for _, selectorElement := range selector { + if s == nil { + return nil + } + switch t := s.(type) { + case selectable: + s = t.Select(selectorElement) + case map[string]interface{}: + s = t[selectorElement] + case map[string]Variant: + s = t[selectorElement] + default: + // Fall back to reflection for maps with unexpected value types + isMap := reflect.TypeOf(s).Kind() == reflect.Map + if isMap { + iter := reflect.ValueOf(s).MapRange() + for iter.Next() { + mapKey := iter.Key().String() + mapValue := iter.Value().Interface() + if mapKey == selectorElement { + s = mapValue + } + } + } else { + return nil + } + } + } + return s +} diff --git a/internal/evaluation/selectable_test.go b/internal/evaluation/selectable_test.go new file mode 100644 index 0000000..8a94ebb --- /dev/null +++ b/internal/evaluation/selectable_test.go @@ -0,0 +1,154 @@ +package evaluation + +import ( + "encoding/json" + "reflect" + "testing" +) + +var primitiveObjectJson = ` +{ + "null": null, + "string": "value", + "int": 13, + "double": 13.12, + "boolean": true, + "array": [null, "value", 13, 13.12, true], + "object": { + "null": null, + "string": "value", + "int": 13, + "double": 13.12, + "boolean": true, + "array": [null, "value", 13, 13.12, true], + "object": { + "null": null, + "string": "value", + "int": 13, + "double": 13.12, + "boolean": true + } + } +} +` + +func TestUnstructuredJson(t *testing.T) { + var object map[string]interface{} + err := json.Unmarshal([]byte(primitiveObjectJson), &object) + if err != nil { + panic(err) + } + missingValue := selectEach(object, []string{"does", "not", "exist"}) + nullValue := selectEach(object, []string{"null"}) + stringValue := selectEach(object, []string{"string"}) + intValue := selectEach(object, []string{"int"}) + doubleValue := selectEach(object, []string{"double"}) + booleanValue := selectEach(object, []string{"boolean"}) + arrayValue := selectEach(object, []string{"array"}) + objectValue := selectEach(object, []string{"object"}) + + if missingValue != nil { + t.Fatalf("unexpected value %v", missingValue) + } + if nullValue != nil { + t.Fatalf("unexpected value %v", nullValue) + } + if stringValue != "value" { + t.Fatalf("unexpected value %v", stringValue) + } + if intValue != float64(13) { + t.Fatalf("unexpected value %v", intValue) + } + if doubleValue != 13.12 { + t.Fatalf("unexpected value %v", doubleValue) + } + if booleanValue != true { + t.Fatalf("unexpected value %v", booleanValue) + } + if !reflect.DeepEqual(arrayValue, []interface{}{nil, "value", float64(13), 13.12, true}) { + t.Fatalf("unexpected value %v", arrayValue) + } + if !reflect.DeepEqual(objectValue, map[string]interface{}{ + "null": nil, + "string": "value", + "int": float64(13), + "double": 13.12, + "boolean": true, + "array": []interface{}{nil, "value", float64(13), 13.12, true}, + "object": map[string]interface{}{ + "null": nil, + "string": "value", + "int": float64(13), + "double": 13.12, + "boolean": true, + }, + }) { + t.Fatalf("unexpected value %v", objectValue) + } + + nestedMissingValue := selectEach(object, []string{"object", "does", "not", "exist"}) + nestedNullValue := selectEach(object, []string{"object", "null"}) + nestedStringValue := selectEach(object, []string{"object", "string"}) + nestedIntValue := selectEach(object, []string{"object", "int"}) + nestedDoubleValue := selectEach(object, []string{"object", "double"}) + nestedBooleanValue := selectEach(object, []string{"object", "boolean"}) + nestedArrayValue := selectEach(object, []string{"object", "array"}) + nestedObjectValue := selectEach(object, []string{"object", "object"}) + + if nestedMissingValue != nil { + t.Fatalf("unexpected value %v", nestedMissingValue) + } + if nestedNullValue != nil { + t.Fatalf("unexpected value %v", nestedNullValue) + } + if nestedStringValue != "value" { + t.Fatalf("unexpected value %v", nestedStringValue) + } + if nestedIntValue != float64(13) { + t.Fatalf("unexpected value %v", nestedIntValue) + } + if nestedDoubleValue != 13.12 { + t.Fatalf("unexpected value %v", nestedDoubleValue) + } + if nestedBooleanValue != true { + t.Fatalf("unexpected value %v", nestedBooleanValue) + } + if !reflect.DeepEqual(nestedArrayValue, []interface{}{nil, "value", float64(13), 13.12, true}) { + t.Fatalf("unexpected value %v", nestedArrayValue) + } + if !reflect.DeepEqual(nestedObjectValue, map[string]interface{}{ + "null": nil, + "string": "value", + "int": float64(13), + "double": 13.12, + "boolean": true, + }) { + t.Fatalf("unexpected value %v", nestedObjectValue) + } + + nestedMissingValue2 := selectEach(object, []string{"object", "object", "does", "not", "exist"}) + nestedNullValue2 := selectEach(object, []string{"object", "object", "null"}) + nestedStringValue2 := selectEach(object, []string{"object", "object", "string"}) + nestedIntValue2 := selectEach(object, []string{"object", "object", "int"}) + nestedDoubleValue2 := selectEach(object, []string{"object", "object", "double"}) + nestedBooleanValue2 := selectEach(object, []string{"object", "object", "boolean"}) + + if nestedMissingValue2 != nil { + t.Fatalf("unexpected value %v", nestedMissingValue2) + } + if nestedNullValue2 != nil { + t.Fatalf("unexpected value %v", nestedNullValue2) + } + if nestedStringValue2 != "value" { + t.Fatalf("unexpected value %v", nestedStringValue2) + } + if nestedIntValue2 != float64(13) { + t.Fatalf("unexpected value %v", nestedIntValue2) + } + if nestedDoubleValue2 != 13.12 { + t.Fatalf("unexpected value %v", nestedDoubleValue2) + } + if nestedBooleanValue2 != true { + t.Fatalf("unexpected value %v", nestedBooleanValue2) + } +} diff --git a/internal/evaluation/types.go b/internal/evaluation/types.go new file mode 100644 index 0000000..d9060c8 --- /dev/null +++ b/internal/evaluation/types.go @@ -0,0 +1,68 @@ +package evaluation + +type Flag struct { + Key string `json:"key,omitempty"` + Variants map[string]*Variant `json:"variants,omitempty"` + Segments []*Segment `json:"segments,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type Variant struct { + Key string `json:"key,omitempty"` + Value interface{} `json:"value,omitempty"` + Payload interface{} `json:"payload,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type Segment struct { + Bucket *Bucket `json:"bucket,omitempty"` + Conditions [][]*Condition `json:"conditions,omitempty"` + Variant string `json:"variant,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type Bucket struct { + Selector []string `json:"selector,omitempty"` + Salt string `json:"salt,omitempty"` + Allocations []*Allocation `json:"allocations,omitempty"` +} + +type Condition struct { + Selector []string `json:"selector,omitempty"` + Op string `json:"op,omitempty"` + Values []string `json:"values,omitempty"` +} + +type Allocation struct { + Range []uint64 `json:"range,omitempty"` + Distributions []*Distribution `json:"distributions,omitempty"` +} + +type Distribution struct { + Variant string `json:"variant,omitempty"` + Range []uint64 `json:"range,omitempty"` +} + +const ( + OpIs = "is" + OpIsNot = "is not" + OpContains = "contains" + OpDoesNotContain = "does not contain" + OpLessThan = "less" + OpLessThanEquals = "less or equal" + OpGreaterThan = "greater" + OpGreaterThanEquals = "greater or equal" + OpVersionLessThan = "version less" + OpVersionLessThanEquals = "version less or equal" + OpVersionGreaterThan = "version greater" + OpVersionGreaterThanEquals = "version greater or equal" + OpSetIs = "set is" + OpSetIsNot = "set is not" + OpSetContains = "set contains" + OpSetDoesNotContain = "set does not contain" + OpSetContainsAny = "set contains any" + OpSetDoesNotContainAny = "set does not contain any" + OpRegexMatch = "regex match" + OpRegexDoesNotMatch = "regex does not match" +) diff --git a/internal/evaluation/version.go b/internal/evaluation/version.go new file mode 100644 index 0000000..4740d8b --- /dev/null +++ b/internal/evaluation/version.go @@ -0,0 +1,74 @@ +package evaluation + +import ( + "regexp" + "strconv" +) + +type version struct { + major int + minor int + patch int + preRelease string +} + +const versionPattern = "^(\\d+)\\.(\\d+)(\\.(\\d+)(-(([-\\w]+\\.?)*))?)?$" + +var regex, _ = regexp.Compile(versionPattern) + +func parseVersion(versionString string) *version { + if len(versionString) == 0 { + return nil + } + matchGroup := regex.FindStringSubmatch(versionString) + if matchGroup == nil { + return nil + } + major, err := strconv.Atoi(matchGroup[1]) + if err != nil { + return nil + } + minor, err := strconv.Atoi(matchGroup[2]) + if err != nil { + return nil + } + patch, _ := strconv.Atoi(matchGroup[4]) + preRelease := matchGroup[5] + return &version{major, minor, patch, preRelease} +} + +func versionCompare(v1, v2 version) int { + switch { + case v1.major > v2.major: + return 1 + case v1.major < v2.major: + return -1 + case v1.minor > v2.minor: + return 1 + case v1.minor < v2.minor: + return -1 + case v1.patch > v2.patch: + return 1 + case v1.patch < v2.patch: + return -1 + case len(v1.preRelease) > 0 && len(v2.preRelease) == 0: + return -1 + case len(v1.preRelease) == 0 && len(v2.preRelease) > 0: + return 1 + case len(v1.preRelease) > 0 && len(v2.preRelease) > 0: + return stringCompare(v1.preRelease, v2.preRelease) + default: + return 0 + } +} + +func stringCompare(s1, s2 string) int { + switch { + case s1 < s2: + return -1 + case s1 > s2: + return 1 + default: + return 0 + } +} diff --git a/internal/evaluation/version_test.go b/internal/evaluation/version_test.go new file mode 100644 index 0000000..195f658 --- /dev/null +++ b/internal/evaluation/version_test.go @@ -0,0 +1,142 @@ +package evaluation + +import ( + "testing" +) + +func TestInvalidVersions(t *testing.T) { + // just major + assertInvalidVersion(t, "10") + // trailing dots + assertInvalidVersion(t, "10.") + assertInvalidVersion(t, "10..") + assertInvalidVersion(t, "10.2.") + assertInvalidVersion(t, "10.2.33.") + // trailing dots on prerelease tags are not handled because prerelease tags are considered + // strings anyway for comparison which should be fine - e.g. "10.2.33-alpha1.2." + + // dots in the middle + assertInvalidVersion(t, "10..2.33") + assertInvalidVersion(t, "102...33") + + // invalid characters + assertInvalidVersion(t, "a.2.3") + assertInvalidVersion(t, "23!") + assertInvalidVersion(t, "23.#5") + assertInvalidVersion(t, "") + + // more numbers + assertInvalidVersion(t, "2.3.4.567") + assertInvalidVersion(t, "2.3.4.5.6.7") + + // prerelease if provided should always have major, minor, patch + assertInvalidVersion(t, "10.2.alpha") + assertInvalidVersion(t, "10.alpha") + assertInvalidVersion(t, "alpha-1.2.3") + + // prerelease should be separated by a hyphen after patch + assertInvalidVersion(t, "10.2.3alpha") + assertInvalidVersion(t, "10.2.3alpha-1.2.3") + + // negative numbers + assertInvalidVersion(t, "-10.1") + assertInvalidVersion(t, "10.-1") +} + +func TestValidVersions(t *testing.T) { + assertValidVersions(t, "100.2") + assertValidVersions(t, "0.102.39") + assertValidVersions(t, "0.0.0") + + // versions with leading 0s would be converted to int + assertValidVersions(t, "01.02") + assertValidVersions(t, "001.001100.000900") + + // prerelease tags + assertValidVersions(t, "10.20.30-alpha") + assertValidVersions(t, "10.20.30-1.x.y") + assertValidVersions(t, "10.20.30-aslkjd") + assertValidVersions(t, "10.20.30-b894") + assertValidVersions(t, "10.20.30-b8c9") +} + +func TestVersionComparison(t *testing.T) { + // EQUALS case + assertVersionComparison(t, "66.12.23", OpIs, "66.12.23") + // patch if not specified equals 0 + assertVersionComparison(t, "5.6", OpIs, "5.6.0") + // leading 0s are not stored when parsed + assertVersionComparison(t, "06.007.0008", OpIs, "6.7.8") + // with pre release + assertVersionComparison(t, "1.23.4-b-1.x.y", OpIs, "1.23.4-b-1.x.y") + + // DOES NOT EQUAL case + assertVersionComparison(t, "1.23.4-alpha-1.2", OpIsNot, "1.23.4-alpha-1") + // trailing 0s aren't stripped + assertVersionComparison(t, "1.2.300", OpIsNot, "1.2.3") + assertVersionComparison(t, "1.20.3", OpIsNot, "1.2.3") + + // LESS THAN case + // patch of .1 makes it greater + assertVersionComparison(t, "50.2", OpVersionLessThan, "50.2.1") + // minor 9 > minor 20 + assertVersionComparison(t, "20.9", OpVersionLessThan, "20.20") + // same version with pre release should be lesser + assertVersionComparison(t, "20.9.4-alpha1", OpVersionLessThan, "20.9.4") + // compare prerelease as strings + assertVersionComparison(t, "20.9.4-a-1.2.3", OpVersionLessThan, "20.9.4-a-1.3") + // since prerelease is compared as strings a1.23 < a1.5 because 2 < 5 + assertVersionComparison(t, "20.9.4-a1.23", OpVersionLessThan, "20.9.4-a1.5") + + // GREATER THAN case + assertVersionComparison(t, "12.30.2", OpVersionGreaterThan, "12.4.1") + // 100 > 1 + assertVersionComparison(t, "7.100", OpVersionGreaterThan, "7.1") + // 10 > 9 + assertVersionComparison(t, "7.10", OpVersionGreaterThan, "7.9") + // converts to 7.10.20 > 7.9.1 + assertVersionComparison(t, "07.010.0020", OpVersionGreaterThan, "7.009.1") + // patch comparison comes first + assertVersionComparison(t, "20.5.6-b1.2.x", OpVersionGreaterThan, "20.5.5") +} + +func assertInvalidVersion(t *testing.T, ver string) { + if parseVersion(ver) != nil { + t.Fatalf("expected invalid version %v", ver) + } +} + +func assertValidVersions(t *testing.T, ver string) { + if parseVersion(ver) == nil { + t.Fatalf("expected valid version %v", ver) + } +} + +func assertVersionComparison(t *testing.T, v1, op, v2 string) { + sv1 := parseVersion(v1) + if sv1 == nil { + t.Fatalf("expected valid version %v", v1) + } + sv2 := parseVersion(v2) + if sv2 == nil { + t.Fatalf("expected valid version %v", v2) + } + switch op { + case OpIs: + if versionCompare(*sv1, *sv2) != 0 { + t.Fatalf("expected %v == %v", v1, v2) + } + case OpIsNot: + if versionCompare(*sv1, *sv2) == 0 { + t.Fatalf("expected %v != %v", v1, v2) + } + case OpVersionLessThan: + if versionCompare(*sv1, *sv2) >= 0 { + t.Fatalf("expected %v < %v", v1, v2) + } + case OpVersionGreaterThan: + if versionCompare(*sv1, *sv2) <= 0 { + t.Fatalf("expected %v < %v", v1, v2) + } + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 8644aa6..1af7b4d 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -6,26 +6,49 @@ import ( "os" ) +type Level int + +const ( + Verbose Level = iota + Debug + Error +) + type Log struct { - logger *log.Logger - isDebug bool + logger *log.Logger + level Level } func New(debug bool) *Log { + var level Level + if debug { + level = Debug + } else { + level = Error + } return &Log{ - logger: log.New(os.Stderr, "", log.LstdFlags), - isDebug: debug, + logger: log.New(os.Stderr, "", log.LstdFlags), + level: level, + } +} + +func (l *Log) Verbose(format string, args ...interface{}) { + if l.level <= Verbose { + format = fmt.Sprintf("DEBUG - %v\n", format) + l.logger.Printf(format, args...) } } func (l *Log) Debug(format string, args ...interface{}) { - if l.isDebug { + if l.level <= Debug { format = fmt.Sprintf("DEBUG - %v\n", format) l.logger.Printf(format, args...) } } func (l *Log) Error(format string, args ...interface{}) { - format = fmt.Sprintf("ERROR - %v\n", format) - l.logger.Printf(format, args...) + if l.level <= Error { + format = fmt.Sprintf("ERROR - %v\n", format) + l.logger.Printf(format, args...) + } } diff --git a/pkg/experiment/local/assignment.go b/pkg/experiment/local/assignment.go index 8b0a747..f918513 100644 --- a/pkg/experiment/local/assignment.go +++ b/pkg/experiment/local/assignment.go @@ -9,15 +9,15 @@ import ( type assignment struct { user *experiment.User - results *evaluationResult - timestamp int + results map[string]experiment.Variant + timestamp int64 } -func newAssignment(user *experiment.User, results *evaluationResult) *assignment { +func newAssignment(user *experiment.User, results map[string]experiment.Variant) *assignment { assignment := &assignment{ user: user, results: results, - timestamp: int(time.Now().UnixNano() / int64(time.Millisecond)), + timestamp: time.Now().UnixMilli(), } return assignment @@ -33,14 +33,14 @@ func (a *assignment) Canonicalize() string { sb.WriteString(" ") } - keys := make([]string, 0, len(*a.results)) - for key := range *a.results { + keys := make([]string, 0, len(a.results)) + for key := range a.results { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { - value := (*a.results)[key].Variant.Key + value := a.results[key].Key sb.WriteString(key) sb.WriteString(" ") sb.WriteString(value) diff --git a/pkg/experiment/local/assignment_filter.go b/pkg/experiment/local/assignment_filter.go index ed223e6..9334a28 100644 --- a/pkg/experiment/local/assignment_filter.go +++ b/pkg/experiment/local/assignment_filter.go @@ -19,7 +19,7 @@ func newAssignmentFilter(size int) *assignmentFilter { } func (f *assignmentFilter) shouldTrack(assignment *assignment) bool { - if len(*assignment.results) == 0 { + if len(assignment.results) == 0 { return false } canonicalAssignment := assignment.Canonicalize() diff --git a/pkg/experiment/local/assignment_filter_test.go b/pkg/experiment/local/assignment_filter_test.go index ac9b75a..8a7246c 100644 --- a/pkg/experiment/local/assignment_filter_test.go +++ b/pkg/experiment/local/assignment_filter_test.go @@ -12,18 +12,13 @@ func TestSingleAssignment(t *testing.T) { DeviceId: "device", } - results := &evaluationResult{ - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "on", - }, - IsDefaultVariant: false, + results := map[string]experiment.Variant{ + "flag-key-1": { + Key: "on", }, - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "control", - }, - IsDefaultVariant: true, + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, } @@ -40,18 +35,13 @@ func TestDuplicateAssignment(t *testing.T) { DeviceId: "device", } - results := &evaluationResult{ - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "on", - }, - IsDefaultVariant: false, + results := map[string]experiment.Variant{ + "flag-key-1": { + Key: "on", }, - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "control", - }, - IsDefaultVariant: true, + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, } @@ -72,33 +62,23 @@ func TestSameUserDifferentResults(t *testing.T) { DeviceId: "device", } - results1 := &evaluationResult{ - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "on", - }, - IsDefaultVariant: false, + results1 := map[string]experiment.Variant{ + "flag-key-1": { + Key: "on", }, - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "control", - }, - IsDefaultVariant: true, + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, } - results2 := &evaluationResult{ - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "control", - }, - IsDefaultVariant: true, + results2 := map[string]experiment.Variant{ + "flag-key-1": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "on", - }, - IsDefaultVariant: false, + "flag-key-2": { + Key: "on", }, } @@ -124,18 +104,13 @@ func TestSameResultsDifferentUser(t *testing.T) { DeviceId: "different-device", } - results := &evaluationResult{ - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "on", - }, - IsDefaultVariant: false, + results := map[string]experiment.Variant{ + "flag-key-1": { + Key: "on", }, - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "control", - }, - IsDefaultVariant: true, + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, } @@ -161,7 +136,7 @@ func TestEmptyResult(t *testing.T) { DeviceId: "different-device", } - results := &evaluationResult{} + results := map[string]experiment.Variant{} assignment1 := newAssignment(user1, results) assignment2 := newAssignment(user1, results) @@ -184,33 +159,23 @@ func TestDuplicateAssignmentsWithDifferentResultOrder(t *testing.T) { DeviceId: "device", } - results1 := &evaluationResult{ - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "on", - }, - IsDefaultVariant: false, + results1 := map[string]experiment.Variant{ + "flag-key-1": { + Key: "on", }, - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "control", - }, - IsDefaultVariant: true, + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, } - results2 := &evaluationResult{ - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "control", - }, - IsDefaultVariant: true, + results2 := map[string]experiment.Variant{ + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "on", - }, - IsDefaultVariant: false, + "flag-key-1": { + Key: "on", }, } @@ -241,18 +206,13 @@ func TestLRUReplacement(t *testing.T) { DeviceId: "device", } - results := &evaluationResult{ - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "on", - }, - IsDefaultVariant: false, + results := map[string]experiment.Variant{ + "flag-key-1": { + Key: "on", }, - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "control", - }, - IsDefaultVariant: true, + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, } @@ -285,18 +245,13 @@ func TestTTLBasedEviction(t *testing.T) { DeviceId: "different-device", } - results := &evaluationResult{ - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "on", - }, - IsDefaultVariant: false, + results := map[string]experiment.Variant{ + "flag-key-1": { + Key: "on", }, - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "control", - }, - IsDefaultVariant: true, + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, } diff --git a/pkg/experiment/local/assignment_service.go b/pkg/experiment/local/assignment_service.go index ce0a034..7dad9f4 100644 --- a/pkg/experiment/local/assignment_service.go +++ b/pkg/experiment/local/assignment_service.go @@ -7,7 +7,6 @@ import ( const dayMillis = 24 * 60 * 60 * 1000 const flagTypeMutualExclusionGroup = "mutual-exclusion-group" -const flagTypeHoldoutGroup = "mutual-holdout-group" type assignmentService struct { amplitude *amplitude.Client @@ -31,21 +30,29 @@ func toEvent(assignment *assignment) amplitude.Event { } // Loop to set event_properties - for resultsKey, result := range *assignment.results { - event.EventProperties[fmt.Sprintf("%s.variant", resultsKey)] = result.Variant.Key + for resultsKey, result := range assignment.results { + version, _ := result.Metadata["version"].(int) + segmentName, _ := result.Metadata["segmentName"].(string) + event.EventProperties[fmt.Sprintf("%s.variant", resultsKey)] = result.Key + if version != 0 && len(segmentName) > 0 { + details := fmt.Sprintf("v%v rule:%v", version, segmentName) + event.EventProperties[fmt.Sprintf("%s.details", resultsKey)] = details + } } set := make(map[string]interface{}) unset := make(map[string]interface{}) // Loop to set user_properties - for resultsKey, result := range *assignment.results { - if result.Type == flagTypeMutualExclusionGroup { + for resultsKey, result := range assignment.results { + flagType, _ := result.Metadata["flagType"].(string) + isDefault, _ := result.Metadata["default"].(bool) + if flagType == flagTypeMutualExclusionGroup { continue - } else if result.IsDefaultVariant { + } else if isDefault { unset[fmt.Sprintf("[Experiment] %s", resultsKey)] = "-" } else { - set[fmt.Sprintf("[Experiment] %s", resultsKey)] = result.Variant.Key + set[fmt.Sprintf("[Experiment] %s", resultsKey)] = result.Key } } diff --git a/pkg/experiment/local/assignment_service_test.go b/pkg/experiment/local/assignment_service_test.go index 0b3cca8..86cd36e 100644 --- a/pkg/experiment/local/assignment_service_test.go +++ b/pkg/experiment/local/assignment_service_test.go @@ -3,6 +3,7 @@ package local import ( "fmt" "github.com/amplitude/experiment-go-server/pkg/experiment" + "reflect" "testing" ) @@ -12,18 +13,80 @@ func TestToEvent(t *testing.T) { DeviceId: "device", } - results := &evaluationResult{ - "flag-key-1": flagResult{ - Variant: evaluationVariant{ - Key: "on", + results := map[string]experiment.Variant{ + "flag-key-1": { + Key: "on", + Metadata: map[string]interface{}{ + "segmentName": "Segment", + "version": 13, }, - IsDefaultVariant: false, }, - "flag-key-2": flagResult{ - Variant: evaluationVariant{ - Key: "control", + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{ + "default": true, + "segmentName": "All Other Users", + "version": 12, }, - IsDefaultVariant: true, + }, + } + + assignment := newAssignment(user, results) + event := toEvent(assignment) + canonicalization := "user device flag-key-1 on flag-key-2 control " + expectedInsertID := fmt.Sprintf("user device %d %d", hashCode(canonicalization), assignment.timestamp/dayMillis) + if event.UserID != "user" { + t.Errorf("UserID was %s, expected %s", event.UserID, "user") + } + if event.DeviceID != "device" { + t.Errorf("DeviceID was %s, expected %s", event.DeviceID, "device") + } + if len(event.UserProperties) != 2 { + t.Errorf("Length of UserProperties was %d, expected %d", len(event.UserProperties), 2) + } + if len(event.UserProperties["$set"]) != 1 { + t.Errorf("Length of UserProperties.$set was %d, expected %d", len(event.UserProperties["$set"]), 1) + } + setUserProperty := event.UserProperties["$set"]["[Experiment] flag-key-1"] + if setUserProperty != "on" { + t.Errorf("Unexpected user property value%d", setUserProperty) + } + if len(event.UserProperties["$unset"]) != 1 { + t.Errorf("Length of UserProperties.$unset was %d, expected %d", len(event.UserProperties["$unset"]), 1) + } + unsetUserProperty := event.UserProperties["$unset"]["[Experiment] flag-key-2"] + if unsetUserProperty != "-" { + t.Errorf("Unexpected user property value%d", setUserProperty) + } + expectedEventProperties := map[string]interface{}{ + "flag-key-1.variant": "on", + "flag-key-1.details": "v13 rule:Segment", + "flag-key-2.variant": "control", + "flag-key-2.details": "v12 rule:All Other Users", + } + if !reflect.DeepEqual(expectedEventProperties, event.EventProperties) { + t.Errorf("Unexpected event properties %v", event.EventProperties) + + } + if event.InsertID != expectedInsertID { + t.Errorf("InsertID was %s, expected %s", event.InsertID, expectedInsertID) + } + +} + +func TestToEventNoDetails(t *testing.T) { + user := &experiment.User{ + UserId: "user", + DeviceId: "device", + } + + results := map[string]experiment.Variant{ + "flag-key-1": { + Key: "on", + }, + "flag-key-2": { + Key: "control", + Metadata: map[string]interface{}{"default": true}, }, } diff --git a/pkg/experiment/local/client.go b/pkg/experiment/local/client.go index be65865..33520c6 100644 --- a/pkg/experiment/local/client.go +++ b/pkg/experiment/local/client.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" "net/url" + "reflect" "sync" "github.com/amplitude/experiment-go-server/internal/evaluation" @@ -26,7 +27,9 @@ type Client struct { config *Config client *http.Client poller *poller - flags map[string]interface{} + flags map[string]*evaluation.Flag + flagsMutex *sync.RWMutex + engine *evaluation.Engine assignmentService *assignmentService } @@ -38,94 +41,152 @@ func Initialize(apiKey string, config *Config) *Client { panic("api key must be set") } config = fillConfigDefaults(config) + log := logger.New(config.Debug) + var as *assignmentService + if config.AssignmentConfig != nil && config.AssignmentConfig.Config.IsValid() { + amplitudeClient := amplitude.NewClient(config.AssignmentConfig.Config) + as = &assignmentService{ + amplitude: &litudeClient, + filter: newAssignmentFilter(config.AssignmentConfig.CacheCapacity), + } + } client = &Client{ - log: logger.New(config.Debug), - apiKey: apiKey, - config: config, - client: &http.Client{}, - poller: newPoller(), + log: log, + apiKey: apiKey, + config: config, + client: &http.Client{}, + poller: newPoller(), + flags: make(map[string]*evaluation.Flag), + flagsMutex: &sync.RWMutex{}, + engine: evaluation.NewEngine(log), + assignmentService: as, } client.log.Debug("config: %v", *config) - } - // create assignment service if apikey is provided - if config.AssignmentConfig != nil && config.AssignmentConfig.Config.IsValid() { - instance := amplitude.NewClient(config.AssignmentConfig.Config) - filter := newAssignmentFilter(config.AssignmentConfig.CacheCapacity) - client.assignmentService = &assignmentService{ - amplitude: &instance, filter: filter, - } + clients[apiKey] = client } initMutex.Unlock() return client } func (c *Client) Start() error { - result, err := c.doFlags() + result, err := c.doFlagsV2() if err != nil { return err } c.flags = result c.poller.Poll(c.config.FlagConfigPollerInterval, func() { - result, err := c.doFlags() + result, err := c.doFlagsV2() if err != nil { return } + c.flagsMutex.Lock() c.flags = result + c.flagsMutex.Unlock() }) return nil } +// Deprecated: Use EvaluateV2 func (c *Client) Evaluate(user *experiment.User, flagKeys []string) (map[string]experiment.Variant, error) { - variants := make(map[string]experiment.Variant) - if len(c.flags) == 0 { - c.log.Debug("evaluate: no flags") - return variants, nil - } - userJson, err := json.Marshal(user) + variants, err := c.EvaluateV2(user, flagKeys) if err != nil { return nil, err } + results := make(map[string]experiment.Variant) + for key, variant := range variants { + isDefault, ok := variant.Metadata["default"].(bool) + if !ok { + isDefault = false + } + isDeployed, ok := variant.Metadata["deployed"].(bool) + if !ok { + isDeployed = true + } + if !isDefault && isDeployed { + results[key] = variant + } + } + return results, nil +} + +func (c *Client) EvaluateV2(user *experiment.User, flagKeys []string) (map[string]experiment.Variant, error) { + userContext := evaluation.UserToContext(user) + c.flagsMutex.RLock() sortedFlags, err := topologicalSort(c.flags, flagKeys) + c.flagsMutex.RUnlock() if err != nil { return nil, err } - flagsJson, err := json.Marshal(sortedFlags) + c.log.Debug("evaluate:\n\t- user: %v\n\t- flags: %v\n", user, sortedFlags) + results := c.engine.Evaluate(userContext, sortedFlags) + variants := make(map[string]experiment.Variant) + for key, result := range results { + variants[key] = experiment.Variant{ + Key: result.Key, + Value: coerceString(result.Value), + Payload: result.Payload, + Metadata: result.Metadata, + } + } + if c.assignmentService != nil { + c.assignmentService.Track(newAssignment(user, variants)) + } + return variants, nil +} + +func (c *Client) FlagsV2() (string, error) { + flags, err := c.doFlagsV2() + if err != nil { + return "", err + } + flagsJson, err := json.Marshal(flags) + if err != nil { + return "", err + } + flagsString := string(flagsJson) + return flagsString, nil +} + +func (c *Client) doFlagsV2() (map[string]*evaluation.Flag, error) { + client := &http.Client{} + endpoint, err := url.Parse("https://api.lab.amplitude.com/") if err != nil { return nil, err } - c.log.Debug("evaluate:\n\t- user: %v\n\t- rules: %v\n", string(userJson), string(flagsJson)) - resultJson := evaluation.Evaluate(string(flagsJson), string(userJson)) - c.log.Debug("evaluate result: %v\n", resultJson) - var interopResult *interopResult - err = json.Unmarshal([]byte(resultJson), &interopResult) + endpoint.Path = "sdk/v2/flags" + endpoint.RawQuery = "v=0" + ctx, cancel := context.WithTimeout(context.Background(), c.config.FlagConfigPollerRequestTimeout) + defer cancel() + req, err := http.NewRequest("GET", endpoint.String(), nil) if err != nil { return nil, err } - if interopResult.Error != nil { - return nil, fmt.Errorf("evaluation resulted in error: %v", *interopResult.Error) + req = req.WithContext(ctx) + req.Header.Set("Authorization", fmt.Sprintf("Api-Key %s", c.apiKey)) + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + req.Header.Set("X-Amp-Exp-Library", fmt.Sprintf("experiment-go-server/%v", experiment.VERSION)) + resp, err := client.Do(req) + if err != nil { + return nil, err } - result := interopResult.Result - assignmentResult := evaluationResult{} - - filter := len(flagKeys) != 0 - for k, v := range *result { - included := !filter || contains(flagKeys, k) - if !v.IsDefaultVariant && included { - variants[k] = experiment.Variant{ - Value: v.Variant.Key, - Payload: v.Variant.Payload, - } - } - if included || v.Type == flagTypeMutualExclusionGroup || v.Type == flagTypeHoldoutGroup { - assignmentResult[k] = v - } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err } - if c.assignmentService != nil { - (*c.assignmentService).Track(newAssignment(user, &assignmentResult)) + var flagsArray []*evaluation.Flag + err = json.Unmarshal(body, &flagsArray) + if err != nil { + return nil, err } - return variants, nil + flags := make(map[string]*evaluation.Flag) + for _, flag := range flagsArray { + flags[flag.Key] = flag + } + return flags, nil } +// Deprecated: This function returns an old data model that is no longer used. func (c *Client) Rules() (map[string]interface{}, error) { return c.doRules() } @@ -170,6 +231,7 @@ func (c *Client) doRules() (map[string]interface{}, error) { return result, nil } +// Deprecated: This function returns an old data model that is no longer used. func (c *Client) Flags() (*string, error) { flags, err := c.doFlags() if err != nil { @@ -236,3 +298,17 @@ func contains(s []string, e string) bool { } return false } + +func coerceString(value interface{}) string { + if value == nil { + return "" + } + kind := reflect.TypeOf(value).Kind() + if kind == reflect.Map || kind == reflect.Slice || kind == reflect.Array { + b, err := json.Marshal(value) + if err == nil { + return string(b) + } + } + return fmt.Sprintf("%v", value) +} diff --git a/pkg/experiment/local/client_test.go b/pkg/experiment/local/client_test.go new file mode 100644 index 0000000..761bf87 --- /dev/null +++ b/pkg/experiment/local/client_test.go @@ -0,0 +1,145 @@ +package local + +import ( + "github.com/amplitude/experiment-go-server/pkg/experiment" + "testing" +) + +var client *Client + +func init() { + client = Initialize("server-qz35UwzJ5akieoAdIgzM4m9MIiOLXLoz", nil) + err := client.Start() + if err != nil { + panic(err) + } +} + +func TestClientInitialize(t *testing.T) { + client1 := Initialize("apiKey1", nil) + client2 := Initialize("apiKey1", nil) + client3 := Initialize("apiKey2", nil) + if client1 != client2 { + t.Fatalf("Expected equal client references.") + } + if client1 == client3 { + t.Fatalf("Expected different client references.") + } +} + +func TestEvaluate(t *testing.T) { + user := &experiment.User{UserId: "test_user"} + result, err := client.Evaluate(user, nil) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + variant := result["sdk-local-evaluation-ci-test"] + if variant.Key != "on" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Value != "on" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Payload != "payload" { + t.Fatalf("Unexpected variant %v", variant) + } + variant = result["sdk-ci-test"] + if variant.Key != "" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Value != "" { + t.Fatalf("Unexpected variant %v", variant) + } +} + + +func TestEvaluateV2AllFlags(t *testing.T) { + user := &experiment.User{UserId: "test_user"} + result, err := client.EvaluateV2(user, nil) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + variant := result["sdk-local-evaluation-ci-test"] + if variant.Key != "on" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Value != "on" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Payload != "payload" { + t.Fatalf("Unexpected variant %v", variant) + } + variant = result["sdk-ci-test"] + if variant.Key != "off" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Value != "" { + t.Fatalf("Unexpected variant %v", variant) + } +} + +func TestEvaluateV2OneFlag(t *testing.T) { + user := &experiment.User{UserId: "test_user"} + flagKeys := []string{"sdk-local-evaluation-ci-test"} + result, err := client.EvaluateV2(user, flagKeys) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + variant := result["sdk-local-evaluation-ci-test"] + if variant.Key != "on" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Value != "on" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Payload != "payload" { + t.Fatalf("Unexpected variant %v", variant) + } +} + +func TestEvaluateV2AllFlagsWithDependencies(t *testing.T) { + user := &experiment.User{UserId: "user_id", DeviceId: "device_id"} + result, err := client.EvaluateV2(user, nil) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + variant := result["sdk-ci-local-dependencies-test"] + if variant.Key != "control" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Value != "control" { + t.Fatalf("Unexpected variant %v", variant) + } +} + +func TestEvaluateV2OneFlagWithDependencies(t *testing.T) { + user := &experiment.User{UserId: "user_id", DeviceId: "device_id"} + flagKeys := []string{"sdk-ci-local-dependencies-test"} + result, err := client.EvaluateV2(user, flagKeys) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + variant := result["sdk-ci-local-dependencies-test"] + if variant.Key != "control" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Value != "control" { + t.Fatalf("Unexpected variant %v", variant) + } +} + +func TestEvaluateV2UnknownFlagKey(t *testing.T) { + user := &experiment.User{UserId: "user_id", DeviceId: "device_id"} + flagKeys := []string{"does-not-exist"} + result, err := client.EvaluateV2(user, flagKeys) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + variant := result["sdk-local-dependencies-test"] + if variant.Key != "" { + t.Fatalf("Unexpected variant %v", variant) + } + if variant.Value != "" { + t.Fatalf("Unexpected variant %v", variant) + } +} diff --git a/pkg/experiment/local/config.go b/pkg/experiment/local/config.go index 12973e5..4087f46 100644 --- a/pkg/experiment/local/config.go +++ b/pkg/experiment/local/config.go @@ -26,7 +26,7 @@ var DefaultConfig = &Config{ } var DefaultAssignmentConfig = &AssignmentConfig{ - CacheCapacity: 65536, + CacheCapacity: 524288, } func fillConfigDefaults(c *Config) *Config { diff --git a/pkg/experiment/local/sort.go b/pkg/experiment/local/sort.go index a45bd53..e56e3ab 100644 --- a/pkg/experiment/local/sort.go +++ b/pkg/experiment/local/sort.go @@ -1,12 +1,15 @@ package local -import "fmt" +import ( + "fmt" + "github.com/amplitude/experiment-go-server/internal/evaluation" +) -func topologicalSort(flags map[string]interface{}, flagKeys []string) ([]interface{}, error) { - result := make([]interface{}, 0) +func topologicalSort(flags map[string]*evaluation.Flag, flagKeys []string) ([]*evaluation.Flag, error) { + result := make([]*evaluation.Flag, 0) // Extract keys and copy flags map keys := make([]string, 0) - available := make(map[string]interface{}) + available := make(map[string]*evaluation.Flag) for k, v := range flags { keys = append(keys, k) available[k] = v @@ -31,18 +34,18 @@ func topologicalSort(flags map[string]interface{}, flagKeys []string) ([]interfa return result, nil } -func parentTraversal(flagKey string, available map[string]interface{}, path []string) ([]interface{}, error) { +func parentTraversal(flagKey string, available map[string]*evaluation.Flag, path []string) ([]*evaluation.Flag, error) { flag := available[flagKey] if flag == nil { return nil, nil } - dependencies := extractDependencies(flag) + dependencies := flag.Dependencies if len(dependencies) == 0 { delete(available, flagKey) - return []interface{}{flag}, nil + return []*evaluation.Flag{flag}, nil } path = append(path, flagKey) - result := make([]interface{}, 0) + result := make([]*evaluation.Flag, 0) for _, parentKey := range dependencies { if contains(path, parentKey) { return nil, fmt.Errorf("detected a cycle between flags %v", path) @@ -59,29 +62,3 @@ func parentTraversal(flagKey string, available map[string]interface{}, path []st delete(available, flagKey) return result, nil } - -func extractDependencies(flag interface{}) []string { - switch f := flag.(type) { - case map[string]interface{}: - parentDependenciesAny := f["parentDependencies"] - if parentDependenciesAny == nil { - return nil - } - switch parentDependencies := parentDependenciesAny.(type) { - case map[string]interface{}: - flagsAny := parentDependencies["flags"] - if flagsAny == nil { - return nil - } - switch flags := flagsAny.(type) { - case map[string]interface{}: - result := make([]string, 0) - for k := range flags { - result = append(result, k) - } - return result - } - } - } - return nil -} diff --git a/pkg/experiment/local/sort_test.go b/pkg/experiment/local/sort_test.go index c45b370..5ca5895 100644 --- a/pkg/experiment/local/sort_test.go +++ b/pkg/experiment/local/sort_test.go @@ -1,6 +1,7 @@ package local import ( + "github.com/amplitude/experiment-go-server/internal/evaluation" "reflect" "testing" ) @@ -31,27 +32,27 @@ func TestEmpty(t *testing.T) { func TestSingleFlagNoDependencies(t *testing.T) { // No flag keys { - inputFlags := flagsArray(flag{Key: "1", Dependencies: []string{}}) + inputFlags := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{}}) inputFlagKeys := make([]string, 0) actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) - expected := flagsArray(flag{Key: "1", Dependencies: []string{}}) + expected := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{}}) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) } } // With flag keys { - inputFlags := flagsArray(flag{Key: "1", Dependencies: []string{}}) + inputFlags := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{}}) inputFlagKeys := []string{"1"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) - expected := flagsArray(flag{Key: "1", Dependencies: []string{}}) + expected := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{}}) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) } } // With flag no match { - inputFlags := flagsArray(flag{Key: "1", Dependencies: []string{}}) + inputFlags := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{}}) inputFlagKeys := []string{"999"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray() @@ -64,27 +65,27 @@ func TestSingleFlagNoDependencies(t *testing.T) { func TestSingleFlagWithDependencies(t *testing.T) { // No flag keys { - inputFlags := flagsArray(flag{Key: "1", Dependencies: []string{"2"}}) + inputFlags := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{"2"}}) inputFlagKeys := make([]string, 0) actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) - expected := flagsArray(flag{Key: "1", Dependencies: []string{"2"}}) + expected := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{"2"}}) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) } } // With flag keys { - inputFlags := flagsArray(flag{Key: "1", Dependencies: []string{"2"}}) + inputFlags := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{"2"}}) inputFlagKeys := []string{"1"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) - expected := flagsArray(flag{Key: "1", Dependencies: []string{"2"}}) + expected := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{"2"}}) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) } } // With flag no match { - inputFlags := flagsArray(flag{Key: "1", Dependencies: []string{"2"}}) + inputFlags := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{"2"}}) inputFlagKeys := []string{"999"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray() @@ -97,13 +98,13 @@ func TestMultipleFlagsNoDependencies(t *testing.T) { // No flag keys { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{}}, - flag{Key: "2", Dependencies: []string{}}) + evaluation.Flag{Key: "1", Dependencies: []string{}}, + evaluation.Flag{Key: "2", Dependencies: []string{}}) inputFlagKeys := make([]string, 0) actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray( - flag{Key: "1", Dependencies: []string{}}, - flag{Key: "2", Dependencies: []string{}}) + evaluation.Flag{Key: "1", Dependencies: []string{}}, + evaluation.Flag{Key: "2", Dependencies: []string{}}) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) } @@ -111,13 +112,13 @@ func TestMultipleFlagsNoDependencies(t *testing.T) { // With flag keys { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{}}, - flag{Key: "2", Dependencies: []string{}}) + evaluation.Flag{Key: "1", Dependencies: []string{}}, + evaluation.Flag{Key: "2", Dependencies: []string{}}) inputFlagKeys := []string{"1", "2"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray( - flag{Key: "1", Dependencies: []string{}}, - flag{Key: "2", Dependencies: []string{}}) + evaluation.Flag{Key: "1", Dependencies: []string{}}, + evaluation.Flag{Key: "2", Dependencies: []string{}}) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) } @@ -125,8 +126,8 @@ func TestMultipleFlagsNoDependencies(t *testing.T) { // With flag no match { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{}}, - flag{Key: "2", Dependencies: []string{}}) + evaluation.Flag{Key: "1", Dependencies: []string{}}, + evaluation.Flag{Key: "2", Dependencies: []string{}}) inputFlagKeys := []string{"99", "999"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray() @@ -139,15 +140,15 @@ func TestMultipleFlagWithDependencies(t *testing.T) { // No flag keys { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{"2"}}, - flag{Key: "2", Dependencies: []string{"3"}}, - flag{Key: "3", Dependencies: []string{}}) + evaluation.Flag{Key: "1", Dependencies: []string{"2"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"3"}}, + evaluation.Flag{Key: "3", Dependencies: []string{}}) inputFlagKeys := make([]string, 0) actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray( - flag{Key: "3", Dependencies: []string{}}, - flag{Key: "2", Dependencies: []string{"3"}}, - flag{Key: "1", Dependencies: []string{"2"}}) + evaluation.Flag{Key: "3", Dependencies: []string{}}, + evaluation.Flag{Key: "2", Dependencies: []string{"3"}}, + evaluation.Flag{Key: "1", Dependencies: []string{"2"}}) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) } @@ -155,15 +156,15 @@ func TestMultipleFlagWithDependencies(t *testing.T) { // With flag keys { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{"2"}}, - flag{Key: "2", Dependencies: []string{"3"}}, - flag{Key: "3", Dependencies: []string{}}) + evaluation.Flag{Key: "1", Dependencies: []string{"2"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"3"}}, + evaluation.Flag{Key: "3", Dependencies: []string{}}) inputFlagKeys := []string{"1", "2"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray( - flag{Key: "3", Dependencies: []string{}}, - flag{Key: "2", Dependencies: []string{"3"}}, - flag{Key: "1", Dependencies: []string{"2"}}) + evaluation.Flag{Key: "3", Dependencies: []string{}}, + evaluation.Flag{Key: "2", Dependencies: []string{"3"}}, + evaluation.Flag{Key: "1", Dependencies: []string{"2"}}) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) } @@ -171,9 +172,9 @@ func TestMultipleFlagWithDependencies(t *testing.T) { // With flag no match { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{"2"}}, - flag{Key: "2", Dependencies: []string{"3"}}, - flag{Key: "3", Dependencies: []string{}}) + evaluation.Flag{Key: "1", Dependencies: []string{"2"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"3"}}, + evaluation.Flag{Key: "3", Dependencies: []string{}}) inputFlagKeys := []string{"999"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray() @@ -185,7 +186,7 @@ func TestMultipleFlagWithDependencies(t *testing.T) { func TestSingleFlagCycle(t *testing.T) { // No flag keys { - inputFlags := flagsArray(flag{Key: "1", Dependencies: []string{"1"}}) + inputFlags := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{"1"}}) inputFlagKeys := make([]string, 0) _, err := topologicalSortArray(inputFlags, inputFlagKeys) if err == nil { @@ -194,7 +195,7 @@ func TestSingleFlagCycle(t *testing.T) { } // With flag keys { - inputFlags := flagsArray(flag{Key: "1", Dependencies: []string{"1"}}) + inputFlags := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{"1"}}) inputFlagKeys := []string{"1"} _, err := topologicalSortArray(inputFlags, inputFlagKeys) if err == nil { @@ -204,7 +205,7 @@ func TestSingleFlagCycle(t *testing.T) { } // With flag no match { - inputFlags := flagsArray(flag{Key: "1", Dependencies: []string{"1"}}) + inputFlags := flagsArray(evaluation.Flag{Key: "1", Dependencies: []string{"1"}}) inputFlagKeys := []string{"999"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray() @@ -217,8 +218,8 @@ func TestTwoFlagCycle(t *testing.T) { // No flag keys { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{"2"}}, - flag{Key: "2", Dependencies: []string{"1"}}) + evaluation.Flag{Key: "1", Dependencies: []string{"2"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"1"}}) inputFlagKeys := make([]string, 0) _, err := topologicalSortArray(inputFlags, inputFlagKeys) if err == nil { @@ -228,8 +229,8 @@ func TestTwoFlagCycle(t *testing.T) { // With flag keys { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{"2"}}, - flag{Key: "2", Dependencies: []string{"1"}}) + evaluation.Flag{Key: "1", Dependencies: []string{"2"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"1"}}) inputFlagKeys := []string{"2"} _, err := topologicalSortArray(inputFlags, inputFlagKeys) if err == nil { @@ -239,8 +240,8 @@ func TestTwoFlagCycle(t *testing.T) { // With flag no match { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{"2"}}, - flag{Key: "2", Dependencies: []string{"1"}}) + evaluation.Flag{Key: "1", Dependencies: []string{"2"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"1"}}) inputFlagKeys := []string{"999"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray() @@ -251,17 +252,17 @@ func TestTwoFlagCycle(t *testing.T) { } func TestMultipleFlagsComplexCycle(t *testing.T) { inputFlags := flagsArray( - flag{Key: "3", Dependencies: []string{"1", "2"}}, - flag{Key: "1", Dependencies: []string{}}, - flag{Key: "4", Dependencies: []string{"21", "3"}}, - flag{Key: "2", Dependencies: []string{}}, - flag{Key: "5", Dependencies: []string{"3"}}, - flag{Key: "6", Dependencies: []string{}}, - flag{Key: "7", Dependencies: []string{}}, - flag{Key: "8", Dependencies: []string{"9"}}, - flag{Key: "9", Dependencies: []string{}}, - flag{Key: "20", Dependencies: []string{"4"}}, - flag{Key: "21", Dependencies: []string{"20"}}, + evaluation.Flag{Key: "3", Dependencies: []string{"1", "2"}}, + evaluation.Flag{Key: "1", Dependencies: []string{}}, + evaluation.Flag{Key: "4", Dependencies: []string{"21", "3"}}, + evaluation.Flag{Key: "2", Dependencies: []string{}}, + evaluation.Flag{Key: "5", Dependencies: []string{"3"}}, + evaluation.Flag{Key: "6", Dependencies: []string{}}, + evaluation.Flag{Key: "7", Dependencies: []string{}}, + evaluation.Flag{Key: "8", Dependencies: []string{"9"}}, + evaluation.Flag{Key: "9", Dependencies: []string{}}, + evaluation.Flag{Key: "20", Dependencies: []string{"4"}}, + evaluation.Flag{Key: "21", Dependencies: []string{"20"}}, ) inputFlagKeys := make([]string, 0) _, err := topologicalSortArray(inputFlags, inputFlagKeys) @@ -271,36 +272,36 @@ func TestMultipleFlagsComplexCycle(t *testing.T) { } func TestComplexNoCycleStartingWithLeaf(t *testing.T) { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{"6", "3"}}, - flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, - flag{Key: "3", Dependencies: []string{"6", "5"}}, - flag{Key: "4", Dependencies: []string{"8", "7"}}, - flag{Key: "5", Dependencies: []string{"10", "7"}}, - flag{Key: "7", Dependencies: []string{"8"}}, - flag{Key: "6", Dependencies: []string{"7", "4"}}, - flag{Key: "8", Dependencies: []string{}}, - flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, - flag{Key: "10", Dependencies: []string{"7"}}, - flag{Key: "20", Dependencies: []string{}}, - flag{Key: "21", Dependencies: []string{"20"}}, - flag{Key: "30", Dependencies: []string{}}, + evaluation.Flag{Key: "1", Dependencies: []string{"6", "3"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, + evaluation.Flag{Key: "3", Dependencies: []string{"6", "5"}}, + evaluation.Flag{Key: "4", Dependencies: []string{"8", "7"}}, + evaluation.Flag{Key: "5", Dependencies: []string{"10", "7"}}, + evaluation.Flag{Key: "7", Dependencies: []string{"8"}}, + evaluation.Flag{Key: "6", Dependencies: []string{"7", "4"}}, + evaluation.Flag{Key: "8", Dependencies: []string{}}, + evaluation.Flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, + evaluation.Flag{Key: "10", Dependencies: []string{"7"}}, + evaluation.Flag{Key: "20", Dependencies: []string{}}, + evaluation.Flag{Key: "21", Dependencies: []string{"20"}}, + evaluation.Flag{Key: "30", Dependencies: []string{}}, ) inputFlagKeys := make([]string, 0) actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray( - flag{Key: "8", Dependencies: []string{}}, - flag{Key: "7", Dependencies: []string{"8"}}, - flag{Key: "4", Dependencies: []string{"8", "7"}}, - flag{Key: "6", Dependencies: []string{"7", "4"}}, - flag{Key: "10", Dependencies: []string{"7"}}, - flag{Key: "5", Dependencies: []string{"10", "7"}}, - flag{Key: "3", Dependencies: []string{"6", "5"}}, - flag{Key: "1", Dependencies: []string{"6", "3"}}, - flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, - flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, - flag{Key: "20", Dependencies: []string{}}, - flag{Key: "21", Dependencies: []string{"20"}}, - flag{Key: "30", Dependencies: []string{}}, + evaluation.Flag{Key: "8", Dependencies: []string{}}, + evaluation.Flag{Key: "7", Dependencies: []string{"8"}}, + evaluation.Flag{Key: "4", Dependencies: []string{"8", "7"}}, + evaluation.Flag{Key: "6", Dependencies: []string{"7", "4"}}, + evaluation.Flag{Key: "10", Dependencies: []string{"7"}}, + evaluation.Flag{Key: "5", Dependencies: []string{"10", "7"}}, + evaluation.Flag{Key: "3", Dependencies: []string{"6", "5"}}, + evaluation.Flag{Key: "1", Dependencies: []string{"6", "3"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, + evaluation.Flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, + evaluation.Flag{Key: "20", Dependencies: []string{}}, + evaluation.Flag{Key: "21", Dependencies: []string{"20"}}, + evaluation.Flag{Key: "30", Dependencies: []string{}}, ) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) @@ -308,36 +309,36 @@ func TestComplexNoCycleStartingWithLeaf(t *testing.T) { } func TestComplexNoCycleStartingWithMiddle(t *testing.T) { inputFlags := flagsArray( - flag{Key: "6", Dependencies: []string{"7", "4"}}, - flag{Key: "1", Dependencies: []string{"6", "3"}}, - flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, - flag{Key: "3", Dependencies: []string{"6", "5"}}, - flag{Key: "4", Dependencies: []string{"8", "7"}}, - flag{Key: "5", Dependencies: []string{"10", "7"}}, - flag{Key: "7", Dependencies: []string{"8"}}, - flag{Key: "8", Dependencies: []string{}}, - flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, - flag{Key: "10", Dependencies: []string{"7"}}, - flag{Key: "20", Dependencies: []string{}}, - flag{Key: "21", Dependencies: []string{"20"}}, - flag{Key: "30", Dependencies: []string{}}, + evaluation.Flag{Key: "6", Dependencies: []string{"7", "4"}}, + evaluation.Flag{Key: "1", Dependencies: []string{"6", "3"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, + evaluation.Flag{Key: "3", Dependencies: []string{"6", "5"}}, + evaluation.Flag{Key: "4", Dependencies: []string{"8", "7"}}, + evaluation.Flag{Key: "5", Dependencies: []string{"10", "7"}}, + evaluation.Flag{Key: "7", Dependencies: []string{"8"}}, + evaluation.Flag{Key: "8", Dependencies: []string{}}, + evaluation.Flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, + evaluation.Flag{Key: "10", Dependencies: []string{"7"}}, + evaluation.Flag{Key: "20", Dependencies: []string{}}, + evaluation.Flag{Key: "21", Dependencies: []string{"20"}}, + evaluation.Flag{Key: "30", Dependencies: []string{}}, ) inputFlagKeys := make([]string, 0) actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray( - flag{Key: "8", Dependencies: []string{}}, - flag{Key: "7", Dependencies: []string{"8"}}, - flag{Key: "4", Dependencies: []string{"8", "7"}}, - flag{Key: "6", Dependencies: []string{"7", "4"}}, - flag{Key: "10", Dependencies: []string{"7"}}, - flag{Key: "5", Dependencies: []string{"10", "7"}}, - flag{Key: "3", Dependencies: []string{"6", "5"}}, - flag{Key: "1", Dependencies: []string{"6", "3"}}, - flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, - flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, - flag{Key: "20", Dependencies: []string{}}, - flag{Key: "21", Dependencies: []string{"20"}}, - flag{Key: "30", Dependencies: []string{}}, + evaluation.Flag{Key: "8", Dependencies: []string{}}, + evaluation.Flag{Key: "7", Dependencies: []string{"8"}}, + evaluation.Flag{Key: "4", Dependencies: []string{"8", "7"}}, + evaluation.Flag{Key: "6", Dependencies: []string{"7", "4"}}, + evaluation.Flag{Key: "10", Dependencies: []string{"7"}}, + evaluation.Flag{Key: "5", Dependencies: []string{"10", "7"}}, + evaluation.Flag{Key: "3", Dependencies: []string{"6", "5"}}, + evaluation.Flag{Key: "1", Dependencies: []string{"6", "3"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, + evaluation.Flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, + evaluation.Flag{Key: "20", Dependencies: []string{}}, + evaluation.Flag{Key: "21", Dependencies: []string{"20"}}, + evaluation.Flag{Key: "30", Dependencies: []string{}}, ) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) @@ -345,36 +346,36 @@ func TestComplexNoCycleStartingWithMiddle(t *testing.T) { } func TestComplexNoCycleStartingWithRoot(t *testing.T) { inputFlags := flagsArray( - flag{Key: "8", Dependencies: []string{}}, - flag{Key: "1", Dependencies: []string{"6", "3"}}, - flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, - flag{Key: "3", Dependencies: []string{"6", "5"}}, - flag{Key: "4", Dependencies: []string{"8", "7"}}, - flag{Key: "5", Dependencies: []string{"10", "7"}}, - flag{Key: "7", Dependencies: []string{"8"}}, - flag{Key: "6", Dependencies: []string{"7", "4"}}, - flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, - flag{Key: "10", Dependencies: []string{"7"}}, - flag{Key: "20", Dependencies: []string{}}, - flag{Key: "21", Dependencies: []string{"20"}}, - flag{Key: "30", Dependencies: []string{}}, + evaluation.Flag{Key: "8", Dependencies: []string{}}, + evaluation.Flag{Key: "1", Dependencies: []string{"6", "3"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, + evaluation.Flag{Key: "3", Dependencies: []string{"6", "5"}}, + evaluation.Flag{Key: "4", Dependencies: []string{"8", "7"}}, + evaluation.Flag{Key: "5", Dependencies: []string{"10", "7"}}, + evaluation.Flag{Key: "7", Dependencies: []string{"8"}}, + evaluation.Flag{Key: "6", Dependencies: []string{"7", "4"}}, + evaluation.Flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, + evaluation.Flag{Key: "10", Dependencies: []string{"7"}}, + evaluation.Flag{Key: "20", Dependencies: []string{}}, + evaluation.Flag{Key: "21", Dependencies: []string{"20"}}, + evaluation.Flag{Key: "30", Dependencies: []string{}}, ) inputFlagKeys := make([]string, 0) actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray( - flag{Key: "8", Dependencies: []string{}}, - flag{Key: "7", Dependencies: []string{"8"}}, - flag{Key: "4", Dependencies: []string{"8", "7"}}, - flag{Key: "6", Dependencies: []string{"7", "4"}}, - flag{Key: "10", Dependencies: []string{"7"}}, - flag{Key: "5", Dependencies: []string{"10", "7"}}, - flag{Key: "3", Dependencies: []string{"6", "5"}}, - flag{Key: "1", Dependencies: []string{"6", "3"}}, - flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, - flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, - flag{Key: "20", Dependencies: []string{}}, - flag{Key: "21", Dependencies: []string{"20"}}, - flag{Key: "30", Dependencies: []string{}}, + evaluation.Flag{Key: "8", Dependencies: []string{}}, + evaluation.Flag{Key: "7", Dependencies: []string{"8"}}, + evaluation.Flag{Key: "4", Dependencies: []string{"8", "7"}}, + evaluation.Flag{Key: "6", Dependencies: []string{"7", "4"}}, + evaluation.Flag{Key: "10", Dependencies: []string{"7"}}, + evaluation.Flag{Key: "5", Dependencies: []string{"10", "7"}}, + evaluation.Flag{Key: "3", Dependencies: []string{"6", "5"}}, + evaluation.Flag{Key: "1", Dependencies: []string{"6", "3"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, + evaluation.Flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, + evaluation.Flag{Key: "20", Dependencies: []string{}}, + evaluation.Flag{Key: "21", Dependencies: []string{"20"}}, + evaluation.Flag{Key: "30", Dependencies: []string{}}, ) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) @@ -383,31 +384,31 @@ func TestComplexNoCycleStartingWithRoot(t *testing.T) { func TestComplexNoCycleWithFlagKeys(t *testing.T) { inputFlags := flagsArray( - flag{Key: "1", Dependencies: []string{"6", "3"}}, - flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, - flag{Key: "3", Dependencies: []string{"6", "5"}}, - flag{Key: "4", Dependencies: []string{"8", "7"}}, - flag{Key: "5", Dependencies: []string{"10", "7"}}, - flag{Key: "7", Dependencies: []string{"8"}}, - flag{Key: "6", Dependencies: []string{"7", "4"}}, - flag{Key: "8", Dependencies: []string{}}, - flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, - flag{Key: "10", Dependencies: []string{"7"}}, - flag{Key: "20", Dependencies: []string{}}, - flag{Key: "21", Dependencies: []string{"20"}}, - flag{Key: "30", Dependencies: []string{}}, + evaluation.Flag{Key: "1", Dependencies: []string{"6", "3"}}, + evaluation.Flag{Key: "2", Dependencies: []string{"8", "5", "3", "1"}}, + evaluation.Flag{Key: "3", Dependencies: []string{"6", "5"}}, + evaluation.Flag{Key: "4", Dependencies: []string{"8", "7"}}, + evaluation.Flag{Key: "5", Dependencies: []string{"10", "7"}}, + evaluation.Flag{Key: "7", Dependencies: []string{"8"}}, + evaluation.Flag{Key: "6", Dependencies: []string{"7", "4"}}, + evaluation.Flag{Key: "8", Dependencies: []string{}}, + evaluation.Flag{Key: "9", Dependencies: []string{"10", "7", "5"}}, + evaluation.Flag{Key: "10", Dependencies: []string{"7"}}, + evaluation.Flag{Key: "20", Dependencies: []string{}}, + evaluation.Flag{Key: "21", Dependencies: []string{"20"}}, + evaluation.Flag{Key: "30", Dependencies: []string{}}, ) inputFlagKeys := []string{"1"} actual, _ := topologicalSortArray(inputFlags, inputFlagKeys) expected := flagsArray( - flag{Key: "8", Dependencies: []string{}}, - flag{Key: "7", Dependencies: []string{"8"}}, - flag{Key: "4", Dependencies: []string{"8", "7"}}, - flag{Key: "6", Dependencies: []string{"7", "4"}}, - flag{Key: "10", Dependencies: []string{"7"}}, - flag{Key: "5", Dependencies: []string{"10", "7"}}, - flag{Key: "3", Dependencies: []string{"6", "5"}}, - flag{Key: "1", Dependencies: []string{"6", "3"}}, + evaluation.Flag{Key: "8", Dependencies: []string{}}, + evaluation.Flag{Key: "7", Dependencies: []string{"8"}}, + evaluation.Flag{Key: "4", Dependencies: []string{"8", "7"}}, + evaluation.Flag{Key: "6", Dependencies: []string{"7", "4"}}, + evaluation.Flag{Key: "10", Dependencies: []string{"7"}}, + evaluation.Flag{Key: "5", Dependencies: []string{"10", "7"}}, + evaluation.Flag{Key: "3", Dependencies: []string{"6", "5"}}, + evaluation.Flag{Key: "1", Dependencies: []string{"6", "3"}}, ) if !reflect.DeepEqual(expected, actual) { t.Fatalf("expected %v, actual %v", expected, actual) @@ -416,46 +417,23 @@ func TestComplexNoCycleWithFlagKeys(t *testing.T) { // Utilities -type flag struct { - Key string - Dependencies []string -} - -func flagsArray(flags ...flag) []interface{} { - result := make([]interface{}, 0) - for _, f := range flags { - result = append(result, flagInterface(f)) +func flagsArray(flags ...evaluation.Flag) []*evaluation.Flag { + result := make([]*evaluation.Flag, 0) + for i := 0; i < len(flags); i++ { + f := flags[i] + result = append(result, &f) } return result } -func flagInterface(flag flag) interface{} { - dependencyMap := make(map[string]interface{}) - for _, dependency := range flag.Dependencies { - dependencyMap[dependency] = true - } - return map[string]interface{}{ - "flagKey": flag.Key, - "parentDependencies": map[string]interface{}{ - "flags": dependencyMap, - }, - } -} - // Used for testing to ensure the correct ordering of iteration. -func topologicalSortArray(flags []interface{}, flagKeys []string) ([]interface{}, error) { +func topologicalSortArray(flags []*evaluation.Flag, flagKeys []string) ([]*evaluation.Flag, error) { // Extract keys and create flags map keys := make([]string, 0) - available := make(map[string]interface{}) - for _, flagAny := range flags { - switch flag := flagAny.(type) { - case map[string]interface{}: - switch flagKey := flag["flagKey"].(type) { - case string: - keys = append(keys, flagKey) - available[flagKey] = flag - } - } + available := make(map[string]*evaluation.Flag) + for _, f := range flags { + keys = append(keys, f.Key) + available[f.Key] = f } // Get the starting keys var startingKeys []string diff --git a/pkg/experiment/local/types.go b/pkg/experiment/local/types.go deleted file mode 100644 index 41adbc0..0000000 --- a/pkg/experiment/local/types.go +++ /dev/null @@ -1,20 +0,0 @@ -package local - -type evaluationVariant struct { - Key string `json:"key,omitempty"` - Payload interface{} `json:"payload,omitempty"` -} - -type flagResult struct { - Variant evaluationVariant `json:"variant,omitempty"` - Description string `json:"description,omitempty"` - IsDefaultVariant bool `json:"isDefaultVariant,omitempty"` - Type string `json:"type,omitempty"` -} - -type evaluationResult = map[string]flagResult - -type interopResult struct { - Result *evaluationResult `json:"result,omitempty"` - Error *string `json:"error,omitempty"` -} diff --git a/pkg/experiment/types.go b/pkg/experiment/types.go index 2fc096b..a57638c 100644 --- a/pkg/experiment/types.go +++ b/pkg/experiment/types.go @@ -22,6 +22,8 @@ type User struct { } type Variant struct { - Value string `json:"value,omitempty"` - Payload interface{} `json:"payload,omitempty"` + Value string `json:"value,omitempty"` + Payload interface{} `json:"payload,omitempty"` + Key string `json:"key,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` }