File: macro.rb

package info (click to toggle)
ruby-jsonb-accessor 1.4.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 316 kB
  • sloc: ruby: 1,819; makefile: 10; sh: 5
file content (139 lines) | stat: -rw-r--r-- 6,913 bytes parent folder | download
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# frozen_string_literal: true

module JsonbAccessor
  module Macro
    module ClassMethods
      def jsonb_accessor(jsonb_attribute, global_options = {}, **definitions)
        names_and_store_keys = {}
        names_and_defaults = {}
        names_and_attribute_names = {}

        definitions.each do |name, value|
          args = Array(value)
          options = args.last.is_a?(Hash) ? args.pop : {}

          # Determine store keys and default values for each field
          names_and_store_keys[name.to_s] = (options.delete(:store_key) || name).to_s
          names_and_defaults[name.to_s] = options.delete(:default) unless options[:default].nil?

          prefix = options.delete(:prefix) || global_options[:prefix]
          suffix = options.delete(:suffix) || global_options[:suffix]
          attribute_name = JsonbAccessor::Helpers.define_attribute_name(jsonb_attribute, name, prefix, suffix)

          # Define virtual attributes for each field
          names_and_attribute_names[name.to_s] = attribute_name
          attribute attribute_name, *args, **options
        end

        store_key_mapping_method_name = "jsonb_store_key_mapping_for_#{jsonb_attribute}"
        attribute_name_mapping_method_name = "jsonb_attribute_name_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

          # Allows us to get a mapping of field names to attribute names scoped to the column
          define_method(attribute_name_mapping_method_name) do
            superclass_mapping = superclass.try(attribute_name_mapping_method_name) || {}
            superclass_mapping.merge(names_and_attribute_names)
          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 store keys to default values mapping
        store_keys_and_defaults = JsonbAccessor::Helpers.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|
            attribute_name = names_and_attribute_names[name]

            define_method("#{attribute_name}=") do |value|
              super(value)

              # If enum was defined, take the value from the enum and not what comes out directly from the getter
              attribute_value = defined_enums[attribute_name].present? ? defined_enums[attribute_name][value] : public_send(attribute_name)

              # Rails always saves time based on `default_timezone`. Since #as_json considers timezone, manual conversion is needed
              if attribute_value.acts_like?(:time)
                attribute_value = (JsonbAccessor::Helpers.active_record_default_timezone == :utc ? attribute_value.utc : attribute_value.in_time_zone).strftime("%F %R:%S.%L")
              end

              new_values = (public_send(jsonb_attribute) || {}).merge(store_key => attribute_value)
              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)
            names_to_attribute_names = self.class.public_send(attribute_name_mapping_method_name)

            # this is the raw hash we want to save in the jsonb_attribute
            value_with_store_keys = JsonbAccessor::Helpers.convert_keys_to_store_keys(value, names_to_store_keys)
            write_attribute(jsonb_attribute, value_with_store_keys)

            # this maps attributes to values
            value_with_named_keys = JsonbAccessor::Helpers.convert_store_keys_to_keys(value, names_to_store_keys)

            empty_named_attributes = names_to_store_keys.transform_values { nil }
            empty_named_attributes.merge(value_with_named_keys).each do |name, attribute_value|
              # Only proceed if this attribute has been defined using `jsonb_accessor`.
              next unless names_to_store_keys.key?(name)

              attribute_name = names_to_attribute_names[name]
              write_attribute(attribute_name, attribute_value)
            end
          end
        end
        include setters

        # Makes sure new objects have the appropriate values in their jsonb fields.
        after_initialize do
          next unless 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)
            attribute_name = names_and_attribute_names[name]

            next unless attribute_name

            write_attribute(
              attribute_name,
              JsonbAccessor::Helpers.deserialize_value(value, self.class.type_for_attribute(attribute_name).type)
            )
            clear_attribute_change(attribute_name) if persisted?
          end
        end

        JsonbAccessor::AttributeQueryMethods.new(self).define(store_key_mapping_method_name, jsonb_attribute)
      end
    end
  end
end