active_record_compose 1.0.1 → 1.1.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: a8fa5fa4117b180d20233c6dc06ca8797c2c9931cca2708e5a01bd2b560c42d2
4
- data.tar.gz: cdb73d45e04a3195f9834390f9e9b6e8242e4a9b325a6ec684d3c7c2f97076ef
3
+ metadata.gz: 76bd8c66869f7b24c2938ecb72cf2c3b9f47cdcc34a8e1c87f57329f50880ecd
4
+ data.tar.gz: f6efc5ec40f5e870064dec9928e56a3ee8a3ca07ea00bd5dcbe86f0855d440ce
5
5
  SHA512:
6
- metadata.gz: 2317d9e39b054c728b0847c6cde3f556edfcfa57de9b1c6023f10cf264f5c498bcf654e252d7a29c0a071d349ade5f00943e512938a427b6d253bd43592ebace
7
- data.tar.gz: 71ad4690a7af527a1d323e3ccc84b25c0b7f875fcc3655883d0b8c2e35fe165b3b1f17a6201152e6d0281b21716976c77eec26db6c5712c0630d2741e6b72886
6
+ metadata.gz: c0361c3c95394e550e8571ea4880545dd8e566e9a6d4c829eb494db90226c009487361ca7eed8e3691d6dbf59674e7fc2fa782b2db006b79113c95c5b229f95c
7
+ data.tar.gz: 2a2ac6e1d86640c8d7df1448fc362e459790561a8223066351cadd7ea297e456bc093a015fb8a765e755deccbd5071e342fe035180a4c222ce81584305388085
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2025-11-19
4
+
5
+ * Implemented ActiveRecord-like #inspect
6
+ In activerecord's `#inspect`, the string is a list of attributes, and we have reproduced a similar format.
7
+ (https://github.com/hamajyotan/active_record_compose/pull/45)
8
+ * `.with_connection` `.lease_connection` and `.connection` are deprecated. Use `ActiveRecord::Base.with_connection` etc. instead.
9
+ (https://github.com/hamajyotan/active_record_compose/pull/46)
10
+ * refactor: Remove `ActiveRecord::Transactions` module dependency
11
+ (https://github.com/hamajyotan/active_record_compose/pull/44)
12
+
3
13
  ## [1.0.1] - 2025-10-17
4
14
 
5
15
  * Removed the private interface `composite_primary_key?`
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/parameter_filter"
4
+ require_relative "attributes"
5
+
6
+ module ActiveRecordCompose
7
+ # @private
8
+ #
9
+ # It provides #inspect behavior.
10
+ # It tries to replicate the inspect format provided by ActiveRecord as closely as possible.
11
+ #
12
+ # @example
13
+ # class Model < ActiveRecordCompose::Model
14
+ # def initialize(ar_model)
15
+ # @ar_model = ar_model
16
+ # super
17
+ # end
18
+ #
19
+ # attribute :foo, :date, default: -> { Date.today }
20
+ # delegate_attribute :bar, to: :ar_model
21
+ #
22
+ # private attr_reader :ar_model
23
+ # end
24
+ #
25
+ # m = Model.new(ar_model)
26
+ # m.inspect #=> #<Model:0x00007ff0fe75fe58 foo: "2025-11-14", bar: "bar">
27
+ #
28
+ # @example
29
+ # class Model < ActiveRecordCompose::Model
30
+ # self.filter_attributes += %i[foo]
31
+ #
32
+ # # ...
33
+ # end
34
+ #
35
+ # m = Model.new(ar_model)
36
+ # m.inspect #=> #<Model:0x00007ff0fe75fe58 foo: [FILTERED], bar: "bar">
37
+ #
38
+ module Inspectable
39
+ extend ActiveSupport::Concern
40
+ include ActiveRecordCompose::Attributes
41
+
42
+ included do
43
+ self.filter_attributes = []
44
+ end
45
+
46
+ module ClassMethods
47
+ def filter_attributes
48
+ if @filter_attributes.nil?
49
+ superclass.filter_attributes # steep:ignore
50
+ else
51
+ @filter_attributes
52
+ end
53
+ end
54
+
55
+ def filter_attributes=(value)
56
+ @inspection_filter = nil
57
+ @filter_attributes = value
58
+ end
59
+
60
+ # steep:ignore:start
61
+
62
+ def inspection_filter
63
+ if @filter_attributes.nil?
64
+ superclass.inspection_filter
65
+ else
66
+ @inspection_filter ||= ActiveSupport::ParameterFilter.new(filter_attributes, mask: FILTERED_MASK)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def inherited(subclass)
73
+ super
74
+
75
+ subclass.class_eval do
76
+ @inspection_filter = nil
77
+ @filter_attributes ||= nil
78
+ end
79
+ end
80
+
81
+ FILTERED_MASK =
82
+ Class.new(DelegateClass(::String)) do
83
+ def pretty_print(pp)
84
+ pp.text __getobj__
85
+ end
86
+ end.new(ActiveSupport::ParameterFilter::FILTERED).freeze
87
+ private_constant :FILTERED_MASK
88
+
89
+ # steep:ignore:end
90
+ end
91
+
92
+ # Returns a formatted string representation of the record's attributes.
93
+ #
94
+ def inspect
95
+ inspection =
96
+ if @attributes
97
+ attributes.map { |k, v| "#{k}: #{format_for_inspect(k, v)}" }.join(", ")
98
+ else
99
+ "not initialized"
100
+ end
101
+
102
+ "#<#{self.class} #{inspection}>"
103
+ end
104
+
105
+ # It takes a PP and pretty prints that record.
106
+ #
107
+ def pretty_print(pp)
108
+ pp.object_address_group(self) do
109
+ if @attributes
110
+ attrs = attributes
111
+ pp.seplist(attrs.keys, proc { pp.text "," }) do |attr|
112
+ pp.breakable " "
113
+ pp.group(1) do
114
+ pp.text attr
115
+ pp.text ":"
116
+ pp.breakable
117
+ pp.text format_for_inspect(attr, attrs[attr])
118
+ end
119
+ end
120
+ else
121
+ pp.breakable " "
122
+ pp.text "not initialized"
123
+ end
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def format_for_inspect(name, value)
130
+ return value.inspect if value.nil?
131
+
132
+ inspected_value =
133
+ if value.is_a?(String) && value.length > 50
134
+ "#{value[0, 50]}...".inspect
135
+ elsif value.is_a?(Date) || value.is_a?(Time)
136
+ %("#{value.to_fs(:inspect)}")
137
+ else
138
+ value.inspect
139
+ end
140
+
141
+ self.class.inspection_filter.filter_param(name, inspected_value)
142
+ end
143
+ end
144
+ 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,6 +87,7 @@ module ActiveRecordCompose
86
87
  include ActiveRecordCompose::Persistence
87
88
  include ActiveRecordCompose::Validations
88
89
  include ActiveRecordCompose::TransactionSupport
90
+ include ActiveRecordCompose::Inspectable
89
91
 
90
92
  begin
91
93
  # @group Model Core
@@ -282,7 +284,7 @@ module ActiveRecordCompose
282
284
  # The need for such a value indicates that operations from multiple contexts are being processed.
283
285
  # If the contexts differ, we recommend separating them into different model definitions.
284
286
  #
285
- # @params [Hash] Optional parameters.
287
+ # @param options [Hash] parameters.
286
288
  # @option options [Boolean] :validate Whether to run validations.
287
289
  # This option is intended for internal use only.
288
290
  # Users should avoid explicitly passing <tt>validate: false</tt>,
@@ -351,9 +353,50 @@ module ActiveRecordCompose
351
353
  # Registers a block to be called after the transaction is rolled back.
352
354
 
353
355
  # @endgroup
354
- end
355
356
 
356
- # @group Model Core
357
+ # @!method inspect
358
+ # Returns a formatted string representation of the record's attributes.
359
+ # It tries to replicate the inspect format provided by ActiveRecord as closely as possible.
360
+ #
361
+ # @example
362
+ # class Model < ActiveRecordCompose::Model
363
+ # def initialize(ar_model)
364
+ # @ar_model = ar_model
365
+ # super
366
+ # end
367
+ #
368
+ # attribute :foo, :date, default: -> { Date.today }
369
+ # delegate_attribute :bar, to: :ar_model
370
+ #
371
+ # private attr_reader :ar_model
372
+ # end
373
+ #
374
+ # m = Model.new(ar_model)
375
+ # m.inspect #=> #<Model:0x00007ff0fe75fe58 foo: "2025-11-14", bar: "bar">
376
+ #
377
+ # @example use {.filter_attributes}
378
+ # class Model < ActiveRecordCompose::Model
379
+ # self.filter_attributes += %i[foo]
380
+ #
381
+ # # ...
382
+ # end
383
+ #
384
+ # m = Model.new(ar_model)
385
+ # m.inspect #=> #<Model:0x00007ff0fe75fe58 foo: [FILTERED], bar: "bar">
386
+ #
387
+ # @return [String] formatted string representation of the record's attributes.
388
+ # @see .filter_attributes
389
+
390
+ # @!method self.filter_attributes
391
+ # Returns columns not to expose when invoking {#inspect}.
392
+ # @return [Array<Symbol>]
393
+ # @see #inspect
394
+
395
+ # @!method self.filter_attributes=(value)
396
+ # Specify columns not to expose when invoking {#inspect}.
397
+ # @param [Array<Symbol>] value
398
+ # @see #inspect
399
+ end
357
400
 
358
401
  def initialize(attributes = {})
359
402
  super
@@ -382,6 +425,8 @@ module ActiveRecordCompose
382
425
  #
383
426
  def id = nil
384
427
 
428
+ # @group Model Core
429
+
385
430
  private
386
431
 
387
432
  # Returns a collection of model elements to encapsulate.
@@ -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,130 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/module"
4
+
3
5
  module ActiveRecordCompose
4
6
  # @private
5
7
  module TransactionSupport
6
8
  extend ActiveSupport::Concern
7
- include ActiveRecord::Transactions
8
9
 
9
10
  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
11
+ define_callbacks :commit, :rollback, :before_commit, scope: [ :kind, :name ]
15
12
  end
16
13
 
17
14
  module ClassMethods
18
- delegate :with_connection, :lease_connection, to: :ar_class
15
+ # steep:ignore:start
16
+
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
+ # @deprecated
24
+ def lease_connection(...)
25
+ ActiveRecord.deprecator.warn("`lease_connection` is deprecated. Use `ActiveRecord::Base.lease_connection` instead.")
26
+ ActiveRecord::Base.lease_connection(...)
27
+ end
28
+
29
+ # @deprecated
30
+ def connection(...)
31
+ ActiveRecord.deprecator.warn("`connection` is deprecated. Use `ActiveRecord::Base.connection` instead.")
32
+ ActiveRecord::Base.connection(...)
33
+ end
34
+
35
+ # steep:ignore:end
36
+
37
+ def before_commit(*args, &block)
38
+ set_options_for_callbacks!(args)
39
+ set_callback(:before_commit, :before, *args, &block) # steep:ignore
40
+ end
41
+
42
+ def after_commit(*args, &block)
43
+ set_options_for_callbacks!(args, prepend_option)
44
+ set_callback(:commit, :after, *args, &block) # steep:ignore
45
+ end
19
46
 
20
- # In ActiveRecord, it is soft deprecated.
21
- delegate :connection, to: :ar_class
47
+ def after_rollback(*args, &block)
48
+ set_options_for_callbacks!(args, prepend_option)
49
+ set_callback(:rollback, :after, *args, &block) # steep:ignore
50
+ end
22
51
 
23
52
  private
24
53
 
25
- def ar_class = ActiveRecord::Base
54
+ def prepend_option
55
+ if ActiveRecord.run_after_transaction_callbacks_in_order_defined # steep:ignore
56
+ { prepend: true }
57
+ else
58
+ {}
59
+ end
60
+ end
61
+
62
+ def set_options_for_callbacks!(args, enforced_options = {})
63
+ options = args.extract_options!.merge!(enforced_options)
64
+ args << options
65
+ end
26
66
  end
27
67
 
28
- def trigger_transactional_callbacks? = true
29
- def restore_transaction_record_state(_force_restore_state = false) = nil
68
+ def save(**options) = with_transaction_returning_status { super }
69
+
70
+ def save!(**options) = with_transaction_returning_status { super }
71
+
72
+ concerning :SupportForActiveRecordConnectionAdaptersTransaction do
73
+ def trigger_transactional_callbacks? = true
74
+
75
+ def before_committed!
76
+ _run_before_commit_callbacks
77
+ end
78
+
79
+ def committed!(should_run_callbacks: true)
80
+ _run_commit_callbacks if should_run_callbacks
81
+ end
82
+
83
+ def rolledback!(force_restore_state: false, should_run_callbacks: true)
84
+ _run_rollback_callbacks if should_run_callbacks
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def with_transaction_returning_status
91
+ connection_pool.with_connection do |connection|
92
+ with_pool_transaction_isolation_level(connection) do
93
+ ensure_finalize = !connection.transaction_open?
94
+
95
+ connection.transaction do
96
+ connection.add_transaction_record(self, ensure_finalize || has_transactional_callbacks?) # steep:ignore
97
+
98
+ yield.tap { raise ActiveRecord::Rollback unless _1 }
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ def default_ar_class = ActiveRecord::Base
105
+
106
+ def connection_pool(ar_class: default_ar_class)
107
+ connection_specification_name = ar_class.connection_specification_name
108
+ role = ar_class.current_role
109
+ shard = ar_class.current_shard # steep:ignore
110
+ connection_handler = ar_class.connection_handler # steep:ignore
111
+ retrieve_options = { role:, shard: }
112
+ retrieve_options[:strict] = true if ActiveRecord.gem_version.release >= Gem::Version.new("7.2.0")
113
+
114
+ connection_handler.retrieve_connection_pool(connection_specification_name, **retrieve_options)
115
+ end
116
+
117
+ def with_pool_transaction_isolation_level(connection, &block)
118
+ if ActiveRecord.gem_version.release >= Gem::Version.new("8.1.0")
119
+ isolation_level = ActiveRecord.default_transaction_isolation_level # steep:ignore
120
+ connection.pool.with_pool_transaction_isolation_level(isolation_level, connection.transaction_open?, &block)
121
+ else
122
+ block.call
123
+ end
124
+ end
125
+
126
+ def has_transactional_callbacks?
127
+ _rollback_callbacks.present? || _commit_callbacks.present? || _before_commit_callbacks.present? # steep:ignore
128
+ end
30
129
  end
31
130
  end
@@ -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.0"
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)
@@ -6,6 +6,8 @@ module ActiveRecordCompose
6
6
 
7
7
  def delegated_attributes: () -> Array[Delegation]
8
8
 
9
+ @attributes: untyped
10
+
9
11
  module ClassMethods : Module
10
12
  include ActiveModel::Attributes::ClassMethods
11
13
  include ActiveModel::AttributeMethods::ClassMethods
@@ -83,6 +85,29 @@ module ActiveRecordCompose
83
85
  include PackagePrivate
84
86
  end
85
87
 
88
+ module Inspectable
89
+ extend ActiveSupport::Concern
90
+ include Attributes
91
+ extend Inspectable::ClassMethods
92
+
93
+ def inspect: () -> String
94
+ def pretty_print: (untyped q) -> void
95
+
96
+ private
97
+ def format_for_inspect: (String name, untyped value) -> String
98
+
99
+ module ClassMethods
100
+ FILTERED_MASK: String
101
+
102
+ def inspection_filter: () -> ActiveSupport::ParameterFilter
103
+ def filter_attributes: () -> Array[untyped]
104
+ def filter_attributes=: (Array[untyped]) -> void
105
+
106
+ @inspection_filter: ActiveSupport::ParameterFilter?
107
+ @filter_attributes: untyped
108
+ end
109
+ end
110
+
86
111
  class Model
87
112
  include Attributes
88
113
  extend Attributes::ClassMethods
@@ -100,15 +125,30 @@ module ActiveRecordCompose
100
125
  module TransactionSupport
101
126
  extend ActiveSupport::Concern
102
127
  include ActiveRecord::Transactions
128
+ extend TransactionSupport::ClassMethods
103
129
 
104
130
  module ClassMethods
105
- def connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
106
- def lease_connection: -> ActiveRecord::ConnectionAdapters::AbstractAdapter
107
- def with_connection: [T] () { () -> T } -> T
131
+ include ActiveSupport::Callbacks::ClassMethods
132
+
133
+ def before_commit: (*untyped) -> untyped
134
+ def after_commit: (*untyped) -> untyped
135
+ def after_rollback: (*untyped) -> untyped
108
136
 
109
137
  private
110
- def ar_class: -> singleton(ActiveRecord::Base)
138
+ def prepend_option: -> ({ prepend: true } | {})
139
+ def set_options_for_callbacks!: (untyped, ?({ prepend: true } | {})) -> untyped
111
140
  end
141
+
142
+ def save: (**untyped options) -> bool
143
+ def save!: (**untyped options) -> untyped
144
+ def _run_before_commit_callbacks: () -> untyped
145
+ def _run_commit_callbacks: () -> untyped
146
+ def _run_rollback_callbacks: () -> untyped
147
+
148
+ private
149
+ def default_ar_class: -> singleton(ActiveRecord::Base)
150
+ def connection_pool: (?ar_class: singleton(ActiveRecord::Base)) -> ActiveRecord::ConnectionAdapters::ConnectionPool
151
+ def with_pool_transaction_isolation_level: [T] (ActiveRecord::ConnectionAdapters::AbstractAdapter) { () -> T } -> T
112
152
  end
113
153
 
114
154
  module Persistence
@@ -128,6 +168,10 @@ module ActiveRecordCompose
128
168
  def raise_on_save_error_message: -> String
129
169
  end
130
170
 
171
+ class Railtie < Rails::Railtie
172
+ extend Rails::Initializable::ClassMethods
173
+ end
174
+
131
175
  module Validations : Model
132
176
  extend ActiveSupport::Concern
133
177
  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.0
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