Module: ActiveRecord::Deepstore

Defined in:
lib/active_record/deepstore.rb,
lib/active_record/deepstore/version.rb

Overview

The ActiveRecord::Deepstore module extends ActiveRecord models with additional functionality for handling deeply nested data structures within a database column.

Defined Under Namespace

Classes: Error

Constant Summary collapse

VERSION =
"0.1.2"

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#deep_stored_accessorsObject (readonly)

Returns the value of attribute deep_stored_accessors.



12
13
14
# File 'lib/active_record/deepstore.rb', line 12

def deep_stored_accessors
  @deep_stored_accessors
end

Instance Method Details

#cast_type_from_name(name) ⇒ ActiveRecord::Type::Value

Determines the data type for serialization based on the value type.



211
212
213
# File 'lib/active_record/deepstore.rb', line 211

def cast_type_from_name(name)
  ActiveRecord::Type.lookup name.to_sym, adapter: ActiveRecord::Type.adapter_name_from(self)
end

#cast_type_name_from_value(value) ⇒ Symbol

Determines the data type name based on the value.



219
220
221
222
223
224
225
226
227
228
# File 'lib/active_record/deepstore.rb', line 219

def cast_type_name_from_value(value)
  type_mappings = {
    TrueClass => :boolean,
    FalseClass => :boolean,
    NilClass => :string,
    Hash => :text
  }

  type_mappings.fetch(value.class, value.class.name.underscore.to_sym)
end

#clear_deep_store_changes_informationvoid

This method returns an undefined value.

Clears deep store changes information.



140
141
142
143
144
145
# File 'lib/active_record/deepstore.rb', line 140

define_method(:clear_deep_store_changes_information) do
  self.class.deep_stored_accessors.each do |accessor|
    formatted_accessor = accessor.to_s.parameterize.underscore
    instance_variable_set(:"@#{formatted_accessor}_was", send(formatted_accessor))
  end
end

#deep_accessor_name(accessor_name, key) ⇒ String

Generates a unique name for accessor methods based on the accessor name and key.



203
204
205
# File 'lib/active_record/deepstore.rb', line 203

def deep_accessor_name(accessor_name, key)
  "#{key.to_s.parameterize.underscore}_#{accessor_name.to_s.parameterize.underscore}"
end

#deep_store(accessor_name, payload, suffix: true, column_required: true) ⇒ void

This method returns an undefined value.

Defines behavior for storing deeply nested data in a database column.

Raises:

  • (ActiveRecord::Deepstore::Error)

    If the deep store is already declared.

  • (NotImplementedError)

    If the required column is not found in the database table.



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
# File 'lib/active_record/deepstore.rb', line 54

def deep_store(accessor_name, payload, suffix: true, column_required: true)
  accessor_name = accessor_name.to_s.parameterize.underscore

  raise Error, "Deep store '#{accessor_name}' is already declared" if deep_stores.include?(accessor_name)

  @deep_stores << accessor_name

  if column_required && (columns.find do |c|
                           c.name == accessor_name.to_s
                         end).blank?
    raise NotImplementedError,
          "Column #{accessor_name} not found for table #{table_name}"
  end

  serialize accessor_name, type: Hash, default: payload, yaml: { unsafe_load: true } if payload.is_a?(Hash)

  define_method(:"default_#{accessor_name}") { payload.try(:with_indifferent_access) || payload }

  define_method(:"reset_#{accessor_name}") { assign_attributes(accessor_name => send(:"default_#{accessor_name}")) }

  define_method(:"reset_#{accessor_name}!") { update(accessor_name => send(:"default_#{accessor_name}")) }

  define_method(:"#{accessor_name}_changes") do
    old_value = send(:"#{accessor_name}_was")
    current_value = send(accessor_name)
    old_value == current_value ? {} : { old_value => current_value }
  end

  define_method(:"#{accessor_name}=") do |value|
    old_value = send(:"#{accessor_name}_was")

    if value.is_a?(Hash)
      value = {}.with_indifferent_access if value.blank?
      self.class.leaves(value).each do |leaf_path, leaf_value|
        default_value = leaf_path.inject(payload.with_indifferent_access) do |h, key|
          h.is_a?(Hash) ? h.fetch(key, h) : h
        end
        cast_type = self.class.cast_type_from_name(self.class.cast_type_name_from_value(default_value))

        # Traverse the hash using the leaf path and update the leaf value.
        leaf_key = leaf_path.pop
        parent_hash = leaf_path.inject(value, :[])
        #  old_leaf_value = parent_hash[leaf_key]
        new_leaf_value = cast_type.cast(leaf_value)
        old_parent_hash = parent_hash.dup
        parent_hash[leaf_key] = new_leaf_value

        instance_variable_set(:"@#{leaf_path.join("_")}_#{accessor_name}_was",
                              old_parent_hash.with_indifferent_access)
      end

      formatted_value = payload.with_indifferent_access.deep_merge(value)
    else
      default_value = send(:"default_#{accessor_name}")
      cast_type = self.class.cast_type_from_name(self.class.cast_type_name_from_value(default_value))
      formatted_value = cast_type.cast(value)
    end

    instance_variable_set(:"@#{accessor_name}_was", old_value)

    super(formatted_value)
  end
  # rubocop:enable Metrics/AbcSize
  # rubocop:enable Metrics/CyclomaticComplexity
  # rubocop:enable Metrics/MethodLength
  # rubocop:enable Metrics/PerceivedComplexity

  return unless payload.is_a?(Hash)

  payload.each do |key, value|
    deep_store_accessor(accessor_name, payload, key, value, suffix)
  end
