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,38 @@
1
+ module SweetTooth
2
+ class APIResource < SweetToothObject
3
+ def self.class_name
4
+ self.name.split('::')[-1]
5
+ end
6
+
7
+ # Override for irregular plurals, eg. 'Activities'
8
+ def self.class_name_plural
9
+ "#{self.class_name}s"
10
+ end
11
+
12
+ def self.url()
13
+ if self == APIResource
14
+ raise NotImplementedError.new('APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)')
15
+ end
16
+ "/v1/#{CGI.escape(class_name_plural.downcase)}"
17
+ end
18
+
19
+ def url
20
+ unless id = self['id']
21
+ raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
22
+ end
23
+ "#{self.class.url}/#{CGI.escape(id)}"
24
+ end
25
+
26
+ def refresh
27
+ response, api_key = SweetTooth.request(:get, url, @api_key, @retrieve_options)
28
+ refresh_from(response, api_key)
29
+ self
30
+ end
31
+
32
+ def self.retrieve(id, api_key=nil)
33
+ instance = self.new(id, api_key)
34
+ instance.refresh
35
+ instance
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ module SweetTooth
2
+ class CollectionObject < SweetToothObject
3
+
4
+ def [](k)
5
+ case k
6
+ when String, Symbol
7
+ super
8
+ else
9
+ raise ArgumentError.new("You tried to access the #{k.inspect} index, but CollectionObject types only support String keys. (HINT: List calls return an object with a 'items' (which is the items array). You likely want to call #items[#{k.inspect}])")
10
+ end
11
+ end
12
+
13
+ def each(&blk)
14
+ self.items.each(&blk)
15
+ end
16
+
17
+ def retrieve(id, api_key=nil)
18
+ api_key ||= @api_key
19
+ response, api_key = SweetTooth.request(:get,"#{url}/#{CGI.escape(id)}", api_key)
20
+ Util.convert_to_sweettooth_object(response, api_key)
21
+ end
22
+
23
+ def create(params={}, api_key=nil)
24
+ api_key ||= @api_key
25
+ response, api_key = SweetTooth.request(:post, url, api_key, params)
26
+ Util.convert_to_sweettooth_object(response, api_key)
27
+ end
28
+
29
+ def all(params={}, api_key=nil)
30
+ api_key ||= @api_key
31
+ response, api_key = SweetTooth.request(:get, url, api_key, params)
32
+ Util.convert_to_sweettooth_object(response, api_key)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,8 @@
1
+ module SweetTooth
2
+ class Customer < APIResource
3
+ include SweetTooth::APIOperations::Create
4
+ include SweetTooth::APIOperations::Delete
5
+ include SweetTooth::APIOperations::Update
6
+ include SweetTooth::APIOperations::List
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ module SweetTooth
2
+ class APIConnectionError < SweetToothError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module SweetTooth
2
+ class APIError < SweetToothError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module SweetTooth
2
+ class AuthenticationError < SweetToothError
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module SweetTooth
2
+ class InvalidRequestError < SweetToothError
3
+ attr_accessor :param
4
+
5
+ def initialize(message, param, http_status=nil, http_body=nil, json_body=nil)
6
+ super(message, http_status, http_body, json_body)
7
+ @param = param
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module SweetTooth
2
+ class SweetToothError < StandardError
3
+ attr_reader :message
4
+ attr_reader :http_status
5
+ attr_reader :http_body
6
+ attr_reader :json_body
7
+
8
+ def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil)
9
+ @message = message
10
+ @http_status = http_status
11
+ @http_body = http_body
12
+ @json_body = json_body
13
+ end
14
+
15
+ def to_s
16
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
17
+ "#{status_string}#{@message}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ module SweetTooth
2
+ module JSON
3
+ if MultiJson.respond_to?(:dump)
4
+ def self.dump(*args)
5
+ MultiJson.dump(*args)
6
+ end
7
+
8
+ def self.load(*args)
9
+ MultiJson.load(*args)
10
+ end
11
+ else
12
+ def self.dump(*args)
13
+ MultiJson.encode(*args)
14
+ end
15
+
16
+ def self.load(*args)
17
+ MultiJson.decode(*args)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module SweetTooth
2
+ class Redemption < APIResource
3
+ include SweetTooth::APIOperations::Create
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module SweetTooth
2
+ class RedemptionOption < APIResource
3
+ include SweetTooth::APIOperations::List
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ module SweetTooth
2
+ class SingletonAPIResource < APIResource
3
+ def self.url()
4
+ if self == SingletonAPIResource
5
+ raise NotImplementedError.new('SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Account, etc.)')
6
+ end
7
+ "/v1/#{CGI.escape(class_name.downcase)}"
8
+ end
9
+
10
+ def url
11
+ self.class.url
12
+ end
13
+
14
+ def self.retrieve(api_key=nil)
15
+ instance = self.new(nil, api_key)
16
+ instance.refresh
17
+ instance
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,168 @@
1
+ module SweetTooth
2
+ class SweetToothObject
3
+ include Enumerable
4
+
5
+ attr_accessor :api_key
6
+ @@permanent_attributes = Set.new([:api_key, :id])
7
+
8
+ # The default :id method is deprecated and isn't useful to us
9
+ if method_defined?(:id)
10
+ undef :id
11
+ end
12
+
13
+ def initialize(id=nil, api_key=nil)
14
+ # parameter overloading!
15
+ if id.kind_of?(Hash)
16
+ @retrieve_options = id.dup
17
+ @retrieve_options.delete(:id)
18
+ id = id[:id]
19
+ else
20
+ @retrieve_options = {}
21
+ end
22
+
23
+ @api_key = api_key
24
+ @values = {}
25
+ # This really belongs in APIResource, but not putting it there allows us
26
+ # to have a unified inspect method
27
+ @unsaved_values = Set.new
28
+ @transient_values = Set.new
29
+ @values[:id] = id if id
30
+ end
31
+
32
+ def self.construct_from(values, api_key=nil)
33
+ obj = self.new(values[:id], api_key)
34
+ obj.refresh_from(values, api_key)
35
+ obj
36
+ end
37
+
38
+ def to_s(*args)
39
+ SweetTooth::JSON.dump(@values, :pretty => true)
40
+ end
41
+
42
+ def inspect()
43
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
44
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + SweetTooth::JSON.dump(@values, :pretty => true)
45
+ end
46
+
47
+ def refresh_from(values, api_key, partial=false)
48
+ @api_key = api_key
49
+
50
+ @previous_metadata = values[:metadata]
51
+ removed = partial ? Set.new : Set.new(@values.keys - values.keys)
52
+ added = Set.new(values.keys - @values.keys)
53
+ # Wipe old state before setting new. This is useful for e.g. updating a
54
+ # customer, where there is a parameter that is not persistent. Mark those values
55
+ # which don't persist as transient
56
+
57
+ instance_eval do
58
+ remove_accessors(removed)
59
+ add_accessors(added)
60
+ end
61
+ removed.each do |k|
62
+ @values.delete(k)
63
+ @transient_values.add(k)
64
+ @unsaved_values.delete(k)
65
+ end
66
+ values.each do |k, v|
67
+ @values[k] = Util.convert_to_sweettooth_object(v, api_key)
68
+ @transient_values.delete(k)
69
+ @unsaved_values.delete(k)
70
+ end
71
+ end
72
+
73
+ def [](k)
74
+ @values[k.to_sym]
75
+ end
76
+
77
+ def []=(k, v)
78
+ send(:"#{k}=", v)
79
+ end
80
+
81
+ def keys
82
+ @values.keys
83
+ end
84
+
85
+ def values
86
+ @values.values
87
+ end
88
+
89
+ def to_json(*a)
90
+ SweetTooth::JSON.dump(@values)
91
+ end
92
+
93
+ def as_json(*a)
94
+ @values.as_json(*a)
95
+ end
96
+
97
+ def to_hash
98
+ @values
99
+ end
100
+
101
+ def each(&blk)
102
+ @values.each(&blk)
103
+ end
104
+
105
+ protected
106
+
107
+ def metaclass
108
+ class << self; self; end
109
+ end
110
+
111
+ def remove_accessors(keys)
112
+ metaclass.instance_eval do
113
+ keys.each do |k|
114
+ next if @@permanent_attributes.include?(k)
115
+ k_eq = :"#{k}="
116
+ remove_method(k) if method_defined?(k)
117
+ remove_method(k_eq) if method_defined?(k_eq)
118
+ end
119
+ end
120
+ end
121
+
122
+ def add_accessors(keys)
123
+ metaclass.instance_eval do
124
+ keys.each do |k|
125
+ next if @@permanent_attributes.include?(k)
126
+ k_eq = :"#{k}="
127
+ define_method(k) { @values[k] }
128
+ define_method(k_eq) do |v|
129
+ if v == ""
130
+ raise ArgumentError.new(
131
+ "You cannot set #{k} to an empty string." +
132
+ "We interpret empty strings as nil in requests." +
133
+ "You may set #{self}.#{k} = nil to delete the property.")
134
+ end
135
+ @values[k] = v
136
+ @unsaved_values.add(k)
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ def method_missing(name, *args)
143
+ # TODO: only allow setting in updateable classes.
144
+ if name.to_s.end_with?('=')
145
+ attr = name.to_s[0...-1].to_sym
146
+ add_accessors([attr])
147
+ begin
148
+ mth = method(name)
149
+ rescue NameError
150
+ raise NoMethodError.new("Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}")
151
+ end
152
+ return mth.call(args[0])
153
+ else
154
+ return @values[name] if @values.has_key?(name)
155
+ end
156
+
157
+ begin
158
+ super
159
+ rescue NoMethodError => e
160
+ if @transient_values.include?(name)
161
+ raise NoMethodError.new(e.message + ". HINT: The '#{name}' attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by SweetTooth's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
162
+ else
163
+ raise
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,102 @@
1
+ module SweetTooth
2
+ module Util
3
+ def self.objects_to_ids(h)
4
+ case h
5
+ when APIResource
6
+ h.id
7
+ when Hash
8
+ res = {}
9
+ h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
10
+ res
11
+ when Array
12
+ h.map { |v| objects_to_ids(v) }
13
+ else
14
+ h
15
+ end
16
+ end
17
+
18
+ def self.object_classes
19
+ @object_classes ||= {
20
+ 'activity' => Activity,
21
+ 'customer' => Customer,
22
+ 'redemption' => Redemption,
23
+ 'redemption_option' => RedemptionOption,
24
+ 'collection' => CollectionObject,
25
+ }
26
+ end
27
+
28
+ def self.convert_to_sweettooth_object(resp, api_key)
29
+ case resp
30
+ when Array
31
+ resp.map { |i| convert_to_sweettooth_object(i, api_key) }
32
+ when Hash
33
+ # Try converting to a known object class. If none available, fall back to generic SweetToothObject
34
+ object_classes.fetch(resp[:_object], SweetToothObject).construct_from(resp, api_key)
35
+ else
36
+ resp
37
+ end
38
+ end
39
+
40
+ def self.file_readable(file)
41
+ # This is nominally equivalent to File.readable?, but that can
42
+ # report incorrect results on some more oddball filesystems
43
+ # (such as AFS)
44
+ begin
45
+ File.open(file) { |f| }
46
+ rescue
47
+ false
48
+ else
49
+ true
50
+ end
51
+ end
52
+
53
+ def self.symbolize_names(object)
54
+ case object
55
+ when Hash
56
+ new = {}
57
+ object.each do |key, value|
58
+ key = (key.to_sym rescue key) || key
59
+ new[key] = symbolize_names(value)
60
+ end
61
+ new
62
+ when Array
63
+ object.map { |value| symbolize_names(value) }
64
+ else
65
+ object
66
+ end
67
+ end
68
+
69
+ def self.url_encode(key)
70
+ URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
71
+ end
72
+
73
+ def self.flatten_params(params, parent_key=nil)
74
+ result = []
75
+ params.each do |key, value|
76
+ calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
77
+ if value.is_a?(Hash)
78
+ result += flatten_params(value, calculated_key)
79
+ elsif value.is_a?(Array)
80
+ result += flatten_params_array(value, calculated_key)
81
+ else
82
+ result << [calculated_key, value]
83
+ end
84
+ end
85
+ result
86
+ end
87
+
88
+ def self.flatten_params_array(value, calculated_key)
89
+ result = []
90
+ value.each do |elem|
91
+ if elem.is_a?(Hash)
92
+ result += flatten_params(elem, calculated_key)
93
+ elsif elem.is_a?(Array)
94
+ result += flatten_params_array(elem, calculated_key)
95
+ else
96
+ result << ["#{calculated_key}[]", elem]
97
+ end
98
+ end
99
+ result
100
+ end
101
+ end
102
+ end