prefab-cloud-ruby 0.5.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 214acdf959717e10b66a8e6df483365a4283d093
4
- data.tar.gz: afabe80e96eac0f0a563fdb4a403a1038a41cb80
2
+ SHA256:
3
+ metadata.gz: 171f3c104141000ec55b8c331f9f0439d56e991936c69a032f7c78394560ddea
4
+ data.tar.gz: bb2fa93f2bc589c7e58bda95f7ab9519d780d80de212b62e1a29b1e187ab55a1
5
5
  SHA512:
6
- metadata.gz: e775bbfa7e80a1a578b89bedc01c456d052cb164745c9d151597764e0519d759d2ae041d6efa974fb410d5d9727d590fe83d64b4f7b2342d0597447ffa2c09c6
7
- data.tar.gz: 647786436076bf00240175dcaef26beb4f7fb82b99fcef382e6838da8c0762b5416c980dcae9d7c2482a9773929d1f7c33bfb8e085b3d6f2c93c19ef635b0b98
6
+ metadata.gz: c883ea3ba1f608c5fd88e0371302dca440968ad34fae8c279b735f180a73986498c9a87b95d2d51571daa2be723944568824bf8444425a8ff463c210bb9c7429
7
+ data.tar.gz: 278cccca1c2e834e79971394310fdba30c03e806b31708094d90d62ed1623c7c5d2e19cdd204165437a75a8711c979f915f0b1ecd86220991fa22697ddad3a49
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.0.3
data/CODEOWNERS ADDED
@@ -0,0 +1 @@
1
+ * @prefab-cloud/prefabdevs @prefab-cloud/prefabmaintainers @prefab-cloud/prefabadmins
data/Gemfile CHANGED
@@ -2,13 +2,16 @@ source "https://rubygems.org"
2
2
 
3
3
  gem 'concurrent-ruby', '~> 1.0', '>= 1.0.5'
4
4
  gem 'faraday'
5
- gem 'grpc', '~> 1.18.0'
5
+ gem 'grpc', :platforms => :ruby
6
+ gem 'google-protobuf', :platforms => :ruby
7
+ gem 'googleapis-common-protos-types', :platforms => :ruby
6
8
 
7
9
  group :development do
8
- gem 'grpc-tools', '~> 1.17.1'
10
+ gem 'grpc-tools', :platforms => :ruby
9
11
  gem "shoulda", ">= 0"
10
12
  gem "rdoc", "~> 3.12"
11
- gem "bundler", "~> 1.0"
13
+ gem "bundler"
12
14
  gem "juwelier", "~> 2.4.9"
13
15
  gem "simplecov", ">= 0"
16
+ gem 'thin'
14
17
  end
data/Gemfile.lock CHANGED
@@ -1,37 +1,43 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
- activesupport (5.2.0)
4
+ activesupport (5.2.4.5)
5
5
  concurrent-ruby (~> 1.0, >= 1.0.2)
6
6
  i18n (>= 0.7, < 2)
7
7
  minitest (~> 5.1)
8
8
  tzinfo (~> 1.1)
9
- addressable (2.6.0)
10
- public_suffix (>= 2.0.2, < 4.0)
11
- builder (3.2.3)
12
- concurrent-ruby (1.0.5)
9
+ addressable (2.7.0)
10
+ public_suffix (>= 2.0.2, < 5.0)
11
+ builder (3.2.4)
12
+ concurrent-ruby (1.1.8)
13
+ daemons (1.4.1)
13
14
  descendants_tracker (0.0.4)
14
15
  thread_safe (~> 0.3, >= 0.3.1)
15
- docile (1.3.0)
16
- faraday (0.15.4)
16
+ docile (1.3.5)
17
+ eventmachine (1.2.7)
18
+ faraday (1.3.0)
19
+ faraday-net_http (~> 1.0)
17
20
  multipart-post (>= 1.2, < 3)
18
- git (1.5.0)
19
- github_api (0.18.2)
21
+ ruby2_keywords
22
+ faraday-net_http (1.0.1)
23
+ git (1.8.1)
24
+ rchardet (~> 1.8)
25
+ github_api (0.19.0)
20
26
  addressable (~> 2.4)
