fera-api 0.1.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/.idea/fera-api.iml +86 -0
  3. data/.idea/modules.xml +8 -0
  4. data/.idea/vcs.xml +6 -0
  5. data/.idea/workspace.xml +91 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +56 -0
  8. data/Gemfile +14 -0
  9. data/Gemfile.lock +92 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +256 -0
  12. data/Rakefile +12 -0
  13. data/lib/fera/api/version.rb +7 -0
  14. data/lib/fera/api.rb +69 -0
  15. data/lib/fera/app.rb +33 -0
  16. data/lib/fera/models/base.rb +374 -0
  17. data/lib/fera/models/collection.rb +26 -0
  18. data/lib/fera/models/concerns/belongs_to_customer.rb +70 -0
  19. data/lib/fera/models/concerns/belongs_to_order.rb +87 -0
  20. data/lib/fera/models/concerns/belongs_to_product.rb +48 -0
  21. data/lib/fera/models/concerns/belongs_to_review.rb +40 -0
  22. data/lib/fera/models/concerns/belongs_to_submission.rb +39 -0
  23. data/lib/fera/models/concerns/has_many_orders.rb +30 -0
  24. data/lib/fera/models/concerns/has_many_reviews.rb +30 -0
  25. data/lib/fera/models/concerns/has_many_submissions.rb +30 -0
  26. data/lib/fera/models/concerns/has_media.rb +95 -0
  27. data/lib/fera/models/concerns/has_subject.rb +44 -0
  28. data/lib/fera/models/customer.rb +8 -0
  29. data/lib/fera/models/media.rb +55 -0
  30. data/lib/fera/models/order.rb +5 -0
  31. data/lib/fera/models/photo.rb +5 -0
  32. data/lib/fera/models/product.rb +12 -0
  33. data/lib/fera/models/rating.rb +9 -0
  34. data/lib/fera/models/review.rb +18 -0
  35. data/lib/fera/models/store.rb +11 -0
  36. data/lib/fera/models/submission.rb +19 -0
  37. data/lib/fera/models/video.rb +5 -0
  38. data/lib/fera/models/webhook.rb +4 -0
  39. data/lib/fera.rb +4 -0
  40. metadata +167 -0
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fera
4
+ module API
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/fera/api.rb ADDED
@@ -0,0 +1,69 @@
1
+ require 'require_all'
2
+
3
+ require 'active_resource'
4
+
5
+ require_rel "./api/version"
6
+ require_rel "./api"
7
+ require_rel "./app"
8
+ require_rel "./models"
9
+ require_rel "./models/concerns"
10
+
11
+ module Fera
12
+ module API
13
+ class Error < StandardError; end
14
+
15
+ DEFAULT_HEADERS = {
16
+ 'Api-Client' => "fera_ruby_sdk-#{ API::VERSION }",
17
+ }
18
+
19
+ ##
20
+ # @param api_key [String] Public API key, Secret API key or Auth Token (if app)
21
+ def self.configure(api_key, api_url: nil, strict_mode: false)
22
+ previous_base_site = Base.site
23
+ previous_base_headers = Base.headers
24
+
25
+ Base.site = api_url || 'https://api.fera.ai/v3/private'
26
+
27
+ if api_key =~ /^sk_/
28
+ Base.headers['Secret-Key'] = api_key
29
+ elsif api_key =~ /^pk_/
30
+ Base.headers['Public-Key'] = api_key
31
+ else
32
+ Base.headers['Authorization'] = "Bearer #{ api_key }"
33
+ end
34
+
35
+ Base.headers['Strict-Mode'] = strict_mode if strict_mode
36
+
37
+ if block_given?
38
+ begin
39
+ result = yield
40
+ ensure
41
+ Base.site = previous_base_site
42
+ previous_base_headers.each do |key, value|
43
+ Base.headers[key] = value
44
+ end
45
+ end
46
+
47
+ result
48
+ else
49
+ self
50
+ end
51
+
52
+ end
53
+
54
+ def self.revoke_token!(client_id:, client_secret:, auth_token:)
55
+ previous_site = Base.site
56
+
57
+ Base.site = "https://app.fera.ai"
58
+
59
+ body = { client_id: client_id, client_secret: client_secret, token: auth_token }
60
+
61
+ result = Base.connection.post("https://app.fera.ai/oauth/revoke", body.to_json)
62
+
63
+ Base.site = previous_site
64
+
65
+ result
66
+ end
67
+ end
68
+ end
69
+ Fera::Api = Fera::API # @alias
data/lib/fera/app.rb ADDED
@@ -0,0 +1,33 @@
1
+ module Fera
2
+ class App
3
+ def initialize(client_id, client_secret, options = {})
4
+ @client_id = client_id
5
+ @client_secret = client_secret
6
+ @options = options
7
+
8
+ @app_url = options[:app_url] || 'https://app.fera.ai'
9
+ @api_url = options[:api_url] || 'https://api.fera.ai'
10
+ end
11
+
12
+ def revoke_token!(auth_token)
13
+ previous_site = Base.site
14
+
15
+ Base.site = @app_url
16
+
17
+ body = { client_id: @client_id, client_secret: @client_secret, token: auth_token }
18
+
19
+ result = Base.connection.post("#{ @app_url }/oauth/revoke", body.to_json)
20
+
21
+ Base.site = previous_site
22
+
23
+ result
24
+ end
25
+
26
+ def decode_jwt(jwt)
27
+ JWT.decode(jwt, @client_secret, true).try(:first).to_h.with_indifferent_access
28
+ rescue StandardError
29
+ nil
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,374 @@
1
+ require_relative './collection'
2
+
3
+ module Fera
4
+ class Base < ActiveResource::Base
5
+ # include ActiveModel::Dirty
6
+
7
+ attr_reader :last_response, :last_response_body, :last_response_message, :last_response_exception, :options
8
+
9
+ self.collection_parser = ::Fera::Collection
10
+
11
+ class << self
12
+ def belongs_to(name, options = {})
13
+ @belongs_tos = @belongs_tos.to_h.merge(name => options)
14
+ end
15
+
16
+ def belongs_tos; @belongs_tos.to_h; end
17
+
18
+ def has_many(name, options = {})
19
+ @has_manys = @has_manys.to_h.merge(name => options)
20
+ end
21
+
22
+ def has_manys; @has_manys.to_h; end
23
+
24
+ def has_one(name, options = {})
25
+ @has_ones = @has_ones.to_h.merge(name => options)
26
+ end
27
+
28
+ def has_ones; @has_ones.to_h; end
29
+
30
+ def headers
31
+ if _headers_defined?
32
+ _headers
33
+ elsif superclass != Object && superclass.headers
34
+ superclass.headers
35
+ else
36
+ _headers ||= {}
37
+ end
38
+ end
39
+
40
+ def default_params=(default_params)
41
+ @default_params = default_params
42
+ end
43
+
44
+ ##
45
+ # @override to support extra_params
46
+ def create(attributes = {}, extra_params = {})
47
+ self.new(attributes, false).tap { |resource| resource.create(extra_params) }
48
+ end
49
+
50
+ ##
51
+ # @override to fix issue that it's not raising the error and also to support extra_params
52
+ def create!(attributes = {}, extra_params = {})
53
+ self.new(attributes, false).tap { |resource| resource.create!(extra_params) }
54
+ end
55
+
56
+ ##
57
+ # @override to support default params
58
+ def find_every(options)
59
+ super(add_default_params(options))
60
+ end
61
+
62
+ ##
63
+ # @override to support default params
64
+ def find_one(options)
65
+ super(add_default_params(options))
66
+ end
67
+
68
+ ##
69
+ # @override to support default params
70
+ def find_single(scope, options)
71
+ options = add_default_params(options)
72
+ prefix_options, query_options = split_options(options[:params])
73
+ path = element_path(scope, prefix_options, query_options)
74
+
75
+ response = connection.get(path, headers)
76
+ record = instantiate_record(format.decode(response.body), prefix_options)
77
+
78
+ record.set_last_response(response)
79
+ record
80
+ end
81
+
82
+ def new_element_path(prefix_options = {}, extra_params = {})
83
+ url = "#{prefix(prefix_options)}#{collection_name}/new#{format_extension}"
84
+ url += "?#{ extra_params.to_param }" if extra_params.present?
85
+ url
86
+ end
87
+
88
+ private
89
+
90
+ def add_default_params(options)
91
+ if @default_params.present?
92
+ options ||= {}
93
+ options[:params] = options[:params].to_h.merge(@default_params)
94
+ end
95
+
96
+ options
97
+ end
98
+ end
99
+
100
+ def initialize(attributes = nil, persisted = nil, options = {})
101
+ @options = options.to_h
102
+
103
+ dynamic_attributes = attributes.to_h.dup
104
+
105
+ association_keys = self.class.has_manys.keys + self.class.has_ones.keys + self.class.belongs_tos.keys
106
+
107
+ dynamic_attributes.except!(*(association_keys + association_keys.map(&:to_s)))
108
+
109
+ super(dynamic_attributes, persisted)
110
+
111
+ association_keys.each do |name, opts|
112
+ if attributes.key?(name.to_s) || attributes.key?(name.to_sym)
113
+ val = attributes.to_h[name.to_s] || attributes.to_h[name.to_sym]
114
+ self.send("#{ name }=", val) if respond_to?("#{ name }=")
115
+ end
116
+ end if attributes.present?
117
+ end
118
+
119
+ def load(attributes, *args)
120
+ load_result = super(attributes, *args)
121
+
122
+ attributes.each do |attr, val|
123
+ if respond_to?("#{ attr }=".to_sym)
124
+ self.send("#{ attr }=".to_sym, val)
125
+ end
126
+ end
127
+
128
+ @clean_copy = clone_with_nil if persisted? && !options[:cloned]
129
+
130
+ load_result
131
+ end
132
+
133
+ def destroy!
134
+ destroy
135
+ end
136
+
137
+ def created_at=(new_created_at)
138
+ return super(Time.parse(new_created_at)) if new_created_at.is_a?(String)
139
+ super
140
+ end
141
+
142
+ def updated_at=(new_updated_at)
143
+ return super(Time.parse(new_updated_at)) if new_updated_at.is_a?(String)
144
+ super
145
+ end
146
+
147
+ def update(changed_attributes, extra_params = {}, raise = false)
148
+ run_callbacks(:update) do
149
+ connection.put(element_path(prefix_options, extra_params), dynamic_changed_attributes.to_json, self.class.headers).tap do |response|
150
+ load_attributes_from_response(response)
151
+ end
152
+
153
+ load(changed_attributes)
154
+ end
155
+
156
+ true
157
+ rescue ActiveResource::ConnectionError => e
158
+ set_last_response(e)
159
+
160
+ if raise
161
+ raise(ActiveResource::ResourceInvalid.new(last_response, last_response_message.presence))
162
+ end
163
+
164
+ false
165
+ end
166
+
167
+ def update!(changed_attributes, extra_params = {})
168
+ update(changed_attributes, extra_params, true)
169
+ end
170
+
171
+ def valid?(context = nil)
172
+ super()
173
+ end
174
+
175
+ ##
176
+ # @override to add exgtra params
177
+ def create(extra_params = {}, raise = false)
178
+ run_callbacks :create do
179
+
180
+ data = as_json
181
+ (self.class.belongs_tos.merge(self.class.has_ones)).each do |name, opts|
182
+ next unless instance_variable_defined?(:"@#{ name }")
183
+ nested_resource = self.send(name)
184
+ if nested_resource.present? && !nested_resource.persisted?
185
+ nested_resource.validate!
186
+ data[name] = nested_resource.as_json
187
+ end
188
+ end
189
+
190
+ self.class.has_manys.each do |name, opts|
191
+ next unless instance_variable_defined?(:"@#{ name }")
192
+ nested_resource = self.send(name)
193
+
194
+ next if nested_resource.nil?
195
+
196
+ nested_resource.each do |nested_resource_instance|
197
+ next if nested_resource_instance.persisted?
198
+
199
+ nested_resource_instance.validate!
200
+
201
+ data[name] ||= []
202
+ data[name] << nested_resource_instance.as_json
203
+ end
204
+ end
205
+
206
+ connection.post(collection_path(nil, extra_params), data.to_json, self.class.headers).tap do |response|
207
+ self.id = id_from_response(response)
208
+ load_attributes_from_response(response)
209
+ end
210
+ end
211
+
212
+ true
213
+ rescue ActiveResource::ConnectionError => e
214
+ set_last_response(e)
215
+
216
+ if raise
217
+ raise(ActiveResource::ResourceInvalid.new(last_response, last_response_message.presence))
218
+ end
219
+
220
+ false
221
+ end
222
+
223
+ def create!(extra_params = {})
224
+ create(extra_params, true)
225
+ end
226
+
227
+ def save(extra_params = {}, raise = false)
228
+ run_callbacks :save do
229
+ if new?
230
+ create(extra_params, raise) # We'll raise the error below
231
+ else
232
+ # find changes
233
+ changed_attributes = attributes.filter { |key, value| !@clean_copy.attributes.key?(key) || (@clean_copy.attributes[key] != value) || (key == self.class.primary_key) }
234
+ changed_attributes.reject! { |k| k == 'id' }
235
+ return false unless changed_attributes.keys.any?
236
+
237
+ # save
238
+ update(changed_attributes, extra_params, raise)
239
+ end
240
+
241
+ @clean_copy = clone_with_nil # Clear changes
242
+
243
+ self
244
+ end
245
+
246
+ end
247
+
248
+ def save!(extra_params = {})
249
+ save(extra_params, true)
250
+ end
251
+
252
+ def clone_with_nil
253
+ # Clone all attributes except the pk and any nested ARes
254
+ cloned = Hash[attributes.reject { |k, v| k == self.class.primary_key || v.is_a?(ActiveResource::Base) }.map { |k, v| [k, v.clone] }]
255
+ # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which
256
+ # attempts to convert hashes into member objects and arrays into collections of objects. We want
257
+ # the raw objects to be cloned so we bypass load by directly setting the attributes hash.
258
+ resource = self.class.new({}, true, { cloned: true })
259
+ resource.prefix_options = prefix_options
260
+ resource.send :instance_variable_set, '@attributes', cloned
261
+ resource
262
+ end
263
+
264
+ def clone_selected_fields(model, fields)
265
+ fields = fields.is_a?(Array) ? fields : fields.to_s.split(',').map(&:strip)
266
+
267
+ # find fields
268
+ changed_attributes = HashWithIndifferentAccess.new
269
+ changed_attributes[model.class.primary_key] = model.attributes[model.class.primary_key]
270
+ fields.each do |key|
271
+ if key.include?(':')
272
+ clone_sub_fields(model, key, changed_attributes)
273
+ elsif fields.include?(key)
274
+ changed_attributes[key] = model.attributes[key]
275
+ end
276
+ end
277
+
278
+ # create new object
279
+ self.class.new(changed_attributes, true)
280
+ end
281
+
282
+ #
283
+ # Method missing adapters to define public_*_id
284
+ #
285
+
286
+ def method_missing(method_name, *args, &block)
287
+ matcher = method_name.to_s.match(/^(?!is_)([a-z_]+)\?$/) || method_name.to_s.match(/^is_([a-z_]+)\?$/)
288
+ if matcher.present?
289
+ attribute_name = matcher[1]
290
+ return super if attribute_name.blank?
291
+ attribute_name = "is_#{ attribute_name }" unless attribute_name =~ /^is_/
292
+ return super unless known_attribute?(attribute_name.to_s)
293
+ return !!(send(attribute_name.to_sym).presence)
294
+ end
295
+
296
+ super
297
+ end
298
+
299
+ def respond_to_missing?(method_name, include_private = false)
300
+ matcher = method_name.to_s.match(/^(?!is_)([a-z_]+)\?$/) || method_name.to_s.match(/^is_([a-z_]+)\?$/)
301
+ if matcher.present?
302
+ attribute_name = matcher[1]
303
+ return super if attribute_name.blank?
304
+ attribute_name = "is_#{ attribute_name }" unless attribute_name =~ /^is_/
305
+ return true if known_attribute?(attribute_name)
306
+ end
307
+
308
+ super
309
+ end
310
+
311
+ def known_attribute?(attribute_name)
312
+ known_attributes.map(&:to_s).include?(attribute_name.to_s)
313
+ end
314
+
315
+ def set_last_response(result)
316
+ response = if result.is_a?(StandardError)
317
+ @last_response_exception = result
318
+ @last_response_exception.response
319
+ else
320
+ @last_response_exception = nil
321
+ result
322
+ end
323
+
324
+ @last_response = response
325
+ @last_response_body = response.body.present? ? self.class.format.decode(response.body) : nil
326
+ @last_response_message = last_response_body.to_h['message']
327
+ end
328
+
329
+ protected
330
+
331
+ def load_attributes_from_response(response)
332
+ set_last_response(response)
333
+ super(response)
334
+ end
335
+
336
+ private
337
+
338
+ def new_has_many_associated_model(model_class, input = nil)
339
+ model = if input.blank? || input.is_a?(Hash)
340
+ model_class.new(input.to_h.with_indifferent_access, false)
341
+ else
342
+ model_class.instantiate_record(input, input.id.present?)
343
+ end
344
+ model.send("#{ self.class.name.demodulize.underscore }=", self)
345
+ model
346
+ end
347
+
348
+ def element_path(options = nil, extra_params = {})
349
+ self.class.element_path(to_param, options || prefix_options, extra_params)
350
+ end
351
+
352
+ def element_url(options = nil, extra_params = {})
353
+ self.class.element_url(to_param, options || prefix_options, extra_params)
354
+ end
355
+
356
+ def new_element_path(extra_params = {})
357
+ self.class.new_element_path(prefix_options, extra_params)
358
+ end
359
+
360
+ ##
361
+ # @override
362
+ def collection_path(options = nil, extra_params = {})
363
+ self.class.collection_path(options || prefix_options, extra_params)
364
+ end
365
+
366
+ def clone_sub_fields(model, key, changed_attributes)
367
+ sub_fields = key.split(':')
368
+ sub_key = sub_fields.first
369
+ values = model.attributes[sub_key]
370
+ sub_fields = sub_fields.drop(1)
371
+ changed_attributes[sub_key] = values.map { |value| clone_selected_fields(value, sub_fields) }
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,26 @@
1
+ require 'active_resource/collection'
2
+
3
+ module Fera
4
+ class Collection < ActiveResource::Collection
5
+ attr_reader :result_count, :total_count, :page, :total_pages, :page_size, :offset, :limit
6
+
7
+ def initialize(parsed = {})
8
+ @elements = parsed['data']
9
+ @result_count = parsed['result_count']
10
+ @total_count = parsed['total_count']
11
+
12
+ @using_pagination = parsed.key?('page')
13
+
14
+ if @using_pagination
15
+ @page = parsed['page']
16
+ @total_pages = parsed['total_pages']
17
+ @page_size = parsed['page_size']
18
+ else
19
+ @offset = parsed['offset']
20
+ @limit = parsed['limit']
21
+ end
22
+ end
23
+
24
+ def using_pagination?; @using_pagination; end
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ require 'active_support/concern'
2
+
3
+ module Fera
4
+ module BelongsToCustomer
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ belongs_to :customer, class_name: "Fera::Customer"
9
+ end
10
+
11
+ def customer=(customer)
12
+ if customer.is_a?(Customer)
13
+ @customer = customer
14
+ self.attributes['customer_id'] = customer.id
15
+ self.attributes['external_customer_id'] = customer.try(:external_id)
16
+ self.attributes.delete('customer')
17
+ elsif customer.is_a?(Hash)
18
+ customer_id = customer.with_indifferent_access[:id]
19
+
20
+ if customer.with_indifferent_access.key?(:id) # Hash
21
+ if customer_id =~ /^fcus_/
22
+ self.attributes['customer_id'] = customer_id
23
+ else
24
+ self.attributes['external_customer_id'] = customer_id
25
+ end
26
+ end
27
+
28
+ if customer.with_indifferent_access.key?(:external_id) # Hash
29
+ self.attributes['external_customer_id'] = customer.with_indifferent_access[:external_id]
30
+ end
31
+
32
+ @customer = Customer.new(customer, customer_id.present?)
33
+ self.attributes.delete('customer')
34
+ end
35
+
36
+
37
+ @customer
38
+ end
39
+
40
+ def customer_id=(new_id)
41
+ if @customer.present?
42
+ @customer.id = new_id
43
+ end
44
+
45
+ self.attributes['customer_id'] = new_id
46
+ end
47
+
48
+ def external_customer_id=(new_external_id)
49
+ if @customer.present?
50
+ @customer.external_id = new_external_id
51
+ end
52
+
53
+ self.attributes['external_customer_id'] = new_external_id
54
+ end
55
+
56
+ def customer
57
+ if @customer.present?
58
+ @customer
59
+ elsif attributes.key?('customer') && attributes['customer'].present?
60
+ Customer.new(attributes['customer'], true)
61
+ elsif attributes.key?('customer_id') && customer_id.present?
62
+ Customer.find(customer_id)
63
+ elsif attributes.key?('external_customer_id') && external_customer_id.present?
64
+ Customer.find(external_customer_id)
65
+ else
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,87 @@
1
+ require 'active_support/concern'
2
+
3
+ module Fera
4
+ module BelongsToOrder
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ belongs_to :order, class_name: "Fera::Order"
9
+ end
10
+
11
+ def order=(order)
12
+ order_id = if order.is_a?(Order)
13
+ order.id
14
+ else
15
+ order.try(:with_indifferent_access).try(:[], :id)
16
+ end
17
+ external_order_id = if order.is_a?(Order)
18
+ order.external_id
19
+ else
20
+ order.try(:with_indifferent_access).try(:[], :external_id)
21
+ end
22
+ @order = if order.is_a?(Order)
23
+ order
24
+ else
25
+ Order.new(order, order_id.present?)
26
+ end
27
+ self.attributes['order_id'] = order_id
28
+ self.attributes['external_order_id'] = external_order_id
29
+ self.attributes.delete('order')
30
+ @order
31
+ end
32
+
33
+ def order_id=(new_id)
34
+ return new_id if order_id == new_id
35
+
36
+ if new_id.nil?
37
+ reset_order_instance_assoc
38
+ else
39
+ self.attributes['order_id'] = new_id
40
+ end
41
+ end
42
+
43
+ def external_order_id=(new_id)
44
+ return new_id if external_order_id == new_id
45
+
46
+ if new_id.nil?
47
+ reset_order_instance_assoc
48
+ else
49
+ self.attributes['external_order_id'] = new_id
50
+ end
51
+ end
52
+
53
+ def order
54
+ if @order.present?
55
+ @order
56
+ else
57
+ load_order
58
+ end
59
+ end
60
+
61
+ def reload
62
+ reset_order_instance_assoc
63
+ super
64
+ end
65
+
66
+ private
67
+
68
+ def reset_order_instance_assoc
69
+ remove_instance_variable(:@order)
70
+ self.attributes['order_id'] = nil
71
+ self.attributes['external_order_id'] = nil
72
+ end
73
+
74
+ def load_order
75
+ if attributes.key?('order') && attributes['order'].present?
76
+ Order.new(attributes['order'], true)
77
+ elsif attributes.key?('order_id') && order_id.present?
78
+ Order.find(order_id)
79
+ elsif attributes.key?('external_order_id') && external_order_id.present?
80
+ Order.find(external_order_id)
81
+ else
82
+ nil
83
+ end
84
+ end
85
+
86
+ end
87
+ end