fluidfeatures 0.3.0

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.
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.swp
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
6
+ *.rbc
7
+ *.sassc
8
+ .sass-cache
9
+ capybara-*.html
10
+ .rspec
11
+ /.bundle
12
+ /vendor/bundle
13
+ /log/*
14
+ /tmp/*
15
+ /db/*.sqlite3
16
+ /public/system/*
17
+ /coverage/
18
+ /spec/tmp/*
19
+ **.orig
20
+ rerun.txt
21
+ pickle-email-*.html
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in fluidfeatures.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ [![Build Status](https://secure.travis-ci.org/FluidFeatures/fluidfeatures-ruby.png)](http://travis-ci.org/FluidFeatures/fluidfeatures-ruby)
2
+
3
+ fluidfeatures-ruby
4
+ ===================
5
+
6
+ Ruby client for API of FluidFeatures.com
7
+
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "fluidfeatures/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "fluidfeatures"
7
+ s.version = FluidFeatures::VERSION
8
+ s.authors = ["Phil Whelan"]
9
+ s.email = ["phil@fluidfeatures.com"]
10
+ s.homepage = "https://github.com/FluidFeatures/fluidfeatures-ruby"
11
+ s.summary = %q{Ruby client for the FluidFeatures service.}
12
+ s.description = %q{Ruby client for the FluidFeatures service.}
13
+ s.rubyforge_project = s.name
14
+ s.files = `git ls-files`.split("\n")
15
+ s.require_paths = ["lib"]
16
+ s.add_dependency "persistent_http", "~>1.0.3"
17
+ end
@@ -0,0 +1,207 @@
1
+
2
+ require "logger"
3
+
4
+ if RUBY_VERSION < "1.9.2"
5
+ require "pre_ruby192/uri"
6
+ end
7
+
8
+ module FluidFeatures
9
+ class Client
10
+
11
+ attr_accessor :logger
12
+
13
+ def initialize(base_uri, app_id, secret, options={})
14
+
15
+ @logger = options[:logger] || ::Logger.new(STDERR)
16
+
17
+ @baseuri = base_uri
18
+ @app_id = app_id
19
+ @secret = secret
20
+
21
+ @http = PersistentHTTP.new(
22
+ :name => 'fluidfeatures',
23
+ :logger => @logger,
24
+ :pool_size => 10,
25
+ :warn_timeout => 0.25,
26
+ :force_retry => true,
27
+ :url => @baseuri
28
+ )
29
+
30
+ @unknown_features = {}
31
+ @last_fetch_duration = nil
32
+
33
+ end
34
+
35
+ #
36
+ # This can be used to control how much of your user-base sees a
37
+ # particular feature. It may be easier to use the dashboard provided
38
+ # at https://www.fluidfeatures.com/dashboard to manage this, or to
39
+ # set timers to automate the gradual rollout of your new features.
40
+ #
41
+ def feature_set_enabled_percent(feature_name, enabled_percent)
42
+ begin
43
+ uri = URI(@baseuri + "/app/" + @app_id.to_s + "/features/" + feature_name.to_s)
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}
57
+ end
58
+ rescue
59
+ logger.error{"[FF] Request to set feature enabled percent failed : " + uri.to_s}
60
+ raise
61
+ end
62
+ end
63
+
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
+ begin
72
+ uri = URI(@baseuri + "/app/" + @app_id.to_s + "/features")
73
+ request = Net::HTTP::Get.new uri.path
74
+ request["Accept"] = "application/json"
75
+ request['AUTHORIZATION'] = @secret
76
+ response = @http.request request
77
+ if response.is_a?(Net::HTTPSuccess)
78
+ features = JSON.parse(response.body)
79
+ end
80
+ rescue
81
+ logger.error{"[FF] Request failed when getting feature set from " + uri.to_s}
82
+ raise
83
+ end
84
+ if not features
85
+ logger.error{"[FF] Empty feature set returned from " + uri.to_s}
86
+ end
87
+ features
88
+ end
89
+
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
133
+ begin
134
+ uri = URI("#{@baseuri}/app/#{@app_id}/user/#{user[:id]}/features")
135
+ uri.query = URI.encode_www_form( attribute_ids )
136
+ url_path = uri.path
137
+ if uri.query
138
+ url_path += "?" + uri.query
139
+ end
140
+ request = Net::HTTP::Get.new url_path
141
+ request["Accept"] = "application/json"
142
+ request['AUTHORIZATION'] = @secret
143
+ response = @http.request request
144
+ if response.is_a?(Net::HTTPSuccess)
145
+ features = JSON.parse(response.body)
146
+ else
147
+ logger.error{"[FF] [#{response.code}] Failed to get user features : #{uri} : #{response.body}"}
148
+ end
149
+ rescue
150
+ logger.error{"[FF] Request to get user features failed : #{uri}"}
151
+ raise
152
+ 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
+ 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)
184
+ begin
185
+ (payload[:stats] ||= {})[:ff_latency] = @last_fetch_duration
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")
191
+ request = Net::HTTP::Post.new uri.path
192
+ request["Content-Type"] = "application/json"
193
+ request["Accept"] = "application/json"
194
+ request['AUTHORIZATION'] = @secret
195
+ request.body = JSON.dump(payload)
196
+ response = @http.request request
197
+ unless response.is_a?(Net::HTTPSuccess)
198
+ logger.error{"[FF] [" + response.code.to_s + "] Failed to log features hit : " + uri.to_s + " : " + response.body.to_s}
199
+ end
200
+ rescue Exception => e
201
+ logger.error{"[FF] Request to log user features hit failed : " + uri.to_s}
202
+ raise
203
+ end
204
+ end
205
+
206
+ end
207
+ end
@@ -0,0 +1,3 @@
1
+ module FluidFeatures
2
+ VERSION = '0.3.0'
3
+ end
@@ -0,0 +1,120 @@
1
+
2
+ # https://bitbucket.org/ged/ruby-axis/raw/ef212387adcbd567a39fa0d51eb6dc6051c416bf/lib/axis/monkeypatches.rb
3
+
4
+ # Backport of Ruby 1.9.2 URI methods to 1.8.7.
5
+ module URIFormEncoding
6
+
7
+ TBLENCWWWCOMP_ = {} # :nodoc:
8
+ TBLDECWWWCOMP_ = {} # :nodoc:
9
+
10
+
11
+ # Encode given +str+ to URL-encoded form data.
12
+ #
13
+ # This doesn't convert *, -, ., 0-9, A-Z, _, a-z,
14
+ # does convert SP to +, and convert others to %XX.
15
+ #
16
+ # This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
17
+ #
18
+ # See URI.decode_www_form_component, URI.encode_www_form
19
+ def encode_www_form_component( str )
20
+ if TBLENCWWWCOMP_.empty?
21
+ 256.times do |i|
22
+ TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
23
+ end
24
+ TBLENCWWWCOMP_[' '] = '+'
25
+ TBLENCWWWCOMP_.freeze
26
+ end
27
+ return str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_)
28
+ end
29
+
30
+ # Decode given +str+ of URL-encoded form data.
31
+ #
32
+ # This decodes + to SP.
33
+ #
34
+ # See URI.encode_www_form_component, URI.decode_www_form
35
+ def decode_www_form_component( str )
36
+ if TBLDECWWWCOMP_.empty?
37
+ 256.times do |i|
38
+ h, l = i>>4, i&15
39
+ TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
40
+ TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
41
+ TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
42
+ TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
43
+ end
44
+ TBLDECWWWCOMP_['+'] = ' '
45
+ TBLDECWWWCOMP_.freeze
46
+ end
47
+ raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%\h\h|[^%]+)*\z/ =~ str
48
+ return str.gsub( /\+|%\h\h/, TBLDECWWWCOMP_ )
49
+ end
50
+
51
+ # Generate URL-encoded form data from given +enum+.
52
+ #
53
+ # This generates application/x-www-form-urlencoded data defined in HTML5
54
+ # from given an Enumerable object.
55
+ #
56
+ # This internally uses URI.encode_www_form_component(str).
57
+ #
58
+ # This doesn't convert encodings of give items, so convert them before call
59
+ # this method if you want to send data as other than original encoding or
60
+ # mixed encoding data. (strings which is encoded in HTML5 ASCII incompatible
61
+ # encoding is converted to UTF-8)
62
+ #
63
+ # This doesn't treat files. When you send a file, use multipart/form-data.
64
+ #
65
+ # This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
66
+ #
67
+ # See URI.encode_www_form_component, URI.decode_www_form
68
+ def encode_www_form( enum )
69
+ str = nil
70
+ enum.each do |k,v|
71
+ if str
72
+ str << '&'
73
+ else
74
+ str = nil.to_s
75
+ end
76
+ str << encode_www_form_component(k)
77
+ str << '='
78
+ str << encode_www_form_component(v)
79
+ end
80
+ str
81
+ end
82
+
83
+ WFKV_ = '(?:%\h\h|[^%#=;&])' # :nodoc:
84
+
85
+ # Decode URL-encoded form data from given +str+.
86
+ #
87
+ # This decodes application/x-www-form-urlencoded data
88
+ # and returns array of key-value array.
89
+ # This internally uses URI.decode_www_form_component.
90
+ #
91
+ # This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
92
+ #
93
+ # ary = URI.decode_www_form("a=1&a=2&b=3")
94
+ # p ary #=> [['a', '1'], ['a', '2'], ['b', '3']]
95
+ # p ary.assoc('a').last #=> '1'
96
+ # p ary.assoc('b').last #=> '3'
97
+ # p ary.rassoc('a').last #=> '2'
98
+ # p Hash[ary] # => {"a"=>"2", "b"=>"3"}
99
+ #
100
+ # See URI.decode_www_form_component, URI.encode_www_form
101
+ def decode_www_form( str )
102
+ return [] if str.empty?
103
+ unless /\A#{WFKV_}*=#{WFKV_}*(?:[;&]#{WFKV_}*=#{WFKV_}*)*\z/o =~ str
104
+ raise ArgumentError, "invalid data of application/x-www-form-urlencoded (#{str})"
105
+ end
106
+ ary = []
107
+ $&.scan(/([^=;&]+)=([^;&]*)/) do
108
+ ary << [decode_www_form_component($1, enc), decode_www_form_component($2, enc)]
109
+ end
110
+ ary
111
+ end
112
+
113
+ end
114
+
115
+
116
+ unless URI.methods.include?( :encode_www_form )
117
+ URI.extend( URIFormEncoding )
118
+ end
119
+
120
+
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluidfeatures
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Phil Whelan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: persistent_http
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.3
30
+ description: Ruby client for the FluidFeatures service.
31
+ email:
32
+ - phil@fluidfeatures.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - Gemfile
39
+ - README.md
40
+ - fluidfeatures.gemspec
41
+ - lib/fluidfeatures/client.rb
42
+ - lib/fluidfeatures/version.rb
43
+ - lib/pre_ruby192/uri.rb
44
+ homepage: https://github.com/FluidFeatures/fluidfeatures-ruby
45
+ licenses: []
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubyforge_project: fluidfeatures
64
+ rubygems_version: 1.8.24
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: Ruby client for the FluidFeatures service.
68
+ test_files: []