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.



145
146
147
# File 'lib/parse/model/core/properties.rb', line 145

def attributes
  @attributes ||= BASE.dup
end

#attributes=(hash) ⇒ Hash

Set the property fields for this class.

Returns:



140
141
142
# File 'lib/parse/model/core/properties.rb', line 140

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.



150
151
152
# File 'lib/parse/model/core/properties.rb', line 150

def defaults_list
  @defaults_list ||= []
end

#enumsHash

Returns the fields that are marked as enums.

Returns:

  • (Hash)

    the fields that are marked as enums.



111
112
113
# File 'lib/parse/model/core/properties.rb', line 111

def enums
  @enums ||= {}
end

#field_mapHash

Returns the field map for this subclass.

Returns:

  • (Hash)

    the field map for this subclass.



106
107
108
# File 'lib/parse/model/core/properties.rb', line 106

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.



95
96
97
98
99
100
101
102
103
# File 'lib/parse/model/core/properties.rb', line 95

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.



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
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/parse/model/core/properties.rb', line 174

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 = :polygon if data_type == :geo_polygon
  data_type = :integer if data_type == :int || data_type == :number
  data_type = :phone if data_type == :phone_number || data_type == :mobile || data_type == :e164
  data_type = :email if data_type == :email_address

  # set defaults
  opts = { required: false,
           alias: true,
           symbolize: false,
           enum: nil,
           scopes: true,
           _prefix: nil,
           _suffix: false,
           _description: nil,  # Agent metadata: semantic description for LLMs
           _enum: nil,         # Agent metadata: per-value enum descriptions ({ value => description })
           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 property is already defined (either as a custom property on this class or as a
  # core property on a Parse::Object subclass), decide whether to silently apply non-structural
  # updates, raise, or warn-and-drop. Structural changes (different data type or different
  # remote field name) are almost always bugs — like declaring Installation#badge as :string
  # when the server stores it as :integer — so they raise when Parse.strict_property_redefinition
  # is enabled (the default). Non-structural redeclarations (same type, same remote field) are
  # allowed and may refine metadata such as :default, :_description, and :_enum without warning;
  # this covers class reopens that re-affirm an existing property after a parse-stack upgrade
  # adds the same definition upstream, or that bolt a default value onto an inherited field.
  if (self.fields[key].present? && BASE_FIELD_MAP[key].nil?) || (self < Parse::Object && BASE_FIELD_MAP.has_key?(key))
    existing_type = self.fields[key]
    existing_parse_field = self.field_map[key]
    if existing_type == data_type && existing_parse_field == parse_field
      # Non-structural redeclaration: apply safe metadata-only updates and bail out before
      # the rest of the method redefines getters/setters/validations/scopes.
      if opts.key?(:default)
        default_value = opts[:default]
        defaults_list.push(key) unless defaults_list.include?(key)
        define_method("#{key}_default") do
          default_value.is_a?(Proc) ? default_value.call(self) : default_value
        end
      end
      if opts[:_description].present?
        self.property_descriptions[key] = opts[:_description].to_s.freeze
      end
      if opts[:_enum].is_a?(Hash) && opts[:_enum].any?
        normalized = opts[:_enum].each_with_object({}) do |(value, desc), h|
          h[value.to_s] = desc.to_s.freeze
        end
        self.property_enum_descriptions[key] = normalized.freeze
      end
      return true
    end
    if Parse.strict_property_redefinition
      raise ArgumentError,
            "Property #{self}##{key} is already defined as :#{existing_type} " \
            "(remote field :#{existing_parse_field}); refusing to redeclare as :#{data_type} " \
            "(remote field :#{parse_field}). Set Parse.strict_property_redefinition = false " \
            "to fall back to warn-and-ignore behavior."
    end
    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)

  # Store the property description for agent metadata if provided
  if opts[:_description].present?
    self.property_descriptions[key] = opts[:_description].to_s.freeze
  end

  # Store per-value enum descriptions for agent metadata if provided.
  # Accepts a Hash mapping each allowed value (Symbol or String) to a
  # description string. Stored with stringified value keys to match the
  # wire-format shape an LLM will see in query constraints. Distinct
  # from the existing `enum:` option, which is a validation construct.
  if opts[:_enum].is_a?(Hash) && opts[:_enum].any?
    normalized = opts[:_enum].each_with_object({}) do |(value, desc), h|
      h[value.to_s] = desc.to_s.freeze
    end
    self.property_enum_descriptions[key] = normalized.freeze
  end

  # 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

  # phone datatypes validate E.164 format.
  if data_type == :phone
    validates_each key do |record, attribute, value|
      # Parse::Phone objects have a `valid?` method to determine if the phone is valid E.164.
      unless value.nil? || value.valid?
        record.errors.add(attribute, "field :#{attribute} must be a valid E.164 phone number (e.g., +14155551234).")
      end
    end # validates_each
  end # data_type == :phone

  # email datatypes validate email format.
  if data_type == :email
    validates_each key do |record, attribute, value|
      # Parse::Email objects have a `valid?` method to determine if the email is valid.
      unless value.nil? || value.valid?
        record.errors.add(attribute, "field :#{attribute} must be a valid email address.")
      end
    end # validates_each
  end # data_type == :email

  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
        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.
    # Also autofetch if object was selectively fetched and this field wasn't included.
    should_autofetch = value.nil? && (pointer? || (has_selective_keys? && !field_was_fetched?(key)))
    if should_autofetch
      # If autofetch is disabled and we're accessing an unfetched field on a
      # selectively fetched object, raise an error to make the issue explicit
      if autofetch_disabled? && has_selective_keys? && !field_was_fetched?(key)
        raise Parse::UnfetchedFieldAccessError.new(key, self.class.name)
      end
      # 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
      prepare_for_dirty_tracking!(key)
      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

#property_descriptionsHash

Maps property names (symbols) to their description strings.

Returns:

  • (Hash)

    semantic descriptions for properties (used by Parse::Agent).



117
118
119
# File 'lib/parse/model/core/properties.rb', line 117

def property_descriptions
  @property_descriptions ||= {}
end

#property_enum_descriptionsHash

properties (used by Parse::Agent). Maps property names (symbols) to a ‘{ “value” => “description” }` hash. Orthogonal to the existing `enum:` option on `property` — `enum:` validates the set of allowed values, `_enum:` documents each one for an LLM.

**Intended for string-typed columns only.** Value keys are stringified at declaration time and the schema response carries ‘“1”, …` regardless of the underlying column type. Declaring `_enum:` on an integer/boolean column will surface string-shaped values to the LLM that won’t match the column in a ‘where:` filter — userland is responsible for keeping `_enum:` on string-typed properties.

Returns:

  • (Hash)

    per-value descriptions for enum-shaped string



134
135
136
# File 'lib/parse/model/core/properties.rb', line 134

def property_enum_descriptions
  @property_enum_descriptions ||= {}
end