active_record_compose 0.11.2 → 0.12.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: 2e0428468971c6a369fd86cfae9b5413acddf18a252a1f502498e11a55e23b92
4
- data.tar.gz: fea23f0f94a09d84b462e27af4feb01c1e40f0c8ff4ba306a9dd391f89eeb9f7
3
+ metadata.gz: 954ce34124a1c8e83a45710e104dd965c407afb5ba296b74d4a4e5f9d13d971a
4
+ data.tar.gz: c1bb35ef5458a804f243b886c037aff3794822477ea2aaf5a57bfb12a4001477
5
5
  SHA512:
6
- metadata.gz: f293d9167fe2a1f879df2f3936e784a2f6f33b1df20160606a5ef65a23f2e4a81040ada14ed7d42aa1ed289899db278880945f75d8a792f369a02f5301e31a5f
7
- data.tar.gz: b7db34b71359f1f7d90bb6bbb25b5139595742b202c29b8f009e506dd2244a485f80151fd74edf5245aca224ef6a58bf9c507ade5b95bc69b84963bf3203f9a9
6
+ metadata.gz: 2e7f81f304cec10420cb4f405ad8ad7f93f3811c7f43bf9952c05dbca84e651bbf0c851e648349d7e93986cf67cb177110785588a47c8951718f4d271cf53944
7
+ data.tar.gz: 8ac490bd69c0d51e6e89f1f198a4e052eccbeedcf1b234edfa8c9635819fe336bbe10b6b64b6b55907524543ad34eb9343892b00f54f2737a65c45c120456afa
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --private
2
+ --markup markdown
3
+ --markup-provider redcarpet
4
+ --default-return void
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.12.0] - 2025-08-21
4
+
5
+ - Omits default arguments for `#update` and `#update!`. It's to align I/F with ActiveRecord.
6
+ (https://github.com/hamajyotan/active_record_compose/pull/25)
7
+ - `#update(attributes = {})` to `#update(attributes)`
8
+ - `#update!(attributes = {})` to `#update!(attributes)`
9
+ - Omitted Specify instance variables in the `:to` option of `delegate_attribute`.
10
+ (https://github.com/hamajyotan/active_record_compose/pull/29)
11
+ - Omitted `#destroy` and `#touch` from `ActiveRecordCompose::Model`.
12
+ These were unintentionally provided by the `ActiveRecord::Transactions` module. The but in fact did not work correctly.
13
+ (https://github.com/hamajyotan/active_record_compose/pull/27)
14
+
15
+ ## [0.11.3] - 2025-07-13
16
+
17
+ - refactor: Aggregation attribute module.
18
+ (https://github.com/hamajyotan/active_record_compose/pull/24)
19
+ - Warn against specifying instance variables, etc. directly in the `:to` option of `delegate_attribute`.
20
+ - Deprecated:
21
+ ```ruby
22
+ delegate_attribute :foo, to: :@model
23
+ ```
24
+ - Recommended:
25
+ ```ruby
26
+ delegate_attribute :foo, to: :model
27
+ private
28
+ attr_reader :model
29
+ ```
30
+ - doc: Expansion of yard documentation comments.
31
+
3
32
  ## [0.11.2] - 2025-06-29
4
33
 
5
34
  - `ActiveModel::Attributes.attribute_names` now takes into account attributes declared in `.delegate_attribute`
data/README.md CHANGED
@@ -381,20 +381,20 @@ end
381
381
  ```ruby
382
382
  r = Registration.new(name: 'foo', email: 'example@example.com', accept: false)
383
383
  r.valid?
384
- => true
384
+ #=> true
385
385
 
386
386
  r.valid?(:education)
387
- => false
387
+ #=> false
388
388
  r.errors.map { [_1.attribute, _1.type] }
389
- => [[:email, :invalid], [:accept, :blank]]
389
+ #=> [[:email, :invalid], [:accept, :blank]]
390
390
 
391
391
  r.email = 'example@example.edu'
392
392
  r.accept = true
393
393
 
394
394
  r.valid?(:education)
395
- => true
395
+ #=> true
396
396
  r.save(context: :education)
397
- => true
397
+ #=> true
398
398
  ```
399
399
 
400
400
  ## Sample application as an example
@@ -405,6 +405,7 @@ With Github Codespaces, it can also be run directly in the browser. Naturally, a
405
405
 
406
406
  ## Links
407
407
 
408
+ - [Document from YARD](https://hamajyotan.github.io/active_record_compose/)
408
409
  - [Smart way to update multiple models simultaneously in Rails](https://dev.to/hamajyotan/smart-way-to-update-multiple-models-simultaneously-in-rails-51b6)
409
410
 
410
411
  ## Development
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordCompose
4
+ module Attributes
5
+ # @private
6
+ class AttributePredicate
7
+ def initialize(value)
8
+ @value = value
9
+ end
10
+
11
+ def call
12
+ case value
13
+ when true then true
14
+ when false, nil then false
15
+ else
16
+ if value.respond_to?(:zero?)
17
+ !value.zero?
18
+ else
19
+ value.present?
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :value
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute_predicate"
4
+
5
+ module ActiveRecordCompose
6
+ module Attributes
7
+ # @private
8
+ class Delegation
9
+ # @return [Symbol] The attribute name as symbol
10
+ attr_reader :attribute
11
+
12
+ def initialize(attribute:, to:, allow_nil: false)
13
+ @attribute = attribute.to_sym
14
+ @to = to.to_sym
15
+ @allow_nil = !!allow_nil
16
+
17
+ freeze
18
+ end
19
+
20
+ def define_delegated_attribute(klass)
21
+ klass.delegate(reader, writer, to:, allow_nil:)
22
+ klass.module_eval <<~RUBY, __FILE__, __LINE__ + 1
23
+ def #{reader}?
24
+ ActiveRecordCompose::Attributes::AttributePredicate.new(#{reader}).call
25
+ end
26
+ RUBY
27
+ end
28
+
29
+ # @return [String] The attribute name as string
30
+ def attribute_name = attribute.to_s
31
+
32
+ # @return [Hash<String, Object>]
33
+ def attribute_hash(model)
34
+ { attribute_name => model.public_send(attribute) }
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :to, :allow_nil
40
+
41
+ def reader = attribute.to_s
42
+
43
+ def writer = "#{attribute}="
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute_predicate"
4
+
5
+ module ActiveRecordCompose
6
+ module Attributes
7
+ # @private
8
+ # This provides predicate methods based on the attributes.
9
+ #
10
+ # @example
11
+ # class AccountRegistration < ActiveRecordCompose::Model
12
+ # def initialize
13
+ # @account = Account.new
14
+ # super()
15
+ # models << account
16
+ # end
17
+ #
18
+ # attribute :original_attr
19
+ # delegate_attribute :name, :email, to: :account
20
+ #
21
+ # private
22
+ #
23
+ # attr_reader :account
24
+ # end
25
+ #
26
+ # model = AccountRegistration.new
27
+ #
28
+ # model.name #=> nil
29
+ # model.name? #=> false
30
+ # model.name = "Alice"
31
+ # model.name? #=> true
32
+ #
33
+ # model.original_attr = "Bob"
34
+ # model.original_attr? #=> true
35
+ # model.original_attr = ""
36
+ # model.original_attr? #=> false
37
+ #
38
+ # # If the value is numeric, it returns the result of checking whether it is zero or not.
39
+ # # This behavior is consistent with `ActiveRecord::AttributeMethods::Query`.
40
+ # model.original_attr = 123
41
+ # model.original_attr? #=> true
42
+ # model.original_attr = 0
43
+ # model.original_attr? #=> false
44
+ #
45
+ module Querying
46
+ extend ActiveSupport::Concern
47
+ include ActiveModel::AttributeMethods
48
+
49
+ included do
50
+ attribute_method_suffix "?", parameters: false
51
+ end
52
+
53
+ private
54
+
55
+ def attribute?(attr_name) = query?(public_send(attr_name))
56
+
57
+ def query?(value)
58
+ ActiveRecordCompose::Attributes::AttributePredicate.new(value).call
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attributes/attribute_predicate"
4
+ require_relative "attributes/delegation"
5
+ require_relative "attributes/querying"
6
+
7
+ module ActiveRecordCompose
8
+ # @private
9
+ #
10
+ # Provides attribute-related functionality for use within ActiveRecordCompose::Model.
11
+ #
12
+ # This module allows you to define attributes on your composed model, including support
13
+ # for query methods (e.g., `#attribute?`) and delegation of attributes to underlying
14
+ # ActiveRecord instances via macros.
15
+ #
16
+ # For example, `.delegate_attribute` defines attribute accessors that delegate to
17
+ # a specific model, similar to:
18
+ #
19
+ # delegate :name, :name=, to: :account
20
+ #
21
+ # Additionally, delegated attributes are included in the composed model's `#attributes`
22
+ # hash.
23
+ #
24
+ # @example
25
+ # class AccountRegistration < ActiveRecordCompose::Model
26
+ # def initialize(account, attributes = {})
27
+ # @account = account
28
+ # super(attributes)
29
+ # models.push(account)
30
+ # end
31
+ #
32
+ # attribute :original_attribute, :string, default: "qux"
33
+ # delegate_attribute :name, to: :account
34
+ #
35
+ # private
36
+ #
37
+ # attr_reader :account
38
+ # end
39
+ #
40
+ # account = Account.new
41
+ # account.name = "foo"
42
+ #
43
+ # registration = AccountRegistration.new(account)
44
+ # registration.name # => "foo" (delegated)
45
+ # registration.name? # => true (delegated attribute method + `?`)
46
+ #
47
+ # registration.name = "bar" # => updates account.name
48
+ # account.name # => "bar"
49
+ # account.name? # => true
50
+ #
51
+ # registration.attributes
52
+ # # => { "original_attribute" => "qux", "name" => "bar" }
53
+ #
54
+ module Attributes
55
+ extend ActiveSupport::Concern
56
+ include ActiveModel::Attributes
57
+
58
+ included do
59
+ include Querying
60
+
61
+ # @type self: Class
62
+ class_attribute :delegated_attributes, instance_writer: false
63
+ end
64
+
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
94
+ #
95
+ # registration.attributes
96
+ # # => { "original_attribute" => "qux", "name" => "bar" }
97
+ #
98
+ def delegate_attribute(*attributes, to:, allow_nil: false)
99
+ if to.start_with?("@")
100
+ raise ArgumentError, "Instance variables cannot be specified in delegate to. (#{to})"
101
+ end
102
+
103
+ delegations = attributes.map { Delegation.new(attribute: _1, to:, allow_nil:) }
104
+ delegations.each { _1.define_delegated_attribute(self) }
105
+
106
+ self.delegated_attributes = (delegated_attributes.to_a + delegations).reverse.uniq { _1.attribute }.reverse
107
+ end
108
+
109
+ # Returns a array of attribute name.
110
+ # Attributes declared with `delegate_attribute` are also merged.
111
+ #
112
+ # @return [Array<String>] array of attribute name.
113
+ def attribute_names = super + delegated_attributes.to_a.map { _1.attribute_name }
114
+ end
115
+
116
+ # Returns a array of attribute name.
117
+ # Attributes declared with `delegate_attribute` are also merged.
118
+ #
119
+ # @return [Array<String>] array of attribute name.
120
+ def attribute_names = super + delegated_attributes.to_a.map { _1.attribute_name }
121
+
122
+ # Returns a hash with the attribute name as key and the attribute value as value.
123
+ # Attributes declared with `delegate_attribute` are also merged.
124
+ #
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
133
+ #
134
+ # attribute :original_attribute, :string, default: "qux"
135
+ # delegate_attribute :name, to: :account
136
+ #
137
+ # private
138
+ #
139
+ # attr_reader :account
140
+ # end
141
+ #
142
+ # account = Account.new
143
+ # account.name = "foo"
144
+ #
145
+ # registration = AccountRegistration.new(account)
146
+ #
147
+ # registration.attributes # => { "original_attribute" => "qux", "name" => "bar" }
148
+ #
149
+ def attributes
150
+ super.merge(*delegated_attributes.to_a.map { _1.attribute_hash(self) })
151
+ end
152
+ end
153
+ end
@@ -1,6 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordCompose
4
+ # @private
5
+ #
6
+ # Provides hooks into the life cycle of an ActiveRecordCompose model,
7
+ # allowing you to insert custom logic before or after changes to the object's state.
8
+ #
9
+ # The callback flow generally follows the same structure as Active Record:
10
+ #
11
+ # * `before_validation`
12
+ # * `after_validation`
13
+ # * `before_save`
14
+ # * `before_create` (or `before_update` for update operations)
15
+ # * `after_create` (or `after_update` for update operations)
16
+ # * `after_save`
17
+ # * `after_commit` (or `after_rollback` when the transaction is rolled back)
18
+ #
4
19
  module Callbacks
5
20
  extend ActiveSupport::Concern
6
21
  include ActiveModel::Validations::Callbacks
@@ -13,8 +28,15 @@ module ActiveRecordCompose
13
28
 
14
29
  private
15
30
 
31
+ # Evaluate while firing callbacks such as `before_save` `after_save`
32
+ # before and after block evaluation.
33
+ #
16
34
  def with_callbacks(&block) = run_callbacks(:save) { run_callbacks(callback_context, &block) }
17
35
 
36
+ # Returns the symbol representing the callback context, which is `:create` if the record
37
+ # is new, or `:update` if it has been persisted.
38
+ #
39
+ # @return [:create, :update] either `:create` if not persisted, or `:update` if persisted
18
40
  def callback_context = persisted? ? :update : :create
19
41
  end
20
42
  end
@@ -5,6 +5,9 @@ require_relative "wrapped_model"
5
5
  module ActiveRecordCompose
6
6
  using WrappedModel::PackagePrivate
7
7
 
8
+ # Object obtained by {ActiveRecordCompose::Model#models}.
9
+ #
10
+ # It functions as a collection that contains the object to be saved.
8
11
  class ComposedCollection
9
12
  include Enumerable
10
13
 
@@ -15,7 +18,7 @@ module ActiveRecordCompose
15
18
 
16
19
  # Enumerates model objects.
17
20
  #
18
- # @yieldparam [Object] the model instance
21
+ # @yieldparam [Object] model model instance
19
22
  # @return [Enumerator] when not block given.
20
23
  # @return [self] when block given, returns itself.
21
24
  def each
@@ -27,7 +30,7 @@ module ActiveRecordCompose
27
30
 
28
31
  # Appends model to collection.
29
32
  #
30
- # @param model [Object] the model instance
33
+ # @param model [Object] model instance
31
34
  # @return [self] returns itself.
32
35
  def <<(model)
33
36
  models << wrap(model, destroy: false)
@@ -36,12 +39,14 @@ module ActiveRecordCompose
36
39
 
37
40
  # Appends model to collection.
38
41
  #
39
- # @param model [Object] the model instance
40
- # @param destroy [Boolean] given true, destroy model.
41
- # @param destroy [Proc] when proc returning true, destroy model.
42
- # @param destroy [Symbol] applies boolean value of result of sending a message to `owner` to evaluation.
43
- # @param if [Proc] evaluation result is false, it will not be included in the renewal.
44
- # @param if [Symbol] applies boolean value of result of sending a message to `owner` to evaluation.
42
+ # @param model [Object] model instance
43
+ # @param destroy [Boolean, Proc, Symbol] Controls whether the model should be destroyed.
44
+ # - Boolean: if `true`, the model will be destroyed.
45
+ # - Proc: the model will be destroyed if the proc returns `true`.
46
+ # - Symbol: sends the symbol as a method to `owner`; if the result is truthy, the model will be destroyed.
47
+ # @param if [Proc, Symbol] Controls conditional inclusion in renewal.
48
+ # - Proc: the proc is called, and if it returns `false`, the model is excluded.
49
+ # - Symbol: sends the symbol as a method to `owner`; if the result is falsy, the model is excluded.
45
50
  # @return [self] returns itself.
46
51
  def push(model, destroy: false, if: nil)
47
52
  models << wrap(model, destroy:, if:)
@@ -64,7 +69,7 @@ module ActiveRecordCompose
64
69
  # Removes the specified model from the collection.
65
70
  # Returns nil if the deletion fails, self if it succeeds.
66
71
  #
67
- # @param model [Object] the model instance
72
+ # @param model [Object] model instance
68
73
  # @return [self] Successful deletion
69
74
  # @return [nil] If deletion fails
70
75
  def delete(model)
@@ -76,8 +81,10 @@ module ActiveRecordCompose
76
81
 
77
82
  private
78
83
 
84
+ # @private
79
85
  attr_reader :owner, :models
80
86
 
87
+ # @private
81
88
  def wrap(model, destroy: false, if: nil)
82
89
  if destroy.is_a?(Symbol)
83
90
  method = destroy