lockstep_rails 0.3.22

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 (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