21
27
  descendants_tracker (~> 0.0.4)
22
- faraday (~> 0.8)
28
+ faraday (>= 0.8, < 2)
23
29
  hashie (~> 3.5, >= 3.5.2)
24
30
  oauth2 (~> 1.0)
25
- google-protobuf (3.6.1)
26
- googleapis-common-protos-types (1.0.3)
27
- google-protobuf (~> 3.0)
28
- grpc (1.18.0)
29
- google-protobuf (~> 3.1)
30
- googleapis-common-protos-types (~> 1.0.0)
31
- grpc-tools (1.17.1)
31
+ google-protobuf (3.19.3)
32
+ googleapis-common-protos-types (1.3.0)
33
+ google-protobuf (~> 3.14)
34
+ grpc (1.43.1)
35
+ google-protobuf (~> 3.18)
36
+ googleapis-common-protos-types (~> 1.0)
37
+ grpc-tools (1.43.1)
32
38
  hashie (3.6.0)
33
- highline (2.0.1)
34
- i18n (1.0.1)
39
+ highline (2.0.3)
40
+ i18n (1.8.9)
35
41
  concurrent-ruby (~> 1.0)
36
42
  json (1.8.6)
37
43
  juwelier (2.4.9)
@@ -46,57 +52,67 @@ GEM
46
52
  rake
47
53
  rdoc
48
54
  semver2
49
- jwt (2.1.0)
55
+ jwt (2.2.2)
50
56
  kamelcase (0.0.2)
51
57
  semver2 (~> 3)
52
- mini_portile2 (2.4.0)
53
- minitest (5.11.3)
54
- multi_json (1.13.1)
58
+ mini_portile2 (2.7.1)
59
+ minitest (5.14.4)
60
+ multi_json (1.15.0)
55
61
  multi_xml (0.6.0)
56
- multipart-post (2.0.0)
57
- nokogiri (1.10.1)
58
- mini_portile2 (~> 2.4.0)
59
- oauth2 (1.4.1)
60
- faraday (>= 0.8, < 0.16.0)
62
+ multipart-post (2.1.1)
63
+ nokogiri (1.13.1)
64
+ mini_portile2 (~> 2.7.0)
65
+ racc (~> 1.4)
66
+ oauth2 (1.4.7)
67
+ faraday (>= 0.8, < 2.0)
61
68
  jwt (>= 1.0, < 3.0)
62
69
  multi_json (~> 1.3)
63
70
  multi_xml (~> 0.5)
64
71
  rack (>= 1.2, < 3)
65
- psych (3.1.0)
66
- public_suffix (3.0.3)
67
- rack (2.0.6)
68
- rake (12.3.2)
72
+ psych (3.3.1)
73
+ public_suffix (4.0.6)
74
+ racc (1.6.0)
75
+ rack (2.2.3)
76
+ rake (13.0.3)
77
+ rchardet (1.8.0)
69
78
  rdoc (3.12.2)
70
79
  json (~> 1.4)
80
+ ruby2_keywords (0.0.4)
71
81
  semver2 (3.4.2)
72
- shoulda (3.5.0)
73
- shoulda-context (~> 1.0, >= 1.0.1)
74
- shoulda-matchers (>= 1.4.1, < 3.0)
75
- shoulda-context (1.2.2)
76
- shoulda-matchers (2.8.0)
77
- activesupport (>= 3.0.0)
78
- simplecov (0.16.1)
82
+ shoulda (4.0.0)
83
+ shoulda-context (~> 2.0)
84
+ shoulda-matchers (~> 4.0)
85
+ shoulda-context (2.0.0)
86
+ shoulda-matchers (4.5.1)
87
+ activesupport (>= 4.2.0)
88
+ simplecov (0.18.5)
79
89
  docile (~> 1.1)
