active_record_compose 1.0.1 → 1.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: a8fa5fa4117b180d20233c6dc06ca8797c2c9931cca2708e5a01bd2b560c42d2
4
- data.tar.gz: cdb73d45e04a3195f9834390f9e9b6e8242e4a9b325a6ec684d3c7c2f97076ef
3
+ metadata.gz: 491f46737a3744c5c5d3e0319de3ee9f64ae21a9adfab39f36cedc478bfa0e7c
4
+ data.tar.gz: 406f4c471e0d7c40ec7b3a0d492f8e7e6c96d93fd4b43204a128bf52c1f7ed4c
5
5
  SHA512:
6
- metadata.gz: 2317d9e39b054c728b0847c6cde3f556edfcfa57de9b1c6023f10cf264f5c498bcf654e252d7a29c0a071d349ade5f00943e512938a427b6d253bd43592ebace
7
- data.tar.gz: 71ad4690a7af527a1d323e3ccc84b25c0b7f875fcc3655883d0b8c2e35fe165b3b1f17a6201152e6d0281b21716976c77eec26db6c5712c0630d2741e6b72886
6
+ metadata.gz: 9ff0e91cfb0ca22322d1b9ca47c32f09372afa206e9fa6690c1dfe5ca30d221269ccc55fe17556627234ea8a7019118ddec8a46ad24a52c47239e694dbcac323
7
+ data.tar.gz: 925001a566ee81600ce91862c0c95f241dba2cd735f6ecbc3e987e83e6cd6f59d217e67afa4bed0a981f35c16645661ef7eeb00adb18df92999f9127e22116e0
data/.yardopts CHANGED
@@ -1,4 +1,6 @@
1
1
  --private
2
+ --no-private
2
3
  --markup markdown
3
4
  --markup-provider redcarpet
