lockstep_rails 0.3.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (174) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +216 -0
  4. data/Rakefile +31 -0
  5. data/app/assets/config/lockstep_rails_manifest.js +0 -0
  6. data/app/concepts/lockstep/active_records/association.rb +78 -0
  7. data/app/concepts/lockstep/api_record.rb +1118 -0
  8. data/app/concepts/lockstep/api_records/scopes.rb +20 -0
  9. data/app/concepts/lockstep/client.rb +162 -0
  10. data/app/concepts/lockstep/error.rb +50 -0
  11. data/app/concepts/lockstep/exceptions.rb +4 -0
  12. data/app/concepts/lockstep/query.rb +409 -0
  13. data/app/concepts/lockstep/query_methods.rb +75 -0
  14. data/app/concepts/lockstep/relation_array.rb +100 -0
  15. data/app/helpers/types.rb +3 -0
  16. data/app/models/lockstep/account.rb +15 -0
  17. data/app/models/lockstep/connection.rb +19 -0
  18. data/app/models/lockstep/contact.rb +9 -0
  19. data/app/models/lockstep/customer_summary.rb +6 -0
  20. data/app/models/lockstep/invoice.rb +7 -0
  21. data/app/models/lockstep/invoice_summary.rb +6 -0
  22. data/app/models/lockstep/invoices/address.rb +24 -0
  23. data/app/models/lockstep/note.rb +7 -0
  24. data/app/models/lockstep/payment.rb +7 -0
  25. data/app/models/lockstep/payment_summary.rb +6 -0
  26. data/app/models/lockstep/user.rb +10 -0
  27. data/app/platform_api/model_template.rb.erb +27 -0
  28. data/app/platform_api/schema/action_result.rb +15 -0
  29. data/app/platform_api/schema/activity.rb +141 -0
  30. data/app/platform_api/schema/activity_fetch_result.rb +26 -0
  31. data/app/platform_api/schema/activity_stream_item.rb +58 -0
  32. data/app/platform_api/schema/activity_x_ref.rb +37 -0
  33. data/app/platform_api/schema/aging.rb +24 -0
  34. data/app/platform_api/schema/api_key.rb +71 -0
  35. data/app/platform_api/schema/api_key_fetch_result.rb +26 -0
  36. data/app/platform_api/schema/app_enrollment.rb +88 -0
  37. data/app/platform_api/schema/app_enrollment_custom_field.rb +67 -0
  38. data/app/platform_api/schema/app_enrollment_custom_field_fetch_result.rb +26 -0
  39. data/app/platform_api/schema/app_enrollment_fetch_result.rb +26 -0
  40. data/app/platform_api/schema/application.rb +89 -0
  41. data/app/platform_api/schema/application_fetch_result.rb +26 -0
  42. data/app/platform_api/schema/ar_aging_header_info.rb +47 -0
  43. data/app/platform_api/schema/ar_header_info.rb +118 -0
  44. data/app/platform_api/schema/assembly.rb +68 -0
  45. data/app/platform_api/schema/at_risk_invoice_summary.rb +90 -0
  46. data/app/platform_api/schema/at_risk_invoice_summary_fetch_result.rb +26 -0
  47. data/app/platform_api/schema/attachment.rb +92 -0
  48. data/app/platform_api/schema/attachment_fetch_result.rb +26 -0
  49. data/app/platform_api/schema/attachment_header_info.rb +41 -0
  50. data/app/platform_api/schema/batch_sync.rb +18 -0
  51. data/app/platform_api/schema/bulk_currency_conversion.rb +19 -0
  52. data/app/platform_api/schema/cashflow_report.rb +35 -0
  53. data/app/platform_api/schema/code_definition.rb +58 -0
  54. data/app/platform_api/schema/code_definition_fetch_result.rb +26 -0
  55. data/app/platform_api/schema/company.rb +243 -0
  56. data/app/platform_api/schema/company_fetch_result.rb +26 -0
  57. data/app/platform_api/schema/company_sync.rb +142 -0
  58. data/app/platform_api/schema/connector_info.rb +27 -0
  59. data/app/platform_api/schema/constructor_info.rb +128 -0
  60. data/app/platform_api/schema/contact.rb +148 -0
  61. data/app/platform_api/schema/contact_fetch_result.rb +26 -0
  62. data/app/platform_api/schema/contact_sync.rb +109 -0
  63. data/app/platform_api/schema/country.rb +62 -0
  64. data/app/platform_api/schema/country_fetch_result.rb +26 -0
  65. data/app/platform_api/schema/credit_memo_applied.rb +104 -0
  66. data/app/platform_api/schema/credit_memo_applied_fetch_result.rb +26 -0
  67. data/app/platform_api/schema/credit_memo_applied_sync.rb +67 -0
  68. data/app/platform_api/schema/credit_memo_invoice.rb +77 -0
  69. data/app/platform_api/schema/currency.rb +31 -0
  70. data/app/platform_api/schema/currency_fetch_result.rb +26 -0
  71. data/app/platform_api/schema/currency_rate.rb +28 -0
  72. data/app/platform_api/schema/custom_attribute_data.rb +18 -0
  73. data/app/platform_api/schema/custom_attribute_named_argument.rb +24 -0
  74. data/app/platform_api/schema/custom_attribute_typed_argument.rb +16 -0
  75. data/app/platform_api/schema/custom_field_definition.rb +76 -0
  76. data/app/platform_api/schema/custom_field_definition_fetch_result.rb +26 -0
  77. data/app/platform_api/schema/custom_field_sync.rb +71 -0
  78. data/app/platform_api/schema/custom_field_value.rb +75 -0
  79. data/app/platform_api/schema/custom_field_value_fetch_result.rb +26 -0
  80. data/app/platform_api/schema/customer_details.rb +98 -0
  81. data/app/platform_api/schema/customer_details_payment.rb +60 -0
  82. data/app/platform_api/schema/customer_summary.rb +88 -0
  83. data/app/platform_api/schema/customer_summary_fetch_result.rb +26 -0
  84. data/app/platform_api/schema/daily_sales_outstanding_report.rb +25 -0
  85. data/app/platform_api/schema/developer_account_submit.rb +23 -0
  86. data/app/platform_api/schema/email.rb +160 -0
  87. data/app/platform_api/schema/email_fetch_result.rb +26 -0
  88. data/app/platform_api/schema/erp.rb +23 -0
  89. data/app/platform_api/schema/erp_fetch_result.rb +26 -0
  90. data/app/platform_api/schema/erp_info.rb +18 -0
  91. data/app/platform_api/schema/erp_info_data.rb +23 -0
  92. data/app/platform_api/schema/event_info.rb +59 -0
  93. data/app/platform_api/schema/exception.rb +41 -0
  94. data/app/platform_api/schema/field_info.rb +105 -0
  95. data/app/platform_api/schema/financial_account.rb +90 -0
  96. data/app/platform_api/schema/financial_account_balance_history.rb +90 -0
  97. data/app/platform_api/schema/financial_account_balance_history_fetch_result.rb +26 -0
  98. data/app/platform_api/schema/financial_account_fetch_result.rb +26 -0
  99. data/app/platform_api/schema/financial_year_setting.rb +74 -0
  100. data/app/platform_api/schema/financial_year_setting_fetch_result.rb +26 -0
  101. data/app/platform_api/schema/invite.rb +25 -0
  102. data/app/platform_api/schema/invite_data.rb +18 -0
  103. data/app/platform_api/schema/invite_submit.rb +15 -0
  104. data/app/platform_api/schema/invoice.rb +226 -0
  105. data/app/platform_api/schema/invoice_address.rb +97 -0
  106. data/app/platform_api/schema/invoice_fetch_result.rb +29 -0
  107. data/app/platform_api/schema/invoice_history.rb +188 -0
  108. data/app/platform_api/schema/invoice_history_fetch_result.rb +26 -0
  109. data/app/platform_api/schema/invoice_line.rb +142 -0
  110. data/app/platform_api/schema/invoice_line_sync.rb +208 -0
  111. data/app/platform_api/schema/invoice_payment_detail.rb +65 -0
  112. data/app/platform_api/schema/invoice_summary.rb +85 -0
  113. data/app/platform_api/schema/invoice_summary_fetch_result.rb +26 -0
  114. data/app/platform_api/schema/invoice_sync.rb +280 -0
  115. data/app/platform_api/schema/lead.rb +32 -0
  116. data/app/platform_api/schema/member_info.rb +36 -0
  117. data/app/platform_api/schema/method_base.rb +128 -0
  118. data/app/platform_api/schema/method_info.rb +137 -0
  119. data/app/platform_api/schema/module.rb +44 -0
  120. data/app/platform_api/schema/module_handle.rb +15 -0
  121. data/app/platform_api/schema/note.rb +72 -0
  122. data/app/platform_api/schema/note_fetch_result.rb +26 -0
  123. data/app/platform_api/schema/parameter_info.rb +64 -0
  124. data/app/platform_api/schema/payment.rb +147 -0
  125. data/app/platform_api/schema/payment_applied.rb +95 -0
  126. data/app/platform_api/schema/payment_applied_fetch_result.rb +26 -0
  127. data/app/platform_api/schema/payment_applied_sync.rb +74 -0
  128. data/app/platform_api/schema/payment_detail.rb +110 -0
  129. data/app/platform_api/schema/payment_detail_fetch_result.rb +26 -0
  130. data/app/platform_api/schema/payment_detail_header.rb +43 -0
  131. data/app/platform_api/schema/payment_fetch_result.rb +26 -0
  132. data/app/platform_api/schema/payment_summary.rb +79 -0
  133. data/app/platform_api/schema/payment_summary_fetch_result.rb +26 -0
  134. data/app/platform_api/schema/payment_sync.rb +112 -0
  135. data/app/platform_api/schema/problem_details.rb +31 -0
  136. data/app/platform_api/schema/property_info.rb +60 -0
  137. data/app/platform_api/schema/provisioning.rb +28 -0
  138. data/app/platform_api/schema/provisioning_finalize_request.rb +28 -0
  139. data/app/platform_api/schema/provisioning_response.rb +42 -0
  140. data/app/platform_api/schema/risk_rate.rb +57 -0
  141. data/app/platform_api/schema/runtime_field_handle.rb +13 -0
  142. data/app/platform_api/schema/runtime_method_handle.rb +13 -0
  143. data/app/platform_api/schema/runtime_type_handle.rb +13 -0
  144. data/app/platform_api/schema/state.rb +22 -0
  145. data/app/platform_api/schema/state_fetch_result.rb +26 -0
  146. data/app/platform_api/schema/status.rb +72 -0
  147. data/app/platform_api/schema/struct_layout_attribute.rb +16 -0
  148. data/app/platform_api/schema/sync_entity_result.rb +34 -0
  149. data/app/platform_api/schema/sync_request.rb +71 -0
  150. data/app/platform_api/schema/sync_request_fetch_result.rb +26 -0
  151. data/app/platform_api/schema/sync_submit.rb +15 -0
  152. data/app/platform_api/schema/test_argument_exception.rb +41 -0
  153. data/app/platform_api/schema/test_timeout_exception.rb +41 -0
  154. data/app/platform_api/schema/transfer_owner.rb +16 -0
  155. data/app/platform_api/schema/transfer_owner_submit.rb +15 -0
  156. data/app/platform_api/schema/type.rb +278 -0
  157. data/app/platform_api/schema/type_info.rb +287 -0
  158. data/app/platform_api/schema/uri.rb +15 -0
  159. data/app/platform_api/schema/user_account.rb +152 -0
  160. data/app/platform_api/schema/user_account_fetch_result.rb +26 -0
  161. data/app/platform_api/schema/user_role.rb +50 -0
  162. data/app/platform_api/schema/user_role_fetch_result.rb +26 -0
  163. data/app/platform_api/schema/webhook.rb +96 -0
  164. data/app/platform_api/schema/webhook_fetch_result.rb +26 -0
  165. data/app/platform_api/schema/webhook_history_table_storage.rb +64 -0
  166. data/app/platform_api/schema/webhook_history_table_storage_fetch_result.rb +26 -0
  167. data/app/platform_api/swagger.json +22056 -0
  168. data/config/routes.rb +2 -0
  169. data/lib/lockstep_rails/engine.rb +4 -0
  170. data/lib/lockstep_rails/version.rb +3 -0
  171. data/lib/lockstep_rails.rb +5 -0
  172. data/lib/tasks/lockstep_rails_tasks.rake +4 -0
  173. data/lib/tasks/update_api_schema.rake +159 -0
  174. metadata +230 -0
