fluidfeatures 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/fluidfeatures/app/feature.rb +44 -0
- data/lib/fluidfeatures/app/user.rb +180 -0
- data/lib/fluidfeatures/app.rb +53 -0
- data/lib/fluidfeatures/client.rb +47 -154
- data/lib/fluidfeatures/const.rb +4 -0
- data/lib/fluidfeatures/version.rb +1 -1
- data/lib/fluidfeatures.rb +15 -0
- metadata +6 -1
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
require "fluidfeatures/const"
|
3
|
+
|
4
|
+
module FluidFeatures
|
5
|
+
class AppFeatureVersion
|
6
|
+
|
7
|
+
attr_accessor :app, :feature_name, :version_name
|
8
|
+
|
9
|
+
DEFAULT_VERSION_NAME = "default"
|
10
|
+
|
11
|
+
def initialize(app, feature_name, version_name=DEFAULT_VERSION_NAME)
|
12
|
+
|
13
|
+
raise "app invalid : #{app}" unless app.is_a? FluidFeatures::App
|
14
|
+
raise "feature_name invalid : #{feature_name}" unless feature_name.is_a? String
|
15
|
+
version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME
|
16
|
+
raise "version_name invalid : #{version_name}" unless version_name.is_a? String
|
17
|
+
|
18
|
+
@app = client
|
19
|
+
@feature_name = feature_name
|
20
|
+
@version_name = version_name
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# This can be used to control how much of your user-base sees a
|
26
|
+
# particular feature. It may be easier to use the dashboard provided
|
27
|
+
# at https://www.fluidfeatures.com/dashboard to manage this.
|
28
|
+
#
|
29
|
+
def set_enabled_percent(percent)
|
30
|
+
|
31
|
+
unless percent.is_a? Numeric and percent >= 0.0 and percent <= 100.0
|
32
|
+
raise "percent invalid : #{percent}"
|
33
|
+
end
|
34
|
+
|
35
|
+
app.put("/feature/#{feature_name}/#{version_name}/enabled/percent", {
|
36
|
+
:enabled => {
|
37
|
+
:percent => enabled_percent
|
38
|
+
}
|
39
|
+
})
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,180 @@
|
|
1
|
+
|
2
|
+
require "fluidfeatures/const"
|
3
|
+
|
4
|
+
module FluidFeatures
|
5
|
+
class AppUser
|
6
|
+
|
7
|
+
attr_accessor :app, :unique_id, :display_name, :anonymous, :unique_attrs, :cohort_attrs
|
8
|
+
|
9
|
+
def initialize(app, user_id, display_name, is_anonymous, unique_attrs, cohort_attrs)
|
10
|
+
|
11
|
+
raise "app invalid : #{app}" unless app.is_a? FluidFeatures::App
|
12
|
+
|
13
|
+
@app = app
|
14
|
+
@unique_id = user_id
|
15
|
+
@display_name = display_name
|
16
|
+
@anonymous = is_anonymous
|
17
|
+
@unique_attrs = unique_attrs
|
18
|
+
@cohort_attrs = cohort_attrs
|
19
|
+
|
20
|
+
@features = nil
|
21
|
+
@features_hit = {}
|
22
|
+
@goals_hit = {}
|
23
|
+
@unknown_features = {}
|
24
|
+
|
25
|
+
if not unique_id or is_anonymous
|
26
|
+
|
27
|
+
# We're an anonymous user
|
28
|
+
@anonymous = true
|
29
|
+
|
30
|
+
# if we were not given a user[:id] for this anonymous user, then get
|
31
|
+
# it from an existing cookie or create a new one.
|
32
|
+
unless unique_id
|
33
|
+
# Create new unique id (for cookie). Use rand + micro-seconds of current time
|
34
|
+
@unique_id = "anon-" + Random.rand(9999999999).to_s + "-" + ((Time.now.to_f * 1000000).to_i % 1000000).to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Returns all the features enabled for a specific user.
|
42
|
+
# This will depend on the user's unique_id and how many
|
43
|
+
# users each feature is enabled for.
|
44
|
+
#
|
45
|
+
def load_features
|
46
|
+
|
47
|
+
# extract just attribute ids into simple hash
|
48
|
+
attribute_ids = {
|
49
|
+
:anonymous => anonymous
|
50
|
+
}
|
51
|
+
[unique_attrs, cohort_attrs].each do |attrs|
|
52
|
+
if attrs
|
53
|
+
attrs.each do |attr_key, attr|
|
54
|
+
if attr.is_a? Hash
|
55
|
+
if attr.has_key? :id
|
56
|
+
attribute_ids[attr_key] = attr[:id]
|
57
|
+
end
|
58
|
+
else
|
59
|
+
attribute_ids[attr_key] = attr
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# normalize attributes ids as strings
|
66
|
+
attribute_ids.each do |attr_key, attr_id|
|
67
|
+
if attr_id.is_a? FalseClass or attr_id.is_a? TrueClass
|
68
|
+
attribute_ids[attr_key] = attr_id.to_s.downcase
|
69
|
+
elsif not attr_id.is_a? String
|
70
|
+
attribute_ids[attr_key] = attr_id.to_s
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
app.get("/user/#{unique_id}/features", attribute_ids) || {}
|
75
|
+
end
|
76
|
+
|
77
|
+
def features
|
78
|
+
@features ||= load_features
|
79
|
+
end
|
80
|
+
|
81
|
+
def feature_enabled?(feature_name, version_name, default_enabled)
|
82
|
+
|
83
|
+
raise "feature_name invalid : #{feature_name}" unless feature_name.is_a? String
|
84
|
+
version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME
|
85
|
+
|
86
|
+
if features.has_key? feature_name
|
87
|
+
feature = features[feature_name]
|
88
|
+
if feature.is_a? Hash
|
89
|
+
if feature.has_key? version_name
|
90
|
+
enabled = feature[version_name]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if enabled === nil
|
96
|
+
enabled = default_enabled
|
97
|
+
|
98
|
+
# Tell FluidFeatures about this amazing new feature...
|
99
|
+
unknown_feature_hit(feature_name, version_name, default_enabled)
|
100
|
+
end
|
101
|
+
|
102
|
+
if enabled
|
103
|
+
@features_hit[feature_name] ||= {}
|
104
|
+
@features_hit[feature_name][version_name.to_s] = {}
|
105
|
+
end
|
106
|
+
|
107
|
+
enabled
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# This is called when we encounter a feature_name that
|
112
|
+
# FluidFeatures has no record of for your application.
|
113
|
+
# This will be reported back to the FluidFeatures service so
|
114
|
+
# that it can populate your dashboard with this feature.
|
115
|
+
# The parameter "default_enabled" is a boolean that says whether
|
116
|
+
# this feature should be enabled to all users or no users.
|
117
|
+
# Usually, this is "true" for existing features that you are
|
118
|
+
# planning to phase out and "false" for new feature that you
|
119
|
+
# intend to phase in.
|
120
|
+
#
|
121
|
+
def unknown_feature_hit(feature_name, version_name, default_enabled)
|
122
|
+
if not @unknown_features[feature_name]
|
123
|
+
@unknown_features[feature_name] = { :versions => {} }
|
124
|
+
end
|
125
|
+
@unknown_features[feature_name][:versions][version_name] = default_enabled
|
126
|
+
end
|
127
|
+
|
128
|
+
def goal_hit(goal_name, goal_version_name)
|
129
|
+
sleep 10
|
130
|
+
raise "goal_name invalid : #{goal_name}" unless goal_name.is_a? String
|
131
|
+
goal_version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME
|
132
|
+
raise "goal_version_name invalid : #{goal_version_name}" unless goal_version_name.is_a? String
|
133
|
+
@goals_hit[goal_name.to_s] ||= {}
|
134
|
+
@goals_hit[goal_name.to_s][goal_version_name.to_s] = {}
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# This reports back to FluidFeatures which features we
|
139
|
+
# encountered during this request, the request duration,
|
140
|
+
# and statistics on time spent talking to the FluidFeatures
|
141
|
+
# service. Any new features encountered will also be reported
|
142
|
+
# back with the default_enabled status (see unknown_feature_hit)
|
143
|
+
# so that FluidFeatures can auto-populate the dashboard.
|
144
|
+
#
|
145
|
+
def end_transaction(url, stats)
|
146
|
+
|
147
|
+
payload = {
|
148
|
+
:url => url,
|
149
|
+
:user => {
|
150
|
+
:id => unique_id
|
151
|
+
},
|
152
|
+
:hits => {
|
153
|
+
:feature => @features_hit,
|
154
|
+
:goal => @goals_hit
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
if stats
|
159
|
+
raise "stats invalid : #{stats}" unless stats.is_a? Hash
|
160
|
+
payload[:stats] = stats
|
161
|
+
end
|
162
|
+
|
163
|
+
payload_user = payload[:user] ||= {}
|
164
|
+
payload_user[:name] = display_name if display_name
|
165
|
+
payload_user[:anonymous] = anonymous if anonymous
|
166
|
+
payload_user[:unique] = unique_attrs if unique_attrs
|
167
|
+
payload_user[:cohorts] = cohort_attrs if cohort_attrs
|
168
|
+
|
169
|
+
(payload[:stats] ||= {})[:ff_latency] = app.client.last_fetch_duration
|
170
|
+
if @unknown_features.size
|
171
|
+
(payload[:features] ||= {})[:unknown] = @unknown_features
|
172
|
+
@unknown_features = {}
|
173
|
+
end
|
174
|
+
|
175
|
+
app.post("/user/#{unique_id}/features/hit", payload)
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
require "fluidfeatures/app/user"
|
3
|
+
require "fluidfeatures/app/feature"
|
4
|
+
|
5
|
+
module FluidFeatures
|
6
|
+
class App
|
7
|
+
|
8
|
+
attr_accessor :client, :app_id, :secret, :logger
|
9
|
+
|
10
|
+
def initialize(client, app_id, secret, logger)
|
11
|
+
|
12
|
+
raise "client invalid : #{client}" unless client.is_a? FluidFeatures::Client
|
13
|
+
raise "app_id invalid : #{app_id}" unless app_id.is_a? String
|
14
|
+
raise "secret invalid : #{secret}" unless secret.is_a? String
|
15
|
+
|
16
|
+
@client = client
|
17
|
+
@app_id = app_id
|
18
|
+
@secret = secret
|
19
|
+
@logger = logger
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(path, params=nil)
|
24
|
+
client.get("/app/#{app_id}#{path}", secret, params)
|
25
|
+
end
|
26
|
+
|
27
|
+
def put(path, payload)
|
28
|
+
client.put("/app/#{app_id}#{path}", secret, payload)
|
29
|
+
end
|
30
|
+
|
31
|
+
def post(path, payload)
|
32
|
+
client.post("/app/#{app_id}#{path}", secret, payload)
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Returns all the features that FluidFeatures knows about for
|
37
|
+
# your application. The enabled percentage (how much of your user-base)
|
38
|
+
# sees each feature is also provided.
|
39
|
+
#
|
40
|
+
def features
|
41
|
+
get("/features")
|
42
|
+
end
|
43
|
+
|
44
|
+
def user(user_id, display_name, is_anonymous, unique_attrs, cohort_attrs)
|
45
|
+
::FluidFeatures::AppUser.new(self, user_id, display_name, is_anonymous, unique_attrs, cohort_attrs)
|
46
|
+
end
|
47
|
+
|
48
|
+
def feature_version(feature_name, version_name)
|
49
|
+
::FluidFeatures::AppFeatureVersion.new(self, feature_name, version_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
data/lib/fluidfeatures/client.rb
CHANGED
@@ -1,204 +1,97 @@
|
|
1
1
|
|
2
|
-
require
|
2
|
+
require 'net/http'
|
3
|
+
require 'persistent_http'
|
4
|
+
require "pre_ruby192/uri" if RUBY_VERSION < "1.9.2"
|
3
5
|
|
4
|
-
|
5
|
-
require "pre_ruby192/uri"
|
6
|
-
end
|
6
|
+
require "fluidfeatures/app"
|
7
7
|
|
8
8
|
module FluidFeatures
|
9
9
|
class Client
|
10
10
|
|
11
|
-
attr_accessor :logger
|
12
|
-
|
13
|
-
def initialize(base_uri, app_id, secret, options={})
|
11
|
+
attr_accessor :base_uri, :logger, :last_fetch_duration
|
14
12
|
|
15
|
-
|
13
|
+
def initialize(base_uri, logger)
|
16
14
|
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@secret = secret
|
15
|
+
@logger = logger
|
16
|
+
@base_uri = base_uri
|
20
17
|
|
21
|
-
@http = PersistentHTTP.new(
|
18
|
+
@http = ::PersistentHTTP.new(
|
22
19
|
:name => 'fluidfeatures',
|
23
|
-
:logger =>
|
20
|
+
:logger => logger,
|
24
21
|
:pool_size => 10,
|
25
22
|
:warn_timeout => 0.25,
|
26
23
|
:force_retry => true,
|
27
|
-
:url =>
|
24
|
+
:url => base_uri
|
28
25
|
)
|
29
26
|
|
30
|
-
@unknown_features = {}
|
31
27
|
@last_fetch_duration = nil
|
32
28
|
|
33
29
|
end
|
34
30
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
request = Net::HTTP::Put.new uri.path
|
45
|
-
request["Content-Type"] = "application/json"
|
46
|
-
request["Accept"] = "application/json"
|
47
|
-
request['AUTHORIZATION'] = @secret
|
48
|
-
payload = {
|
49
|
-
:enabled => {
|
50
|
-
:percent => enabled_percent
|
51
|
-
}
|
52
|
-
}
|
53
|
-
request.body = JSON.dump(payload)
|
54
|
-
response = @http.request uri, request
|
55
|
-
if response.is_a?(Net::HTTPSuccess)
|
56
|
-
logger.error{"[FF] [" + response.code.to_s + "] Failed to set feature enabled percent : " + uri.to_s + " : " + response.body.to_s}
|
31
|
+
def get(path, auth_token, url_params=nil)
|
32
|
+
payload = nil
|
33
|
+
|
34
|
+
uri = URI(@base_uri + path)
|
35
|
+
url_path = uri.path
|
36
|
+
if url_params
|
37
|
+
uri.query = URI.encode_www_form( url_params )
|
38
|
+
if uri.query
|
39
|
+
url_path += "?" + uri.query
|
57
40
|
end
|
58
|
-
rescue
|
59
|
-
logger.error{"[FF] Request to set feature enabled percent failed : " + uri.to_s}
|
60
|
-
raise
|
61
41
|
end
|
62
|
-
end
|
63
42
|
|
64
|
-
#
|
65
|
-
# Returns all the features that FluidFeatures knows about for
|
66
|
-
# your application. The enabled percentage (how much of your user-base)
|
67
|
-
# sees each feature is also provided.
|
68
|
-
#
|
69
|
-
def get_feature_set
|
70
|
-
features = nil
|
71
43
|
begin
|
72
|
-
|
73
|
-
request = Net::HTTP::Get.new uri.path
|
44
|
+
request = Net::HTTP::Get.new url_path
|
74
45
|
request["Accept"] = "application/json"
|
75
|
-
request['AUTHORIZATION'] =
|
46
|
+
request['AUTHORIZATION'] = auth_token
|
47
|
+
fetch_start_time = Time.now
|
76
48
|
response = @http.request request
|
77
49
|
if response.is_a?(Net::HTTPSuccess)
|
78
|
-
|
50
|
+
payload = JSON.parse(response.body)
|
51
|
+
@last_fetch_duration = Time.now - fetch_start_time
|
79
52
|
end
|
80
53
|
rescue
|
81
|
-
logger.error{"[FF] Request failed when getting
|
54
|
+
logger.error{"[FF] Request failed when getting #{path}"}
|
82
55
|
raise
|
83
56
|
end
|
84
|
-
if not
|
85
|
-
logger.error{"[FF] Empty
|
57
|
+
if not payload
|
58
|
+
logger.error{"[FF] Empty response from #{path}"}
|
86
59
|
end
|
87
|
-
|
60
|
+
payload
|
88
61
|
end
|
89
62
|
|
90
|
-
|
91
|
-
# Returns all the features enabled for a specific user.
|
92
|
-
# This will depend on the user_id and how many users each
|
93
|
-
# feature is enabled for.
|
94
|
-
#
|
95
|
-
def get_user_features(user)
|
96
|
-
logger.debug{"[FF] get_user_features #{user}"}
|
97
|
-
if not user
|
98
|
-
raise "user object should be a Hash"
|
99
|
-
end
|
100
|
-
if not user[:id]
|
101
|
-
raise "user does not contain :id field"
|
102
|
-
end
|
103
|
-
|
104
|
-
# extract just attribute ids into simple hash
|
105
|
-
attribute_ids = {
|
106
|
-
:anonymous => !!user[:anonymous]
|
107
|
-
}
|
108
|
-
[:unique, :cohorts].each do |attr_type|
|
109
|
-
if user.has_key? attr_type
|
110
|
-
user[attr_type].each do |attr_key, attr|
|
111
|
-
if attr.is_a? Hash
|
112
|
-
if attr.has_key? :id
|
113
|
-
attribute_ids[attr_key] = attr[:id]
|
114
|
-
end
|
115
|
-
else
|
116
|
-
attribute_ids[attr_key] = attr
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# normalize attributes ids as strings
|
123
|
-
attribute_ids.each do |attr_key, attr_id|
|
124
|
-
if attr_id.is_a? FalseClass or attr_id.is_a? TrueClass
|
125
|
-
attribute_ids[attr_key] = attr_id.to_s.downcase
|
126
|
-
elsif not attr_id.is_a? String
|
127
|
-
attribute_ids[attr_key] = attr_id.to_s
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
features = {}
|
132
|
-
fetch_start_time = Time.now
|
63
|
+
def put(path, auth_token, payload)
|
133
64
|
begin
|
134
|
-
uri = URI(
|
135
|
-
|
136
|
-
|
137
|
-
if uri.query
|
138
|
-
url_path += "?" + uri.query
|
139
|
-
end
|
140
|
-
request = Net::HTTP::Get.new url_path
|
65
|
+
uri = URI(@base_uri + path)
|
66
|
+
request = Net::HTTP::Put.new uri.path
|
67
|
+
request["Content-Type"] = "application/json"
|
141
68
|
request["Accept"] = "application/json"
|
142
|
-
request['AUTHORIZATION'] =
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
logger.error{"[FF] [#{response.code}] Failed to get user features : #{uri} : #{response.body}"}
|
69
|
+
request['AUTHORIZATION'] = auth_token
|
70
|
+
request.body = JSON.dump(payload)
|
71
|
+
response = @http.request uri, request
|
72
|
+
unless response.is_a?(Net::HTTPSuccess)
|
73
|
+
logger.error{"[FF] Request unsuccessful when putting #{path}"}
|
148
74
|
end
|
149
|
-
rescue
|
150
|
-
logger.error{"[FF] Request
|
75
|
+
rescue Exception => err
|
76
|
+
logger.error{"[FF] Request failed putting #{path} : #{err.message}"}
|
151
77
|
raise
|
152
78
|
end
|
153
|
-
@last_fetch_duration = Time.now - fetch_start_time
|
154
|
-
features
|
155
|
-
end
|
156
|
-
|
157
|
-
#
|
158
|
-
# This is called when we encounter a feature_name that
|
159
|
-
# FluidFeatures has no record of for your application.
|
160
|
-
# This will be reported back to the FluidFeatures service so
|
161
|
-
# that it can populate your dashboard with this feature.
|
162
|
-
# The parameter "default_enabled" is a boolean that says whether
|
163
|
-
# this feature should be enabled to all users or no users.
|
164
|
-
# Usually, this is "true" for existing features that you are
|
165
|
-
# planning to phase out and "false" for new feature that you
|
166
|
-
# intend to phase in.
|
167
|
-
#
|
168
|
-
def unknown_feature_hit(feature_name, version_name, defaults)
|
169
|
-
if not @unknown_features[feature_name]
|
170
|
-
@unknown_features[feature_name] = { :versions => {} }
|
171
|
-
end
|
172
|
-
@unknown_features[feature_name][:versions][version_name] = defaults
|
173
79
|
end
|
174
|
-
|
175
|
-
|
176
|
-
# This reports back to FluidFeatures which features we
|
177
|
-
# encountered during this request, the request duration,
|
178
|
-
# and statistics on time spent talking to the FluidFeatures
|
179
|
-
# service. Any new features encountered will also be reported
|
180
|
-
# back with the default_enabled status (see unknown_feature_hit)
|
181
|
-
# so that FluidFeatures can auto-populate the dashboard.
|
182
|
-
#
|
183
|
-
def log_request(user_id, payload)
|
80
|
+
|
81
|
+
def post(path, auth_token, payload)
|
184
82
|
begin
|
185
|
-
(
|
186
|
-
if @unknown_features.size
|
187
|
-
(payload[:features] ||= {})[:unknown] = @unknown_features
|
188
|
-
@unknown_features = {}
|
189
|
-
end
|
190
|
-
uri = URI(@baseuri + "/app/#{@app_id}/user/#{user_id}/features/hit")
|
83
|
+
uri = URI(@base_uri + path)
|
191
84
|
request = Net::HTTP::Post.new uri.path
|
192
85
|
request["Content-Type"] = "application/json"
|
193
86
|
request["Accept"] = "application/json"
|
194
|
-
request['AUTHORIZATION'] =
|
87
|
+
request['AUTHORIZATION'] = auth_token
|
195
88
|
request.body = JSON.dump(payload)
|
196
89
|
response = @http.request request
|
197
90
|
unless response.is_a?(Net::HTTPSuccess)
|
198
|
-
logger.error{"[FF]
|
91
|
+
logger.error{"[FF] Request unsuccessful when posting #{path}"}
|
199
92
|
end
|
200
|
-
rescue Exception =>
|
201
|
-
logger.error{"[FF] Request
|
93
|
+
rescue Exception => err
|
94
|
+
logger.error{"[FF] Request failed posting #{path} : #{err.message}"}
|
202
95
|
raise
|
203
96
|
end
|
204
97
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
require "fluidfeatures/client"
|
5
|
+
require "fluidfeatures/app"
|
6
|
+
|
7
|
+
module FluidFeatures
|
8
|
+
|
9
|
+
def self.app(base_uri, app_id, secret, logger=nil)
|
10
|
+
logger ||= ::Logger.new(STDERR)
|
11
|
+
client = ::FluidFeatures::Client.new(base_uri, logger)
|
12
|
+
::FluidFeatures::App.new(client, app_id, secret, logger)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fluidfeatures
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -38,7 +38,12 @@ files:
|
|
38
38
|
- Gemfile
|
39
39
|
- README.md
|
40
40
|
- fluidfeatures.gemspec
|
41
|
+
- lib/fluidfeatures.rb
|
42
|
+
- lib/fluidfeatures/app.rb
|
43
|
+
- lib/fluidfeatures/app/feature.rb
|
44
|
+
- lib/fluidfeatures/app/user.rb
|
41
45
|
- lib/fluidfeatures/client.rb
|
46
|
+
- lib/fluidfeatures/const.rb
|
42
47
|
- lib/fluidfeatures/version.rb
|
43
48
|
- lib/pre_ruby192/uri.rb
|
44
49
|
homepage: https://github.com/FluidFeatures/fluidfeatures-ruby
|