store_model 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5678f8d809e7ed7745c5bd21b10bb455ecef32fac89cbc0fe9d5928c99dcf7c8
4
- data.tar.gz: 13fd4acba36ff7b96d8111043de74e94442f63a02d718571fdda3682521e0dab
3
+ metadata.gz: a3b12509ebafeb20d7a2bcb2ae1094dcd756f1b86f31dd8066b0abd61d8f998a
4
+ data.tar.gz: d3a06944b46ebf4094de55736e892f15ae27514e0604047260e74a033bbc7cda
5
5
  SHA512:
6
- metadata.gz: e57e9ebc4b7e6547c75c8b0375742194eafa6104d2abfd2b55e9e657c64e2af935e99505bb712e8e1ef3d38123f4852d89603f10bfacfa620ec2cb38751995b6
7
- data.tar.gz: a2c9e761568bc100697c7240762ed27aac4d8ccde2429f4e037357121351f120a1b9a17909f6670f0215691f909032131979ebbe0001d67bba1d2cadeebaadeb
6
+ metadata.gz: 7d8ec95caf59aea39e3cedc20a471db9289f4c1eda9d083a1b2fd321f732e7d056b67ac58a7f4f437cb04e932c1f14f18e56dfb9b04c884e5d64025cc7d7989b
7
+ data.tar.gz: e526dbc89d1c4186b1fe94581acb2b1d80e207bcf08d69809dffc393d43e3d8d2cd08c905d3d3d4f842c9bf7db9ad1581611368fabe8206eb0445c16a2c615eb
data/README.md CHANGED
@@ -76,7 +76,49 @@ product.save
76
76
  _Usage note: Rails and assigning Arrays/Hashes to records_
77
77
 
78
78
  - Assigned attributes must be a String, Hash, Array of Hashes, or StoreModel. For example, if the attributes are coming from a controller, be sure to convert any ActionController::Parameters as needed.
79
- - Any changes made to a StoreModel instance requires the attribute be re-assigned; Rails doesn't track mutations of objects. For example: `self.my_stored_models = my_stored_models.map(&:as_json)`
79
+ - Any changes made to a StoreModel instance requires the attribute be flagged as dirty, either by reassignment (`self.my_stored_models = my_stored_models.map(&:as_json)`) or by `will_change!` (`self.my_stored_models_will_change!`)
80
+ - Mixing `StoreModel::NestedAttributes` into your model will allow you to use `accepts_nested_attributes_for` in the same way as ActiveRecord.
81
+
82
+ ```ruby
83
+ class Supplier < ActiveRecord::Base
84
+ include StoreModel::NestedAttributes
85
+
86
+ has_many :bicycles, dependent: :destroy
87
+
88
+ attribute :products, Product.to_array_type
89
+
90
+ accepts_nested_attributes_for :bicycles, :products, allow_destroy: true
91
+ end
92
+ ```
93
+
94
+ This will allow the form builders to work their magic:
95
+
96
+ ```erb
97
+ <%= form_with model: @supplier do |form| %>
98
+ <%= form.fields_for :products do |product_fields| %>
99
+ <%= product_fields.text_field :name %>
100
+ <% end %>
101
+ <% end %>
102
+ ```
103
+
104
+ Resulting in:
105
+ ```html
106
+ <input type="text" name="supplier[products_attributes][0][name]" id="supplier_products_attributes_0_name">
107
+ ```
108
+
109
+ In the controller:
110
+ ```ruby
111
+ def create
112
+ @supplier = Supplier.new(supplier_params)
113
+ @supplier.save
114
+ end
115
+
116
+ private
117
+
118
+ def supplier_params
119
+ params.require(:supplier).permit(products_attributes: [:name])
120
+ end
121
+ ```
80
122
 
81
123
  ## Documentation
82
124
 
@@ -15,6 +15,10 @@ module StoreModel
15
15
  # @return [Boolean]
16
16
  attr_accessor :serialize_unknown_attributes
17
17
 
18
+ # Controls if the result of `as_json` will serialize enum fiels using `as_json`
19
+ # @return [Boolean]
20
+ attr_accessor :serialize_enums_using_as_json
21
+
18
22
  def initialize
19
23
  @serialize_unknown_attributes = true
20
24
  end
@@ -7,7 +7,7 @@ require "store_model/nested_attributes"
7
7
 
8
8
  module StoreModel
9
9
  # When included into class configures it to handle JSON column
