factory_bot-with 0.4.0 → 0.5.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 +7 -1
- data/README.md +65 -17
- data/lib/factory_bot/with/version.rb +1 -1
- data/lib/factory_bot/with.rb +37 -17
- metadata +2 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: fa2396f8ce3d44a17caacc293e2b5914b781eb167c50de541c2950b6f3e8ce0f
         | 
| 4 | 
            +
              data.tar.gz: a5b0e4b93ba6f97439dd25290c2ab5c3832175799e7e45f538e0c9db28be14bf
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: a655f59ca867e26316fb08d3fd80e9352d4cefeb600c9d324de89358096a031e549acc5d98da43d67f608f615084bfc384a4c8d45f9e528891d4f90f9fe9de5f
         | 
| 7 | 
            +
              data.tar.gz: 4709882eea9a1a90e6719a44133d66e1de9c189da2c602963f77b9c80b14952b99dbd2b350085d496f15cf70f20bbbce4edc1a5a37b2dcf9272d244be31d7589
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,6 +1,12 @@ | |
| 1 1 | 
             
            ## [Unreleased]
         | 
| 2 2 |  | 
| 3 | 
            -
            ## [0. | 
| 3 | 
            +
            ## [0.5.0] - 2025-03-20
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            - Added: When using `with` scope syntax, blocks can now take given objects as arguments
         | 
| 6 | 
            +
            - Added: `with_list` also works as a scope syntax, but calls a block for each product of objects
         | 
| 7 | 
            +
            - **Changed**: Passing blocks to factory methods behaves same as `with` scope syntax
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ## [0.4.0] - 2025-03-15
         | 
| 4 10 |  | 
| 5 11 | 
             
            - Fixed: Improved error message for incorrect factory usage
         | 
| 6 12 | 
             
            - Added: Added `with` scope syntax for automatic association resolution
         | 
    
        data/README.md
    CHANGED
    
    | @@ -62,7 +62,7 @@ FactoryBot::With overrides the behavior when factory methods are called without | |
| 62 62 |  | 
| 63 63 | 
             
            ```ruby
         | 
| 64 64 | 
             
            create(:foo, ...)  # behaves in the same way as FactoryBot.create
         | 
| 65 | 
            -
            create             # returns a Proxy (an  | 
| 65 | 
            +
            create             # returns a Proxy (an intermediate) object
         | 
| 66 66 | 
             
            create.foo(...)    # is equivalent to create(:foo, ...)
         | 
| 67 67 | 
             
            ```
         | 
| 68 68 |  | 
| @@ -147,6 +147,70 @@ create.blog(with.article) # autocomplete to :blog_article | |
| 147 147 |  | 
| 148 148 | 
             
            ## Additional features
         | 
| 149 149 |  | 
| 150 | 
            +
            ### `with` scope syntax for automatic association resolution
         | 
| 151 | 
            +
             | 
| 152 | 
            +
            By calling `with` without positional arguments, but with keyword arguments that define the relationship between factory names and objects, along with a block, it creates a scope where those objects become candidates for automatic association resolution.
         | 
| 153 | 
            +
             | 
| 154 | 
            +
            ```ruby
         | 
| 155 | 
            +
            let(:blog) { create.blog }
         | 
| 156 | 
            +
             | 
| 157 | 
            +
            before do
         | 
| 158 | 
            +
              with(blog:) do
         | 
| 159 | 
            +
                # Just like when using `create.blog(with.article)`,
         | 
| 160 | 
            +
                # `blog:` is completed automatically at each `create.article`
         | 
| 161 | 
            +
                create.article(with.comment)
         | 
| 162 | 
            +
                create.article(with_list.comment(3))
         | 
| 163 | 
            +
              end
         | 
| 164 | 
            +
            end
         | 
| 165 | 
            +
            ```
         | 
| 166 | 
            +
             | 
| 167 | 
            +
            The same behavior occurs when a block is passed to factory methods.
         | 
| 168 | 
            +
             | 