end

#deep_store_accessor(accessor_name, payload, key, value, suffix) ⇒ void

This method returns an undefined value.

Defines accessor methods for nested keys within the deep store hash.



155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/active_record/deepstore.rb', line 155

def deep_store_accessor(accessor_name, payload, key, value, suffix)
  store_json_accessor(accessor_name, payload, key, suffix)

  deep_store(deep_accessor_name(accessor_name, key), value, column_required: false)

  return if value.is_a?(Hash)

  define_method(deep_accessor_name(accessor_name, key)) do
    return value unless (hash = public_send(accessor_name)).is_a?(Hash) && hash.key?(key)

    hash[key]
  end
end

#deep_storesArray<String>

Retrieves or initializes the array containing names of attributes declared as deep stores.



17
18
19
# File 'lib/active_record/deepstore.rb', line 17

def deep_stores
  @deep_stores ||= []
end

#leaves(hash, path: [], current_depth: 0, max_depth: nil) ⇒ Hash

Recursively traverses a nested hash and returns a flattened representation of leaf nodes along with their paths.



28
29
30
31
32
33
34
35
36
37
38
# File 'lib/active_record/deepstore.rb', line 28

def leaves(hash, path: [], current_depth: 0, max_depth: nil)
  hash.each_with_object({}) do |(key, value), result|
    current_path = path + [key]

    if value.is_a?(Hash) && (max_depth.nil? || current_depth < max_depth)
      result.merge!(leaves(value, path: current_path, current_depth: current_depth + 1, max_depth: max_depth))
    else
      result[current_path] = value
    end
  end
end

#reloadvoid

This method returns an undefined value.

Reloads the model instance and clears deep store changes information.



132
133
134
135
# File 'lib/active_record/deepstore.rb', line 132

define_method(:reload) do |*args|
  clear_deep_store_changes_information
  super(*args)
end

#store_json_accessor(accessor_name, hash, key, suffix) ⇒ void

This method returns an undefined value.

Defines accessor methods for individual keys within the nested hash.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/active_record/deepstore.rb', line 178

def store_json_accessor(accessor_name, hash, key, suffix)
  store_accessor(accessor_name.to_sym, key, suffix: suffix)
  base_method_name = deep_accessor_name(accessor_name, key)
  @deep_stored_accessors ||= []
  @deep_stored_accessors << base_method_name
  attribute base_method_name, cast_type_name_from_value(hash[key])

  define_method(:"#{base_method_name}_was") do
    method_name = :"#{base_method_name}_was"
    return instance_variable_get("@#{method_name}") if instance_variable_defined?("@#{method_name}")

    instance_variable_set("@#{method_name}", send(base_method_name))
  end

  define_method(:"#{base_method_name}_changed?") do
    send(:"#{base_method_name}_changes").any?
  end
end