yggdrasil-engine 0.0.5.beta.11-arm64-darwin

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: 70bd0ff7b0f301c7f41b878d26515939a67a6f608f803369f11722ab370a0651
4
+ data.tar.gz: e077b7f0064f714835ba2ea4e05254ff7906fe0f28f7d819c79661941701c476
5
+ SHA512:
6
+ metadata.gz: e7a948a9f26ac6ca804eef6582176ae70bd0e9b395afd36696377053160f0f8edde0c826c635f82cf98909b539103dade008f1cc5d9493b2247f3a384af07a96
7
+ data.tar.gz: a3b7d9e6de536830ec4f9c9692bccfb1be6cef4034c69e82c1b4f40cfb8c3c93770f6f5c6e20d5919019450df4ed35becd511a7b74aa985911fcc2bdcf0713fe
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,114 @@
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
+ case RbConfig::CONFIG['host_os']
11
+ when /darwin|mac os/
12
+ 'libyggdrasilffi.dylib'
13
+ when /linux/
14
+ 'libyggdrasilffi.so'
15
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
16
+ 'libyggdrasilffi.dll'
17
+ else
18
+ raise "unsupported platform #{RbConfig::CONFIG['host_os']}"
19
+ end
20
+ end
21
+
22
+ def to_variant(raw_variant)
23
+ payload = raw_variant[:payload] && raw_variant[:payload].transform_keys(&:to_s)
24
+ {
25
+ name: raw_variant[:name],
26
+ enabled: raw_variant[:enabled],
27
+ payload: payload,
28
+ }
29
+ end
30
+
31
+ class YggdrasilEngine
32
+ extend FFI::Library
33
+ ffi_lib File.expand_path(platform_specific_lib, __dir__)
34
+
35
+ attach_function :new_engine, [], :pointer
36
+ attach_function :free_engine, [:pointer], :void
37
+
38
+ attach_function :take_state, %i[pointer string], :pointer
39
+ attach_function :check_enabled, %i[pointer string string string], :pointer
40
+ attach_function :check_variant, %i[pointer string string string], :pointer
41
+ attach_function :get_metrics, [:pointer], :pointer
42
+ attach_function :free_response, [:pointer], :void
43
+
44
+ attach_function :count_toggle, %i[pointer string bool], :void
45
+ attach_function :count_variant, %i[pointer string string], :void
46
+
47
+ def initialize
48
+ @engine = YggdrasilEngine.new_engine
49
+ @custom_strategy_handler = CustomStrategyHandler.new
50
+ ObjectSpace.define_finalizer(self, self.class.finalize(@engine))
51
+ end
52
+
53
+ def self.finalize(engine)
54
+ proc { YggdrasilEngine.free_engine(engine) }
55
+ end
56
+
57
+ def take_state(toggles)
58
+ @custom_strategy_handler.update_strategies(toggles)
59
+ response_ptr = YggdrasilEngine.take_state(@engine, toggles)
60
+ take_toggles_response = JSON.parse(response_ptr.read_string, symbolize_names: true)
61
+ YggdrasilEngine.free_response(response_ptr)
62
+ end
63
+
64
+ def get_variant(name, context)
65
+ context_json = (context || {}).to_json
66
+ custom_strategy_results = @custom_strategy_handler.evaluate_custom_strategies(name, context).to_json
67
+
68
+ variant_def_json_ptr = YggdrasilEngine.check_variant(@engine, name, context_json, custom_strategy_results)
69
+ variant_def_json = variant_def_json_ptr.read_string
70
+ YggdrasilEngine.free_response(variant_def_json_ptr)
71
+ variant_response = JSON.parse(variant_def_json, symbolize_names: true)
72
+
73
+ return nil if variant_response[:status_code] == TOGGLE_MISSING_RESPONSE
74
+ variant = variant_response[:value]
75
+
76
+ return to_variant(variant) if variant_response[:status_code] == OK_RESPONSE
77
+ end
78
+
79
+ def enabled?(toggle_name, context)
80
+ context_json = (context || {}).to_json
81
+ custom_strategy_results = @custom_strategy_handler.evaluate_custom_strategies(toggle_name, context).to_json
82
+
83
+ response_ptr = YggdrasilEngine.check_enabled(@engine, toggle_name, context_json, custom_strategy_results)
84
+ response_json = response_ptr.read_string
85
+ YggdrasilEngine.free_response(response_ptr)
86
+ response = JSON.parse(response_json, symbolize_names: true)
87
+
88
+ raise "Error: #{response[:error_message]}" if response[:status_code] == ERROR_RESPONSE
89
+ return nil if response[:status_code] == TOGGLE_MISSING_RESPONSE
90
+
91
+ return response[:value] == true
92
+ end
93
+
94
+ def count_toggle(toggle_name, enabled)
95
+ response_ptr = YggdrasilEngine.count_toggle(@engine, toggle_name, enabled)
96
+ YggdrasilEngine.free_response(response_ptr)
97
+ end
98
+
99
+ def count_variant(toggle_name, variant_name)
100
+ response_ptr = YggdrasilEngine.count_variant(@engine, toggle_name, variant_name)
101
+ YggdrasilEngine.free_response(response_ptr)
102
+ end
103
+
104
+ def get_metrics
105
+ metrics_ptr = YggdrasilEngine.get_metrics(@engine)
106
+ metrics = JSON.parse(metrics_ptr.read_string, symbolize_names: true)
107
+ YggdrasilEngine.free_response(metrics_ptr)
108
+ metrics[:value]
109
+ end
110
+
111
+ def register_custom_strategies(strategies)
112
+ @custom_strategy_handler.register_custom_strategies(strategies)
113
+ end
114
+ 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,139 @@
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
+ RSpec.describe YggdrasilEngine do
9
+ let(:yggdrasil_engine) { YggdrasilEngine.new }
10
+
11
+ describe '#checking a toggle' do
12
+ it 'that does not exist should yield a not found' do
13
+ is_enabled = yggdrasil_engine.enabled?('missing-toggle', {})
14
+ expect(is_enabled).to be_nil
15
+ end
16
+ end
17
+
18
+ describe '#metrics' do
19
+ it 'should clear metrics when get_metrics is called' do
20
+ feature_name = 'Feature.A'
21
+
22
+ suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
23
+ suite_data = JSON.parse(File.read(suite_path))
24
+
25
+ yggdrasil_engine.take_state(suite_data['state'].to_json)
26
+
27
+ yggdrasil_engine.count_toggle(feature_name, true)
28
+ yggdrasil_engine.count_toggle(feature_name, false)
29
+
30
+ metrics = yggdrasil_engine.get_metrics() # This should clear the metrics buffer
31
+
32
+ metric = metrics[:toggles][feature_name.to_sym]
33
+ expect(metric[:yes]).to eq(1)
34
+ expect(metric[:no]).to eq(1)
35
+
36
+ metrics = yggdrasil_engine.get_metrics()
37
+ expect(metrics).to be_nil
38
+ end
39
+
40
+ it 'should increment toggle count when it exists' do
41
+ toggle_name = 'Feature.A'
42
+
43
+ suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
44
+ suite_data = JSON.parse(File.read(suite_path))
45
+
46
+ yggdrasil_engine.take_state(suite_data['state'].to_json)
47
+
48
+ yggdrasil_engine.count_toggle(toggle_name, true)
49
+ yggdrasil_engine.count_toggle(toggle_name, false)
50
+
51
+ metrics = yggdrasil_engine.get_metrics()
52
+ metric = metrics[:toggles][toggle_name.to_sym]
53
+
54
+ expect(metric[:yes]).to eq(1)
55
+ expect(metric[:no]).to eq(1)
56
+ end
57
+
58
+ it 'should increment toggle count when the toggle does not exist' do
59
+ toggle_name = 'Feature.X'
60
+
61
+ yggdrasil_engine.count_toggle(toggle_name, true)
62
+ yggdrasil_engine.count_toggle(toggle_name, false)
63
+
64
+ metrics = yggdrasil_engine.get_metrics()
65
+ metric = metrics[:toggles][toggle_name.to_sym]
66
+
67
+ expect(metric[:yes]).to eq(1)
68
+ expect(metric[:no]).to eq(1)
69
+ end
70
+
71
+ it 'should increment variant' do
72
+ toggle_name = 'Feature.Q'
73
+
74
+ suite_path = File.join('../client-specification/specifications', '01-simple-examples.json')
75
+ suite_data = JSON.parse(File.read(suite_path))
76
+
77
+ yggdrasil_engine.take_state(suite_data['state'].to_json)
78
+
79
+ yggdrasil_engine.count_variant(toggle_name, 'disabled')
80
+
81
+ metrics = yggdrasil_engine.get_metrics()
82
+ metric = metrics[:toggles][toggle_name.to_sym]
83
+
84
+ expect(metric[:variants][:disabled]).to eq(1)
85
+ end
86
+ end
87
+ end
88
+
89
+ RSpec.describe 'Client Specification' do
90
+ let(:yggdrasil_engine) { YggdrasilEngine.new }
91
+
92
+ test_suites.each do |suite|
93
+ suite_path = File.join('../client-specification/specifications', suite)
94
+ suite_data = JSON.parse(File.read(suite_path), symbolize_names: true)
95
+
96
+ describe "Suite '#{suite}'" do
97
+ before(:each) do
98
+ yggdrasil_engine.take_state(suite_data[:state].to_json)
99
+ end
100
+
101
+ suite_data.fetch(:tests, []).each do |test|
102
+ describe "Test '#{test[:description]}'" do
103
+ let(:context) { test[:context] }
104
+ let(:toggle_name) { test[:toggleName] }
105
+ let(:expected_result) { test[:expectedResult] }
106
+
107
+ it 'returns correct result for `is_enabled?` method' do
108
+ result = yggdrasil_engine.enabled?(toggle_name, context) || false
109
+
110
+ expect(result).to eq(expected_result),
111
+ "Failed test '#{test['description']}': expected #{expected_result}, got #{result}"
112
+ end
113
+ end
114
+ end
115
+
116
+ suite_data.fetch(:variantTests, []).each do |test|
117
+ next unless test[:expectedResult]
118
+
119
+ describe "Variant Test '#{test[:description]}'" do
120
+ let(:context) { test[:context] }
121
+ let(:toggle_name) { test[:toggleName] }
122
+ let(:expected_result) { to_variant(test[:expectedResult]) }
123
+
124
+ it 'returns correct result for `get_variant` method' do
125
+ result = yggdrasil_engine.get_variant(toggle_name, context) || {
126
+ :name => 'disabled',
127
+ :payload => nil,
128
+ :enabled => false
129
+ }
130
+
131
+ expect(result[:name]).to eq(expected_result[:name])
132
+ expect(result[:payload]).to eq(expected_result[:payload])
133
+ expect(result[:enabled]).to eq(expected_result[:enabled])
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ 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.5.beta.11
5
+ platform: arm64-darwin
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.dylib
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: '0'
57
+ requirements: []
58
+ rubygems_version: 3.5.18
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Unleash engine for evaluating feature toggles
62
+ test_files: []