moonclerk 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/README.md +118 -0
  7. data/Rakefile +1 -0
  8. data/bin/console +14 -0
  9. data/bin/setup +7 -0
  10. data/lib/moonclerk.rb +110 -0
  11. data/lib/moonclerk/api_operations/list.rb +40 -0
  12. data/lib/moonclerk/api_operations/request.rb +30 -0
  13. data/lib/moonclerk/api_resource.rb +42 -0
  14. data/lib/moonclerk/customer.rb +11 -0
  15. data/lib/moonclerk/errors/api_error.rb +4 -0
  16. data/lib/moonclerk/errors/authentication_error.rb +4 -0
  17. data/lib/moonclerk/errors/invalid_request_error.rb +10 -0
  18. data/lib/moonclerk/errors/moonclerk_error.rb +26 -0
  19. data/lib/moonclerk/form.rb +5 -0
  20. data/lib/moonclerk/list_object.rb +98 -0
  21. data/lib/moonclerk/moonclerk_object.rb +309 -0
  22. data/lib/moonclerk/payment.rb +10 -0
  23. data/lib/moonclerk/util.rb +49 -0
  24. data/lib/moonclerk/version.rb +3 -0
  25. data/moonclerk.gemspec +34 -0
  26. data/spec/dummy/README.rdoc +28 -0
  27. data/spec/dummy/Rakefile +6 -0
  28. data/spec/dummy/app/assets/images/.keep +0 -0
  29. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  30. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  31. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  32. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  33. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  34. data/spec/dummy/app/mailers/.keep +0 -0
  35. data/spec/dummy/app/models/.keep +0 -0
  36. data/spec/dummy/app/models/concerns/.keep +0 -0
  37. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  38. data/spec/dummy/bin/bundle +3 -0
  39. data/spec/dummy/bin/rails +4 -0
  40. data/spec/dummy/bin/rake +4 -0
  41. data/spec/dummy/bin/setup +29 -0
  42. data/spec/dummy/config.ru +4 -0
  43. data/spec/dummy/config/application.rb +26 -0
  44. data/spec/dummy/config/boot.rb +5 -0
  45. data/spec/dummy/config/database.yml +25 -0
  46. data/spec/dummy/config/environment.rb +5 -0
  47. data/spec/dummy/config/environments/development.rb +41 -0
  48. data/spec/dummy/config/environments/production.rb +79 -0
  49. data/spec/dummy/config/environments/test.rb +42 -0
  50. data/spec/dummy/config/initializers/assets.rb +11 -0
  51. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  52. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  53. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  54. data/spec/dummy/config/initializers/inflections.rb +16 -0
  55. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  56. data/spec/dummy/config/initializers/session_store.rb +3 -0
  57. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  58. data/spec/dummy/config/locales/en.yml +23 -0
  59. data/spec/dummy/config/routes.rb +56 -0
  60. data/spec/dummy/config/secrets.yml +22 -0
  61. data/spec/dummy/db/test.sqlite3 +0 -0
  62. data/spec/dummy/lib/assets/.keep +0 -0
  63. data/spec/dummy/log/.keep +0 -0
  64. data/spec/dummy/public/404.html +67 -0
  65. data/spec/dummy/public/422.html +67 -0
  66. data/spec/dummy/public/500.html +66 -0
  67. data/spec/dummy/public/favicon.ico +0 -0
  68. data/spec/models/moonclerk/customer_spec.rb +8 -0
  69. data/spec/moonclerk_spec.rb +7 -0
  70. data/spec/spec_helper.rb +63 -0
  71. metadata +314 -0
