let_each 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 649eafd9476ae0aeffe70f65df3351c3d2c4744f1a844039966cec1d155ac26a
4
+ data.tar.gz: 5a8cc3329f8b40133f36baaa2463a790938cbcfd8e1945c05a8f4a8848aa96bc
5
+ SHA512:
6
+ metadata.gz: '0184e7882ccb883e2e3e79e6661a9e3dced08549ebcb72c50de9f0fb4f8d3e63ebb194be376ae5d21150c459455dd39f43906288ea3e50917c8ad8c6bcfb0994'
7
+ data.tar.gz: bb6ed3372685424c08f478baf9d94d7aeab508a0af694507576ee7c897092674b824892d4dd79b69a1706e828ea942c8ea6c303e2e91c54c39ac5edb33c90bfb
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2026] [Andrew Logsdon]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ ![Tests](https://github.com/Alogsdon/rspec-let-each/actions/workflows/test.yml/badge.svg)
2
+ # LetEach
3
+ (rspec-let-each)
4
+
5
+ `let_each` is an ergonomic RSpec helper that spawns context blocks with corresponding `let`s for each value in an array
6
+
7
+ a common dilemma I found when writing specs was
8
+ enumerating all edge cases is DRY and gives good coverage
9
+ but it requires too much block nesting/boiler plate.
10
+ Also, contexts written like this are not compatible with `let`s,
11
+ without some hackery.
12
+
13
+ e.g.
14
+ ```ruby
15
+ [foo,bar].each do |x_local|
16
+ context "with x=#{x_local}" do
17
+ let(:x) { x_local }
18
+
19
+ it_behaves_like 'an example'
20
+ end
21
+ end
22
+ ```
23
+
24
+ alternatively, let + sample is succint
25
+ but it gives flakey coverage
26
+ e.g.
27
+ ```ruby
28
+ let(:x) { [foo, bar].sample }
29
+
30
+ it_behaves_like 'an example'
31
+ ```
32
+
33
+ with this helper, we should be able to have the best of both worlds
34
+ - same coverage as first example
35
+ - compatible with `let` variables
36
+ - same succintness as second example
37
+ ```ruby
38
+ let_each(:x, 2) { [foo, bar] }
39
+
40
+ it_behaves_like 'an example'
41
+ ```
42
+
43
+ Using the `let_each` by itself can lead to some awkward patterns when zipping expectations into the examples.
44
+ That's why I added a chainable method `with`, intended for the corresponding expectations, or possibly actions.
45
+ (The length of all chained `with`s is assumed to be the same as the parent `let_each`)
46
+ now we might write
47
+ ```ruby
48
+ subject { test_method(x) }
49
+
50
+ let_each(:x, 2) { [foo, bar] }
51
+ .with(:expected_x) { [foo_expect, bar_expect] }
52
+
53
+ it { is_expected.to eq(expected_x) }
54
+ ```
55
+ it can be continually chained, in case we need variables in tripples or quads too, why not.
56
+
57
+ ## Installation
58
+
59
+ Add this line to your application's `Gemfile` typically inside the `:test` group:
60
+
61
+ ```ruby
62
+ group :test do
63
+ gem 'let_each'
64
+ end
65
+ ```
66
+
67
+ ## Setup
68
+
69
+ `let_each` is configured to automatically mix into RSpec's example groups when loaded.
70
+ In some projects, the `lib` folder is automatically loaded. If not, just add a require for `let_each` in your test setup.
71
+ ```ruby
72
+ # spec/spec_helper.rb
73
+ require 'let_each'
74
+ ```
75
+ You can immediately start using let_each inside your describe or context blocks.
76
+
77
+
78
+ ### Verifying Installation
79
+ You can verify that the helper is loaded correctly by running a simple test:
80
+
81
+ ```ruby
82
+ RSpec.describe "LetEach Integration" do
83
+ let_each(:val) { [1, 2] }
84
+
85
+ it "works" do
86
+ expect([1, 2]).to include(val)
87
+ end
88
+ end
89
+ ```
90
+
91
+ ## Compatibility
92
+
93
+ `let_each` is tested against and supports:
94
+ - **Ruby:** 2.7, 3.0, 3.1, 3.2, 3.3, 3.4
95
+ - **RSpec:** 3.0 and newer
96
+
97
+ ## License
98
+ MIT
@@ -0,0 +1,116 @@
1
+ module LetEach
2
+ module Extension
3
+ # Usage:
4
+ # lazy signature
5
+ # let_each(:x, 2) { [let_foo, let_bar] }
6
+ # .with(:y) { [foo_expected, bar_expected] }
7
+ #
8
+ # eager signature
9
+ # let_each(:y, [eager_foo, eager_bar])
10
+ #
11
+ # the lazy_array_block plays nice with other `let`s, but contexts are eagerly evaluated
12
+ # so we need to provide a "length" to know how many contexts to spawn
13
+ # then the array values can still be lazily evaluated
14
+ # alternatively, just pass an eager array if you don't need the laziness on the values
15
+ #
16
+ # There is possibly some overhead to using this in its present state. It could be optimized more but this is just my POC for the feature.
17
+ # Careful not to exponentially spawn contexts, every call to this helper will multiply the number of examples
18
+ # In a future feature we may allow for automatically limiting the number of contexts, then trending towards `let(:x) { [foo, bar].sample }`
19
+ # AFAIK, this works in nested contexts, and with shared_examples/contexts,
20
+ # but I'd suggest pushing the usage as close to the actual examples as possible, so you don't enumerate too much
21
+ #
22
+ # I've added a chainable `with` method to allow for parallely assigned lets
23
+ # this can be also be chained multiple times
24
+ def let_each(name, length_or_array, &lazy_array_block)
25
+ if lazy_array_block
26
+ raise 'must specify the length when providing a lazy array block' unless length_or_array.is_a?(Integer)
27
+ length = length_or_array
28
+ else
29
+ raise 'must provide an array when not providing a lazy array block' unless length_or_array.respond_to?(:length)
30
+ array = length_or_array
31
+ length = array.length
32
+ lazy_array_block = -> { array }
33
+ end
34
+
35
+ array_proc_key = :"_#{name}_array_proc"
36
+ # I just didn't handle this case. doing so is a bit tricky with the approach I used
37
+ # we'd need to replay the `it` overrides with the changed value removed
38
+ raise "let_each already used for key: #{name}" if instance_methods.include?(array_proc_key)
39
+ # `it` was already used in this context but we didn't get a chance to override it yet
40
+ if defined?(@context_leafs)
41
+ raise 'let_each used after an example. either nest in a new context or arrange `let_each` above examples'
42
+ end
43
+
44
+ # behavior is also unexpected if we `let` with this same name, but I'm not going out of my way to guard against that
45
+
46
+ let(array_proc_key, &lazy_array_block) # memoize the array proc result
47
+ # `super` would only work the first time we call let_each per context. so we'll just closure it every time
48
+ old_it = method(:it).unbind
49
+ chainable = LetEachWithChainable.new(self, length)
50
+ define_singleton_method(:it) do |*args, &block|
51
+ if defined?(@context_leafs)
52
+ # we make a lot of contexts with this helper
53
+ # this is an improvement to reuse them when possible
54
+ # (when `it` is used multiple times in the same context)
55
+ # should be able to do something similar with `context` but I'm not worried about it right now
56
+ # new context = caches are dumped anyway, so there's probably not much to gain
57
+ @context_leafs.each do |leaf|
58
+ old_it.bind_call(leaf, *args, &block)
59
+ end
60
+ else
61
+ # first time we're calling `it` in this context
62
+ # instance variable will be inaccessible from within these context blocks
63
+ # so we assign the local variable too
64
+ @context_leafs = context_leafs = []
65
+ length.times do |i|
66
+ context "when #{name}[#{i}]" do
67
+ let(name) { send(array_proc_key)[i] }
68
+ chainable.each do |proc|
69
+ instance_exec(i, &proc)
70
+ end
71
+
72
+ old_it.bind_call(self, *args, &block)
73
+ context_leafs << self
74
+ end
75
+ end
76
+ end
77
+ end
78
+ chainable
79
+ end
80
+
81
+ class LetEachWithChainable
82
+ attr_accessor :withs, :example_group
83
+
84
+ def initialize(example_group, length)
85
+ @example_group = example_group
86
+ @length = length
87
+ @withs = []
88
+ end
89
+
90
+ def each(&block)
91
+ withs.each(&block)
92
+ end
93
+
94
+ def with(name, array = nil, &lazy_array_block)
95
+ if lazy_array_block
96
+ # length is assumed to be the same as the base let_each
97
+ raise 'dont need to provide a second argument when providing a lazy array block' if array
98
+ else
99
+ lazy_array_block = -> { array }
100
+ end
101
+ array_proc_key = :"_#{name}_array_proc"
102
+ # can memoize the proc right away
103
+ example_group.let(array_proc_key, &lazy_array_block)
104
+ # we can't unload the main `let` until we're in the context
105
+ # so just store the proc
106
+ withs << lambda do |index|
107
+ # self is the only variable not closured here
108
+ # we'll instance_exec this on the context
109
+ let(name) { send(array_proc_key)[index] }
110
+ end
111
+
112
+ self
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,3 @@
1
+ module LetEach
2
+ VERSION = '0.1.0'
3
+ end
data/lib/let_each.rb ADDED
@@ -0,0 +1,8 @@
1
+
2
+ if defined?(RSpec)
3
+ require 'let_each/version'
4
+ require 'let_each/extension'
5
+ RSpec.configure do |config|
6
+ config.extend LetEach::Extension
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: let_each
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Logsdon
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rspec-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: bundler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ executables: []
69
+ extensions: []
70
+ extra_rdoc_files: []
71
+ files:
72
+ - LICENSE.txt
73
+ - README.md
74
+ - lib/let_each.rb
75
+ - lib/let_each/extension.rb
76
+ - lib/let_each/version.rb
77
+ homepage: https://github.com/Alogsdon/rspec-let-each
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '2.7'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 4.0.4
96
+ specification_version: 4
97
+ summary: Ergonomic context spawning for RSpec
98
+ test_files: []