featurehub-sdk 1.3.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/CLAUDE.md +85 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +18 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +20 -8
- data/README.md +306 -119
- data/examples/rails_example/.ruby-version +1 -1
- data/examples/rails_example/Dockerfile +1 -1
- data/examples/sinatra/.dockerignore +7 -0
- data/examples/sinatra/.ruby-version +1 -1
- data/examples/sinatra/Dockerfile +14 -25
- data/examples/sinatra/Gemfile +5 -4
- data/examples/sinatra/Gemfile.lock +40 -32
- data/examples/sinatra/app/application.rb +21 -9
- data/examples/sinatra/docker-compose.yaml +24 -0
- data/examples/sinatra/feature-flags.yaml +6 -0
- data/examples/sinatra/sinatra.iml +35 -14
- data/examples/sinatra/start.sh +2 -0
- data/featurehub-sdk.gemspec +4 -1
- data/lib/feature_hub/sdk/context.rb +28 -7
- data/lib/feature_hub/sdk/feature_hub_config.rb +68 -12
- data/lib/feature_hub/sdk/feature_repository.rb +52 -13
- data/lib/feature_hub/sdk/{feature_state.rb → feature_state_holder.rb} +13 -9
- data/lib/feature_hub/sdk/interceptors.rb +10 -6
- data/lib/feature_hub/sdk/internal_feature_repository.rb +7 -3
- data/lib/feature_hub/sdk/local_yaml_interceptor.rb +99 -0
- data/lib/feature_hub/sdk/local_yaml_store.rb +71 -0
- data/lib/feature_hub/sdk/poll_edge_service.rb +10 -15
- data/lib/feature_hub/sdk/raw_update_feature_listener.rb +19 -0
- data/lib/feature_hub/sdk/redis_session_store.rb +130 -0
- data/lib/feature_hub/sdk/strategy_attributes.rb +7 -0
- data/lib/feature_hub/sdk/streaming_edge_service.rb +5 -7
- data/lib/feature_hub/sdk/version.rb +1 -10
- data/lib/featurehub-sdk.rb +5 -1
- data/sig/feature_hub/featurehub.rbs +127 -28
- metadata +27 -5
|
@@ -1,55 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
remote:
|
|
3
|
-
revision: fa551295ec9703e630452e094d3856117c3d013d
|
|
4
|
-
glob: featurehub-sdk/*.gemspec
|
|
1
|
+
PATH
|
|
2
|
+
remote: ../..
|
|
5
3
|
specs:
|
|
6
|
-
featurehub-sdk (
|
|
7
|
-
concurrent-ruby (~> 1.
|
|
8
|
-
faraday (~> 2
|
|
9
|
-
ld-eventsource (~> 2.
|
|
10
|
-
murmurhash3 (~> 0.1.
|
|
4
|
+
featurehub-sdk (2.0.0)
|
|
5
|
+
concurrent-ruby (~> 1.3)
|
|
6
|
+
faraday (~> 2)
|
|
7
|
+
ld-eventsource (~> 2.5.1)
|
|
8
|
+
murmurhash3 (~> 0.1.7)
|
|
11
9
|
sem_version (~> 2.0.0)
|
|
12
10
|
|
|
13
11
|
GEM
|
|
14
12
|
remote: https://rubygems.org/
|
|
15
13
|
specs:
|
|
16
|
-
addressable (2.8.
|
|
17
|
-
public_suffix (>= 2.0.2, <
|
|
18
|
-
concurrent-ruby (1.
|
|
14
|
+
addressable (2.8.9)
|
|
15
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
16
|
+
concurrent-ruby (1.3.6)
|
|
17
|
+
connection_pool (3.0.2)
|
|
19
18
|
daemons (1.4.1)
|
|
20
|
-
domain_name (0.
|
|
21
|
-
unf (>= 0.0.5, < 1.0.0)
|
|
19
|
+
domain_name (0.6.20240107)
|
|
22
20
|
eventmachine (1.2.7)
|
|
23
|
-
faraday (2.
|
|
24
|
-
faraday-net_http (>= 2.0, < 3.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
faraday (2.14.1)
|
|
22
|
+
faraday-net_http (>= 2.0, < 3.5)
|
|
23
|
+
json
|
|
24
|
+
logger
|
|
25
|
+
faraday-net_http (3.4.2)
|
|
26
|
+
net-http (~> 0.5)
|
|
27
|
+
ffi (1.17.3-x86_64-darwin)
|
|
28
|
+
ffi-compiler (1.3.2)
|
|
29
|
+
ffi (>= 1.15.5)
|
|
30
30
|
rake
|
|
31
|
-
http (5.
|
|
31
|
+
http (5.3.1)
|
|
32
32
|
addressable (~> 2.8)
|
|
33
33
|
http-cookie (~> 1.0)
|
|
34
34
|
http-form_data (~> 2.2)
|
|
35
|
-
llhttp-ffi (~> 0.
|
|
36
|
-
http-cookie (1.0
|
|
35
|
+
llhttp-ffi (~> 0.5.0)
|
|
36
|
+
http-cookie (1.1.0)
|
|
37
37
|
domain_name (~> 0.5)
|
|
38
38
|
http-form_data (2.3.0)
|
|
39
|
-
|
|
39
|
+
json (2.19.2)
|
|
40
|
+
ld-eventsource (2.5.1)
|
|
40
41
|
concurrent-ruby (~> 1.0)
|
|
41
42
|
http (>= 4.4.1, < 6.0.0)
|
|
42
|
-
llhttp-ffi (0.
|
|
43
|
+
llhttp-ffi (0.5.1)
|
|
43
44
|
ffi-compiler (~> 1.0)
|
|
44
45
|
rake (~> 13.0)
|
|
46
|
+
logger (1.7.0)
|
|
45
47
|
murmurhash3 (0.1.7)
|
|
46
48
|
mustermann (2.0.2)
|
|
47
49
|
ruby2_keywords (~> 0.0.1)
|
|
48
|
-
|
|
50
|
+
net-http (0.9.1)
|
|
51
|
+
uri (>= 0.11.1)
|
|
52
|
+
public_suffix (7.0.5)
|
|
49
53
|
rack (2.2.6.4)
|
|
50
54
|
rack-protection (2.2.3)
|
|
51
55
|
rack
|
|
52
|
-
rake (13.
|
|
56
|
+
rake (13.3.1)
|
|
57
|
+
redis (5.4.1)
|
|
58
|
+
redis-client (>= 0.22.0)
|
|
59
|
+
redis-client (0.28.0)
|
|
60
|
+
connection_pool
|
|
53
61
|
ruby2_keywords (0.0.5)
|
|
54
62
|
sem_version (2.0.1)
|
|
55
63
|
shotgun (0.9.2)
|
|
@@ -64,23 +72,23 @@ GEM
|
|
|
64
72
|
eventmachine (~> 1.0, >= 1.0.4)
|
|
65
73
|
rack (>= 1, < 3)
|
|
66
74
|
tilt (2.1.0)
|
|
67
|
-
|
|
68
|
-
unf_ext
|
|
69
|
-
unf_ext (0.0.8.2)
|
|
75
|
+
uri (1.1.1)
|
|
70
76
|
|
|
71
77
|
PLATFORMS
|
|
72
78
|
x86_64-darwin-21
|
|
79
|
+
x86_64-darwin-24
|
|
73
80
|
x86_64-linux
|
|
74
81
|
|
|
75
82
|
DEPENDENCIES
|
|
76
83
|
featurehub-sdk!
|
|
77
84
|
rack
|
|
85
|
+
redis
|
|
78
86
|
shotgun
|
|
79
87
|
sinatra
|
|
80
88
|
thin
|
|
81
89
|
|
|
82
90
|
RUBY VERSION
|
|
83
|
-
ruby
|
|
91
|
+
ruby 3.3.10p183
|
|
84
92
|
|
|
85
93
|
BUNDLED WITH
|
|
86
94
|
2.3.15
|
|
@@ -7,12 +7,20 @@ require "json"
|
|
|
7
7
|
|
|
8
8
|
def configure_featurehub
|
|
9
9
|
puts "FeatureHub SDK Version is #{FeatureHub::Sdk::VERSION}"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
|
|
11
|
+
config = FeatureHub::Sdk::FeatureHubConfig.new
|
|
12
|
+
repo = config.repository
|
|
13
|
+
|
|
14
|
+
if ENV["FEATUREHUB_REDIS_STORE"]
|
|
15
|
+
config.register_raw_update_listener(FeatureHub::Sdk::RedisSessionStore.new(ENV["FEATUREHUB_REDIS_STORE"], repo))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if ENV["FEATUREHUB_LOCAL_YAML"]
|
|
19
|
+
config.register_raw_update_listener(FeatureHub::Sdk::LocalYamlStore.new(repo))
|
|
20
|
+
config.register_interceptor(FeatureHub::Sdk::LocalYamlValueInterceptor.new(watch: true))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# connect to edge service
|
|
16
24
|
config.init
|
|
17
25
|
end
|
|
18
26
|
|
|
@@ -34,6 +42,10 @@ class App < Sinatra::Base
|
|
|
34
42
|
content_type "application/json"
|
|
35
43
|
end
|
|
36
44
|
|
|
45
|
+
get("/config/disconnect-edge") do
|
|
46
|
+
settings.fh_config.close_edge
|
|
47
|
+
end
|
|
48
|
+
|
|
37
49
|
# Routes
|
|
38
50
|
# "resolve" a specific todo for this user
|
|
39
51
|
put("/todo/:user/:id/resolve") do
|
|
@@ -127,10 +139,10 @@ class App < Sinatra::Base
|
|
|
127
139
|
|
|
128
140
|
new_title = new_title.upcase if ctx.enabled?("FEATURE_TITLE_TO_UPPERCASE")
|
|
129
141
|
|
|
130
|
-
puts("features via repository: #{settings.fh_config.repository.features}")
|
|
131
|
-
puts("features via edge service: #{settings.fh_config.get_or_create_edge_service.repository}")
|
|
142
|
+
# puts("features via repository: #{settings.fh_config.repository.features}")
|
|
143
|
+
# puts("features via edge service: #{settings.fh_config.get_or_create_edge_service.repository}")
|
|
132
144
|
|
|
133
|
-
puts("enabled? #{ctx.repo.features}")
|
|
145
|
+
# puts("enabled? #{ctx.repo.features}")
|
|
134
146
|
puts(ctx.enabled?("FEATURE_TITLE_TO_UPPERCASE"))
|
|
135
147
|
puts(ctx.flag("FEATURE_TITLE_TO_UPPERCASE"))
|
|
136
148
|
puts(settings.fh_config.repository.feature("FEATURE_TITLE_TO_UPPERCASE").feature_type)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
services:
|
|
2
|
+
party-server:
|
|
3
|
+
image: featurehub/party-server:1.9.4
|
|
4
|
+
ports:
|
|
5
|
+
- 8085:8085
|
|
6
|
+
user: 999:999
|
|
7
|
+
# allow us to restart the server causing the client to lose connectivity
|
|
8
|
+
# but when we start it again its OK
|
|
9
|
+
volumes:
|
|
10
|
+
- featurehub-h2-data:/db
|
|
11
|
+
redis:
|
|
12
|
+
image: redis:latest
|
|
13
|
+
ports:
|
|
14
|
+
- 6379:6379
|
|
15
|
+
ruby-sinatra:
|
|
16
|
+
image: ruby-sinatra:latest
|
|
17
|
+
ports:
|
|
18
|
+
- 8099:8099
|
|
19
|
+
environment:
|
|
20
|
+
FEATUREHUB_EDGE_URL: http://party-server:8085
|
|
21
|
+
FEATUREHUB_BASE_URL: http://party-server:8085
|
|
22
|
+
FEATUREHUB_REDIS_STORE: redis
|
|
23
|
+
volumes:
|
|
24
|
+
featurehub-h2-data:
|
|
@@ -3,20 +3,41 @@
|
|
|
3
3
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
4
|
<exclude-output />
|
|
5
5
|
<content url="file://$MODULE_DIR$" />
|
|
6
|
-
<orderEntry type="jdk" jdkName="rbenv: 3.
|
|
6
|
+
<orderEntry type="jdk" jdkName="rbenv: 3.3.10" jdkType="RUBY_SDK" />
|
|
7
7
|
<orderEntry type="sourceFolder" forTests="false" />
|
|
8
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
9
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
10
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
11
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
12
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
13
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
14
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
15
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
16
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
17
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
18
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
19
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
20
|
-
<orderEntry type="library" scope="PROVIDED" name="
|
|
8
|
+
<orderEntry type="library" scope="PROVIDED" name="addressable (v2.8.9, rbenv: 3.3.10) [gem]" level="application" />
|
|
9
|
+
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.3.15, rbenv: 3.3.10) [gem]" level="application" />
|
|
10
|
+
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.6, rbenv: 3.3.10) [gem]" level="application" />
|
|
11
|
+
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v3.0.2, rbenv: 3.3.10) [gem]" level="application" />
|
|
12
|
+
<orderEntry type="library" scope="PROVIDED" name="daemons (v1.4.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
13
|
+
<orderEntry type="library" scope="PROVIDED" name="domain_name (v0.6.20240107, rbenv: 3.3.10) [gem]" level="application" />
|
|
14
|
+
<orderEntry type="library" scope="PROVIDED" name="eventmachine (v1.2.7, rbenv: 3.3.10) [gem]" level="application" />
|
|
15
|
+
<orderEntry type="library" scope="PROVIDED" name="faraday (v2.14.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
16
|
+
<orderEntry type="library" scope="PROVIDED" name="faraday-net_http (v3.4.2, rbenv: 3.3.10) [gem]" level="application" />
|
|
17
|
+
<orderEntry type="library" scope="PROVIDED" name="ffi (v1.17.3, rbenv: 3.3.10) [gem]" level="application" />
|
|
18
|
+
<orderEntry type="library" scope="PROVIDED" name="ffi-compiler (v1.3.2, rbenv: 3.3.10) [gem]" level="application" />
|
|
19
|
+
<orderEntry type="library" scope="PROVIDED" name="http (v5.3.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
20
|
+
<orderEntry type="library" scope="PROVIDED" name="http-cookie (v1.1.0, rbenv: 3.3.10) [gem]" level="application" />
|
|
21
|
+
<orderEntry type="library" scope="PROVIDED" name="http-form_data (v2.3.0, rbenv: 3.3.10) [gem]" level="application" />
|
|
22
|
+
<orderEntry type="library" scope="PROVIDED" name="json (v2.19.2, rbenv: 3.3.10) [gem]" level="application" />
|
|
23
|
+
<orderEntry type="library" scope="PROVIDED" name="ld-eventsource (v2.5.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
24
|
+
<orderEntry type="library" scope="PROVIDED" name="llhttp-ffi (v0.5.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
25
|
+
<orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, rbenv: 3.3.10) [gem]" level="application" />
|
|
26
|
+
<orderEntry type="library" scope="PROVIDED" name="murmurhash3 (v0.1.7, rbenv: 3.3.10) [gem]" level="application" />
|
|
27
|
+
<orderEntry type="library" scope="PROVIDED" name="mustermann (v2.0.2, rbenv: 3.3.10) [gem]" level="application" />
|
|
28
|
+
<orderEntry type="library" scope="PROVIDED" name="net-http (v0.9.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
29
|
+
<orderEntry type="library" scope="PROVIDED" name="public_suffix (v7.0.5, rbenv: 3.3.10) [gem]" level="application" />
|
|
30
|
+
<orderEntry type="library" scope="PROVIDED" name="rack (v2.2.6.4, rbenv: 3.3.10) [gem]" level="application" />
|
|
31
|
+
<orderEntry type="library" scope="PROVIDED" name="rack-protection (v2.2.3, rbenv: 3.3.10) [gem]" level="application" />
|
|
32
|
+
<orderEntry type="library" scope="PROVIDED" name="rake (v13.3.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
33
|
+
<orderEntry type="library" scope="PROVIDED" name="redis (v5.4.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
34
|
+
<orderEntry type="library" scope="PROVIDED" name="redis-client (v0.28.0, rbenv: 3.3.10) [gem]" level="application" />
|
|
35
|
+
<orderEntry type="library" scope="PROVIDED" name="ruby2_keywords (v0.0.5, rbenv: 3.3.10) [gem]" level="application" />
|
|
36
|
+
<orderEntry type="library" scope="PROVIDED" name="sem_version (v2.0.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
37
|
+
<orderEntry type="library" scope="PROVIDED" name="shotgun (v0.9.2, rbenv: 3.3.10) [gem]" level="application" />
|
|
38
|
+
<orderEntry type="library" scope="PROVIDED" name="sinatra (v2.2.3, rbenv: 3.3.10) [gem]" level="application" />
|
|
39
|
+
<orderEntry type="library" scope="PROVIDED" name="thin (v1.8.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
40
|
+
<orderEntry type="library" scope="PROVIDED" name="tilt (v2.1.0, rbenv: 3.3.10) [gem]" level="application" />
|
|
41
|
+
<orderEntry type="library" scope="PROVIDED" name="uri (v1.1.1, rbenv: 3.3.10) [gem]" level="application" />
|
|
21
42
|
</component>
|
|
22
43
|
</module>
|
data/examples/sinatra/start.sh
CHANGED
data/featurehub-sdk.gemspec
CHANGED
|
@@ -36,7 +36,10 @@ Gem::Specification.new do |spec|
|
|
|
36
36
|
|
|
37
37
|
spec.add_dependency "concurrent-ruby", "~> 1.3"
|
|
38
38
|
spec.add_dependency "faraday", "~> 2"
|
|
39
|
-
spec.add_dependency "ld-eventsource", "~> 2.
|
|
39
|
+
spec.add_dependency "ld-eventsource", "~> 2.5.1"
|
|
40
40
|
spec.add_dependency "murmurhash3", "~> 0.1.7"
|
|
41
41
|
spec.add_dependency "sem_version", "~> 2.0.0"
|
|
42
|
+
|
|
43
|
+
# we will dynamically determine if redis is available
|
|
44
|
+
spec.add_development_dependency "redis", "~> 5"
|
|
42
45
|
end
|
|
@@ -17,9 +17,19 @@ module FeatureHub
|
|
|
17
17
|
class ClientContext
|
|
18
18
|
attr_reader :repo
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
WELL_KNOWN_KEY_METHODS = {
|
|
21
|
+
ContextKeys::USERKEY => :user_key,
|
|
22
|
+
ContextKeys::SESSION => :session_key,
|
|
23
|
+
ContextKeys::COUNTRY => :country,
|
|
24
|
+
ContextKeys::PLATFORM => :platform,
|
|
25
|
+
ContextKeys::DEVICE => :device,
|
|
26
|
+
ContextKeys::VERSION => :version
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
def initialize(repo, attrs = nil)
|
|
21
30
|
@repo = repo
|
|
22
31
|
@attributes = {}
|
|
32
|
+
assign(attrs) if attrs
|
|
23
33
|
end
|
|
24
34
|
|
|
25
35
|
def user_key(value)
|
|
@@ -54,7 +64,9 @@ module FeatureHub
|
|
|
54
64
|
|
|
55
65
|
# this takes an array parameter
|
|
56
66
|
def attribute_value(key, values)
|
|
57
|
-
if
|
|
67
|
+
return self if key.nil? || key.to_s.empty?
|
|
68
|
+
|
|
69
|
+
if values.nil? || values.empty?
|
|
58
70
|
@attributes.delete(key.to_sym)
|
|
59
71
|
else
|
|
60
72
|
@attributes[key.to_sym] = if values.is_a?(Array)
|
|
@@ -67,6 +79,19 @@ module FeatureHub
|
|
|
67
79
|
self
|
|
68
80
|
end
|
|
69
81
|
|
|
82
|
+
def assign(attrs)
|
|
83
|
+
attrs.each do |key, value|
|
|
84
|
+
sym_key = key.to_sym
|
|
85
|
+
method_name = WELL_KNOWN_KEY_METHODS[sym_key]
|
|
86
|
+
if method_name
|
|
87
|
+
send(method_name, value)
|
|
88
|
+
else
|
|
89
|
+
attribute_value(sym_key, value)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
70
95
|
def clear
|
|
71
96
|
@attributes = {}
|
|
72
97
|
self
|
|
@@ -90,7 +115,7 @@ module FeatureHub
|
|
|
90
115
|
end
|
|
91
116
|
|
|
92
117
|
def feature(key)
|
|
93
|
-
@repo.feature(key)
|
|
118
|
+
@repo.feature(key).with_context(self)
|
|
94
119
|
end
|
|
95
120
|
|
|
96
121
|
def set?(key)
|
|
@@ -153,10 +178,6 @@ module FeatureHub
|
|
|
153
178
|
def build_sync
|
|
154
179
|
self
|
|
155
180
|
end
|
|
156
|
-
|
|
157
|
-
def feature(key)
|
|
158
|
-
@repo.feature(key).with_context(self)
|
|
159
|
-
end
|
|
160
181
|
end
|
|
161
182
|
|
|
162
183
|
# context used when evaluating server side
|
|
@@ -4,8 +4,14 @@ module FeatureHub
|
|
|
4
4
|
module Sdk
|
|
5
5
|
# interface style definition for all edge services
|
|
6
6
|
class EdgeService
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
attr_reader :repository
|
|
8
|
+
|
|
9
|
+
def initialize(repository, api_keys, edge_url, logger = nil)
|
|
10
|
+
@repository = repository
|
|
11
|
+
@api_keys = api_keys
|
|
12
|
+
@edge_url = edge_url
|
|
13
|
+
@logger = logger
|
|
14
|
+
end
|
|
9
15
|
|
|
10
16
|
# abstract
|
|
11
17
|
def poll; end
|
|
@@ -21,24 +27,47 @@ module FeatureHub
|
|
|
21
27
|
class FeatureHubConfig
|
|
22
28
|
attr_reader :edge_url, :api_keys, :client_evaluated, :logger
|
|
23
29
|
|
|
24
|
-
def initialize(edge_url, api_keys, repository = nil, edge_provider = nil, logger = nil)
|
|
25
|
-
|
|
30
|
+
def initialize(edge_url = nil, api_keys = nil, repository = nil, edge_provider = nil, logger = nil) # rubocop:disable Metrics/ParameterLists
|
|
31
|
+
@logger = logger
|
|
32
|
+
@repository = repository || FeatureHub::Sdk::FeatureHubRepository.new(nil, @logger)
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
resolved_url = resolve_edge_url(edge_url)
|
|
35
|
+
resolved_keys = resolve_api_keys(api_keys)
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
if resolved_url && resolved_keys && !resolved_keys.empty?
|
|
38
|
+
detect_client_evaluated(resolved_keys)
|
|
39
|
+
@edge_url = parse_edge_url(resolved_url)
|
|
40
|
+
@api_keys = resolved_keys
|
|
41
|
+
@edge_service_provider = edge_provider || method(:create_default_provider)
|
|
42
|
+
else
|
|
43
|
+
@edge_url = nil
|
|
44
|
+
@api_keys = []
|
|
45
|
+
@client_evaluated = false
|
|
46
|
+
@edge_service_provider = edge_provider || method(:create_null_provider)
|
|
47
|
+
end
|
|
36
48
|
end
|
|
37
49
|
|
|
38
50
|
def repository(repo = nil)
|
|
39
51
|
@repository = repo || @repository
|
|
40
52
|
end
|
|
41
53
|
|
|
54
|
+
def feature(key, attrs = nil)
|
|
55
|
+
@repository.feature(key, attrs)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def value(key, default_value = nil, attrs = nil)
|
|
59
|
+
@repository.value(key, default_value, attrs)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def register_interceptor(interceptor)
|
|
63
|
+
@repository.register_interceptor(interceptor)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def register_raw_update_listener(listener)
|
|
67
|
+
@repository ||= FeatureHub::Sdk::FeatureHubRepository.new(nil, @logger)
|
|
68
|
+
@repository.register_raw_update_listener(listener)
|
|
69
|
+
end
|
|
70
|
+
|
|
42
71
|
def init
|
|
43
72
|
get_or_create_edge_service.poll
|
|
44
73
|
self
|
|
@@ -90,6 +119,18 @@ module FeatureHub
|
|
|
90
119
|
end
|
|
91
120
|
|
|
92
121
|
def close
|
|
122
|
+
unless @repository.nil?
|
|
123
|
+
@repository.close
|
|
124
|
+
@repository = nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
return if @edge_service.nil?
|
|
128
|
+
|
|
129
|
+
@edge_service.close
|
|
130
|
+
@edge_service = nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def close_edge
|
|
93
134
|
return if @edge_service.nil?
|
|
94
135
|
|
|
95
136
|
@edge_service.close
|
|
@@ -110,6 +151,21 @@ module FeatureHub
|
|
|
110
151
|
FeatureHub::Sdk::StreamingEdgeService.new(repo, api_keys, edge_url, logger)
|
|
111
152
|
end
|
|
112
153
|
|
|
154
|
+
def resolve_edge_url(edge_url)
|
|
155
|
+
url = edge_url.nil? || edge_url.strip.empty? ? ENV.fetch("FEATUREHUB_EDGE_URL", nil) : edge_url
|
|
156
|
+
url&.strip&.empty? ? nil : url
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def resolve_api_keys(api_keys)
|
|
160
|
+
return api_keys if api_keys.is_a?(Array) && !api_keys.empty?
|
|
161
|
+
|
|
162
|
+
[ENV.fetch("FEATUREHUB_CLIENT_API_KEY", nil), ENV.fetch("FEATUREHUB_SERVER_API_KEY", nil)].compact
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def create_null_provider(repo, api_keys, edge_url, logger)
|
|
166
|
+
EdgeService.new(repo, api_keys, edge_url, logger)
|
|
167
|
+
end
|
|
168
|
+
|
|
113
169
|
def detect_client_evaluated(api_keys)
|
|
114
170
|
@client_evaluated = !api_keys.detect { |k| k.include?("*") }.nil?
|
|
115
171
|
if api_keys.detect { |k| (@client_evaluated && !k.include?("*")) || (!@client_evaluated && k.include?("*")) }
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "concurrent-ruby"
|
|
4
|
+
|
|
3
5
|
module FeatureHub
|
|
4
6
|
module Sdk
|
|
5
7
|
# the core implementation of a feature repository
|
|
6
8
|
class FeatureHubRepository < InternalFeatureRepository
|
|
7
9
|
attr_reader :features
|
|
8
10
|
|
|
9
|
-
def initialize(apply_features = nil)
|
|
11
|
+
def initialize(apply_features = nil, logger = nil)
|
|
10
12
|
super()
|
|
11
13
|
@strategy_matcher = apply_features || FeatureHub::Sdk::Impl::ApplyFeature.new
|
|
12
14
|
@interceptors = []
|
|
15
|
+
@raw_listeners = []
|
|
13
16
|
@features = {}
|
|
14
17
|
@ready = false
|
|
18
|
+
@logger = logger
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
def apply(strategies, key, feature_id, context)
|
|
18
22
|
@strategy_matcher.apply(strategies, key, feature_id, context)
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
def notify(status, data)
|
|
25
|
+
def notify(status, data, source = "unknown")
|
|
22
26
|
return unless status
|
|
23
27
|
|
|
24
28
|
if status.to_sym == :failed
|
|
@@ -31,26 +35,58 @@ module FeatureHub
|
|
|
31
35
|
case status.to_sym
|
|
32
36
|
when :features
|
|
33
37
|
update_features(data)
|
|
38
|
+
@logger&.debug("[featurehubsdk] became ready through updates from #{source}") unless @ready
|
|
34
39
|
@ready = true
|
|
40
|
+
notify_raw_listeners_async { |l| l.process_updates(data, source) }
|
|
41
|
+
@logger&.debug("[featurehubsdk] full updates from #{source} are #{data}")
|
|
35
42
|
when :feature
|
|
43
|
+
return if data.nil? || data["key"].nil?
|
|
44
|
+
|
|
36
45
|
update_feature(data)
|
|
46
|
+
@logger&.debug("[featurehubsdk] became ready through updates from #{source}") unless @ready
|
|
37
47
|
@ready = true
|
|
48
|
+
notify_raw_listeners_async { |l| l.process_update(data, source) }
|
|
49
|
+
@logger&.debug("[featurehubsdk] single feature update from #{source} are #{data}")
|
|
38
50
|
when :delete_feature
|
|
51
|
+
return unless data && data["key"]
|
|
52
|
+
|
|
39
53
|
delete_feature(data)
|
|
54
|
+
notify_raw_listeners_async { |l| l.delete_feature(data, source) }
|
|
55
|
+
@logger&.debug("[featurehubsdk] delete from #{source} are #{data}")
|
|
40
56
|
end
|
|
41
57
|
end
|
|
42
58
|
|
|
43
|
-
def feature(key)
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
def feature(key, attrs = nil)
|
|
60
|
+
holder = @features[key.to_sym] || make_feature_holder(key.to_sym)
|
|
61
|
+
return holder unless attrs
|
|
62
|
+
|
|
63
|
+
ClientContext.new(self, attrs).feature(key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def value(key, default_value = nil, attrs = nil)
|
|
67
|
+
f = feature(key, attrs)
|
|
68
|
+
f.present? ? f.value : default_value
|
|
46
69
|
end
|
|
47
70
|
|
|
48
71
|
def register_interceptor(interceptor)
|
|
49
72
|
@interceptors.push(interceptor)
|
|
50
73
|
end
|
|
51
74
|
|
|
52
|
-
def
|
|
53
|
-
@
|
|
75
|
+
def register_raw_update_listener(listener)
|
|
76
|
+
@raw_listeners.push(listener)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def find_interceptor(feature_key, feature_state = nil)
|
|
80
|
+
@interceptors.each do |interceptor|
|
|
81
|
+
matched, value = interceptor.intercepted_value(feature_key, self, feature_state)
|
|
82
|
+
return [true, value] if matched
|
|
83
|
+
end
|
|
84
|
+
[false, nil]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def close
|
|
88
|
+
@interceptors.each(&:close)
|
|
89
|
+
@raw_listeners.each(&:close)
|
|
54
90
|
end
|
|
55
91
|
|
|
56
92
|
def ready?
|
|
@@ -70,15 +106,13 @@ module FeatureHub
|
|
|
70
106
|
private
|
|
71
107
|
|
|
72
108
|
def delete_feature(data)
|
|
73
|
-
return unless data && data["key"]
|
|
74
|
-
|
|
75
109
|
feat = @features[data["key"].to_sym]
|
|
76
110
|
|
|
77
111
|
feat&.update_feature_state(nil)
|
|
78
112
|
end
|
|
79
113
|
|
|
80
114
|
def make_feature_holder(key)
|
|
81
|
-
fs = FeatureHub::Sdk::
|
|
115
|
+
fs = FeatureHub::Sdk::FeatureStateHolder.new(key, self)
|
|
82
116
|
@features[key.to_sym] = fs
|
|
83
117
|
fs
|
|
84
118
|
end
|
|
@@ -89,13 +123,18 @@ module FeatureHub
|
|
|
89
123
|
end
|
|
90
124
|
end
|
|
91
125
|
|
|
92
|
-
def
|
|
93
|
-
return if
|
|
126
|
+
def notify_raw_listeners_async(&block)
|
|
127
|
+
return if @raw_listeners.empty?
|
|
128
|
+
|
|
129
|
+
listeners = @raw_listeners.dup
|
|
130
|
+
Concurrent::Future.execute { listeners.each(&block) }
|
|
131
|
+
end
|
|
94
132
|
|
|
133
|
+
def update_feature(feature_state)
|
|
95
134
|
key = feature_state["key"].to_sym
|
|
96
135
|
holder = @features[key]
|
|
97
136
|
if !holder
|
|
98
|
-
@features[key] = FeatureHub::Sdk::
|
|
137
|
+
@features[key] = FeatureHub::Sdk::FeatureStateHolder.new(key, self, feature_state)
|
|
99
138
|
return
|
|
100
139
|
elsif feature_state["version"] < holder.version
|
|
101
140
|
return
|