fera-api 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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