Module: Parse::Properties::ClassMethods

Defined in:
lib/parse/model/core/properties.rb

Overview

The class methods added to Parse::Objects

Instance Method Summary collapse

Instance Method Details

#attributesHash

Returns the fields that are marked as enums.

Returns:

  • (Hash)

    the fields that are marked as enums.



73
74
75
# File 'lib/parse/model/core/properties.rb', line 73

def attributes
  @attributes ||= BASE.dup
end

#attributes=(hash) ⇒ Hash

Set the property fields for this class.

Returns:



68
69
70
# File 'lib/parse/model/core/properties.rb', line 68

def attributes=(hash)
  @attributes = BASE.merge(hash)
end

#defaults_listArray

Returns the list of fields that have defaults.

Returns:

  • (Array)

    the list of fields that have defaults.



78
79
80
# File 'lib/parse/model/core/properties.rb', line 78

def defaults_list
  @defaults_list ||= []
end

#enumsHash

Returns the fields that are marked as enums.

Returns:

  • (Hash)

    the fields that are marked as enums.



62
63
64
# File 'lib/parse/model/core/properties.rb', line 62

def enums
  @enums ||= {}
end

#field_mapHash

Returns the field map for this subclass.

Returns:

  • (Hash)

    the field map for this subclass.



57
58
59
# File 'lib/parse/model/core/properties.rb', line 57

def field_map
  @field_map ||= BASE_FIELD_MAP.dup
end

#fields(type = nil) ⇒ Hash

The fields method returns a mapping of all local attribute names and their data type. if type is passed, we return only the fields that matched that data type. If `type` is provided, it will only return the fields that match the data type.

Parameters:

  • type (Symbol) (defaults to: nil)

    a property type.

Returns:

  • (Hash)

    the defined fields for this Parse collection with their data type.



46
47
48
49
50
51
52
53
54
# File 'lib/parse/model/core/properties.rb', line 46

def fields(type = nil)
  # if it's Parse::Object, then only use the initial set, otherwise add the other base fields.
  @fields ||= (self == Parse::Object ? CORE_FIELDS : Parse::Object.fields).dup
  if type.present?
    type = type.to_sym
    return @fields.select { |k, v| v == type }
  end
  @fields
end

#property(key, data_type = :string, **opts) ⇒ Object

This is the class level property method to be used when declaring properties. This helps builds specific methods, formatters and conversion handlers for property storing and saving data for a particular parse class. The first parameter is the name of the local attribute you want to declare with its corresponding data type. Declaring a `property :my_date, :date`, would declare the attribute my_date with a corresponding remote column called “myDate” (lower-first-camelcase) with a Parse data type of Date. You can override the implicit naming behavior by passing the option :field to override.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/parse/model/core/properties.rb', line 102