@@ -0,0 +1,5 @@
1
+ module Moonclerk
2
+ class Form < APIResource
3
+ extend Moonclerk::APIOperations::List
4
+ end
5
+ end
@@ -0,0 +1,98 @@
1
+ module Moonclerk
2
+ class ListObject < APIResource
3
+ include Enumerable
4
+ include Moonclerk::APIOperations::List
5
+
6
+ # This accessor allows a `ListObject` to inherit a count that was given to
7
+ # a predecessor. This allows consistent counts as a user pages through
8
+ # resources. Offset is used to shift the starting point of the list.
9
+ attr_accessor :count, :offset
10
+
11
+ # An empty list object. This is returned from +next+ when we know that
12
+ # there isn't a next page in order to replicate the behavior of the API
13
+ # when it attempts to return a page beyond the last.
14
+ def self.empty_list
15
+ ListObject.construct_from({ data: [], object: "" })
16
+ end
17
+
18
+ def [](k)
19
+ case k
20
+ when String, Symbol
21
+ super
22
+ else
23
+ raise ArgumentError.new("You tried to access the #{k.inspect} index, but ListObject types only support String keys.")
24
+ end
25
+ end
26
+
27
+ # Iterates through each resource in the page represented by the current
28
+ # `ListObject`.
29
+ #
30
+ # Note that this method makes no effort to fetch a new page when it gets to
31
+ # the end of the current page's resources. See also +auto_paging_each+.
32
+ def each(&blk)
33
+ self.data.each(&blk)
34
+ end
35
+
36
+ # Iterates through each resource in all pages, making additional fetches to
37
+ # the API as necessary.
38
+ #
39
+ # Note that this method will make as many API calls as necessary to fetch
40
+ # all resources. For more granular control, please see +each+ and
41
+ # +next_page+.
42
+ def auto_paging_each(&blk)
43
+ return enum_for(:auto_paging_each) unless block_given?
44
+
45
+ page = self
46
+ loop do
47
+ page.each(&blk)
48
+ page = page.next_page
49
+ break if page.empty?
50
+ end
51
+ end
52
+
53
+ # Returns true if the page object contains no elements.
54
+ def empty?
55
+ self.data.blank?
56
+ end
57
+
58
+ def retrieve(id)
59
+ id, retrieve_params = Util.normalize_id(id)
60
+ response = request(:get,"#{url}/#{CGI.escape(id)}", retrieve_params)
61
+ Util.convert_to_moonclerk_object(response)
62
+ end
63
+
64
+ def create(params = {})
65
+ response = request(:post, url, params)
66
+ Util.convert_to_moonclerk_object(response)
67
+ end
68
+
69
+ # Fetches the next page in the resource list (if there is one).
70
+ #
71
+ # This method will try to respect the count of the current page. If none
72
+ # was given, the default count will be fetched again.
73
+ def next_page(params = {})
74
+ params = {
75
+ :count => count, # may be nil
76
+ :offset => (offset || 0) + (count || 20),
77
+ }.merge(params)
78
+
79
+ list(params)
80
+ end
81
+
82
+ # Fetches the previous page in the resource list (if there is one).
83
+ #
84
+ # This method will try to respect the count of the current page. If none
85
+ # was given, the default count will be fetched again.
86
+ def previous_page(params = {})
87
+ new_offset = (offset || 0) - (count || 20)
88
+ new_offset = 0 if new_offset < 0
89
+
90
+ params = {
91
+ :count => count, # may be nil
92
+ :offset => new_offset,
93
+ }.merge(params)
94
+
95
+ list(params)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,309 @@
1
+ module Moonclerk
2
+ class MoonclerkObject
3
+ include Enumerable
4
+
5
+ @@permanent_attributes = Set.new([:id])
6
+
7
+ # The default :id method is deprecated and isn't useful to us
8
+ if method_defined?(:id)
9
+ undef :id
10
+ end
11
+
12
+ def initialize(id = nil)
13
+ id, @retrieve_params = Util.normalize_id(id)
14
+ @values = {}
15
+ # This really belongs in APIResource, but not putting it there allows us
16
+ # to have a unified inspect method
17
+ @unsaved_values = Set.new
18
+ @transient_values = Set.new
19
+ @values[:id] = id if id
20
+ end
21
+
22
+ def self.construct_from(values)
23
+ values = Moonclerk::Util.symbolize_names(values)
24
+ self.new(values[:id]).refresh_from(values)
25
+ end
26
+
27
+ # Determines the equality of two Moonclerk objects. Moonclerk objects are
28
+ # considered to be equal if they have the same set of values and each one
29
+ # of those values is the same.
30
+ def ==(other)
31
+ @values == other.instance_variable_get(:@values)
32
+ end
33
+
34
+ def to_s(*args)
35
+ JSON.pretty_generate(@values)
36
+ end
37
+
38
+ def inspect
39
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
40
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
41
+ end
42
+
43
+ def refresh_from(values)
44
+ @original_values = Marshal.load(Marshal.dump(values)) # deep copy
45
+
46
+ removed = Set.new(@values.keys - values.keys)
47
+ added = Set.new(values.keys - @values.keys)
48
+
49
+ # Wipe old state before setting new. This is useful for e.g. updating a
50
+ # customer, where there is no persistent card parameter. Mark those values
51
+ # which don't persist as transient
52
+
53
+ instance_eval do
54
+ remove_accessors(removed)
55
+ add_accessors(added, values)
56
+ end
57
+
58
+ removed.each do |k|
59
+ @values.delete(k)
60
+ @transient_values.add(k)
61
+ @unsaved_values.delete(k)
62
+ end
63
+
64
+ update_attributes(values)
65
+ values.each do |k, _|
66
+ @transient_values.delete(k)
67
+ @unsaved_values.delete(k)
68
+ end
69
+
70
+ return self
71
+ end
72
+
73
+ # Mass assigns attributes on the model.
74
+ def update_attributes(values)
75
+ values.each do |k, v|
76
+ if !@@permanent_attributes.include?(k) && !self.respond_to?(:"#{k}=")
77
+ next
78
+ end
79
+
80
+
81
+ if self.is_a?(Moonclerk::ListObject) && k == :data
82
+ @values[k] = Util.convert_to_moonclerk_object(v, values[:object])
83
+ else
84
+ @values[k] = Util.convert_to_moonclerk_object(v)
85
+ end
86
+
87
+ @unsaved_values.add(k)
88
+ end
89
+ self
90
+ end
91
+
92
+ def [](k)
93
+ @values[k.to_sym]
94
+ end
95
+
96
+ def []=(k, v)
97
+ send(:"#{k}=", v)
98
+ end
99
+
100
+ def keys
101
+ @values.keys
102
+ end
103
+
104
+ def values
105
+ @values.values
106
+ end
107
+
108
+ def to_json(*a)
109
+ JSON.generate(@values)
110
+ end
111
+
112
+ def as_json(*a)
113
+ @values.as_json(*a)
114
+ end
115
+
116
+ def to_hash
117
+ maybe_to_hash = lambda do |value|
118
+ value.respond_to?(:to_hash) ? value.to_hash : value
119
+ end
120
+
121
+ @values.inject({}) do |acc, (key, value)|
122
+ acc[key] = case value
123
+ when Array
124
+ value.map(&maybe_to_hash)
125
+ else
126
+ maybe_to_hash.call(value)
127
+ end
128
+ acc
129
+ end
130
+ end
131
+
132
+ def each(&blk)
133
+ @values.each(&blk)
134
+ end
135
+
136
+ def _dump(level)
137
+ Marshal.dump([@values])
138
+ end
139
+
140
+ def self._load(args)
141
+ values = Marshal.load(args)
142
+ construct_from(values)
143
+ end
144
+
145
+ if RUBY_VERSION < '1.9.2'
146
+ def respond_to?(symbol)
147
+ @values.has_key?(symbol) || super
148
+ end
149
+ end
150
+
151
+ def serialize_nested_object(key)
152
+ new_value = @values[key]
153
+ if new_value.is_a?(APIResource)
154
+ return {}
155
+ end
156
+
157
+ if @unsaved_values.include?(key)
158
+ # the object has been reassigned
159
+ # e.g. as object.key = {foo => bar}
160
+ update = new_value
161
+ new_keys = update.keys.map(&:to_sym)
162
+
163
+ # remove keys at the server, but not known locally
164
+ if @original_values[key]
165
+ keys_to_unset = @original_values[key].keys - new_keys
166
+ keys_to_unset.each {|key| update[key] = ''}
167
+ end
168
+
169
+ update
170
+ else
171
+ # can be serialized normally
172
+ self.class.serialize_params(new_value)
173
+ end
174
+ end
175
+
176
+ def self.serialize_params(obj, original_value=nil)
177
+ case obj
178
+ when nil
179
+ ''
180
+ when MoonclerkObject
181
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
182
+ obj_values = obj.instance_variable_get(:@values)
183
+ update_hash = {}
184
+
185
+ unsaved_keys.each do |k|
186
+ update_hash[k] = serialize_params(obj_values[k])
187
+ end
188
+
189
+ obj_values.each do |k, v|
190
+ if v.is_a?(MoonclerkObject) || v.is_a?(Hash)
191
+ update_hash[k] = obj.serialize_nested_object(k)
192
+ elsif v.is_a?(Array)
193
+ original_value = obj.instance_variable_get(:@original_values)[k]
194
+ if original_value && original_value.length > v.length
195
+ # url params provide no mechanism for deleting an item in an array,
196
+ # just overwriting the whole array or adding new items. So let's not
197
+ # allow deleting without a full overwrite until we have a solution.
198
+ raise ArgumentError.new(
199
+ "You cannot delete an item from an array, you must instead set a new array"
200
+ )
201
+ end
202
+ update_hash[k] = serialize_params(v, original_value)
203
+ end
204
+ end
205
+
206
+ update_hash
207
+ when Array
208
+ update_hash = {}
209
+ obj.each_with_index do |value, index|
210
+ update = serialize_params(value)
211
+ if update != {} && (!original_value || update != original_value[index])
212
+ update_hash[index] = update
213
+ end
214
+ end
215
+
216
+ if update_hash == {}
217
+ nil
218
+ else
219
+ update_hash
220
+ end
221
+ else
222
+ obj
223
+ end
224
+ end
225
+
226
+ protected
227
+
228
+ def metaclass
229
+ class << self; self; end
230
+ end
231
+
232
+ def protected_fields
233
+ []
234
+ end
235
+
236
+ def remove_accessors(keys)
237
+ f = protected_fields
238
+ metaclass.instance_eval do
239
+ keys.each do |k|
240
+ next if f.include?(k)
241
+ next if @@permanent_attributes.include?(k)
242
+ k_eq = :"#{k}="
243
+ remove_method(k) if method_defined?(k)
244
+ remove_method(k_eq) if method_defined?(k_eq)
245
+ end
246
+ end
247
+ end
248
+
249
+ def add_accessors(keys, values)
250
+ f = protected_fields
251
+ metaclass.instance_eval do
252
+ keys.each do |k|
253
+ next if f.include?(k)
254
+ next if @@permanent_attributes.include?(k)
255
+ k_eq = :"#{k}="
256
+ define_method(k) { @values[k] }
257
+ define_method(k_eq) do |v|
258
+ if v == ""
259
+ raise ArgumentError.new(
260
+ "You cannot set #{k} to an empty string." \
261
+ "We interpret empty strings as nil in requests." \
262
+ "You may set #{self}.#{k} = nil to delete the property.")
263
+ end
264
+ @values[k] = v
265
+ @unsaved_values.add(k)
266
+ end
267
+
268
+ if [FalseClass, TrueClass].include?(values[k].class)
269
+ k_bool = :"#{k}?"
270
+ define_method(k_bool) { @values[k] }
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ def method_missing(name, *args)
277
+ # TODO: only allow setting in updateable classes.
278
+ if name.to_s.end_with?('=')
279
+ attr = name.to_s[0...-1].to_sym
280
+
281
+ # the second argument is only required when adding boolean accessors
282
+ add_accessors([attr], {})
283
+
284
+ begin
285
+ mth = method(name)
286
+ rescue NameError
287
+ raise NoMethodError.new("Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}")
288
+ end
289
+ return mth.call(args[0])
290
+ else
291
+ return @values[name] if @values.has_key?(name)
292
+ end
293
+
294
+ begin
295
+ super
296
+ rescue NoMethodError => e
297
+ if @transient_values.include?(name)
298
+ 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 Moonclerk's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
299
+ else
300
+ raise
301
+ end
302
+ end
303
+ end
304
+
305
+ def respond_to_missing?(symbol, include_private = false)
306
+ @values && @values.has_key?(symbol) || super
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,10 @@
1
+ module Moonclerk
2
+ class Payment < APIResource
3
+ extend Moonclerk::APIOperations::List
4
+ @permitted_attributes = [:form_id,
5
+ :customer_id,
6
+ :date_from,
7
+ :date_to,
8
+ :status]
9
+ end
10
+ end
@@ -0,0 +1,49 @@
1
+ module Moonclerk
2
+ module Util
3
+ def self.object_classes
4
+ @object_classes ||= {
5
+ 'customer' => Customer,
6
+ 'form' => Form,
7
+ 'payment' => Payment
8
+ }
9
+ end
10
+
11
+ def self.convert_to_moonclerk_object(resp, class_name = nil)
12
+ case resp
13
+ when Array
14
+ resp.map { |i| convert_to_moonclerk_object(i, class_name) }
15
+ when Hash
16
+ # Try converting to a known object class. If none available, fall back to generic MoonclerkObject
17
+ object_classes.fetch(class_name, MoonclerkObject).construct_from(resp)
18
+ else
19
+ resp
20
+ end
21
+ end
22
+
23
+ def self.normalize_id(id)
24
+ if id.kind_of?(Hash) # overloaded id
25
+ params_hash = id.dup
26
+ id = params_hash.delete(:id)
27
+ else
28
+ params_hash = {}
29
+ end
30
+ [id, params_hash]
31
+ end
32
+
33
+ def self.symbolize_names(object)
34
+ case object
35
+ when Hash
36
+ new_hash = {}
37
+ object.each do |key, value|
38
+ key = (key.to_sym rescue key) || key
39
+ new_hash[key] = symbolize_names(value)
40
+ end
41
+ new_hash
42
+ when Array
43
+ object.map { |value| symbolize_names(value) }
44
+ else
45
+ object
46
+ end
47
+ end
48
+ end
49
+ end