Module: Parse::Properties::ClassMethods
- Defined in:
- lib/parse/model/core/properties.rb
Overview
The class methods added to Parse::Objects
Instance Method Summary collapse
-
#attributes ⇒ Hash
The fields that are marked as enums.
-
#attributes=(hash) ⇒ Hash
Set the property fields for this class.
-
#defaults_list ⇒ Array
The list of fields that have defaults.
-
#enums ⇒ Hash
The fields that are marked as enums.
-
#field_map ⇒ Hash
The field map for this subclass.
-
#fields(type = nil) ⇒ Hash
The fields method returns a mapping of all local attribute names and their data type.
-
#property(key, data_type = :string, **opts) ⇒ Object
This is the class level property method to be used when declaring properties.
-
#property_descriptions ⇒ Hash
Maps property names (symbols) to their description strings.
-
#property_enum_descriptions ⇒ Hash
properties (used by Parse::Agent).
Instance Method Details
#attributes ⇒ Hash
Returns 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.
140 141 142 |
# File 'lib/parse/model/core/properties.rb', line 140 def attributes=(hash) @attributes = BASE.merge(hash) end |
#defaults_list ⇒ Array
Returns 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 |
#enums ⇒ Hash
Returns the fields that are marked as enums.
111 112 113 |
# File 'lib/parse/model/core/properties.rb', line 111 def enums @enums ||= {} end |
#field_map ⇒ Hash
Returns 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.
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_descriptions ⇒ Hash
Maps property names (symbols) to their description strings.
117 118 119 |
# File 'lib/parse/model/core/properties.rb', line 117 def property_descriptions @property_descriptions ||= {} end |
#property_enum_descriptions ⇒ Hash
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.
134 135 136 |
# File 'lib/parse/model/core/properties.rb', line 134 def property_enum_descriptions @property_enum_descriptions ||= {} end |