moonclerk 1.0.1

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 (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