Module: HasHelpers::Constant::ClassMethods

Defined in:
lib/has_helpers/constant.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#abstract_constant_classObject

An abstract constant class is a class that constant classes inherit from but does not have any constants of its own. This prevents the ALL constant from being defined on an ancestor, which can cause problems for its descendants.



304
305
306
# File 'lib/has_helpers/constant.rb', line 304

def abstract_constant_class
  @abstract_constant_class
end

#groupedObject

grouped it is used to filter constants with group_id = nil as default scope.



315
316
317
# File 'lib/has_helpers/constant.rb', line 315

def grouped
  @grouped || false
end

#simpleObject



319
320
321
# File 'lib/has_helpers/constant.rb', line 319

def simple
  @simple || (superclass.respond_to?(:simple) && superclass.simple)
end

Instance Method Details

#const_missing(constant_name) ⇒ Array, ActiveRecord

Method to be able to reference a constant via Active Record Handles a GET request

Parameters:

  • constant_name (CONSTANT)

    the constant being requested

Returns:



335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/has_helpers/constant.rb', line 335

def const_missing(constant_name)
  constant_name = constant_name.to_sym
  if constant_name == :ALL && !self.abstract_constant_class # Special case for ::ALL
    const_set(:ALL, __regular_constant_definitions__.map { |definition| __fetch_const_from_definition__(definition) })
  elsif constant_name == :ALL_GROUPS && !self.abstract_constant_class # Special case for :ALL_GROUPS
    const_set(:ALL_GROUPS, __parent_constants__.map { |definition| __fetch_const_from_definition__(definition) })
  elsif __constants__.has_key?(constant_name)
    const_set(constant_name, __constants__[constant_name].load(self))
  else
    super
  end
end

#constant_namesObject



455
456
457
# File 'lib/has_helpers/constant.rb', line 455

def constant_names
  __regular_constant_definitions__.map(&:name)
end

#define_constant(constant_definition, **attrs) ⇒ Object



370
371
372
# File 'lib/has_helpers/constant.rb', line 370

def define_constant(constant_definition, **attrs)
  define_constants(constant_definition, **attrs).first
end

#define_constant_group(parent, group_name: nil, group_foreign_key: nil, constants: nil, include_parent_group_id: true, &block) ⇒ Object

Defines a single constant group

Constants in the group can be either actual ruby constants, constant definitions, or strings.

If they're actual ruby constants, then they should go in a block that's passed into either the to_constant method of the parent or the define_constant_group method.

If they're strings or constant definitions, then they should go in the constants parameter.

The group_name parameter is used to set a custom group name. This is only used when the group is persisted in the database. If the group is persisted and no group_name is given, then it defaults to #HasHelpers::Constant::ClassMethods.parentparent.keyparent.key.upcase_GROUP

The group_foreign_key parameter determines whether or not the group association is persisted into the database. If it's nil, then the group exists only in memory, otherwise both the children and the parent will have the column specified by the group_foreign_key set to the parent's id.

The include_parent_group_id parameter is used to set the group_id of the parent (false by default) This behavior sometimes is needed.

Examples

# This shows the different ways to put constants in a group
define_constants('Lapsed', 'Reversed', 'Returned', 'Replaced')
define_constant_group(
'Closed'.to_constant { [LAPSED, REVERSED] }, # Closed is the parent, LAPSED and REVERSE are in the group
group_name: "CLOSED_STATUSES", # CLOSED evaluates to the parent closed status, while CLOSED_STATUSES gives the constants in the group.
group_foreign_key: :group_id, # All constants in the group will have their group_id set to CLOSED.id
constants: ['DECLINED', 'SURRENDERED']
) { [RETURNED, REPLACED] } # RETURNED and REPLACED are in the group

define_constant_group "POLICIES" do
[AMENDMENT_FORM, APPLICATIONS, AWAIT_FUNDING, TENTHIRTYFIVE_FORM]
end


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
# File 'lib/has_helpers/constant.rb', line 410

def define_constant_group(parent, group_name: nil, group_foreign_key: nil, constants: nil, include_parent_group_id: true, &block)
  child_defs = constants ? Array(constants).map(&:to_constant).each { |child| child.group_foreign_key = group_foreign_key } : nil
  child_defs = define_constants(*child_defs.map(&:to_constant)) if child_defs&.any?
  group_block = group_proc(child_defs, parent, block)
  parent_definition = parent.to_constant.tap do |parent_def|
    parent_def.group_foreign_key = group_foreign_key
    parent_def.persisted_parent = !!group_foreign_key
    parent_def.block = group_block if group_block
    parent_def.include_parent_group_id = include_parent_group_id
  end

  if group_foreign_key
    # When the parent is an actual constant in the database, we assign a separate name for the group
    # so that parent constant still evaluates to its record.
    group_name ||= "#{parent.to_s.constant_case}_GROUP"
    group_definition = group_name.to_constant.tap do |group|
      group.block = proc do
        child_constants = group_block.call
        [__fetch_const_from_definition__(parent_definition), *child_constants].uniq
      end
    end

    if self.reflect_on_association(:children).nil?
      self.has_many :group_children, class_name: self.name, foreign_key: "#{group_foreign_key}"
      self.belongs_to :group_parent, class_name: self.name, foreign_key: "#{group_foreign_key}", optional: true
    end

    define_constant(group_definition)
  end
  define_constant(parent_definition)
end

#define_constants(*constant_definitions, key_prefix: nil, **attrs) ⇒ Object

Returns constant definitions



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/has_helpers/constant.rb', line 349

