yggdrasil-engine 0.0.6-x64-mingw32 → 0.0.8.beta.2-x64-mingw32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +36 -36
- data/lib/custom_strategy.rb +60 -60
- data/lib/yggdrasil_engine.rb +145 -131
- data/lib/yggdrasilffi_x86_64.dll +0 -0
- data/spec/custom_strategy_spec.rb +136 -136
- data/spec/yggdrasil_engine_spec.rb +161 -151
- metadata +12 -11
- data/lib/libyggdrasilffi_x86_64.dll +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 426df748bb6e5a41d283db03bb52282d3b5382018ec4894bc21082af16f3132a
|
4
|
+
data.tar.gz: 4de6173ec4a11a4c7805b28e663d06d2e19c9b3d7f713a78cbff2e138232dfbc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 164404f4662905acdbbad8c924768350532e9f1a083fd207cf2162f76336bac3de23d0434946d1f37089e934e19da603797f9f5a39d80413c07261ecec9e6e0b
|
7
|
+
data.tar.gz: f0690fadaf6b98234ef87ba8879332d9edf2137b2b697ac99b1ebce6050cb0471bd3e2e2dcc4a3dfb23a64ee284c316e5a2665c8c0a9310e8326a8d86cae5287
|
data/README.md
CHANGED
@@ -1,36 +1,36 @@
|
|
1
|
-
# Ruby Bindings to Yggdrasil
|
2
|
-
|
3
|
-
## Running the tests
|
4
|
-
|
5
|
-
First make sure that you have built the native FFI code and it's located in the right place. This can be done with the build script in `build.sh`:
|
6
|
-
|
7
|
-
|
8
|
-
```bash
|
9
|
-
./build.sh
|
10
|
-
```
|
11
|
-
|
12
|
-
Then you can run the tests with:
|
13
|
-
|
14
|
-
```bash
|
15
|
-
rspec
|
16
|
-
```
|
17
|
-
|
18
|
-
There's also a `mem_check.rb` in the scripts folder. This is not a bullet proof test, but it can be helpful for detecting large leaks. This requires human interaction - you need to read the output and understand what it's telling you, so it's not run as part of the test suite.
|
19
|
-
|
20
|
-
```bash
|
21
|
-
ruby scripts/mem_check.rb
|
22
|
-
```
|
23
|
-
|
24
|
-
## Build
|
25
|
-
|
26
|
-
You can build the gem with:
|
27
|
-
|
28
|
-
```bash
|
29
|
-
gem build yggdrasil-engine.gemspec
|
30
|
-
```
|
31
|
-
|
32
|
-
Then you can install the gem for local development with:
|
33
|
-
|
34
|
-
```
|
35
|
-
gem install yggdrasil-engine-0.0.1.gem
|
36
|
-
```
|
1
|
+
# Ruby Bindings to Yggdrasil
|
2
|
+
|
3
|
+
## Running the tests
|
4
|
+
|
5
|
+
First make sure that you have built the native FFI code and it's located in the right place. This can be done with the build script in `build.sh`:
|
6
|
+
|
7
|
+
|
8
|
+
```bash
|
9
|
+
./build.sh
|
10
|
+
```
|
11
|
+
|
12
|
+
Then you can run the tests with:
|
13
|
+
|
14
|
+
```bash
|
15
|
+
rspec
|
16
|
+
```
|
17
|
+
|
18
|
+
There's also a `mem_check.rb` in the scripts folder. This is not a bullet proof test, but it can be helpful for detecting large leaks. This requires human interaction - you need to read the output and understand what it's telling you, so it's not run as part of the test suite.
|
19
|
+
|
20
|
+
```bash
|
21
|
+
ruby scripts/mem_check.rb
|
22
|
+
```
|
23
|
+
|
24
|
+
## Build
|
25
|
+
|
26
|
+
You can build the gem with:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
gem build yggdrasil-engine.gemspec
|
30
|
+
```
|
31
|
+
|
32
|
+
Then you can install the gem for local development with:
|
33
|
+
|
34
|
+
```
|
35
|
+
gem install yggdrasil-engine-0.0.1.gem
|
36
|
+
```
|
data/lib/custom_strategy.rb
CHANGED
@@ -1,60 +1,60 @@
|
|
1
|
-
STANDARD_STRATEGIES = [
|
2
|
-
"default",
|
3
|
-
"userWithId",
|
4
|
-
"gradualRolloutUserId",
|
5
|
-
"gradualRolloutSessionId",
|
6
|
-
"gradualRolloutRandom",
|
7
|
-
"flexibleRollout",
|
8
|
-
"remoteAddress",
|
9
|
-
].freeze
|
10
|
-
|
11
|
-
class CustomStrategyHandler
|
12
|
-
def initialize
|
13
|
-
@custom_strategies_definitions = {}
|
14
|
-
@custom_strategy_implementations = {}
|
15
|
-
end
|
16
|
-
|
17
|
-
def update_strategies(json_str)
|
18
|
-
custom_strategies = {}
|
19
|
-
parsed_json = JSON.parse(json_str)
|
20
|
-
|
21
|
-
parsed_json["features"].each do |feature|
|
22
|
-
toggle_name = feature["name"]
|
23
|
-
strategies = feature["strategies"]
|
24
|
-
|
25
|
-
custom_strategies_for_toggle = strategies.select do |strategy|
|
26
|
-
!STANDARD_STRATEGIES.include?(strategy["name"])
|
27
|
-
end
|
28
|
-
|
29
|
-
unless custom_strategies_for_toggle.empty?
|
30
|
-
custom_strategies[toggle_name] = custom_strategies_for_toggle
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
@custom_strategies_definitions = custom_strategies
|
35
|
-
end
|
36
|
-
|
37
|
-
def register_custom_strategies(strategies)
|
38
|
-
strategies.each do |strategy|
|
39
|
-
if strategy.respond_to?(:name) && strategy.name.is_a?(String) &&
|
40
|
-
strategy.respond_to?(:enabled?)
|
41
|
-
@custom_strategy_implementations[strategy.name] = strategy
|
42
|
-
else
|
43
|
-
raise "Invalid strategy object. Must have a name method that returns a String and an enabled? method."
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def evaluate_custom_strategies(toggle_name, context)
|
49
|
-
results = {}
|
50
|
-
|
51
|
-
@custom_strategies_definitions[toggle_name]&.each_with_index do |strategy, index|
|
52
|
-
key = "customStrategy#{index + 1}"
|
53
|
-
strategy_impl = @custom_strategy_implementations[strategy["name"]]
|
54
|
-
result = strategy_impl&.enabled?(strategy["parameters"], context) || false
|
55
|
-
results[key] = result
|
56
|
-
end
|
57
|
-
|
58
|
-
results
|
59
|
-
end
|
60
|
-
end
|
1
|
+
STANDARD_STRATEGIES = [
|
2
|
+
"default",
|
3
|
+
"userWithId",
|
4
|
+
"gradualRolloutUserId",
|
5
|
+
"gradualRolloutSessionId",
|
6
|
+
"gradualRolloutRandom",
|
7
|
+
"flexibleRollout",
|
8
|
+
"remoteAddress",
|
9
|
+
].freeze
|
10
|
+
|
11
|
+
class CustomStrategyHandler
|
12
|
+
def initialize
|
13
|
+
@custom_strategies_definitions = {}
|
14
|
+
@custom_strategy_implementations = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def update_strategies(json_str)
|
18
|
+
custom_strategies = {}
|
19
|
+
parsed_json = JSON.parse(json_str)
|
20
|
+
|
21
|
+
parsed_json["features"].each do |feature|
|
22
|
+
toggle_name = feature["name"]
|
23
|
+
strategies = feature["strategies"]
|
24
|
+
|
25
|
+
custom_strategies_for_toggle = strategies.select do |strategy|
|
26
|
+
!STANDARD_STRATEGIES.include?(strategy["name"])
|
27
|
+
end
|
28
|
+
|
29
|
+
unless custom_strategies_for_toggle.empty?
|
30
|
+
custom_strategies[toggle_name] = custom_strategies_for_toggle
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
@custom_strategies_definitions = custom_strategies
|
35
|
+
end
|
36
|
+
|
37
|
+
def register_custom_strategies(strategies)
|
38
|
+
strategies.each do |strategy|
|
39
|
+
if strategy.respond_to?(:name) && strategy.name.is_a?(String) &&
|
40
|
+
strategy.respond_to?(:enabled?)
|
41
|
+
@custom_strategy_implementations[strategy.name] = strategy
|
42
|
+
else
|
43
|
+
raise "Invalid strategy object. Must have a name method that returns a String and an enabled? method."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def evaluate_custom_strategies(toggle_name, context)
|
49
|
+
results = {}
|
50
|
+
|
51
|
+
@custom_strategies_definitions[toggle_name]&.each_with_index do |strategy, index|
|
52
|
+
key = "customStrategy#{index + 1}"
|
53
|
+
strategy_impl = @custom_strategy_implementations[strategy["name"]]
|
54
|
+
result = strategy_impl&.enabled?(strategy["parameters"], context) || false
|
55
|
+
results[key] = result
|
56
|
+
end
|
57
|
+
|
58
|
+
results
|
59
|
+
end
|
60
|
+
end
|
data/lib/yggdrasil_engine.rb
CHANGED
@@ -1,131 +1,145 @@
|
|
1
|
-
require 'ffi'
|
2
|
-
require 'json'
|
3
|
-
require 'custom_strategy'
|
4
|
-
|
5
|
-
TOGGLE_MISSING_RESPONSE = 'NotFound'.freeze
|
6
|
-
ERROR_RESPONSE = 'Error'.freeze
|
7
|
-
OK_RESPONSE = 'Ok'.freeze
|
8
|
-
|
9
|
-
def platform_specific_lib
|
10
|
-
os = RbConfig::CONFIG['host_os']
|
11
|
-
cpu = RbConfig::CONFIG['host_cpu']
|
12
|
-
|
13
|
-
extension = case os
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
arch_suffix = case cpu
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
{
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
attach_function :
|
58
|
-
attach_function :
|
59
|
-
|
60
|
-
|
61
|
-
attach_function :
|
62
|
-
attach_function :
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
YggdrasilEngine.
|
79
|
-
end
|
80
|
-
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
YggdrasilEngine.
|
125
|
-
|
126
|
-
end
|
127
|
-
|
128
|
-
def
|
129
|
-
|
130
|
-
|
131
|
-
|
1
|
+
require 'ffi'
|
2
|
+
require 'json'
|
3
|
+
require 'custom_strategy'
|
4
|
+
|
5
|
+
TOGGLE_MISSING_RESPONSE = 'NotFound'.freeze
|
6
|
+
ERROR_RESPONSE = 'Error'.freeze
|
7
|
+
OK_RESPONSE = 'Ok'.freeze
|
8
|
+
|
9
|
+
def platform_specific_lib
|
10
|
+
os = RbConfig::CONFIG['host_os']
|
11
|
+
cpu = RbConfig::CONFIG['host_cpu']
|
12
|
+
|
13
|
+
extension, prefix = case os
|
14
|
+
when /darwin|mac os/
|
15
|
+
['dylib', 'lib']
|
16
|
+
when /linux/
|
17
|
+
['so', 'lib']
|
18
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
19
|
+
['dll', '']
|
20
|
+
else
|
21
|
+
raise "unsupported platform #{os}"
|
22
|
+
end
|
23
|
+
|
24
|
+
arch_suffix = case cpu
|
25
|
+
when /x86_64/
|
26
|
+
'x86_64'
|
27
|
+
when /arm|aarch64/
|
28
|
+
'arm64'
|
29
|
+
else
|
30
|
+
raise "unsupported architecture #{cpu}"
|
31
|
+
end
|
32
|
+
|
33
|
+
lib_type_suffix = if os =~ /linux/
|
34
|
+
musl = system("ldd /bin/sh | grep -q musl") # Check if musl is in use
|
35
|
+
musl ? "-musl" : ""
|
36
|
+
else
|
37
|
+
""
|
38
|
+
end
|
39
|
+
|
40
|
+
"#{prefix}yggdrasilffi_#{arch_suffix}#{lib_type_suffix}.#{extension}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_variant(raw_variant)
|
44
|
+
payload = raw_variant[:payload] && raw_variant[:payload].transform_keys(&:to_s)
|
45
|
+
{
|
46
|
+
name: raw_variant[:name],
|
47
|
+
enabled: raw_variant[:enabled],
|
48
|
+
feature_enabled: raw_variant[:featureEnabled],
|
49
|
+
payload: payload,
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
class YggdrasilEngine
|
54
|
+
extend FFI::Library
|
55
|
+
ffi_lib File.expand_path(platform_specific_lib, __dir__)
|
56
|
+
|
57
|
+
attach_function :new_engine, [], :pointer
|
58
|
+
attach_function :free_engine, [:pointer], :void
|
59
|
+
|
60
|
+
attach_function :take_state, %i[pointer string], :pointer
|
61
|
+
attach_function :check_enabled, %i[pointer string string string], :pointer
|
62
|
+
attach_function :check_variant, %i[pointer string string string], :pointer
|
63
|
+
attach_function :get_metrics, [:pointer], :pointer
|
64
|
+
attach_function :free_response, [:pointer], :void
|
65
|
+
|
66
|
+
attach_function :count_toggle, %i[pointer string bool], :void
|
67
|
+
attach_function :count_variant, %i[pointer string string], :void
|
68
|
+
|
69
|
+
attach_function :list_known_toggles, [:pointer], :pointer
|
70
|
+
|
71
|
+
def initialize
|
72
|
+
@engine = YggdrasilEngine.new_engine
|
73
|
+
@custom_strategy_handler = CustomStrategyHandler.new
|
74
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(@engine))
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.finalize(engine)
|
78
|
+
proc { YggdrasilEngine.free_engine(engine) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def take_state(toggles)
|
82
|
+
@custom_strategy_handler.update_strategies(toggles)
|
83
|
+
response_ptr = YggdrasilEngine.take_state(@engine, toggles)
|
84
|
+
take_toggles_response = JSON.parse(response_ptr.read_string, symbolize_names: true)
|
85
|
+
YggdrasilEngine.free_response(response_ptr)
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_variant(name, context)
|
89
|
+
context_json = (context || {}).to_json
|
90
|
+
custom_strategy_results = @custom_strategy_handler.evaluate_custom_strategies(name, context).to_json
|
91
|
+
|
92
|
+
variant_def_json_ptr = YggdrasilEngine.check_variant(@engine, name, context_json, custom_strategy_results)
|
93
|
+
variant_def_json = variant_def_json_ptr.read_string
|
94
|
+
YggdrasilEngine.free_response(variant_def_json_ptr)
|
95
|
+
variant_response = JSON.parse(variant_def_json, symbolize_names: true)
|
96
|
+
|
97
|
+
return nil if variant_response[:status_code] == TOGGLE_MISSING_RESPONSE
|
98
|
+
variant = variant_response[:value]
|
99
|
+
|
100
|
+
return to_variant(variant) if variant_response[:status_code] == OK_RESPONSE
|
101
|
+
end
|
102
|
+
|
103
|
+
def enabled?(toggle_name, context)
|
104
|
+
context_json = (context || {}).to_json
|
105
|
+
custom_strategy_results = @custom_strategy_handler.evaluate_custom_strategies(toggle_name, context).to_json
|
106
|
+
|
107
|
+
response_ptr = YggdrasilEngine.check_enabled(@engine, toggle_name, context_json, custom_strategy_results)
|
108
|
+
response_json = response_ptr.read_string
|
109
|
+
YggdrasilEngine.free_response(response_ptr)
|
110
|
+
response = JSON.parse(response_json, symbolize_names: true)
|
111
|
+
|
112
|
+
raise "Error: #{response[:error_message]}" if response[:status_code] == ERROR_RESPONSE
|
113
|
+
return nil if response[:status_code] == TOGGLE_MISSING_RESPONSE
|
114
|
+
|
115
|
+
return response[:value] == true
|
116
|
+
end
|
117
|
+
|
118
|
+
def count_toggle(toggle_name, enabled)
|
119
|
+
response_ptr = YggdrasilEngine.count_toggle(@engine, toggle_name, enabled)
|
120
|
+
YggdrasilEngine.free_response(response_ptr)
|
121
|
+
end
|
122
|
+
|
123
|
+
def count_variant(toggle_name, variant_name)
|
124
|
+
response_ptr = YggdrasilEngine.count_variant(@engine, toggle_name, variant_name)
|
125
|
+
YggdrasilEngine.free_response(response_ptr)
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_metrics
|
129
|
+
metrics_ptr = YggdrasilEngine.get_metrics(@engine)
|
130
|
+
metrics = JSON.parse(metrics_ptr.read_string, symbolize_names: true)
|
131
|
+
YggdrasilEngine.free_response(metrics_ptr)
|
132
|
+
metrics[:value]
|
133
|
+
end
|
134
|
+
|
135
|
+
def list_known_toggles
|
136
|
+
response_ptr = YggdrasilEngine.list_known_toggles(@engine)
|
137
|
+
response_json = response_ptr.read_string
|
138
|
+
YggdrasilEngine.free_response(response_ptr)
|
139
|
+
JSON.parse(response_json, symbolize_names: true)
|
140
|
+
end
|
141
|
+
|
142
|
+
def register_custom_strategies(strategies)
|
143
|
+
@custom_strategy_handler.register_custom_strategies(strategies)
|
144
|
+
end
|
145
|
+
end
|
Binary file
|
@@ -1,136 +1,136 @@
|
|
1
|
-
require_relative '../lib/custom_strategy'
|
2
|
-
|
3
|
-
RSpec.describe "custom strategies" do
|
4
|
-
let(:raw_state) do
|
5
|
-
{
|
6
|
-
"version": 1,
|
7
|
-
"features": [
|
8
|
-
{
|
9
|
-
"name": "Feature.A",
|
10
|
-
"enabled": true,
|
11
|
-
"strategies": [
|
12
|
-
{
|
13
|
-
"name": "default",
|
14
|
-
"parameters": {}
|
15
|
-
},
|
16
|
-
{
|
17
|
-
"name": "custom",
|
18
|
-
"parameters": {
|
19
|
-
"gerkhins": "yes"
|
20
|
-
}
|
21
|
-
},
|
22
|
-
{
|
23
|
-
"name": "some-other-custom",
|
24
|
-
"parameters": {
|
25
|
-
"gerkhins": "yes"
|
26
|
-
}
|
27
|
-
},
|
28
|
-
]
|
29
|
-
}
|
30
|
-
]
|
31
|
-
}
|
32
|
-
end
|
33
|
-
|
34
|
-
let(:handler) { CustomStrategyHandler.new }
|
35
|
-
|
36
|
-
before do
|
37
|
-
handler.update_strategies(raw_state.to_json)
|
38
|
-
end
|
39
|
-
|
40
|
-
describe 'computing custom strategies' do
|
41
|
-
it 'respects the logic contained in the enabled function' do
|
42
|
-
class TestStrategy
|
43
|
-
attr_reader :name
|
44
|
-
|
45
|
-
def initialize(name)
|
46
|
-
@name = name
|
47
|
-
end
|
48
|
-
|
49
|
-
def enabled?(params, context)
|
50
|
-
params["gerkhins"] == "yes"
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
handler.register_custom_strategies([TestStrategy.new("custom")])
|
55
|
-
strategy_results = handler.evaluate_custom_strategies("Feature.A", {})
|
56
|
-
expect(strategy_results.length).to eq(2)
|
57
|
-
expect(strategy_results["customStrategy1"]).to eq(true)
|
58
|
-
expect(strategy_results["customStrategy2"]).to eq(false)
|
59
|
-
end
|
60
|
-
|
61
|
-
it 'creates a strategy result for every custom strategy thats implemented and defined' do
|
62
|
-
class TestStrategy
|
63
|
-
attr_reader :name
|
64
|
-
|
65
|
-
def initialize(name)
|
66
|
-
@name = name
|
67
|
-
end
|
68
|
-
|
69
|
-
def enabled?(params, context)
|
70
|
-
params["gerkhins"] == "yes"
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
handler.register_custom_strategies([TestStrategy.new("custom"), TestStrategy.new("some-other-custom")])
|
75
|
-
strategy_results = handler.evaluate_custom_strategies("Feature.A", {})
|
76
|
-
expect(strategy_results.length).to eq(2)
|
77
|
-
expect(strategy_results["customStrategy1"]).to eq(true)
|
78
|
-
expect(strategy_results["customStrategy2"]).to eq(true)
|
79
|
-
end
|
80
|
-
|
81
|
-
it 'returns false for missing implementations' do
|
82
|
-
handler.register_custom_strategies([])
|
83
|
-
strategy_results = handler.evaluate_custom_strategies("Feature.A", {})
|
84
|
-
expect(strategy_results.length).to eq(2)
|
85
|
-
expect(strategy_results["customStrategy1"]).to eq(false)
|
86
|
-
end
|
87
|
-
|
88
|
-
it "should calculate custom strategies e2e" do
|
89
|
-
class TestStrategy
|
90
|
-
attr_reader :name
|
91
|
-
|
92
|
-
def initialize(name)
|
93
|
-
@name = name
|
94
|
-
end
|
95
|
-
|
96
|
-
def enabled?(params, context)
|
97
|
-
context[:userId] == "123"
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
state = {
|
102
|
-
"version": 1,
|
103
|
-
"features": [
|
104
|
-
{
|
105
|
-
"name": "Feature.A",
|
106
|
-
"enabled": true,
|
107
|
-
"strategies": [
|
108
|
-
{
|
109
|
-
"name": "custom",
|
110
|
-
"parameters": {
|
111
|
-
"gerkhins": "yes"
|
112
|
-
}
|
113
|
-
}
|
114
|
-
]
|
115
|
-
}
|
116
|
-
]
|
117
|
-
}
|
118
|
-
|
119
|
-
engine = YggdrasilEngine.new
|
120
|
-
engine.register_custom_strategies([TestStrategy.new("custom")])
|
121
|
-
|
122
|
-
engine.take_state(state.to_json)
|
123
|
-
|
124
|
-
should_be_enabled = engine.enabled?("Feature.A", {
|
125
|
-
userId: "123"
|
126
|
-
})
|
127
|
-
|
128
|
-
should_not_be_enabled = engine.enabled?("Feature.A", {
|
129
|
-
userId: "456"
|
130
|
-
})
|
131
|
-
|
132
|
-
expect(should_be_enabled).to eq(true)
|
133
|
-
expect(should_not_be_enabled).to eq(false)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
1
|
+
require_relative '../lib/custom_strategy'
|
2
|
+
|
3
|
+
RSpec.describe "custom strategies" do
|
4
|
+
let(:raw_state) do
|
5
|
+
{
|
6
|
+
"version": 1,
|
7
|
+
"features": [
|
8
|
+
{
|
9
|
+
"name": "Feature.A",
|
10
|
+
"enabled": true,
|
11
|
+
"strategies": [
|
12
|
+
{
|
13
|
+
"name": "default",
|
14
|
+
"parameters": {}
|
15
|
+
},
|
16
|
+
{
|
17
|
+
"name": "custom",
|
18
|
+
"parameters": {
|
19
|
+
"gerkhins": "yes"
|
20
|
+
}
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"name": "some-other-custom",
|
24
|
+
"parameters": {
|
25
|
+
"gerkhins": "yes"
|
26
|
+
}
|
27
|
+
},
|
28
|
+
]
|
29
|
+
}
|
30
|
+
]
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:handler) { CustomStrategyHandler.new }
|
35
|
+
|
36
|
+
before do
|
37
|
+
handler.update_strategies(raw_state.to_json)
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'computing custom strategies' do
|
41
|
+
it 'respects the logic contained in the enabled function' do
|
42
|
+
class TestStrategy
|
43
|
+
attr_reader :name
|
44
|
+
|
45
|
+
def initialize(name)
|
46
|
+
@name = name
|
47
|
+
end
|
48
|
+
|
49
|
+
def enabled?(params, context)
|
50
|
+
params["gerkhins"] == "yes"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
handler.register_custom_strategies([TestStrategy.new("custom")])
|
55
|
+
strategy_results = handler.evaluate_custom_strategies("Feature.A", {})
|
56
|
+
expect(strategy_results.length).to eq(2)
|
57
|
+
expect(strategy_results["customStrategy1"]).to eq(true)
|
58
|
+
expect(strategy_results["customStrategy2"]).to eq(false)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'creates a strategy result for every custom strategy thats implemented and defined' do
|
62
|
+
class TestStrategy
|
63
|
+
attr_reader :name
|
64
|
+
|
65
|
+
def initialize(name)
|
66
|
+
@name = name
|
67
|
+
end
|
68
|
+
|
69
|
+
def enabled?(params, context)
|
70
|
+
params["gerkhins"] == "yes"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
handler.register_custom_strategies([TestStrategy.new("custom"), TestStrategy.new("some-other-custom")])
|
75
|
+
strategy_results = handler.evaluate_custom_strategies("Feature.A", {})
|
76
|
+
expect(strategy_results.length).to eq(2)
|
77
|
+
expect(strategy_results["customStrategy1"]).to eq(true)
|
78
|
+
expect(strategy_results["customStrategy2"]).to eq(true)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns false for missing implementations' do
|
82
|
+
handler.register_custom_strategies([])
|
83
|
+
strategy_results = handler.evaluate_custom_strategies("Feature.A", {})
|
84
|
+
expect(strategy_results.length).to eq(2)
|
85
|
+
expect(strategy_results["customStrategy1"]).to eq(false)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should calculate custom strategies e2e" do
|
89
|
+
class TestStrategy
|
90
|
+
attr_reader :name
|
91
|
+
|
92
|
+
def initialize(name)
|
93
|
+
@name = name
|
94
|
+
end
|
95
|
+
|
96
|
+
def enabled?(params, context)
|
97
|
+
context[:userId] == "123"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
state = {
|
102
|
+
"version": 1,
|
103
|
+
"features": [
|
104
|
+
{
|
105
|
+
"name": "Feature.A",
|
106
|
+
"enabled": true,
|
107
|
+
"strategies": [
|
108
|
+
{
|
109
|
+
"name": "custom",
|
110
|
+
"parameters": {
|
111
|
+
"gerkhins": "yes"
|
112
|
+
}
|
113
|
+
}
|
114
|
+
]
|
115
|
+
}
|
116
|
+
]
|
117
|
+
}
|
118
|
+
|
119
|
+
engine = YggdrasilEngine.new
|
120
|
+
engine.register_custom_strategies([TestStrategy.new("custom")])
|
121
|
+
|
122
|
+
engine.take_state(state.to_json)
|
123
|
+
|
124
|
+
should_be_enabled = engine.enabled?("Feature.A", {
|
125
|
+
userId: "123"
|
126
|
+
})
|
127
|
+
|
128
|
+
should_not_be_enabled = engine.enabled?("Feature.A", {
|
129
|
+
userId: "456"
|
130
|
+
})
|
131
|
+
|
132
|
+
expect(should_be_enabled).to eq(true)
|
133
|
+
expect(should_not_be_enabled).to eq(false)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -1,151 +1,161 @@
|
|
1
|
-
require 'rspec'
|
2
|
-
require 'json'
|
3
|
-
require_relative '../lib/yggdrasil_engine'
|
4
|
-
|
5
|
-
index_file_path = '../client-specification/specifications/index.json'
|
6
|
-
test_suites = JSON.parse(File.read(index_file_path))
|
7
|
-
|
8
|
-
def test_suite_variant(base_variant)
|
9
|
-
payload = base_variant[:payload] && base_variant[:payload].transform_keys(&:to_s)
|
10
|
-
{
|
11
|
-
name: base_variant[:name],
|
12
|
-
enabled: base_variant[:enabled],
|
13
|
-
feature_enabled: base_variant[:feature_enabled],
|
14
|
-
payload: payload,
|
15
|
-
}
|
16
|
-
end
|
17
|
-
|
18
|
-
RSpec.describe YggdrasilEngine do
|
19
|
-
let(:yggdrasil_engine) { YggdrasilEngine.new }
|
20
|
-
|
21
|
-
describe '#checking a toggle' do
|
22
|
-
it 'that does not exist should yield a not found' do
|
23
|
-
is_enabled = yggdrasil_engine.enabled?('missing-toggle', {})
|
24
|
-
expect(is_enabled).to be_nil
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
describe '#metrics' do
|
29
|
-
it 'should clear metrics when get_metrics is called' do
|
30
|
-
feature_name = 'Feature.A'
|
31
|
-
|
32
|
-
suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
|
33
|
-
suite_data = JSON.parse(File.read(suite_path))
|
34
|
-
|
35
|
-
yggdrasil_engine.take_state(suite_data['state'].to_json)
|
36
|
-
|
37
|
-
yggdrasil_engine.count_toggle(feature_name, true)
|
38
|
-
yggdrasil_engine.count_toggle(feature_name, false)
|
39
|
-
|
40
|
-
metrics = yggdrasil_engine.get_metrics() # This should clear the metrics buffer
|
41
|
-
|
42
|
-
metric = metrics[:toggles][feature_name.to_sym]
|
43
|
-
expect(metric[:yes]).to eq(1)
|
44
|
-
expect(metric[:no]).to eq(1)
|
45
|
-
|
46
|
-
metrics = yggdrasil_engine.get_metrics()
|
47
|
-
expect(metrics).to be_nil
|
48
|
-
end
|
49
|
-
|
50
|
-
it 'should increment toggle count when it exists' do
|
51
|
-
toggle_name = 'Feature.A'
|
52
|
-
|
53
|
-
suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
|
54
|
-
suite_data = JSON.parse(File.read(suite_path))
|
55
|
-
|
56
|
-
yggdrasil_engine.take_state(suite_data['state'].to_json)
|
57
|
-
|
58
|
-
yggdrasil_engine.count_toggle(toggle_name, true)
|
59
|
-
yggdrasil_engine.count_toggle(toggle_name, false)
|
60
|
-
|
61
|
-
metrics = yggdrasil_engine.get_metrics()
|
62
|
-
metric = metrics[:toggles][toggle_name.to_sym]
|
63
|
-
|
64
|
-
expect(metric[:yes]).to eq(1)
|
65
|
-
expect(metric[:no]).to eq(1)
|
66
|
-
end
|
67
|
-
|
68
|
-
it 'should increment toggle count when the toggle does not exist' do
|
69
|
-
toggle_name = 'Feature.X'
|
70
|
-
|
71
|
-
yggdrasil_engine.count_toggle(toggle_name, true)
|
72
|
-
yggdrasil_engine.count_toggle(toggle_name, false)
|
73
|
-
|
74
|
-
metrics = yggdrasil_engine.get_metrics()
|
75
|
-
metric = metrics[:toggles][toggle_name.to_sym]
|
76
|
-
|
77
|
-
expect(metric[:yes]).to eq(1)
|
78
|
-
expect(metric[:no]).to eq(1)
|
79
|
-
end
|
80
|
-
|
81
|
-
it 'should increment variant' do
|
82
|
-
toggle_name = 'Feature.Q'
|
83
|
-
|
84
|
-
suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
|
85
|
-
suite_data = JSON.parse(File.read(suite_path))
|
86
|
-
|
87
|
-
yggdrasil_engine.take_state(suite_data['state'].to_json)
|
88
|
-
|
89
|
-
yggdrasil_engine.count_variant(toggle_name, 'disabled')
|
90
|
-
|
91
|
-
metrics = yggdrasil_engine.get_metrics()
|
92
|
-
metric = metrics[:toggles][toggle_name.to_sym]
|
93
|
-
|
94
|
-
expect(metric[:variants][:disabled]).to eq(1)
|
95
|
-
end
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
1
|
+
require 'rspec'
|
2
|
+
require 'json'
|
3
|
+
require_relative '../lib/yggdrasil_engine'
|
4
|
+
|
5
|
+
index_file_path = '../client-specification/specifications/index.json'
|
6
|
+
test_suites = JSON.parse(File.read(index_file_path))
|
7
|
+
|
8
|
+
def test_suite_variant(base_variant)
|
9
|
+
payload = base_variant[:payload] && base_variant[:payload].transform_keys(&:to_s)
|
10
|
+
{
|
11
|
+
name: base_variant[:name],
|
12
|
+
enabled: base_variant[:enabled],
|
13
|
+
feature_enabled: base_variant[:feature_enabled],
|
14
|
+
payload: payload,
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec.describe YggdrasilEngine do
|
19
|
+
let(:yggdrasil_engine) { YggdrasilEngine.new }
|
20
|
+
|
21
|
+
describe '#checking a toggle' do
|
22
|
+
it 'that does not exist should yield a not found' do
|
23
|
+
is_enabled = yggdrasil_engine.enabled?('missing-toggle', {})
|
24
|
+
expect(is_enabled).to be_nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#metrics' do
|
29
|
+
it 'should clear metrics when get_metrics is called' do
|
30
|
+
feature_name = 'Feature.A'
|
31
|
+
|
32
|
+
suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
|
33
|
+
suite_data = JSON.parse(File.read(suite_path))
|
34
|
+
|
35
|
+
yggdrasil_engine.take_state(suite_data['state'].to_json)
|
36
|
+
|
37
|
+
yggdrasil_engine.count_toggle(feature_name, true)
|
38
|
+
yggdrasil_engine.count_toggle(feature_name, false)
|
39
|
+
|
40
|
+
metrics = yggdrasil_engine.get_metrics() # This should clear the metrics buffer
|
41
|
+
|
42
|
+
metric = metrics[:toggles][feature_name.to_sym]
|
43
|
+
expect(metric[:yes]).to eq(1)
|
44
|
+
expect(metric[:no]).to eq(1)
|
45
|
+
|
46
|
+
metrics = yggdrasil_engine.get_metrics()
|
47
|
+
expect(metrics).to be_nil
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should increment toggle count when it exists' do
|
51
|
+
toggle_name = 'Feature.A'
|
52
|
+
|
53
|
+
suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
|
54
|
+
suite_data = JSON.parse(File.read(suite_path))
|
55
|
+
|
56
|
+
yggdrasil_engine.take_state(suite_data['state'].to_json)
|
57
|
+
|
58
|
+
yggdrasil_engine.count_toggle(toggle_name, true)
|
59
|
+
yggdrasil_engine.count_toggle(toggle_name, false)
|
60
|
+
|
61
|
+
metrics = yggdrasil_engine.get_metrics()
|
62
|
+
metric = metrics[:toggles][toggle_name.to_sym]
|
63
|
+
|
64
|
+
expect(metric[:yes]).to eq(1)
|
65
|
+
expect(metric[:no]).to eq(1)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should increment toggle count when the toggle does not exist' do
|
69
|
+
toggle_name = 'Feature.X'
|
70
|
+
|
71
|
+
yggdrasil_engine.count_toggle(toggle_name, true)
|
72
|
+
yggdrasil_engine.count_toggle(toggle_name, false)
|
73
|
+
|
74
|
+
metrics = yggdrasil_engine.get_metrics()
|
75
|
+
metric = metrics[:toggles][toggle_name.to_sym]
|
76
|
+
|
77
|
+
expect(metric[:yes]).to eq(1)
|
78
|
+
expect(metric[:no]).to eq(1)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should increment variant' do
|
82
|
+
toggle_name = 'Feature.Q'
|
83
|
+
|
84
|
+
suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
|
85
|
+
suite_data = JSON.parse(File.read(suite_path))
|
86
|
+
|
87
|
+
yggdrasil_engine.take_state(suite_data['state'].to_json)
|
88
|
+
|
89
|
+
yggdrasil_engine.count_variant(toggle_name, 'disabled')
|
90
|
+
|
91
|
+
metrics = yggdrasil_engine.get_metrics()
|
92
|
+
metric = metrics[:toggles][toggle_name.to_sym]
|
93
|
+
|
94
|
+
expect(metric[:variants][:disabled]).to eq(1)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should list all the features that were loaded' do
|
98
|
+
suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
|
99
|
+
suite_data = JSON.parse(File.read(suite_path))
|
100
|
+
|
101
|
+
yggdrasil_engine.take_state(suite_data['state'].to_json)
|
102
|
+
|
103
|
+
toggles = yggdrasil_engine.list_known_toggles()
|
104
|
+
expect(toggles.length).to eq(3)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
RSpec.describe 'Client Specification' do
|
110
|
+
let(:yggdrasil_engine) { YggdrasilEngine.new }
|
111
|
+
|
112
|
+
test_suites.each do |suite|
|
113
|
+
suite_path = File.join('../client-specification/specifications', suite)
|
114
|
+
suite_data = JSON.parse(File.read(suite_path), symbolize_names: true)
|
115
|
+
|
116
|
+
describe "Suite '#{suite}'" do
|
117
|
+
before(:each) do
|
118
|
+
yggdrasil_engine.take_state(suite_data[:state].to_json)
|
119
|
+
end
|
120
|
+
|
121
|
+
suite_data.fetch(:tests, []).each do |test|
|
122
|
+
describe "Test '#{test[:description]}'" do
|
123
|
+
let(:context) { test[:context] }
|
124
|
+
let(:toggle_name) { test[:toggleName] }
|
125
|
+
let(:expected_result) { test[:expectedResult] }
|
126
|
+
|
127
|
+
it 'returns correct result for `is_enabled?` method' do
|
128
|
+
result = yggdrasil_engine.enabled?(toggle_name, context) || false
|
129
|
+
|
130
|
+
expect(result).to eq(expected_result),
|
131
|
+
"Failed test '#{test['description']}': expected #{expected_result}, got #{result}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
suite_data.fetch(:variantTests, []).each do |test|
|
137
|
+
next unless test[:expectedResult]
|
138
|
+
|
139
|
+
describe "Variant Test '#{test[:description]}'" do
|
140
|
+
let(:context) { test[:context] }
|
141
|
+
let(:toggle_name) { test[:toggleName] }
|
142
|
+
let(:expected_result) { test_suite_variant(test[:expectedResult]) }
|
143
|
+
|
144
|
+
it 'returns correct result for `get_variant` method' do
|
145
|
+
result = yggdrasil_engine.get_variant(toggle_name, context) || {
|
146
|
+
:name => 'disabled',
|
147
|
+
:payload => nil,
|
148
|
+
:enabled => false,
|
149
|
+
:feature_enabled => false
|
150
|
+
}
|
151
|
+
|
152
|
+
expect(result[:name]).to eq(expected_result[:name])
|
153
|
+
expect(result[:payload]).to eq(expected_result[:payload])
|
154
|
+
expect(result[:enabled]).to eq(expected_result[:enabled])
|
155
|
+
expect(result[:feature_enabled]).to eq(expected_result[:feature_enabled])
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
metadata
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: yggdrasil-engine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8.beta.2
|
5
5
|
platform: x64-mingw32
|
6
6
|
authors:
|
7
7
|
- Unleash
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
date: 2023-06-28 00:00:00.000000000 Z
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 1.
|
19
|
+
version: 1.16.3
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 1.
|
26
|
+
version: 1.16.3
|
27
27
|
description: "..."
|
28
28
|
email: liquidwicked64@gmail.com
|
29
29
|
executables: []
|
@@ -32,15 +32,16 @@ extra_rdoc_files: []
|
|
32
32
|
files:
|
33
33
|
- README.md
|
34
34
|
- lib/custom_strategy.rb
|
35
|
-
- lib/libyggdrasilffi_x86_64.dll
|
36
35
|
- lib/yggdrasil_engine.rb
|
36
|
+
- lib/yggdrasilffi_x86_64.dll
|
37
37
|
- spec/custom_strategy_spec.rb
|
38
38
|
- spec/yggdrasil_engine_spec.rb
|
39
39
|
homepage: http://github.com/username/my_gem
|
40
40
|
licenses:
|
41
41
|
- MIT
|
42
|
-
metadata:
|
43
|
-
|
42
|
+
metadata:
|
43
|
+
yggdrasil_core_version: 0.14.0
|
44
|
+
post_install_message:
|
44
45
|
rdoc_options: []
|
45
46
|
require_paths:
|
46
47
|
- lib
|
@@ -51,12 +52,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
52
|
version: '0'
|
52
53
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
54
|
requirements:
|
54
|
-
- - "
|
55
|
+
- - ">"
|
55
56
|
- !ruby/object:Gem::Version
|
56
|
-
version:
|
57
|
+
version: 1.3.1
|
57
58
|
requirements: []
|
58
|
-
rubygems_version: 3.
|
59
|
-
signing_key:
|
59
|
+
rubygems_version: 3.3.5
|
60
|
+
signing_key:
|
60
61
|
specification_version: 4
|
61
62
|
summary: Unleash engine for evaluating feature toggles
|
62
63
|
test_files: []
|
Binary file
|