| 169 | 
            +
            ```ruby
         | 
| 170 | 
            +
            create.blog do
         | 
| 171 | 
            +
              create.article(with.comment)
         | 
| 172 | 
            +
              create.article(with_list.comment(3))
         | 
| 173 | 
            +
            end
         | 
| 174 | 
            +
            ```
         | 
| 175 | 
            +
             | 
| 176 | 
            +
            <details>
         | 
| 177 | 
            +
            <summary>Comparison</summary>
         | 
| 178 | 
            +
             | 
| 179 | 
            +
            `_pair` and `_list` methods have [an incompatible behavior](./lib/factory_bot/with.rb#L121) with FactoryBot. If you want to avoid this, just use `Object#tap`.
         | 
| 180 | 
            +
             | 
| 181 | 
            +
            ```ruby
         | 
| 182 | 
            +
            # This creates a blog with 2 articles, each with 1 comment
         | 
| 183 | 
            +
            # plain factory_bot:
         | 
| 184 | 
            +
            create(:blog) do |blog|
         | 
| 185 | 
            +
              create_list(:article, 2, blog:) do |articles| # yielded once with an array of articles in plain factory_bot
         | 
| 186 | 
            +
                articles.each { |article| create(:comment, article:) }
         | 
| 187 | 
            +
              end
         | 
| 188 | 
            +
            end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
            # `with` operator:
         | 
| 191 | 
            +
            create.blog(
         | 
| 192 | 
            +
              with_list.article(2, with.comment)
         | 
| 193 | 
            +
            )
         | 
| 194 | 
            +
             | 
| 195 | 
            +
            # factory methods with blocks:
         | 
| 196 | 
            +
            create.blog do
         | 
| 197 | 
            +
              create_list.article(2) { create.comment } # yielded *for each article* in factory_bot-with
         | 
| 198 | 
            +
            end
         | 
| 199 | 
            +
             | 
| 200 | 
            +
            # with as a scope syntax for existing blog:
         | 
| 201 | 
            +
            blog = create.blog
         | 
| 202 | 
            +
            with(blog:) do
         | 
| 203 | 
            +
              create_list.article(2) { create.comment }
         | 
| 204 | 
            +
            end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
            # with_list can also be used as a scope syntax:
         | 
| 207 | 
            +
            blog = create.blog
         | 
| 208 | 
            +
            articles = create_list.article(2, blog:)
         | 
| 209 | 
            +
            with_list(article: articles) { create.comment } # yielded *for each article*
         | 
| 210 | 
            +
            ```
         | 
| 211 | 
            +
             | 
| 212 | 
            +
            </details>
         | 
| 213 | 
            +
             | 
| 150 214 | 
             
            ### `with` as a template
         | 
| 151 215 |  | 
| 152 216 | 
             
            `with` can also be used stand-alone. Stand-alone `with` can be used in place of the factory name. It works as a template for factory method calls.
         | 
| @@ -180,22 +244,6 @@ context "when published more than one year ago" do | |
| 180 244 | 
             
            end
         | 
| 181 245 | 
             
            ```
         | 
| 182 246 |  | 
| 183 | 
            -
            ### `with` scope for automatic association resolution
         | 
| 184 | 
            -
             | 
| 185 | 
            -
            By calling `with` without positional arguments, but with keyword arguments that define the relationship between factory names and objects, along with a block, `factory_bot-with` creates a scope where those objects become candidates for automatic association resolution.
         | 
| 186 | 
            -
             | 
| 187 | 
            -
            ```ruby
         | 
| 188 | 
            -
            let(:blog) { create.blog }
         | 
| 189 | 
            -
             | 
| 190 | 
            -
            before do
         | 
| 191 | 
            -
              with(blog:) do
         | 
| 192 | 
            -
                # Just like when using create.blog, blog is automatically resolved when create.article is called
         | 
| 193 | 
            -
                create.article(with.comment)
         | 
| 194 | 
            -
                create.article(with_list.comment(3))
         | 
| 195 | 
            -
              end
         | 
| 196 | 
            -
            end
         | 
| 197 | 
            -
            ```
         | 
| 198 | 
            -
             | 
| 199 247 | 
             
            ## Development
         | 
| 200 248 |  | 
| 201 249 | 
             
            ```bash
         | 
    
        data/lib/factory_bot/with.rb
    CHANGED
    
    | @@ -63,9 +63,9 @@ module FactoryBot | |
| 63 63 | 
             
                      return first unless second
         | 
| 64 64 | 
             
                      return second unless first
         | 
| 65 65 |  | 
| 66 | 
            -
                       | 
| 67 | 
            -
                        first.call( | 
| 68 | 
            -
                        second.call( | 
| 66 | 
            +
                      proc do |arg|
         | 
| 67 | 
            +
                        first.call(arg)
         | 
| 68 | 
            +
                        second.call(arg)
         | 
| 69 69 | 
             
                      end
         | 
| 70 70 | 
             
                    end.call(block, other.block)
         | 
| 71 71 | 
             
                  )
         | 
| @@ -110,14 +110,17 @@ module FactoryBot | |
| 110 110 | 
             
                    else
         | 
| 111 111 | 
             
                      [@factory_name, @attrs]
         | 
| 112 112 | 
             
                    end
         | 
| 113 | 
            -
                  result = FactoryBot.__send__(factory_bot_method, factory_name, *traits, **attrs | 
| 113 | 
            +
                  result = FactoryBot.__send__(factory_bot_method, factory_name, *traits, **attrs)
         | 
| 114 114 |  | 
| 115 | 
            -
                   | 
| 116 | 
            -
                    parents = variation == :singular ? [result] : result
         | 
| 115 | 
            +
                  if block || !withes.empty?
         | 
| 117 116 | 
             
                    assoc_info = AssocInfo.get(factory_name)
         | 
| 117 | 
            +
                    parents = variation == :singular ? [result] : result
         | 
| 118 118 | 
             
                    parents.each do |parent|
         | 
| 119 119 | 
             
                      ancestors_for_children = [[assoc_info, parent], *ancestors || []]
         | 
| 120 120 | 
             
                      withes.each { _1.instantiate(build_strategy, ancestors_for_children) }
         | 
| 121 | 
            +
                      # We call the block for each parent object. This is an incompatible behavior with FactoryBot!
         | 
| 122 | 
            +
                      # If you want to avoid this, use `Object#tap` manually.
         | 
| 123 | 
            +
                      self.class.with_scoped_ancestors(ancestors_for_children) { block.call(result) } if block
         | 
| 121 124 | 
             
                    end
         | 
| 122 125 | 
             
                  end
         | 
| 123 126 |  | 
| @@ -154,9 +157,14 @@ module FactoryBot | |
| 154 157 | 
             
                        elsif args.empty? && kwargs.empty? && !block
         | 
| 155 158 | 
             
                          # <__method__>.<factory_name>(...)
         | 
| 156 159 | 
             
                          Proxy.new(self, __method__)
         | 
| 157 | 
            -
                        elsif __method__ == :with && args.empty? && !kwargs.empty?
         | 
| 160 | 
            +
                        elsif __method__ == :with && args.empty? && !kwargs.empty? && block
         | 
| 158 161 | 
             
                          # with(<factory_name>: <object>, ...) { ... }
         | 
| 159 | 
            -
                          With. | 
| 162 | 
            +
                          block = With.call_with_scope_adapter(&block)
         | 
| 163 | 
            +
                          With.call_with_scope(kwargs, &block)
         | 
| 164 | 
            +
                        elsif __method__ == :with_list && args.empty? && !kwargs.empty? && block
         | 
| 165 | 
            +
                          # with_list(<factory_name>: [<object>, ...], ...) { ... }
         | 
| 166 | 
            +
                          block = With.call_with_scope_adapter(&block)
         | 
| 167 | 
            +
                          kwargs.values.inject(:product).map { With.call_with_scope(kwargs.keys.zip(_1).to_h, &block) }
         | 
| 160 168 | 
             
                        else
         | 
| 161 169 | 
             
                          raise ArgumentError, "Invalid use of #{__method__}"
         | 
| 162 170 | 
             
                        end
         | 
| @@ -165,23 +173,35 @@ module FactoryBot | |
| 165 173 | 
             
                  end
         | 
| 166 174 |  | 
| 167 175 | 
             
                  # @!visibility private
         | 
| 168 | 
            -
                  # @ | 
| 169 | 
            -
                  def  | 
| 170 | 
            -
                    return unless block_given?
         | 
| 176 | 
            +
                  # @return [Array<Array(AssocInfo, Object)>, nil]
         | 
| 177 | 
            +
                  def scoped_ancestors = Thread.current[:factory_bot_with_scoped_ancestors]
         | 
| 171 178 |  | 
| 179 | 
            +
                  # @param ancestors [Array<Array(AssocInfo, Object)>]
         | 
| 180 | 
            +
                  def with_scoped_ancestors(ancestors, &)
         | 
| 172 181 | 
             
                    tmp_scoped_ancestors = scoped_ancestors
         | 
| 173 | 
            -
                    Thread.current[:factory_bot_with_scoped_ancestors] = [
         | 
| 174 | 
            -
                      *objects.map { [AssocInfo.get(_1), _2] },
         | 
| 175 | 
            -
                      *tmp_scoped_ancestors || [],
         | 
| 176 | 
            -
                    ]
         | 
| 182 | 
            +
                    Thread.current[:factory_bot_with_scoped_ancestors] = [*ancestors, *tmp_scoped_ancestors || []]
         | 
| 177 183 | 
             
                    result = yield
         | 
| 178 184 | 
             
                    Thread.current[:factory_bot_with_scoped_ancestors] = tmp_scoped_ancestors
         | 
| 179 185 | 
             
                    result
         | 
| 180 186 | 
             
                  end
         | 
| 181 187 |  | 
| 182 188 | 
             
                  # @!visibility private
         | 
| 183 | 
            -
                  # @ | 
| 184 | 
            -
                  def  | 
| 189 | 
            +
                  # @param objects [{Symbol => Object}]
         | 
| 190 | 
            +
                  def call_with_scope(objects, &block)
         | 
| 191 | 
            +
                    with_scoped_ancestors(objects.map { [AssocInfo.get(_1), _2] }) { block.call(objects) }
         | 
| 192 | 
            +
                  end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                  # @!visibility private
         | 
| 195 | 
            +
                  def call_with_scope_adapter(&block)
         | 
| 196 | 
            +
                    params = block.parameters
         | 
| 197 | 
            +
                    if params.any? { %i[req opt rest].include?(_1[0]) }
         | 
| 198 | 
            +
                      ->(objects) { block.call(*objects.values) }
         | 
| 199 | 
            +
                    elsif params.any? { %i[keyreq key keyrest].include?(_1[0]) }
         | 
| 200 | 
            +
                      ->(objects) { block.call(**objects) }
         | 
| 201 | 
            +
                    else
         | 
| 202 | 
            +
                      ->(_) { block.call }
         | 
| 203 | 
            +
                    end
         | 
| 204 | 
            +
                  end
         | 
| 185 205 | 
             
                end
         | 
| 186 206 |  | 
| 187 207 | 
             
                %i[build build_stubbed create attributes_for with].each { register_strategy _1 }
         | 
    
        metadata
    CHANGED
    
    | @@ -1,13 +1,13 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: factory_bot-with
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.5.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - yubrot
         | 
| 8 8 | 
             
            bindir: exe
         | 
| 9 9 | 
             
            cert_chain: []
         | 
| 10 | 
            -
            date: 2025-03- | 
| 10 | 
            +
            date: 2025-03-20 00:00:00.000000000 Z
         | 
| 11 11 | 
             
            dependencies:
         | 
| 12 12 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 13 13 | 
             
              name: factory_bot
         |