store_model 4.4.0 → 4.6.0

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: fe31f43187a97021f54c79cbc8372b7e2b1c5a3c4bcb7b07b13def128bf69f34
4
- data.tar.gz: '0456813d7c47e3a9888c23bd83dc90c233f8919734766e348c064350c3d502ce'
3
+ metadata.gz: e69ab65c67b3cd46e0285be5860d8cd5b1d501b38e126f9833fbe01b3dda3ee8
4
+ data.tar.gz: 04b8d780feaaa6bfcb578465582b845d66960ba5f9df6e5d7aaa8d99f3d568bb
5
5
  SHA512:
6
- metadata.gz: 54a697b1a4782fa6bf3a6f84972789f6b42aab7ef9702e243d7fa4b9d7a6c035ea1421a560eb848083dd414b9bd594d577ca82a50642718d80607d742db64561
7
- data.tar.gz: d591bf765bb19d91cc7972762349075edbb5ec9d633f3b0e4b9c290069af132e35b9d011e4537de43c18f3de76ca863e8b0576d93502ac95b24e4abb250bedd3
6
+ metadata.gz: f8176e449148b7b7aa04596bf75b4932a156f737bac32395e79ae4ca5af9d6e186addb5c1de47b503f754b491f3a98a2ae081ce44fb8f3608c077f81fbcbe8d7
7
+ data.tar.gz: 9a771879567e9bbe266fa3825327469ac5b2e2008e7f90a7b608b72d28bbbb9704bd7f28e1498e4d65879c6014321a368557f54ec04fe23af0c7d4f8074d0613
data/README.md CHANGED
@@ -22,7 +22,10 @@ class Product < ApplicationRecord
22
22
  end
23
23
  ```
24
24
 
25
- You can support my open–source work [here](https://boosty.to/dmitry_tsepelev).
25
+ ## Professional Support
26
+
27
+ Working on a complex Rails application and need an experienced eye?
28
+ I'm available for consulting — [get in touch](https://dmitrytsepelev.dev/consulting).
26
29
 
27
30
  ## Why should I wrap my JSON columns?
28
31
 
@@ -122,6 +125,48 @@ def supplier_params
122
125
  end
123
126
  ```
124
127
 
128
+ ### ActiveAdmin Integration
129
+
130
+ If you're using [ActiveAdmin](https://activeadmin.info/), enable compatibility mode to make the `has_many` form helper work with StoreModel attributes:
131
+
132
+ ```ruby
133
+ # config/initializers/store_model.rb
134
+ StoreModel.config.active_admin_compatibility = true
135
+ ```
136
+
137
+ Example usage:
138
+
139
+ ```ruby
140
+ # app/models/supplier.rb
141
+ class Supplier
142
+ include StoreModel::Model
143
+
144
+ attribute :title, :string
145
+ attribute :address, :string
146
+ end
147
+
148
+ # app/models/product.rb
149
+ class Product < ApplicationRecord
150
+ include StoreModel::NestedAttributes
151
+
152
+ attribute :suppliers, Supplier.to_array_type
153
+ accepts_nested_attributes_for :suppliers, allow_destroy: true
154
+ end
155
+
156
+ # app/admin/products.rb
157
+ ActiveAdmin.register Product do
158
+ permit_params suppliers_attributes: [:title, :address, :_destroy]
159
+
160
+ form do |f|
161
+ f.has_many :suppliers, allow_destroy: true do |s|
162
+ s.input :title
163
+ s.input :address
164
+ end
165
+ f.actions
166
+ end
167
+ end
168
+ ```
169
+
125
170
  ## Documentation
126
171
 
127
172
  1. [Installation](./docs/installation.md)
@@ -32,11 +32,26 @@ module StoreModel
32
32
  # @return [Boolean]
33
33
  attr_accessor :enable_parent_assignment
34
34
 
35
+ # Controls if ActiveAdmin compatibility patches are applied.
36
+ # When enabled, adds methods like `new_record?` and `reflect_on_association`
37
+ # that are expected by ActiveAdmin's form builders.
38
+ # Default: false
39
+ # @return [Boolean]
40
+ attr_accessor :active_admin_compatibility
41
+
42
+ # Controls whether nested attributes updates preserve existing model instances by default.
43
+ # When true, acts like `update_only: true` for all singular nested attributes unless overridden.
44
+ # Default: false
45
+ # @return [Boolean]
46
+ attr_accessor :nested_attributes_update_only
47
+
35
48
  def initialize
36
49
  @serialize_unknown_attributes = true
37
50
  @enable_parent_assignment = true
38
51
  @serialize_enums_using_as_json = true
39
52
  @serialize_empty_attributes = true
53
+ @active_admin_compatibility = false
54
+ @nested_attributes_update_only = false
40
55
  end
41
56
  end