10
- module Model
10
+ module Model # rubocop:disable Metrics/ModuleLength
11
11
  def self.included(base) # :nodoc:
12
12
  base.include ActiveModel::Model
13
13
  base.include ActiveModel::Attributes
@@ -31,16 +31,27 @@ module StoreModel
31
31
  # @param options [Hash]
32
32
  #
33
33
  # @return [Hash]
34
- def as_json(options = {})
34
+ def as_json(options = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
35
35
  serialize_unknown_attributes = if options.key?(:serialize_unknown_attributes)
36
36
  options[:serialize_unknown_attributes]
37
37
  else
38
38
  StoreModel.config.serialize_unknown_attributes
39
39
  end
40
40
 
41
- result = attributes.with_indifferent_access
41
+ serialize_enums_using_as_json = if options.key?(:serialize_enums_using_as_json)
42
+ options[:serialize_enums_using_as_json]
43
+ else
44
+ StoreModel.config.serialize_enums_using_as_json
45
+ end
46
+
47
+ result = @attributes.keys.each_with_object({}) do |key, values|
48
+ values[key] = serialized_attribute(key)
49
+ end.with_indifferent_access
50
+
42
51
  result.merge!(unknown_attributes) if serialize_unknown_attributes
43
- result.as_json(options)
52
+ result.as_json(options).tap do |json|
53
+ serialize_enums!(json) if serialize_enums_using_as_json
54
+ end
44
55
  end
45
56
 
46
57
  # Returns an Object, similar to Hash#fetch, raises
@@ -167,17 +178,6 @@ module StoreModel
167
178
  @unknown_attributes ||= {}
168
179
  end
169
180
 
170
- # Serialized values for storing in db
171
- #
172
- # @return [Hash]
173
- def values_for_database
174
- result = @attributes.keys.each_with_object({}) do |key, values|
175
- values[key] = @attributes.fetch(key).value_for_database
176
- end
177
- result.merge!(unknown_attributes)
178
- result
179
- end
180
-
181
181
  private
182
182
 
183
183
  def attribute?(attribute)
@@ -186,5 +186,29 @@ module StoreModel
186
186
  else value.present?
187
187
  end
188
188
  end
189
+
190
+ def serialize_enums!(json)
191
+ enum_mappings =
192
+ self.class
193
+ .attribute_types
194
+ .select { |_, type| type.is_a?(StoreModel::Types::EnumType) }
195
+
196
+ enum_mappings.each do |name, _|
197
+ next unless json.key?(name)
198
+
199
+ json[name] = public_send(name).as_json unless json[name].nil?
200
+ end
201
+ end
202
+
203
+ def serialized_attribute(attr_name)
204
+ attr = @attributes.fetch(attr_name)
205
+ if attr.value.is_a? StoreModel::Model
206
+ Types::RawJSONEncoder.new(attr.value_for_database)
207
+ elsif attr.value.is_a? Array
208
+ attr.value.as_json
209
+ else
210
+ attr.value_for_database
211
+ end
212
+ end
189
213
  end
190
214
  end
@@ -13,27 +13,54 @@ module StoreModel
13
13
  # @param associations [Array] list of associations and options to define attributes, for example:
14
14
  # accepts_nested_attributes_for [:suppliers, allow_destroy: true]
15
15
  #
16
+ # Alternatively, use the standard Rails syntax:
17
+ #
18
+ # @param associations [Array] list of associations and attributes to define getters and setters.
19
+ #
20
+ # @param options [Hash] options not supported by StoreModel will still be passed to ActiveRecord.
21
+ #
16
22
  # Supported options:
17
23
  # [:allow_destroy]
18
24
  # If true, destroys any members from the attributes hash with a
19
25
  # <tt>_destroy</tt> key and a value that evaluates to +true+
20
26
  # (e.g. 1, '1', true, or 'true'). This option is off by default.
21
- def accepts_nested_attributes_for(*associations)
22
- associations.each do |association, options|
23
- case attribute_types[association.to_s]
24
- when Types::One
25
- define_association_setter_for_single(association, options)
26
- alias_method "#{association}_attributes=", "#{association}="
27
- when Types::Many
28
- define_association_setter_for_many(association, options)
29
- end
27
+ #
28
+ # [:reject_if]
29
+ # Allows you to specify a Proc or a Symbol pointing to a method that
30
+ # checks whether a record should be built for a certain attribute hash.
31
+ # The hash is passed to the supplied Proc or the method and it should
32
+ # return either true or false. Passing <tt>:all_blank</tt> instead of a Proc
33
+ # will create a proc that will reject a record where all the attributes
34
+ # are blank excluding any value for <tt>_destroy</tt>.
35
+ #
36
+ # See https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for
37
+ def accepts_nested_attributes_for(*attributes)
38
+ global_options = attributes.extract_options!
30
39
 
31
- define_attr_accessor_for_destroy(association, options)
40
+ attributes.each do |attribute, options|
41
+ case attribute_types[attribute.to_s]
42
+ when Types::OneBase, Types::ManyBase
43
+ define_store_model_attr_accessors(attribute, options || global_options)
44
+ else
45
+ super(attribute, options || global_options)
46
+ end
32
47
  end
33
48
  end
34
49
 
35
50
  private
36
51
 
52
+ def define_store_model_attr_accessors(attribute, options)
53
+ case attribute_types[attribute.to_s]
54
+ when Types::OneBase
55
+ define_association_setter_for_single(attribute, options)
56
+ alias_method "#{attribute}_attributes=", "#{attribute}="
57
+ when Types::ManyBase
58
+ define_association_setter_for_many(attribute, options)
59
+ end
60
+
61
+ define_attr_accessor_for_destroy(attribute, options)
62
+ end
63
+
37
64
  def define_attr_accessor_for_destroy(association, options)
38
65
  return unless options&.dig(:allow_destroy)
39
66
 
@@ -61,8 +88,6 @@ module StoreModel
61
88
  end
62
89
  end
63
90
 
64
- private
65
-
66
91
  def assign_nested_attributes_for_collection_association(association, attributes, options)
67
92
  attributes = attributes.values if attributes.is_a?(Hash)
68
93
 
@@ -72,7 +97,20 @@ module StoreModel
72
97
  end
73
98
  end
74
99
 
100
+ attributes.reject! { |attribute| call_reject_if(attribute, options[:reject_if]) } if options&.dig(:reject_if)
101
+
75
102
  send("#{association}=", attributes)
76
103
  end
104
+
105
+ def call_reject_if(attributes, callback)
106
+ callback = ActiveRecord::NestedAttributes::ClassMethods::REJECT_ALL_BLANK_PROC if callback == :all_blank
107
+
108
+ case callback
109
+ when Symbol
110
+ method(callback).arity.zero? ? send(callback) : send(callback, attributes)
111
+ when Proc
112
+ callback.call(attributes)
113
+ end
114
+ end
77
115
  end
78
116
  end
@@ -46,9 +46,7 @@ module StoreModel
46
46
  # @return [String] serialized value
47
47
  def serialize(value)
48
48
  case value
49
- when @model_klass
50
- ActiveSupport::JSON.encode(value.values_for_database)
51
- when Hash
49
+ when @model_klass, Hash
52
50
  ActiveSupport::JSON.encode(value)
53
51
  else
54
52
  super
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StoreModel
4
+ module Types
5
+ # Implements #encode_json and #as_json methods.
6
+ # By wrapping serialized objects in this type, it prevents duplicate
7
+ # JSON serialization passes on nested object. It is named as Encoder
8
+ # as it will not work to inflate typed attributes and is intended
9
+ # to be used internally.
10
+ class RawJSONEncoder < String
11
+ def encode_json(_encoder)
12
+ self
13
+ end
14
+
15
+ def as_json(_options = nil)
16
+ JSON.parse(self)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -14,6 +14,8 @@ require "store_model/types/enum_type"
14
14
 
15
15
  require "store_model/types/one_of"
16
16
 
17
+ require "store_model/types/raw_json"
18
+
17
19
  module StoreModel
18
20
  # Contains all custom types.
19
21
  module Types
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StoreModel # :nodoc:
4
- VERSION = "2.0.1"
4
+ VERSION = "2.1.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: store_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-09 00:00:00.000000000 Z
11
+ date: 2023-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -115,6 +115,7 @@ files:
115
115
  - lib/store_model/types/one_of.rb
116
116
  - lib/store_model/types/one_polymorphic.rb
117
117
  - lib/store_model/types/polymorphic_helper.rb
118
+ - lib/store_model/types/raw_json.rb
118
119
  - lib/store_model/version.rb
119
120
  homepage: https://github.com/DmitryTsepelev/store_model
120
121
  licenses: