sweettooth 1.0.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/CONTRIBUTORS +1 -0
  4. data/Gemfile +2 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +32 -0
  7. data/Rakefile +15 -0
  8. data/VERSION +1 -0
  9. data/gemfiles/default-with-activesupport.gemfile +3 -0
  10. data/gemfiles/json.gemfile +4 -0
  11. data/gemfiles/yajl.gemfile +4 -0
  12. data/lib/sweettooth.rb +237 -0
  13. data/lib/sweettooth/account.rb +4 -0
  14. data/lib/sweettooth/activity.rb +10 -0
  15. data/lib/sweettooth/api_operations/create.rb +16 -0
  16. data/lib/sweettooth/api_operations/delete.rb +11 -0
  17. data/lib/sweettooth/api_operations/list.rb +16 -0
  18. data/lib/sweettooth/api_operations/update.rb +57 -0
  19. data/lib/sweettooth/api_resource.rb +38 -0
  20. data/lib/sweettooth/collection_object.rb +35 -0
  21. data/lib/sweettooth/customer.rb +8 -0
  22. data/lib/sweettooth/errors/api_connection_error.rb +4 -0
  23. data/lib/sweettooth/errors/api_error.rb +4 -0
  24. data/lib/sweettooth/errors/authentication_error.rb +4 -0
  25. data/lib/sweettooth/errors/invalid_request_error.rb +10 -0
  26. data/lib/sweettooth/errors/sweettooth_error.rb +20 -0
  27. data/lib/sweettooth/json.rb +21 -0
  28. data/lib/sweettooth/redemption.rb +5 -0
  29. data/lib/sweettooth/redemption_option.rb +5 -0
  30. data/lib/sweettooth/singleton_api_resource.rb +20 -0
  31. data/lib/sweettooth/sweettooth_object.rb +168 -0
  32. data/lib/sweettooth/util.rb +102 -0
  33. data/lib/sweettooth/version.rb +3 -0
  34. data/sweettooth.gemspec +26 -0
  35. data/test/sweettooth/activity_test.rb +13 -0
  36. data/test/sweettooth/customer_test.rb +36 -0
  37. data/test/sweettooth/redemption_option_test.rb +14 -0
  38. data/test/sweettooth/redemption_test.rb +13 -0
  39. data/test/test_helper.rb +168 -0
  40. metadata +192 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c25d2dad930f01702ec9f332a73cd7a25cf7e509
