Module: JsonbAccessor::Macro::ClassMethods

Defined in:
lib/jsonb_accessor/macro.rb

Instance Method Summary collapse

Instance Method Details

#jsonb_accessor(jsonb_attribute, field_types) ⇒ Object



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
# File 'lib/jsonb_accessor/macro.rb', line 6

def jsonb_accessor(jsonb_attribute, field_types)
  names_and_store_keys = field_types.each_with_object({}) do |(name, type), mapping|
    _type, options = Array(type)
    mapping[name.to_s] = (options.try(:delete, :store_key) || name).to_s
  end

  # Defines virtual attributes for each jsonb field.
  field_types.each do |name, type|
    next attribute name, type unless type.is_a?(Array)
    next attribute name, *type unless type.last.is_a?(Hash)

    *args, keyword_args = type
    attribute name, *args, **keyword_args
  end

  store_key_mapping_method_name = "jsonb_store_key_mapping_for_#{jsonb_attribute}"
  # Defines methods on the model class
  class_methods = Module.new do
    # Allows us to get a mapping of field names to store keys scoped to the column
    define_method(store_key_mapping_method_name) do
      superclass_mapping = superclass.try(store_key_mapping_method_name) || {}
      superclass_mapping.merge(names_and_store_keys)
    end
  end
  # We extend with class methods here so we can use the results of methods it defines to define more useful methods later
  extend class_methods

  # Get field names to default values mapping
  names_and_defaults = field_types.each_with_object({}) do |(name, type), mapping|
    _type, options = Array(type)
    field_default = options.try(:delete, :default)
    mapping[name.to_s] = field_default unless field_default.nil?
  end

  # Get store keys to default values mapping
  store_keys_and_defaults = ::JsonbAccessor::QueryHelper.convert_keys_to_store_keys(names_and_defaults, public_send(store_key_mapping_method_name))

  # Define jsonb_defaults_mapping_for_<jsonb_attribute>
  defaults_mapping_method_name = "jsonb_defaults_mapping_for_#{jsonb_attribute}"
  class_methods.instance_eval do
    define_method(defaults_mapping_method_name) do
      superclass_mapping = superclass.try(defaults_mapping_method_name) || {}
      superclass_mapping.merge(store_keys_and_defaults)
    end
  end

  all_defaults_mapping = public_send(defaults_mapping_method_name)
  # Fields may have procs as default value. This means `all_defaults_mapping` may contain procs as values. To make this work
  # with the attributes API, we need to wrap `all_defaults_mapping` with a proc itself, making sure it returns a plain hash
  # each time it is evaluated.
  all_defaults_mapping_proc =
    if all_defaults_mapping.present?
      -> { all_defaults_mapping.transform_values { |value| value.respond_to?(:call) ? value.call : value }.to_h.compact }
    end
  attribute jsonb_attribute, :jsonb, default: all_defaults_mapping_proc if all_defaults_mapping_proc.present?

  # Setters are in a module to allow users to override them and still be able to use `super`.
  setters = Module.new do
    # Overrides the setter created by `attribute` above to make sure the jsonb attribute is kept in sync.
    names_and_store_keys.each do |name, store_key|
      define_method("#{name}=") do |value|
        super(value)
        new_values = (public_send(jsonb_attribute) || {}).merge(store_key => public_send(name))
        write_attribute(jsonb_attribute, new_values)
      end
    end

    # Overrides the jsonb attribute setter to make sure the jsonb fields are kept in sync.
    define_method("#{jsonb_attribute}=") do |value|
      value ||= {}
      names_to_store_keys = self.class.public_send(store_key_mapping_method_name)
      store_keys_to_names = names_to_store_keys.invert

      # this is the raw hash we want to save in the jsonb_attribute
      value_with_store_keys = value.transform_keys do |k|
        names_to_store_keys[k.to_s] || k
      end
      write_attribute(jsonb_attribute, value_with_store_keys)

      # this maps attributes to values
      value_with_named_keys = value.transform_keys do |k|
        store_keys_to_names[k.to_s] || k
      end

      empty_named_attributes = names_to_store_keys.transform_values { nil }
      empty_named_attributes.merge(value_with_named_keys).each do |name, attribute_value|
        write_attribute(name, attribute_value)
      end
    end
  end
  include setters

  # Makes sure new objects have the appropriate values in their jsonb fields.
  after_initialize do
    if has_attribute?(jsonb_attribute)
      jsonb_values = public_send(jsonb_attribute) || {}
      jsonb_values.each do |store_key, value|
        name = names_and_store_keys.key(store_key)
        next unless name

        write_attribute(name, value)
        clear_attribute_change(name) if persisted?
      end
    end
  end

  # <jsonb_attribute>_where scope
  scope("#{jsonb_attribute}_where", lambda do |attributes|
    store_key_attributes = ::JsonbAccessor::QueryHelper.convert_keys_to_store_keys(attributes, all.model.public_send(store_key_mapping_method_name))
    jsonb_where(jsonb_attribute, store_key_attributes)
  end)

  # <jsonb_attribute>_where_not scope
  scope("#{jsonb_attribute}_where_not", lambda do |attributes|
    store_key_attributes = ::JsonbAccessor::QueryHelper.convert_keys_to_store_keys(attributes, all.model.public_send(store_key_mapping_method_name))
    jsonb_where_not(jsonb_attribute, store_key_attributes)
  end)

  # <jsonb_attribute>_order scope
  scope("#{jsonb_attribute}_order", lambda do |*args|
    ordering_options = args.extract_options!
    order_by_defaults = args.each_with_object({}) { |attribute, config| config[attribute] = :asc }
    store_key_mapping = all.model.public_send(store_key_mapping_method_name)

    order_by_defaults.merge(ordering_options).reduce(all) do |query, (name, direction)|
      key = store_key_mapping[name.to_s]
      order_query = jsonb_order(jsonb_attribute, key, direction)
      query.merge(order_query)
    end
  end)
end