Skip to content

Commit 5624909

Browse files
committed
Merge branch 'bmoelk-faraday-and-idiomatic-ruby'
2 parents 51e5db9 + 0c5aeea commit 5624909

File tree

9 files changed

+403
-97
lines changed

9 files changed

+403
-97
lines changed

.vscode/launch.json

Lines changed: 0 additions & 15 deletions
This file was deleted.

Rakefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
require "bundler/gem_tasks"
2+
require "rspec/core/rake_task"
3+
4+
RSpec::Core::RakeTask.new(:spec)
5+
6+
task :default => :spec

bullet-train-ruby.gemspec

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@ Gem::Specification.new do |spec|
88
spec.summary = "Bullet Train - Ship features with confidence"
99
spec.description = "Ruby Client for Bullet-Train. Ship features with confidence using feature flags and remote config. Host yourself or use our hosted version at https://bullet-train.io"
1010
spec.homepage = "https://bullet-train.io"
11+
12+
spec.add_development_dependency 'bundler'
13+
spec.add_development_dependency 'rake'
14+
spec.add_development_dependency 'rspec'
15+
spec.add_dependency 'faraday'
1116
end

lib/bullet-train.rb

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,3 @@
1-
# require "bullet/train/ruby/version"
2-
require "open-uri"
3-
require "json"
1+
# frozen_string_literal: true
42

5-
class BulletTrainClient
6-
@@apiUrl = ""
7-
@@environmentKey = ""
8-
9-
def initialize(apiKey = nil, apiUrl = "https://api.bullet-train.io/api/v1/")
10-
@@environmentKey = apiKey
11-
@@apiUrl = apiUrl
12-
end
13-
14-
def getJSON(method = nil)
15-
response = open(@@apiUrl + "" + method.concat("?format=json"),
16-
"x-environment-key" => @@environmentKey).read
17-
return JSON.parse(response)
18-
end
19-
20-
def processFlags(inputFlags)
21-
flags = {}
22-
23-
for feature in inputFlags
24-
featureName = feature["feature"]["name"].downcase.gsub(/\s+/, "_")
25-
enabled = feature["enabled"]
26-
state = feature["feature_state_value"]
27-
flags[featureName] = {"enabled" => enabled, "value" => state}
28-
end
29-
30-
return flags
31-
end
32-
33-
def getFlagsForUser(identity = nil)
34-
processFlags(getJSON("flags/#{identity}"))
35-
end
36-
37-
def getFlags()
38-
processFlags(getJSON("flags/"))
39-
end
40-
41-
def getValue(key, userId = nil)
42-
flags = nil
43-
# Get the features
44-
if userId != nil
45-
flags = getFlagsForUser(userId)
46-
else
47-
flags = getFlags()
48-
end
49-
# Return the value
50-
return flags[key]["value"]
51-
end
52-
53-
def hasFeature(key, userId = nil)
54-
# Get the features
55-
flags = nil
56-
if userId != nil
57-
flags = getFlagsForUser(userId)
58-
else
59-
flags = getFlags()
60-
end
61-
62-
# Work out if this feature exists
63-
if flags[key] == nil
64-
return false
65-
else
66-
return flags[key]["enabled"]
67-
end
68-
end
69-
end
3+
require 'bullet_train'