def define_constants(*constant_definitions, key_prefix: nil, **attrs)
  constant_definitions.map(&:to_constant).tap do |definitions|
    definitions.each do |constant_definition|
      constant_definition.attrs.merge!(attrs)
      constant_definition.key = "#{ key_prefix.constant_case }_#{ constant_definition.key }" if key_prefix

      existing_attrs = __constants__[constant_definition.key]&.attrs || {}
      constant_definition.attrs = existing_attrs.merge!(constant_definition.attrs)
      __constants__[constant_definition.key] = constant_definition # Register the definition in the look-up hash

      # This gets a bit complicated, but in short we need a way to prevent Ruby from returning
      # a constant which is defined in an ancestor module. E.g. if `IMO` is a top-level class
      # then we don't want `MyConstant::IMO` to return that top-level class.
      # To stop this undesired behavior the code below sets the constant on the current class
      # to be a proxy. When that Proxy is evaluated it will set load the constant as expected.
      remove_const(constant_definition.key) if const_defined?(constant_definition.key, false)
      const_set(constant_definition.key, Proxy.new(self, constant_definition.key))
    end
  end
end

#group_proc(const_defs, parent, block) ⇒ Object



442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/has_helpers/constant.rb', line 442

def group_proc(const_defs, parent, block)
  existing_parent_block = parent.to_constant.block
  proc do
    grouped_constants = []
    grouped_constants << existing_parent_block.call if existing_parent_block
    grouped_constants << __defs_to_consts__(const_defs) if const_defs
    grouped_constants << block.call if block
    grouped_constants.flatten!
    grouped_constants = grouped_constants.first if grouped_constants.one? && grouped_constants.first.is_a?(::Hash)
    grouped_constants
  end
end

#is_parent?(klass) ⇒ Boolean

Returns:

  • (Boolean)


327
328
329
# File 'lib/has_helpers/constant.rb', line 327

def is_parent?(klass)
  __parent_constants__.any? { |c| c.name === klass.name }
end

#lookup(query = nil, name: query, id: nil, key: nil, case_sensitive: false, **attrs) ⇒ Object

Accepts a query string and returns the matching record, if any. If there is an ambiguity which causes more than one record to be match then an error will be raised. For such cases it may be necessary to override this method and provide a way to disambiguate lookups.

Examples

Lookup by name

FujitaScale.lookup("F1") # => FujitaScale.lookup(name: "F1") # =>

# Lookup by an attribute other than name
FujitaScale.lookup(key: :F1)      # => <FujitaScale name="F1">
State.lookup(abbreviation: "KS")  # => <State name="Kansas" abbreviation="KS">

# Lookup by incorrect name (does not exist)
FujitaScale.lookup("F14")       # => nil

# Ambiguous lookup (multiple matches)
Alignment.lookup(type: "Horizontal")  # => Error!


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
# File 'lib/has_helpers/constant.rb', line 516

def lookup(query = nil, name: query, id: nil, key: nil, case_sensitive: false, **attrs)
  if name || id || key || attrs.any? # Perform lookup if any criterion is present. Otherwise nil is returned.
    matches = Enumerator.new do |yielder|
      already_yielded = false
      case_converter = case_sensitive ? :itself : :downcase
      __constants__.
        lazy.
        select do |constant_key, definition|
        (!name || definition.name.send(case_converter) == name.send(case_converter)) &&
          (!key || key.to_s == constant_key.to_s) &&
          attrs.all? { |k, v| definition.attrs(force_eval: true)[k] == v }
      end.
        each do |_, definition|
        raise "Lookup matched more than one record" if already_yielded

        const = __fetch_const_from_definition__(definition)

        # We can't lookup by id until after loading constants
        if !id || (!const.is_a?(Array) && const.id.to_s == id.to_s)
          yielder << const
          already_yielded = true

          # Break early if we find a matching record with an id.
          # An id will be unique so we don't care about raising
          # an error when there are more than one matched record.
          break if id
        end
      end
    end
    matches.to_a.first
  end
end

#seed!Object

Load the seeds for this constant. Note, if the current constant depends on other constants, then those should be loaded first. Do so by overriding the .seed! method, loading the dependencies, and then calling super.

class Registration
def self.seed!
  ::Exam.seed!  # Load Exams before Registrations.
  super
end


468
469
470
471
472
473
# File 'lib/has_helpers/constant.rb', line 468

def seed!
  self.grouped = false
  __regular_constant_definitions__.each { |definition| definition.seed!(self) }
  set_parent_child_associations
  nil # Returns this rather than the enumerator from the previous line.
end

#set_parent_child_associationsObject



475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/has_helpers/constant.rb', line 475

def set_parent_child_associations
  __parent_constants__.each do |parent_definition|
    parent_instance = parent_definition.load(self)
    parent_instance.reload if parent_definition.include_parent_group_id
    parent_definition.attrs["#{parent_definition.group_foreign_key}"] = parent_instance.id
    parent_instance.send("#{parent_definition.group_foreign_key}=", parent_instance.id)
    parent_instance.save!
    children = parent_definition.evaluate
    children.each do |child_instance|
      foreign_key = parent_definition.group_foreign_key
      # Simple constants are created w/o fetching from the db.
      # Since we're saving the constant, we need to load from the database so attributes are up-to-date
      child_instance.reload if simple
      child_instance.public_send("#{foreign_key}=", parent_instance.id)
      child_instance.save!
      child_def = child_instance.definition
      child_def.attrs["#{foreign_key}"] = parent_instance.id
      __constants__[child_def.key] = child_def
    end
  end
end

#supports_grouping?Boolean

Returns:

  • (Boolean)


323
324
325
# File 'lib/has_helpers/constant.rb', line 323

def supports_grouping?
  !!reflect_on_association(:group_children)
end