80
- json (>= 1.8, < 3)
81
- simplecov-html (~> 0.10.0)
82
- simplecov-html (0.10.2)
90
+ simplecov-html (~> 0.11)
91
+ simplecov-html (0.12.3)
92
+ thin (1.8.1)
93
+ daemons (~> 1.0, >= 1.0.9)
94
+ eventmachine (~> 1.0, >= 1.0.4)
95
+ rack (>= 1, < 3)
83
96
  thread_safe (0.3.6)
84
- tzinfo (1.2.5)
97
+ tzinfo (1.2.9)
85
98
  thread_safe (~> 0.1)
86
99
 
87
100
  PLATFORMS
88
101
  ruby
89
102
 
90
103
  DEPENDENCIES
91
- bundler (~> 1.0)
104
+ bundler
92
105
  concurrent-ruby (~> 1.0, >= 1.0.5)
93
106
  faraday
94
- grpc (~> 1.18.0)
95
- grpc-tools (~> 1.17.1)
107
+ google-protobuf
108
+ googleapis-common-protos-types
109
+ grpc
110
+ grpc-tools
96
111
  juwelier (~> 2.4.9)
97
112
  rdoc (~> 3.12)
98
113
  shoulda
99
114
  simplecov
115
+ thin
100
116
 
101
117
  BUNDLED WITH
102
- 1.16.0
118
+ 2.3.5
data/README.md CHANGED
@@ -53,7 +53,13 @@ end
53
53
  * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
54
54
  * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
55
55
 
56
+ ## Release
57
+
58
+ ```shell
59
+ REMOTE_BRANCH=main LOCAL_BRANCH=main bundle exec rake release
60
+ ```
61
+
56
62
  ## Copyright
57
63
 
58
- Copyright (c) 2018 Jeff Dwyer. See LICENSE.txt for
64
+ Copyright (c) 2022 Jeff Dwyer. See LICENSE.txt for
59
65
  further details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.8.0
data/compile_protos.sh CHANGED
@@ -1,2 +1,5 @@
1
1
  #!/usr/bin/env bash
2
2
  grpc_tools_ruby_protoc -I ../prefab-cloud/ --ruby_out=lib --grpc_out=lib prefab.proto
3
+ # on M1 you need to
4
+ # 1. run in rosetta
5
+ # 2. mv gems/2.6.0/gems/grpc-tools-1.43.1/bin/x86_64-macos x86-macos
data/lib/prefab/client.rb CHANGED
@@ -8,7 +8,7 @@ module Prefab
8
8
  }
9
9
 
10
10
 
11
- attr_reader :account_id, :shared_cache, :stats, :namespace, :interceptor, :api_key
11
+ attr_reader :project_id, :shared_cache, :stats, :namespace, :interceptor, :api_key, :environment
12
12
 
13
13
  def initialize(api_key: ENV['PREFAB_API_KEY'],
14
14
  logdev: nil,
@@ -19,13 +19,15 @@ module Prefab
19
19
  log_formatter: DEFAULT_LOG_FORMATTER
20
20
  )
21
21
  raise "No API key. Set PREFAB_API_KEY env var" if api_key.nil? || api_key.empty?
22
+ raise "PREFAB_API_KEY format invalid. Expecting 123-development-yourapikey-SDK" unless api_key.count("-") == 3
22
23
  @logdev = (logdev || $stdout)
23
24
  @log_formatter = log_formatter
24
25
  @local = local
25
26
  @stats = (stats || NoopStats.new)
26
27
  @shared_cache = (shared_cache || NoopCache.new)
27
28
  @api_key = api_key
28
- @account_id = api_key.split("|")[0].to_i
29
+ @project_id = api_key.split("-")[0].to_i
30
+ @environment = api_key.split("-")[1]
29
31
  @namespace = namespace
30
32
  @interceptor = Prefab::AuthInterceptor.new(api_key)
31
33
  @stubs = {}
@@ -37,7 +39,8 @@ module Prefab
37
39
 
38
40
  def channel
39
41
  credentials = ENV["PREFAB_CLOUD_HTTP"] == "true" ? :this_channel_is_insecure : creds
40
- url = ENV["PREFAB_API_URL"] || 'api.prefab.cloud:443'
42
+ url = ENV["PREFAB_API_URL"] || 'grpc.prefab.cloud:443'
43
+ log_internal Logger::DEBUG, "GRPC Channel #{url} #{credentials}"
41
44
  @_channel ||= GRPC::Core::Channel.new(url, nil, credentials)
