gecko-ruby 0.0.4

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 (132) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rubocop.yml +9 -0
  4. data/.travis.yml +5 -0
  5. data/CHANGELOG.md +41 -0
  6. data/CONTRIBUTING.md +0 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +144 -0
  10. data/Rakefile +19 -0
  11. data/gecko-ruby.gemspec +34 -0
  12. data/generate.thor +81 -0
  13. data/lib/gecko-ruby.rb +1 -0
  14. data/lib/gecko.rb +31 -0
  15. data/lib/gecko/client.rb +132 -0
  16. data/lib/gecko/helpers/association_helper.rb +69 -0
  17. data/lib/gecko/helpers/inspection_helper.rb +51 -0
  18. data/lib/gecko/helpers/record_helper.rb +29 -0
  19. data/lib/gecko/helpers/serialization_helper.rb +61 -0
  20. data/lib/gecko/helpers/validation_helper.rb +91 -0
  21. data/lib/gecko/record/account.rb +66 -0
  22. data/lib/gecko/record/address.rb +33 -0
  23. data/lib/gecko/record/base.rb +66 -0
  24. data/lib/gecko/record/base_adapter.rb +365 -0
  25. data/lib/gecko/record/company.rb +41 -0
  26. data/lib/gecko/record/contact.rb +25 -0
  27. data/lib/gecko/record/currency.rb +23 -0
  28. data/lib/gecko/record/exceptions.rb +15 -0
  29. data/lib/gecko/record/fulfillment.rb +42 -0
  30. data/lib/gecko/record/fulfillment_line_item.rb +26 -0
  31. data/lib/gecko/record/image.rb +38 -0
  32. data/lib/gecko/record/invoice.rb +29 -0
  33. data/lib/gecko/record/invoice_line_item.rb +18 -0
  34. data/lib/gecko/record/location.rb +23 -0
  35. data/lib/gecko/record/order.rb +50 -0
  36. data/lib/gecko/record/order_line_item.rb +35 -0
  37. data/lib/gecko/record/product.rb +27 -0
  38. data/lib/gecko/record/purchase_order.rb +43 -0
  39. data/lib/gecko/record/purchase_order_line_item.rb +29 -0
  40. data/lib/gecko/record/tax_type.rb +21 -0
  41. data/lib/gecko/record/user.rb +44 -0
  42. data/lib/gecko/record/variant.rb +96 -0
  43. data/lib/gecko/version.rb +3 -0
  44. data/test/client_test.rb +29 -0
  45. data/test/fixtures/vcr_cassettes/accounts.yml +57 -0
  46. data/test/fixtures/vcr_cassettes/accounts_current.yml +57 -0
  47. data/test/fixtures/vcr_cassettes/addresses.yml +68 -0
  48. data/test/fixtures/vcr_cassettes/addresses_count.yml +58 -0
  49. data/test/fixtures/vcr_cassettes/companies.yml +62 -0
  50. data/test/fixtures/vcr_cassettes/companies_count.yml +58 -0
  51. data/test/fixtures/vcr_cassettes/contacts.yml +60 -0
  52. data/test/fixtures/vcr_cassettes/contacts_count.yml +58 -0
  53. data/test/fixtures/vcr_cassettes/currencies.yml +62 -0
  54. data/test/fixtures/vcr_cassettes/currencies_count.yml +58 -0
  55. data/test/fixtures/vcr_cassettes/fulfillments.yml +59 -0
  56. data/test/fixtures/vcr_cassettes/fulfillments_count.yml +58 -0
  57. data/test/fixtures/vcr_cassettes/images.yml +59 -0
  58. data/test/fixtures/vcr_cassettes/images_count.yml +58 -0
  59. data/test/fixtures/vcr_cassettes/invoice_line_items.yml +63 -0
  60. data/test/fixtures/vcr_cassettes/invoice_line_items_count.yml +62 -0
  61. data/test/fixtures/vcr_cassettes/invoices.yml +63 -0
  62. data/test/fixtures/vcr_cassettes/invoices_count.yml +62 -0
  63. data/test/fixtures/vcr_cassettes/locations.yml +65 -0
  64. data/test/fixtures/vcr_cassettes/locations_count.yml +58 -0
  65. data/test/fixtures/vcr_cassettes/order_line_items.yml +63 -0
  66. data/test/fixtures/vcr_cassettes/order_line_items_count.yml +62 -0
  67. data/test/fixtures/vcr_cassettes/orders.yml +62 -0
  68. data/test/fixtures/vcr_cassettes/orders_count.yml +58 -0
  69. data/test/fixtures/vcr_cassettes/products.yml +79 -0
  70. data/test/fixtures/vcr_cassettes/products_count.yml +58 -0
  71. data/test/fixtures/vcr_cassettes/products_new_invalid.yml +54 -0
  72. data/test/fixtures/vcr_cassettes/products_new_valid.yml +58 -0
  73. data/test/fixtures/vcr_cassettes/purchase_order_line_items.yml +64 -0
  74. data/test/fixtures/vcr_cassettes/purchase_order_line_items_count.yml +62 -0
  75. data/test/fixtures/vcr_cassettes/purchase_orders.yml +63 -0
  76. data/test/fixtures/vcr_cassettes/purchase_orders_count.yml +62 -0
  77. data/test/fixtures/vcr_cassettes/tax_types.yml +74 -0
  78. data/test/fixtures/vcr_cassettes/tax_types_count.yml +62 -0
  79. data/test/fixtures/vcr_cassettes/users.yml +60 -0
  80. data/test/fixtures/vcr_cassettes/users_count.yml +58 -0
  81. data/test/fixtures/vcr_cassettes/users_current.yml +54 -0
  82. data/test/fixtures/vcr_cassettes/variants.yml +60 -0
  83. data/test/fixtures/vcr_cassettes/variants_count.yml +58 -0
  84. data/test/gecko_test.rb +7 -0
  85. data/test/helpers/association_helper_test.rb +56 -0
  86. data/test/helpers/inspection_helper_test.rb +27 -0
  87. data/test/helpers/serialization_helper_test.rb +30 -0
  88. data/test/helpers/validation_helper_test.rb +24 -0
  89. data/test/record/account_adapter_test.rb +43 -0
  90. data/test/record/address_adapter_test.rb +14 -0
  91. data/test/record/address_test.rb +18 -0
  92. data/test/record/company_adapter_test.rb +14 -0
  93. data/test/record/company_test.rb +18 -0
  94. data/test/record/contact_adapter_test.rb +14 -0
  95. data/test/record/contact_test.rb +18 -0
  96. data/test/record/currency_adapter_test.rb +14 -0
  97. data/test/record/currency_test.rb +18 -0
  98. data/test/record/fulfillment_adapter_test.rb +24 -0
  99. data/test/record/fulfillment_line_item_adapter_test.rb +21 -0
  100. data/test/record/fulfillment_line_item_test.rb +18 -0
  101. data/test/record/fulfillment_test.rb +27 -0
  102. data/test/record/image_adapter_test.rb +14 -0
  103. data/test/record/image_test.rb +25 -0
  104. data/test/record/invoice_adapter_test.rb +14 -0
  105. data/test/record/invoice_line_item_adapter_test.rb +20 -0
  106. data/test/record/invoice_line_item_test.rb +18 -0
  107. data/test/record/invoice_test.rb +18 -0
  108. data/test/record/location_adapter_test.rb +14 -0
  109. data/test/record/location_test.rb +18 -0
  110. data/test/record/order_adapter_test.rb +14 -0
  111. data/test/record/order_line_item_adapter_test.rb +14 -0
  112. data/test/record/order_line_item_test.rb +18 -0
  113. data/test/record/order_test.rb +18 -0
  114. data/test/record/product_adapter_test.rb +32 -0
  115. data/test/record/product_test.rb +18 -0
  116. data/test/record/purchase_order_adapter_test.rb +14 -0
  117. data/test/record/purchase_order_line_item_adapter_test.rb +14 -0
  118. data/test/record/purchase_order_line_item_test.rb +18 -0
  119. data/test/record/purchase_order_test.rb +18 -0
  120. data/test/record/tax_type_adapter_test.rb +14 -0
  121. data/test/record/tax_type_test.rb +18 -0
  122. data/test/record/user_adapter_test.rb +27 -0
  123. data/test/record/user_test.rb +18 -0
  124. data/test/record/variant_adapter_test.rb +14 -0
  125. data/test/record/variant_test.rb +44 -0
  126. data/test/support/let.rb +10 -0
  127. data/test/support/shared_adapter_examples.rb +159 -0
  128. data/test/support/shared_record_examples.rb +21 -0
  129. data/test/support/testing_adapter.rb +11 -0
  130. data/test/support/vcr_support.rb +7 -0
  131. data/test/test_helper.rb +21 -0
  132. metadata +430 -0
