store_model 2.0.1 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: