Class: Brick::JoinArray

Inherits:
Array
  • Object
show all
Defined in:
lib/brick/join_array.rb

Overview

JoinArray and JoinHash

These JOIN-related collection classes – JoinArray and its related “partner in crime” JoinHash – both interact to more easily build out nested sets of hashes and arrays to be used with ActiveRecord’s .joins() method. For example, if there is an Order, Customer, and Employee model, and Order belongs_to :customer and :employee, then from the perspective of Order all these three could be JOINed together by referencing the two belongs_to association names:

Order.joins([:customer, :employee])

and from the perspective of Employee it would instead use a hash like this, using the has_many :orders association and the :customer belongs_to:

Employee.joins({ orders: :customer })

(in both cases the same three tables are being JOINed, the two approaches differ just based on their starting standpoint.) These utility classes are designed to make building out any goofy linkages like this pretty simple in a few ways: ** if the same association is requested more than once then no duplicates. ** If a bunch of intermediary associations are referenced leading up to a final one then all of them get automatically built

out and added along the way, without any having to previously exist.

** If one reference was made previously and now another neighbouring one is called for, then what used to be a simple symbol

is automatically graduated into an array so that both members can be held.  For instance, if with the Order example above
there was also a LineItem model that belongs_to Order, then let's say you start from LineItem and want to now get all 4
related models.  You could start by going through :order to :employee like this:

line_item_joins = JoinArray.new line_item_joins = :employee

> { order: :employee }

and then add in the reference to :customer like this:

line_item_joins = :customer

> { order: [:employee, :customer] }

and then carry on incrementally building out more JOINs in whatever sequence makes the best sense.  This bundle of nested
stuff can then be used to query ActiveRecord like this:

LineItem.joins(line_item_joins)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#orig_parentObject (readonly)

Returns the value of attribute orig_parent.



41
42
43
# File 'lib/brick/join_array.rb', line 41

def orig_parent
  @orig_parent
end

#parentObject (readonly)

Returns the value of attribute parent.



41
42
43
# File 'lib/brick/join_array.rb', line 41

def parent
  @parent
end

#parent_keyObject (readonly)

Returns the value of attribute parent_key.



41
42
43
# File 'lib/brick/join_array.rb', line 41

def parent_key
  @parent_key
end

Class Method Details

.attach_back_to_root(collection, key = nil, value = nil) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/brick/join_array.rb', line 80

def self.attach_back_to_root(collection, key = nil, value = nil)
  # Create a list of layers which start at the root
  layers = []
  layer = collection
  while layer.parent
    layers << layer
    layer = layer.parent
  end
  # Go through the layers from root down to child, attaching everything
  layers.each do |layer|
    if (prnt = layer.remove_instance_variable(:@parent))
      layer.instance_variable_set(:@orig_parent, prnt)
    end
    case prnt
    when ::Brick::JoinHash
      value = if prnt.key?(layer.parent_key)
                if layer.is_a?(Hash)
                  layer
                else
                  ::Brick::JoinArray.new.replace([prnt.fetch(layer.parent_key, nil), layer])
                end
              else
                layer
              end
      # This is as if we did:  prnt[layer.parent_key] = value
      # but calling it that way would attempt to infinitely recurse back onto this overridden version of the []= method,
      # so we go directly to ._brick_store() instead.
      prnt._brick_store(layer.parent_key, value)
    when ::Brick::JoinArray
      if (key)
        puts "X1"
        prnt[layer.parent_key][key] = value
      else
        prnt[layer.parent_key] = layer
      end
    end
  end
end

Instance Method Details

#[](*args) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/brick/join_array.rb', line 44

def [](*args)
  if !(key = args[0]).is_a?(Symbol)
    super
  else
    idx = -1
    # Whenever a JoinHash has a value of a JoinArray with a single member then it is a wrapper, usually for a Symbol
    matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
    case matching
    when ::Brick::JoinHash
      matching[key]
    when ::Brick::JoinArray
      matching.first
    else
      ::Brick::JoinHash.new.tap do |child|
        child.instance_variable_set(:@parent, self)
        child.instance_variable_set(:@parent_key, key) # %%% Use idx instead of key?
      end
    end
  end
