rspec_in_context 1.1.0.3 → 1.2.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.
data/LLMS.md ADDED
@@ -0,0 +1,388 @@
1
+ # rspec_in_context — LLM Reference
2
+
3
+ ## What is this gem?
4
+
5
+ `rspec_in_context` is a Ruby gem that provides `define_context` / `in_context` as a replacement for RSpec's `shared_examples` / `it_behaves_like`. The key advantage: contexts can accept blocks that get injected at a specific point via `execute_tests`, making them composable like methods. Contexts can also accept arguments, be namespaced, and nest within each other.
6
+
7
+ ## Setup
8
+
9
+ **Gemfile:**
10
+ ```ruby
11
+ gem 'rspec_in_context'
12
+ ```
13
+
14
+ **spec_helper.rb (or rails_helper.rb):**
15
+ ```ruby
16
+ require 'rspec_in_context'
17
+
18
+ RSpec.configure do |config|
19
+ config.include RspecInContext
20
+ end
21
+ ```
22
+
23
+ Requires Ruby >= 3.2.
24
+
25
+ ## API Reference
26
+
27
+ ### `RSpec.define_context(name, namespace: nil, ns: nil, silent: true, print_context: nil, &block)`
28
+
29
+ Define a global context (available in all spec files). Use outside any `describe`/`context` block.
30
+
31
+ ```ruby
32
+ RSpec.define_context :with_frozen_time do
33
+ before { freeze_time }
34
+ execute_tests
35
+ end
36
+ ```
37
+
38
+ ### `define_context(name, namespace: nil, ns: nil, silent: true, print_context: nil, &block)`
39
+
40
+ Define a scoped context (available only within the enclosing `describe`/`context` block). Use inside a spec.
41
+
42
+ ```ruby
43
+ RSpec.describe MyService do
44
+ define_context :with_valid_params do
45
+ let(:params) { { name: "test" } }
46
+ execute_tests
47
+ end
48
+
49
+ in_context :with_valid_params do
50
+ it "works" do
51
+ expect(MyService.call(params)).to be_success
52
+ end
53
+ end
54
+ end
55
+ ```
56
+
57
+ **Parameters:**
58
+ - `name` (String, Symbol) — required. The context identifier.
59
+ - `namespace:` / `ns:` (String, Symbol) — optional namespace to avoid name collisions.
60
+ - `silent:` (Boolean, default: `true`) — when `true`, wraps in anonymous context. When `false`, wraps in a named context visible in `--format doc`.
61
+ - `print_context:` (Boolean) — inverse of `silent`. `print_context: true` is equivalent to `silent: false`.
62
+ - `&block` — required. The context body.
63
+
64
+ ### `in_context(name, *args, namespace: nil, ns: nil, &block)`
65
+
66
+ Use a previously defined context. The optional block is injected where `execute_tests` appears in the context definition.
67
+
68
+ ```ruby
69
+ in_context :with_frozen_time do
70
+ it "uses frozen time" do
71
+ expect(Time.current).to eq(Time.current) # always true
72
+ end
73
+ end
74
+
75
+ # With arguments (no block)
76
+ in_context :validates_field, :email
77
+ ```
78
+
79
+ **Parameters:**
80
+ - `name` (String, Symbol) — the context to use.
81
+ - `*args` — positional arguments passed to the context block.
82
+ - `namespace:` / `ns:` — namespace to look in.
83
+ - `&block` — optional. Injected at `execute_tests`.
84
+
85
+ ### `execute_tests` / `instantiate_context`
86
+
87
+ Placeholder inside a `define_context` block. Marks where the caller's block (from `in_context`) will be injected.
88
+
89
+ ```ruby
90
+ RSpec.define_context :setup_env do
91
+ let(:user) { create(:user) }
92
+ before { sign_in user }
93
+
94
+ execute_tests # <-- caller's block runs here
95
+ end
96
+ ```
97
+
98
+ These are aliases. `instanciate_context` also works but is deprecated (typo).
99
+
100
+ ## Key Concepts
101
+
102
+ ### Scoped vs Global contexts
103
+
104
+ - **Scoped**: `define_context` inside a `describe`/`context` — automatically removed when the block ends.
105
+ - **Global**: `RSpec.define_context` outside any describe — available everywhere, never cleaned up.
106
+
107
+ ### Block injection (execute_tests)
108
+
109
+ The core feature. When you call `in_context :foo do ... end`, the block is stored and injected where `execute_tests` appears in the `:foo` definition. If `execute_tests` is absent and you pass a block, the block is silently ignored.
110
+
111
+ ### Arguments
112
+
113
+ Context blocks can accept arguments via block parameters:
114
+
115
+ ```ruby
116
+ RSpec.define_context :validates_field do |field_name|
117
+ context "when #{field_name} is nil" do
118
+ let(field_name) { nil }
119
+ it { is_expected.not_to be_valid }
120
+ end
121
+ end
122
+
123
+ in_context :validates_field, :email
124
+ ```
125
+
126
+ ### Namespacing
127
+
128
+ Prevents name collisions when different parts of your test suite define contexts with the same name:
129
+
130
+ ```ruby
131
+ RSpec.define_context :valid_params, ns: :users do ... end
132
+ RSpec.define_context :valid_params, ns: :posts do ... end
133
+
134
+ in_context :valid_params, ns: :users do ... end
135
+ ```
136
+
137
+ If the same name exists in multiple namespaces and you don't specify `ns:`, `AmbiguousContextName` is raised.
138
+
139
+ ### Silent mode
140
+
141
+ - `silent: true` (default): `in_context` wraps in an anonymous `context {}` block — invisible in `--format doc`.
142
+ - `silent: false` or `print_context: true`: wraps in `context("context_name") {}` — visible in `--format doc`.
143
+
144
+ ### Thread-local block stack for nesting
145
+
146
+ Nested `in_context` calls work correctly. The gem uses `Thread.current[:test_block_stack]` (a stack) to track which block should be injected at each nesting level.
147
+
148
+ ## Common Patterns
149
+
150
+ ### Pattern: Setup context (before/let + execute_tests)
151
+
152
+ The most common pattern. Set up state, then let the caller provide assertions.
153
+
154
+ ```ruby
155
+ RSpec.define_context :as_admin do
156
+ let(:current_user) { create(:user, role: :admin) }
157
+ before { sign_in current_user }
158
+ execute_tests
159
+ end
160
+
161
+ in_context :as_admin do
162
+ it "can access admin panel" do
163
+ get admin_path
164
+ expect(response).to have_http_status(:ok)
165
+ end
166
+ end
167
+ ```
168
+
169
+ ### Pattern: Parameterized context (arguments)
170
+
171
+ Pass data to customize the context behavior.
172
+
173
+ ```ruby
174
+ RSpec.define_context :contract_validation do |required_fields|
175
+ required_fields.each do |field|
176
+ context "when #{field} is missing" do
177
+ let(field) { nil }
178
+ it("fails") { expect(subject).to be_a_failure }
179
+ end
180
+ end
181
+ end
182
+
183
+ in_context :contract_validation, %i[name email phone]
184
+ ```
185
+
186
+ ### Pattern: Built-in tests + execute_tests
187
+
188
+ Include both built-in tests and a placeholder for injected tests.
189
+
190
+ ```ruby
191
+ RSpec.define_context :authenticated_request do
192
+ let(:user) { create(:user) }
193
+ before { sign_in user }
194
+
195
+ context "without authentication" do
196
+ before { sign_out user }
197
+ it("returns 401") { expect(response).to have_http_status(:unauthorized) }
198
+ end
199
+
200
+ execute_tests # caller's authenticated tests go here
201
+ end
202
+ ```
203
+
204
+ ### Pattern: Inline define + use
205
+
206
+ For contexts only needed in one spec file.
207
+
208
+ ```ruby
209
+ RSpec.describe ReportGenerator do
210
+ define_context :with_sample_data do
211
+ let!(:records) { create_list(:record, 5) }
212
+ execute_tests
213
+ end
214
+
215
+ in_context :with_sample_data do
216
+ it "generates the report" do
217
+ expect(ReportGenerator.call.rows.count).to eq(5)
218
+ end
219
+ end
220
+ end
221
+ ```
222
+
223
+ ### Pattern: Nested in_context in define_context
224
+
225
+ Compose higher-level contexts from smaller ones.
226
+
227
+ ```ruby
228
+ RSpec.define_context :full_integration do
229
+ in_context :with_frozen_time do
230
+ in_context :as_admin do
231
+ execute_tests
232
+ end
233
+ end
234
+ end
235
+ ```
236
+
237
+ ### Pattern: File organization (spec/contexts/*.rb)
238
+
239
+ ```
240
+ spec/
241
+ contexts/
242
+ authenticated_request_context.rb
243
+ frozen_time_context.rb
244
+ contract_validation_context.rb
245
+ spec_helper.rb # requires all contexts
246
+ ```
247
+
248
+ ```ruby
249
+ # spec_helper.rb
250
+ Dir[File.join(__dir__, "contexts", "**", "*.rb")].each { |f| require f }
251
+ ```
252
+
253
+ ## Error Reference
254
+
255
+ ### `RspecInContext::NoContextFound`
256
+
257
+ **Cause:** `in_context` references a name that doesn't exist or is out of scope.
258
+
259
+ **Fix:** Check spelling, ensure the context is defined globally or in an enclosing scope.
260
+
261
+ ### `RspecInContext::AmbiguousContextName`
262
+
263
+ **Cause:** The same context name exists in multiple namespaces and no namespace was specified.
264
+
265
+ **Fix:** Add `ns: :your_namespace` to `in_context`.
266
+
267
+ ### `RspecInContext::InvalidContextName`
268
+
269
+ **Cause:** `define_context` called with `nil` or `""`.
270
+
271
+ **Fix:** Provide a valid string or symbol name.
272
+
273
+ ### `RspecInContext::MissingDefinitionBlock`
274
+
275
+ **Cause:** `define_context` called without a block.
276
+
277
+ **Fix:** Add a block: `define_context(:name) { ... }`.
278
+
279
+ ## Anti-patterns / Gotchas
280
+
281
+ ### Forgetting execute_tests when passing a block
282
+
283
+ ```ruby
284
+ # BAD — the block passed to in_context is silently ignored
285
+ RSpec.define_context :setup do
286
+ let(:user) { create(:user) }
287
+ # missing execute_tests!
288
+ end
289
+
290
+ in_context :setup do
291
+ it "never runs" do # <-- this test is lost
292
+ expect(user).to be_present
293
+ end
294
+ end
295
+
296
+ # GOOD
297
+ RSpec.define_context :setup do
298
+ let(:user) { create(:user) }
299
+ execute_tests
300
+ end
301
+ ```
302
+
303
+ ### Ambiguous context names without namespace
304
+
305
+ ```ruby
306
+ # BAD — raises AmbiguousContextName
307
+ RSpec.define_context :valid_params, ns: :users do ... end
308
+ RSpec.define_context :valid_params, ns: :posts do ... end
309
+ in_context :valid_params do ... end # which one?
310
+
311
+ # GOOD
312
+ in_context :valid_params, ns: :users do ... end
313
+ ```
314
+
315
+ ### Using deprecated instanciate_context
316
+
317
+ ```ruby
318
+ # DEPRECATED — emits warning
319
+ instanciate_context
320
+
321
+ # GOOD
322
+ execute_tests
323
+ # or
324
+ instantiate_context
325
+ ```
326
+
327
+ ## Real-World Examples
328
+
329
+ The `examples/` directory in the repository contains real-world usage patterns:
330
+
331
+ - `examples/contexts/` — Context definitions (authentication, interactor contracts, frozen time, job setup, mailer, composed contexts)
332
+ - `examples/usage/` — Spec files showing how to use those contexts in practice
333
+
334
+ See `examples/README.md` for the full list.
335
+
336
+ ## Complete Example
337
+
338
+ Showing multiple features together:
339
+
340
+ ```ruby
341
+ # spec/contexts/api_context.rb
342
+ RSpec.define_context :authenticated_api, silent: false do
343
+ let(:user) { create(:user) }
344
+ let(:headers) { { "Authorization" => "Bearer #{user.token}" } }
345
+
346
+ context "without authentication" do
347
+ let(:headers) { {} }
348
+
349
+ it "returns 401" do
350
+ send(http_method, endpoint, headers: headers)
351
+ expect(response).to have_http_status(:unauthorized)
352
+ end
353
+ end
354
+
355
+ execute_tests
356
+ end
357
+
358
+ RSpec.define_context :validates_contract do |required_fields|
359
+ required_fields.each do |field|
360
+ context "when #{field} is missing" do
361
+ let(field) { nil }
362
+ it("returns 422") { expect(response).to have_http_status(:unprocessable_entity) }
363
+ end
364
+ end
365
+ end
366
+
367
+ # spec/requests/api/projects_spec.rb
368
+ RSpec.describe "API Projects", type: :request do
369
+ let(:http_method) { :post }
370
+ let(:endpoint) { api_projects_path }
371
+
372
+ in_context :authenticated_api do
373
+ let(:name) { "My Project" }
374
+ let(:description) { "A great project" }
375
+
376
+ before do
377
+ post endpoint, params: { name: name, description: description }, headers: headers
378
+ end
379
+
380
+ in_context :validates_contract, %i[name]
381
+
382
+ it "creates the project" do
383
+ expect(response).to have_http_status(:created)
384
+ expect(json_response["name"]).to eq("My Project")
385
+ end
386
+ end
387
+ end
388
+ ```