yggdrasil-engine 0.0.6.beta.3-arm-linux

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: 236bbc4b5f8f5b12d4c2eea078fa9929c9ad45e4dddeac486a7f1470cdb53018
4
+ data.tar.gz: '0348b3151b2db553f94625c14119b90299ad3ddd32dced847a4a89ecfceefe8a'
5
+ SHA512:
6
+ metadata.gz: 7149fed57beb17ebaf6bcd3422135fea34b2c14113cbdcae5d92d5b1e56c0a34a86956c447ae99579507ab9bc119f022c977fe072bf6e6b66eb7c0f1abdfa0fe
7
+ data.tar.gz: 3d7a745e7bd1eda4b9b409b2fafd030894ab9aa55872adcd3eb6c5f842fea3b3bde90a6a201733bace2d931f6745e4fc18103f77bb7c5cbad26569f295af383e
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
@@ -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,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yggdrasil-engine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6.beta.3
5
+ platform: arm-linux
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.15.5
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.so
36
+ - lib/yggdrasil_engine.rb
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
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">"
55
+ - !ruby/object:Gem::Version
56
+ version: 1.3.1
57
+ requirements: []
58
+ rubygems_version: 3.3.5
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Unleash engine for evaluating feature toggles
62
+ test_files: []