Module: Virtus

Defined in:
lib/virtus/relations.rb,
lib/virtus/relations/version.rb

Defined Under Namespace

Modules: Relations

Class Method Summary collapse

Class Method Details

.relations(params = {}) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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
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
# File 'lib/virtus/relations.rb', line 5

def self.relations(params = {})
  rel_name = params.fetch(:as, :parent)
  fail TypeError, 'Symbol expected' unless rel_name.is_a?(Symbol)

  mod = Module.new
  mod.instance_variable_set(:@rel_name, rel_name)

  mod.module_eval do
    def self.class_methods
      Module.new do
        # During instantiation, .relation_attributes is called
        # If class was instantiated before, just return the list of related
        # attributes
        #
        # If instantiated for the first time, then monkey-patch some instance
        # methods to allow relation attributes to work with accessor methods.
        #
        #
        # patch Model#attribute=
        #
        #   1. create {list}
        #   2. for each {attribute} in {list}
        #   2.1. patch the accessor method
        #   2.2. patch the lazy initializer method (if any)
        #
        def relation_attributes
          @relation_attributes ||= attribute_set.select do |attribute|
            next unless attribute.options[:relation] == true

            patch_accessor_method(attribute)
            patch_lazy_initializer(attribute)
            true
          end
        end

        #
        # When the original method returns something, we need to
        # actually set ourselves as its parent
        # If that something is a collection, do it for each element
        #
        def relate(object, method_return)
          if object.kind_of?(Array)
            object.each { |o| relate(o, method_return) }
          else
            object.define_singleton_method(relation_name) { method_return }
            object
          end
        end


        protected
        #   1. rename method "{attribute}=" to "{attribute}_not_related="
        #   2. define method "{attribute}=", which:
        #     2.1. calls "{attribute}_not_related=", takes the {return_object}
        #     2.2. defines {return_object}.parent which returns self
        #     2.3. returns {return_object}
        #   3. return the list
        def patch_accessor_method(attribute)
          if [Numeric, Symbol].any? { |c| attribute.primitive.ancestors.include?(c) }
            fail "Relations don't work with Numeric and Symbol types"
          end

          old_method = "#{attribute.name}_not_related="
          new_method = "#{attribute.name}="

          # Suffix the original method with '_not_related'
          define_method(old_method, instance_method(new_method))
          define_method(new_method) do |value|
            return_value = send(old_method, value)
            self.class.relate(return_value, self)
          end

          private old_method
          visibility = attribute.options[:writer]
          send(visibility, new_method)
        end

        # (see above for the explanation of step 2.)
        #   Wrap the original method/proc into a new one, which:
        #   1. calls the original one, takes the {return_object}
        #   2. coerces {return_object}
        #   3. defines {return_object}.parent which returns self
        #   4. returns {return_object}
        #
        def patch_lazy_initializer(attribute)
          return unless attribute.lazy?

          case attribute.default_value.value
          when Proc
            old_proc = attribute.default_value.value
            new_proc = proc do |object, *args|
              return_value = attribute.coerce(old_proc.call(*[object, *args]))
              object.class.relate(return_value, object)
            end

            attribute.default_value.instance_variable_set(:@value, new_proc)
          else
            old_method = "#{attribute.default_value.value}_not_related"
            new_method = attribute.default_value.value
            visibility = if private_method_defined?(new_method)
                           :private
                         elsif protected_method_defined?(new_method)
                           :protected
                         else
                           :public
                         end

            define_method(old_method, instance_method(new_method))
            define_method(new_method) do
              return_value = attribute.coerce(send(old_method))
              self.class.relate(return_value, self)
              return_value
            end

            private old_method
            send(visibility, new_method)
          end
        end
      end # Module.new
    end # def

    def self.instance_methods
      Module.new do
        #
        # Enhance the initialize process to allow
        # related attributes to work with mass-assignment
        #
        def initialize(mass_assignment_attributes = {})
          super

          self.class.relation_attributes.each do |ra|
            # set self as the child's parent only if child was mass-
            # assigned during self.initialize (e.g. hash has child's key)
            # This prevents children from doing this against the parents
            # when they also have relation: true on their attributes
            # (since their initializers will not receive the parent as
            #  a part of the mass-assignment)

            if mass_assignment_attributes.key?(ra.name)
              self.class.relate(ra.get(self), self)
            end
          end
        end

        alias_method :dup_not_related, :dup

        # Add #parent to duped objects, when available
        def dup
          rel = self.class.relation_name
          if respond_to?(rel)
            self.class.relate(dup_not_related, rel)
          else
            dup_not_related
          end
        end
      end # Module.new
    end # def

    def self.included(base)
      required = [
        Virtus::InstanceMethods::MassAssignment,
        Virtus::InstanceMethods::Constructor
      ]

      unless required.all? { |mod| base.included_modules.include?(mod) }
        fail 'Virtus.model must be included prior to Virtus.relations'
      end

      # Using a local variable in the parent scope
      # with the same name as the singleton method
      # prevents recursion
      relation_name = @rel_name
      base.define_singleton_method(:relation_name) { relation_name }

      base.send(:include, instance_methods)
      base.extend(class_methods)
    end

    mod
  end
end