4
+ data.tar.gz: c8b2668e549834d0f9be3b4202ac77986b88bf08
5
+ SHA512:
6
+ metadata.gz: 603faf1752792593d0be4ff22d677d6bee812d69700f4baf062bd9f98404e39d5db6e37d4e31142e59d31042918749e69d7c6fe6d5f888955803a0281204db9e
7
+ data.tar.gz: eb20d0dea61cad5ba3e353fe7a191652f3eb87d4b28e07210ba199231507364d2ba2183070002e63de7dbd4e917af7d8e0a02bbc26b80bf6142f85f0aa649c96
@@ -0,0 +1,9 @@
1
+ # Ignore gem stuff
2
+ /.bundle
3
+ /sweettooth-*.gem
4
+ .rvmrc
5
+ Gemfile.lock
6
+
7
+ # Mac stuff
8
+ .DS_Store
9
+
@@ -0,0 +1 @@
1
+ Bill Curtis <wcurtis@sweettoothhq.com>
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Sweet Tooth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,32 @@
1
+ = Sweet Tooth Ruby bindings
2
+
3
+ == Installation
4
+
5
+ You can install the Sweet Tooth gem by running:
6
+
7
+ gem install sweettooth
8
+
9
+ == Basic Usage
10
+
11
+ require "sweettooth"
12
+ SweetTooth.api_key = "sk_gUjtToMzKybHZ3yGg3C4Sv4L"
13
+
14
+ SweetTooth::Customer.create(
15
+ :first_name => "Wayne",
16
+ :last_name => "Rooney",
17
+ :email => "wrooney@example.com"
18
+ )
19
+
20
+ == Requirements
21
+
22
+ * Ruby 1.8.7 or above. (Ruby 1.8.6 may work if you load
23
+ ActiveSupport.)
24
+ * rest-client, multi_json
25
+
26
+ == Tests
27
+
28
+ Test cases can be run with: `bundle exec rake test`
29
+
30
+ == Acknowledgements
31
+
32
+ Inspired by the Stripe Ruby bindings
@@ -0,0 +1,15 @@
1
+ task :default => [:all]
2
+
3
+ task :test do
4
+ ret = true
5
+ Dir["test/**/*_test.rb"].each do |f|
6
+ ret = ret && ruby(f, '')
7
+ end
8
+ end
9
+
10
+ task :all do
11
+ Rake::Task["test"].invoke
12
+ require 'active_support/all'
13
+ Rake::Task["test"].reenable
14
+ Rake::Task["test"].invoke
15
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+ gemspec :path => File.join(File.dirname(__FILE__), "..")
3
+ gem "activesupport", "~> 3.0"
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+ gemspec :path => File.join(File.dirname(__FILE__), "..")
3
+ gem "json"
4
+ gem "activesupport", "~> 3.0"
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+ gemspec :path => File.join(File.dirname(__FILE__), "..")
3
+ gem "yajl-ruby"
4
+ gem "activesupport", "~> 3.0"
@@ -0,0 +1,237 @@
1
+ # Sweet Tooth Ruby bindings
2
+ # API spec at https://www.sweettoothrewards.com/api/docs
3
+ require 'cgi'
4
+ require 'set'
5
+ require 'openssl'
6
+ require 'rest_client'
7
+ require 'multi_json'
8
+ require 'base64'
9
+
10
+ # Version
11
+ require 'sweettooth/version'
12
+
13
+ # API operations
14
+ require 'sweettooth/api_operations/create'
15
+ require 'sweettooth/api_operations/update'
16
+ require 'sweettooth/api_operations/delete'
17
+ require 'sweettooth/api_operations/list'
18
+
19
+ # Resources
20
+ require 'sweettooth/util'
21
+ require 'sweettooth/json'
22
+ require 'sweettooth/sweettooth_object'
23
+ require 'sweettooth/api_resource'
24
+ require 'sweettooth/singleton_api_resource'
25
+ require 'sweettooth/collection_object'
26
+ require 'sweettooth/activity'
27
+ require 'sweettooth/customer'
28
+ require 'sweettooth/redemption'
29
+ require 'sweettooth/redemption_option'
30
+
31
+ # Errors
32
+ require 'sweettooth/errors/sweettooth_error'
33
+ require 'sweettooth/errors/api_error'
34
+ require 'sweettooth/errors/api_connection_error'
35
+ require 'sweettooth/errors/invalid_request_error'
36
+ require 'sweettooth/errors/authentication_error'
37
+
38
+ module SweetTooth
39
+ @api_base = 'https://api.sweettooth.io'
40
+
41
+ @verify_ssl_certs = true
42
+
43
+ class << self
44
+ attr_accessor :api_key, :api_base, :verify_ssl_certs, :api_version
45
+ end
46
+
47
+ def self.api_url(url='')
48
+ @api_base + url
49
+ end
50
+
51
+ def self.request(method, url, api_key, params={}, headers={})
52
+ unless api_key ||= @api_key
53
+ raise AuthenticationError.new('No API key provided. ' +
54
+ 'Set your API key using "SweetTooth.api_key = <API-KEY>". ' +
55
+ 'You can generate API keys from the Sweet Tooth web interface. ' +
56
+ 'See https://www.sweettoothrewards.com/api for details, or email support@sweettoothhq.com ' +
57
+ 'if you have any questions.')
58
+ end
59
+
60
+ if api_key =~ /\s/
61
+ raise AuthenticationError.new('Your API key is invalid, as it contains ' +
62
+ 'whitespace. (HINT: You can double-check your API key from the ' +
63
+ 'Sweet Tooth web interface. See https://www.sweettoothrewards.com/api for details, or ' +
64
+ 'email support@sweettoothhq.com if you have any questions.)')
65
+ end
66
+
67
+ request_opts = { :verify_ssl => false }
68
+
69
+ params = Util.objects_to_ids(params)
70
+ url = api_url(url)
71
+
72
+ case method.to_s.downcase.to_sym
73
+ when :get, :head, :delete
74
+ # Make params into GET parameters
75
+ url += "#{URI.parse(url).query ? '&' : '?'}#{uri_encode(params)}" if params && params.any?
76
+ payload = nil
77
+ else
78
+ payload = uri_encode(params)
79
+ end
80
+
81
+ request_opts.update(:headers => request_headers(api_key).update(headers),
82
+ :method => method, :open_timeout => 30,
83
+ :payload => payload, :url => url, :timeout => 80)
84
+
85
+ begin
86
+ response = execute_request(request_opts)
87
+ rescue SocketError => e
88
+ handle_restclient_error(e)
89
+ rescue NoMethodError => e
90
+ # Work around RestClient bug
91
+ if e.message =~ /\WRequestFailed\W/
92
+ e = APIConnectionError.new('Unexpected HTTP response code')
93
+ handle_restclient_error(e)
94
+ else
95
+ raise
96
+ end
97
+ rescue RestClient::ExceptionWithResponse => e
98
+ if rcode = e.http_code and rbody = e.http_body
99
+ handle_api_error(rcode, rbody)
100
+ else
101
+ handle_restclient_error(e)
102
+ end
103
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
104
+ handle_restclient_error(e)
105
+ end
106
+
107
+ [parse(response), api_key]
108
+ end
109
+
110
+ private
111
+
112
+ def self.user_agent
113
+ @uname ||= get_uname
114
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
115
+
116
+ {
117
+ :bindings_version => SweetTooth::VERSION,
118
+ :lang => 'ruby',
119
+ :lang_version => lang_version,
120
+ :platform => RUBY_PLATFORM,
121
+ :publisher => 'sweettooth',
122
+ :uname => @uname
123
+ }
124
+
125
+ end
126
+
127
+ def self.get_uname
128
+ `uname -a 2>/dev/null`.strip if RUBY_PLATFORM =~ /linux|darwin/i
129
+ rescue Errno::ENOMEM => ex # couldn't create subprocess
130
+ "uname lookup failed"
131
+ end
132
+
133
+ def self.uri_encode(params)
134
+ Util.flatten_params(params).
135
+ map { |k,v| "#{k}=#{Util.url_encode(v)}" }.join('&')
136
+ end
137
+
138
+ def self.request_headers(api_key)
139
+ headers = {
140
+ :user_agent => "SweetTooth/v1 RubyBindings/#{SweetTooth::VERSION}",
141
+ :authorization => "Basic " + Base64.encode64(api_key + ':'),
142
+ :content_type => 'application/x-www-form-urlencoded'
143
+ }
144
+
145
+ headers[:sweettooth_version] = api_version if api_version
146
+
147
+ begin
148
+ headers.update(:x_sweettooth_client_user_agent => SweetTooth::JSON.dump(user_agent))
149
+ rescue => e
150
+ headers.update(:x_sweettooth_client_raw_user_agent => user_agent.inspect,
151
+ :error => "#{e} (#{e.class})")
152
+ end
153
+ end
154
+
155
+ def self.execute_request(opts)
156
+ RestClient::Request.execute(opts)
157
+ end
158
+
159
+ def self.parse(response)
160
+ begin
161
+ # Would use :symbolize_names => true, but apparently there is
162
+ # some library out there that makes symbolize_names not work.
163
+ response = SweetTooth::JSON.load(response.body)
164
+ rescue MultiJson::DecodeError
165
+ raise general_api_error(response.code, response.body)
166
+ end
167
+
168
+ Util.symbolize_names(response)
169
+ end
170
+
171
+ def self.general_api_error(rcode, rbody)
172
+ APIError.new("Invalid response object from API: #{rbody.inspect} " +
173
+ "(HTTP response code was #{rcode})", rcode, rbody)
174
+ end
175
+
176
+ def self.handle_api_error(rcode, rbody)
177
+ begin
178
+ error_obj = SweetTooth::JSON.load(rbody)
179
+ error_obj = Util.symbolize_names(error_obj)
180
+ error = error_obj or raise SweetToothError.new # escape from parsing
181
+
182
+ rescue MultiJson::DecodeError, SweetToothError
183
+ raise general_api_error(rcode, rbody)
184
+ end
185
+
186
+ case rcode
187
+ when 400, 404
188
+ raise invalid_request_error error, rcode, rbody, error_obj
189
+ when 401
190
+ raise authentication_error error, rcode, rbody, error_obj
191
+ else
192
+ raise api_error error, rcode, rbody, error_obj
193
+ end
194
+
195
+ end
196
+
197
+ def self.invalid_request_error(error, rcode, rbody, error_obj)
198
+ InvalidRequestError.new(error[:message], error[:param], rcode,
199
+ rbody, error_obj)
200
+ end
201
+
202
+ def self.authentication_error(error, rcode, rbody, error_obj)
203
+ AuthenticationError.new(error[:message], rcode, rbody, error_obj)
204
+ end
205
+
206
+ def self.api_error(error, rcode, rbody, error_obj)
207
+ APIError.new(error[:message], rcode, rbody, error_obj)
208
+ end
209
+
210
+ def self.handle_restclient_error(e)
211
+ case e
212
+ when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
213
+ message = "Could not connect to Sweet Tooth (#{@api_base}). " +
214
+ "Please check your internet connection and try again. " +
215
+ "If this problem persists, you should check Sweet Tooth's service status at " +
216
+ "https://twitter.com/sweettoothstatus, or let us know at support@sweettoothhq.com."
217
+
218
+ when RestClient::SSLCertificateNotVerified
219
+ message = "Could not verify Sweet Tooth's SSL certificate. " +
220
+ "Please make sure that your network is not intercepting certificates. " +
221
+ "(Try going to https://api.sweettooth.io/v1 in your browser.) " +
222
+ "If this problem persists, let us know at support@sweettoothhq.com."
223
+
224
+ when SocketError
225
+ message = "Unexpected error communicating when trying to connect to Sweet Tooth. " +
226
+ "You may be seeing this message because your DNS is not working. " +
227
+ "To check, try running 'host sweettooth.io' from the command line."
228
+
229
+ else
230
+ message = "Unexpected error communicating with Sweet Tooth. " +
231
+ "If this problem persists, let us know at support@sweettoothhq.com."
232
+
233
+ end
234
+
235
+ raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
236
+ end
237
+ end
@@ -0,0 +1,4 @@
1
+ module SweetTooth
2
+ class Account < SingletonAPIResource
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module SweetTooth
2
+ class Activity < APIResource
3
+
4
+ def self.class_name_plural
5
+ 'Activities'
6
+ end
7
+
8
+ include SweetTooth::APIOperations::Create
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ module SweetTooth
2
+ module APIOperations
3
+ module Create
4
+ module ClassMethods
5
+ def create(params={}, api_key=nil)
6
+ response, api_key = SweetTooth.request(:post, self.url, api_key, params)
7
+ Util.convert_to_sweettooth_object(response, api_key)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module SweetTooth
2
+ module APIOperations
3
+ module Delete
4
+ def delete
5
+ response, api_key = SweetTooth.request(:delete, url, @api_key)
6
+ refresh_from(response, api_key)
7
+ self
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module SweetTooth
2
+ module APIOperations
3
+ module List
4
+ module ClassMethods
5
+ def all(filters={}, api_key=nil)
6
+ response, api_key = SweetTooth.request(:get, url, api_key, filters)
7
+ Util.convert_to_sweettooth_object(response, api_key)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ module SweetTooth
2
+ module APIOperations
3
+ module Update
4
+ def save
5
+ values = serialize_params(self)
6
+
7
+ if @values[:metadata]
8
+ values[:metadata] = serialize_metadata
9
+ end
10
+
11
+ if values.length > 0
12
+ values.delete(:id)
13
+
14
+ response, api_key = SweetTooth.request(:post, url, @api_key, values)
15
+ refresh_from(response, api_key)
16
+ end
17
+ self
18
+ end
19
+
20
+ def serialize_metadata
21
+ if @unsaved_values.include?(:metadata)
22
+ # the metadata object has been reassigned
23
+ # i.e. as object.metadata = {key => val}
24
+ metadata_update = @values[:metadata] # new hash
25
+ new_keys = metadata_update.keys.map(&:to_sym)
26
+ # remove keys at the server, but not known locally
27
+ keys_to_unset = @previous_metadata.keys - new_keys
28
+ keys_to_unset.each {|key| metadata_update[key] = ''}
29
+
30
+ metadata_update
31
+ else
32
+ # metadata is a SweetToothObject, and can be serialized normally
33
+ serialize_params(@values[:metadata])
34
+ end
35
+ end
36
+
37
+ def serialize_params(obj)
38
+ case obj
39
+ when nil
40
+ ''
41
+ when SweetToothObject
42
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
43
+ obj_values = obj.instance_variable_get(:@values)
44
+ update_hash = {}
45
+
46
+ unsaved_keys.each do |k|
47
+ update_hash[k] = serialize_params(obj_values[k])
48
+ end
49
+
50
+ update_hash
51
+ else
52
+ obj
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end