yggdrasil-engine 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +36 -0
- data/lib/custom_strategy.rb +60 -0
- data/lib/libyggdrasilffi.so +0 -0
- data/lib/yggdrasil_engine.rb +114 -0
- data/spec/custom_strategy_spec.rb +136 -0
- data/spec/yggdrasil_engine_spec.rb +139 -0
- metadata +76 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 60e31c7c12dd84fafcc5c0fa02061cc577d6ca9bb6a061eba51b3290cfd0a000
|
4
|
+
data.tar.gz: 26dcc66f90912709b60b918c2522ca1257a9d8c6907a4895ce01a471c3dbaa7d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ca3263a27a8780d064a44ce49c127cbb13ad8c7d340d8267481d4c1d6b9a15f1b17dca25032e7b6888e210b3f14cdd89081bf0e409ca42aaddcbb71456b0d89f
|
7
|
+
data.tar.gz: 45540c794984350d0c6cf4a03baa436715f775a017b0dfc8f21c06a064f72262a98496dd528e08c745f7fd367c2737b3492fef37c844aaa7ade02671fc213b2c
|
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,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: yggdrasil-engine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Your Name
|
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
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fiddle
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.0.6
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.0.6
|
41
|
+
description: "..."
|
42
|
+
email: you@example.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- README.md
|
48
|
+
- lib/custom_strategy.rb
|
49
|
+
- lib/libyggdrasilffi.so
|
50
|
+
- lib/yggdrasil_engine.rb
|
51
|
+
- spec/custom_strategy_spec.rb
|
52
|
+
- spec/yggdrasil_engine_spec.rb
|
53
|
+
homepage: http://github.com/username/my_gem
|
54
|
+
licenses:
|
55
|
+
- MIT
|
56
|
+
metadata: {}
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubygems_version: 3.4.10
|
73
|
+
signing_key:
|
74
|
+
specification_version: 4
|
75
|
+
summary: Unleash engine for evaluating feature toggles
|
76
|
+
test_files: []
|