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
-
#deep_stored_accessors ⇒ Object
readonly
Returns the value of attribute deep_stored_accessors.
Instance Method Summary collapse
-
#cast_type_from_name(name) ⇒ ActiveRecord::Type::Value
Determines the data type for serialization based on the value type.
-
#cast_type_name_from_value(value) ⇒ Symbol
Determines the data type name based on the value.
-
#clear_deep_store_changes_information ⇒ void
Clears deep store changes information.
-
#deep_accessor_name(accessor_name, key) ⇒ String
Generates a unique name for accessor methods based on the accessor name and key.
-
#deep_store(accessor_name, payload, suffix: true, column_required: true) ⇒ void
Defines behavior for storing deeply nested data in a database column.
-
#deep_store_accessor(accessor_name, payload, key, value, suffix) ⇒ void
Defines accessor methods for nested keys within the deep store hash.
-
#deep_stores ⇒ Array<String>
Retrieves or initializes the array containing names of attributes declared as deep stores.
-
#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.
-
#reload ⇒ void
Reloads the model instance and clears deep store changes information.
-
#store_json_accessor(accessor_name, hash, key, suffix) ⇒ void
Defines accessor methods for individual keys within the nested hash.
Instance Attribute Details
#deep_stored_accessors ⇒ Object (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_information ⇒ void
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.
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_stores ⇒ Array<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 |
#reload ⇒ void
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 |