yggdrasil-engine 0.0.8-arm64-mingw32

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: 952cdcb395b71cd9152c6b796dbb3cffbc63282e1c25f513d3e91dba39b46f55
4
+ data.tar.gz: 7b7789f0cf93745813515b140f5b4bffef2d42e28d15e9f34372a433a90dd502
5
+ SHA512:
6
+ metadata.gz: b63d335565df1ce3de75fd6b98b29296ed1601590bf71b8b2dd80c246c05091f3f40dbe58289a7e7fe8631ee9d2cd5c466ea68e520874dcc3af20768bb188e04
7
+ data.tar.gz: '09e3f250db0f7881e65b0f28e996f5e913fc65cb8fddfee3ba86694bf9747e3b9f28d53b901b307f02a33934be045080f60f120998ee6ef5bcf43a93c39ef8e7'
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: 0.0.8
5
+ platform: arm64-mingw32
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_arm64.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.0
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: []