yggdrasil-engine 1.0.0-x64-mingw-ucrt

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ea76118d13a05cc0d733785f1493c1e45af63f3e78ddb89074070663a9cc4dee
4
+ data.tar.gz: dc49b39ee6b39e5a69e84e31ac260abdf672729b4f7bdeda1f024d2258b8f413
5
+ SHA512:
6
+ metadata.gz: 25e030238b05317d09cc2de7c58ce490f3636dce89420a9fbc94881f6902744c2f526732a560a8000185eb8a71793e842f8e6a093bf2d8783c0563029e30e5f6
7
+ data.tar.gz: 2c9f57642e5dd55152ba9f369149f729800c709b593edbf9f74abc9370a475be6b25b6d7f8036c7c8ff8337d5e096f94a173233fc5533742c9e3f3d3e64adcee
data/README.md ADDED
@@ -0,0 +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
+ ```
@@ -0,0 +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
@@ -0,0 +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, 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")
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
@@ -0,0 +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
@@ -0,0 +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
+ 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 ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yggdrasil-engine
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: x64-mingw-ucrt
6
+ authors:
7
+ - Unleash
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-06-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.16.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.16.3
27
+ description: "..."
28
+ email: liquidwicked64@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - lib/custom_strategy.rb
35
+ - lib/yggdrasil_engine.rb
36
+ - lib/yggdrasilffi_x86_64.dll
37
+ - spec/custom_strategy_spec.rb
38
+ - spec/yggdrasil_engine_spec.rb
39
+ homepage: http://github.com/username/my_gem
40
+ licenses:
41
+ - MIT
42
+ metadata:
43
+ yggdrasil_core_version: 0.14.2
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.3.5
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: Unleash engine for evaluating feature toggles
63
+ test_files: []