5
+ --plugin activesupport-concern
4
6
  --default-return void
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.1] - 2025-12-04
4
+
5
+ * fix: the save method would return nil instead of false.
6
+ * doc: We've simplified the documentation comment yard.
7
+
8
+ ## [1.1.0] - 2025-11-19
9
+
10
+ * Implemented ActiveRecord-like #inspect
11
+ In activerecord's `#inspect`, the string is a list of attributes, and we have reproduced a similar format.
12
+ (https://github.com/hamajyotan/active_record_compose/pull/45)
13
+ * `.with_connection` `.lease_connection` and `.connection` are deprecated. Use `ActiveRecord::Base.with_connection` etc. instead.
14
+ (https://github.com/hamajyotan/active_record_compose/pull/46)
15
+ * refactor: Remove `ActiveRecord::Transactions` module dependency
16
+ (https://github.com/hamajyotan/active_record_compose/pull/44)
17
+
3
18
  ## [1.0.1] - 2025-10-17
4
19
 
5
20
  * Removed the private interface `composite_primary_key?`
@@ -5,8 +5,6 @@ require_relative "attributes/delegation"
5
5
  require_relative "attributes/querying"
6
6
 
7
7
  module ActiveRecordCompose
8
- # @private
9
- #
10
8
  # Provides attribute-related functionality for use within ActiveRecordCompose::Model.
11
9
  #
12
10
  # This module allows you to define attributes on your composed model, including support
@@ -56,45 +54,32 @@ module ActiveRecordCompose
56
54
  include ActiveModel::Attributes
57
55
 
58
56
  included do
57
+ # @type self: Class
58
+
59
59
  include Querying
60
60
 
61
- # @type self: Class
62
61
  class_attribute :delegated_attributes, instance_writer: false
63
62
  end
64
63
 
65
- module ClassMethods
66
- # Defines the reader and writer for the specified attribute.
67
- #
68
- # @example
69
- # class AccountRegistration < ActiveRecordCompose::Model
70
- # def initialize(account, attributes = {})
71
- # @account = account
72
- # super(attributes)
73
- # models.push(account)
74
- # end
75
- #
76
- # attribute :original_attribute, :string, default: "qux"
77
- # delegate_attribute :name, to: :account
78
- #
79
- # private
80
- #
81
- # attr_reader :account
82
- # end
83
- #
84
- # account = Account.new
85
- # account.name = "foo"
86
- #
87
- # registration = AccountRegistration.new(account)
88
- # registration.name # => "foo" (delegated)
89
- # registration.name? # => true (delegated attribute method + `?`)
90
- #
91
- # registration.name = "bar" # => updates account.name
92
- # account.name # => "bar"
93
- # account.name? # => true
64
+ # steep:ignore:start
65
+
66
+ class_methods do
67
+ # Provides a method of attribute access to the encapsulated model.
94
68
  #
95
- # registration.attributes
96
- # # => { "original_attribute" => "qux", "name" => "bar" }
69
+ # It provides a way to access the attributes of the model it encompasses,
70
+ # allowing transparent access as if it had those attributes itself.
97
71
  #
72
+ # @param [Array<Symbol, String>] attributes
73
+ # attributes A variable-length list of attribute names to delegate.
74
+ # @param [Symbol, String] to
75
+ # The target object to which attributes are delegated (keyword argument).
76
+ # @param [Boolean] allow_nil
77
+ # allow_nil Whether to allow nil values. Defaults to false.
78
+ # @example Basic usage
79
+ # delegate_attribute :name, :email, to: :profile
80
+ # @example Allowing nil
81
+ # delegate_attribute :bio, to: :profile, allow_nil: true
82
+ # @see Module#delegate for similar behavior in ActiveSupport
98
83
  def delegate_attribute(*attributes, to:, allow_nil: false)
99
84
  if to.start_with?("@")
100
85
  raise ArgumentError, "Instance variables cannot be specified in delegate to. (#{to})"
@@ -107,45 +92,68 @@ module ActiveRecordCompose
107
92
  end
108
93
 
109
94
  # Returns a array of attribute name.
110
- # Attributes declared with `delegate_attribute` are also merged.
95
+ # Attributes declared with {.delegate_attribute} are also merged.
111
96
  #
97
+ # @see #attribute_names
112
98
  # @return [Array<String>] array of attribute name.
113
99
  def attribute_names = super + delegated_attributes.to_a.map { _1.attribute_name }
114
100
  end
115
101
 
102
+ # steep:ignore:end
103
+
116
104
  # Returns a array of attribute name.
117
- # Attributes declared with `delegate_attribute` are also merged.
105
+ # Attributes declared with {.delegate_attribute} are also merged.
106
+ #
107
+ # class Foo < ActiveRecordCompose::Base
108
+ # def initialize(attributes = {})
109
+ # @account = Account.new
110
+ # super
111
+ # end
112
+ #
113
+ # attribute :confirmation, :boolean, default: false # plain attribute
114
+ # delegate_attribute :name, to: :account # delegated attribute
115
+ #
116
+ # private
117
+ #
118
+ # attr_reader :account
119
+ # end
118
120
  #
121
+ # Foo.attribute_names # Returns the merged state of plain and delegated attributes
122
+ # # => ["confirmation" ,"name"]
123
+ #
124
+ # foo = Foo.new
125
+ # foo.attribute_names # Similar behavior for instance method version
126
+ # # => ["confirmation", "name"]
127
+ #
128
+ # @see #attributes
119
129
  # @return [Array<String>] array of attribute name.
120
130
  def attribute_names = super + delegated_attributes.to_a.map { _1.attribute_name }
121
131
 
122
132
  # Returns a hash with the attribute name as key and the attribute value as value.
123
- # Attributes declared with `delegate_attribute` are also merged.
133
+ # Attributes declared with {.delegate_attribute} are also merged.
124
134
  #
125
- # @return [Hash] hash with the attribute name as key and the attribute value as value.
126
- # @example
127
- # class AccountRegistration < ActiveRecordCompose::Model
128
- # def initialize(account, attributes = {})
129
- # @account = account
130
- # super(attributes)
131
- # models.push(account)
132
- # end
135
+ # class Foo < ActiveRecordCompose::Base
136
+ # def initialize(attributes = {})
137
+ # @account = Account.new
138
+ # super
139
+ # end
133
140
  #
134
- # attribute :original_attribute, :string, default: "qux"
135
- # delegate_attribute :name, to: :account
141
+ # attribute :confirmation, :boolean, default: false # plain attribute
142
+ # delegate_attribute :name, to: :account # delegated attribute
136
143
  #
137
- # private
144
+ # private
138
145
  #
139
- # attr_reader :account
140
- # end
141
- #
142
- # account = Account.new
143
- # account.name = "foo"
146
+ # attr_reader :account
147
+ # end
144
148
  #
145
- # registration = AccountRegistration.new(account)
149
+ # foo = Foo.new
150
+ # foo.name = "Alice"
151
+ # foo.confirmation = true
146
152
  #
147
- # registration.attributes # => { "original_attribute" => "qux", "name" => "bar" }
153
+ # foo.attributes # Returns the merged state of plain and delegated attributes
154
+ # # => { "confirmation" => true, "name" => "Alice" }
148
155
  #
156
+ # @return [Hash<String, Object>] hash with the attribute name as key and the attribute value as value.
149
157
  def attributes
150
158
  super.merge(*delegated_attributes.to_a.map { _1.attribute_hash(self) })
151
159
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordCompose
4
- # @private
5
- #
6
4
  # Provides hooks into the life cycle of an ActiveRecordCompose model,
7
5
  # allowing you to insert custom logic before or after changes to the object's state.
8
6
  #
@@ -21,18 +19,48 @@ module ActiveRecordCompose
21
19
  include ActiveModel::Validations::Callbacks
22
20
 
23
21
  included do
22
+ # @!method self.before_save(*args, &block)
23
+ # Registers a callback to be called before a model is saved.
24
+
25
+ # @!method self.around_save(*args, &block)
26
+ # Registers a callback to be called around the save of a model.
27
+
28
+ # @!method self.after_save(*args, &block)
29
+ # Registers a callback to be called after a model is saved.
30
+
24
31
  define_model_callbacks :save
32
+
33
+ # @!method self.before_create(*args, &block)
34
+ # Registers a callback to be called before a model is created.
35
+
36
+ # @!method self.around_create(*args, &block)
37
+ # Registers a callback to be called around the creation of a model.
38
+
39
+ # @!method self.after_create(*args, &block)
40
+ # Registers a callback to be called after a model is created.
41
+
25
42
  define_model_callbacks :create
43
+
44
+ # @!method self.before_update(*args, &block)
45
+ # Registers a callback to be called before a model is updated.
46
+
47
+ # @!method self.around_update(*args, &block)
48
+ # Registers a callback to be called around the update of a model.
49
+
50
+ # @!method self.after_update(*args, &block)
51
+ # Registers a callback to be called after a update is updated.
26
52
  define_model_callbacks :update
27
53
  end
28
54
 
29
55
  private
30
56
 
57
+ # @private
31
58
  # Evaluate while firing callbacks such as `before_save` `after_save`
32
59
  # before and after block evaluation.
33
60
  #
34
61
  def with_callbacks(&block) = run_callbacks(:save) { run_callbacks(callback_context, &block) }
35
62
 
63
+ # @private
36
64
  # Returns the symbol representing the callback context, which is `:create` if the record
37
65
  # is new, or `:update` if it has been persisted.
38
66
  #
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/parameter_filter"
4
+ require_relative "attributes"
5
+
6
+ module ActiveRecordCompose
7
+ # It provides #inspect behavior.
8
+ # It tries to replicate the inspect format provided by ActiveRecord as closely as possible.
9
+ #
10
+ # @example
11
+ # class Model < ActiveRecordCompose::Model
12
+ # def initialize(ar_model)
13
+ # @ar_model = ar_model
14
+ # super
15
+ # end
16
+ #
17
+ # attribute :foo, :date, default: -> { Date.today }
18
+ # delegate_attribute :bar, to: :ar_model
19
+ #
20
+ # private attr_reader :ar_model
21
+ # end
22
+ #
23
+ # m = Model.new(ar_model)
24
+ # m.inspect #=> #<Model:0x00007ff0fe75fe58 foo: "2025-11-14", bar: "bar">
25
+ #
26
+ # @example
27
+ # class Model < ActiveRecordCompose::Model
28
+ # self.filter_attributes += %i[foo]
29
+ #
30
+ # # ...
31
+ # end
32
+ #
33
+ # m = Model.new(ar_model)
34
+ # m.inspect #=> #<Model:0x00007ff0fe75fe58 foo: [FILTERED], bar: "bar">
35
+ #
36
+ module Inspectable
37
+ extend ActiveSupport::Concern
38
+ include ActiveRecordCompose::Attributes
39
+
40
+ # steep:ignore:start
41
+
42
+ # @private
43
+ FILTERED_MASK =
44
+ Class.new(DelegateClass(::String)) do
45
+ def pretty_print(pp)
46
+ pp.text __getobj__
47
+ end
48
+ end.new(ActiveSupport::ParameterFilter::FILTERED).freeze
49
+ private_constant :FILTERED_MASK
50
+
51
+ # steep:ignore:end
52
+
53
+ included do
54
+ self.filter_attributes = []
55
+ end
56
+
57
+ # steep:ignore:start
58
+
59
+ class_methods do
60
+ # Returns columns not to expose when invoking {#inspect}.
61
+ #
62
+ # @return [Array<Symbol>]
63
+ # @see #inspect
64
+ def filter_attributes
65
+ if @filter_attributes.nil?
66
+ superclass.filter_attributes
67
+ else
68
+ @filter_attributes
69
+ end
70
+ end
71
+
72
+ # Specify columns not to expose when invoking {#inspect}.
73
+ #
74
+ # @param [Array<Symbol>] value
75
+ # @see #inspect
76
+ def filter_attributes=(value)
77
+ @inspection_filter = nil
78
+ @filter_attributes = value
79
+ end
80
+
81
+ # @private
82
+ def inspection_filter
83
+ if @filter_attributes.nil?
84
+ superclass.inspection_filter
85
+ else
86
+ @inspection_filter ||= ActiveSupport::ParameterFilter.new(filter_attributes, mask: FILTERED_MASK)
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def inherited(subclass)
93
+ super
94
+
95
+ subclass.class_eval do
96
+ @inspection_filter = nil
97
+ @filter_attributes ||= nil
98
+ end
99
+ end
100
+ end
101
+
102
+ # steep:ignore:end
103
+
104
+ # Returns a formatted string representation of the record's attributes.
105
+ # It tries to replicate the inspect format provided by ActiveRecord as closely as possible.
106
+ #
107
+ # @example
108
+ # class Model < ActiveRecordCompose::Model
109
+ # def initialize(ar_model)
110
+ # @ar_model = ar_model
111
+ # super
112
+ # end
113
+ #
114
+ # attribute :foo, :date, default: -> { Date.today }
115
+ # delegate_attribute :bar, to: :ar_model
116
+ #
117
+ # private attr_reader :ar_model
118
+ # end
119
+ #
120
+ # m = Model.new(ar_model)
121
+ # m.inspect #=> #<Model:0x00007ff0fe75fe58 foo: "2025-11-14", bar: "bar">
122
+ #
123
+ # @example use {.filter_attributes}
124
+ # class Model < ActiveRecordCompose::Model
125
+ # self.filter_attributes += %i[foo]
126
+ #
127
+ # # ...
128
+ # end
129
+ #
130
+ # m = Model.new(ar_model)
131
+ # m.inspect #=> #<Model:0x00007ff0fe75fe58 foo: [FILTERED], bar: "bar">
132
+ #
133
+ # @return [String] formatted string representation of the record's attributes.
134
+ # @see .filter_attributes
135
+ def inspect
136
+ inspection =
137
+ if @attributes
138
+ attributes.map { |k, v| "#{k}: #{format_for_inspect(k, v)}" }.join(", ")
139
+ else
140
+ "not initialized"
141
+ end
142
+
143
+ "#<#{self.class} #{inspection}>"
144
+ end
145
+
146
+ # It takes a PP and pretty prints that record.
147
+ #
148
+ def pretty_print(pp)
149
+ pp.object_address_group(self) do
150
+ if @attributes
151
+ attrs = attributes
152
+ pp.seplist(attrs.keys, proc { pp.text "," }) do |attr|
153
+ pp.breakable " "
154
+ pp.group(1) do
155
+ pp.text attr
156
+ pp.text ":"
157
+ pp.breakable
158
+ pp.text format_for_inspect(attr, attrs[attr])
159
+ end
160
+ end
161
+ else
162
+ pp.breakable " "
163
+ pp.text "not initialized"
164
+ end
165
+ end
166
+ end
167
+
168
+ private
169
+
170
+ # @private
171
+ def format_for_inspect(name, value)
172
+ return value.inspect if value.nil?
173
+
174
+ inspected_value =
175
+ if value.is_a?(String) && value.length > 50
176
+ "#{value[0, 50]}...".inspect
177
+ elsif value.is_a?(Date) || value.is_a?(Time)
178
+ %("#{value.to_fs(:inspect)}")
179
+ else
180
+ value.inspect
181
+ end
182
+
183
+ self.class.inspection_filter.filter_param(name, inspected_value)
184
+ end
185
+ end
186
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "attributes"
4
4
  require_relative "composed_collection"
5
+ require_relative "inspectable"
5
6
  require_relative "persistence"
6
7
  require_relative "transaction_support"
7
8
  require_relative "validations"
@@ -86,274 +87,7 @@ module ActiveRecordCompose
86
87
  include ActiveRecordCompose::Persistence
87
88
  include ActiveRecordCompose::Validations
88
89
  include ActiveRecordCompose::TransactionSupport
89
-
90
- begin
91
- # @group Model Core
92
-
93
- # @!method self.delegate_attribute(*attributes, to:, allow_nil: false)
94
- # Provides a method of attribute access to the encapsulated model.
95
- #
96
- # It provides a way to access the attributes of the model it encompasses,
97
- # allowing transparent access as if it had those attributes itself.
98
- #
99
- # @param [Array<Symbol, String>] attributes
100
- # attributes A variable-length list of attribute names to delegate.
101
- # @param [Symbol, String] to
102
- # The target object to which attributes are delegated (keyword argument).
103
- # @param [Boolean] allow_nil
104
- # allow_nil Whether to allow nil values. Defaults to false.
105
- # @example Basic usage
106
- # delegate_attribute :name, :email, to: :profile
107
- # @example Allowing nil
108
- # delegate_attribute :bio, to: :profile, allow_nil: true
109
- # @see Module#delegate for similar behavior in ActiveSupport
110
-
111
- # @!method self.attribute_names
112
- # Returns a array of attribute name.
113
- # Attributes declared with {.delegate_attribute} are also merged.
114
- #
115
- # @see #attribute_names
116
- # @return [Array<String>] array of attribute name.
117
-
118
- # @!method attribute_names
119
- # Returns a array of attribute name.
120
- # Attributes declared with {.delegate_attribute} are also merged.
121
- #
122
- # class Foo < ActiveRecordCompose::Base
123
- # def initialize(attributes = {})
124
- # @account = Account.new
125
- # super
126
- # end
127
- #
128
- # attribute :confirmation, :boolean, default: false # plain attribute
129
- # delegate_attribute :name, to: :account # delegated attribute
130
- #
131
- # private
132
- #
133
- # attr_reader :account
134
- # end
135
- #
136
- # Foo.attribute_names # Returns the merged state of plain and delegated attributes
137
- # # => ["confirmation" ,"name"]
138
- #
139
- # foo = Foo.new
140
- # foo.attribute_names # Similar behavior for instance method version
141
- # # => ["confirmation", "name"]
142
- #
143
- # @see #attributes
144
- # @return [Array<String>] array of attribute name.
145
-
146
- # @!method attributes
147
- # Returns a hash with the attribute name as key and the attribute value as value.
148
- # Attributes declared with {.delegate_attribute} are also merged.
149
- #
150
- # class Foo < ActiveRecordCompose::Base
151
- # def initialize(attributes = {})
152
- # @account = Account.new
153
- # super
154
- # end
155
- #
156
- # attribute :confirmation, :boolean, default: false # plain attribute
157
- # delegate_attribute :name, to: :account # delegated attribute
158
- #
159
- # private
160
- #
161
- # attr_reader :account
162
- # end
163
- #
164
- # foo = Foo.new
165
- # foo.name = "Alice"
166
- # foo.confirmation = true
167
- #
168
- # foo.attributes # Returns the merged state of plain and delegated attributes
169
- # # => { "confirmation" => true, "name" => "Alice" }
170
- #
171
- # @return [Hash<String, Object>] hash with the attribute name as key and the attribute value as value.
172
-
173
- # @!method persisted?
174
- # Returns true if model is persisted.
175
- #
176
- # By overriding this definition, you can control the callbacks that are triggered when a save is made.
177
- # For example, returning false will trigger before_create, around_create and after_create,
178
- # and returning true will trigger {.before_update}, {.around_update} and {.after_update}.
179
- #
180
- # @return [Boolean] returns true if model is persisted.
181
- # @example
182
- # # A model where persistence is always false
183
- # class Foo < ActiveRecordCompose::Model
184
- # before_save { puts "before_save called" }
185
- # before_create { puts "before_create called" }
186
- # before_update { puts "before_update called" }
187
- # after_update { puts "after_update called" }
188
- # after_create { puts "after_create called" }
189
- # after_save { puts "after_save called" }
190
- #
191
- # def persisted? = false
192
- # end
193
- #
194
- # # A model where persistence is always true
195
- # class Bar < Foo
196
- # def persisted? = true
197
- # end
198
- #
199
- # Foo.new.save!
200
- # # before_save called
201
- # # before_create called
202
- # # after_create called
203
- # # after_save called
204
- #
205
- # Bar.new.save!
206
- # # before_save called
207
- # # before_update called
208
- # # after_update called
209
- # # after_save called
210
-
211
- # @endgroup
212
-
213
- # @group Validations
214
-
215
- # @!method valid?(context = nil)
216
- # Runs all the validations and returns the result as true or false.
217
- # @param context Validation context.
218
- # @return [Boolean] true on success, false on failure.
219
-
220
- # @!method validate(context = nil)
221
- # Alias for {#valid?}
222
- # @see #valid? Validation context.
223
- # @param context
224
- # @return [Boolean] true on success, false on failure.
225
-
226
- # @!method validate!(context = nil)
227
- # @see #valid?
228
- # Runs all the validations within the specified context.
229
- # no errors are found, raises `ActiveRecord::RecordInvalid` otherwise.
230
- # @param context Validation context.
231
- # @raise ActiveRecord::RecordInvalid
232
-
233
- # @!method errors
234
- # Returns the `ActiveModel::Errors` object that holds all information about attribute error messages.
235
- #
236
- # The `ActiveModel::Base` implementation itself,
237
- # but also aggregates error information for objects stored in {#models} when validation is performed.
238
- #
239
- # class Account < ActiveRecord::Base
240
- # validates :name, :email, presence: true
241
- # end
242
- #
243
- # class AccountRegistration < ActiveRecordCompose::Model
244
- # def initialize(attributes = {})
245
- # @account = Account.new
246
- # super(attributes)
247
- # models << account
248
- # end
249
- #
250
- # attribute :confirmation, :boolean, default: false
251
- # validates :confirmation, presence: true
252
- #
253
- # private
254
- #
255
- # attr_reader :account
256
- # end
257
- #
258
- # registration = AccountRegistration
259
- # registration.valid?
260
- # #=> false
261
- #
262
- # # In addition to the model's own validation error information (`confirmation`), also aggregates
263
- # # error information for objects stored in `account` (`name`, `email`) when validation is performed.
264
- #
265
- # registration.errors.map { _1.attribute } #=> [:name, :email, :confirmation]
266
- #
267
- # @return [ActiveModel::Errors]
268
-
269
- # @endgroup
270
-
271
- # @group Persistences
272
-
273
- # @!method save(**options)
274
- # Save the models that exist in models.
275
- # Returns false if any of the targets fail, true if all succeed.
276
- #
277
- # The save is performed within a single transaction.
278
- #
279
- # Only the `:validate` option takes effect as it is required internally.
280
- # However, we do not recommend explicitly specifying `validate: false` to skip validation.
281
- # Additionally, the `:context` option is not accepted.
282
- # The need for such a value indicates that operations from multiple contexts are being processed.
283
- # If the contexts differ, we recommend separating them into different model definitions.
284
- #
285
- # @params [Hash] Optional parameters.
286
- # @option options [Boolean] :validate Whether to run validations.
287
- # This option is intended for internal use only.
288
- # Users should avoid explicitly passing <tt>validate: false</tt>,
289
- # as skipping validations can lead to unexpected behavior.
290
- # @return [Boolean] returns true on success, false on failure.
291
-
292
- # @!method save!(**options)
293
- # Behavior is same to {#save}, but raises an exception prematurely on failure.
294
- # @see #save
295
- # @raise ActiveRecord::RecordInvalid
296
- # @raise ActiveRecord::RecordNotSaved
297
-
298
- # @!method update(attributes)
299
- # Assign attributes and {#save}.
300
- #
301
- # @param [Hash<String, Object>] attributes
302
- # new attributes.
303
- # @see #save
304
- # @return [Boolean] returns true on success, false on failure.
305
-
306
- # @!method update!(attributes)
307
- # Behavior is same to {#update}, but raises an exception prematurely on failure.
308
- #
309
- # @param [Hash<String, Object>] attributes
310
- # new attributes.
311
- # @see #save
312
- # @see #update
313
- # @raise ActiveRecord::RecordInvalid
314
- # @raise ActiveRecord::RecordNotSaved
315
-
316
- # @endgroup
317
-
318
- # @group Callbacks
319
-
320
- # @!method self.before_save(*args, &block)
321
- # Registers a callback to be called before a model is saved.
322
-
323
- # @!method self.around_save(*args, &block)
324
- # Registers a callback to be called around the save of a model.
325
-
326
- # @!method self.after_save(*args, &block)
327
- # Registers a callback to be called after a model is saved.
328
-
329
- # @!method self.before_create(*args, &block)
330
- # Registers a callback to be called before a model is created.
331
-
332
- # @!method self.around_create(*args, &block)
333
- # Registers a callback to be called around the creation of a model.
334
-
335
- # @!method self.after_create(*args, &block)
336
- # Registers a callback to be called after a model is created.
337
-
338
- # @!method self.before_update(*args, &block)
339
- # Registers a callback to be called before a model is updated.
340
-
341
- # @!method self.around_update(*args, &block)
342
- # Registers a callback to be called around the update of a model.
343
-
344
- # @!method self.after_update(*args, &block)
345
- # Registers a callback to be called after a update is updated.
346
-
347
- # @!method self.after_commit(*args, &block)
348
- # Registers a block to be called after the transaction is fully committed.
349
-
350
- # @!method self.after_rollback(*args, &block)
351
- # Registers a block to be called after the transaction is rolled back.
352
-
353
- # @endgroup
354
- end
355
-
356
- # @group Model Core
90
+ include ActiveRecordCompose::Inspectable
357
91
 
358
92
  def initialize(attributes = {})
359
93
  super
@@ -394,7 +128,5 @@ module ActiveRecordCompose
394
128
  # @return [ActiveRecordCompose::ComposedCollection]
395
129
  #
396
130
  def models = @__models ||= ActiveRecordCompose::ComposedCollection.new(self)
397
-
398
- # @endgroup
399
131
  end
400
132
  end
@@ -6,7 +6,6 @@ require_relative "composed_collection"
6
6
  module ActiveRecordCompose
7
7
  using ComposedCollection::PackagePrivate
8
8
 
9
- # @private
10
9
  module Persistence
11
10
  extend ActiveSupport::Concern
12
11
  include ActiveRecordCompose::Callbacks
@@ -22,6 +21,11 @@ module ActiveRecordCompose
22
21
  # The need for such a value indicates that operations from multiple contexts are being processed.
23
22
  # If the contexts differ, we recommend separating them into different model definitions.
24
23
  #
24
+ # @param options [Hash] parameters.
25
+ # @option options [Boolean] :validate Whether to run validations.
26
+ # This option is intended for internal use only.
27
+ # Users should avoid explicitly passing <tt>validate: false</tt>,
28
+ # as skipping validations can lead to unexpected behavior.
25
29
  # @return [Boolean] returns true on success, false on failure.
26
30
  def save(**options)
27
31
  with_callbacks { save_models(**options, bang: false) }
@@ -29,38 +33,80 @@ module ActiveRecordCompose
29
33
  false
30
34
  end
31
35
 
32
- # Save the models that exist in models.
33
- # Unlike #save, an exception is raises on failure.
34
- #
35
- # Saving, like `#save`, is performed within a single transaction.
36
- #
37
- # Only the `:validate` option takes effect as it is required internally.
38
- # However, we do not recommend explicitly specifying `validate: false` to skip validation.
39
- # Additionally, the `:context` option is not accepted.
40
- # The need for such a value indicates that operations from multiple contexts are being processed.
41
- # If the contexts differ, we recommend separating them into different model definitions.
36
+ # Behavior is same to {#save}, but raises an exception prematurely on failure.
42
37
  #
38
+ # @see #save
39
+ # @raise ActiveRecord::RecordInvalid
40
+ # @raise ActiveRecord::RecordNotSaved
43
41
  def save!(**options)
44
42
  with_callbacks { save_models(**options, bang: true) } || raise_on_save_error
45
43
  end
46
44
 
47
- # Assign attributes and save.
45
+ # Assign attributes and {#save}.
48
46
  #
47
+ # @param [Hash<String, Object>] attributes
48
+ # new attributes.
49
+ # @see #save
49
50
  # @return [Boolean] returns true on success, false on failure.
50
51
  def update(attributes)
51
52
  assign_attributes(attributes)
52
53
  save
53
54
  end
54
55
 
55
- # Behavior is same to `#update`, but raises an exception prematurely on failure.
56
+ # Behavior is same to {#update}, but raises an exception prematurely on failure.
56
57
  #
58
+ # @param [Hash<String, Object>] attributes
59
+ # new attributes.
60
+ # @see #save
61
+ # @see #update
62
+ # @raise ActiveRecord::RecordInvalid
63
+ # @raise ActiveRecord::RecordNotSaved
57
64
  def update!(attributes)
58
65
  assign_attributes(attributes)
59
66
  save!
60
67
  end
61
68
 
69
+ # @!method persisted?
70
+ # Returns true if model is persisted.
71
+ #
72
+ # By overriding this definition, you can control the callbacks that are triggered when a save is made.
73
+ # For example, returning false will trigger before_create, around_create and after_create,
74
+ # and returning true will trigger {.before_update}, {.around_update} and {.after_update}.
75
+ #
76
+ # @return [Boolean] returns true if model is persisted.
77
+ # @example
78
+ # # A model where persistence is always false
79
+ # class Foo < ActiveRecordCompose::Model
80
+ # before_save { puts "before_save called" }
81
+ # before_create { puts "before_create called" }
82
+ # before_update { puts "before_update called" }
83
+ # after_update { puts "after_update called" }
84
+ # after_create { puts "after_create called" }
85
+ # after_save { puts "after_save called" }
86
+ #
87
+ # def persisted? = false
88
+ # end
89
+ #
90
+ # # A model where persistence is always true
91
+ # class Bar < Foo
92
+ # def persisted? = true
93
+ # end
94
+ #
95
+ # Foo.new.save!
96
+ # # before_save called
97
+ # # before_create called
98
+ # # after_create called
99
+ # # after_save called
100
+ #
101
+ # Bar.new.save!
102
+ # # before_save called
103
+ # # before_update called
104
+ # # after_update called
105
+ # # after_save called
106
+
62
107
  private
63
108
 
109
+ # @private
64
110
  def save_models(bang:, **options)
65
111
  models.__wrapped_models.all? do |model|
66
112
  if bang
@@ -71,8 +117,10 @@ module ActiveRecordCompose
71
117
  end
72
118
  end
73
119
 
120
+ # @private
74
121
  def raise_on_save_error = raise ActiveRecord::RecordNotSaved.new(raise_on_save_error_message, self)
75
122
 
123
+ # @private
76
124
  def raise_on_save_error_message = "Failed to save the model."
77
125
  end
78
126
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "active_record_compose"
5
+ require "active_record/railtie"
6
+
7
+ module ActiveRecordCompose
8
+ class Railtie < Rails::Railtie
9
+ initializer "active_record_compose.set_filter_attributes", after: "active_record.set_filter_attributes" do
10
+ ActiveSupport.on_load(:active_record) do
11
+ ActiveRecordCompose::Model.filter_attributes += ActiveRecord::Base.filter_attributes
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,31 +1,154 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/module"
4
+
3
5
  module ActiveRecordCompose
4
- # @private
5
6
  module TransactionSupport
6
7
  extend ActiveSupport::Concern
7
- include ActiveRecord::Transactions
8
8
 
9
9
  included do
10
- # ActiveRecord::Transactions is defined so that methods such as save,
11
- # destroy and touch are wrapped with_transaction_returning_status.
12
- # However, ActiveRecordCompose::Model does not support destroy and touch, and
13
- # we want to keep these operations as undefined behavior, so we remove the definition here.
14
- undef_method :destroy, :touch
10
+ define_callbacks :commit, :rollback, :before_commit, scope: [ :kind, :name ]
11
+ end
12
+
13
+ # steep:ignore:start
14
+
15
+ class_methods do
16
+ # @private
17
+ # @deprecated
18
+ def with_connection(...)
19
+ ActiveRecord.deprecator.warn("`with_connection` is deprecated. Use `ActiveRecord::Base.with_connection` instead.")
20
+ ActiveRecord::Base.with_connection(...)
21
+ end
22
+
23
+ # @private
24
+ # @deprecated
25
+ def lease_connection(...)
26
+ ActiveRecord.deprecator.warn("`lease_connection` is deprecated. Use `ActiveRecord::Base.lease_connection` instead.")
27
+ ActiveRecord::Base.lease_connection(...)
28
+ end
29
+
30
+ # @private
31
+ # @deprecated
32
+ def connection(...)
33
+ ActiveRecord.deprecator.warn("`connection` is deprecated. Use `ActiveRecord::Base.connection` instead.")
34
+ ActiveRecord::Base.connection(...)
35
+ end
15
36
  end
16
37
 
17
- module ClassMethods
18
- delegate :with_connection, :lease_connection, to: :ar_class
38
+ # steep:ignore:end
39
+
40
+ # steep:ignore:start
41
+
42
+ class_methods do
43
+ # @private
44
+ def before_commit(*args, &block)
45
+ set_options_for_callbacks!(args)
46
+ set_callback(:before_commit, :before, *args, &block)
47
+ end
19
48
 
20
- # In ActiveRecord, it is soft deprecated.
21
- delegate :connection, to: :ar_class
49
+ # Registers a block to be called after the transaction is fully committed.
50
+ #
51
+ def after_commit(*args, &block)
52
+ set_options_for_callbacks!(args, prepend_option)
53
+ set_callback(:commit, :after, *args, &block)
54
+ end
55
+
56
+ # Registers a block to be called after the transaction is rolled back.
57
+ #
58
+ def after_rollback(*args, &block)
59
+ set_options_for_callbacks!(args, prepend_option)
60
+ set_callback(:rollback, :after, *args, &block)
61
+ end
22
62
 
23
63
  private
24
64
 
25
- def ar_class = ActiveRecord::Base
65
+ # @private
66
+ def prepend_option
67
+ if ActiveRecord.run_after_transaction_callbacks_in_order_defined
68
+ { prepend: true }
69
+ else
70
+ {}
71
+ end
72
+ end
73
+
74
+ # @private
75
+ def set_options_for_callbacks!(args, enforced_options = {})
76
+ options = args.extract_options!.merge!(enforced_options)
77
+ args << options
78
+ end
79
+ end
80
+
81
+ # steep:ignore:end
82
+
83
+ concerning :SupportForActiveRecordConnectionAdaptersTransaction do
84
+ # @private
85
+ def trigger_transactional_callbacks? = true
86
+
87
+ # @private
88
+ def before_committed!
89
+ _run_before_commit_callbacks
90
+ end
91
+
92
+ # @private
93
+ def committed!(should_run_callbacks: true)
94
+ _run_commit_callbacks if should_run_callbacks
95
+ end
96
+
97
+ # @private
98
+ def rolledback!(force_restore_state: false, should_run_callbacks: true)
99
+ _run_rollback_callbacks if should_run_callbacks
100
+ end
101
+ end
102
+
103
+ def save(**options) = with_transaction_returning_status { super }
104
+
105
+ def save!(**options) = with_transaction_returning_status { super }
106
+
107
+ private
108
+
109
+ # @private
110
+ def with_transaction_returning_status
111
+ connection_pool.with_connection do |connection|
112
+ with_pool_transaction_isolation_level(connection) do
113
+ ensure_finalize = !connection.transaction_open?
114
+
115
+ connection.transaction do
116
+ connection.add_transaction_record(self, ensure_finalize || has_transactional_callbacks?) # steep:ignore
117
+
118
+ yield.tap { raise ActiveRecord::Rollback unless _1 }
119
+ end || false
120
+ end
121
+ end
26
122
  end
27
123
 
28
- def trigger_transactional_callbacks? = true
29
- def restore_transaction_record_state(_force_restore_state = false) = nil
124
+ # @private
125
+ def default_ar_class = ActiveRecord::Base
126
+
127
+ # @private
128
+ def connection_pool(ar_class: default_ar_class)
129
+ connection_specification_name = ar_class.connection_specification_name
130
+ role = ar_class.current_role
131
+ shard = ar_class.current_shard # steep:ignore
132
+ connection_handler = ar_class.connection_handler # steep:ignore
133
+ retrieve_options = { role:, shard: }
134
+ retrieve_options[:strict] = true if ActiveRecord.gem_version.release >= Gem::Version.new("7.2.0")
135
+
136
+ connection_handler.retrieve_connection_pool(connection_specification_name, **retrieve_options)
137
+ end
138
+
139
+ # @private
140
+ def with_pool_transaction_isolation_level(connection, &block)
141
+ if ActiveRecord.gem_version.release >= Gem::Version.new("8.1.0")
142
+ isolation_level = ActiveRecord.default_transaction_isolation_level # steep:ignore
143
+ connection.pool.with_pool_transaction_isolation_level(isolation_level, connection.transaction_open?, &block)
144
+ else
145
+ block.call
146
+ end
147
+ end
148
+
149
+ # @private
150
+ def has_transactional_callbacks?
151
+ _rollback_callbacks.present? || _commit_callbacks.present? || _before_commit_callbacks.present? # steep:ignore
152
+ end
30
153
  end
31
154
  end
@@ -5,7 +5,6 @@ require_relative "composed_collection"
5
5
  module ActiveRecordCompose
6
6
  using ComposedCollection::PackagePrivate
7
7
 
8
- # @private
9
8
  module Validations
10
9
  extend ActiveSupport::Concern
11
10
  include ActiveModel::Validations::Callbacks
@@ -22,27 +21,86 @@ module ActiveRecordCompose
22
21
  perform_validations(options) ? super : raise_validation_error
23
22
  end
24
23
 
24
+ # Runs all the validations and returns the result as true or false.
25
+ #
26
+ # @param context Validation context.
27
+ # @return [Boolean] true on success, false on failure.
25
28
  def valid?(context = nil) = context_for_override_validation.with_override(context) { super }
26
29
 
30
+ # @!method validate(context = nil)
31
+ # Alias for {#valid?}
32
+ # @see #valid? Validation context.
33
+ # @param context
34
+ # @return [Boolean] true on success, false on failure.
35
+
36
+ # @!method validate!(context = nil)
37
+ # @see #valid?
38
+ # Runs all the validations within the specified context.
39
+ # no errors are found, raises `ActiveRecord::RecordInvalid` otherwise.
40
+ # @param context Validation context.
41
+ # @raise ActiveRecord::RecordInvalid
42
+
43
+ # @!method errors
44
+ # Returns the `ActiveModel::Errors` object that holds all information about attribute error messages.
45
+ #
46
+ # The `ActiveModel::Base` implementation itself,
47
+ # but also aggregates error information for objects stored in {#models} when validation is performed.
48
+ #
49
+ # class Account < ActiveRecord::Base
50
+ # validates :name, :email, presence: true
51
+ # end
52
+ #
53
+ # class AccountRegistration < ActiveRecordCompose::Model
54
+ # def initialize(attributes = {})
55
+ # @account = Account.new
56
+ # super(attributes)
57
+ # models << account
58
+ # end
59
+ #
60
+ # attribute :confirmation, :boolean, default: false
61
+ # validates :confirmation, presence: true
62
+ #
63
+ # private
64
+ #
65
+ # attr_reader :account
66
+ # end
67
+ #
68
+ # registration = AccountRegistration
69
+ # registration.valid?
70
+ # #=> false
71
+ #
72
+ # # In addition to the model's own validation error information (`confirmation`), also aggregates
73
+ # # error information for objects stored in `account` (`name`, `email`) when validation is performed.
74
+ #
75
+ # registration.errors.map { _1.attribute } #=> [:name, :email, :confirmation]
76
+ #
77
+ # @return [ActiveModel::Errors]
78
+
27
79
  private
28
80
 
81
+ # @private
29
82
  def validate_models
30
83
  context = override_validation_context
31
84
  models.__wrapped_models.lazy.select { _1.invalid?(context) }.each { errors.merge!(_1) }
32
85
  end
33
86
 
87
+ # @private
34
88
  def perform_validations(options)
35
89
  options[:validate] == false || valid?(options[:context])
36
90
  end
37
91
 
92
+ # @private
38
93
  def raise_validation_error = raise ActiveRecord::RecordInvalid, self
39
94
 
95
+ # @private
40
96
  def context_for_override_validation
41
97
  @context_for_override_validation ||= OverrideValidationContext.new
42
98
  end
43
99
 
100
+ # @private
44
101
  def override_validation_context = context_for_override_validation.context
45
102
 
103
+ # @private
46
104
  class OverrideValidationContext
47
105
  attr_reader :context
48
106
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordCompose
4
- VERSION = "1.0.1"
4
+ VERSION = "1.1.1"
5
5
  end
@@ -11,3 +11,5 @@ require_relative "active_record_compose/model"
11
11
  #
12
12
  module ActiveRecordCompose
13
13
  end
14
+
15
+ require "active_record_compose/railtie" if defined?(::Rails::Railtie)
@@ -4,16 +4,12 @@ module ActiveRecordCompose
4
4
  include ActiveModel::Attributes
5
5
  include Querying
6
6
 
7
+ def self.delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: bool) -> untyped
8
+ def self.delegated_attributes: () -> Array[Delegation]
9
+ def self.delegated_attributes=: (Array[Delegation]) -> untyped
7
10
  def delegated_attributes: () -> Array[Delegation]
8
11
 
9
- module ClassMethods : Module
10
- include ActiveModel::Attributes::ClassMethods
11
- include ActiveModel::AttributeMethods::ClassMethods
12
-
13
- def delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: bool) -> untyped
14
- def delegated_attributes: () -> Array[Delegation]
15
- def delegated_attributes=: (Array[Delegation]) -> untyped
16
- end
12
+ @attributes: untyped
17
13
 
18
14
  class AttributePredicate
19
15
  def initialize: (untyped value) -> void
@@ -83,11 +79,23 @@ module ActiveRecordCompose
83
79
  include PackagePrivate
84
80
  end
85
81
 
82
+ module Inspectable
83
+ extend ActiveSupport::Concern
84
+ include Attributes
85
+
86
+ def self.inspection_filter: () -> ActiveSupport::ParameterFilter
87
+ def self.filter_attributes: () -> Array[untyped]
88
+ def self.filter_attributes=: (Array[untyped]) -> void
89
+ def inspect: () -> String
90
+ def pretty_print: (untyped q) -> void
91
+
92
+ private
93
+ def format_for_inspect: (String name, untyped value) -> String
94
+ end
95
+
86
96
  class Model
87
97
  include Attributes
88
- extend Attributes::ClassMethods
89
98
  include TransactionSupport
90
- extend TransactionSupport::ClassMethods
91
99
  include Callbacks
92
100
 
93
101
  @__models: ComposedCollection
@@ -100,21 +108,28 @@ module ActiveRecordCompose
100
108
  module TransactionSupport
101
109
  extend ActiveSupport::Concern
102
110
  include ActiveRecord::Transactions
111
+ include ActiveSupport::Callbacks
112
+ extend ActiveSupport::Callbacks::ClassMethods
103
113
 
104
- module ClassMethods
105
- def connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
106
- def lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
107
- def with_connection: [T] () { () -> T } -> T
114
+ def self.before_commit: (*untyped) -> untyped
115
+ def self.after_commit: (*untyped) -> untyped
116
+ def self.after_rollback: (*untyped) -> untyped
108
117
 
109
- private
110
- def ar_class: -> singleton(ActiveRecord::Base)
111
- end
118
+ def save: (**untyped options) -> bool
119
+ def save!: (**untyped options) -> untyped
120
+ def _run_before_commit_callbacks: () -> untyped
121
+ def _run_commit_callbacks: () -> untyped
122
+ def _run_rollback_callbacks: () -> untyped
123
+
124
+ private
125
+ def default_ar_class: -> singleton(ActiveRecord::Base)
126
+ def connection_pool: (?ar_class: singleton(ActiveRecord::Base)) -> ActiveRecord::ConnectionAdapters::ConnectionPool
127
+ def with_pool_transaction_isolation_level: [T] (ActiveRecord::ConnectionAdapters::AbstractAdapter) { () -> T } -> T
112
128
  end
113
129
 
114
130
  module Persistence
115
131
  include Callbacks
116
132
  include TransactionSupport
117
- extend TransactionSupport::ClassMethods
118
133
 
119
134
  def save: (**untyped options) -> bool
120
135
  def save!: (**untyped options) -> untyped
@@ -128,6 +143,10 @@ module ActiveRecordCompose
128
143
  def raise_on_save_error_message: -> String
129
144
  end
130
145
 
146
+ class Railtie < Rails::Railtie
147
+ extend Rails::Initializable::ClassMethods
148
+ end
149
+
131
150
  module Validations : Model
132
151
  extend ActiveSupport::Concern
133
152
  extend ActiveModel::Validations::ClassMethods
@@ -72,9 +72,9 @@ module ActiveRecordCompose
72
72
  def self.after_rollback: (*callback[instance], ?if: condition[instance], ?unless: condition[instance], **untyped) ?{ () [self: instance] -> void } -> void
73
73
 
74
74
  def self.delegate_attribute: (*untyped methods, to: untyped, ?allow_nil: bool) -> untyped
75
- def self.connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
76
- def self.lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
77
- def self.with_connection: [T] () { () -> T } -> T
75
+
76
+ def self.filter_attributes: () -> Array[untyped]
77
+ def self.filter_attributes=: (Array[untyped]) -> untyped
78
78
 
79
79
  def initialize: (?Hash[attribute_name, untyped]) -> void
80
80
  def save: (**untyped options) -> bool
@@ -84,6 +84,9 @@ module ActiveRecordCompose
84
84
 
85
85
  def id: -> untyped
86
86
 
87
+ def inspect: () -> String
88
+ def pretty_print: (untyped q) -> void
89
+
87
90
  private
88
91
  def models: -> ComposedCollection
89
92
  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.0.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamajyotan
@@ -50,8 +50,10 @@ 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/inspectable.rb
53
54
  - lib/active_record_compose/model.rb
54
55
  - lib/active_record_compose/persistence.rb
56
+ - lib/active_record_compose/railtie.rb
55
57
  - lib/active_record_compose/transaction_support.rb
56
58
  - lib/active_record_compose/validations.rb
57
59
  - lib/active_record_compose/version.rb