42
57
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StoreModel
4
+ # ActiveAdmin compatibility patches
5
+ #
6
+ # This module contains patches that make StoreModel compatible with ActiveAdmin's
7
+ # form builders, particularly the has_many helper which expects certain ActiveRecord-like
8
+ # methods to be present.
9
+ #
10
+ # To enable these patches, set:
11
+ # StoreModel.config.active_admin_compatibility = true
12
+ module ActiveAdminCompatibility
13
+ # Reflection class for StoreModel associations.
14
+ # This provides compatibility with form builders like ActiveAdmin's has_many
15
+ # that expect ActiveRecord-style reflection objects.
16
+ class Reflection
17
+ attr_reader :name, :klass
18
+
19
+ # @param name [Symbol] association name
20
+ # @param klass [Class] the StoreModel class
21
+ def initialize(name, klass)
22
+ @name = name
23
+ @klass = klass
24
+ end
25
+ end
26
+
27
+ # Patch for StoreModel::Model to add new_record? method
28
+ module NewRecordPatch
29
+ # Always returns true for StoreModel instances when ActiveAdmin compatibility is enabled.
30
+ # This is needed for compatibility with form builders like ActiveAdmin's has_many.
31
+ # For ActiveRecord models, delegates to the original implementation.
32
+ #
33
+ # @return [Boolean]
34
+ def new_record?
35
+ super
36
+ rescue NoMethodError
37
+ true
38
+ end
39
+ end
40
+
41
+ # Patch for StoreModel::NestedAttributes::ClassMethods to add reflection methods
42
+ module ReflectionMethods
43
+ # Returns reflection for the given association name.
44
+ # This provides compatibility with form builders like ActiveAdmin's has_many.
45
+ # First checks if the attribute is a StoreModel collection type, and if so,
46
+ # returns a reflection for it. Otherwise, delegates to the original implementation
47
+ # for ActiveRecord associations.
48
+ #
49
+ # @param name [Symbol, String] association name
50
+ # @return [StoreModel::ActiveAdminCompatibility::Reflection, nil]
51
+ def reflect_on_association(name)
52
+ # First check if this is a StoreModel attribute
53
+ # Use attribute_types directly to get the type for the given attribute
54
+ type = attribute_types[name.to_s]
55
+
56
+ if type.is_a?(StoreModel::Types::ManyBase) && type.respond_to?(:model_klass) && type.model_klass
57
+ # Return reflection for StoreModel collection attributes
58
+ return StoreModel::ActiveAdminCompatibility::Reflection.new(name.to_sym, type.model_klass)
59
+ end
60
+
61
+ # Not a StoreModel attribute, try to call the original method for ActiveRecord associations
62
+ super
63
+ rescue NoMethodError
64
+ # No super method exists (pure StoreModel class), return nil
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end
@@ -8,14 +8,25 @@ module StoreModel
8
8
  include ParentAssignment
9
9
 
10
10
  def _read_attribute(*)
11
- super.tap do |attribute|
12
- assign_parent_to_store_model_relation(attribute)
13
- end
11
+ value = super
12
+ assign_parent_to_store_model_relation(value) if store_model_attribute?(value)
13
+ value
14
14
  end
15
15
 
16
16
  def _write_attribute(*)
17
- super.tap do |attribute|
18
- assign_parent_to_store_model_relation(attribute)
17
+ value = super
18
+ assign_parent_to_store_model_relation(value) if store_model_attribute?(value)
19
+ value
20
+ end
21
+
22
+ private
23
+
24
+ def store_model_attribute?(value)
25
+ case value
26
+ when StoreModel::Model then true
27
+ when Array then value.first.is_a?(StoreModel::Model)
28
+ when Hash then value.each_value.any? { |v| v.is_a?(StoreModel::Model) }
29
+ else false
19
30
  end
20
31
  end
21
32
  end
@@ -13,7 +13,10 @@ module StoreModel
13
13
  end
14
14
 
15
15
  def assign_parent_to_singular_store_model(item)
16
- item.parent = self if item.is_a?(StoreModel::Model)
16
+ return unless item.is_a?(StoreModel::Model)
17
+ return if item.frozen?
18
+
19
+ item.parent = self
17
20
  end
18
21
  end
19
22
  end
@@ -15,6 +15,7 @@ module StoreModel
15
15
  base.include ActiveRecord::AttributeMethods::BeforeTypeCast
16
16
  base.include ActiveModel::AttributeMethods
17
17
  base.include StoreModel::NestedAttributes
18
+ base.include ActiveModel::Validations::Callbacks
18
19
 
19
20
  if ActiveModel::VERSION::MAJOR >= 8 && ActiveModel::VERSION::MINOR >= 1
20
21
  base.include ActiveModel::Attributes::Normalization
@@ -215,7 +216,10 @@ module StoreModel
215
216
  #
216
217
  # @return [Hash]
217
218
  def unknown_attributes
218
- @unknown_attributes ||= {}
219
+ return @unknown_attributes if defined?(@unknown_attributes)
220
+ return {}.freeze if frozen?
221
+
222
+ @unknown_attributes = {}
219
223
  end
220
224
 
221
225
  # Returns the value of the `@serialize_unknown_attributes` instance
@@ -313,6 +317,10 @@ module StoreModel
313
317
  return unless Array(attr.value).all? { |value| value.is_a?(StoreModel::Model) }
314
318
 
315
319
  Array(attr.value).each do |value|