lib/bullet_train.rb

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# frozen_string_literal: true
2+
require 'faraday'
3+
4+
class BulletTrain
5+
attr_reader :bt_api
6+
7+
def initialize(opts = {})
8+
@opts = {
9+
api_key: opts[:api_key] || self.class.api_key,
10+
url: opts[:url] || self.class.api_url
11+
}
12+
13+
@bt_api = Faraday.new(url: @opts[:url]) do |faraday|
14+
faraday.headers['Accept'] = 'application/json'
15+
faraday.headers['Content-Type'] = 'application/json'
16+
faraday.headers['x-environment-key'] = @opts[:api_key]
17+
faraday.response :json
18+
# TODO: add timeout adjustment here
19+
faraday.adapter Faraday.default_adapter
20+
end
21+
end
22+
23+
def get_flags(user_id = nil)
24+
if user_id.nil?
25+
res = @bt_api.get('flags/')
26+
flags = transform_flags(res.body).select { |flag| flag[:segment].nil? }
27+
flags_to_hash(flags)
28+
else
29+
res = @bt_api.get("identities/?identifier=#{user_id}")
30+
flags_to_hash(transform_flags(res.body['flags']))
31+
end
32+
end
33+
34+
def feature_enabled?(feature, user_id = nil, default = false)
35+
flag = get_flags(user_id)[normalize_key(feature)]
36+
return default if flag.nil?
37+
38+
flag[:enabled]
39+
end
40+
41+
def get_value(key, user_id = nil, default = nil)
42+
flag = get_flags(user_id)[normalize_key(key)]
43+
return default if flag.nil?
44+
45+
flag[:value]
46+
end
47+
48+
def set_trait(user_id, trait, value)
49+
raise StandardError, 'user_id cannot be nil' if user_id.nil?
50+
51+
res = @bt_api.post('traits/', { identity: { identifier: user_id }, trait_key: normalize_key(trait), trait_value: value }.to_json)
52+
res.body
53+
end
54+
55+
def get_traits(user_id)
56+
return {} if user_id.nil?
57+
58+
res = @bt_api.get("identities/?identifier=#{user_id}")
59+
traits_to_hash(res.body)
60+
end
61+
62+
# def remove_trait(user_id, trait_id)
63+
# # Request URL: https://api.bullet-train.io/api/v1/environments/API_KEY/identities/12345/traits/54321/
64+
# # Request Method: DELETE
65+
# end
66+
67+
def transform_flags(flags)
68+
flags.map do |flag|
69+
{
70+
name: flag['feature']['name'],
71+
enabled: flag['enabled'],
72+
value: flag['feature_state_value'],
73+
segment: flag['feature_segment']
74+
}
75+
end
76+
end
77+
78+
def flags_to_hash(flags)
79+
result = {}
80+
flags.each do |flag|
81+
key = normalize_key(flag.delete(:name))
82+
result[key] = flag
83+
end
84+
result
85+
end
86+
87+
def traits_to_hash(user_flags)
88+
result = {}
89+
user_flags['traits']&.each { |t| result[normalize_key(t['trait_key'])] = t['trait_value'] }
90+
result
91+
end
92+
93+
def normalize_key(key)
94+
key.to_s.downcase
95+
end
96+
97+
alias :hasFeature :feature_enabled?
98+
alias :getValue :get_value
99+
alias :getFlags :get_flags
100+
alias :getFlagsForUser :get_flags
101+
102+
def self.api_key
103+
ENV['BULLETTRAIN_API_KEY']
104+
end
105+
106+
def self.api_url
107+
ENV.fetch('BULLETTRAIN_URL') { 'https://api.bullet-train.io/api/v1/' }
108+
end
109+
end