end

#[]=(*args) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/brick/join_array.rb', line 65

def []=(*args)
  ::Brick::JoinArray.attach_back_to_root(self, args[0], args[1])

  if (key = args[0]).is_a?(Symbol) && ((value = args[1]).is_a?(::Brick::JoinHash) || value.is_a?(Symbol) || value.nil?)
    # %%% This is for the first symbol added to a JoinArray, cleaning out the leftover {} that is temporarily built out
    # when doing my_join_array[:value1][:value2] = nil.
    idx = -1
    delete_at(idx) if value.nil? && any? { |x| idx += 1; x.is_a?(::Brick::JoinHash) && x.empty? }

    set_matching(key, value)
  else
    super
  end
end

#_brick_setObject



42
# File 'lib/brick/join_array.rb', line 42

alias _brick_set []=

#add_parts(parts) ⇒ Object



169
170
171
172
173
# File 'lib/brick/join_array.rb', line 169

def add_parts(parts)
  s = self
  parts[0..-3].each { |part| s = s[part.to_sym] }
  s[parts[-2].to_sym] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
end

#set_matching(key, value) ⇒ Object



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
# File 'lib/brick/join_array.rb', line 119

def set_matching(key, value)
  idx = -1
  matching = find { |x| idx += 1; (x.is_a?(::Brick::JoinArray) && x.first == key) || (x.is_a?(::Brick::JoinHash) && x.key?(key)) || x == key }
  case matching
  when ::Brick::JoinHash
    matching[key] = value
  when Symbol
    if value.nil? # If it already exists then no worries
      matching
    else
      # Not yet there, so we will "graduate" this single value into being a key / value pair found in a JoinHash.  The
      # destination hash to be used will be either an existing one if there is a neighbouring JoinHash available, or a
      # newly-built one placed in the "new_hash" variable if none yet exists.
      hash = find { |x| x.is_a?(::Brick::JoinHash) } || (new_hash = ::Brick::JoinHash.new)
      hash._brick_store(key, ::Brick::JoinArray.new.tap { |val_array| val_array.replace([value]) })
      # hash.instance_variable_set(:@parent, matching.parent) if matching.parent
      # hash.instance_variable_set(:@parent_key, matching.parent_key) if matching.parent_key

      # When a new JoinHash was created, we place it at the same index where the original lone symbol value was pulled from.
      # If instead we used an existing JoinHash then since that symbol has now been graduated into a new key / value pair in
      # the existing JoinHash then we delete the original symbol by its index.
      new_hash ? _brick_set(idx, new_hash) : delete_at(idx)
    end
  when ::Brick::JoinArray # Replace this single thing (usually a Symbol found as a value in a JoinHash)
    (hash = ::Brick::JoinHash.new)._brick_store(key, value)
    if matching.parent
      hash.instance_variable_set(:@parent, matching.parent)
      hash.instance_variable_set(:@parent_key, matching.parent_key)
    end
    _brick_set(idx, hash)
  else # Doesn't already exist anywhere, so add it to the end of this JoinArray and return the new member
    if value
      ::Brick::JoinHash.new.tap do |hash|
        val_collection = if value.is_a?(::Brick::JoinHash)
                           value
                         else
                           ::Brick::JoinArray.new.tap { |array| array.replace([value]) }
                         end
        val_collection.instance_variable_set(:@parent, hash)
        val_collection.instance_variable_set(:@parent_key, key)
        hash._brick_store(key, val_collection)
        hash.instance_variable_set(:@parent, self)
        hash.instance_variable_set(:@parent_key, length)
      end
    else
      key
    end.tap { |member| push(member) }
  end
end