@@ -0,0 +1,365 @@
1
+ module Gecko
2
+ module Record
3
+ class BaseAdapter
4
+ attr_reader :client
5
+ # Instantiates a new Record Adapter
6
+ #
7
+ # @param [Gecko::Client] client
8
+ # @param [String] model_name
9
+ #
10
+ # @return [undefined]
11
+ #
12
+ # @api private
13
+ def initialize(client, model_name)
14
+ @client = client
15
+ @model_name = model_name
16
+ @identity_map = {}
17
+ end
18
+
19
+ # Find a record via ID, first searches the Identity Map, then makes an
20
+ # API request.
21
+ #
22
+ # @example
23
+ # client.Product.find(12)
24
+ #
25
+ # @param [Integer] id ID of record
26
+ #
27
+ # @return [Gecko::Record::Base] if a record was found
28
+ # either in the identity map or via the API
29
+ # @return [nil] If no record was found
30
+ #
31
+ # @api public
32
+ def find(id)
33
+ if has_record_for_id?(id)
34
+ record_for_id(id)
35
+ else
36
+ fetch(id)
37
+ end
38
+ end
39
+
40
+ # Searches the Identity Map for a record via ID
41
+ #
42
+ # @example
43
+ # client.Product.record_for_id(12)
44
+ #
45
+ # @return [Gecko::Record::Base] if a record was found in the identity map.
46
+ # @raise [Gecko::Record::RecordNotInIdentityMap] If no record was found
47
+ #
48
+ # @api private
49
+ def record_for_id(id)
50
+ verify_id_presence!(id)
51
+ @identity_map.fetch(id) { record_not_in_identity_map!(id) }
52
+ end
53
+
54
+ # Returns whether the Identity Map has a record for a particular ID
55
+ #
56
+ # @example
57
+ # client.Product.has_record_for_id?(12)
58
+ #
59
+ # @return [Boolean] if a record was found in the identity map.
60
+ #
61
+ # @api private
62
+ def has_record_for_id?(id)
63
+ @identity_map.key?(id)
64
+ end
65
+
66
+ # Find multiple records via IDs, searching the Identity Map, then making an
67
+ # API request for remaining records. May return nulls
68
+ #
69
+ # @example
70
+ # client.Product.find_many([12, 13, 14])
71
+ #
72
+ # @param [Array<Integer>] ids IDs of records to fetch
73
+ #
74
+ # @return [Array<Gecko::Record::Base>] Records for the ids
75
+ # either in the identity map or via the API
76
+ #
77
+ # @api public
78
+ def find_many(ids)
79
+ existing, required = ids.partition { |id| has_record_for_id?(id) }
80
+ if required.any?
81
+ where(ids: ids) + existing.map { |id| record_for_id(id) }
82
+ else
83
+ existing.map { |id| record_for_id(id) }
84
+ end
85
+ end
86
+
87
+ # Make an API request with parameters. Parameters vary via Record Type
88
+ #
89
+ # @example Fetch via ID
90
+ # client.Product.where(ids: [1,2]
91
+ #
92
+ # @example Fetch via date
93
+ # client.Product.where(updated_at_min: "2014-03-03T21:09:00")
94
+ #
95
+ # @example Search
96
+ # client.Product.where(q: "gecko")
97
+ #
98
+ # @param [#to_hash] params
99
+ # @option params [String] :q Search query
100
+ # @option params [Integer] :page (1) Page number for pagination
101
+ # @option params [Integer] :limit (100) Page limit for pagination
102
+ # @option params [Array<Integer>] :ids IDs to search for
103
+ # @option params [String] :updated_at_min Last updated_at minimum
104
+ # @option params [String] :updated_at_max Last updated_at maximum
105
+ # @option params [String] :order Sort order i.e 'name asc'
106
+ # @option params [String, Array<String>] :status Record status/es
107
+ #
108
+ # @return [Array<Gecko::Record::Base>] Records via the API
109
+ #
110
+ # @api public
111
+ def where(params={})
112
+ response = request(:get, plural_path, params: params)
113
+ parsed_response = response.parsed
114
+ set_pagination(response.headers)
115
+ parse_records(parsed_response)
116
+ end
117
+
118
+ # Return total count for a record type. Fetched from the metadata
119
+ #
120
+ # @example
121
+ # client.Product.count
122
+ #
123
+ # @return [Integer] Total record count
124
+ #
125
+ # @api public
126
+ def count
127
+ self.where(limit: 0)
128
+ @pagination['total_records']
129
+ end
130
+
131
+ # Fetch a record via API, regardless of whether it is already in identity map.
132
+ #
133
+ # @example
134
+ # client.Product.fetch(12)
135
+ #
136
+ # @return [Gecko::Record::Base] if a record was found
137
+ # @return [nil] if no record was found
138
+ #
139
+ # @api private
140
+ def fetch(id)
141
+ verify_id_presence!(id)
142
+ response = request(:get, plural_path + '/' + id.to_s)
143
+ record_json = extract_record(response.parsed)
144
+ instantiate_and_register_record(record_json)
145
+ rescue OAuth2::Error => ex
146
+ case ex.response.status
147
+ when 404
148
+ record_not_found!(id)
149
+ else
150
+ raise
151
+ end
152
+ end
153
+
154
+ # Parse a json collection and instantiate records
155
+ #
156
+ # @return [Array<Gecko::Record::Base>]
157
+ #
158
+ # @api private
159
+ def parse_records(json)
160
+ extract_collection(json).map do |record_json|
161
+ instantiate_and_register_record(record_json)
162
+ end
163
+ end
164
+
165
+ # Extract a collection from an API response
166
+ #
167
+ # @return [Hash]
168
+ #
169
+ # @api private
170
+ def extract_collection(json)
171
+ json[plural_path]
172
+ end
173
+
174
+ # Extract a record from an API response
175
+ #
176
+ # @return Hash
177
+ #
178
+ # @api private
179
+ def extract_record(json)
180
+ json && json[json_root]
181
+ end
182
+
183
+ # Build a new record
184
+ #
185
+ # @example
186
+ # new_order = client.Order.build(company_id: 123, order_number: 1234)
187
+ #
188
+ # @example
189
+ # new_order = client.Order.build
190
+ # new_order.order_number = 1234
191
+ #
192
+ # @param [#to_hash] initial attributes to set up the record
193
+ #
194
+ # @return [Gecko::Record::Base]
195
+ #
196
+ # @api public
197
+ def build(attributes={})
198
+ model_class.new(@client, attributes)
199
+ end
200
+
201
+ # Save a record
202
+ #
203
+ # @params [Object] :record A Gecko::Record object
204
+ #
205
+ # @return [Boolean] whether the save was successful.
206
+ # If false the record will contain an errors hash
207
+ #
208
+ # @api private
209
+ def save(record)
210
+ if record.persisted?
211
+ update_record(record)
212
+ else
213
+ create_record(record)
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ # Returns the json key for a record adapter
220
+ #
221
+ # @example
222
+ # product_adapter.json_root #=> "product"
223
+ #
224
+ # @return [String]
225
+ #
226
+ # @api private
227
+ def json_root
228
+ @model_name.to_s.underscore
229
+ end
230
+
231
+ # Returns the pluralized name of a record class used for generating API endpoint
232
+ #
233
+ # @return [String]
234
+ #
235
+ # @api private
236
+ def plural_path
237
+ json_root + 's'
238
+ end
239
+
240
+ # Returns the model class associated with an adapter
241
+ #
242
+ # @example
243
+ # product_adapter.model_class #=> Gecko::Record::Product
244
+ #
245
+ # @return [Class]
246
+ #
247
+ # @api private
248
+ def model_class
249
+ Gecko::Record.const_get(@model_name)
250
+ end
251
+
252
+ # Instantiates a record from it's JSON representation and registers
253
+ # it into the identity map
254
+ #
255
+ # @return [Gecko::Record::Base]
256
+ #
257
+ # @api private
258
+ def instantiate_and_register_record(record_json)
259
+ record = model_class.new(@client, record_json)
260
+ register_record(record)
261
+ record
262
+ end
263
+
264
+ # Registers a record into the identity map
265
+ #
266
+ # @return [Gecko::Record::Base]
267
+ #
268
+ # @api private
269
+ def register_record(record)
270
+ @identity_map[record.id] = record
271
+ end
272
+
273
+ # Create a record via API
274
+ #
275
+ # @return [OAuth2::Response]
276
+ #
277
+ # @api private
278
+ def create_record(record)
279
+ response = request(:post, plural_path, {
280
+ body: record.as_json,
281
+ raise_errors: false
282
+ })
283
+ handle_response(record, response)
284
+ end
285
+
286
+ # Update a record via API
287
+ #
288
+ # @return [OAuth2::Response]
289
+ #
290
+ # @api private
291
+ def update_record(record)
292
+ response = request(:put, plural_path + "/" + record.id.to_s, {
293
+ body: record.as_json,
294
+ raise_errors: false
295
+ })
296
+ handle_response(record, response)
297
+ end
298
+
299
+ # Handle the API response.
300
+ # - Updates the record if attributes are returned
301
+ # - Adds validation errors from a 422
302
+ #
303
+ # @return [OAuth2::Response]
304
+ #
305
+ # @api private
306
+ def handle_response(record, response)
307
+ case response.status
308
+ when 200..299
309
+ if response_json = extract_record(response.parsed)
310
+ record.attributes = response_json
311
+ register_record(record)
312
+ end
313
+ true
314
+ when 422
315
+ record.errors.from_response(response.parsed['errors'])
316
+ false
317
+ else
318
+ fail OAuth2::Error.new(response)
319
+ end
320
+ end
321
+
322
+ # Sets up the pagination metadata on a record adapter
323
+ #
324
+ # @api private
325
+ def set_pagination(headers)
326
+ @pagination = JSON.parse(headers["x-pagination"]) if headers["x-pagination"]
327
+ end
328
+
329
+ # Makes a request to the API.
330
+ #
331
+ # @param [Symbol] verb the HTTP request method
332
+ # @param [String] path the HTTP URL path of the request
333
+ # @param [Hash] opts the options to make the request with
334
+ # @option opts [Hash] :params params for request
335
+ #
336
+ # @return [OAuth2::Response]
337
+ #
338
+ # @api private
339
+ def request(verb, path, options={})
340
+ ActiveSupport::Notifications.instrument('request.gecko') do |payload|
341
+ payload[:verb] = verb
342
+ payload[:params] = options[:params]
343
+ payload[:body] = options[:body]
344
+ payload[:model_class] = model_class
345
+ payload[:request_path] = path
346
+ payload[:response] = @client.access_token.request(verb, path, options)
347
+ end
348
+ end
349
+
350
+ def record_not_found!(id)
351
+ fail RecordNotFound, "Couldn't find #{model_class.name} with id=#{id}"
352
+ end
353
+
354
+ def record_not_in_identity_map!(id)
355
+ fail RecordNotInIdentityMap, "Couldn't find #{model_class.name} with id=#{id}"
356
+ end
357
+
358
+ def verify_id_presence!(id)
359
+ if id.respond_to?(:empty?) ? id.empty? : !id
360
+ fail RecordNotFound, "Couldn't find #{model_class.name} without an ID"
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,41 @@
1
+ require 'gecko/record/base'
2
+
3
+ module Gecko
4
+ module Record
5
+ class Company < Base
6
+ belongs_to :assignee, class_name: 'User'
7
+
8
+ has_many :addresses
9
+ has_many :contacts
10
+ has_many :notes
11
+
12
+ attribute :name, String
13
+ attribute :description, String
14
+ attribute :company_code, String
15
+ attribute :phone_number, String
16
+ attribute :fax, String
17
+ attribute :email, String
18
+ attribute :website, String
19
+ attribute :company_type, String
20
+
21
+ attribute :status, String, readonly: true
22
+
23
+ attribute :tax_number, String
24
+
25
+ attribute :default_tax_rate, BigDecimal
26
+ attribute :default_tax_type_id, Integer
27
+
28
+ attribute :default_discount_rate, BigDecimal
29
+
30
+ # belongs_to :default_price_list, class_name: "PriceList"
31
+ # belongs_to :default_payment_term, class_name: "PaymentTerm"
32
+ end
33
+
34
+ class CompanyAdapter < BaseAdapter
35
+ # Override plural_path to properly pluralize company
36
+ def plural_path
37
+ 'companies'
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ require 'gecko/record/base'
2
+
3
+ module Gecko
4
+ module Record
5
+ class Contact < Base
6
+ belongs_to :company
7
+
8
+ attribute :email, String
9
+ attribute :first_name, String
10
+ attribute :last_name, String
11
+ attribute :location, String
12
+ attribute :mobile, String
13
+ attribute :notes, String
14
+ attribute :phone_number, String
15
+ attribute :fax, String
16
+ attribute :position, String
17
+ attribute :phone, String
18
+
19
+ attribute :status, String, readonly: true
20
+ end
21
+
22
+ class ContactAdapter < BaseAdapter
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ require 'gecko/record/base'
2
+
3
+ module Gecko
4
+ module Record
5
+ class Currency < Base
6
+ attribute :iso, String
7
+ attribute :name, String
8
+ attribute :rate, BigDecimal
9
+ attribute :symbol, String
10
+ attribute :separator, String
11
+ attribute :delimiter, String
12
+ attribute :precision, Integer
13
+ attribute :format, String
14
+ end
15
+
16
+ class CurrencyAdapter < BaseAdapter
17
+ # Override plural_path to properly pluralize currency
18
+ def plural_path
19
+ 'currencies'
20
+ end
21
+ end
22
+ end
23
+ end