def property(key, data_type = :string, **opts)
  key = key.to_sym
  ivar = :"@#{key}"
  will_change_method = :"#{key}_will_change!"
  set_attribute_method = :"#{key}_set_attribute!"

  if data_type.is_a?(Hash)
    opts.merge!(data_type)
    data_type = :string
    # future: automatically use :timezone datatype for timezone-like fields.
    # when the data_type was not specifically set.
    # data_type = :timezone if key == :time_zone || key == :timezone
  end

  data_type = :timezone if data_type == :string && (key == :time_zone || key == :timezone)

  # allow :bool for :boolean
  data_type = :boolean if data_type == :bool
  data_type = :timezone if data_type == :time_zone
  data_type = :geopoint if data_type == :geo_point
  data_type = :integer if data_type == :int || data_type == :number

  # set defaults
  opts = { required: false,
          alias: true,
          symbolize: false,
          enum: nil,
          scopes: true,
          _prefix: nil,
          _suffix: false,
          field: key.to_s.camelize(:lower) }.merge(opts)
  #By default, the remote field name is a lower-first-camelcase version of the key
  # it can be overriden by the :field parameter
  parse_field = opts[:field].to_sym
  # if this is a custom property that is already defined, OR it is a subclass trying to define a core property
  # then warn and exit.
  if (self.fields[key].present? && BASE_FIELD_MAP[key].nil?) || (self < Parse::Object && BASE_FIELD_MAP.has_key?(key))
    warn "Property #{self}##{key} already defined with data type :#{data_type}. Will be ignored."
    return false
  end
  # We keep the list of fields that are on the remote Parse store
  if self.fields[parse_field].present? || (self < Parse::Object && BASE.has_key?(parse_field))
    warn "Alias property #{self}##{parse_field} conflicts with previously defined property. Will be ignored."
    return false
    # raise ArgumentError
  end
  #dirty tracking. It is declared to use with ActiveModel DirtyTracking
  define_attribute_methods key

  # this hash keeps list of attributes (based on remote fields) and their data types
  self.attributes.merge!(parse_field => data_type)
  # this maps all the possible attribute fields and their data types. We use both local
  # keys and remote keys because when we receive a remote object that has the remote field name
  # we need to know what the data type conversion should be.
  self.fields.merge!(key => data_type, parse_field => data_type)
  # This creates a mapping between the local field and the remote field name.
  self.field_map.merge!(key => parse_field)

  # if the field is marked as required, then add validations
  if opts[:required]
    # if integer or float, validate that it's a number
    if data_type == :integer || data_type == :float
      validates_numericality_of key
    end
    # validate that it is not empty
    validates_presence_of key
  end

  # timezone datatypes are basically enums based on IANA time zone identifiers.
  if data_type == :timezone
    validates_each key do |record, attribute, value|
      # Parse::TimeZone objects have a `valid?` method to determine if the timezone is valid.
      unless value.nil? || value.valid?
        record.errors.add(attribute, "field :#{attribute} must be a valid IANA time zone identifier.")
      end
    end # validates_each
  end # data_type == :timezone

  is_enum_type = opts[:enum].nil? == false

  if is_enum_type
    unless data_type == :string
      raise ArgumentError, "Property #{self}##{parse_field} :enum option is only supported on :string data types."
    end

    enum_values = opts[:enum]
    unless enum_values.is_a?(Array) && enum_values.empty? == false
      raise ArgumentError, "Property #{self}##{parse_field} :enum option must be an Array type of symbols."
    end
    opts[:symbolize] = true

    enum_values = enum_values.dup.map(&:to_sym).freeze

    self.enums.merge!(key => enum_values)
    allow_nil = opts[:required] == false
    validates key, inclusion: { in: enum_values }, allow_nil: allow_nil

    unless opts[:scopes] == false
      # You can use the :_prefix or :_suffix options when you need to define multiple enums with same values.
      # If the passed value is true, the methods are prefixed/suffixed with the name of the enum. It is also possible to supply a custom value:
      prefix = opts[:_prefix]
      unless opts[:_prefix].nil? || prefix.is_a?(Symbol) || prefix.is_a?(String)
        raise ArgumentError, "Enumeration option :_prefix must either be a symbol or string for #{self}##{key}."
      end

      unless opts[:_suffix].is_a?(TrueClass) || opts[:_suffix].is_a?(FalseClass)
        raise ArgumentError, "Enumeration option :_suffix must either be true or false for #{self}##{key}."
      end

      add_suffix = opts[:_suffix] == true
      prefix_or_key = (prefix.blank? ? key : prefix).to_sym

      class_method_name = prefix_or_key.to_s.pluralize.to_sym
      if singleton_class.method_defined?(class_method_name)
        raise ArgumentError, "You tried to define an enum named `#{key}` for #{self} " + "but this will generate a method  `#{self}.#{class_method_name}` " + " which is already defined. Try using :_suffix or :_prefix options."
      end

      define_singleton_method(class_method_name) { enum_values }

      method_name = add_suffix ? :"valid_#{prefix_or_key}?" : :"#{prefix_or_key}_valid?"
      define_method(method_name) do
        value = send(key) # call default getter
        return true if allow_nil && value.nil?
        enum_values.include?(value.to_s.to_sym)
      end

      enum_values.each do |enum|
        method_name = enum # default
        scope_name = enum
        if add_suffix
          method_name = :"#{enum}_#{prefix_or_key}"
        elsif prefix.present?
          method_name = :"#{prefix}_#{enum}"
        end
        self.scope method_name, ->(ex = {}) { ex.merge!(key => enum); query(ex) }

        define_method("#{method_name}!") { send set_attribute_method, enum, true }
        define_method("#{method_name}?") { enum == send(key).to_s.to_sym }
      end
    end # unless scopes
  end # if is enum

  symbolize_value = opts[:symbolize]

  #only support symbolization of string data types
  if symbolize_value && (data_type == :string || data_type == :array) == false
    raise ArgumentError, "Tried to symbolize #{self}##{key}, but it is only supported on :string or :array data types."
  end

  # Here is the where the 'magic' begins. For each property defined, we will
  # generate special setters and getters that will take advantage of ActiveModel
  # helpers.
  # get the default value if provided (or Proc)
  default_value = opts[:default]
  unless default_value.nil?
    defaults_list.push(key) unless default_value.nil?

    define_method("#{key}_default") do
      # If the default object provided is a Proc, then run the proc, otherwise
      # we'll assume it's just a plain literal value
      default_value.is_a?(Proc) ? default_value.call(self) : default_value
    end
  end

  # We define a getter with the key

  define_method(key) do

    # we will get the value using the internal value of the instance variable
    # using the instance_variable_get
    value = instance_variable_get ivar

    # If the value is nil and this current Parse::Object instance is a pointer?
    # then someone is calling the getter for this, which means they probably want
    # its value - so let's go turn this pointer into a full object record
    if value.nil? && pointer?
      # call autofetch to fetch the entire record
      # and then get the ivar again cause it might have been updated.
      autofetch!(key)
      value = instance_variable_get ivar
    end

    # if value is nil (even after fetching), then lets see if the developer
    # set a default value for this attribute.
    if value.nil? && respond_to?("#{key}_default")
      value = send("#{key}_default")
      value = format_value(key, value, data_type)
      # lets set the variable with the updated value
      instance_variable_set ivar, value
      send will_change_method
    elsif value.nil? && data_type == :array
      value = Parse::CollectionProxy.new [], delegate: self, key: key
      instance_variable_set ivar, value
      # don't send the notification yet until they actually add something
      # which will be handled by the collection proxy.
      # send will_change_method
    end

    # if the value is a String (like an iso8601 date) and the data type of
    # this object is :date, then let's be nice and create a parse date for it.
    if value.is_a?(String) && data_type == :date
      value = format_value(key, value, data_type)
      instance_variable_set ivar, value
      send will_change_method
    end
    # finally return the value
    if symbolize_value
      if data_type == :string
        return value.respond_to?(:to_sym) ? value.to_sym : value
      elsif data_type == :array && value.is_a?(Array)
        # value.map(&:to_sym)
        return value.compact.map { |m| m.respond_to?(:to_sym) ? m.to_sym : m }
      end
    end

    value
  end

  # support question mark methods for boolean
  if data_type == :boolean
    if self.method_defined?("#{key}?")
      warn "Creating boolean helper :#{key}?. Will overwrite existing method #{self}##{key}?."
    end

    # returns true if set to true, false otherwise
    define_method("#{key}?") { (send(key) == true) }
    unless opts[:scopes] == false
      scope key, ->(opts = {}) { query(opts.merge(key => true)) }
    end
  elsif data_type == :integer || data_type == :float
    if self.method_defined?("#{key}_increment!")
      warn "Creating increment helper :#{key}_increment!. Will overwrite existing method #{self}##{key}_increment!."
    end

    define_method("#{key}_increment!") do |amount = 1|
      unless amount.is_a?(Numeric)
        raise ArgumentError, "Amount needs to be an integer"
      end
      result = self.op_increment!(key, amount)
      if result
        new_value = send(key).to_i + amount
        # set the updated value, with no dirty tracking
        self.send set_attribute_method, new_value, false
      end
      result
    end

    if self.method_defined?("#{key}_decrement!")
      warn "Creating decrement helper :#{key}_decrement!. Will overwrite existing method #{self}##{key}_decrement!."
    end

    define_method("#{key}_decrement!") do |amount = -1|
      unless amount.is_a?(Numeric)
        raise ArgumentError, "Amount needs to be an integer"
      end
      amount = -amount if amount > 0
      send("#{key}_increment!", amount)
    end
  end

  # The second method to be defined is a setter method. This is done by
  # defining :key with a '=' sign. However, to support setting the attribute
  # with and without dirty tracking, we really will just proxy it to another method

  define_method("#{key}=") do |val|
    #we proxy the method passing the value and true. Passing true to the
    # method tells it to make sure dirty tracking is enabled.
    self.send set_attribute_method, val, true
  end

  # This is the real setter method. Takes two arguments, the value to set
  # and whether to mark it as dirty tracked.
  define_method(set_attribute_method) do |val, track = true|
    # Each value has a data type, based on that we can treat the incoming
    # value as input, and format it to the correct storage format. This method is
    # defined in this file (instance method)
    val = format_value(key, val, data_type)
    # if dirty trackin is enabled, call the ActiveModel required method of _will_change!
    # this will grab the current value and keep a copy of it - but we only do this if
    # the new value being set is different from the current value stored.
    if track == true
      send will_change_method unless val == instance_variable_get(ivar)
    end

    if symbolize_value
      if data_type == :string
        val = nil if val.blank?
        val = val.to_sym if val.respond_to?(:to_sym)
      elsif val.is_a?(Parse::CollectionProxy)
        items = val.collection.map { |m| m.respond_to?(:to_sym) ? m.to_sym : m }
        val.set_collection! items
      end
    end

    # if is_enum_type
    #
    # end
    # now set the instance value
    instance_variable_set ivar, val
  end

  # The core methods above support all attributes with the base local :key parameter
  # however, for ease of use and to handle that the incoming fields from parse have different
  # names, we will alias all those methods defined above with the defined parse_field.
  # if both the local name matches the calculated/provided remote column name, don't create
  # an alias method since it is the same thing. Ex. attribute 'username' would probably have the
  # remote column name also called 'username'.
  return true if parse_field == key

  # we will now create the aliases, however if the method is already defined
  # we warn the user unless the field is :objectId since we are in charge of that one.
  # this is because it is possible they want to override. You can turn off this
  # behavior by passing false to :alias

  if self.method_defined?(parse_field) == false && opts[:alias]
    alias_method parse_field, key
    alias_method "#{parse_field}=", "#{key}="
    alias_method "#{parse_field}_set_attribute!", set_attribute_method
  elsif parse_field.to_sym != :objectId
    warn "Alias property method #{self}##{parse_field} already defined."
  end
  true
end