summaryrefslogtreecommitdiff
path: root/lib/has_array_field.rb
blob: 2381e4f5997e2dd16fa0636a1298cd3588d5b3af (plain)
1
2
3
4
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
# frozen_string_literal: true

module HasArrayField
  extend ActiveSupport::Concern

  class_methods do
    def has_array_field(accessor_name, klass = nil)
      klass ||= accessor_name.to_s.singularize.capitalize.constantize
      column_name = "#{accessor_name.to_s.singularize}_ids".to_sym

      define_method(accessor_name) do
        _get_ary_accessor(column_name, klass)
      end

      define_method("#{accessor_name}=") do |vals|
        _set_ary_vals(column_name, klass, vals)
      end

      define_method("#{column_name}=") do |ids|
        @_array_accessors ||= {}
        @_array_accessors[column_name] = nil
        super(ids)
      end

      after_reload do
        @_array_accessors = {}
      end
    end
  end

  def _get_ary_accessor(column_name, klass)
    @_array_accessors ||= {}
    @_array_accessors[column_name] ||= ArrayFieldAccessor.new(self, column_name, klass)
  end

  def _set_ary_vals(column_name, klass, vals)
    @_array_accessors ||= {}
    new_vals = vals.to_a.flatten.compact.select { |v| v.is_a?(klass) && v.persisted? }
    send("#{column_name}=", new_vals.map(&:id))
    @_array_accessors[column_name] = ArrayFieldAccessor.new(self, column_name, klass, new_vals)
    new_vals
  end

  class ArrayFieldAccessor
    def initialize(on_obj, column_name, klass, records = nil)
      @on_obj = on_obj
      @column_name = column_name
      @klass = klass
      @loaded = !records.nil?
      @records = records
    end

    def base_query
      @klass.where(id: @on_obj.send(@column_name))
    end

    def push(objs)
      modify_ary_with(objs) do |existing, new|
        existing + new
      end
    end
    alias << push

    def delete(obj)
      modify_ary_with(obj) do |existing, new|
        existing - new
      end
    end

    def inspect
      if @loaded
        @records.inspect
      else
        base_query.inspect
      end
    end

    def method_missing(name, *args, &block)
      result = nil

      if @loaded
        # Try running method against loaded Array.
        result = if @records.respond_to? name
          @records.send(name, *args, &block)
        else
          base_query.send(name, *args, &block)
        end
      elsif [].respond_to?(name) && (name.to_sym != :first)
        # Load records now.
        @records = base_query.send(:to_a)
        @loaded = true
        result = @records.send(name, *args, &block)
      else
        # Proxy response to base_query.
        result = base_query.send(name, *args, &block)
      end

      result
    end

    private

    def modify_ary_with(*objs)
      if @loaded
        @on_obj._set_ary_vals(@column_name, @klass, (yield @records, objs))
      else
        record_ids = @on_obj.send(@column_name)
        @on_obj.send("#{@column_name}=", (yield record_ids, objs.map(&:id)))
      end
      true # to prevent chaining
    end
  end
end

ActiveRecord::Base.send(:include, HasArrayField)