320
+ # Skip frozen objects - they cannot have instance variables modified
321
+ # but will still serialize correctly with their existing settings
322
+ next if value.frozen?
323
+
316
324
  value.serialize_unknown_attributes = serialize_unknown_attributes
317
325
  value.serialize_enums_using_as_json = serialize_enums_using_as_json
318
326
  value.serialize_empty_attributes = serialize_empty_attributes
@@ -50,7 +50,7 @@ module StoreModel
50
50
 
51
51
  attributes.each do |attribute|
52
52
  if nested_attribute_type(attribute).is_a?(Types::Base)
53
- options.reverse_merge!(allow_destroy: false, update_only: false)
53
+ options.reverse_merge!(allow_destroy: false, update_only: StoreModel.config.nested_attributes_update_only)
54
54
  define_store_model_attr_accessors(attribute, options)
55
55
  else
56
56
  super(*attribute, options)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "store_model/ext/active_model/attributes"
4
4
  require "store_model/ext/active_record/base"
5
+ require "store_model/ext/active_admin_compatibility"
5
6
 
6
7
  module StoreModel # :nodoc:
7
8
  class Railtie < Rails::Railtie # :nodoc:
@@ -11,6 +12,11 @@ module StoreModel # :nodoc:
11
12
  ActiveModel::Attributes.prepend(Attributes)
12
13
  prepend(Base)
13
14
  end
15
+
16
+ if StoreModel.config.active_admin_compatibility
17
+ StoreModel::Model.prepend(ActiveAdminCompatibility::NewRecordPatch)
18
+ StoreModel::NestedAttributes::ClassMethods.prepend(ActiveAdminCompatibility::ReflectionMethods)
19
+ end
14
20
  end
15
21
  end
16
22
  end
@@ -7,12 +7,13 @@ module StoreModel
7
7
  # Implements ActiveModel::Type::Value type for handling an array of
8
8
  # StoreModel::Model
9
9
  class OneOf
10
- def initialize(&block)
10
+ def initialize(union: false, &block)
11
11
  @block = block
12
+ @union = union
12
13
  end
13
14
 
14
15
  def to_type
15
- Types::OnePolymorphic.new(@block)
16
+ Types::OnePolymorphic.new(@block, union: @union)
16
17
  end
17
18
 
18
19
  def to_array_type
@@ -13,8 +13,9 @@ module StoreModel
13
13
  # @param model_wrapper [Proc] class to handle
14
14
  #
15
15
  # @return [StoreModel::Types::OnePolymorphic ]
16
- def initialize(model_wrapper)
16
+ def initialize(model_wrapper, union: false)
17
17
  @model_wrapper = model_wrapper
18
+ @union = union
18
19
  super()
19
20
  end
20
21
 
@@ -30,8 +31,9 @@ module StoreModel
30
31
  # @param value [Object] a value to cast
31
32
  #
32
33
  # @return StoreModel::Model
33
- def cast_value(value) # rubocop:disable Metrics/MethodLength
34
+ def cast_value(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
34
35
  return nil if value.nil?
36
+ return nil if @union && value.respond_to?(:empty?) && value.empty?
35
37
 
36
38
  if value.is_a?(String)
37
39
  decode_and_initialize(value)
@@ -87,6 +89,18 @@ module StoreModel
87
89
  "Hash or instances which implement StoreModel::Model are allowed"
88
90
  end
89
91
 
92
+ # rubocop:disable Style/RescueModifier
93
+ def decode_and_initialize(value)
94
+ decoded = ActiveSupport::JSON.decode(value) rescue nil
95
+ return nil if decoded.nil?
96
+ return nil if @union && decoded.respond_to?(:empty?) && decoded.empty?
97
+
98
+ model_instance(decoded)
99
+ rescue ActiveModel::UnknownAttributeError => e
100
+ handle_unknown_attribute(decoded, e)
101
+ end
102
+ # rubocop:enable Style/RescueModifier
103
+
90
104
  def model_instance(value)
91
105
  extract_model_klass(value).new(value)
92
106
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StoreModel # :nodoc:
4
- VERSION = "4.4.0"
4
+ VERSION = "4.6.0"
5
5
  end
data/lib/store_model.rb CHANGED
@@ -58,7 +58,7 @@ module StoreModel # :nodoc:
58
58
  end
59
59
 
60
60
  def union_one_of(discriminator, class_map)
61
- Types::OneOf.new do |attributes|
61
+ Types::OneOf.new(union: true) do |attributes|
62
62
  next nil unless attributes
63
63
 
64
64
  discriminator_value = attributes.with_indifferent_access[discriminator]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: store_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.4.0
4
+ version: 4.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
@@ -84,6 +84,7 @@ files:
84
84
  - lib/store_model/combine_errors_strategies/merge_hash_error_strategy.rb
85
85
  - lib/store_model/configuration.rb
86
86
  - lib/store_model/enum.rb
87
+ - lib/store_model/ext/active_admin_compatibility.rb
87
88
  - lib/store_model/ext/active_model/attributes.rb
88
89
  - lib/store_model/ext/active_record/base.rb
89
90
  - lib/store_model/ext/parent_assignment.rb