yggdrasil-engine 0.0.5.beta.19-universal-java-21

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ccce3b3e3107ce29e23caf7a912897e22aafc23399ea9c66913791b5e51e03b3
4
+ data.tar.gz: 7b7687b3f9ed21091459aeadbf5072d99dbcfb4826a2103b4e8ed93c25d6f882
5
+ SHA512:
6
+ metadata.gz: 8c1bc8d32ed21d1ed663394fd3a4e0f807edcd03bfa22e05d9137f0d3fdea02650436abaf5dca5a1e039741cdf8fb86d4eb508cf0b98492f4046ee19eb2287a0
7
+ data.tar.gz: 919eda38482431332707273ca6193d8e2b24754137684cf8a5ecd7025be9bc79ad36b705dca81b5721d2f00a4de3d241462c7b043b5f1e911abb9e357e789ee9
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
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,129 @@
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
+ when /darwin|mac os/
15
+ 'dylib'
16
+ when /linux/
17
+ 'so'
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
+ "libyggdrasilffi_#{arch_suffix}.#{extension}"
34
+ end
35
+
36
+ def to_variant(raw_variant)
37
+ payload = raw_variant[:payload] && raw_variant[:payload].transform_keys(&:to_s)
38
+ {
39
+ name: raw_variant[:name],
40
+ enabled: raw_variant[:enabled],
41
+ feature_enabled: raw_variant[:featureEnabled],
42
+ payload: payload,
43
+ }
44
+ end
45
+
46
+ class YggdrasilEngine
47
+ extend FFI::Library
48
+ ffi_lib File.expand_path(platform_specific_lib, __dir__)
49
+
50
+ attach_function :new_engine, [], :pointer
51
+ attach_function :free_engine, [:pointer], :void
52
+
53
+ attach_function :take_state, %i[pointer string], :pointer
54
+ attach_function :check_enabled, %i[pointer string string string], :pointer
55
+ attach_function :check_variant, %i[pointer string string string], :pointer
56
+ attach_function :get_metrics, [:pointer], :pointer
57
+ attach_function :free_response, [:pointer], :void
58
+
59
+ attach_function :count_toggle, %i[pointer string bool], :void
60
+ attach_function :count_variant, %i[pointer string string], :void
61
+
62
+ def initialize
63
+ @engine = YggdrasilEngine.new_engine
64
+ @custom_strategy_handler = CustomStrategyHandler.new
65
+ ObjectSpace.define_finalizer(self, self.class.finalize(@engine))
66
+ end
67
+
68
+ def self.finalize(engine)
69
+ proc { YggdrasilEngine.free_engine(engine) }
70
+ end
71
+
72
+ def take_state(toggles)
73
+ @custom_strategy_handler.update_strategies(toggles)
74
+ response_ptr = YggdrasilEngine.take_state(@engine, toggles)
75
+ take_toggles_response = JSON.parse(response_ptr.read_string, symbolize_names: true)
76
+ YggdrasilEngine.free_response(response_ptr)
77
+ end
78
+
79
+ def get_variant(name, context)
80
+ context_json = (context || {}).to_json
81
+ custom_strategy_results = @custom_strategy_handler.evaluate_custom_strategies(name, context).to_json
82
+
83
+ variant_def_json_ptr = YggdrasilEngine.check_variant(@engine, name, context_json, custom_strategy_results)
84
+ variant_def_json = variant_def_json_ptr.read_string
85
+ YggdrasilEngine.free_response(variant_def_json_ptr)
86
+ variant_response = JSON.parse(variant_def_json, symbolize_names: true)
87
+
88
+ return nil if variant_response[:status_code] == TOGGLE_MISSING_RESPONSE
89
+ variant = variant_response[:value]
90
+
91
+ return to_variant(variant) if variant_response[:status_code] == OK_RESPONSE
92
+ end
93
+
94
+ def enabled?(toggle_name, context)
95
+ context_json = (context || {}).to_json
96
+ custom_strategy_results = @custom_strategy_handler.evaluate_custom_strategies(toggle_name, context).to_json
97
+
98
+ response_ptr = YggdrasilEngine.check_enabled(@engine, toggle_name, context_json, custom_strategy_results)
99
+ response_json = response_ptr.read_string
100
+ YggdrasilEngine.free_response(response_ptr)
101
+ response = JSON.parse(response_json, symbolize_names: true)
102
+
103
+ raise "Error: #{response[:error_message]}" if response[:status_code] == ERROR_RESPONSE
104
+ return nil if response[:status_code] == TOGGLE_MISSING_RESPONSE
105
+
106
+ return response[:value] == true
107
+ end
108
+
109
+ def count_toggle(toggle_name, enabled)
110
+ response_ptr = YggdrasilEngine.count_toggle(@engine, toggle_name, enabled)
111
+ YggdrasilEngine.free_response(response_ptr)
112
+ end
113
+
114
+ def count_variant(toggle_name, variant_name)
115
+ response_ptr = YggdrasilEngine.count_variant(@engine, toggle_name, variant_name)
116
+ YggdrasilEngine.free_response(response_ptr)
117
+ end
118
+
119
+ def get_metrics
120
+ metrics_ptr = YggdrasilEngine.get_metrics(@engine)
121
+ metrics = JSON.parse(metrics_ptr.read_string, symbolize_names: true)
122
+ YggdrasilEngine.free_response(metrics_ptr)
123
+ metrics[:value]
124
+ end
125
+
126
+ def register_custom_strategies(strategies)
127
+ @custom_strategy_handler.register_custom_strategies(strategies)
128
+ end
129
+ end
@@ -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,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
+ end
97
+ end
98
+
99
+ RSpec.describe 'Client Specification' do
100
+ let(:yggdrasil_engine) { YggdrasilEngine.new }
101
+
102
+ test_suites.each do |suite|
103
+ suite_path = File.join('../client-specification/specifications', suite)
104
+ suite_data = JSON.parse(File.read(suite_path), symbolize_names: true)
105
+
106
+ describe "Suite '#{suite}'" do
107
+ before(:each) do
108
+ yggdrasil_engine.take_state(suite_data[:state].to_json)
109
+ end
110
+
111
+ suite_data.fetch(:tests, []).each do |test|
112
+ describe "Test '#{test[:description]}'" do
113
+ let(:context) { test[:context] }
114
+ let(:toggle_name) { test[:toggleName] }
115
+ let(:expected_result) { test[:expectedResult] }
116
+
117
+ it 'returns correct result for `is_enabled?` method' do
118
+ result = yggdrasil_engine.enabled?(toggle_name, context) || false
119
+
120
+ expect(result).to eq(expected_result),
121
+ "Failed test '#{test['description']}': expected #{expected_result}, got #{result}"
122
+ end
123
+ end
124
+ end
125
+
126
+ suite_data.fetch(:variantTests, []).each do |test|
127
+ next unless test[:expectedResult]
128
+
129
+ describe "Variant Test '#{test[:description]}'" do
130
+ let(:context) { test[:context] }
131
+ let(:toggle_name) { test[:toggleName] }
132
+ let(:expected_result) { test_suite_variant(test[:expectedResult]) }
133
+
134
+ it 'returns correct result for `get_variant` method' do
135
+ result = yggdrasil_engine.get_variant(toggle_name, context) || {
136
+ :name => 'disabled',
137
+ :payload => nil,
138
+ :enabled => false,
139
+ :feature_enabled => false
140
+ }
141
+
142
+ expect(result[:name]).to eq(expected_result[:name])
143
+ expect(result[:payload]).to eq(expected_result[:payload])
144
+ expect(result[:enabled]).to eq(expected_result[:enabled])
145
+ expect(result[:feature_enabled]).to eq(expected_result[:feature_enabled])
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yggdrasil-engine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5.beta.19
5
+ platform: universal-java-21
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
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 1.15.5
19
+ name: ffi
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.15.5
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/libyggdrasilffi_arm64.dylib
36
+ - lib/libyggdrasilffi_x86_64.dll
37
+ - lib/libyggdrasilffi_x86_64.dylib
38
+ - lib/libyggdrasilffi_x86_64.so
39
+ - lib/yggdrasil_engine.rb
40
+ - spec/custom_strategy_spec.rb
41
+ - spec/yggdrasil_engine_spec.rb
42
+ homepage: http://github.com/username/my_gem
43
+ licenses:
44
+ - MIT
45
+ metadata: {}
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">"
58
+ - !ruby/object:Gem::Version
59
+ version: 1.3.1
60
+ requirements: []
61
+ rubygems_version: 3.3.26
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Unleash engine for evaluating feature toggles
65
+ test_files: []