42
45
  end
43
46
 
@@ -88,7 +91,7 @@ module Prefab
88
91
  end
89
92
 
90
93
  def cache_key(post_fix)
91
- "prefab:#{account_id}:#{post_fix}"
94
+ "prefab:#{project_id}:#{post_fix}"
92
95
  end
93
96
 
94
97
  def reset!
@@ -6,6 +6,7 @@ module Prefab
6
6
 
7
7
  def initialize(base_client, timeout)
8
8
  @base_client = base_client
9
+ @base_client.log_internal Logger::DEBUG, "Initialize ConfigClient"
9
10
  @timeout = timeout
10
11
  @initialization_lock = Concurrent::ReadWriteLock.new
11
12
 
@@ -14,11 +15,14 @@ module Prefab
14
15
  @config_loader = Prefab::ConfigLoader.new(@base_client)
15
16
  @config_resolver = Prefab::ConfigResolver.new(@base_client, @config_loader)
16
17
 
18
+ @base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquireWriteLock"
17
19
  @initialization_lock.acquire_write_lock
20
+ @base_client.log_internal Logger::DEBUG, "Initialize ConfigClient: AcquiredWriteLock"
18
21
 
19
22
  @cancellable_interceptor = Prefab::CancellableInterceptor.new(@base_client)
20
23
 
21
24
  @s3_cloud_front = ENV["PREFAB_S3CF_BUCKET"] || DEFAULT_S3CF_BUCKET
25
+
22
26
  load_checkpoint
23
27
  start_checkpointing_thread
24
28
  end
@@ -28,9 +32,9 @@ module Prefab
28
32
  start_api_connection_thread(@config_loader.highwater_mark)
29
33
  end
30
34
 
31
- def get(prop)
35
+ def get(key)
32
36
  @initialization_lock.with_read_lock do
33
- @config_resolver.get(prop)
37
+ @config_resolver.get(key)
34
38
  end
35
39
  end
36
40
 
@@ -86,9 +90,12 @@ module Prefab
86
90
  end
87
91
 
88
92
  def load_checkpoint_from_config
