Module: AttrJson::Record
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/attr_json/record.rb,
lib/attr_json/record/query_scopes.rb,
lib/attr_json/record/query_builder.rb
Overview
The mix-in to provide AttrJson support to ActiveRecord::Base models.
We call it Record
instead of ActiveRecord
to avoid confusing namespace
shadowing errors, sorry!
Defined Under Namespace
Modules: QueryScopes Classes: QueryBuilder
Class Method Summary collapse
-
.attr_json(name, type, **options) ⇒ Object
Registers an attr_json attribute, and a Rails attribute covering it.
-
.attr_json_config(new_values = {}) ⇒ Object
Access or set class-wide json_attribute_config.
Instance Method Summary collapse
-
#attr_json_sync_to_rails_attributes ⇒ Object
Sync all values FROM the json_attributes json column TO rails attributes.
Class Method Details
.attr_json(name, type, **options) ⇒ Object
Registers an attr_json attribute, and a Rails attribute covering it.
Type can be a symbol that will be looked up in ActiveModel::Type.lookup
,
or an ActiveModel:::Type::Value).
attr_json_config.default_container_attribute
, which defaults to :json_attributes
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/attr_json/record.rb', line 157 def attr_json(name, type, **) = { validate: true, container_attribute: self.attr_json_config.default_container_attribute, }.merge!() .assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :accepts_nested_attributes]) container_attribute = [:container_attribute] # Make sure to "lazily" register attribute for *container* class if this is the first time # this container attribute hsa been encountered for this specific class. The registry # helps us keep track. Kinda messy, in future we may want a more explicit API # that does not require us to implicitly track first-time per-container. unless self.attr_json_registry.container_attribute_registered?(model: self, attribute_name: container_attribute.to_sym) attribute container_attribute.to_sym, AttrJson::Type::ContainerAttribute.new(self, container_attribute), default: -> { {} } self.attr_json_registry.register_container_attribute(model: self, attribute_name: container_attribute.to_sym) end self.attr_json_registry = attr_json_registry.with( AttributeDefinition.new(name.to_sym, type, .except(:validate, :accepts_nested_attributes)) ) # By default, automatically validate nested models, allowing nils. if type.kind_of?(AttrJson::Type::Model) && [:validate] # implementation adopted from: # https://github.com/rails/rails/blob/v7.0.4.1/activerecord/lib/active_record/validations/associated.rb#L6-L10 # # but had to customize to allow nils in an array through validates_each name.to_sym do |record, attr, value| if Array(value).reject { |element| element.nil? || element.valid? }.any? record.errors.add(attr, :invalid, value: value) end end end # Register as a Rails attribute attr_json_definition = attr_json_registry[name] attribute_args = attr_json_definition.has_default? ? { default: attr_json_definition.default_argument } : {} self.attribute name.to_sym, attr_json_definition.type, **attribute_args # For getter and setter, we consider the container has the "canonical" data location. # But setter also writes to rails attribute, and tries to keep them in sync with the # *same object*, so mutations happen to both places. # # This began roughly modelled on approach of Rail sstore_accessor implementation: # https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96 # # ...But wound up with lots of evolution to try to get dirty tracking working as well # as we could -- without a completely custom separate dirty tracking implementation # like store_accessor tries! _attr_jsons_module.module_eval do define_method("#{name}=") do |value| super(value) # should write to rails attribute # write to container hash, with value read from attribute to try to keep objects # sync'd to exact same object in rails attribute and container hash. attribute_def = self.class.attr_json_registry.fetch(name.to_sym) public_send(attribute_def.container_attribute)[attribute_def.store_key] = read_attribute(name) end define_method("#{name}") do # read from container hash -- we consider that the canonical location. attribute_def = self.class.attr_json_registry.fetch(name.to_sym) public_send(attribute_def.container_attribute)[attribute_def.store_key] end end accepts_nested_attributes = if .has_key?(:accepts_nested_attributes) [:accepts_nested_attributes] elsif attr_json_definition.single_model_type? || attr_json_definition.array_type? # use configured default only if we have a type appropriate for it! self.attr_json_config.default_accepts_nested_attributes else false end if accepts_nested_attributes = accepts_nested_attributes == true ? {} : accepts_nested_attributes self.attr_json_accepts_nested_attributes_for name, ** end end |
.attr_json_config(new_values = {}) ⇒ Object
Access or set class-wide json_attribute_config. Inherited by sub-classes, but setting on sub-classes is unique to subclass. Similar to how rails class_attribute's are used.
TODO make Model match please.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/attr_json/record.rb', line 103 def attr_json_config(new_values = {}) if new_values.present? # get one without new values, then merge new values into it, and # set it locally for this class. @attr_json_config = attr_json_config.merge(new_values) else if instance_variable_defined?("@attr_json_config") # we have a custom one for this class, return it. @attr_json_config elsif superclass.respond_to?(:attr_json_config) # return superclass without setting it locally, so changes in superclass # will continue effecting us. superclass.attr_json_config else # no superclass, no nothing, set it to blank one. @attr_json_config = Config.new(mode: :record) end end end |
Instance Method Details
#attr_json_sync_to_rails_attributes ⇒ Object
Sync all values FROM the json_attributes json column TO rails attributes
If values have for some reason gotten out of sync this will make them the identical objects again, with the container hash value being the source.
In some cases, the values may already be equivalent but different objects -- This is meant to ensure they are the same object in both places, so mutation of mutable object will effect both places, for instance for dirty tracking.
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 |
# File 'lib/attr_json/record.rb', line 52 def attr_json_sync_to_rails_attributes self.class.attr_json_registry.definitions.group_by(&:container_attribute).each_pair do |container_attribute, definitions| begin # column may have eg been left out of an explicit 'select' next unless has_attribute?(container_attribute) container_value = public_send(container_attribute) # isn't expected to be possible to be nil rather than empty hash, but # if it is from some edge case, well, we don't have values to sync, fine next if container_value.nil? definitions.each do |attribute_def| attr_name = attribute_def.name value = container_value[attribute_def.store_key] if value # TODO, can we just make this use the setter? write_attribute(attr_name, value) clear_attribute_change(attr_name) if persisted? # writing and clearning will result in a new object stored in # rails attributes, we want # to make sure the exact same object is in the json attribute, # so in-place mutation changes to it are reflected in both places. container_value[attribute_def.store_key] = read_attribute(attr_name) end end rescue AttrJson::Type::Model::BadCast, AttrJson::Type::PolymorphicModel::TypeError => e # There was bad data in the DB, we're just going to skip the Rails attribute sync. # Should we log? end end end |