store_model 1.6.1 → 4.4.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 +4 -4
- data/README.md +54 -6
- data/lib/active_model/validations/store_model_validator.rb +13 -0
- data/lib/store_model/combine_errors_strategies/mark_invalid_error_strategy.rb +2 -2
- data/lib/store_model/combine_errors_strategies/merge_hash_error_strategy.rb +22 -0
- data/lib/store_model/combine_errors_strategies.rb +15 -0
- data/lib/store_model/configuration.rb +20 -0
- data/lib/store_model/enum.rb +19 -10
- data/lib/store_model/ext/parent_assignment.rb +2 -2
- data/lib/store_model/model.rb +148 -5
- data/lib/store_model/nested_attributes.rb +89 -15
- data/lib/store_model/railtie.rb +4 -2
- data/lib/store_model/type_builders.rb +6 -0
- data/lib/store_model/types/base.rb +25 -0
- data/lib/store_model/types/enum_type.rb +8 -6
- data/lib/store_model/types/hash.rb +53 -0
- data/lib/store_model/types/hash_base.rb +79 -0
- data/lib/store_model/types/hash_polymorphic.rb +57 -0
- data/lib/store_model/types/many.rb +1 -0
- data/lib/store_model/types/many_base.rb +7 -17
- data/lib/store_model/types/many_polymorphic.rb +1 -0
- data/lib/store_model/types/one.rb +47 -8
- data/lib/store_model/types/one_base.rb +2 -15
- data/lib/store_model/types/one_of.rb +4 -0
- data/lib/store_model/types/one_polymorphic.rb +20 -9
- data/lib/store_model/types/raw_json.rb +20 -0
- data/lib/store_model/types.rb +7 -0
- data/lib/store_model/version.rb +1 -1
- data/lib/store_model.rb +56 -0
- metadata +14 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe31f43187a97021f54c79cbc8372b7e2b1c5a3c4bcb7b07b13def128bf69f34
|
|
4
|
+
data.tar.gz: '0456813d7c47e3a9888c23bd83dc90c233f8919734766e348c064350c3d502ce'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54a697b1a4782fa6bf3a6f84972789f6b42aab7ef9702e243d7fa4b9d7a6c035ea1421a560eb848083dd414b9bd594d577ca82a50642718d80607d742db64561
|
|
7
|
+
data.tar.gz: d591bf765bb19d91cc7972762349075edbb5ec9d633f3b0e4b9c290069af132e35b9d011e4537de43c18f3de76ca863e8b0576d93502ac95b24e4abb250bedd3
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# StoreModel [](https://rubygems.org/gems/store_model)
|
|
1
|
+
# StoreModel [](https://rubygems.org/gems/store_model) 
|
|
2
2
|
|
|
3
3
|
**StoreModel** gem allows you to wrap JSON-backed DB columns with ActiveModel-like classes.
|
|
4
4
|
|
|
@@ -22,6 +22,8 @@ class Product < ApplicationRecord
|
|
|
22
22
|
end
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
You can support my open–source work [here](https://boosty.to/dmitry_tsepelev).
|
|
26
|
+
|
|
25
27
|
## Why should I wrap my JSON columns?
|
|
26
28
|
|
|
27
29
|
Imagine that you have a model `Product` with a `jsonb` column called `configuration`. This is how you likely gonna work with this column:
|
|
@@ -76,20 +78,66 @@ product.save
|
|
|
76
78
|
_Usage note: Rails and assigning Arrays/Hashes to records_
|
|
77
79
|
|
|
78
80
|
- 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
|
|
81
|
+
- 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!`)
|
|
82
|
+
- Mixing `StoreModel::NestedAttributes` into your model will allow you to use `accepts_nested_attributes_for` in the same way as ActiveRecord.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class Supplier < ActiveRecord::Base
|
|
86
|
+
include StoreModel::NestedAttributes
|
|
87
|
+
|
|
88
|
+
has_many :bicycles, dependent: :destroy
|
|
89
|
+
|
|
90
|
+
attribute :products, Product.to_array_type
|
|
91
|
+
|
|
92
|
+
accepts_nested_attributes_for :bicycles, :products, allow_destroy: true
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This will allow the form builders to work their magic:
|
|
97
|
+
|
|
98
|
+
```erb
|
|
99
|
+
<%= form_with model: @supplier do |form| %>
|
|
100
|
+
<%= form.fields_for :products do |product_fields| %>
|
|
101
|
+
<%= product_fields.text_field :name %>
|
|
102
|
+
<% end %>
|
|
103
|
+
<% end %>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Resulting in:
|
|
107
|
+
```html
|
|
108
|
+
<input type="text" name="supplier[products_attributes][0][name]" id="supplier_products_attributes_0_name">
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
In the controller:
|
|
112
|
+
```ruby
|
|
113
|
+
def create
|
|
114
|
+
@supplier = Supplier.from_value(supplier_params)
|
|
115
|
+
@supplier.save
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def supplier_params
|
|
121
|
+
params.require(:supplier).permit(products_attributes: [:name])
|
|
122
|
+
end
|
|
123
|
+
```
|
|
80
124
|
|
|
81
125
|
## Documentation
|
|
82
126
|
|
|
83
127
|
1. [Installation](./docs/installation.md)
|
|
84
|
-
2. StoreModel::Model API:
|
|
128
|
+
2. `StoreModel::Model` API:
|
|
129
|
+
* [Instantiation](./docs/instantiation.md)
|
|
85
130
|
* [Validations](./docs/validations.md)
|
|
86
131
|
* [Enums](./docs/enums.md)
|
|
87
132
|
* [Nested models](./docs/nested_models.md)
|
|
88
133
|
* [Unknown attributes](./docs/unknown_attributes.md)
|
|
134
|
+
* [Empty attributes](./docs/empty_attributes.md)
|
|
89
135
|
3. [Array of stored models](./docs/array_of_stored_models.md)
|
|
90
|
-
4. [
|
|
91
|
-
|
|
92
|
-
|
|
136
|
+
4. [Hash of stored models](./docs/hash_of_stored_models.md)
|
|
137
|
+
5. [One of](./docs/one_of.md)
|
|
138
|
+
6. [Alternatives](./docs/alternatives.md)
|
|
139
|
+
7. [Defining custom types](./docs/defining_custom_types.md)
|
|
140
|
+
8. [Disabling Parent Tracking](./docs/enable_parent_assignment.md)
|
|
93
141
|
|
|
94
142
|
## Credits
|
|
95
143
|
|
|
@@ -14,6 +14,7 @@ module ActiveModel
|
|
|
14
14
|
# @param record [ApplicationRecord] object to validate
|
|
15
15
|
# @param attribute [String] name of the validated attribute
|
|
16
16
|
# @param value [Object] value of the validated attribute
|
|
17
|
+
# rubocop:disable Metrics/MethodLength
|
|
17
18
|
def validate_each(record, attribute, value)
|
|
18
19
|
if value.nil?
|
|
19
20
|
record.errors.add(attribute, :blank)
|
|
@@ -25,8 +26,11 @@ module ActiveModel
|
|
|
25
26
|
call_json_strategy(record, attribute, value)
|
|
26
27
|
when :array, :polymorphic_array
|
|
27
28
|
call_array_strategy(record, attribute, value)
|
|
29
|
+
when :hash, :polymorphic_hash
|
|
30
|
+
call_hash_strategy(record, attribute, value)
|
|
28
31
|
end
|
|
29
32
|
end
|
|
33
|
+
# rubocop:enable Metrics/MethodLength
|
|
30
34
|
|
|
31
35
|
private
|
|
32
36
|
|
|
@@ -39,6 +43,11 @@ module ActiveModel
|
|
|
39
43
|
array_strategy.call(attribute, record.errors, value) if any_invalid
|
|
40
44
|
end
|
|
41
45
|
|
|
46
|
+
def call_hash_strategy(record, attribute, value)
|
|
47
|
+
invalid_values = value.select { |_k, v| v.invalid?(record.validation_context) }
|
|
48
|
+
hash_strategy.call(attribute, record.errors, invalid_values) if invalid_values.present?
|
|
49
|
+
end
|
|
50
|
+
|
|
42
51
|
def strategy
|
|
43
52
|
@strategy ||= StoreModel::CombineErrorsStrategies.configure(options)
|
|
44
53
|
end
|
|
@@ -46,6 +55,10 @@ module ActiveModel
|
|
|
46
55
|
def array_strategy
|
|
47
56
|
@array_strategy ||= StoreModel::CombineErrorsStrategies.configure_array(options)
|
|
48
57
|
end
|
|
58
|
+
|
|
59
|
+
def hash_strategy
|
|
60
|
+
@hash_strategy ||= StoreModel::CombineErrorsStrategies.configure_hash(options)
|
|
61
|
+
end
|
|
49
62
|
end
|
|
50
63
|
end
|
|
51
64
|
end
|
|
@@ -10,8 +10,8 @@ module StoreModel
|
|
|
10
10
|
# @param base_errors [ActiveModel::Errors] errors object of the parent record
|
|
11
11
|
# @param _store_model_errors [ActiveModel::Errors] errors object of the
|
|
12
12
|
# StoreModel::Model attribute
|
|
13
|
-
def call(attribute, base_errors,
|
|
14
|
-
base_errors.add(attribute, :invalid)
|
|
13
|
+
def call(attribute, base_errors, store_model_errors)
|
|
14
|
+
base_errors.add(attribute, :invalid, errors: store_model_errors)
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StoreModel
|
|
4
|
+
module CombineErrorsStrategies
|
|
5
|
+
# +MergeHashErrorStrategy+ copies errors from the StoreModel::Model to the parent
|
|
6
|
+
# record attribute errors with hash key prefixes.
|
|
7
|
+
class MergeHashErrorStrategy
|
|
8
|
+
# Merges errors on +attribute+ from the child model with parent errors.
|
|
9
|
+
#
|
|
10
|
+
# @param attribute [String] name of the validated attribute
|
|
11
|
+
# @param base_errors [ActiveModel::Errors] errors object of the parent record
|
|
12
|
+
# @param store_models [Hash] a hash of store_models that have been validated
|
|
13
|
+
def call(attribute, base_errors, store_models)
|
|
14
|
+
store_models.each do |key, store_model|
|
|
15
|
+
store_model.errors.full_messages.each do |full_message|
|
|
16
|
+
base_errors.add(attribute, :invalid, message: "[#{key}] #{full_message}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "store_model/combine_errors_strategies/mark_invalid_error_strategy"
|
|
4
4
|
require "store_model/combine_errors_strategies/merge_error_strategy"
|
|
5
5
|
require "store_model/combine_errors_strategies/merge_array_error_strategy"
|
|
6
|
+
require "store_model/combine_errors_strategies/merge_hash_error_strategy"
|
|
6
7
|
|
|
7
8
|
module StoreModel
|
|
8
9
|
# Module with built-in strategies for combining errors.
|
|
@@ -37,6 +38,20 @@ module StoreModel
|
|
|
37
38
|
)
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
# Finds a hash strategy based on +options+ and global config.
|
|
42
|
+
#
|
|
43
|
+
# @param options [Hash]
|
|
44
|
+
#
|
|
45
|
+
# @return [Object] strategy
|
|
46
|
+
def configure_hash(options)
|
|
47
|
+
configured_strategy = options[:merge_hash_errors] || StoreModel.config.merge_hash_errors
|
|
48
|
+
|
|
49
|
+
get_configured_strategy(
|
|
50
|
+
configured_strategy,
|
|
51
|
+
StoreModel::CombineErrorsStrategies::MergeHashErrorStrategy
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
40
55
|
def get_configured_strategy(configured_strategy, true_strategy_class)
|
|
41
56
|
if configured_strategy.respond_to?(:call)
|
|
42
57
|
configured_strategy
|
|
@@ -11,12 +11,32 @@ module StoreModel
|
|
|
11
11
|
# @return [Boolean]
|
|
12
12
|
attr_accessor :merge_array_errors
|
|
13
13
|
|
|
14
|
+
# Controls usage of MergeHashErrorStrategy
|
|
15
|
+
# @return [Boolean]
|
|
16
|
+
attr_accessor :merge_hash_errors
|
|
17
|
+
|
|
14
18
|
# Controls if the result of `as_json` will contain the unknown attributes of the model
|
|
15
19
|
# @return [Boolean]
|
|
16
20
|
attr_accessor :serialize_unknown_attributes
|
|
17
21
|
|
|
22
|
+
# Controls if the result of `as_json` will contain the nulls attributes of the model
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
attr_accessor :serialize_empty_attributes
|
|
25
|
+
|
|
26
|
+
# Controls if the result of `as_json` will serialize enum fields using `as_json`
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
attr_accessor :serialize_enums_using_as_json
|
|
29
|
+
|
|
30
|
+
# Controls if parent tracking functionality is enabled.
|
|
31
|
+
# Default: true
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
attr_accessor :enable_parent_assignment
|
|
34
|
+
|
|
18
35
|
def initialize
|
|
19
36
|
@serialize_unknown_attributes = true
|
|
37
|
+
@enable_parent_assignment = true
|
|
38
|
+
@serialize_enums_using_as_json = true
|
|
39
|
+
@serialize_empty_attributes = true
|
|
20
40
|
end
|
|
21
41
|
end
|
|
22
42
|
end
|
data/lib/store_model/enum.rb
CHANGED
|
@@ -10,12 +10,12 @@ module StoreModel
|
|
|
10
10
|
# @param kwargs [Object]
|
|
11
11
|
def enum(name, values = nil, **kwargs)
|
|
12
12
|
values ||= kwargs[:in] || kwargs
|
|
13
|
-
options = kwargs
|
|
13
|
+
options = retrieve_options(kwargs)
|
|
14
14
|
|
|
15
15
|
ensure_hash(values).tap do |mapping|
|
|
16
|
-
define_attribute(name, mapping, options
|
|
16
|
+
define_attribute(name, mapping, options)
|
|
17
17
|
define_reader(name, mapping)
|
|
18
|
-
define_writer(name, mapping)
|
|
18
|
+
define_writer(name, mapping, options[:raise_on_invalid_values])
|
|
19
19
|
define_method("#{name}_value") { attributes[name.to_s] }
|
|
20
20
|
define_map_readers(name, mapping)
|
|
21
21
|
define_predicate_methods(name, mapping, options)
|
|
@@ -24,16 +24,25 @@ module StoreModel
|
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
|
-
def
|
|
28
|
-
|
|
27
|
+
def retrieve_options(options)
|
|
28
|
+
default_options = { raise_on_invalid_values: true }
|
|
29
|
+
options.slice(:_prefix, :_suffix, :default, :raise_on_invalid_values)
|
|
30
|
+
.reverse_merge(default_options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def define_attribute(name, mapping, options)
|
|
34
|
+
attribute name, cast_type(mapping, options[:raise_on_invalid_values]), default: options[:default]
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
def define_reader(name, mapping)
|
|
32
|
-
define_method(name)
|
|
38
|
+
define_method(name) do
|
|
39
|
+
raw_value = send("#{name}_value")
|
|
40
|
+
(mapping.key(raw_value) || raw_value).to_s
|
|
41
|
+
end
|
|
33
42
|
end
|
|
34
43
|
|
|
35
|
-
def define_writer(name, mapping)
|
|
36
|
-
type = cast_type(mapping)
|
|
44
|
+
def define_writer(name, mapping, raise_on_invalid_values)
|
|
45
|
+
type = cast_type(mapping, raise_on_invalid_values)
|
|
37
46
|
define_method("#{name}=") { |value| super type.cast_value(value) }
|
|
38
47
|
end
|
|
39
48
|
|
|
@@ -50,8 +59,8 @@ module StoreModel
|
|
|
50
59
|
singleton_class.alias_method(ActiveSupport::Inflector.pluralize(name), "#{name}_values")
|
|
51
60
|
end
|
|
52
61
|
|
|
53
|
-
def cast_type(mapping)
|
|
54
|
-
StoreModel::Types::EnumType.new(mapping)
|
|
62
|
+
def cast_type(mapping, raise_on_invalid_values)
|
|
63
|
+
StoreModel::Types::EnumType.new(mapping, raise_on_invalid_values)
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
def ensure_hash(values)
|
|
@@ -7,9 +7,9 @@ module StoreModel
|
|
|
7
7
|
|
|
8
8
|
def assign_parent_to_store_model_relation(attribute)
|
|
9
9
|
assign_parent_to_singular_store_model(attribute)
|
|
10
|
-
return
|
|
10
|
+
return if !attribute.is_a?(Array) && !attribute.is_a?(Hash)
|
|
11
11
|
|
|
12
|
-
attribute.each(&method(:assign_parent_to_singular_store_model))
|
|
12
|
+
(attribute.try(:values) || attribute).each(&method(:assign_parent_to_singular_store_model))
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def assign_parent_to_singular_store_model(item)
|
data/lib/store_model/model.rb
CHANGED
|
@@ -7,7 +7,8 @@ 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
|
+
# rubocop:disable Metrics/MethodLength
|
|
11
12
|
def self.included(base) # :nodoc:
|
|
12
13
|
base.include ActiveModel::Model
|
|
13
14
|
base.include ActiveModel::Attributes
|
|
@@ -15,13 +16,40 @@ module StoreModel
|
|
|
15
16
|
base.include ActiveModel::AttributeMethods
|
|
16
17
|
base.include StoreModel::NestedAttributes
|
|
17
18
|
|
|
19
|
+
if ActiveModel::VERSION::MAJOR >= 8 && ActiveModel::VERSION::MINOR >= 1
|
|
20
|
+
base.include ActiveModel::Attributes::Normalization
|
|
21
|
+
end
|
|
22
|
+
|
|
18
23
|
base.extend StoreModel::Enum
|
|
19
24
|
base.extend StoreModel::TypeBuilders
|
|
20
25
|
|
|
21
26
|
base.attribute_method_suffix "?"
|
|
27
|
+
|
|
28
|
+
base.extend(ClassMethods)
|
|
29
|
+
end
|
|
30
|
+
# rubocop:enable Metrics/MethodLength
|
|
31
|
+
|
|
32
|
+
# Class methods for StoreModel::Model
|
|
33
|
+
module ClassMethods
|
|
34
|
+
def from_value(value)
|
|
35
|
+
to_type.cast_value(value)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def from_values(values)
|
|
39
|
+
to_array_type.cast_value(values)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Defines a discriminator attribute with a value
|
|
43
|
+
# @param discriminator [Symbol, String] attribute name (default: :type)
|
|
44
|
+
# @param type [Symbol, String] attribute type (default: :string)
|
|
45
|
+
# @param value [String] the discriminator value
|
|
46
|
+
def discriminator_attribute(discriminator = "type", value:, type: :string)
|
|
47
|
+
attribute discriminator, type, default: value
|
|
48
|
+
end
|
|
22
49
|
end
|
|
23
50
|
|
|
24
51
|
attr_accessor :parent
|
|
52
|
+
attr_writer :serialize_unknown_attributes, :serialize_enums_using_as_json, :serialize_empty_attributes
|
|
25
53
|
|
|
26
54
|
delegate :each_value, to: :attributes
|
|
27
55
|
|
|
@@ -31,16 +59,39 @@ module StoreModel
|
|
|
31
59
|
# @param options [Hash]
|
|
32
60
|
#
|
|
33
61
|
# @return [Hash]
|
|
34
|
-
def as_json(options = {})
|
|
62
|
+
def as_json(options = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
35
63
|
serialize_unknown_attributes = if options.key?(:serialize_unknown_attributes)
|
|
36
64
|
options[:serialize_unknown_attributes]
|
|
37
65
|
else
|
|
38
66
|
StoreModel.config.serialize_unknown_attributes
|
|
39
67
|
end
|
|
40
68
|
|
|
41
|
-
|
|
69
|
+
serialize_enums_using_as_json = if options.key?(:serialize_enums_using_as_json)
|
|
70
|
+
options[:serialize_enums_using_as_json]
|
|
71
|
+
else
|
|
72
|
+
StoreModel.config.serialize_enums_using_as_json
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
serialize_empty_attributes = if options.key?(:serialize_empty_attributes)
|
|
76
|
+
options[:serialize_empty_attributes]
|
|
77
|
+
else
|
|
78
|
+
StoreModel.config.serialize_empty_attributes
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# If the model is nested, we need to ensure that the serialization
|
|
82
|
+
|
|
83
|
+
result = @attributes.keys.each_with_object({}) do |key, values|
|
|
84
|
+
attr = @attributes.fetch(key)
|
|
85
|
+
assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json,
|
|
86
|
+
serialize_empty_attributes)
|
|
87
|
+
serialized = serialized_attribute(attr)
|
|
88
|
+
values[key] = serialized if serialize_empty_attributes || !serialized.nil?
|
|
89
|
+
end.with_indifferent_access
|
|
90
|
+
|
|
42
91
|
result.merge!(unknown_attributes) if serialize_unknown_attributes
|
|
43
|
-
result.as_json(options)
|
|
92
|
+
result.as_json(options).tap do |json|
|
|
93
|
+
serialize_enums!(json) if serialize_enums_using_as_json
|
|
94
|
+
end
|
|
44
95
|
end
|
|
45
96
|
|
|
46
97
|
# Returns an Object, similar to Hash#fetch, raises
|
|
@@ -107,7 +158,7 @@ module StoreModel
|
|
|
107
158
|
#
|
|
108
159
|
# @return [String]
|
|
109
160
|
def inspect
|
|
110
|
-
attribute_string = attributes.map { |name, value| "#{name}: #{value.
|
|
161
|
+
attribute_string = attributes.map { |name, value| "#{name}: #{value.inspect}" }
|
|
111
162
|
.join(", ")
|
|
112
163
|
"#<#{self.class.name} #{attribute_string}>"
|
|
113
164
|
end
|
|
@@ -167,6 +218,55 @@ module StoreModel
|
|
|
167
218
|
@unknown_attributes ||= {}
|
|
168
219
|
end
|
|
169
220
|
|
|
221
|
+
# Returns the value of the `@serialize_unknown_attributes` instance
|
|
222
|
+
# variable. In the current specification, unknown attributes must be
|
|
223
|
+
# persisted in the database regardless of the globally configured
|
|
224
|
+
# `serialize_unknown_attributes` option. Therefore, it returns the
|
|
225
|
+
# default value `true` if the instance variable is `nil`.
|
|
226
|
+
#
|
|
227
|
+
# This method is used to ensure that the `serialize_unknown_attributes`
|
|
228
|
+
# option is correctly applied to nested StoreModel::Model objects when
|
|
229
|
+
# the `as_json` method is called.
|
|
230
|
+
#
|
|
231
|
+
# @return [Boolean]
|
|
232
|
+
def serialize_unknown_attributes?
|
|
233
|
+
@serialize_unknown_attributes.nil? ? true : @serialize_unknown_attributes
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Returns the value of the `@serialize_enums_using_as_json` instance
|
|
237
|
+
# variable. The default value is the value of the globally configured
|
|
238
|
+
# `serialize_enums_using_as_json` option.
|
|
239
|
+
#
|
|
240
|
+
# This method is used to determine whether enums should be serialized
|
|
241
|
+
# when the `as_json` method is called in nested StoreModel::Model
|
|
242
|
+
# objects.
|
|
243
|
+
#
|
|
244
|
+
# @return [Boolean]
|
|
245
|
+
def serialize_enums_using_as_json?
|
|
246
|
+
if @serialize_enums_using_as_json.nil?
|
|
247
|
+
StoreModel.config.serialize_enums_using_as_json || false
|
|
248
|
+
else
|
|
249
|
+
@serialize_enums_using_as_json
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Returns the value of the `@serialize_empty_attributes` instance
|
|
254
|
+
# variable. The default value is the value of the globally configured
|
|
255
|
+
# `serialize_empty_attributes` option.
|
|
256
|
+
#
|
|
257
|
+
# This method is used to determine whether empty values should be serialized
|
|
258
|
+
# when the `as_json` method is called in nested StoreModel::Model
|
|
259
|
+
# objects.
|
|
260
|
+
#
|
|
261
|
+
# @return [Boolean]
|
|
262
|
+
def serialize_empty_attributes?
|
|
263
|
+
if @serialize_empty_attributes.nil?
|
|
264
|
+
StoreModel.config.serialize_empty_attributes || true
|
|
265
|
+
else
|
|
266
|
+
@serialize_empty_attributes
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
170
270
|
private
|
|
171
271
|
|
|
172
272
|
def attribute?(attribute)
|
|
@@ -175,5 +275,48 @@ module StoreModel
|
|
|
175
275
|
else value.present?
|
|
176
276
|
end
|
|
177
277
|
end
|
|
278
|
+
|
|
279
|
+
def serialize_enums!(json)
|
|
280
|
+
enum_mappings =
|
|
281
|
+
self.class
|
|
282
|
+
.attribute_types
|
|
283
|
+
.select { |_, type| type.is_a?(StoreModel::Types::EnumType) }
|
|
284
|
+
|
|
285
|
+
enum_mappings.each_key do |name|
|
|
286
|
+
next unless json.key?(name)
|
|
287
|
+
|
|
288
|
+
json[name] = public_send(name).as_json unless json[name].nil?
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def serialized_attribute(attr)
|
|
293
|
+
if attr.value.is_a? StoreModel::Model
|
|
294
|
+
Types::RawJSONEncoder.new(attr.value_for_database)
|
|
295
|
+
elsif attr.value.is_a? Array
|
|
296
|
+
serialize_array_attribute(attr.value)
|
|
297
|
+
else
|
|
298
|
+
attr.value_for_database
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def serialize_array_attribute(array)
|
|
303
|
+
return array.as_json unless array.any? && array.all? { |value| value.is_a?(StoreModel::Model) }
|
|
304
|
+
|
|
305
|
+
array.as_json(
|
|
306
|
+
serialize_unknown_attributes: array.first.serialize_unknown_attributes?,
|
|
307
|
+
serialize_enums_using_as_json: array.first.serialize_enums_using_as_json?,
|
|
308
|
+
serialize_empty_attributes: array.first.serialize_empty_attributes?
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json, serialize_empty_attributes) # rubocop:disable Layout/LineLength
|
|
313
|
+
return unless Array(attr.value).all? { |value| value.is_a?(StoreModel::Model) }
|
|
314
|
+
|
|
315
|
+
Array(attr.value).each do |value|
|
|
316
|
+
value.serialize_unknown_attributes = serialize_unknown_attributes
|
|
317
|
+
value.serialize_enums_using_as_json = serialize_enums_using_as_json
|
|
318
|
+
value.serialize_empty_attributes = serialize_empty_attributes
|
|
319
|
+
end
|
|
320
|
+
end
|
|
178
321
|
end
|
|
179
322
|
end
|
|
@@ -8,36 +8,96 @@ module StoreModel
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
module ClassMethods # :nodoc:
|
|
11
|
+
# gather storemodel attribute types on the class-level
|
|
12
|
+
def store_model_attribute_types
|
|
13
|
+
@store_model_attribute_types ||= {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# add storemodel type of attribute if it is storemodel type
|
|
17
|
+
def attribute(name, type = nil, **)
|
|
18
|
+
store_model_attribute_types[name.to_s] = type if type.is_a?(Types::Base)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
11
22
|
# Enables handling of nested StoreModel::Model attributes
|
|
12
23
|
#
|
|
13
24
|
# @param associations [Array] list of associations and options to define attributes, for example:
|
|
14
25
|
# accepts_nested_attributes_for [:suppliers, allow_destroy: true]
|
|
15
26
|
#
|
|
27
|
+
# Alternatively, use the standard Rails syntax:
|
|
28
|
+
#
|
|
29
|
+
# @param associations [Array] list of associations and attributes to define getters and setters.
|
|
30
|
+
#
|
|
31
|
+
# @param options [Hash] options not supported by StoreModel will still be passed to ActiveRecord.
|
|
32
|
+
#
|
|
16
33
|
# Supported options:
|
|
17
34
|
# [:allow_destroy]
|
|
18
35
|
# If true, destroys any members from the attributes hash with a
|
|
19
36
|
# <tt>_destroy</tt> key and a value that evaluates to +true+
|
|
20
37
|
# (e.g. 1, '1', true, or 'true'). This option is off by default.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
#
|
|
39
|
+
# [:reject_if]
|
|
40
|
+
# Allows you to specify a Proc or a Symbol pointing to a method that
|
|
41
|
+
# checks whether a record should be built for a certain attribute hash.
|
|
42
|
+
# The hash is passed to the supplied Proc or the method and it should
|
|
43
|
+
# return either true or false. Passing <tt>:all_blank</tt> instead of a Proc
|
|
44
|
+
# will create a proc that will reject a record where all the attributes
|
|
45
|
+
# are blank excluding any value for <tt>_destroy</tt>.
|
|
46
|
+
#
|
|
47
|
+
# See https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for
|
|
48
|
+
def accepts_nested_attributes_for(*attributes)
|
|
49
|
+
options = attributes.extract_options!
|
|
30
50
|
|
|
31
|
-
|
|
51
|
+
attributes.each do |attribute|
|
|
52
|
+
if nested_attribute_type(attribute).is_a?(Types::Base)
|
|
53
|
+
options.reverse_merge!(allow_destroy: false, update_only: false)
|
|
54
|
+
define_store_model_attr_accessors(attribute, options)
|
|
55
|
+
else
|
|
56
|
+
super(*attribute, options)
|
|
57
|
+
end
|
|
32
58
|
end
|
|
33
59
|
end
|
|
34
60
|
|
|
35
61
|
private
|
|
36
62
|
|
|
63
|
+
# when db connection is not available, it becomes impossible to read attributes types from
|
|
64
|
+
# ActiveModel::AttributeRegistration::ClassMethods.attribute_types, because activerecord
|
|
65
|
+
# overrides _default_attributes and triggers db connection.
|
|
66
|
+
# for activerecord model only use attribute_types if it has db connected
|
|
67
|
+
#
|
|
68
|
+
# @param attribute [String, Symbol]
|
|
69
|
+
# @return [StoreModel::Types::Base, nil]
|
|
70
|
+
def nested_attribute_type(attribute)
|
|
71
|
+
if self < ActiveRecord::Base && !schema_loaded?
|
|
72
|
+
store_model_attribute_types[attribute.to_s]
|
|
73
|
+
else
|
|
74
|
+
attribute_types[attribute.to_s]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def define_store_model_attr_accessors(attribute, options) # rubocop:disable Metrics/MethodLength
|
|
79
|
+
case nested_attribute_type(attribute)
|
|
80
|
+
when Types::OneBase
|
|
81
|
+
define_association_setter_for_single(attribute, options)
|
|
82
|
+
define_method "#{attribute}_attributes=" do |*args, **kwargs|
|
|
83
|
+
existing_model = send(attribute)
|
|
84
|
+
if existing_model && options[:update_only]
|
|
85
|
+
existing_model.assign_attributes(*args, **kwargs)
|
|
86
|
+
else
|
|
87
|
+
send("#{attribute}=", *args, **kwargs)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
when Types::ManyBase
|
|
91
|
+
define_association_setter_for_many(attribute, options)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
define_attr_accessor_for_destroy(attribute, options)
|
|
95
|
+
end
|
|
96
|
+
|
|
37
97
|
def define_attr_accessor_for_destroy(association, options)
|
|
38
98
|
return unless options&.dig(:allow_destroy)
|
|
39
99
|
|
|
40
|
-
|
|
100
|
+
nested_attribute_type(association).model_klass.class_eval do
|
|
41
101
|
attr_accessor :_destroy
|
|
42
102
|
end
|
|
43
103
|
end
|
|
@@ -52,7 +112,7 @@ module StoreModel
|
|
|
52
112
|
return unless options&.dig(:allow_destroy)
|
|
53
113
|
|
|
54
114
|
define_method "#{association}=" do |attributes|
|
|
55
|
-
if ActiveRecord::Type::Boolean.new.cast(attributes.stringify_keys
|
|
115
|
+
if ActiveRecord::Type::Boolean.new.cast(attributes.stringify_keys["_destroy"])
|
|
56
116
|
super(nil)
|
|
57
117
|
else
|
|
58
118
|
super(attributes)
|
|
@@ -61,18 +121,32 @@ module StoreModel
|
|
|
61
121
|
end
|
|
62
122
|
end
|
|
63
123
|
|
|
64
|
-
|
|
124
|
+
# Base
|
|
125
|
+
def assign_nested_attributes_for_collection_association(association, attributes, options = nil)
|
|
126
|
+
return super(association, attributes) unless options
|
|
65
127
|
|
|
66
|
-
def assign_nested_attributes_for_collection_association(association, attributes, options)
|
|
67
128
|
attributes = attributes.values if attributes.is_a?(Hash)
|
|
68
129
|
|
|
69
130
|
if options&.dig(:allow_destroy)
|
|
70
131
|
attributes.reject! do |attribute|
|
|
71
|
-
ActiveRecord::Type::Boolean.new.cast(attribute.stringify_keys
|
|
132
|
+
ActiveRecord::Type::Boolean.new.cast(attribute.stringify_keys["_destroy"])
|
|
72
133
|
end
|
|
73
134
|
end
|
|
74
135
|
|
|
136
|
+
attributes.reject! { call_store_model_reject_if(_1, options[:reject_if]) } if options&.dig(:reject_if)
|
|
137
|
+
|
|
75
138
|
send("#{association}=", attributes)
|
|
76
139
|
end
|
|
140
|
+
|
|
141
|
+
def call_store_model_reject_if(attributes, callback)
|
|
142
|
+
callback = ActiveRecord::NestedAttributes::ClassMethods::REJECT_ALL_BLANK_PROC if callback == :all_blank
|
|
143
|
+
|
|
144
|
+
case callback
|
|
145
|
+
when Symbol
|
|
146
|
+
method(callback).arity.zero? ? send(callback) : send(callback, attributes)
|
|
147
|
+
when Proc
|
|
148
|
+
callback.call(attributes)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
77
151
|
end
|
|
78
152
|
end
|