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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/README.md +118 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/moonclerk.rb +110 -0
- data/lib/moonclerk/api_operations/list.rb +40 -0
- data/lib/moonclerk/api_operations/request.rb +30 -0
- data/lib/moonclerk/api_resource.rb +42 -0
- data/lib/moonclerk/customer.rb +11 -0
- data/lib/moonclerk/errors/api_error.rb +4 -0
- data/lib/moonclerk/errors/authentication_error.rb +4 -0
- data/lib/moonclerk/errors/invalid_request_error.rb +10 -0
- data/lib/moonclerk/errors/moonclerk_error.rb +26 -0
- data/lib/moonclerk/form.rb +5 -0
- data/lib/moonclerk/list_object.rb +98 -0
- data/lib/moonclerk/moonclerk_object.rb +309 -0
- data/lib/moonclerk/payment.rb +10 -0
- data/lib/moonclerk/util.rb +49 -0
- data/lib/moonclerk/version.rb +3 -0
- data/moonclerk.gemspec +34 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +26 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/models/moonclerk/customer_spec.rb +8 -0
- data/spec/moonclerk_spec.rb +7 -0
- data/spec/spec_helper.rb +63 -0
- metadata +314 -0
@@ -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,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
|