spec/bullet_train_spec.rb

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../lib/bullet_train'
4+
require 'ostruct'
5+
require 'json'
6+
7+
describe BulletTrain do
8+
let(:mock_faraday) { double(Faraday) }
9+
let(:mock_api_key) { 'ASDFIEVNQWEPARJ' }
10+
let(:mock_api_url) { 'http://mock.bullet-train.io/api/' }
11+
let(:user_id) { '[email protected]' }
12+
let(:api_flags_response) { File.read('spec/fixtures/GET_flags.json') }
13+
let(:api_identities_response) { File.read('spec/fixtures/GET_identities_user.json') }
14+
let(:flags_response) { OpenStruct.new(body: JSON.parse(api_flags_response)) }
15+
let(:identities_response) { OpenStruct.new(body: JSON.parse(api_identities_response)) }
16+
17+
before do
18+
allow(Faraday).to receive(:new).with(url: mock_api_url).and_return(mock_faraday)
19+
allow(mock_faraday).to receive(:get).with('flags/').and_return(flags_response)
20+
allow(mock_faraday).to receive(:get).with("identities/?identifier=#{user_id}").and_return(identities_response)
21+
end
22+
subject { BulletTrain.new(api_key: mock_api_key, url: mock_api_url) }
23+
24+
describe '#get_flags' do
25+
it 'should return all flags without a segment' do
26+
expect(flags_response.body.length).to eq(4)
27+
flags = subject.get_flags(nil)
28+
expect(flags.length).to eq(3)
29+
expect(flags['feature_three'][:enabled]).to eq(false)
30+
end
31+
32+
context 'with user_id' do
33+
it 'should return all flags adjusted for segments' do
34+
flags = subject.get_flags(user_id)
35+
expect(flags.length).to eq(3)
36+
expect(flags['feature_three'][:enabled]).to eq(true)
37+
end
38+
end
39+
end
40+
41+
describe '#feature_enabled?' do
42+
it 'checks a specific feature' do
43+
expect(subject.feature_enabled?(:feature_one)).to eq(false)
44+
expect(subject.feature_enabled?('feature_two')).to eq(true)
45+
expect(subject.feature_enabled?('Feature_THREE')).to eq(false)
46+
end
47+
48+
context 'with user_id' do
49+
it 'checks a specific feature for a given user' do
50+
expect(subject.feature_enabled?(:feature_one, user_id)).to eq(false)
51+
expect(subject.feature_enabled?('feature_two', user_id)).to eq(true)
52+
expect(subject.feature_enabled?(:feature_three, user_id)).to eq(true)
53+
end
54+
end
55+
end
56+
57+
describe '#get_value' do
58+
it 'returns a value from a key' do
59+
expect(subject.get_value(:feature_one)).to be_nil
60+
expect(subject.get_value(:feature_two)).to eq(42)
61+
expect(subject.get_value(:feature_three)).to be_nil
62+
end
63+
64+
context 'with user_id' do
65+
it 'returns a value for that user' do
66+
expect(subject.get_value(:feature_one, user_id)).to be_nil
67+
expect(subject.get_value(:feature_two, user_id)).to eq(42)
68+
expect(subject.get_value(:feature_three, user_id)).to eq(7)
69+
end
70+
end
71+
end
72+
73+
describe '#set_trait' do
74+
let(:trait_key) { 'foo' }
75+
let(:trait_value) { 'bar' }
76+
let(:post_body) do
77+
{
78+
identity: { identifier: user_id },
79+
trait_key: subject.normalize_key(trait_key),
80+
trait_value: trait_value
81+
}.to_json
82+
end
83+
84+
it 'sets a trait for a given user' do
85+
trait_response = OpenStruct.new(body: {})
86+
expect(mock_faraday).to receive(:post).with('traits/', post_body).and_return(trait_response)
87+
subject.set_trait user_id, trait_key, trait_value
88+
end
89+
90+
it 'errors if user_id.nil?' do
91+
expect { subject.set_trait nil, trait_key, trait_value }.to raise_error(StandardError)
92+
end
93+
end
94+
95+
describe '#get_traits' do
96+
it 'returns hash of traits for a given user' do
97+
traits = subject.get_traits(user_id)
98+
expect(traits['roles']).to eq(%w[admin staff].to_json)
99+
expect(traits.length).to eq(2)
100+
end
101+
it 'returns {} for user_id.nil?' do
102+
expect(subject.get_traits(nil)).to eq({})
103+
end
104+
end
105+
106+
describe '#normalize_key' do
107+
it 'returns an empty string given nil' do
108+
expect(subject.normalize_key(nil)).to eq('')
109+
end
110+
111+
it 'returns lower case string given a symbol' do
112+
expect(subject.normalize_key(:key_value)).to eq('key_value')
113+
end
114+
115+
it 'returns lower case string given a mixed case string' do
116+
expect(subject.normalize_key('KEY_VaLuE')).to eq('key_value')
117+
end
118+
end
119+
120+
describe 'maintain backward compatibility with non-idiomatic ruby' do
121+
it 'aliases hasFeature' do
122+
expect(subject.method(:hasFeature)).to eq(subject.method(:feature_enabled?))
123+
end
124+
125+
it 'aliases getValue' do
126+
expect(subject.method(:getValue)).to eq(subject.method(:get_value))
127+
end
128+
129+
it 'aliases getFlags'do
130+
expect(subject.method(:getFlags)).to eq(subject.method(:get_flags))
131+
end
132+
133+
it 'aliases getFlagsForUser'do
134+
expect(subject.method(:getFlagsForUser)).to eq(subject.method(:get_flags))
135+
end
136+
end
137+
end

0 commit comments

Comments
 (0)