jsonrpc-middleware 0.3.0 → 0.4.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/.claude/commands/test.md +561 -0
- data/.claude/settings.local.json +2 -1
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +6 -2
- data/README.md +47 -18
- data/Rakefile +1 -3
- data/examples/rack/Gemfile.lock +1 -1
- data/examples/rack/app.rb +1 -4
- data/examples/rack-echo/app.rb +1 -4
- data/examples/rack-single-file/config.ru +1 -4
- data/examples/rails/app/controllers/jsonrpc_controller.rb +1 -4
- data/examples/rails-single-file/config.ru +1 -4
- data/examples/sinatra-classic/app.rb +1 -4
- data/examples/sinatra-modular/app.rb +1 -4
- data/lib/jsonrpc/batch_request.rb +32 -0
- data/lib/jsonrpc/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a45b1d353d7fd1547e7e984edba9c9ed1e02669914c00294d08f259cb5bb76e6
|
4
|
+
data.tar.gz: 25d2b7802a03378c90e181d5e5b580489299d41a3070817d762b43063a9d68b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3a417f6096c195940c782cb313f9b4e7a5e6ef2c1c6ed994a4d23e226e7a70ba7df4b635b03c56f392983b45b1d0239f0a121c2b2982101c5ad5c47680bd7f8
|
7
|
+
data.tar.gz: 601bc95a7d965e6721af683765881d73ac34fc5ced0791c398b1ddc4acff054b586d0c74ab0107bf139bb177afcebc760460d96cb86896afec1394972b98b6d9
|
@@ -0,0 +1,561 @@
|
|
1
|
+
# Test
|
2
|
+
|
3
|
+
Write RSpec tests for a given file, module, class or method. Ensure it meets the guidelines. If the file already has
|
4
|
+
tests, simply review the test guidelines.
|
5
|
+
|
6
|
+
## Best Practices
|
7
|
+
|
8
|
+
### Describe Your Methods
|
9
|
+
|
10
|
+
Be clear about what method you are describing. For instance, use the Ruby documentation convention of `.` when
|
11
|
+
referring to a class method's name and `#` when referring to an instance method's name.
|
12
|
+
|
13
|
+
**bad**
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
describe 'the authenticate method for User' do
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'if the user is an admin' do
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
**good**
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
describe '.authenticate' do
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#admin?' do
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
### Use contexts
|
34
|
+
|
35
|
+
Contexts are a powerful method to make your tests clear and well organized (they keep tests easy to read).
|
36
|
+
When describing a context, start its description with 'when', 'with' or 'without'.
|
37
|
+
|
38
|
+
**bad**
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
it 'has 200 status code if logged in' do
|
42
|
+
expect(response).to respond_with 200
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'has 401 status code if not logged in' do
|
46
|
+
expect(response).to respond_with 401
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
**good**
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
context 'when logged in' do
|
54
|
+
it { is_expected.to respond_with 200 }
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when logged out' do
|
58
|
+
it { is_expected.to respond_with 401 }
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
### Keep your description short
|
63
|
+
|
64
|
+
A spec description should never be longer than 40 characters. If this happens you should split it using a context.
|
65
|
+
In the example below, we removed the description related to the status code, which has been replaced by the
|
66
|
+
expectation `is_expected`. If you run this test typing `rspec filename` you will obtain a readable output.
|
67
|
+
|
68
|
+
**bad**
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
it 'has 422 status code if an unexpected params will be added' do
|
72
|
+
```
|
73
|
+
|
74
|
+
**good**
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
context 'when not valid' do
|
78
|
+
it { is_expected.to respond_with 422 }
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
**Formatted output**
|
83
|
+
|
84
|
+
```
|
85
|
+
when not valid
|
86
|
+
it should respond with 422
|
87
|
+
```
|
88
|
+
|
89
|
+
### Single expectation test
|
90
|
+
|
91
|
+
The 'one expectation' tip is more broadly expressed as 'each test should make only one assertion'. This helps you on
|
92
|
+
finding possible errors, going directly to the failing test, and to make your code readable. In isolated unit specs,
|
93
|
+
you want each example to specify one (and only one) behavior. Multiple expectations in the same example are a signal
|
94
|
+
that you may be specifying multiple behaviors.
|
95
|
+
|
96
|
+
**good (isolated)**
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
it { is_expected.to respond_with_content_type(:json) }
|
100
|
+
it { is_expected.to assign_to(:resource) }
|
101
|
+
```
|
102
|
+
|
103
|
+
Anyway, in tests that are not isolated (e.g. ones that integrate with a DB, an external webservice, or
|
104
|
+
end-to-end-tests), you take a massive performance hit to do the same setup over and over again, just to set a different
|
105
|
+
expectation in each test. In these sorts of slower tests, I think it's fine to specify more than one isolated behavior.
|
106
|
+
|
107
|
+
**good (not isolated)**
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
it 'creates a resource' do
|
111
|
+
expect(response).to respond_with_content_type(:json)
|
112
|
+
expect(response).to assign_to(:resource)
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
### Test all possible cases
|
117
|
+
|
118
|
+
Testing is a good practice, but if you do not test the edge cases, it will not be useful. Test valid, edge and invalid
|
119
|
+
case. For example, consider the following action.
|
120
|
+
|
121
|
+
**Destroy Action**
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
before_action :find_owned_resources
|
125
|
+
before_action :find_resource
|
126
|
+
|
127
|
+
def destroy
|
128
|
+
render 'show'
|
129
|
+
@consumption.destroy
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
The error I usually see lies in testing only whether the resource has been removed. But there are at least two edge
|
134
|
+
cases: when the resource is not found and when it's not owned. As a rule of thumb think of all the possible inputs
|
135
|
+
and test them.
|
136
|
+
|
137
|
+
**bad**
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
it 'shows the resource'
|
141
|
+
```
|
142
|
+
|
143
|
+
**good**
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
describe '#destroy' do
|
147
|
+
context 'when resource is found' do
|
148
|
+
it 'responds with 200'
|
149
|
+
it 'shows the resource'
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'when resource is not found' do
|
153
|
+
it 'responds with 404'
|
154
|
+
end
|
155
|
+
|
156
|
+
context 'when resource is not owned' do
|
157
|
+
it 'responds with 404'
|
158
|
+
end
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
### Expect vs Should syntax
|
163
|
+
|
164
|
+
On new projects always use the `expect` syntax.
|
165
|
+
|
166
|
+
**bad**
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
it 'creates a resource' do
|
170
|
+
response.should respond_with_content_type(:json)
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
**good**
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
it 'creates a resource' do
|
178
|
+
expect(response).to respond_with_content_type(:json)
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
Configure the RSpec to only accept the new syntax on new projects, to avoid having the 2 syntax all over the place.
|
183
|
+
|
184
|
+
**good**
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
# spec_helper.rb
|
188
|
+
RSpec.configure do |config|
|
189
|
+
# ...
|
190
|
+
config.expect_with :rspec do |c|
|
191
|
+
c.syntax = :expect
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
On one line expectations or with implicit subject we should use `is_expected.to`.
|
197
|
+
|
198
|
+
**bad**
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
context 'when not valid' do
|
202
|
+
it { should respond_with 422 }
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
**good**
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
context 'when not valid' do
|
210
|
+
it { is_expected.to respond_with 422 }
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
### Use subject
|
215
|
+
|
216
|
+
If you have several tests related to the same subject use `subject{}` to DRY them up.
|
217
|
+
|
218
|
+
**bad**
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
it { expect(assigns('message')).to match /it was born in Bellville/ }
|
222
|
+
```
|
223
|
+
|
224
|
+
**good**
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
subject { assigns('message') }
|
228
|
+
|
229
|
+
it { is_expected.to match /it was born in Billville/ }
|
230
|
+
```
|
231
|
+
|
232
|
+
RSpec has also the ability to use a named subject (learn more about [rspec subject](https://rspec.info/features/3-12/rspec-core/subject/)).
|
233
|
+
|
234
|
+
**good**
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
subject(:hero) { Hero.first }
|
238
|
+
|
239
|
+
it 'carries a sword' do
|
240
|
+
expect(hero.equipment).to include 'sword'
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
### Use let and let!
|
245
|
+
|
246
|
+
When you have to assign a variable instead of using a `before` block to create an instance variable, use `let`. Using
|
247
|
+
`let` the variable lazy loads only when it is used the first time in the test and get cached until that specific test
|
248
|
+
is finished. A really good and deep description of what `let` does can be found in this
|
249
|
+
[stackoverflow answer](http://stackoverflow.com/questions/5359558/when-to-use-rspec-let/5359979#5359979).
|
250
|
+
|
251
|
+
**bad**
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
describe '#type_id' do
|
255
|
+
before { @resource = FactoryBot.create :device }
|
256
|
+
before { @type = Type.find @resource.type_id }
|
257
|
+
|
258
|
+
it 'sets the type_id field' do
|
259
|
+
expect(@resource.type_id).to eq(@type.id)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
264
|
+
**good**
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
describe '#type_id' do
|
268
|
+
let(:resource) { FactoryBot.create :device }
|
269
|
+
let(:type) { Type.find resource.type_id }
|
270
|
+
|
271
|
+
it 'sets the type_id field' do
|
272
|
+
expect(resource.type_id).to eq(type.id)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
Use `let` to initialize actions that are lazy loaded to test your specs.
|
278
|
+
|
279
|
+
**good**
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
context 'when updates a not existing property value' do
|
283
|
+
let(:properties) { { id: Settings.resource_id, value: 'on' } }
|
284
|
+
|
285
|
+
def update
|
286
|
+
resource.properties = properties
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'raises a not found error' do
|
290
|
+
expect { update }.to raise_error Mongoid::Errors::DocumentNotFound
|
291
|
+
end
|
292
|
+
end
|
293
|
+
```
|
294
|
+
|
295
|
+
Use `let!` if you want to define the variable when the block is defined. This can be useful to populate your database
|
296
|
+
to test queries or scopes. Here an example of what let actually is (learn more about
|
297
|
+
[rspec let](https://rspec.info/features/3-12/rspec-core/helper-methods/let/)).
|
298
|
+
|
299
|
+
**Explanation**
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
# this use of let
|
303
|
+
let(:foo) { Foo.new }
|
304
|
+
|
305
|
+
# is very nearly equivalent to this:
|
306
|
+
def foo
|
307
|
+
@foo ||= Foo.new
|
308
|
+
end
|
309
|
+
```
|
310
|
+
|
311
|
+
### Mock or not to mock
|
312
|
+
|
313
|
+
As general rule do not (over)use mocks and test real behavior when possible, as testing real cases is useful when
|
314
|
+
validating your application flow.
|
315
|
+
|
316
|
+
**good**
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
# simulate a not found resource
|
320
|
+
context 'when not found' do
|
321
|
+
before do
|
322
|
+
allow(Resource).to receive(:where).with(created_from: params[:id]).and_return(false)
|
323
|
+
end
|
324
|
+
|
325
|
+
it { is_expected.to respond_with 404 }
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
Mocking makes your specs faster but they are difficult to use. You need to understand them well to use them well. Read
|
330
|
+
[this article](https://web.archive.org/web/20220612005103/http://myronmars.to/n/dev-blog/2012/06/thoughts-on-mocking)
|
331
|
+
to learn more about mocks.
|
332
|
+
|
333
|
+
### Create only the data you need
|
334
|
+
|
335
|
+
If you have ever worked in a medium size project (but also in small ones), test suites can be heavy to run. To solve
|
336
|
+
this problem, it's important not to load more data than needed. Also, if you think you need dozens of records, you are
|
337
|
+
probably wrong.
|
338
|
+
|
339
|
+
**good**
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
describe 'User' do
|
343
|
+
describe '.top' do
|
344
|
+
before { FactoryBot.create_list(:user, 3) }
|
345
|
+
|
346
|
+
it { expect(User.top(2)).to have(2).items }
|
347
|
+
end
|
348
|
+
end
|
349
|
+
```
|
350
|
+
|
351
|
+
### Use factories and not fixtures
|
352
|
+
|
353
|
+
This is an old topic, but it's still good to remember it. Do not use fixtures because they are difficult to control,
|
354
|
+
use factories instead. Use them to reduce the verbosity on creating new data (learn about
|
355
|
+
[Factory Bot](https://github.com/thoughtbot/factory_bot)).
|
356
|
+
|
357
|
+
**bad**
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
user = User.create(
|
361
|
+
name: 'Genoveffa',
|
362
|
+
surname: 'Piccolina',
|
363
|
+
city: 'Billyville',
|
364
|
+
birth: '17 August 1982',
|
365
|
+
active: true
|
366
|
+
)
|
367
|
+
```
|
368
|
+
|
369
|
+
**good**
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
user = FactoryBot.create :user
|
373
|
+
```
|
374
|
+
|
375
|
+
One important note. When talking about unit tests the best practice would be to use neither fixtures or factories. Put
|
376
|
+
as much of your domain logic in libraries that can be tested without needing complex, time consuming setup with either
|
377
|
+
factories or fixtures. Read more in
|
378
|
+
[this article](http://blog.steveklabnik.com/posts/2012-07-14-why-i-don-t-like-factory_girl).
|
379
|
+
|
380
|
+
### Easy to read matchers
|
381
|
+
|
382
|
+
Use readable matchers and double check the available
|
383
|
+
[rspec matchers](https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/).
|
384
|
+
|
385
|
+
**bad**
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
lambda { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
|
389
|
+
```
|
390
|
+
|
391
|
+
**good**
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
|
395
|
+
```
|
396
|
+
|
397
|
+
### Shared examples
|
398
|
+
|
399
|
+
Avoid shared examples.
|
400
|
+
|
401
|
+
### Test what you see
|
402
|
+
|
403
|
+
Deeply test your models and your application behaviour (integration tests). Do not add useless complexity testing
|
404
|
+
controllers.
|
405
|
+
|
406
|
+
When I first started testing my apps I was testing controllers, now I don't. Now I only create integration tests using
|
407
|
+
RSpec and Capybara. Why? Because I believe that you should test what you see and because testing controllers is an
|
408
|
+
extra step you wont usually need. You'll find out that most of your tests go into the models and that integration
|
409
|
+
tests can be easily grouped into shared examples, building a clear and readable test suite.
|
410
|
+
|
411
|
+
This is an open debate in the Ruby community and both sides have good arguments supporting their idea. People
|
412
|
+
supporting the need of testing controllers will tell you that your integration tests don't cover all use cases and that
|
413
|
+
they are slow. Both are wrong. You can easily cover all use cases (why shouldn't you?) and you can run single file specs
|
414
|
+
using automated tools like Guard. In this way you will run only the specs you need to test blazing fast without stopping
|
415
|
+
your flow.
|
416
|
+
|
417
|
+
### Don't use should
|
418
|
+
|
419
|
+
Do not use should when describing your tests. Use the third person in the present tense. Even better start using the
|
420
|
+
new [expectation](http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/) syntax.
|
421
|
+
|
422
|
+
**bad**
|
423
|
+
|
424
|
+
```ruby
|
425
|
+
it 'should not change timings' do
|
426
|
+
consumption.occur_at.should == valid.occur_at
|
427
|
+
end
|
428
|
+
```
|
429
|
+
|
430
|
+
**good**
|
431
|
+
|
432
|
+
```ruby
|
433
|
+
it 'does not change timings' do
|
434
|
+
expect(consumption.occur_at).to eq(valid.occur_at)
|
435
|
+
end
|
436
|
+
```
|
437
|
+
|
438
|
+
See [the should_not gem](https://github.com/should-not/should_not) for a way to enforce this in RSpec and
|
439
|
+
[the should_clean](https://github.com/siyelo/should_clean) gem for a way to clean up existing RSpec examples that begin
|
440
|
+
with 'should.'
|
441
|
+
|
442
|
+
### Stubbing HTTP requests
|
443
|
+
|
444
|
+
Sometimes you need to access external services. In these cases you can't rely on the real service but you should stub
|
445
|
+
it with solutions like webmock.
|
446
|
+
|
447
|
+
**good**
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
context 'with unauthorized access' do
|
451
|
+
let(:uri) { 'http://api.lelylan.com/types' }
|
452
|
+
|
453
|
+
before { stub_request(:get, uri).to_return(status: 401, body: fixture('401.json')) }
|
454
|
+
|
455
|
+
it 'gets a not authorized notification' do
|
456
|
+
page.driver.get uri
|
457
|
+
expect(page).to have_content 'Access denied'
|
458
|
+
end
|
459
|
+
end
|
460
|
+
```
|
461
|
+
|
462
|
+
Learn more about [webmock](https://github.com/bblimke/webmock) and [VCR](https://github.com/vcr/vcr). Here a
|
463
|
+
[nice presentation](http://marnen.github.io/webmock-presentation/webmock.html) explaining how to mix them together.
|
464
|
+
|
465
|
+
### Test Structure
|
466
|
+
|
467
|
+
Use the context-driven approach instead of inline conditions:
|
468
|
+
|
469
|
+
**Good:**
|
470
|
+
```ruby
|
471
|
+
describe '#method' do
|
472
|
+
context 'when a condition is met' do
|
473
|
+
it 'does something' do
|
474
|
+
# test implementation
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
context 'when another condition is met' do
|
479
|
+
it 'does something else' do
|
480
|
+
# test implementation
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
```
|
485
|
+
|
486
|
+
**Avoid:**
|
487
|
+
```ruby
|
488
|
+
describe '#method' do
|
489
|
+
it 'does something when condition is met' do
|
490
|
+
# test implementation
|
491
|
+
end
|
492
|
+
|
493
|
+
it 'does something else when another condition is met' do
|
494
|
+
# test implementation
|
495
|
+
end
|
496
|
+
end
|
497
|
+
```
|
498
|
+
|
499
|
+
### AAA Pattern (Arrange, Act, Assert)
|
500
|
+
|
501
|
+
Structure each test using the AAA pattern with RSpec's `describe`, `context`, `let`, and `before` hooks:
|
502
|
+
|
503
|
+
```ruby
|
504
|
+
describe Calculator do
|
505
|
+
# Arrange - Set up test data using let and/or before blocks
|
506
|
+
let(:calculator) { described_class.new }
|
507
|
+
|
508
|
+
describe '#add' do
|
509
|
+
context 'when given positive numbers' do
|
510
|
+
it 'returns the sum' do
|
511
|
+
# Act - Execute the method being tested
|
512
|
+
result = calculator.add(2, 3)
|
513
|
+
|
514
|
+
# Assert - Verify the expected outcome
|
515
|
+
expect(result).to eq(5)
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
context 'when calculator is in debug mode' do
|
520
|
+
before do
|
521
|
+
# Arrange - Additional setup using before hook
|
522
|
+
calculator.enable_debug_mode
|
523
|
+
allow(calculator).to receive(:log).and_return(true)
|
524
|
+
end
|
525
|
+
|
526
|
+
it 'logs the operation' do
|
527
|
+
# Act
|
528
|
+
calculator.add(2, 3)
|
529
|
+
|
530
|
+
# Assert
|
531
|
+
expect(calculator).to have_received(:log).with('Adding 2 + 3')
|
532
|
+
end
|
533
|
+
|
534
|
+
it 'returns the sum' do
|
535
|
+
# Act
|
536
|
+
result = calculator.add(2, 3)
|
537
|
+
|
538
|
+
# Assert
|
539
|
+
expect(result).to eq(5)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|
544
|
+
```
|
545
|
+
|
546
|
+
## Guidelines
|
547
|
+
|
548
|
+
1. **Use descriptive context names** that describe the condition being tested
|
549
|
+
3. **Use blank lines** to separate Arrange, Act, and Assert sections
|
550
|
+
4. **Use let blocks** for shared setup data
|
551
|
+
5. **Use before blocks** for imperative setup (method calls, mocks, state changes)
|
552
|
+
6. **Use described_class** instead of the class name directly
|
553
|
+
7. **Describe one method per describe block** using Ruby documentation conventions (`#method` for instance methods, `.method` for class methods)
|
554
|
+
8. **Follow the existing codebase patterns** for consistency
|
555
|
+
|
556
|
+
## Running Tests
|
557
|
+
|
558
|
+
- Run all tests: `bundle exec rspec`
|
559
|
+
- Run specific file: `bundle exec rspec spec/path/to/file_spec.rb`
|
560
|
+
- Run with coverage: `COVERAGE=true bundle exec rspec`
|
561
|
+
|
data/.claude/settings.local.json
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -5,7 +5,10 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
-
## [
|
8
|
+
## [0.4.0] - 2025-07-18
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- JSONRPC::BatchRequest#process_each method for simplified batch processing
|
9
12
|
|
10
13
|
## [0.3.0] - 2025-07-17
|
11
14
|
|
@@ -68,6 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
68
71
|
- Helper methods for request and response processing
|
69
72
|
- Examples for basic and advanced usage scenarios
|
70
73
|
|
71
|
-
[
|
74
|
+
[0.4.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.3.0...v0.4.0
|
75
|
+
[0.3.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.2.0...v0.3.0
|
72
76
|
[0.2.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.1.0...v0.2.0
|
73
77
|
[0.1.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/745b5a...v0.1.0
|
data/README.md
CHANGED
@@ -1,12 +1,23 @@
|
|
1
|
-
|
1
|
+
<p align="center">
|
2
|
+
<a href="https://jsonrpc-middleware.com" target="_blank">
|
3
|
+
<picture>
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="./.github/images/logo-dark.svg">
|
5
|
+
<source media="(prefers-color-scheme: light)" srcset="./.github/images/logo-light.svg">
|
6
|
+
<img alt="JSON-RPC Middleware Logo" src="./.github/images/logo-light.svg" width="600" height="120" style="max-width: 100%;">
|
7
|
+
</picture>
|
8
|
+
</a>
|
9
|
+
</p>
|
10
|
+
|
11
|
+
<div align="center">
|
2
12
|
|
3
13
|
[](https://badge.fury.io/rb/jsonrpc-middleware)
|
4
14
|

|
5
15
|
[](https://qlty.sh/gh/wilsonsilva/projects/jsonrpc-middleware)
|
6
16
|
[](https://qlty.sh/gh/wilsonsilva/projects/jsonrpc-middleware)
|
7
17
|
|
8
|
-
|
9
|
-
|
18
|
+
</div>
|
19
|
+
|
20
|
+
A Rack middleware implementing the JSON-RPC 2.0 protocol that integrates easily with all Rack-based applications (Rails, Sinatra, Hanami, etc).
|
10
21
|
|
11
22
|
## Table of contents
|
12
23
|
|
@@ -30,6 +41,24 @@ calls encoded in JSON.
|
|
30
41
|
- **Request validation**: Define request parameter specifications and validations
|
31
42
|
- **Helpers**: Convenient helper methods to simplify request and response processing
|
32
43
|
|
44
|
+
## 🏗️ Architecture
|
45
|
+
|
46
|
+
The gem integrates seamlessly into your Rack-based application:
|
47
|
+
|
48
|
+
```mermaid
|
49
|
+
block-beta
|
50
|
+
columns 4
|
51
|
+
|
52
|
+
App["Your app"]:4
|
53
|
+
Rails:1 Sinatra:1 RackApp["Other Rack-compatible framework"]:2
|
54
|
+
Middleware["JSON-RPC Middleware"]:4
|
55
|
+
Rack["Rack"]:4
|
56
|
+
HTTP["HTTP"]:4
|
57
|
+
|
58
|
+
classDef middlewareStyle fill:#ff6b6b,stroke:#d63031,stroke-width:2px,color:#fff
|
59
|
+
class Middleware middlewareStyle
|
60
|
+
```
|
61
|
+
|
33
62
|
## 📦 Installation
|
34
63
|
|
35
64
|
Install the gem and add to the application's Gemfile by executing:
|
@@ -78,31 +107,26 @@ class App
|
|
78
107
|
@env = env
|
79
108
|
|
80
109
|
if jsonrpc_request?
|
81
|
-
|
82
|
-
jsonrpc_response(
|
110
|
+
sum = add(jsonrpc_request.params)
|
111
|
+
jsonrpc_response(sum)
|
83
112
|
elsif jsonrpc_notification?
|
84
|
-
|
113
|
+
add(jsonrpc_notification.params)
|
85
114
|
jsonrpc_notification_response
|
86
115
|
else
|
87
|
-
|
88
|
-
jsonrpc_batch_response(
|
116
|
+
results = add_in_batches(jsonrpc_batch)
|
117
|
+
jsonrpc_batch_response(results)
|
89
118
|
end
|
90
119
|
end
|
91
120
|
|
92
121
|
private
|
93
122
|
|
94
|
-
def
|
95
|
-
params = request_or_notification.params
|
96
|
-
|
123
|
+
def add(params)
|
97
124
|
addends = params.is_a?(Array) ? params : params['addends'] # Handle positional and named arguments
|
98
125
|
addends.sum
|
99
126
|
end
|
100
127
|
|
101
|
-
def
|
102
|
-
batch.
|
103
|
-
result = handle_single(request_or_notification)
|
104
|
-
JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
|
105
|
-
end.compact
|
128
|
+
def add_in_batches(batch)
|
129
|
+
batch.process_each { |request_or_notification| add(request_or_notification.params) }
|
106
130
|
end
|
107
131
|
end
|
108
132
|
|
@@ -110,9 +134,14 @@ use JSONRPC::Middleware
|
|
110
134
|
run App.new
|
111
135
|
```
|
112
136
|
|
113
|
-
This will give you a fully-featured JSON-RPC server
|
137
|
+
This will give you a fully-featured JSON-RPC server, capable of:
|
138
|
+
- Handling JSON-RPC requests, notifications __and batches__
|
139
|
+
- Validating the allowed JSON-RPC methods (e.g. allow only `add`)
|
140
|
+
- Validating the JSON-RPC method parameters (e.g. allow only non-empty arrays of numbers)
|
141
|
+
- Accept positional and named parameters (`params: [5, 5]`, `params: { addends: [5, 5] }`)
|
142
|
+
- Respond successfully or erroneously, according to the specification
|
114
143
|
|
115
|
-
For more advanced setups, check the [examples](https://github.com/wilsonsilva/jsonrpc-middleware/blob/main/examples/README.md).
|
144
|
+
For more advanced setups, or other frameworks such as Rails or Sinatra, check the [examples](https://github.com/wilsonsilva/jsonrpc-middleware/blob/main/examples/README.md).
|
116
145
|
|
117
146
|
## 📚 Documentation
|
118
147
|
|
data/Rakefile
CHANGED
@@ -14,9 +14,7 @@ yardstick_options = YAML.load_file('.yardstick.yml')
|
|
14
14
|
|
15
15
|
Bundler::Audit::Task.new
|
16
16
|
RSpec::Core::RakeTask.new(:spec)
|
17
|
-
RuboCop::RakeTask.new
|
18
|
-
task.requires << 'rubocop-yard'
|
19
|
-
end
|
17
|
+
RuboCop::RakeTask.new
|
20
18
|
YARD::Rake::YardocTask.new
|
21
19
|
YardJunk::Rake.define_task
|
22
20
|
Yardstick::Rake::Measurement.new(:yardstick_measure, yardstick_options)
|
data/examples/rack/Gemfile.lock
CHANGED
data/examples/rack/app.rb
CHANGED
@@ -40,9 +40,6 @@ class App
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def handle_batch(batch)
|
43
|
-
batch.
|
44
|
-
result = handle_single(request_or_notification)
|
45
|
-
JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
|
46
|
-
end.compact
|
43
|
+
batch.process_each { |request_or_notification| handle_single(request_or_notification) }
|
47
44
|
end
|
48
45
|
end
|
data/examples/rack-echo/app.rb
CHANGED
@@ -27,10 +27,7 @@ class App
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def handle_batch(batch)
|
30
|
-
batch.
|
31
|
-
result = handle_single(request_or_notification)
|
32
|
-
JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
|
33
|
-
end.compact
|
30
|
+
batch.process_each { |request_or_notification| handle_single(request_or_notification) }
|
34
31
|
end
|
35
32
|
end
|
36
33
|
|
@@ -43,10 +43,7 @@ class App
|
|
43
43
|
def handle_single(request_or_notification) = request_or_notification.params
|
44
44
|
|
45
45
|
def handle_batch(batch)
|
46
|
-
batch.
|
47
|
-
result = handle_single(request_or_notification)
|
48
|
-
JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
|
49
|
-
end.compact
|
46
|
+
batch.process_each { |request_or_notification| handle_single(request_or_notification) }
|
50
47
|
end
|
51
48
|
end
|
52
49
|
|
@@ -36,9 +36,6 @@ class JsonrpcController < ApplicationController
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def handle_batch(batch)
|
39
|
-
batch.
|
40
|
-
result = handle_single(request_or_notification)
|
41
|
-
JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
|
42
|
-
end.compact
|
39
|
+
batch.process_each { |request_or_notification| handle_single(request_or_notification) }
|
43
40
|
end
|
44
41
|
end
|
@@ -59,10 +59,7 @@ class JsonrpcController < ActionController::Base
|
|
59
59
|
def handle_single(request_or_notification) = request_or_notification.params
|
60
60
|
|
61
61
|
def handle_batch(batch)
|
62
|
-
batch.
|
63
|
-
result = handle_single(request_or_notification)
|
64
|
-
JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
|
65
|
-
end.compact
|
62
|
+
batch.process_each { |request_or_notification| handle_single(request_or_notification) }
|
66
63
|
end
|
67
64
|
end
|
68
65
|
|
@@ -45,8 +45,5 @@ def handle_single(request_or_notification)
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def handle_batch(batch)
|
48
|
-
batch.
|
49
|
-
result = handle_single(request_or_notification)
|
50
|
-
JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
|
51
|
-
end.compact
|
48
|
+
batch.process_each { |request_or_notification| handle_single(request_or_notification) }
|
52
49
|
end
|
@@ -49,9 +49,6 @@ class App < Sinatra::Base
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def handle_batch(batch)
|
52
|
-
batch.
|
53
|
-
result = handle_single(request_or_notification)
|
54
|
-
JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
|
55
|
-
end.compact
|
52
|
+
batch.process_each { |request_or_notification| handle_single(request_or_notification) }
|
56
53
|
end
|
57
54
|
end
|
@@ -137,6 +137,38 @@ module JSONRPC
|
|
137
137
|
requests.empty?
|
138
138
|
end
|
139
139
|
|
140
|
+
# Handles each request/notification in the batch and returns responses
|
141
|
+
#
|
142
|
+
# @api public
|
143
|
+
#
|
144
|
+
# @example Handle batch with a block
|
145
|
+
# batch.process_each do |request_or_notification|
|
146
|
+
# # Process the request/notification
|
147
|
+
# result = some_processing(request_or_notification.params)
|
148
|
+
# result
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# @yield [request_or_notification] Yields each request/notification in the batch
|
152
|
+
#
|
153
|
+
# @yieldparam request_or_notification [JSONRPC::Request, JSONRPC::Notification] a request or notification
|
154
|
+
# in the batch
|
155
|
+
#
|
156
|
+
# @yieldreturn [Object] the result of processing the request. Notifications yield no results.
|
157
|
+
#
|
158
|
+
# @return [Array<JSONRPC::Response>] responses for requests only (notifications return no response)
|
159
|
+
#
|
160
|
+
def process_each
|
161
|
+
raise ArgumentError, 'Block required' unless block_given?
|
162
|
+
|
163
|
+
flat_map do |request_or_notification|
|
164
|
+
result = yield(request_or_notification)
|
165
|
+
|
166
|
+
if request_or_notification.is_a?(JSONRPC::Request)
|
167
|
+
JSONRPC::Response.new(id: request_or_notification.id, result:)
|
168
|
+
end
|
169
|
+
end.compact
|
170
|
+
end
|
171
|
+
|
140
172
|
private
|
141
173
|
|
142
174
|
# Validates the requests array
|
data/lib/jsonrpc/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonrpc-middleware
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wilson Silva
|
@@ -37,8 +37,8 @@ dependencies:
|
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '2.7'
|
40
|
-
description:
|
41
|
-
|
40
|
+
description: A Rack middleware implementing the JSON-RPC 2.0 protocol that integrates
|
41
|
+
easily with all Rack-based applications (Rails, Sinatra, Hanami, etc).
|
42
42
|
email:
|
43
43
|
- wilson.dsigns@gmail.com
|
44
44
|
executables: []
|
@@ -47,6 +47,7 @@ extra_rdoc_files: []
|
|
47
47
|
files:
|
48
48
|
- ".aiexclude"
|
49
49
|
- ".claude/commands/document.md"
|
50
|
+
- ".claude/commands/test.md"
|
50
51
|
- ".claude/docs/yard.md"
|
51
52
|
- ".claude/settings.local.json"
|
52
53
|
- ".editorconfig"
|
@@ -167,5 +168,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
167
168
|
requirements: []
|
168
169
|
rubygems_version: 3.7.0
|
169
170
|
specification_version: 4
|
170
|
-
summary:
|
171
|
+
summary: Rack middleware implementing the JSON-RPC 2.0 protocol.
|
171
172
|
test_files: []
|