@@ -0,0 +1,1118 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ require "active_model"
4
+ require "erb"
5
+ require "dry-types"
6
+ require "json"
7
+ require "active_support/hash_with_indifferent_access"
8
+ require "lockstep/query"
9
+ require "lockstep/query_methods"
10
+ require "lockstep/error"
11
+ require "lockstep/exceptions"
12
+ require "lockstep/relation_array"
13
+
14
+ module Lockstep
15
+ class ApiRecord
16
+ include Lockstep::ApiRecords::Scopes
17
+ # Lockstep::ApiRecord provides an easy way to use Ruby to interace with a api.lockstep.io backend
18
+ # Usage:
19
+ # class Post < Lockstep::ApiRecord
20
+ # fields :title, :author, :body
21
+ # end
22
+
23
+ # @@has_many_relations = {}.with_indifferent_access
24
+ # @@belongs_to_relations = {}.with_indifferent_access
25
+
26
+ include ActiveModel::Validations
27
+ include ActiveModel::Validations::Callbacks
28
+ include ActiveModel::Conversion
29
+ include ActiveModel::AttributeMethods
30
+ extend ActiveModel::Naming
31
+ extend ActiveModel::Callbacks
32
+ HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
33
+
34
+ attr_accessor :error_instances
35
+
36
+ define_model_callbacks :save, :create, :update, :destroy
37
+
38
+ validate :validate_enum
39
+
40
+ # Instantiates a Lockstep::ApiRecord object
41
+ #
42
+ # @params [Hash], [Boolean] a `Hash` of attributes and a `Boolean` that should be false only if the object already exists
43
+ # @return [Lockstep::ApiRecord] an object that subclasses `Parseresource::Base`
44
+ def initialize(attributes = {}, new = true)
45
+ #attributes = HashWithIndifferentAccess.new(attributes)
46
+
47
+ if new
48
+ @unsaved_attributes = attributes
49
+ @unsaved_attributes.stringify_keys!
50
+ else
51
+ @unsaved_attributes = {}
52
+ end
53
+ @attributes = {}
54
+ self.error_instances = []
55
+
56
+ attributes.each do |k, v|
57
+ # Typecast using dry-types
58
+ if (type = schema[k])
59
+ attributes[k] = type[v]
60
+ elsif v.present? and (enum = enum_config[k]).present? and enum.keys.include?(v.to_s)
61
+ attributes[k] = enum[v]
62
+ end
63
+ end
64
+
65
+ self.attributes.merge!(attributes)
66
+ self.attributes unless self.attributes.empty?
67
+ create_setters_and_getters!
68
+ end
69
+
70
+ def self.schema
71
+ @schema ||= {}.with_indifferent_access
72
+ end
73
+
74
+ def schema
75
+ self.class.schema
76
+ end
77
+
78
+ # Explicitly adds a field to the model.
79
+ #
80
+ # @param [Symbol] name the name of the field, eg `:author`.
81
+ # @param [Boolean] val the return value of the field. Only use this within the class.
82
+ def self.field(fname, type = nil)
83
+ schema[fname] = type
84
+
85
+ fname = fname.to_sym
86
+ class_eval do
87
+ define_method(fname) do
88
+ val = get_attribute("#{fname}")
89
+
90
+ # If enum, substitute with the enum key
91
+ if val.present? && (enum = enum_config[fname]).present?
92
+ val = enum.key(val)
93
+ end
94
+
95
+ val
96
+ end
97
+ end
98
+ unless self.respond_to? "#{fname}="
99
+ class_eval do
100
+ define_method("#{fname}=") do |val|
101
+ set_attribute("#{fname}", val)
102
+
103
+ val
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ # Add multiple fields in one line. Same as `#field`, but accepts multiple args.
110
+ #
111
+ # @param [Array] *args an array of `Symbol`s, `eg :author, :body, :title`.
112
+ def self.fields(*args)
113
+ args.each { |f| field(f) }
114
+ end
115
+
116
+ def belongs_to_relations
117
+ @belongs_to_relations ||= {}
118
+ end
119
+
120
+ # Similar to its ActiveRecord counterpart.
121
+ #
122
+ # @param [Hash] options Added so that you can specify :class_name => '...'. It does nothing at all, but helps you write self-documenting code.
123
+ # @primary_key is the attribute of the referenced :class_name
124
+ # @foreign_key is the attribute of the parent class
125
+ def self.belongs_to(parent, config = {})
126
+ config = config.with_indifferent_access
127
+ class_name = config[:class_name]
128
+ raise "Class name cannot be empty in #{parent}: #{self.name}" if class_name.blank?
129
+
130
+ included = config[:included] || false
131
+ primary_key = config[:primary_key]
132
+ foreign_key = config[:foreign_key]
133
+ polymorphic = config[:polymorphic]
134
+ loader = config[:loader]
135
+
136
+ primary_key ||= class_name.constantize.id_ref
137
+ foreign_key ||= class_name.constantize.id_ref
138
+ field(parent)
139
+ belongs_to_relations[parent] = {
140
+ name: parent, class_name: class_name,
141
+ included: included, primary_key: primary_key, foreign_key: foreign_key,
142
+ loader: loader, polymorphic: polymorphic,
143
+ }
144
+
145
+ # define_method("build_#{parent}") do |attributes_hash|
146
+ # build_belongs_to_association(parent, attributes_hash)
147
+ # end
148
+ end
149
+
150
+ # Creates setter and getter in order access the specified relation for this Model
151
+ #
152
+ # @param [Hash] options Added so that you can specify :class_name => '...'. It does nothing at all, but helps you write self-documenting code.
153
+ # @primary_key is the attribute of the parent_class
154
+ # @foreign_key is the attribute of the referenced :class_name
155
+ # @polymorphic is used to assign polymorphic association with the related ApiModel. It expects a hash with the key-value, or a lambda function which returns a hash
156
+ def self.has_many(parent, config = {})
157
+ config = config.with_indifferent_access
158
+ class_name = config[:class_name]
159
+ raise "Class name cannot be empty in #{parent}: #{self.name}" if class_name.blank?
160
+
161
+ included = config[:included] || false
162
+ primary_key = config[:primary_key]
163
+ foreign_key = config[:foreign_key]
164
+ polymorphic = config[:polymorphic]
165
+ loader = config[:loader]
166
+
167
+ primary_key ||= self.id_ref
168
+ foreign_key ||= self.id_ref
169
+ field(parent)
170
+ has_many_relations[parent] = {
171
+ name: parent, class_name: class_name, included: included,
172
+ primary_key: primary_key, foreign_key: foreign_key, polymorphic: polymorphic,
173
+ loader: loader,
174
+ }
175
+ end
176
+
177
+ def self.load_schema(schema)
178
+ schema.schema.each do |field, type|
179
+ field(field, type)
180
+ end
181
+
182
+ schema.belongs_to_relations.each do |relation, config|
183
+ params = {}
184
+ config.except(:name).each { |k, v| params[k.to_sym] = v }
185
+ self.belongs_to(relation, params)
186
+ end
187
+
188
+ schema.has_many_relations.each do |relation, config|
189
+ params = {}
190
+ config.except(:name).each { |k, v| params[k.to_sym] = v }
191
+ self.has_many(relation, params)
192
+ end
193
+ end
194
+
195
+ def to_pointer
196
+ klass_name = self.class.model_name.to_s
197
+ { "__type" => "Pointer", "className" => klass_name.to_s, self.id_ref => self.id }
198
+ end
199
+
200
+ def self.to_date_object(date)
201
+ date = date.to_time if date.respond_to?(:to_time)
202
+ { "__type" => "Date", "iso" => date.getutc.iso8601 } if date && (date.is_a?(Date) || date.is_a?(DateTime) || date.is_a?(Time))
203
+ end
204
+
205
+ # Creates setter methods for model fields
206
+ def create_setters!(k, v)
207
+ unless self.respond_to? "#{k}="
208
+ self.class.send(:define_method, "#{k}=") do |val|
209
+ set_attribute("#{k}", val)
210
+
211
+ val
212
+ end
213
+ end
214
+ end
215
+
216
+ def method_missing(method, *args, &block)
217
+ raise StandardError.new("#{method} has not been defined for #{self.class.name}")
218
+ # super
219
+ end
220
+
221
+ def self.method_missing(method_name, *args)
222
+ method_name = method_name.to_s
223
+ if method_name.start_with?("find_by_")
224
+ attrib = method_name.gsub(/^find_by_/, "")
225
+ finder_name = "find_all_by_#{attrib}"
226
+
227
+ define_singleton_method(finder_name) do |target_value|
228
+ where({ attrib.to_sym => target_value }).first
229
+ end
230
+
231
+ send(finder_name, args[0])
232
+ elsif method_name.start_with?("find_all_by_")
233
+ attrib = method_name.gsub(/^find_all_by_/, "")
234
+ finder_name = "find_all_by_#{attrib}"
235
+
236
+ define_singleton_method(finder_name) do |target_value|
237
+ where({ attrib.to_sym => target_value }).all
238
+ end
239
+
240
+ send(finder_name, args[0])
241
+ else
242
+ super(method_name.to_sym, *args)
243
+ end
244
+ end
245
+
246
+ # Creates getter methods for model fields
247
+ def create_getters!(k, v)
248
+ unless self.respond_to? "#{k}"
249
+ self.class.send(:define_method, "#{k}") do
250
+ get_attribute("#{k}")
251
+ end
252
+ end
253
+ end
254
+
255
+ def create_setters_and_getters!
256
+ @attributes.each_pair do |k, v|
257
+ create_setters!(k, v)
258
+ create_getters!(k, v)
259
+ end
260
+ end
261
+
262
+ # @@settings ||= nil
263
+
264
+ # Explicitly set Parse.com API keys.
265
+ #
266
+ # @param [String] app_id the Application ID of your Parse database
267
+ # @param [String] master_key the Master Key of your Parse database
268
+ # def self.load!(app_id, master_key)
269
+ # @@settings = { "app_id" => app_id, "master_key" => master_key }
270
+ # end
271
+
272
+ # def self.settings
273
+ # load_settings
274
+ # end
275
+
276
+ # # Gets the current class's model name for the URI
277
+ # def self.model_name_uri
278
+ # # This is a workaround to allow the user to specify a custom class
279
+ # if defined?(self.parse_class_name)
280
+ # "#{self.parse_class_name}"
281
+ # else
282
+ # "#{self.model_name.to_s}"
283
+ # end
284
+ # end
285
+
286
+ def self.id_ref=(val)
287
+ @id_ref = val
288
+ end
289
+
290
+ def self.id_ref
291
+ raise StandardError.new("id_ref has not been defined for #{self.name}") if @id_ref.blank?
292
+
293
+ @id_ref
294
+ end
295
+
296
+ # Alias for id_ref. Used by polymorphic association
297
+ def self.primary_key
298
+ self.id_ref
299
+ end
300
+
301
+ def id_ref
302
+ self.class.id_ref
303
+ end
304
+
305
+ def self.model_name_uri=(val)
306
+ @model_name_uri = val
307
+
308
+ @model_name_uri
309
+ end
310
+
311
+ def self.model_name_uri
312
+ @model_name_uri
313
+ end
314
+
315
+ def self.config
316
+ @config ||= Rails.application.config_for(:lockstep_client)
317
+ end
318
+
319
+ # Gets the current class's Lockstep.io base_uri
320
+ def self.model_base_uri
321
+ raise StandardError.new("Cannot establish connection for auto-generated Schema. Create a new model if you want to retrieve data from Lockstep Platform") if self.name.starts_with?("Schema::")
322
+ raise StandardError.new("URL Path is not defined for #{self.name}") if model_name_uri.blank?
323
+
324
+ base_url = config[:base_url]
325
+ base_url += "/" unless base_url.ends_with?("/")
326
+ base_url += model_name_uri
327
+ base_url += "/" unless base_url.ends_with?("/")
328
+ base_url
329
+ end
330
+
331
+ # Gets the current instance's parent class's Parse.com base_uri
332
+ def model_base_uri
333
+ self.class.send(:model_base_uri)
334
+ end
335
+
336
+ # Creates a RESTful resource
337
+ # sends requests to [base_uri]/[classname]
338
+ #
339
+ def self.resource
340
+ # load_settings
341
+
342
+ #refactor to settings['app_id'] etc
343
+ # app_id = @@settings['app_id']
344
+ # master_key = @@settings['master_key']
345
+ # RestClient::Resource.new(self.model_base_uri, app_id, master_key)
346
+ Lockstep::Client.new(self.model_base_uri)
347
+ end
348
+
349
+ def self.query_path=(value)
350
+ @query_path = value
351
+ end
352
+
353
+ def self.query_path
354
+ @query_path || "query"
355
+ end
356
+
357
+ # Batch requests
358
+ # Sends multiple requests to /batch
359
+ # Set slice_size to send larger batches. Defaults to 20 to prevent timeouts.
360
+ # Parse doesn't support batches of over 20.
361
+ #
362
+ # def self.batch_save(save_objects, slice_size = 20, method = nil)
363
+ # return true if save_objects.blank?
364
+ #
365
+ # res = self.resource
366
+ #
367
+ # # Batch saves seem to fail if they're too big. We'll slice it up into multiple posts if they are.
368
+ # save_objects.each_slice(slice_size) do |objects|
369
+ # # attributes_for_saving
370
+ # batch_json = { "requests" => [] }
371
+ #
372
+ # objects.each do |item|
373
+ # method ||= (item.new?) ? "POST" : "PATCH"
374
+ # object_path = "/1/#{item.class.model_name_uri}"
375
+ # object_path = "#{object_path}/#{item.id}" if item.id
376
+ # json = {
377
+ # "method" => method,
378
+ # "path" => object_path
379
+ # }
380
+ # json["body"] = item.attributes_for_saving unless method == "DELETE"
381
+ # batch_json["requests"] << json
382
+ # end
383
+ # res.post(batch_json.to_json, :content_type => "application/json") do |resp, req, res, &block|
384
+ # response = JSON.parse(resp) rescue nil
385
+ # if resp.code == 400
386
+ # return false
387
+ # end
388
+ # if response && response.is_a?(Array) && response.length == objects.length
389
+ # merge_all_attributes(objects, response) unless method == "DELETE"
390
+ # end
391
+ # end
392
+ # end
393
+ # true
394
+ # end
395
+
396
+ def self.merge_all_attributes(objects, response)
397
+ objects.each_with_index do |item, index|
398
+ next unless response[index]
399
+ new_attributes = response[index].transform_keys { |key| key.underscore }
400
+ item.merge_attributes(new_attributes)
401
+ end
402
+
403
+ true
404
+ end
405
+
406
+ # def self.save_all(objects)
407
+ # batch_save(objects)
408
+ # end
409
+
410
+ # def self.destroy_all(objects = nil)
411
+ # objects ||= self.all
412
+ # batch_save(objects, 20, "DELETE")
413
+ # end
414
+
415
+ # def self.delete_all(o)
416
+ # raise StandardError.new("delete_all doesn't exist. Did you mean destroy_all?")
417
+ # end
418
+
419
+ def self.bulk_import(new_objects, slice_size = 20)
420
+ return true if new_objects.blank?
421
+
422
+ # Batch saves seem to fail if they're too big. We'll slice it up into multiple posts if they are.
423
+ new_objects.each_slice(slice_size) do |objects|
424
+ # attributes_for_saving
425
+ batch_json = []
426
+
427
+ objects.each do |item|
428
+ raise StandardError.new("Bulk Import cannot only create records at the moment. It cannot update records") unless item.new?
429
+
430
+ batch_json << item.attributes_for_saving.transform_keys { |key| key.camelize(:lower) }
431
+ end
432
+
433
+ resp = self.resource.post("", body: batch_json)
434
+ # TODO attach errors if resp code is 400
435
+
436
+ if resp.code.to_s == "400"
437
+ # Error format in JSON
438
+ # "errors": {
439
+ # "[0].EmailAddress": [
440
+ # "The EmailAddress field is not a valid e-mail address."
441
+ # ]
442
+ # }
443
+ error_response = JSON.parse(resp.body)
444
+ errors = error_response["errors"]
445
+ errors.each do |key, messages|
446
+ splits = key.split(".")
447
+ attribute = splits.last&.underscore
448
+ # Remove the [] from the first split to get the position in integer
449
+ position = splits.first[1..(splits.first.size - 2)].to_i
450
+ messages.each do |message|
451
+ new_objects[position].errors.add attribute, ": #{message}"
452
+ end
453
+ end
454
+ return false
455
+ elsif resp.code.to_s != "200"
456
+ return false
457
+ end
458
+
459
+ response = JSON.parse(resp.body)
460
+ if response && response.is_a?(Array) && response.length == objects.length
461
+ # return response.map { |item|
462
+ # Lockstep::Contact.new(item.transform_keys { |key| key.underscore }, false)
463
+ # }
464
+ merge_all_attributes(objects, response)
465
+ end
466
+ end
467
+ true
468
+ end
469
+
470
+ # def self.load_settings
471
+ # @@settings ||= begin
472
+ # path = "config/parse_resource.yml"
473
+ # environment = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env : ENV["RACK_ENV"]
474
+ # if FileTest.exist? (path)
475
+ # YAML.load(ERB.new(File.new(path).read).result)[environment]
476
+ # elsif ENV["PARSE_RESOURCE_APPLICATION_ID"] && ENV["PARSE_RESOURCE_MASTER_KEY"]
477
+ # settings = HashWithIndifferentAccess.new
478
+ # settings['app_id'] = ENV["PARSE_RESOURCE_APPLICATION_ID"]
479
+ # settings['master_key'] = ENV["PARSE_RESOURCE_MASTER_KEY"]
480
+ # settings
481
+ # else
482
+ # raise "Cannot load parse_resource.yml and API keys are not set in environment"
483
+ # end
484
+ # end
485
+ # @@settings
486
+ # end
487
+
488
+ # Creates a RESTful resource for file uploads
489
+ # sends requests to [base_uri]/files
490
+ #
491
+ # def self.upload(file_instance, filename, options={})
492
+ # load_settings
493
+ #
494
+ # base_uri = "https://api.parse.com/1/files"
495
+ #
496
+ # #refactor to settings['app_id'] etc
497
+ # app_id = @@settings['app_id']
498
+ # master_key = @@settings['master_key']
499
+ #
500
+ # options[:content_type] ||= 'image/jpg' # TODO: Guess mime type here.
501
+ # file_instance = File.new(file_instance, 'rb') if file_instance.is_a? String
502
+ #
503
+ # filename = filename.parameterize
504
+ #
505
+ # private_resource = RestClient::Resource.new "#{base_uri}/#{filename}", app_id, master_key
506
+ # private_resource.post(file_instance, options) do |resp, req, res, &block|
507
+ # return false if resp.code == 400
508
+ # return JSON.parse(resp) rescue {"code" => 0, "error" => "unknown error"}
509
+ # end
510
+ # false
511
+ # end
512
+
513
+ # Find a Lockstep::ApiRecord object by ID
514
+ #
515
+ # @param [String] id the ID of the Parse object you want to find.
516
+ # @return [Lockstep::ApiRecord] an object that subclasses Lockstep::ApiRecord.
517
+ def self.find(id)
518
+ raise Lockstep::Exceptions::RecordNotFound, "Couldn't find #{name} without an ID" if id.blank?
519
+ record = where(self.id_ref => id).first
520
+ raise Lockstep::Exceptions::RecordNotFound, "Couldn't find #{name} with id: #{id}" if record.blank?
521
+ record
522
+ end
523
+
524
+ # Find a Lockstep::ApiRecord object by given key/value pair
525
+ #
526
+ def self.find_by(*args)
527
+ raise Lockstep::Exceptions::RecordNotFound, "Couldn't find an object without arguments" if args.blank?
528
+ key, value = args.first.first
529
+ unless valid_attribute?(key, raise_exception: true)
530
+ raise StandardError.new("Attribute '#{key}' has not been defined for #{self.name}")
531
+ end
532
+
533
+ record = where(key => value).first
534
+ record
535
+ end
536
+
537
+ # Find a Lockstep::ApiRecord object by chaining #where method calls.
538
+ #
539
+ def self.where(*args)
540
+ query_builder.where(*args)
541
+ end
542
+
543
+ def self.execute
544
+ query_builder.execute
545
+ end
546
+
547
+ include Lockstep::QueryMethods
548
+
549
+ def self.chunk(attribute)
550
+ query_builder.chunk(attribute)
551
+ end
552
+
553
+ # Create a Lockstep::ApiRecord object.
554
+ #
555
+ # @param [Hash] attributes a `Hash` of attributes
556
+ # @return [Lockstep::ApiRecord] an object that subclasses `Lockstep::ApiRecord`. Or returns `false` if object fails to save.
557
+ def self.create(attributes = {})
558
+ attributes = HashWithIndifferentAccess.new(attributes)
559
+ obj = new(attributes)
560
+ obj.save
561
+ obj
562
+ end
563
+
564
+ # Replaced with a batch destroy_all method.
565
+ # def self.destroy_all(all)
566
+ # all.each do |object|
567
+ # object.destroy
568
+ # end
569
+ # end
570
+
571
+ def self.class_attributes
572
+ @class_attributes ||= {}
573
+ end
574
+
575
+ def self.valid_attribute?(key, raise_exception: false)
576
+ # Valid only if the record is not an API record.
577
+ # Default scopes build queries using ApiRecord to avoid conflicts. In this case, the query results in an
578
+ # exception as the fields wouldn't have been defined in the ApiRecord
579
+ return true if self.name == "Lockstep::ApiRecord"
580
+
581
+ attr = key.to_s
582
+ Lockstep::Query::PREDICATES.keys.each do |predicate|
583
+ if attr.end_with?(predicate)
584
+ attr = attr.gsub(predicate, "")
585
+ break
586
+ end
587
+ end
588
+ valid = schema.has_key?(attr)
589
+ if raise_exception && !valid
590
+ raise StandardError.new("Attribute '#{attr}' has not been defined for #{self.name}")
591
+ end
592
+
593
+ valid
594
+ end
595
+
596
+ def persisted?
597
+ if id
598
+ true
599
+ else
600
+ false
601
+ end
602
+ end
603
+
604
+ def new?
605
+ !persisted?
606
+ end
607
+
608
+ # delegate from Class method
609
+ def resource
610
+ self.class.resource
611
+ end
612
+
613
+ # create RESTful resource for the specific Parse object
614
+ # sends requests to [base_uri]/[classname]/[objectId]
615
+ def instance_resource
616
+ self.class.resource["#{self.id}"]
617
+ end
618
+
619
+ def pointerize(hash)
620
+ new_hash = {}
621
+ hash.each do |k, v|
622
+ if v.respond_to?(:to_pointer)
623
+ new_hash[k] = v.to_pointer
624
+ elsif v.is_a?(Date) || v.is_a?(Time) || v.is_a?(DateTime)
625
+ new_hash[k] = self.class.to_date_object(v)
626
+ else
627
+ new_hash[k] = v
628
+ end
629
+ end
630
+ new_hash
631
+ end
632
+
633
+ def save
634
+ if valid?
635
+ run_callbacks :save do
636
+ if new?
637
+ create
638
+ else
639
+ update
640
+ end
641
+ end
642
+ else
643
+ false
644
+ end
645
+ rescue
646
+ false
647
+ end
648
+
649
+ def create
650
+ attrs = attributes_for_saving.transform_keys { |key| key.camelize(:lower) }
651
+ resp = self.resource.post("", body: [attrs])
652
+ result = post_result(resp)
653
+ end
654
+
655
+ def update(attributes = {})
656
+ attributes = HashWithIndifferentAccess.new(attributes)
657
+
658
+ @unsaved_attributes.merge!(attributes)
659
+ # put_attrs = attributes_for_saving.to_json
660
+
661
+ attrs = attributes_for_saving.transform_keys { |key| key.camelize(:lower) }
662
+ resp = self.resource.patch(self.id, body: attrs)
663
+ result = post_result(resp)
664
+ end
665
+
666
+ # Merges in the return value of a save and resets the unsaved_attributes
667
+ def merge_attributes(results)
668
+ results.transform_keys! { |key| key.underscore }
669
+ @attributes.merge!(results)
670
+ @attributes.merge!(@unsaved_attributes)
671
+
672
+ merge_relations
673
+ @unsaved_attributes = {}
674
+
675
+ create_setters_and_getters!
676
+ @attributes
677
+ end
678
+
679
+ def merge_relations
680
+ # KK 11-17-2012 The response after creation does not return full description of
681
+ # the object nor the relations it contains. Make another request here.
682
+ # TODO: @@has_many_relations structure has been changed from array to hash, need to evaluate the impact here
683
+ if has_many_relations.keys.map { |relation| relation.to_s.to_sym }
684
+ #todo: make this a little smarter by checking if there are any Pointer objects in the objects attributes.
685
+ # @attributes = self.class.to_s.constantize.where(:objectId => @attributes[self.id_ref]).first.attributes
686
+ @attributes = self.class.to_s.constantize.where(self.id_ref => @attributes[self.id_ref]).first.attributes
687
+ end
688
+ end
689
+
690
+ def post_result(resp)
691
+ if resp.code.to_s == "200" || resp.code.to_s == "201"
692
+ body = JSON.parse(resp.body)
693
+ # Create method always responds with an array, whereas update responds with the object
694
+ body = body.first if body.is_a?(Array)
695
+
696
+ merge_attributes(body)
697
+
698
+ return true
699
+ elsif resp.code.to_s == "400"
700
+ error_response = JSON.parse(resp.body)
701
+ errors = error_response["errors"]
702
+ errors.each do |key, messages|
703
+ attribute = key.split(".").last&.underscore
704
+ messages.each do |message|
705
+ self.errors.add attribute, ": #{message}"
706
+ end
707
+ end
708
+ else
709
+ error_response = JSON.parse(resp.body)
710
+ if error_response["error"]
711
+ pe = LockstepError.new(error_response["code"], error_response["error"])
712
+ else
713
+ pe = LockstepError.new(resp.code.to_s)
714
+ end
715
+ self.errors.add(pe.code.to_s.to_sym, pe.msg)
716
+ self.error_instances << pe
717
+ return false
718
+ end
719
+ end
720
+
721
+ def attributes_for_saving
722
+ @unsaved_attributes = pointerize(@unsaved_attributes)
723
+ put_attrs = @unsaved_attributes
724
+
725
+ put_attrs = relations_for_saving(put_attrs)
726
+
727
+ put_attrs.delete(self.id_ref)
728
+ put_attrs.delete("created")
729
+ put_attrs.delete("modified")
730
+ put_attrs
731
+ end
732
+
733
+ def relations_for_saving(put_attrs)
734
+ all_add_item_queries = {}
735
+ all_remove_item_queries = {}
736
+ @unsaved_attributes.each_pair do |key, value|
737
+ next if !value.is_a? Array
738
+
739
+ # Go through the array in unsaved and check if they are in array in attributes (saved stuff)
740
+ add_item_ops = []
741
+ @unsaved_attributes[key].each do |item|
742
+ found_item_in_saved = false
743
+ @attributes[key].each do |item_in_saved|
744
+ if !!(defined? item.attributes) && item.attributes[self.id_ref] == item_in_saved.attributes[self.id_ref]
745
+ found_item_in_saved = true
746
+ end
747
+ end
748
+
749
+ if !found_item_in_saved && !!(defined? item.id)
750
+ # need to send additem operation to parse
751
+ put_attrs.delete(key) # arrays should not be sent along with REST to parse api
752
+ add_item_ops << { "__type" => "Pointer", "className" => item.class.to_s, self.id_ref => item.id }
753
+ end
754
+ end
755
+ all_add_item_queries.merge!({ key => { "__op" => "Add", "objects" => add_item_ops } }) if !add_item_ops.empty?
756
+
757
+ # Go through saved and if it isn't in unsaved perform a removeitem operation
758
+ remove_item_ops = []
759
+ unless @unsaved_attributes.empty?
760
+ @attributes[key].each do |item|
761
+ found_item_in_unsaved = false
762
+ @unsaved_attributes[key].each do |item_in_unsaved|
763
+ if !!(defined? item.attributes) && item.attributes[self.id_ref] == item_in_unsaved.attributes[self.id_ref]
764
+ found_item_in_unsaved = true
765
+ end
766
+ end
767
+
768
+ if !found_item_in_unsaved && !!(defined? item.id)
769
+ # need to send removeitem operation to parse
770
+ remove_item_ops << { "__type" => "Pointer", "className" => item.class.to_s, self.id_ref => item.id }
771
+ end
772
+ end
773
+ end
774
+ all_remove_item_queries.merge!({ key => { "__op" => "Remove", "objects" => remove_item_ops } }) if !remove_item_ops.empty?
775
+ end
776
+
777
+ # TODO figure out a more elegant way to get this working. the remove_item merge overwrites the add.
778
+ # Use a seperate query to add objects to the relation.
779
+ #if !all_add_item_queries.empty?
780
+ # #result = self.instance_resource.put(all_add_item_queries.to_json, {:content_type => "application/json"}) do |resp, req, res, &block|
781
+ # # return puts(resp, req, res, false, &block)
782
+ # #end
783
+ # puts result
784
+ #end
785
+
786
+ put_attrs.merge!(all_add_item_queries) unless all_add_item_queries.empty?
787
+ put_attrs.merge!(all_remove_item_queries) unless all_remove_item_queries.empty?
788
+ put_attrs
789
+ end
790
+
791
+ def update_attributes(attributes = {})
792
+ self.update(attributes)
793
+ end
794
+
795
+ def update_attribute(key, value)
796
+ send(key.to_s + "=", value)
797
+ update
798
+ end
799
+
800
+ def destroy
801
+ resp = self.resource.delete(self.id)
802
+ if resp.code.to_s == "200"
803
+ @attributes = {}
804
+ @unsaved_attributes = {}
805
+ return true
806
+ end
807
+ false
808
+ end
809
+
810
+ def reload
811
+ return false if new?
812
+
813
+ fresh_object = self.class.find(id)
814
+ @attributes = {}
815
+ @attributes.update(fresh_object.instance_variable_get("@attributes"))
816
+ @unsaved_attributes = {}
817
+
818
+ self
819
+ end
820
+
821
+ def dirty?
822
+ @unsaved_attributes.length > 0
823
+ end
824
+
825
+ def clean?
826
+ !dirty?
827
+ end
828
+
829
+ # provides access to @attributes for getting and setting
830
+ def attributes
831
+ @attributes ||= self.class.class_attributes
832
+ @attributes
833
+ end
834
+
835
+ def attributes=(value)
836
+ if value.is_a?(Hash) && value.present?
837
+ value.each do |k, v|
838
+ send "#{k}=", v
839
+ end
840
+ end
841
+ @attributes
842
+ end
843
+
844
+ def get_attribute(k)
845
+ attrs = @unsaved_attributes[k.to_s] ? @unsaved_attributes : @attributes
846
+ case attrs[k]
847
+ when Hash
848
+ klass_name = attrs[k]["className"]
849
+ klass_name = "User" if klass_name == "_User"
850
+ case attrs[k]["__type"]
851
+ when "Pointer"
852
+ result = klass_name.to_s.constantize.find(attrs[k][self.id_ref])
853
+ when "Object"
854
+ result = klass_name.to_s.constantize.new(attrs[k], false)
855
+ when "Date"
856
+ result = DateTime.parse(attrs[k]["iso"]).in_time_zone
857
+ when "File"
858
+ result = attrs[k]["url"]
859
+ when "Relation"
860
+ objects_related_to_self = klass_name.constantize.where("$relatedTo" => { "object" => { "__type" => "Pointer", "className" => self.class.to_s, self.id_ref => self.id }, "key" => k }).all
861
+ attrs[k] = Lockstep::RelationArray.new self, objects_related_to_self, k, klass_name
862
+ @unsaved_attributes[k] = Lockstep::RelationArray.new self, objects_related_to_self, k, klass_name
863
+ result = @unsaved_attributes[k]
864
+ end
865
+ else
866
+ # TODO changed from @@has_many_relations to @@has_many_relations.keys as we have changed the has_many_relations
867
+ # from array to hash to capture more data points. Not sure of the impact of this.
868
+ #relation will assign itself if an array, this will add to unsave_attributes
869
+ if has_many_relations.keys.index(k.to_s)
870
+ if attrs[k].nil?
871
+ # result = nil
872
+ result = load_association(:has_many, k)
873
+ else
874
+ @unsaved_attributes[k] = attrs[k].clone
875
+ result = @unsaved_attributes[k]
876
+ end
877
+ elsif belongs_to_relations.keys.index(k.to_s)
878
+ if attrs[k].nil?
879
+ # result = nil
880
+ result = load_association(:belongs_to, k)
881
+ else
882
+ @unsaved_attributes[k] = attrs[k].clone
883
+ result = @unsaved_attributes[k]
884
+ end
885
+ else
886
+ result = attrs["#{k}"]
887
+ end
888
+ end
889
+ result
890
+ end
891
+
892
+ # Alias of get_attribute
893
+ def _read_attribute(attr)
894
+ get_attribute(attr)
895
+ end
896
+
897
+ def set_attribute(k, v)
898
+ if v.is_a?(Date) || v.is_a?(Time) || v.is_a?(DateTime)
899
+ v = self.class.to_date_object(v)
900
+ # elsif v.respond_to?(:to_pointer)
901
+ # v = v.to_pointer
902
+ elsif (type = schema[k])
903
+ # Typecast the result value using dry-types
904
+ v = type[v]
905
+ elsif v.present? and (enum = enum_config[k]).present? and enum.keys.include?(v.to_s)
906
+ v = enum[v]
907
+ end
908
+
909
+ @unsaved_attributes[k.to_s] = v unless v == @attributes[k.to_s] # || @unsaved_attributes[k.to_s]
910
+ @attributes[k.to_s] = v
911
+ v
912
+ end
913
+
914
+ def self.has_many_relations
915
+ @has_many_relations ||= {}.with_indifferent_access
916
+ end
917
+
918
+ def has_many_relations
919
+ self.class.has_many_relations
920
+ end
921
+
922
+ # Alias for has_many_relations
923
+ def self.lockstep_has_many_relations
924
+ has_many_relations
925
+ end
926
+
927
+ def self.belongs_to_relations
928
+ @belongs_to_relations ||= {}.with_indifferent_access
929
+ end
930
+
931
+ def belongs_to_relations
932
+ self.class.belongs_to_relations
933
+ end
934
+
935
+ # Alias for belongs_to_relations
936
+ def self.lockstep_belongs_to_relations
937
+ belongs_to_relations
938
+ end
939
+
940
+ def primary_key
941
+ self.id_ref
942
+ end
943
+
944
+ # aliasing for idiomatic Ruby
945
+ def id
946
+ get_attribute(self.id_ref) rescue nil
947
+ end
948
+
949
+ def objectId
950
+ get_attribute(self.id_ref) rescue nil
951
+ end
952
+
953
+ def created_at
954
+ get_attribute("created")
955
+ end
956
+
957
+ def updated_at
958
+ get_attribute("modified")
959
+ end
960
+
961
+ def self.included(base)
962
+ base.extend(ClassMethods)
963
+ end
964
+
965
+ module ClassMethods
966
+ end
967
+
968
+ #if we are comparing objects, use id if they are both Lockstep::ApiRecord objects
969
+ def ==(another_object)
970
+ if another_object.class <= Lockstep::ApiRecord
971
+ self.id == another_object.id
972
+ else
973
+ super
974
+ end
975
+ end
976
+
977
+ def load_association(association_type, relation)
978
+ @loaded_associations ||= []
979
+ return nil if @loaded_associations.include?(relation)
980
+
981
+ @loaded_associations << relation # Prevent the load_association from being called the 2nd time
982
+
983
+ val = nil
984
+ case association_type
985
+ when :has_many
986
+ relation_config = has_many_relations[relation]
987
+
988
+ if relation_config[:loader].present?
989
+ val = relation_config[:loader].call(self)
990
+ else
991
+ return val unless relation_config[:foreign_key].present? and relation_config[:primary_key].present?
992
+ relation_klass = relation_config[:class_name].constantize
993
+ return val unless relation_klass.model_name_uri.present?
994
+
995
+ query = { relation_config[:foreign_key] => self.send(relation_config[:primary_key]) }
996
+ if relation_config[:polymorphic]
997
+ polymorphic_config = Lockstep::RelationArray.has_many_polymorphic_attributes(self, relation_config[:polymorphic])
998
+ query.merge!(polymorphic_config)
999
+ end
1000
+ related_objects = relation_klass.send(:where, query).execute
1001
+ val = Lockstep::RelationArray.new self, related_objects, relation, relation_config[:class_name]
1002
+ end
1003
+ when :belongs_to
1004
+ relation_config = belongs_to_relations[relation]
1005
+ if relation_config[:loader].present?
1006
+ val = relation_config[:loader].call(self)
1007
+ else
1008
+ val = relation_config[:class_name].constantize.send(:find_by, relation_config[:primary_key] => self.send(relation_config[:foreign_key]))
1009
+ end
1010
+ end
1011
+
1012
+ set_attribute(relation, val)
1013
+ val
1014
+ end
1015
+
1016
+ # @has_many_associations polymorphic properties builder.
1017
+ # Polymorphic properties is used to further scope down the has_many association while querying or creating
1018
+ #
1019
+ # def has_many_polymorphic_attributes(polymorphic_config)
1020
+ # return polymorphic_config if polymorphic_config.is_a?(Hash)
1021
+ # return polymorphic_config.call(self) if polymorphic_config.is_a?(Proc)
1022
+ #
1023
+ # nil
1024
+ # end
1025
+
1026
+ # TODO: Implement the ability to build_belongs_to_association
1027
+ # Challenge is that it has to update the record's association_id once the new association is created
1028
+ # def build_belongs_to_association(parent, attributes_hash)
1029
+ # if (val = get_attribute(parent)).present?
1030
+ # return val
1031
+ # end
1032
+ #
1033
+ # relation_config = @@belongs_to_relations[parent]
1034
+ # # Assign the parent records primary_key to the foreign_key of the association
1035
+ # foreign_key = relation_config[:foreign_key]
1036
+ # primary_key = relation_config[:primary_key]
1037
+ # attributes_hash[primary_key] = delegate.send(foreign_key)
1038
+ # # TODO implement polymorphic association support
1039
+ # object = self.class_name.constantize.new(attributes_hash)
1040
+ # set_attribute(parent, object)
1041
+ #
1042
+ # object
1043
+ # end
1044
+
1045
+ # Enum implementation - Start
1046
+ def self.enum_config
1047
+ @enum_config ||= {}.with_indifferent_access
1048
+ end
1049
+
1050
+ def enum_config
1051
+ self.class.enum_config
1052
+ end
1053
+
1054
+ def self.enum(config)
1055
+ config.each do |attribute, values|
1056
+ # Standardise values to hash
1057
+ if values.is_a?(Array)
1058
+ value_map = {}.with_indifferent_access
1059
+ values.each { |item| value_map[item] = item }
1060
+ elsif values.is_a?(Hash)
1061
+ value_map = values.with_indifferent_access
1062
+ else
1063
+ raise StandardError.new("Invalid values for enum #{attribute}")
1064
+ end
1065
+
1066
+ # Convert values to string if the value is symbol
1067
+ value_map.each { |k, v| value_map[k] = v.to_s if v.is_a?(Symbol) }
1068
+
1069
+ enum_config[attribute] = value_map
1070
+ class_eval do
1071
+ value_map.each do |k, v|
1072
+ define_method("#{k}!") do
1073
+ set_attribute(attribute, v)
1074
+ return save if persisted?
1075
+
1076
+ true
1077
+ end
1078
+
1079
+ define_method("#{k}?") do
1080
+ get_attribute(attribute) == v
1081
+ end
1082
+ end
1083
+ end
1084
+ end
1085
+ end
1086
+
1087
+ def validate_enum
1088
+ enum_config.each do |attribute, values_map|
1089
+ value = get_attribute(attribute)
1090
+ next if value.nil?
1091
+
1092
+ unless values_map.values.include?(value)
1093
+ errors.add attribute, "has an invalid value"
1094
+ end
1095
+ end
1096
+ end
1097
+
1098
+ # Enum implementation - End
1099
+
1100
+ def self.alias_attribute(new_name, old_name)
1101
+ define_method(new_name) do
1102
+ send(old_name)
1103
+ end
1104
+
1105
+ define_method("#{new_name}=") do |value|
1106
+ send("#{old_name}=", value)
1107
+ end
1108
+ end
1109
+
1110
+ def to_json(options = {})
1111
+ as_json(options).to_json
1112
+ end
1113
+
1114
+ def as_json(options = {})
1115
+ @attributes.merge(@unsaved_attributes).as_json
1116
+ end
1117
+ end
1118
+ end