89
- config_req = Prefab::ConfigServicePointer.new(account_id: @base_client.account_id,
93
+ @base_client.log_internal Logger::DEBUG, "Load Checkpoint From Config"
94
+
95
+ config_req = Prefab::ConfigServicePointer.new(project_id: @base_client.project_id,
90
96
  start_at_id: @config_loader.highwater_mark)
91
97
  resp = stub.get_all_config(config_req)
98
+ @base_client.log_internal Logger::DEBUG, "Got Response #{resp}"
92
99
  load_deltas(resp, :api)
93
100
  resp.deltas.each do |delta|
94
101
  @config_loader.set(delta)
@@ -112,7 +119,6 @@ module Prefab
112
119
  end
113
120
  end
114
121
 
115
-
116
122
  def load_deltas(deltas, source)
117
123
  deltas.deltas.each do |delta|
118
124
  @config_loader.set(delta)
@@ -153,7 +159,7 @@ module Prefab
153
159
  # Setup a streaming connection to the API
154
160
  # Save new config values into the loader
155
161
  def start_api_connection_thread(start_at_id)
156
- config_req = Prefab::ConfigServicePointer.new(account_id: @base_client.account_id,
162
+ config_req = Prefab::ConfigServicePointer.new(project_id: @base_client.project_id,
157
163
  start_at_id: start_at_id)
158
164
  @base_client.log_internal Logger::DEBUG, "start api connection thread #{start_at_id}"
159
165
  @base_client.stats.increment("prefab.config.api.start")
@@ -0,0 +1,20 @@
1
+ module Prefab
2
+ module ConfigHelper
3
+ def value_of(config_value)
4
+ case config_value.type
5
+ when :string
6
+ config_value.string
7
+ when :int
8
+ config_value.int
9
+ when :double
10
+ config_value.double
11
+ when :bool
12
+ config_value.bool
13
+ when :feature_flag
14
+ config_value.feature_flag
15
+ when :segment
16
+ config_value.segment
17
+ end
18
+ end
19
+ end
20
+ end
@@ -14,7 +14,7 @@ module Prefab
14
14
  def calc_config
15
15
  rtn = @classpath_config.clone
16
16
  @api_config.each_key do |k|
17
- rtn[k] = @api_config[k].value
17
+ rtn[k] = @api_config[k]
18
18
  end
19
19
  rtn = rtn.merge(@local_overrides)
20
20
  rtn
@@ -26,7 +26,7 @@ module Prefab
26
26
  return
27
27
  end
28
28
 
29
- if delta.value.nil?
29
+ if delta.default.nil?
30
30
  @api_config.delete(delta.key)
31
31
  else
32
32
  @api_config[delta.key] = delta
@@ -63,7 +63,7 @@ module Prefab
63
63
  Dir.glob(glob).each do |file|
64
64
  yaml = load(file)
65
65
  yaml.each do |k, v|
66
- rtn[k] = Prefab::ConfigValue.new(value_from(v))
66
+ rtn[k] = Prefab::ConfigDelta.new(key: k, default: Prefab::ConfigValue.new(value_from(v)))
67
67
  end
68
68
  end
69
69
  rtn
@@ -1,11 +1,12 @@
1
1
  module Prefab
2
2
  class ConfigResolver
3
+ include Prefab::ConfigHelper
3
4
  NAMESPACE_DELIMITER = ".".freeze
4
- NAME_KEY_DELIMITER = ":".freeze
5
5
 
6
6
  def initialize(base_client, config_loader)
7
7
  @lock = Concurrent::ReadWriteLock.new
8
8
  @local_store = {}
9
+ @environment = base_client.environment
9
10
  @namespace = base_client.namespace
10
11
  @config_loader = config_loader
11
12
  make_local
@@ -16,7 +17,7 @@ module Prefab
16
17
  @lock.with_read_lock do
17
18
  @local_store.each do |k, v|
18
19
  value = v[:value]
19
- str << "|#{k}| in #{v[:namespace]} |#{value_of(value)}|#{value_of(value).class}\n"
20
+ str << "|#{k}| from #{v[:match]} |#{value_of(value)}|#{value_of(value).class}\n"
20
21
  end
21
22
  end
22
23
  str
@@ -39,21 +40,6 @@ module Prefab
39
40
 
40
41
  private
41
42
 
42
- def value_of(config_value)
43
- case config_value.type
44
- when :string
45
- config_value.string
46
- when :int
47
- config_value.int
48
- when :double
49
- config_value.double
50
- when :bool
51
- config_value.bool
52
- when :feature_flag
53
- config_value.feature_flag
54
- end
55
- end
56
-
57
43
  # Should client a.b.c see key in namespace a.b? yes
58
44
  # Should client a.b.c see key in namespace a.b.c? yes
59
45
  # Should client a.b.c see key in namespace a.b.d? no
@@ -61,32 +47,50 @@ module Prefab
61
47
  #
62
48
  def starts_with_ns?(key_namespace, client_namespace)
63
49
  zipped = key_namespace.split(NAMESPACE_DELIMITER).zip(client_namespace.split(NAMESPACE_DELIMITER))
64
- zipped.map do |k, c|
65
- (k.nil? || k.empty?) || c == k
66
- end.all?
50
+ mapped = zipped.map do |k, c|
51
+ (k.nil? || k.empty?) || k == c
52
+ end
53
+ [mapped.all?, mapped.size]
67
54
  end
68
55
 
69
56
  def make_local
70
57
  store = {}
71
- @config_loader.calc_config.each do |prop, value|
72
- property = prop
73
- key_namespace = ""
58
+ @config_loader.calc_config.each do |key, delta|
59
+ # start with the top level default
60
+ to_store = { match: "default", value: delta.default }
61
+ if delta.envs.any?
62
+ env_values = delta.envs.select { |e| e.environment == @environment }
74
63
 
75
- split = prop.split(NAME_KEY_DELIMITER)
64
+ # do we have and env_values that match our env?
65
+ if env_values.any?
66
+ env_value = env_values.first
76
67
 
77
- if split.size > 1
78
- property = split[1..-1].join(NAME_KEY_DELIMITER)
79
- key_namespace = split[0]
80
- end
68
+ # override the top level default with env default
69
+ to_store = { match: "env_default", env: env_value.environment, value: env_value.default }
81
70
 
82
- if starts_with_ns?(key_namespace, @namespace)
83
- existing = store[property]
84
- if existing.nil?
85
- store[property] = { namespace: key_namespace, value: value }
86
- elsif existing[:namespace].split(NAMESPACE_DELIMITER).size < key_namespace.split(NAMESPACE_DELIMITER).size
87
- store[property] = { namespace: key_namespace, value: value }
71
+ if env_value.namespace_values.any?
72
+ # check all namespace_values for match
73
+ env_value.namespace_values.each do |namespace_value|
74
+ (starts_with, count) = starts_with_ns?(namespace_value.namespace, @namespace)
75
+ if starts_with
76
+ # is this match the best match?
77
+ if count > (to_store[:match_depth_count] || 0)
78
+ to_store = { match: namespace_value.namespace, count: count, value: namespace_value.config_value }
79
+ end
80
+ end
81
+ end
82
+ end
88
83
  end
89
84
  end
85
+
86
+ # feature flags are a funny case
87
+ # we only define the variants in the default in order to be DRY
88
+ # but we want to access them in environments, clone them over
89
+ if to_store[:value].type == :feature_flag
90
+ to_store[:value].feature_flag.variants = delta.default.feature_flag.variants
91
+ end
92
+
93
+ store[key] = to_store
90
94
  end
91
95
  @lock.with_write_lock do
92
96
  @local_store = store
@@ -1,6 +1,8 @@
1
1
  module Prefab
2
2
  class FeatureFlagClient
3
+ include Prefab::ConfigHelper
3
4
  MAX_32_FLOAT = 4294967294.0
5
+ DISTRIBUTION_SPACE = 1000
4
6
 
5
7
  def initialize(base_client)
6
8
  @base_client = base_client
@@ -14,38 +16,124 @@ module Prefab
14
16
  feature_is_on_for?(feature_name, nil)
15
17
  end
16
18
 
17
- def feature_is_on_for?(feature_name, lookup_key, attributes: [])
19
+ def feature_is_on_for?(feature_name, lookup_key, attributes: {})
18
20
  @base_client.stats.increment("prefab.featureflag.on", tags: ["feature:#{feature_name}"])
19
21
 
22
+ return is_on?(get(feature_name, lookup_key, attributes))
23
+ end
24
+
25
+ def get(feature_name, lookup_key, attributes)
20
26
  feature_obj = @base_client.config_client.get(feature_name)
21
- return is_on?(feature_name, lookup_key, attributes, feature_obj)
27
+ evaluate(feature_name, lookup_key, attributes, feature_obj)
28
+ end
29
+
30
+ def evaluate(feature_name, lookup_key, attributes, feature_obj)
31
+ value_of(get_variant(feature_name, lookup_key, attributes, feature_obj))
22
32
  end
23
33
 
24
34
  private
25
35
 
26
- def is_on?(feature_name, lookup_key, attributes, feature_obj)
27
- if feature_obj.nil?
36
+ def is_on?(variant)
37
+ if variant.nil?
28
38
  return false
29
39
  end
40
+ variant.bool
41
+ end
42
+
43
+ def get_variant(feature_name, lookup_key, attributes, feature_obj)
44
+ if !feature_obj.active
45
+ return get_variant_obj(feature_obj, feature_obj.inactive_variant_idx)
46
+ end
47
+
48
+ variant_distribution = feature_obj.default
49
+
50
+ # if user_targets.match
51
+ feature_obj.user_targets.each do |target|
52
+ if (target.identifiers.include? lookup_key)
53
+ return get_variant_obj(feature_obj, target.variant_idx)
54
+ end
55
+ end
30
56
 
31
- attributes << lookup_key if lookup_key
32
- if (attributes & feature_obj.whitelisted).size > 0
33
- return true
57
+ # if rules.match
58
+ feature_obj.rules.each do |rule|
59
+ if criteria_match?(rule, lookup_key, attributes)
60
+ variant_distribution = rule.distribution
61
+ end
34
62
  end
35
63
 
36
- if lookup_key
37
- return get_user_pct(feature_name, lookup_key) < feature_obj.pct
64
+ if variant_distribution.type == :variant_idx
65
+ variant_idx = variant_distribution.variant_idx
66
+ else
67
+ percent_through_distribution = rand()
68
+ if lookup_key
69
+ percent_through_distribution = get_user_pct(feature_name, lookup_key)
70
+ end
71
+ distribution_bucket = DISTRIBUTION_SPACE * percent_through_distribution
72
+
73
+ variant_idx = get_variant_idx_from_weights(variant_distribution.variant_weights.weights, distribution_bucket, feature_name)
38
74
  end
39
75
 
40
- return feature_obj.pct > rand()
76
+ return get_variant_obj(feature_obj, variant_idx)
77
+ end
78
+
79
+ def get_variant_obj(feature_obj, idx)
80
+ return feature_obj.variants[idx] if feature_obj.variants.length >= idx
81
+ nil
82
+ end
83
+
84
+ def get_variant_idx_from_weights(variant_weights, bucket, feature_name)
85
+ sum = 0
86
+ variant_weights.each do |variant_weight|
87
+ if bucket < sum + variant_weight.weight
88
+ return variant_weight.variant_idx
89
+ else
90
+ sum += variant_weight.weight
91
+ end
92
+ end
93
+ # variants didn't add up to 100%
94
+ @base_client.log.info("Variants of #{feature_name} did not add to 100%")
95
+ return variant_weights.last.variant
41
96
  end
42
97
 
43
98
  def get_user_pct(feature, lookup_key)
44
- to_hash = "#{@base_client.account_id}#{feature}#{lookup_key}"
99
+ to_hash = "#{@base_client.project_id}#{feature}#{lookup_key}"
45
100
  int_value = Murmur3.murmur3_32(to_hash)
46
101
  int_value / MAX_32_FLOAT
47
102
  end
48
103
 
104
+ def criteria_match?(rule, lookup_key, attributes)
105
+ if rule.criteria.operator == :IN
106
+ return rule.criteria.values.include?(lookup_key)
107
+ elsif rule.criteria.operator == :NOT_IN
108
+ return !rule.criteria.values.include?(lookup_key)
109
+ elsif rule.criteria.operator == :IN_SEG
110
+ return segment_matches(rule.criteria.values, lookup_key, attributes).any?
111
+ elsif rule.criteria.operator == :NOT_IN_SEG
112
+ return segment_matches(rule.criteria.values, lookup_key, attributes).none?
113
+ end
114
+ @base_client.log.info("Unknown Operator")
115
+ false
116
+ end
117
+
118
+ # evaluate each segment key and return whether each one matches
119
+ # there should be an associated segment available as a standard config obj
120
+ def segment_matches(segment_keys, lookup_key, attributes)
121
+ segment_keys.map do |segment_key|
122
+ segment = @base_client.config_client.get(segment_key)
123
+ if segment.nil?
124
+ @base_client.log.info("Missing Segment")
125
+ false
126
+ else
127
+ segment_match?(segment, lookup_key, attributes)
128
+ end
129
+ end
130
+ end
131
+
132
+ def segment_match?(segment, lookup_key, attributes)
133
+ includes = segment.includes.include?(lookup_key)
134
+ excludes = segment.excludes.include?(lookup_key)
135
+ includes && !excludes
136
+ end
49
137
  end
50
138
  end
51
139
 
@@ -4,6 +4,7 @@ require 'faraday'
4
4
  require 'openssl'
5
5
  require 'prefab_pb'
6
6
  require 'prefab_services_pb'
7
+ require 'prefab/config_helper'
7
8
  require 'prefab/config_loader'
8
9
  require 'prefab/config_resolver'
9
10
  require 'prefab/client'