active_record_compose 1.1.1 → 1.2.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/CHANGELOG.md +11 -0
- data/lib/active_record_compose/composed_collection.rb +35 -8
- data/lib/active_record_compose/exceptions.rb +29 -0
- data/lib/active_record_compose/validations.rb +14 -0
- data/lib/active_record_compose/version.rb +1 -1
- data/lib/active_record_compose/wrapped_model.rb +13 -4
- data/lib/active_record_compose.rb +1 -0
- data/sig/_internal/package_private.rbs +9 -7
- data/sig/active_record_compose.rbs +8 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2622aa32a886a2c21fcc7836254285cb92f1ce7745674297c3e7fe46bcbe556a
|
|
4
|
+
data.tar.gz: 747acd3e97cb78aba78b9d3f2c87ab4d9bd34b0727aaf2a1c94d9520ea9eaa08
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6002732e8e6fa09269ccb4a7635af4ef683d06af14e829aa062846c6dbb90c2942d5b3cc2b4d715eeef2182498e732d33b5ae004836ff703f8d8235fec12c70
|
|
7
|
+
data.tar.gz: 18d3f9a635d3f4bf940eeb4020824fa7fc7d91cf27b0a99aa253af53e31fdca6bebd69d402537bea29749de26eba3f5a261e10a3ef2ebf079294e7fc3dbd5127
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - 2026-01-05
|
|
4
|
+
|
|
5
|
+
* Avoid issuing multiple saves on the same object.
|
|
6
|
+
(https://github.com/hamajyotan/active_record_compose/pull/56)
|
|
7
|
+
* The storage of `#models` has been changed from an Array to a Set.
|
|
8
|
+
This prevents duplicate additions of the same object and option combinations.
|
|
9
|
+
Also, `#models#delete` now deletes the model regardless of the options used when it was added.
|
|
10
|
+
(https://github.com/hamajyotan/active_record_compose/pull/57)
|
|
11
|
+
* Adding an `ActiveRecordCompose::Model` to `#models` now throws an error if there is a circular reference.
|
|
12
|
+
(https://github.com/hamajyotan/active_record_compose/pull/58)
|
|
13
|
+
|
|
3
14
|
## [1.1.1] - 2025-12-04
|
|
4
15
|
|
|
5
16
|
* fix: the save method would return nil instead of false.
|
|
@@ -13,7 +13,7 @@ module ActiveRecordCompose
|
|
|
13
13
|
|
|
14
14
|
def initialize(owner)
|
|
15
15
|
@owner = owner
|
|
16
|
-
@models =
|
|
16
|
+
@models = Set.new
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Enumerates model objects.
|
|
@@ -69,13 +69,28 @@ module ActiveRecordCompose
|
|
|
69
69
|
# Removes the specified model from the collection.
|
|
70
70
|
# Returns nil if the deletion fails, self if it succeeds.
|
|
71
71
|
#
|
|
72
|
+
# The specified model instance will be deleted regardless of the options used when it was added.
|
|
73
|
+
#
|
|
74
|
+
# @example
|
|
75
|
+
# model_a = Model.new
|
|
76
|
+
# model_b = Model.new
|
|
77
|
+
#
|
|
78
|
+
# collection.push(model_a, destroy: true)
|
|
79
|
+
# collection.push(model_b)
|
|
80
|
+
# collection.push(model_a, destroy: false)
|
|
81
|
+
# collection.count #=> 3
|
|
82
|
+
#
|
|
83
|
+
# collection.delete(model_a)
|
|
84
|
+
# collection.count #=> 1
|
|
85
|
+
#
|
|
72
86
|
# @param model [Object] model instance
|
|
73
87
|
# @return [self] Successful deletion
|
|
74
88
|
# @return [nil] If deletion fails
|
|
75
89
|
def delete(model)
|
|
76
|
-
|
|
77
|
-
return nil
|
|
90
|
+
matched = models.select { _1.__raw_model == model }
|
|
91
|
+
return nil if matched.blank?
|
|
78
92
|
|
|
93
|
+
matched.each { models.delete(_1) }
|
|
79
94
|
self
|
|
80
95
|
end
|
|
81
96
|
|
|
@@ -87,17 +102,27 @@ module ActiveRecordCompose
|
|
|
87
102
|
# @private
|
|
88
103
|
def wrap(model, destroy: false, if: nil)
|
|
89
104
|
if destroy.is_a?(Symbol)
|
|
90
|
-
|
|
91
|
-
destroy = -> { owner.__send__(method) }
|
|
105
|
+
destroy = symbol_proc_map[destroy]
|
|
92
106
|
end
|
|
107
|
+
|
|
93
108
|
if_option = binding.local_variable_get(:if)
|
|
94
109
|
if if_option.is_a?(Symbol)
|
|
95
|
-
|
|
96
|
-
if_option = -> { owner.__send__(method) }
|
|
110
|
+
if_option = symbol_proc_map[if_option]
|
|
97
111
|
end
|
|
112
|
+
|
|
98
113
|
ActiveRecordCompose::WrappedModel.new(model, destroy:, if: if_option)
|
|
99
114
|
end
|
|
100
115
|
|
|
116
|
+
# @private
|
|
117
|
+
def symbol_proc_map
|
|
118
|
+
@symbol_proc_map ||=
|
|
119
|
+
Hash.new do |h, k|
|
|
120
|
+
h[k] = -> { owner.__send__(k) }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def instance_variables_to_inspect = %i[@owner @models]
|
|
125
|
+
|
|
101
126
|
# @private
|
|
102
127
|
module PackagePrivate
|
|
103
128
|
refine ComposedCollection do
|
|
@@ -105,7 +130,9 @@ module ActiveRecordCompose
|
|
|
105
130
|
#
|
|
106
131
|
# @private
|
|
107
132
|
# @return [Array[WrappedModel]] array of wrapped model instance.
|
|
108
|
-
def __wrapped_models
|
|
133
|
+
def __wrapped_models
|
|
134
|
+
models.reject { _1.ignore? }.uniq { [ _1.__raw_model, !!_1.destroy_context? ] }.select { _1.__raw_model }
|
|
135
|
+
end
|
|
109
136
|
end
|
|
110
137
|
end
|
|
111
138
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordCompose
|
|
4
|
+
# Occurs when a circular reference is detected in the containing model.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# class Model < ActiveRecordCompose::Model
|
|
8
|
+
# def initialize
|
|
9
|
+
# super()
|
|
10
|
+
# models << self # Adding itself to models creates a circular reference.
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
# model = Model.new
|
|
14
|
+
# model.save #=> raises ActiveRecordCompose::CircularReferenceDetected
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# class Model < ActiveRecordCompose::Model
|
|
18
|
+
# attribute :model
|
|
19
|
+
# before_validation { models << model }
|
|
20
|
+
# end
|
|
21
|
+
# inner = Model.new
|
|
22
|
+
# middle = Model.new(model: inner)
|
|
23
|
+
# outer = Model.new(model: middle)
|
|
24
|
+
#
|
|
25
|
+
# inner.model = outer # There is a circular reference in the form outer > middle > inner > outer.
|
|
26
|
+
# outer.save #=> raises ActiveRecordCompose::CircularReferenceDetected
|
|
27
|
+
#
|
|
28
|
+
class CircularReferenceDetected < StandardError; end
|
|
29
|
+
end
|
|
@@ -76,10 +76,24 @@ module ActiveRecordCompose
|
|
|
76
76
|
#
|
|
77
77
|
# @return [ActiveModel::Errors]
|
|
78
78
|
|
|
79
|
+
# @private
|
|
80
|
+
def detect_circular_reference(targets = [])
|
|
81
|
+
raise CircularReferenceDetected if targets.include?(object_id)
|
|
82
|
+
|
|
83
|
+
targets += [ object_id ]
|
|
84
|
+
# steep:ignore:start
|
|
85
|
+
models.select { _1.respond_to?(:detect_circular_reference) }.each do |m|
|
|
86
|
+
m.detect_circular_reference(targets)
|
|
87
|
+
end
|
|
88
|
+
# steep:ignore:end
|
|
89
|
+
end
|
|
90
|
+
|
|
79
91
|
private
|
|
80
92
|
|
|
81
93
|
# @private
|
|
82
94
|
def validate_models
|
|
95
|
+
detect_circular_reference
|
|
96
|
+
|
|
83
97
|
context = override_validation_context
|
|
84
98
|
models.__wrapped_models.lazy.select { _1.invalid?(context) }.each { errors.merge!(_1) }
|
|
85
99
|
end
|
|
@@ -104,19 +104,28 @@ module ActiveRecordCompose
|
|
|
104
104
|
# @param [Object] other
|
|
105
105
|
# @return [Boolean]
|
|
106
106
|
def ==(other)
|
|
107
|
+
return true if equal?(other)
|
|
107
108
|
return false unless self.class == other.class
|
|
108
|
-
return false unless model == other.model
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
equality_key == other.equality_key
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
def eql?(other)
|
|
114
|
+
return true if equal?(other)
|
|
115
|
+
return false unless self.class == other.class
|
|
116
|
+
|
|
117
|
+
equality_key.eql?(other.equality_key)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def hash = equality_key.hash
|
|
121
|
+
|
|
113
122
|
protected
|
|
114
123
|
|
|
115
|
-
|
|
124
|
+
def equality_key = [ model, destroy_context_type, if_option ]
|
|
116
125
|
|
|
117
126
|
private
|
|
118
127
|
|
|
119
|
-
attr_reader :destroy_context_type, :if_option
|
|
128
|
+
attr_reader :model, :destroy_context_type, :if_option
|
|
120
129
|
|
|
121
130
|
# @private
|
|
122
131
|
module PackagePrivate
|
|
@@ -64,16 +64,20 @@ module ActiveRecordCompose
|
|
|
64
64
|
class ComposedCollection
|
|
65
65
|
def initialize: (Model) -> void
|
|
66
66
|
|
|
67
|
+
@symbol_proc_map: Hash[Symbol, (destroy_context_type | condition_type)]
|
|
68
|
+
|
|
67
69
|
private
|
|
68
70
|
attr_reader owner: Model
|
|
69
|
-
attr_reader models:
|
|
71
|
+
attr_reader models: Set[WrappedModel]
|
|
70
72
|
def wrap: (ar_like, ?destroy: (bool | Symbol | destroy_context_type), ?if: (nil | Symbol | condition_type)) -> WrappedModel
|
|
73
|
+
def symbol_proc_map: () -> Hash[Symbol, (destroy_context_type | condition_type)]
|
|
74
|
+
def instance_variables_to_inspect: () -> Array[Symbol]
|
|
71
75
|
|
|
72
76
|
module PackagePrivate
|
|
73
|
-
def __wrapped_models: () ->
|
|
77
|
+
def __wrapped_models: () -> Enumerable[WrappedModel]
|
|
74
78
|
|
|
75
79
|
private
|
|
76
|
-
def models: () ->
|
|
80
|
+
def models: () -> Set[WrappedModel]
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
include PackagePrivate
|
|
@@ -143,10 +147,6 @@ module ActiveRecordCompose
|
|
|
143
147
|
def raise_on_save_error_message: -> String
|
|
144
148
|
end
|
|
145
149
|
|
|
146
|
-
class Railtie < Rails::Railtie
|
|
147
|
-
extend Rails::Initializable::ClassMethods
|
|
148
|
-
end
|
|
149
|
-
|
|
150
150
|
module Validations : Model
|
|
151
151
|
extend ActiveSupport::Concern
|
|
152
152
|
extend ActiveModel::Validations::ClassMethods
|
|
@@ -154,6 +154,7 @@ module ActiveRecordCompose
|
|
|
154
154
|
def save: (**untyped options) -> bool
|
|
155
155
|
def save!: (**untyped options) -> untyped
|
|
156
156
|
def valid?: (?validation_context context) -> bool
|
|
157
|
+
def detect_circular_reference: (?Array[Integer])-> untyped
|
|
157
158
|
|
|
158
159
|
@context_for_override_validation: OverrideValidationContext
|
|
159
160
|
|
|
@@ -187,6 +188,7 @@ module ActiveRecordCompose
|
|
|
187
188
|
attr_reader model: ar_like
|
|
188
189
|
attr_reader destroy_context_type: (bool | destroy_context_type)
|
|
189
190
|
attr_reader if_option: (nil | condition_type)
|
|
191
|
+
def equality_key: () -> [ar_like, (bool | destroy_context_type), (nil | condition_type)]
|
|
190
192
|
|
|
191
193
|
module PackagePrivate
|
|
192
194
|
def __raw_model: () -> ar_like
|
|
@@ -47,6 +47,9 @@ module ActiveRecordCompose
|
|
|
47
47
|
def delete: (ar_like) -> ComposedCollection?
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
class CircularReferenceDetected < StandardError
|
|
51
|
+
end
|
|
52
|
+
|
|
50
53
|
class Model
|
|
51
54
|
include ActiveModel::Model
|
|
52
55
|
include ActiveModel::Validations::Callbacks
|
|
@@ -68,6 +71,7 @@ module ActiveRecordCompose
|
|
|
68
71
|
def self.around_update: (*around_callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
|
|
69
72
|
def self.after_update: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
|
|
70
73
|
|
|
74
|
+
def self.before_commit: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
|
|
71
75
|
def self.after_commit: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
|
|
72
76
|
def self.after_rollback: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
|
|
73
77
|
|
|
@@ -90,4 +94,8 @@ module ActiveRecordCompose
|
|
|
90
94
|
private
|
|
91
95
|
def models: -> ComposedCollection
|
|
92
96
|
end
|
|
97
|
+
|
|
98
|
+
class Railtie < Rails::Railtie
|
|
99
|
+
extend Rails::Initializable::ClassMethods
|
|
100
|
+
end
|
|
93
101
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_record_compose
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamajyotan
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- lib/active_record_compose/attributes/querying.rb
|
|
51
51
|
- lib/active_record_compose/callbacks.rb
|
|
52
52
|
- lib/active_record_compose/composed_collection.rb
|
|
53
|
+
- lib/active_record_compose/exceptions.rb
|
|
53
54
|
- lib/active_record_compose/inspectable.rb
|
|
54
55
|
- lib/active_record_compose/model.rb
|
|
55
56
|
- lib/active_record_compose/persistence.rb
|
|
@@ -83,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
83
84
|
- !ruby/object:Gem::Version
|
|
84
85
|
version: '0'
|
|
85
86
|
requirements: []
|
|
86
|
-
rubygems_version:
|
|
87
|
+
rubygems_version: 4.0.3
|
|
87
88
|
specification_version: 4
|
|
88
89
|
summary: activemodel form object pattern
|
|
89
90
|
test_files: []
|